Skip to content

Step 1 of 5 · Reading · ~4 min

Learn

First Steps

Hello via syscall

By the end of this lesson your program will read a line from standard input and answer on standard output with two kernel calls and nothing else. A CPU only moves bytes and does arithmetic; the operating system owns the screen and the keyboard, and a syscall is how a program asks it for something.

On Linux x86_64 you put a request number and its arguments into specific registers and run the syscall instruction; the CPU traps into the kernel, which does the work and hands control back. This course uses NASM (the Netwide Assembler). There is no libc and no printf: every effect your program has on the outside world is a syscall.

Anatomy of a NASM program

section .data
    banner db "Build OK", 10
    banner_len equ $ - banner

section .text
    global _start
_start:
    mov rax, 1            ; syscall number: 1 = write
    mov rdi, 1            ; arg 1: file descriptor, 1 = stdout
    mov rsi, banner       ; arg 2: address of the first byte
    mov rdx, banner_len   ; arg 3: how many bytes
    syscall

    mov rax, 60           ; syscall number: 60 = exit
    xor rdi, rdi          ; arg 1: exit status 0
    syscall
  • section .data holds initialized bytes. db ("define bytes") lays them out exactly as written: the 8 characters of Build OK, then the raw byte 10, which is the ASCII newline. Assembly has no string type, only bytes you choose to treat as text.
  • banner_len equ $ - banner is computed while assembling. $ means "the address right here", so subtracting banner gives the byte count, 9. Edit the text and the length follows.
  • section .text holds code. global _start exports the entry label. The kernel starts you at _start, not main: main is a C convention that libc's startup code calls, and no libc is linked here.

The syscall convention

The number goes in rax. The arguments go, in order, in rdi, rsi, rdx, r10, r8, r9. The numbers you need now are 0 = read, 1 = write, 60 = exit. File descriptor 0 is standard input, 1 is standard output, 2 is standard error.

  • The numbers are 64-bit specific. On 32-bit Linux write was 4; on x86_64, rax = 4 is stat. The wrong table runs a different syscall instead of failing loudly.
  • syscall clobbers rcx and r11. The kernel parks your return address and flags there. Every other register survives.
  • write ignores null terminators. It sends exactly rdx bytes from rsi. A length of 0 succeeds and prints nothing.

Syscalls hand an answer back

When syscall returns, rax holds the result. For read and write it is the number of bytes actually transferred, or a small negative error code (-9 means bad file descriptor). For read, 0 means end of input.

To read you need somewhere to put the bytes. section .bss reserves zeroed space without storing it in the file:

section .bss
    buf resb 16           ; 16 bytes, all zero at start

Suppose stdin holds Bob and a newline, the four bytes 42 6F 62 0A, and you call read with rdi = 0, rsi = buf, rdx = 16. You asked for up to 16 but the line has 4, so rax comes back as 4 and buf holds those four bytes.

Now the catch. The next syscall needs its own number in rax, which destroys your count. Copy it somewhere that survives, such as rbx, before you set up the next call:

    mov rbx, rax          ; rax is about to become 1
    mov rax, 1
    mov rdi, 1
    mov rsi, buf
    mov rdx, rbx          ; echo exactly what was read
    syscall

That is a complete echo program body. A read may return fewer bytes than you asked for, so use the returned count.

Why exit is not optional

After your last instruction the CPU keeps executing whatever bytes come next in memory, usually ending in a segfault. exit (rax = 60, status in rdi) ends the process on purpose. xor rdi, rdi zeroes a register, shorter than mov rdi, 0.

Common mistakes

  • Reusing rax after it was overwritten. Run mov rax, 1 and then mov rdx, rax, and rdx is 1: you print one byte instead of the line.
  • Counting the newline you did not want. The count from read includes the 0A at the end of the line. Write all of it and a line break lands in the middle of your output.
  • Counting characters. write counts bytes: José is 5 of them.
  • No exit. The program falls off the end and crashes even though the text appeared.

Your exercise

The starter already reads one line into name and then exits without printing anything. Turn it into a greeter using the data labels provided. The detail the tests lean on is the line break at the end of what read returned.

Up nextRegisters and ArithmeticFirst Steps

Discussion

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

Sign in to post a comment or reply.

Loading…