Apparently CPUs seem to know if a number is negative or positive, since they usually have a Sign flag (S flag in Z80 parlance).
However, you might be surprised to know that CPUs don't have a clue about what a negative number is.
Besides the conventioned Sign and Overflow flag, the CPU is clueless for what is a negative/positive number.
By convention, and due to some peculiarities how binary logic was setup to work with numbers, it's most useful to define the most significant bit as the Sign bit. So when it is set (1) it's because it's a negative number. when it is reset (0) it's a positive number. In fact, the S flag is an exact copy of bit 7, it will become obvious why, further ahead.
There is a very peculiar mechanism, called "Two's Complement", that does all the magic for signed integer numbers.
By convention/definition the "Two's Complement" of a number is used to inverse the Sign of a number (Z80 Assembly "NEG" instruction).
For a number N it basically consists on these two steps:
- negate or flip every bit in the number
- add one (1) to the previous result.
The only exception is zero, which has no negative counter part.
But if you apply the above rules, to zero, it still provides the correct result (zero).
Minus zero, is nonsense for computers and also in general, but don't say that to a mathematician, or you will get a deep lesson on limits or similar stuff Quick example
So if we have an 8 bit number with decimal value 1, we get (00000001) binary.
If we apply the above rules, we should get the representation of -1, which is:
1 - Inverse or negate number
!(00000001) = (11111110)
2 - Add one
(11111110)+(00000001) = (11111111)
(11111111) is a nice round number, it could be -1 or 255.
Actually it can be both, it depends on how you look at it, with what conventions in mind.
Two's complement (that extra add), is the reason why negative numbers, always seem to have one more combination than positives (127 versus -128), but in fact the number of positives and negatives is the same, since ZERO is by convention positive.
So in practice the trick consists in setting things up so that when bit data overflow or underflow it wraps to the correct side, in the established convention.
Think of it like this:
if you had a ruler that goes from 0 to 255, you could erase those numbers and rewrite them so that it starts at -128 and ends at +127
This way, you are sure to have a sequence of numbers that is incrementally sound, which allows us to transition from negative to positive values passing throw zero, which is what any civilized human would expect.
And why did the first computer techs decided to implement negative numbers like this ?
Because it makes supporting negative numbers as simple as doing "almost" nothing.
It allows for a lot of math rules and conventions to keep working, with the already existing computer logic for increment, decrement, add, subtract, etc...
Why, because in fact, you are doing the operation conceptually with signed integer numbers, but the computer is just working with unsigned integers, exactly as before.
It basically consists in offsetting the values, using conventions only, hence interpreting the allowed range (00000000 to 11111111) in a different way.
In practice, it was a very smart way to interpret the bits of a number, so that they wrap in the right place, allowing all existing operations to be used without change.
This was an exceptional idea to minimize complexity in the first days of computing, that prevailed to this day.Take note of these:
- When you increment a negative number, it should get closer to zero
Dec (-2) = Bin(11111110)
After a regular bit increment it gets to Bin(11111111) which by convention is DEC -1, which is closer to zero.
- When we decrement a negative number, it should get further away from zero.
which also checks up, when compared with the previous numbers (-1 decremented gives -2)
- Negating a number twice, should give the same number, so (-(-(N)) = N
Which works as expected:
(continuing the 1 to -1 example above, and now performing -1 to 1 )
1 - Inverse or negate number
!(11111111) = (00000000)
2 - Add one
(00000000)+(00000001) = (00000001)
The only critical spot, is when we cross over from conventional max positive 127 = (01111111) to max negative -128 = (10000000), but this already happens with conventional unsigned numbers, but only in the transition from (11111111) to (00000000) and vice-versa.
After this, it should be easy to understand that when CPU is adding 2 + (-4) = -2, it's just a simple bit add (00000010 + 11111100 = 11111110).
How the result is interpreted is what changes the meaning of the result.Why is important to understand this ?
Because knowing it, allows us to take advantage of it, by devising interesting hacks or tricks.
A very simple example, is how to quickly extend an 8 bit signed number to a 16 bit signed number.
I'll leave it as a simple exercise.
Another example, is to determine if an unsigned 8 bit number is >= 128 ?
Just use the sign bit.
Same trick can be done for >= 32768 for a 16 bit unsigned number.
If you need to make something like y = -x -1
You might think you need two operations, swap signal on X, and ADD -1 or subtract 1.
But in fact, you can use just a single Z80 instruction, which is CPL (1's Complement)
Why ?, Because NEG instruction does bit flip (same as CPL) but then adds 1. but then we need to decrement 1 (which cancels the add 1 from NEG), hence we can just use CPL.
It's also good computer geek knowledge.