System Call ABI

This page defines the contract between your kernel and the programs that run on it, starting from when you implement system calls. This contract specifies an Application Binary Interface (ABI) for your kernel.

While we allow you a great deal of freedom to design your operating system, you must follow the ABI specified here: this ensures that your kernel can run programs compiled by other users–either programs we provide to you, or programs compiled your peers!

Concretely, this guide specifies a set of system calls that your operating system must implement. Some guidelines:

  • For all system calls specified in this guide, your operating system MUST use the system call numbers, arguments, return values, and overall behavior described in this document
  • Some system calls are only required for CS 1690/2670 students. If you are taking CS 1670 only, you do not need to implement these, but you MUST NOT implement any other system calls using those numbers, to avoid collisions with other code we may compile for CS 1690/2670 students.
  • If you add your own syscalls outside of this ABI, you MUST use syscall numbers of 100 or higher. This way, the syscall numbers do not collide with numbers we may use in future projects.
  • This page also contains some optional guidelines for some parts of your implementation; we include these as suggestions, but you are not required to follow them.

In Project 4 Quest 4, you will test that your operating system implements our syscall ABI by running a binary for snake that we provide. The snake program requires your kernel to follow this ABI.

Note: This is a living document. We will keep it updated with syscalls added by future projects. From Project 4 onward, the handout will direct you to this document for the specification on any new system calls!

Calling convention

System calls are invoked using the svc instruction. This section describes the calling convention for making system calls.

WhatWhere
System call numberThe immediate operand of the svc instruction: svc #N. The number can be read by the kernel from bits 15:0 of ESR_EL1.
ArgumentsArguments are passed in registers x0, x1, x2, … in order. A syscall can have at most six arguments, i.e., using x0–x5.
int argumentsPassed in the low 32 bits of their register; the kernel must ignore the upper 32 bits (read them as (int)ctx->regs[i]).
Return valuex0.
ErrorsA system call that fails returns -1 (init_canvas, which returns a pointer, returns NULL instead). There are no error codes.
Registers preservedAll registers other than x0 have the same value after the system call as before it. (Your kernel restores the whole saved context, so this holds automatically.)
Return addressThe instruction after the svc. (The hardware puts this address in ELR_EL1; the kernel must not change it.)

Pointers passed to the kernel must point into the calling process’s own memory; any other pointer makes the system call fail with -1. The kernel checks every pointer argument before it does anything else, so a call that fails because of a bad pointer has no effect and does not block. The kernel never returns a pointer into its own memory.

System calls

#
NameDescription
Quest
0yieldCooperative yieldProject 4, Quest 2
1ticksGet the current system tick counter, in millisecondsProject 4, Quest 2
2writeWrite to a file descriptorProject 4, Quest 3
3readRead from a file descriptorProject 4, Quest 3
4peekGet the number of bytes available in a file descriptorProject 4, Quest 3
5getpidGet the current process’ PIDProject 4, Quest 2
6process_infoFetch info about running processesProject 4, Quest 4
7init_canvasAllocate a display canvasProject 4, Quest 4
8wait_for_vsyncWait for compositor frameProject 4, Quest 4
9spawnStart a new processProject 4, Quest 4
10exitExit a processProject 4, Quest 4
11waitWait for a process to exitProject 4, Quest 4
12pipe(CS 1690/2670 only) Create a pipeProject 4, Quest 5
13close(CS 1690/2670 only) Close a file descriptorProject 4, Quest 5
51open_console(CS 1690/2670 only) Open a descriptor of your own on the consoleProject 4, Quest 5
100+Up to you!If you want to define any of your own system calls (e.g., for extra credit), define them here!

In kernel/syscall.h, these become #define SYS_yield 0, #define SYS_ticks 1, and so on; the build turns each of those lines into a stub in the generated user/syscalls.S. Numbers above 50 (and below 100) are for system calls that only CS 1690/2670 students implement. If you add system calls of your own (for extra credit, say), give them numbers from 100 upwards, so that they never collide with numbers we might add later.

yield

int yield(void)

Gives up the CPU: the scheduler may run other processes before the caller runs again.

Arguments: None

Return value: Always 0.

ticks

uint64 ticks(void)

Returns the number of milliseconds since the system started. This value must keep pace with real time, even if interrupts are delayed (see the hints here for more on what this means).

Arguments: None

