I am assembling using MASM 14.0, and i am confused with the output of the below code.
TITLE Exercise 4 from chapter 4
; Author : Saad Ahmed
INCLUDE Irvine32.inc
.code
main PROC
mov eax, 0
mov al, 255
add al, 1
call DumpRegs ; Display registers
mov al, 127
add al, 1
call DumpRegs ; Display registers
exit
main ENDP
END main
Both arithmetic operations are done on unsigned integers 255 and 127.
However, the CPU is treating the first operation 255, as an unsigned integers and setting the carry flag, which would happen when you add 1 to an unsigned 255.
The complete status flags are CF=1 SF=0 ZF=1 OF=0 AF=1 PF=1 with eax as 0
But, the second operation considers the 127 to be a signed integer as it is setting the overflow flag, which would happen if you add 1 to a +127.
The complete status flags are to CF=0 SF=1 ZF=0 OF=1 AF=1 PF=0 with eax as 0.
The question is how does the CPU decides that the first operation was done on an unsigned 255, while the other one was on a signed integer?
For addition (and subtraction) using twos complement there is no notion of signed or unsigned as far as the logic is concerned. Multiply and divide, yes, due to the sign extension required.
Take all combinations of 3 bit numbers from 000 to 111 and add them to all the combinations of 3 bit numbers, something manageable. Maybe write a program if you want or do it by hand. Or just do the corner cases. For each examine each operand as signed or unsigned using twos complement. You will note that the same addition works. 1 + 110 = 111. Now is that 1 + (-2) = -1 or was that 1 + 6 = 7. Both worked.
The carry flag IS the UNSIGNED overflow, the V flag is the SIGNED overflow, that is why we compute both so the user, who knows whether or not those are signed or unsigned numbers can choose the right conditional. And that is why you have signed jump if greater than or equal vs an unsigned jump if greater than or equal.
It is the beauty of twos complement that makes it all work.
Multiply (and divide) is different, because you have to sign extend. Multiply in binary has a beautiful feature, in that if you think about it
abcd
* 0011
=======
abcd
abcd
0000
0000
=======
A bit is either 1 or 0, so you are multiplying the top number by either one or zero, you either add it shifted or you dont. But also notice that very very quickly you are going to overflow. We know from grade school that n^x * n^y = n^(x+y). If your registers are Z bits wide the most significant bit positions of the operands cannot be larger than Z when added otherwise it overflows. Four bits 0010*0010 should work but 0010*1000 will overflow. The right way is the result is twice as wide as the operands.
So what if I want to multiply 1111 * 0010?
That is basically
0000
1111
0000
+0000
========
0011110
Wait was that a 15 (0b1111) or a -1 (0b1111)? -1 * 2 = -2 which is not what we got above, we did an unsigned multiply to do a signed multiply we have to sign extend and toss bits off the left
11..1111
*00..0010
=========
00000000
1111111
000000
00000
=========
11111110
and that gives the right answer for a signed multiply of two four bit registers 1111 and 0010.
Addition works because you only care about one column. Each column has a carry in, two operands a result and a carry out. And then you can cascade that as wide as you want. With a single bit you have 0 and 1. The zero is just zero not plus or minus zero, the 1 can either be a +1 or a -1. I find it easier to work through the combinations with more than one column, but it could be done. For addition the carry in is a 0 so I dont need to represent it operand a, operand b, carry out, and result
00 00 0 + 0 = 0
01 01 0 + 1 = 1; 0 + (-1) = -1
10 01 1 + 0 = 1; (-1) + 0 = -1
11 10 1 + 1 = 0 unsigned overflow. -1 + 1 = 0, 1 + -1 = 0, -1 + -1 = 0 signed overflow
Technically all the signed ones were a signed overflow in that last case, and that is the special case you deal with for any number of bits, take a three bit register 100 + 100 = 000 + carry out of 1 is that a 4 + 4 = 0 unsigned overflow or is that a -4 + -4 = 0 with a signed overflow? Which is why it is easier to see when you use a few bits and go through the combinations tossing the problem case of 1 and then all zeros.
A signed overflow is when the carry in of the most significant column does not match the carry out. an unsigned overflow is when the carry out of the msbit is a one.
Subtraction in logic is done with addition, we know from grade school that a - b = a + (-b) and we know from programming classes that to take the negative using twos complement you invert and add one. Well that works great we can just invert the second operand and invert the carry in of the lsbit making it a one or invert and add one. And that is how that works. The carry out is sometimes inverted coming out of the alu to indicate a borrow. You have to look at the flag combinations to figure this out, some processors invert the carry out some dont. Can sometimes tell from the subtract with borrow if your ISA has that instruction.
I know TL:DR...covered more than you asked.
What's nice about two's complement is that the CPU doesn't need to know.
It just does addition on the unsigned bytes.
The carry flag is set because 255 + 1 doesn't fit in 8 bits (obviously).
The overflow flag is set because the MSB of both operands is 0, and the MSB of the ouput is 1 - indicating that if you consider this to be signed arithmetic, you have overflowed. If you consider it unsigned arithmetic, then you should just ignore the flag. See this wikipedia article.