x86-64 assembly from the ground up

Arithmetic in registers

Adding, subtracting, multiplying and dividing in registers, and the one instruction that surprises everybody: division reads two registers and writes two back.

Part 4 of 12

Beginner16 min readBack to Courses

add and sub

add and sub take the same shapes as mov: a destination and one source, either a register or an immediate. add eax, 5 adds five to whatever is in eax and stores the answer back in the same place, so the destination is both an input and the output.

The width you name decides how wide the sum is. A thirty-two-bit add wraps around at about four billion, and a sixty-four-bit add at a number too large to be interesting. There is no error and no exception when that happens — the answer simply comes back smaller than you expect, which is why an overflow is a bug you find by testing rather than by being told.

Multiply and the two-register division

Multiplication is unremarkable: imul eax, 6 multiplies eax by six and keeps the low half of the product. Division is the odd one. There is no form that takes a dividend and a divisor and leaves a quotient, because one instruction has to produce two answers — the quotient and the remainder.

So division reads a pair of registers as one value and writes the answers back into that same pair. The dividend is edx joined to eax, the divisor is the operand, and afterwards eax holds the quotient while edx holds the remainder. cdq is the instruction that prepares the high half: it copies the sign of eax through the whole of edx, which turns eax into a signed value of twice the width.

Unsigned numbers use div and need a plain zero in edx instead, which xor edx, edx supplies. Both fail loudly on a divisor of zero: the processor raises a fault and the kernel kills the program, with no message from your code at all.

The four operations

add eax, 7        ; eax is now eax plus seven
sub eax, 3        ; eax is now eax minus three
imul eax, 6       ; the low half of the product
neg eax           ; flip the sign

mov eax, 84
cdq               ; sign-extend eax into edx
mov ecx, 5
idiv ecx          ; quotient in eax, remainder in edx

mov eax, 84
xor edx, edx      ; unsigned division starts from zero
div ecx           ; same answers, no signs

One expression, four operations

The program computes three plus four, doubles it, takes away six and halves the result. Run it, then change the numbers and predict the single digit before you run it again.

Output
Click "Run" to execute your code

Forget cdq and the division quietly uses whatever was left in edx as the top half of the dividend, so the answer is wrong rather than late. Forget xor edx, edx before div and the same thing happens with no sign to blame it on.

The same bits, two readings

A register does not store a sign. The number minus one and the number four billion are the same thirty-two bits, and the instruction decides which reading to use. add, sub and imul do not care; idiv treats the dividend as signed, while div treats it as unsigned.

That is why the setup before a division is different even when the numbers are positive. cdq spreads the sign of eax through edx, which is correct for a signed dividend. xor edx, edx puts a plain zero there, which is correct for an unsigned one. Use the wrong one and the processor divides a number you never intended.

Overflow is the same story from the other side. A thirty-two-bit sum that passes the top wraps around to a small or negative value with no warning. Assembly will not check a range for you; the test has to be part of the program.

Two operands, one destination

Arithmetic follows the shape of mov: the destination is also the first input. add eax, 7 means read eax, add seven, write the answer back to eax. The source can be an immediate, another register or a memory operand, but the destination is a register in these forms.

imul has a one-operand form that produces a double-width product in edx:eax, but the two-operand form used here keeps the low half and discards the rest. That is enough for values that fit, and it is the form a compiler reaches for when it knows the range.

neg flips a sign by subtracting the value from zero. It is not a separate arithmetic path; it is the same wrap-around rule applied to a single operand.

Three arithmetic traps. A missing cdq or xor edx, edx gives a wrong quotient from a leftover high half. A zero divisor faults and kills the program with no message. And mixing div with negative numbers, or idiv with large unsigned ones, reads the bits as the wrong kind of number.

Try these next

  • Divide 100 by 7 and print both the quotient and the remainder as digits.
  • Change the dividend to minus 100 and use idiv; predict the sign of the remainder before running.
  • Replace idiv with div in a positive example and confirm the answers match.

Print a quotient and a remainder

The program divides thirty by four and writes both answers into a prepared line. Run it, then change the numbers to thirty-one and five and predict the two digits before running.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Where does the remainder of an idiv end up?