One of the main pieces we need to run a program on top of our kernel is a system call interface. Systems calls are the mechanism that user-space programs use to access protected resources. We’ll begin this implemenetation task with an in-class discussion of how a system call is issued, and how the kernel receives it. That will include some code for you to use, but because there’s a fair amount of code required we’ll spend time in class building it up so you understand what it’s doing.
Once you have a basic interface that you can use to issue and handle system calls, your task will be to implement basic versions of the read and write system calls.
Our kernel doesn’t have any concept of files yet, so these systems calls will be limited to operations on the terminal;
read will only work to read keyboard input using file descriptor 0 (standard input) and write will only work to write to the terminal using file descriptors 1 (standard output) or 2 (standard error).
Update your kernel/Makefile to build assembly files:
CC := clang -target x86_64-elf
LD := x86_64-elf-ld
CFLAGS := --std=c17 -Wall -O2 -I. -ffreestanding -nostdlib -fno-stack-protector -fno-pic -mno-80387 -mno-mmx -mno-3dnow -mno-sse -mno-sse2 -mno-red-zone -mcmodel=kernel -MMD -MP
LDFLAGS := -nostdlib -static
OUT := obj
SRC := $(wildcard *.c)
ASM := $(wildcard *.s)
C_OBJ := $(patsubst %.c, $(OUT)/%.o, $(SRC))
S_OBJ := $(patsubst %.s, $(OUT)/%.o, $(ASM))
DEP := $(patsubst %.c, $(OUT)/%.d, $(SRC))
.PHONY: all
all: kernel.elf
.PHONY: clean
clean:
rm -rf kernel.elf $(OUT)
.PHONY: run
run:
$(MAKE) -C .. run
kernel.elf: $(C_OBJ) $(S_OBJ) linker.ld
$(LD) $(LDFLAGS) -T linker.ld -o $@ $(C_OBJ) $(S_OBJ)
$(C_OBJ): $(OUT)/%.o: %.c
@mkdir -p `dirname $@`
$(CC) $(CFLAGS) -c $< -o $@
$(S_OBJ): $(OUT)/%.o: %.s
@mkdir -p `dirname $@`
$(CC) -c $< -o $@
-include $(DEP)
Create kernel/syscall.s:
.global syscall
# This function is called to issue a system call
# Arguments are:
# syscall number (in %rdi)
# syscall arg0 (in %rsi)
# syscall arg1 (in %rdx)
# syscall arg2 (in %rcx)
# syscall arg3 (in %r8)
# syscall arg4 (in %r9)
# syscall arg5 (at 0x8(%rsp))
syscall:
# Pull argument 5 up into %rax
mov 0x8(%rsp), %rax
# Trigger the system call interrupt
int $0x80
# Return from the function
retq
Create kernel/syscall_entry.s:
.global syscall_entry
.global syscall_handler
# This is the interrupt handler routine called when a system call is issued
syscall_entry:
# The %rax register holds the sixth syscall argument. Put it on the stack.
push %rax
# Call the C-land syscall handler
call syscall_handler
# The %rax register now holds the return value. Move the stack up without overwriting %rax.
add $0x8, %rsp
# Return from the interrupt handler
iretq
Now in kernel/boot.c, add declarations for our two assembly functions and a syscall_handler function that is called when a system call is issued:
int64_t syscall_handler(uint64_t nr, uint64_t arg0, uint64_t arg1, uint64_t arg2, uint64_t arg3, uint64_t arg4, uint64_t arg5) {
kprintf("syscall %d: %d, %d, %d, %d, %d, %d\n", nr, arg0, arg1, arg2, arg3, arg4, arg5);
return 123;
}
extern int64_t syscall(uint64_t nr, ...);
extern void syscall_entry();
Finally, in _start, you can set a handler for the system call interrupt:
void _start() {
...
idt_set_handler(0x80, syscall_entry, IDT_TYPE_TRAP);
...
}
Write kernel-side implementations of the read and write system calls.
Make sure your implementations validate the provided file descriptor numbers.
If read or write is called with anything other than a supported file descriptor, the system call should return -1 to indicate an error.
Otherwise, perform the requested operation and return the number of bytes read or written.
You can test your system call implementations by issuing a system call with the syscall function:
char buf[6];
long rc = syscall(SYS_read, 0, buf, 5);
if (rc <= 0) {
kprintf("read failed\n");
} else {
buf[rc] = '\0';
kprintf("read '%s'\n", buf);
}
You should write a similar test for the write system call, but the buffer should hold characters to be printed rather than space to store read characters.
Hint: the read function should not include backspace characters in the buffer. Instead, when backspace is typed drop the most rightmost character from the buffer and continue reading. If the buffer is empty and backspace is typed it should do nothing. This is probably similar to your implementation of kgets if you attempted it.