Return value: timer count in milliseconds

write

int64 write(int fd, const void* buf, uint64 n)

Writes n bytes from buf to the object that fd refers to, and returns the number of bytes written.

The behavior for writing bytes depends on the type of file descriptor being used:

  • If fd refers to the console: write must send all n bytes to the serial console before returning.
  • (CS 1690/2670 only) If fd is a pipe, write must block while the pipe is full; returns n once all bytes are in the pipe.

Return value:

  • Normally, write returns the number of bytes written, as described above. Since write always blocks until all bytes have been written (to the console, or to the pipe), this means that the return value is always n when write does not fail.
  • Returns -1 if fd is not an open descriptor that can be written, or if [buf, buf + n) is not in the caller’s memory.
  • A write of 0 bytes returns 0.
  • (CS1690/2670 only) Returns -1 if no read end of the pipe is open.

read

int64 read(int fd, void* buf, uint64 n)

Reads up to n bytes from the object that fd refers to into buf, and returns the number of bytes read.

The behavior for read depends on the type of file descriptor being used:

  • If fd refers to the console: read blocks until the calling process’s keyboard input buffer has at least one byte for this process, then returns as many bytes as are available, up to n. Reading marks the process as a keyboard reader (see the focus rules below).
  • (CS1690/2670 only) If fd is a pipe, blocks while the pipe is empty and a write end is still open.

Return value:

  • Returns 0 (end-of-file) if the pipe is empty and no write end is open.
  • Returns -1 if fd is not an open descriptor that can be read, or if [buf, buf + n) is not in the caller’s memory.
  • A read of 0 bytes returns 0 without blocking.

peek

int64 peek(int fd)

Returns the number of bytes that a read() on fd could return right now without blocking (0 if none). Never blocks. On the console, it marks the process as a keyboard reader, like read().

Returns -1 if fd is not an open descriptor that can be read, or (CS 1690/2670 only) if it is the read end of a pipe that has reached end-of-file.

(CS1690/2670 only) Behavior for pipes: On the read end of a pipe, peek() returns the number of bytes in the pipe, as long as there are any—even if no write end is open any more. Once the pipe is empty, it returns 0 if a write end is still open somewhere (a read() would block), and -1 if none is (end-of-file: a read() would return 0 at once). So a program that must not block can tell that the other side has gone away, for instance a terminal emulator whose shell has exited.

getpid

int getpid(void)

Returns the caller’s PID.

process_info

int process_info(struct pinfo* buf, int max)

Fills in up to max entries of the array buf with information about the processes that currently exist (in any state but UNUSED), and returns the number of entries filled in.

Return value: -1 if max is negative or the array is not in the caller’s memory.

init_canvas

struct canvas* init_canvas(void)

Assign the caller a window on the screen and a canvas to use. Returns the address of the canvas’s struct canvas. The struct canvas should live in the process’ own memory.

Return value: on success, returns a pointer to the process' struct canvas. Returns NULL (0) if there is no display, or no free window.

wait_for_vsync

int wait_for_vsync(void)

Blocks until the compositor has next refreshed the screen, and returns 0.

Calling wait_for_vsync tells the compositor that the caller’s canvas holds a complete frame (see Project 3, Quest 4, Part D).

Return value: Always returns 0. If there is no display or the caller has no canvas, returns 0 immediately (i.e., without blocking).

spawn

int spawn(const char* name, int stdin_fd, int stdout_fd)

Creates a new process running the program called name, as a child of the caller. The child process must be set up as follows:

  • The child’s file descriptor 0 must be connected to the same file descriptor object as the caller’s stdin_fd
  • The child’s file descriptor 1 must be connected to the same file descriptor object as the caller’s stdout_fd
  • All of its other file descriptors are closed

The process name (name) must be at most 15 characters, plus a null terminator.

Return value: Returns the PID of the child process, -1 if there is no such program, either descriptor is not open, the name is not a valid string in the caller’s memory, or there is no free process table entry.

exit

[[noreturn]] void exit(int status)

Ends the caller, with exit status status. Closes all its descriptors, gives up its window and canvas, and, if it had the keyboard focus, gives it up as described under Keyboard focus. The process remains a ZOMBIE until its parent waits for it, unless its parent has already exited.

A process that the kernel kills (because of a fault) ends as if it had called exit(-1). A process whose main() returns ends as if it had called exit() with main’s return value (this is done by _start in user/lib/u_start.c).

