AArch64 Playground
4.30 · Talking to the operating system: svc and files

Talking to the operating system: svc and files

printf and scanf never touch the screen or the keyboard themselves. They are library functions that turn numbers into text and back, and then ask the operating system to move the bytes. The operating system is the program that shares the machine among all the others, and its core, the kernel, is the only code allowed to work with devices directly. A request from your program to the kernel is a system call. This lesson makes system calls by hand.

Three layers of input and output

Every byte a program prints passes through three layers:

  1. Library functions such as printf, scanf, and puts. They format numbers as text, and they collect output in a buffer (a block of memory used as a waiting area) so they can hand it on in large pieces.
  2. System calls such as write and read. They move raw bytes and know nothing about %d or %f.
  3. The devices: screen, keyboard, disk. Only the kernel works with these.

Unix treats every device, and every file on the disk, as a file: something you can read bytes from or write bytes to. A program names an open file by a small number, its file descriptor. Every program starts with three already open: 0 is standard input (usually the keyboard), 1 is standard output (usually the screen), and 2 is standard error (also the screen, but kept separate so error messages do not end up inside output that was sent to a file).

How svc works

An ordinary program such as yours runs at EL0, exception level 0, the processor's least trusted mode, where instructions that control devices are not allowed. The kernel runs at EL1. The instruction svc 0 (supervisor call) is the one door between them: it switches the processor to EL1 and jumps into the kernel, which reads your request from the registers, does the work, and comes back to the instruction after the svc.

The request is spelled out in registers:

  • x8 holds the service number, which picks the system call.
  • x0 to x5 hold its arguments, in order.
  • After svc 0, x0 holds the result. A negative result means the call failed.

A system call changes no register except x0, unlike a bl to a library function. These are the calls this lesson uses:

x8callargumentsresult in x0
56openatx0 = directory, x1 = path, x2 = flags, x3 = modea new file descriptor
57closex0 = file descriptor0
63readx0 = file descriptor, x1 = buffer address, x2 = most bytes to readbytes read, 0 at the end
64writex0 = file descriptor, x1 = address of the bytes, x2 = how manybytes written
93exitx0 = exit statusdoes not come back

Writing without printf

The first program prints one line with write. write needs the message's length in bytes, and the assembler can count it. In note_len = . - note, the dot stands for the current address, just past the message's last byte, and taking away the message's first address leaves its length.

The message uses .ascii rather than .string. .string adds a zero byte at the end, which printf needs to find the end of a string but write does not: counted into the length, the zero byte would be sent to the screen as an invisible extra character.

The program then prints the count the kernel returned, this time with printf. Run it: the first line is this line never went through printf and the second is write reported 36 bytes.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 1try it: run it, or step one instruction at a timeOpen in playground

pitfall

printf keeps its text in its buffer and passes it to write later: at the end of each line when the output is a screen, but only when the buffer fills or the program ends when the output goes to a file, which is how an automatic grader reads it. A program that mixes printf and write can then print out of order. This one is safe because its write comes first. When you mix them, put every printf after the last write, or use one of the two throughout. For the same reason, end a program by returning from main, which empties the buffer, and not with service 93, which ends it at once and loses whatever printf was still holding.

Opening a file

openat opens a file and returns a new file descriptor for it, usually 3 for the first file a program opens. It takes four arguments:

  • x0: the directory a relative path starts from. The special value AT_FDCWD, which is -100, means the current directory.
  • x1: the address of the file name, ending in a zero byte (write it with .string, since the kernel looks for that zero).
  • x2: the flags, one bit for each choice, joined with |.
  • x3: the permission mode, which counts only when the call creates the file.

The flags and the mode are written in octal, base 8, which the assembler recognizes by a leading 0: 0644 is octal, and 644 would be decimal. One octal digit stands for exactly three bits, which suits values built from single bits and groups of three:

