0

I am currently trying to deepen my understanding of assembly code and I am stuck since weeks with a seemingly simple instruction :

sub al, BYTE PTR [ebp+4]

Assuming eax = 0x11223300 and BYTE PTR [ebp+4] = 0xaa what is the value of eax after the above instruction ?

From what I understand, al can only affect the last byte in eax (0x00 in this case) so the program tries to compute 0x00 - 0xaa. But the result being negative, I don't get if the result would simply be 0x00 or if numbers are automatically transformed to a negative number, in which case 0xaa can itself be considered as a negative value which would imply we are trying to compute 0x00 - (-0x2a) = 0x2a

I found in the SUB documentation that

The SUB instruction performs integer subtraction. It evaluates the result for both signed and unsigned integer operands and sets the OF and CF flags to indicate an overflow in the signed or unsigned result, respectively. The SF flag indicates the sign of the signed result.

But it only describe some behaviour of the flags and I can't figure out how to look for more about those in such a specific case.

Blackscholes
  • 45
  • 1
  • 8
  • 2
    `sub` doesn't saturate to 0, it wraps. Just like `uint8_t result = 0U - 0xaaU` in C. – Peter Cordes Jan 26 '20 at 19:40
  • 1
    `sub` does not know whether you are requesting signed or unsigned subtraction, as it is used for both. Since `sub` is used for both, it doesn't concern itself with negative numbers or answers: it simply computes a value and lets you decide whether you care about what kind of overflow might have happened. – Erik Eidt Jan 26 '20 at 19:40

2 Answers2

4

You can think of it in two different ways, and they both give the same result.

As unsigned, the result is computed mod 256. So 0x00 – 0xaa mod 0x100 = 0x56.

As signed, 0xaa represents –0x56. (Not –0x2a as in the question.) So, 0x00 – (–0x56) = 0x56.

prl
  • 11,716
  • 2
  • 13
  • 31
2

From what I understand, al can only affect the last byte in eax ...

This is correct.

But the result being negative ...

Now it becomes complicated.

Just like most CPUs, x86 does not distinguish between unsigned integers and "twos-complement" signed integers.

This means that 0xaa may mean 170 or (-86) and it's the task of the programmer or the compiler to interpret the value (if 0xaameans 170 or -86).

This is possible if only the low bits of a calculation are considered. Example:

0 + 170   = 0x000000AA
0 + (-86) = 0xFFFFFFAA

or:

0 - 170   = 0xFFFFFF56
0 - (-86) = 0x00000056

In both results above, the low 8 bits are equal.

So for an instruction that will only modify the low 8 bits, it does not care if 0xaa means 170 or (-86).

The x86's SUB instruction would write the low 8 bits of the result to the register and "throw away" the upper 24 bits.

The CPU sets the 4 bits in the "flags" register to pass additional information about these 24 bits to the program.

if the result would simply be 0x00

Some CPUs support instructions that operate like this: The result of a subtraction is 0 if the "correct" result would be negative.

However, such CPUs typically have two different SUB instructions: One that calculates like the SUB instruction of the x86 and another instruction that will saturate the result to 0.

Martin Rosenau
  • 17,897
  • 3
  • 19
  • 38
  • 1
    *The CPU sets the 4 bits in the "flags" register to pass additional information about these 24 bits to the program.* I don't like this way of describing it. It makes it sound like a wider computation really happened, or could be misleading. I'd prefer to say "about borrow output and signed overflow of that 8-bit subtract". Also, 2 of the FLAGS bits (SF and ZF) only depend on the low 8 result, not the carry/borrow output. And I guess you're ignoring PF and AF which is fine, but "the 4 bits in FLAGS" isn't great phrasing. – Peter Cordes Jan 26 '20 at 21:07
  • @PeterCordes *It makes it sound like a wider computation really happened.* Indeed, if we performed a "wider computation" manually with the pencil on paper, the result of subtracting (or adding) two n-bit numbers would be an (n+1)-bit number. On x86 all n+1 bits are calculated: n bits will be placed in the destination register and one bit will be placed in the "carry flag". So the CPU actually performs a "wider calculation". However, it is true that the CPU will not perform a 32-bit calculation when adding two 8-bit numbers. – Martin Rosenau Jan 27 '20 at 05:35
  • Ok yes, of course it computes the carry-out. All I meant was wider than *that*; as you say not 32-bit or 64-bit. And equally importantly, 2 of the major 4 flag bits (SF and ZF) don't come from outside the operand-size. – Peter Cordes Jan 27 '20 at 06:17