roadmap
week 1 · day 5

Your First Assembly Program

Exit with a code, dissect every byte

Easy 35 min 180 xp
After this you can
  • Assemble and link a program with nasm and ld
  • Explain what a syscall is and how a program talks to the kernel
  • Trace the exact meaning of every line of a minimal program
  • Read the program's exit code and prove your code ran

The smallest real program

Today you write actual assembly, assemble it into a real executable, run it, and prove it worked. No libc, no magic, just instructions and one call into the kernel. The program does exactly one thing: exit with a chosen status code. Tiny, but every piece is real, and we will not skip a single byte.

exit.asm. Linux x86-64, Intel syntax (nasm)asm
1section .text ; the section holding executable instructions
2global _start ; make the label _start visible to the linker
3
4_start: ; execution begins here
5 mov rax, 60 ; rax = 60 -> the syscall number for "exit"
6 mov rdi, 42 ; rdi = 42 -> the first argument: the exit code
7 syscall ; hand control to the kernel; it performs exit(42)

What a syscall actually is

Your program runs in user mode, a restricted context that cannot directly touch hardware, files, or the screen, for safety. The kernel runs in privileged mode and can. When you need something only the kernel can do (exit, write to the screen, open a file), you make a system call: you put a number identifying the service in rax, put its arguments in the argument registers, and execute the syscall instruction. The CPU switches to the kernel, which does the work and switches back.

It is exactly like calling a function you don't own, except the "function" lives in the operating system and the calling convention is fixed by Linux.

RegisterMeaning for a syscallIn this program
raxWhich syscall (the number)60 = exit
rdi1st argument42 = the exit status
rsi2nd argument(unused here)
rdx3rd argument(unused here)
r10, r8, r94th, 5th, 6th arguments(unused here)
Key idea
Notice the argument order for syscalls: rdi, rsi, rdx, r10, r8, r9. Ordinary function calls use almost the same order (rdi, rsi, rdx, rcx, r8, r9. Day 13). Memorizing "rdi is the first argument" now pays off every single day from here on.

Line by line, nothing skipped

`section .text`, an executable is divided into sections. .text is where machine code lives (by tradition "text" means code). Other sections hold data. The assembler needs to know which bucket the following bytes belong to.

`global _start`, _start is the entry point the linker looks for, the very first instruction the OS runs. global exports the label so the linker can find it. Without this you'd get "no entry point" at link time.

`_start:`, a label: a human name for an address. When assembled, _start becomes the numeric address of the next instruction. Labels are how we avoid writing raw addresses; the assembler resolves them for us.

`mov rax, 60`, load the exit syscall number into rax. On Linux x86-64, 60 is exit. (These numbers are fixed by the kernel; you look them up in a table.)

`mov rdi, 42`, the exit code, the one argument exit takes. 42 is arbitrary, pick anything 0-255; it's what the shell will report.

`syscall`, the actual switch into the kernel. The kernel reads rax (60), sees "exit", reads rdi (42), and terminates the process with status 42. Control never returns, which is fine, because exiting is the point.

Build it and prove it ran

Two steps turn text into a running program:

1. Assemble, nasm translates your .asm into an object file (.o), which is machine code plus bookkeeping but not yet runnable. 2. Link, ld resolves labels and wraps the object into a final executable.

Then run it and read $?, the shell variable holding the last program's exit code.

build and check the exit codesh
1nasm -f elf64 exit.asm -o exit.o # assemble to a 64-bit ELF object
2ld exit.o -o exit # link the object into an executable
3./exit # run it (prints nothing)
4echo $? # prints: 42 <- your exit code!
Note
echo $? printing 42 is your proof. The program produced no output, yet you can see it ran and returned exactly the code you chose. That number traveled from mov rdi, 42 → the syscall → the kernel → the shell. You just moved a value across the entire stack of the machine by hand.

Peeking at the actual machine bytes

Your two-instruction core assembles into a handful of bytes. objdump shows them, the assembly you wrote next to the exact bytes the CPU fetches:

objdump -d exit (trimmed)out
10000000000401000 <_start>:
2 401000: b8 3c 00 00 00 mov eax, 0x3c ; 0x3c = 60
3 401005: bf 2a 00 00 00 mov edi, 0x2a ; 0x2a = 42
4 40100a: 0f 05 syscall

Look closely, this is the whole course in one screen: - b8 is the opcode "move an immediate into eax"; the next four bytes 3c 00 00 00 are the value 60, stored little-endian (low byte 0x3c first. Day 4 in action). - bf is "move immediate into edi"; 2a 00 00 00 is 42, little-endian again. - 0f 05 is the two-byte syscall opcode.

Your mnemonic mov rax, 60 and the CPU's byte b8 3c 00 00 00 are the *same instruction* at two levels of description. Assembly is a thin, human-readable skin over these bytes, and you can now read both.

Watch out
The assembler emitted mov eax, 0x3c (the 32-bit name) rather than mov rax, because writing eax already zero-extends to fill rax (Day 3's gotcha) and the 32-bit form is one byte shorter. The assembler optimized your instruction and the result is identical. Recognizing that eax-write == rax-gets-zero-extended is exactly why Day 3 hammered that point.
finished reading?
Your task, you write the code

Assemble, link, run, and change the code

Type exit.asm yourself and build it with the nasm/ld commands shown. Confirm `echo $?` prints your chosen code. Then: (1) change 42 to your age and rebuild, verify $? changes. (2) Try an exit code of 300 and explain in a comment what $? shows and why (hint: recall the byte's range from Day 1/2). (3) Run `objdump -d ./exit` and find the bytes for your exit value in the dump.

deliverable: exit.asm
build & run
$ nasm -f elf64 exit.asm -o exit.o
$ ld exit.o -o exit
$ ./exit; echo $?
$ objdump -d ./exit
self-review before running
  • echo $? prints exactly the number you put in rdi
  • You located your exit value's bytes in the objdump output
  • For exit code 300 you correctly explained that $? shows 44 (300 mod 256), tying back to unsigned-byte wrap
  • You can state what the `syscall` instruction does in one sentence
stretchIf nasm isn't installed, note the alternative: `sudo apt install nasm binutils` (Linux) or use a Linux container/VM, assembly syscalls are Linux-specific and won't run natively on macOS. If you're on macOS, install a Linux VM/container now; Week 2+ assumes a Linux x86-64 environment.

Self-check

01What does the `syscall` instruction do?
02In a Linux x86-64 syscall, the FIRST argument goes in which register?
03The bytes `b8 3c 00 00 00` encode `mov eax, 60`. Why is 60 stored as `3c 00 00 00`?
04You set the exit code to 300. `echo $?` shows:
0/4 correct · 0/4 checked