Up to this point we have worked with unsigned integers, i.e. whole numbers which are zero or bigger than zero. Much like it happened with endianness, the interpretation of the value of several bytes as a negative number depends on the specific interpretation.
In computing there are two main ways to interpret the values of a group of bytes as a negative number: one’s complement and two’s complement.
At the moment GNU poke supports the two complement interpretation, which is really ubiquitous and is the negative encoding used by the vast majority of modern computers and operating systems.
We may consider adding support for one’s complement in the future, but only if there are real needs that would justify the effort (which wouldn’t be a small one ;)).
Unsigned values are never negative. For example:
(poke) 0UB - 1UB 0xffUB
Instead of getting a -1, we get the result of an unsigned underflow, which is the biggest possible value for an unsigned integer of size 8 bits: 0xff.
When using type specifiers like uint<8>
or uint<16>
in a
map, we get unsigned values such as 0UB. It follows that we need
other type specifiers to map signed values. These look like
int<8>
and int<16>
.
For example, let’s map a signed 16-bit value from foo.o:
(poke) .set obase 10 (poke) int<16> @ 0#B 28515H
Note how the suffix of the value is now H
and not UH
.
This means that the value is signed! But in this case it is still
positive, so let’s try to get an actual negative value:
(poke) var h = int<16> @ 0#B (poke) h - h - 1H -1H
Adding two signed integers gives you a signed integer:
(poke) 1 + 2 3
Likewise, adding two unsigned integers results in an unsigned integer:
(poke) 1U + 2U 3U
But, what happens if we mix signed and unsigned values in an expression? Is the result signed, or unsigned? Let’s find out:
(poke) 1U + 2 3U
Looks like combining an unsigned value with a signed value gives us an unsigned value. This actually applies to all the operators that work on integer values: multiplication, division, exponentiation, etc.
What actually happens is that the signed operand is converted to an unsigned value before executing the expression. You can also convert signed values into unsigned values (and vice-versa) using cast constructions:
(poke) 2 as uint<32> 2U
Therefore, the expression 1U + 2
is equivalent to 1U + 2
as uint<32>
:
(poke) 1U + 2 as uint<32> 3U
You may be wondering: why not doing it the other way around? Why not converting the unsigned operand into a signed value and then operate? The reason is that, given an integer of some particular size, the positive range that you can store in it is bigger when interpreted as an unsigned integer than when interpreted as a signed integer. Therefore, converting signed into unsigned before operating reduces the risk of positive overflow. This of course assumes that we, as users, will be working with positive numbers more often than with negative numbers, but that is a reasonable assumption to do, as it is often the case!