x86-64 assembly from the ground up

Writing and exiting

The two kernel calls every program here needs: write sends bytes out, and exit hands a status back. Also why a program that never exits does not simply stop.

Part 3 of 12

Beginner14 min readBack to Courses

The write call

syscall is the instruction that hands control to the kernel. Which call you want is a number in rax: one is write, sixty is exit. The arguments go in fixed registers, always in the same order, so rdi carries the descriptor, rsi the address of the bytes, and rdx the count.

Descriptor one is the standard output. The call returns a number in rax — how many bytes were actually written — so the register you used to ask is overwritten with the answer. A short count is possible in general; for a small message to a terminal it will be the whole thing.

There is no formatting here and no buffering. You give the kernel an address and a length, and those exact bytes appear. A new line is not added for you, which is why every message in this course ends with a byte holding ten.

The descriptor is a small number that names an open file, not the file itself. One is standard output and two is standard error, which is why the same bytes sent to two instead of one appear in the error strip rather than the output pane. A program that reports a failure should use two, so the message survives even when the output is redirected.

Why a program has to exit

The entry label is not a function. Nobody called it, so there is no return address waiting on the stack for a ret, and the processor will simply carry on into whatever bytes follow your last instruction. In practice that is a crash, and the exit status says the program was killed.

So the last thing every program does is ask for the exit call, number sixty, with the status in rdi. Zero means success and anything else is a failure code the shell or the page can read. xor rdi, rdi is the shortest way to write that zero, because a value exclusive-ored with itself is always nothing.

The two calls side by side

mov rax, 1         ; write
mov rdi, 1         ; to the standard output
mov rsi, msg       ; these bytes
mov rdx, len       ; this many of them
syscall

mov rax, 60        ; exit
xor rdi, rdi       ; with the status zero
syscall

Two lines from one write

Run it, then split the message into a second write of its own. Two `db` lines under one label is still one block of bytes, and one length still covers both.

Output
Click "Run" to execute your code

The kernel overwrites rcx and r11 on every call, whatever you had in them. A loop counter kept in rcx across a syscall comes back as something else entirely, which is the bug the loops chapter is careful to avoid.

A call across the privilege line

Your program runs in user mode, where it cannot touch the disk, the terminal or another process directly. syscall is the one instruction that asks the kernel to do those things. It saves a little state, switches to kernel mode, looks up the number in rax, and runs the matching routine.

The arguments travel in registers because there is no stack frame to share. This is the same idea as a function call, but the two sides agree on a fixed table instead of a symbol name. That is also why the numbers matter: one is write and sixty is exit on this system, and no header file is doing the translation.

When the kernel returns, execution continues at the instruction after syscall. Nothing else about your program changed, except the registers the call is allowed to touch.

The answer replaces the request

write returns the number of bytes it accepted, and that answer lands in rax. The request number one is gone. If you want to make a second call, you have to put a number back into rax; assuming it still holds one is a common way to make the second write do nothing.

A short count is legal. The kernel may write fewer bytes than you asked for, especially when the destination is a pipe rather than a terminal. A careful program checks the return value and sends the rest, but for a short line to the page the whole message goes in one go.

exit is the exception: it does not return at all. The status in rdi is handed to whoever started the program, and the process stops there. Any instruction written after it never runs.

Three ways a write goes wrong. A count that is too large sends bytes past the message and prints whatever follows in memory. A count that is too small quietly cuts the line short. And a missing exit call lets the processor fall into the bytes after your code, so the program dies instead of ending cleanly.

Try these next

  • Split the two-line message into two separate writes and give each one its own label and length.
  • Print a word with no newline, then a second write that sends only the newline byte.
  • Reduce a count by one and find which character disappears from the output.

Three writes, one line

Run it, then remove the trailing space from the first two messages. Each write sends exactly the bytes it is given, so the words join together unless a space is one of those bytes.

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

Which register tells the write call how many bytes to send?