x86-64 assembly from the ground up
Your first program
What the processor is doing while your code runs, what a register is, and your first program: four register values and two calls into the kernel.
The loop under every language
A processor is a loop. It reads the next instruction from memory, does the small thing that instruction names, and moves to the one after it. At that level there are no variables, no types and no function names: there are numbers in storage, one register that says where the next instruction is, and a few hundred kinds of operation.
Assembly is that loop written down. One line of assembly is one instruction, so the file you write is very close to what the machine runs. That is why assembly is the best place to see what every other language has been doing on your behalf.
A register is a slot in the processor
The processor keeps a few working values in registers: small storage slots inside the chip, each holding sixty-four bits. The names are short and fixed — rax, rbx, rcx, rdi are four of the sixteen. Reading or writing one costs nothing extra, so arithmetic happens in registers rather than in memory.
mov is how a value gets into one, and the destination is written first: mov rax, 1 puts the number one into rax. A number written in the line like that is called an immediate, because it arrives with the instruction rather than from memory.
The shape of a source file
A NASM file is split into sections. Bytes the program reads live in section .data, and instructions live in section .text, so a reader can see what is stored and what is done without hunting through the file.
The line global _start publishes the entry label, and _start is the address the kernel jumps to when the program begins. Leave that line out and the linker has nothing to point the entry at, so the program never links.
A comment begins with a semicolon and runs to the end of the line, so the four mov lines can explain themselves without changing a byte of the program. NASM ignores the text; a reader does not.
Four registers, one request
section .text
_start:
mov rax, 1 ; the number of the write call
mov rdi, 1 ; descriptor 1 is the standard output
mov rsi, msg ; where the bytes start
mov rdx, len ; how many bytes to send
syscall ; the kernel does the restPrint a line from a data section
Run the program, then edit the text inside the quotes and run it again. Notice that the four `mov` lines are the whole request and nothing about them changes.
Run it, then change the text between the quotes in msg and run it again. The length is measured from the label by len, so a longer message still prints in full without you counting anything.
What the four registers are really saying
The kernel has no idea what your program means. It reads a number from rax, checks that the number names a call it knows, and then reads that call's arguments from fixed registers. Write expects the descriptor in rdi, the address in rsi and the count in rdx, always in that order.
syscall is the doorway. The processor switches to a more privileged mode, the kernel does the work, and control comes back on the next line with the answer in rax. Nothing was called in the ordinary sense: there is no return address on the stack and no function body in your file.
A message is bytes, not characters
The data section stores bytes. The letters of hello are five bytes, the space is a sixth, and the ten at the end is the byte a terminal displays as a new line. The assembler does not store a length with the text and it does not add a terminator, so the program has to say how many bytes to send.
len equ $ - msg works that out before the program exists. $ is the address of the line after the message, msg is where it starts, and the difference is the byte count. Edit the text and the subtraction changes with it, which is why the length never has to be counted by hand.
The terminal does not know what the bytes mean either. It receives ten, forty-eight, sixty-five and so on, and draws the glyph each number names in its current encoding. Send a byte with no glyph — two hundred and fifty-five, say — and the pane may show a replacement character or nothing at all. The message is only text because the reader on the other end agrees to read it that way.
Three mistakes stop a first program. Leave out global _start and the linker has no entry point. Put the message in .text instead of .data and the bytes sit among instructions. Forget the exit call and the processor runs on into whatever follows the last instruction, which ends as a crash rather than a clean stop.
Try these next
- Print a second message by copying the five
movlines and changing the label and the length. - Remove the ten from the end of
msgand run it; the next output would continue on the same line. - Change the exit status from zero to three and explain why the page shows the same text either way.
Two messages, two measured lengths
Run it, then change either message and run it again. Each length is measured from its own label, so the two lines stay whole even when one of them grows.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
Which register holds the number that says which kernel call you want?