Return value: Never returns

wait

int wait(int pid)

Blocks until the caller’s child pid has exited, frees its process table entry, and returns its exit status.

Return value: On success, returns the child process’ exit status. Returns -1 immediately if pid is not a child of the caller. (A child killed by the kernel also has exit status -1, so the two cases look the same to the caller.)

pipe (CS 1690/2670 only)

int pipe(int fds[2])

Creates a pipe and two new descriptors in the caller, using the two lowest-numbered free descriptors:

  • fds[0] refers to the pipe’s read end
  • fds[1] to its write end

Return value Returns 0 on success, -1 if the caller has fewer than two free descriptors, the kernel has no free pipe, or fds is not in the caller’s memory.

close (CS 1690/2670 only)

int close(int fd)

Closes the descriptor fd. When the last descriptor (in any process) that refers to a pipe’s write end is closed, readers of the pipe see end-of-file; when the last descriptor that refers to its read end is closed, any writes to the pipe return -1.

Return value 0 on success, -1 if fd is not an open descriptor

open_console (CS 1690/2670 only)

int open_console(void)

Opens a new descriptor on the console, in the caller’s lowest-numbered free descriptor. read on it reads the keyboard, and write on it writes to the serial console, whatever the caller’s descriptors 0 and 1 refer to—even if they are pipes from a terminal emulator.

This is for a program that wants the keyboard for itself, such as a terminal emulator or a game in a window, when a shell started it: the shell’s descriptors 0 and 1, which the program inherits, lead to another terminal, not to the keyboard. A program that opens the console should close the descriptor 0 it inherited, so that it does not keep the shell’s input pipe open.

The caller becomes a keyboard reader at once, as if it had called read on the console, and takes the focus by the rules under Keyboard focus.

Return value The new descriptor, or -1 if the caller has no free descriptor.

Keyboard focus

Since our system calls may affect keyboard focus, our syscall ABI also specifies some rules for how keyboard focus is managed. These rules extend the focus rules from Project 3, Quest 2, Part E:

  • Keystrokes go to the input buffer of the process with the focus. Shift+Tab moves the focus on to the next process that reads the keyboard.
  • The first time a process calls read or peek on the console (or, in CS 1690/2670, open_console), it becomes a keyboard reader. When this happens, if no process has the focus, or if the focus belongs to the process’s parent, the process takes the focus. Subsequent calls to read or peek never move the focus.
  • When the process with the focus exits, the focus returns to its parent, if the parent is a keyboard reader; otherwise, the focus moves to the next process that would receive it when Shift+Tab is pressed. If there is no other keyboard reader, the focus becomes unassigned.
  • A ZOMBIE process can never have the focus.
  • (CS 1690/2670 only, from Quest 5) Ancestors, not just the parent. In the two rules above, “parent” becomes “ancestor”: a new keyboard reader takes the focus if any of its ancestors (its parent, its parent’s parent, and so on) has it, and the focus of an exiting process goes to its closest ancestor that is a keyboard reader. This matters once a shell sits between a terminal and the programs it starts: the shell reads a pipe from its terminal, never the keyboard, so it never has the focus itself.

User-side library

These functions are not system calls, but every program can use them; they live in user/lib/u_common.c and are built on the system calls above.

FunctionBuilt on
void printf(const char* fmt, ...)formats into a local buffer, then write(1, ...)
char getc(void)read(0, &c, 1)
uint32 hasc(void)peek(0)
_start (in u_start.c)calls main(), then exit() with its return value

Data structures shared with user programs (suggested)

Only your own programs depend on these, so their exact layout is up to you—but the kernel’s definition and the user side’s definition must match exactly. As in Project 3, keep the two copies in step by hand.

// One entry of process_info()'s output.
struct pinfo {
  uint32 pid;
  uint32 ppid;       // parent's PID, or (uint32)-1 if none
  uint32 state;      // your enum procstate, including ZOMBIE
  char name[20];
};

// A process's canvas (unchanged from Project 3, Quest 4).
struct canvas {
  uint32* pixels;
  uint32 width;             // in pixels
  uint32 height;            // in pixels
  uint32 pitch_w;           // distance from one row to the next, in pixels
  volatile uint32 changed;  // CS 1690/2670, Project 3 Quest 5
};