Let's consider an example: a simple x86 Hello World program for Linux, that prints a message to
stdout and exits. It needs to pass a couple of data items to the kernel:
- Text string to output,
- Exit code.
Here's the assembly code (to be compiled with FASM):
format ELF executable
segment readable executable
; system call numbers
; file descriptors
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, message
mov edx, messageLength
mov eax, SYS_EXIT
xor ebx, ebx ; exit code 0
db "Hello, world!",0xa
All this program does to fulfill its main goal (message output) is
- Set appropriate CPU registers to the values representing the system call number (for
sys_write syscall), file descriptor (
stdout), address of the message and the message length
- Do the system call, in this example by means of software interrupt 0x80
Similar sequence is to exit: set the registers to the system call number and exit code, and do the system call.
Which registers to set to which values is defined by the system call calling convention.
After the kernel starts executing the syscall handler, this handler reads the values of the registers from the application's context and interprets them according to the calling convention. In particular, when it sees that system call is
sys_write, it takes the length and address of the message, and uses them to read from the user space memory. Then these data (along with file descriptor) are passed to the drivers that will do the actual work.