flagoctalmeaning
O_RDONLY0read only
O_WRONLY01write only
O_RDWR02read and write
O_CREAT0100create the file if it does not exist
O_TRUNC01000empty the file if it does exist
O_APPEND02000add every write at the end

The mode's three octal digits give the permissions of the file's owner, of the owner's group, and of everyone else. Each digit adds up read (4), write (2), and execute (1), so 0644 lets the owner read and write, and everyone else only read.

Writing a file and reading it back

The next program uses all four calls. It creates notes.txt, writes three lines to it, and closes it. Then it opens the file again to read it and copies it to the screen 32 bytes at a time: read fills a buffer in the frame and returns how many bytes it put there, and write sends exactly that many to file descriptor 1. A read can return fewer bytes than asked for, which is normal; only 0 means the end of the file.

Every call that can fail is checked. If openat returns a negative number, or write stores fewer bytes than it was given, the program writes an error line to standard error and returns 1 from main, the exit status that tells the shell something went wrong. The descriptor stays in x19 so that it is still there if you add a printf later; main saves x19 first, because every subroutine must leave x19 as it found it.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 2try it: run it, or step one instruction at a timeOpen in playground

It prints the three lines it wrote, buy bread, call grandma, and water the plants, and exits with status 0.

note

To watch the error path, delete | O_CREAT from the first openat. The file does not exist yet, and the program no longer asks for it to be made, so openat fails, and the program prints notes: cannot create notes.txt and exits with status 1. A failed system call returns the error's number made negative (-2, for example, when a file does not exist), and the C library's own open turns every failure into -1. Testing for any negative value, as this program does, catches all of them.

Reading until the input ends

Standard input works the same way through file descriptor 0. The last program reads it 8 bytes at a time, adds up what each read returns, and stops when read returns 0. The lesson feeds it three lines, 28 bytes when the newline at the end of each line is counted.

A keyboard never runs out by itself, so after those 28 bytes the program waits, and the console says it is waiting for input. Press Ctrl+D in the console's input box. That marks the end of the input, read returns 0, and the program prints 28 bytes. In a Linux terminal, ./count < notes.txt feeds it a file instead, and the end of the file does the same job.

loading editor...

regfile

N clearZ clearC clearV clear

x0–x30 are the integer registers.

X0arg00x0000000000000000
X1arg10x0000000000000000
X2arg20x0000000000000000
X3arg30x0000000000000000
X4arg40x0000000000000000
X5arg50x0000000000000000
X6arg60x0000000000000000
X7arg70x0000000000000000
X8ind0x0000000000000000
X90x0000000000000000
X100x0000000000000000
X110x0000000000000000
X120x0000000000000000
X130x0000000000000000
X140x0000000000000000
X150x0000000000000000
X16ip00x0000000000000000
X17ip10x0000000000000000
X18pr0x0000000000000000
X190x0000000000000000
X200x0000000000000000
X210x0000000000000000
X220x0000000000000000
X230x0000000000000000
X240x0000000000000000
X250x0000000000000000
X260x0000000000000000
X270x0000000000000000
X280x0000000000000000
X29fp0x0000000000000000
X30lr0x0000000000000000
SP0x0000000080000000
PC0x0000000000400000
console

Output prints here as your program runs.

Press step or run under the editor, or feed stdin from the box below.

not assembled

example 3try it: run it, or step one instruction at a timeOpen in playground

Check yourself

  1. Which register picks the system call, and which holds its result?
  2. What does read return at the end of the input, and what does a negative result mean?
  3. Why does a message for write suit .ascii better than .string?
  4. What permissions does the mode 0640 give?
  5. After svc 0, which registers may have changed?

answers

show answers
  1. x8 picks the call, and x0 holds the result.
  2. It returns 0. A negative result means the call failed.
  3. write sends exactly the number of bytes in x2. A length taken with . - label after .string counts the zero byte, and write would send it.
  4. The owner can read and write (6), the group can read (4), and everyone else has no access (0).
  5. Only x0.

Practice