Skip to content

Step 1 of 5 · Reading · ~4 min

Learn

First Steps

Registers and Arithmetic

By the end of this lesson you can do real arithmetic on the CPU: turn digit characters into numbers, add them, split a result with a division, and turn the pieces back into characters your program can print.

Registers are the CPU's scratch space: 16 general-purpose slots, each 64 bits wide, built into the processor. Memory is far away in CPU terms; registers are right there, and nearly every instruction reads or writes one.

One register, several names

The 64-bit registers are rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp and r8 to r15. Each can be addressed as a narrower slice of itself:

  • rax (64 bits) -> eax (low 32) -> ax (low 16) -> al (low 8)
  • r8 (64 bits) -> r8d (low 32) -> r8w (low 16) -> r8b (low 8)

These are views of the same storage, and that is where the classic width trap lives:

  • Writing the 32-bit view zeroes the upper 32 bits. After mov eax, 5, all of rax is exactly 5.
  • Writing the 16-bit or 8-bit view leaves the upper bits alone. After mov al, 7, bits 8-63 of rax still hold whatever was there before.

So if you load one byte with mov al, [x] and then use all of rax as a number, leftover garbage comes along. To load a byte as a clean number, use movzx rax, byte [x]: "move with zero extension" copies the byte and fills the rest with zeros.

Arithmetic: destination first

NASM uses Intel syntax: the destination comes first and doubles as the left-hand input.

mov rax, 7      ; rax = 7
add rax, 3      ; rax = 10
sub rax, 4      ; rax = 6
imul rax, 5     ; rax = 30  (signed multiply)
inc rax         ; rax = 31
dec rax         ; rax = 30
xor rax, rax    ; rax = 0

xor rax, rax is the idiomatic zero: anything XOR itself is 0, it encodes shorter than mov rax, 0, and the CPU treats it as a fresh start.

Division has fixed registers

div takes one operand, the divisor, and the dividend is implied: the 128-bit number rdx:rax, with rdx as the high half. It leaves the quotient in rax and the remainder in rdx.

Splitting 135 seconds into minutes and seconds with a divisor of 60:

mov rax, 135
xor edx, edx    ; high half of the dividend must be 0
mov rbx, 60
div rbx         ; rax = 2 (minutes), rdx = 15 (seconds)

The trap is the xor edx, edx line. Leave stale bits in rdx and the CPU divides a gigantic number. If rdx is at least the divisor, the quotient cannot fit in rax and the process dies with a divide error (SIGFPE); if it is smaller, you silently get a wrong quotient and remainder. Both are easy to hit: after a read syscall, rdx still holds the count you passed in.

Characters are numbers wearing costumes

Assembly has no character type. A terminal receives bytes and draws them: byte 0x41 is drawn as A, byte 10 as a line break. NASM's 'A' is just a convenient way to write 0x41. The digits '0' to '9' are the consecutive codes 0x30 to 0x39, which gives you the whole conversion rule: a digit character minus the code of '0' is its value, and a value plus the code of '0' is its character.

Upper to lower case works the same way, since 'a' is 32 above 'A':

mov al, 'Q'
add al, 32      ; al = 'q'

Quotes matter. mov bl, 'Q' stores 0x51, while mov bl, 51 stores 0x33, which is the character 3. Leave the quotes off a digit and you store a control character: mov byte [x], 9 stores a TAB. The output looks almost right in a terminal and is wrong byte for byte.

Storing into memory

Square brackets mean "the memory at this address". When one operand is a register its width sets the size (mov [x], al stores one byte). When you store an immediate there is nothing to infer from, so you name the size: mov byte [x], 'Q'. Without byte, NASM stops with operation size not specified.

Common mistakes

  • Adding characters instead of values. '3' + '7' is 0x33 + 0x37 = 106, not 10. Convert first.
  • Skipping xor edx, edx before div. Wrong digits, or a crash with SIGFPE.
  • Forgetting to turn a number back into a character. Storing the value 1 prints an invisible control byte, not 1.
  • Mixing up quotient and remainder. After div, rax is the quotient and rdx the remainder.

Your exercise

The starter reads three bytes (two digits and a newline) and prints a two-byte result template followed by a newline. Your job is the arithmetic in between, and the tests include sums that need a carry into the tens place as well as ones that do not.

Up nextComparisons, Jumps, LoopsControl Flow and Memory

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…