Talking to the operating system: svc and files
prerequisite
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:
- Library functions such as
printf,scanf, andputs. 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. - System calls such as
writeandread. They move raw bytes and know nothing about%dor%f. - 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:
x8holds the service number, which picks the system call.x0tox5hold its arguments, in order.- After
svc 0,x0holds 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:
x8 | call | arguments | result in x0 |
|---|---|---|---|
| 56 | openat | x0 = directory, x1 = path, x2 = flags, x3 = mode | a new file descriptor |
| 57 | close | x0 = file descriptor | 0 |
| 63 | read | x0 = file descriptor, x1 = buffer address, x2 = most bytes to read | bytes read, 0 at the end |
| 64 | write | x0 = file descriptor, x1 = address of the bytes, x2 = how many | bytes written |
| 93 | exit | x0 = exit status | does 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.
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 valueAT_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:
| flag | octal | meaning |
|---|---|---|
O_RDONLY | 0 | read only |
O_WRONLY | 01 | write only |
O_RDWR | 02 | read and write |
O_CREAT | 0100 | create the file if it does not exist |
O_TRUNC | 01000 | empty the file if it does exist |
O_APPEND | 02000 | add 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.
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.
Check yourself
- Which register picks the system call, and which holds its result?
- What does
readreturn at the end of the input, and what does a negative result mean? - Why does a message for
writesuit.asciibetter than.string? - What permissions does the mode
0640give? - After
svc 0, which registers may have changed?
answers
show answers
x8picks the call, andx0holds the result.- It returns 0. A negative result means the call failed.
writesends exactly the number of bytes inx2. A length taken with. - labelafter.stringcounts the zero byte, andwritewould send it.- The owner can read and write (6), the group can read (4), and everyone else has no access (0).
- Only
x0.
Practice
- Count the lines: read standard input with the
readsystem call instead ofscanf. - High score keeper: keep a record in a file with
openat,read,writeandclose. - Echo the sum: read input the library way, with
scanf. - Advanced quiz: ARMv8 assembly: asks about exception levels.
- Basic quiz: input and output: file descriptors, the register that picks a system call, and why
printfandwritecan print out of order. - Fill in the blank: input and output (core): service numbers, descriptors, and the length
writesends. - Predict: input and output (challenge): what
printf,scanf,readandwritereturn.