..

Hex Calculators on Linux

I relatively often need to deal with bitmask constants written as hex numbers or bit shifts (1<<11). At the same time, I’m not an old-school assembler programmer who can decode and do math on them purely in mind.

Honestly I don’t even remember the whole series of powers of 2 … well, may be except that 2^8 = 256 and 2^32 is 4 billion and something. Not to say that I don’t remember by heart how the binary representation of 0x7F or 0x800 looks like, or what (1<<11) is equal.

One way to figure such things is to write an ad-hoc program that does the math and prints the result, but of course, it’s not very convenient (although sometimes inevitable). For that reason, I was looking for a simple hex calculator for Linux, and here is what I found.

Shell Built-Ins

Bash double parenthesis syntax supports bitwise arithmetics and numeric literals in different bases and printf shell built-in supports output in hex form.

For example, let’s convert binary number 1010101011 to the hex representation:

$ printf "%X\n" "$((2#1010101011))"
2AB

Bitwise arithmetics:

printf "%X\n" "$((1|2|4|8|0x10|0x20))"
3F

Bit shifts:

printf "%X\n" "$((1<<11))"
800

If you need result in hex, shell built-ins may be everything you need 🙏

This syntax works at least in bash and zsh.

Unfortunately I don’t know the way to print the output in the binary form using only shell built-ins, so if you need output in binary, you might need to stick to one of the solutions below.

dc

dc is an arbitrary precision calculator that works with the input in the reverse polish notation. It supports different bases for the output and input and thus can be used as hex calculator.

Usually I use it together with shell built-ins (see above) to calculate what I need and then use dc only to print the output in the base I need.

Let’s print one of the previous examples in the binary form:

dc -e "2o $((1|2|4|8|0x10|0x20)) p"
111111

2o here means output in the base of 2. Alternatively, we could set input in the base 2, or use different base than 2. For example:

dc -e "16o 2i 100000 p"
20

Obviously, this syntax can be used to convert between hex and decimal, decimal and octal and so on.

bc

bc works in a similar way to dc, but uses different syntax:

echo "obase=16; ibase=2; 1010101011" | bc
2AB

For my taste “obase” and “ibase” are too many keystrokes, so I usually use dc.

Qalculate!

Qalculate (qalc) is an advanced math calculator that (amongst other things) supports hex and binary conversions. Qalculate! has several different UIs, but I prefer CLI. The examples below are done in qalc REPL that you get by invoking it without arguments (> is REPL prompt).

>0xFF to bin

255 = 0000 0000 1111 1111
>1<<11 to hex

shift(1, 11) = 0x800
>1<<11|0x400 to hex

shift(1, 11) | 1024 = 0xC00

You may ask, why we need such a heavy artillery as qalc if we can use shell built-ins or dc?

One of the reasons, that I haven’t touched yet is that sometimes you might be interested to see a binary representation of a floating-point1 number.

>infinity to float

+∞ = 0111 1111 1000 0000 0000 0000 0000 0000
>-infinity to float

−(+∞) = 1111 1111 1000 0000 0000 0000 0000 0000

Alternative way is to use floatBits:

>floatBits(3.14)

floatBits(3.14) = 1078523331

>1078523331 to bin

>1078523331 = 0100 0000 0100 1000 1111 0101 1100 0011

Floats can be reconstructed back from the binary form using float function (it assumes that the input argument is in the binary if only 1s and 0s are used):

>float(0100 0000 0100 1000 1111 0101 1100 0011)

float(01000000010010001111010111000011) ≈ 3.140000105

There are more useful functions to work with floats, e.g. floatError that shows the difference between IEEE 754 representation and the actual number:

>floatError(3.14)

floatError(3.14) ≈ 0.0000001049041748

But the floats are tricky and sometimes even Qalculate can’t help. One of the limitations that I stumbled on is that you can’t ask it “What is NaN in the floating-point representation”? There are simply no way to give Qalculate NaN as a constant and if NaN appears as a result of calculation Qalculate stops and gives error.

On top of that, NaNs can be represented by different bit patterns and C++ make use of it to have two different NaNs (quiet and signalling). In case when you need to make a look at their binary representations, it’s time to actually use your compiler.

Type Punning

I mostly program in C and C++, so here is the C code that prints the binary representation of a floating-point number:

#include <stdio.h>

void printBin(int num) {
    size_t bits = sizeof(num) * 8;
    for (int i = bits-1; i >= 0; i--) {
        int bit = (num>>i) & 0x1;
        if (bit)
            putchar('1');
        else
            putchar('0');
    }
    putchar('\n');
}

int main() {
    union f2b {
        int s;
        float f;
    } u;
    
    u.f = 3.14;

    printBin(u.s);
    return 0;
}

Output:

01000000010010001111010111000011

In case of C++ NaNs, we can put this code to a .cpp file and add:

#include <cmath>
...

u.f = std::numeric_limits<float>::quiet_NaN();  
printBin(u.s);  

u.f = std::numeric_limits<float>::signaling_NaN();  
printBin(u.s);

Output:

01111111110000000000000000000000
01111111101000000000000000000000

There are more reasons than floating-point to use type punning instead of one of the calculators above. One is implicit assumptions about type sizes that might differ from the calculator you use and the code in your program.

For example, in the case of shell built-ins (at least on my shell):

printf "%X\n" $((-1))
FFFFFFFFFFFFFFFF

It assumes a 64-bit integer, which might not be what your compiler assumes.

Btw, speaking about compiler assumptions I recently stumbled to a bug that was caused by the fact that SHRT_MIN and (short)SHRT_MIN was represented by two different bit patterns, because SHRT_MIN is a preprocessor macro that expands to a numeric literal and numeric literals are integers, not shorts 🤷

There are many similar pitfalls, and normally I use calculators only if I’m checking something simple, where I’m confident that no edge case magic will happen.

There is also a curious world of alternative floating-point formats (bfloat16, TensorFlow-32) and integers. There are some online calculators for them, but I’d also stick to a good old type punning.


  1. Here and further I talk only about IEEE 754 floating-point numbers. ↩︎