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.
| What | Where |
|---|---|
| System call number | The immediate operand of the svc instruction: svc #N. The number can be read by the kernel from bits 15:0 of ESR_EL1. |
| Arguments | Arguments are passed in registers x0, x1, x2, … in order. A syscall can have at most six arguments, i.e., using x0–x5. |
int arguments | Passed in the low 32 bits of their register; the kernel must ignore the upper 32 bits (read them as (int)ctx->regs[i]). |
| Return value | x0. |
| Errors | A system call that fails returns -1 (init_canvas, which returns a pointer, returns NULL instead). There are no error codes. |
| Registers preserved | All 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 address | The 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
# | Name | Description | Quest |
|---|---|---|---|
| 0 | yield | Cooperative yield | Project 4, Quest 2 |
| 1 | ticks | Get the current system tick counter, in milliseconds | Project 4, Quest 2 |
| 2 | write | Write to a file descriptor | Project 4, Quest 3 |
| 3 | read | Read from a file descriptor | Project 4, Quest 3 |
| 4 | peek | Get the number of bytes available in a file descriptor | Project 4, Quest 3 |
| 5 | getpid | Get the current process’ PID | Project 4, Quest 2 |
| 6 | process_info | Fetch info about running processes | Project 4, Quest 4 |
| 7 | init_canvas | Allocate a display canvas | Project 4, Quest 4 |
| 8 | wait_for_vsync | Wait for compositor frame | Project 4, Quest 4 |
| 9 | spawn | Start a new process | Project 4, Quest 4 |
| 10 | exit | Exit a process | Project 4, Quest 4 |
| 11 | wait | Wait for a process to exit | Project 4, Quest 4 |
| 12 | pipe | (CS 1690/2670 only) Create a pipe | Project 4, Quest 5 |
| 13 | close | (CS 1690/2670 only) Close a file descriptor | Project 4, Quest 5 |
| 51 | open_console | (CS 1690/2670 only) Open a descriptor of your own on the console | Project 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
fdrefers to the console:writemust send allnbytes to the serial console before returning. - (CS 1690/2670 only) If
fdis a pipe,writemust block while the pipe is full; returnsnonce all bytes are in the pipe.
Return value:
- Normally,
writereturns the number of bytes written, as described above. Sincewritealways blocks until all bytes have been written (to the console, or to the pipe), this means that the return value is alwaysnwhen write does not fail. - Returns
-1iffdis not an open descriptor that can be written, or if[buf, buf + n)is not in the caller’s memory. - A write of
0bytes returns0. - (CS1690/2670 only) Returns
-1if 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
fdrefers to the console:readblocks 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 ton. Reading marks the process as a keyboard reader (see the focus rules below). - (CS1690/2670 only) If
fdis 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
-1iffdis not an open descriptor that can be read, or if[buf, buf + n)is not in the caller’s memory. - A read of
0bytes returns0without 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 endfds[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
readorpeekon 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 toreadorpeeknever 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
ZOMBIEprocess 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.
| Function | Built 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
};