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
SYS_EXIT=1
SYS_WRITE=4
; file descriptors
STDOUT=1
entry $
start:
mov eax, SYS_WRITE
mov ebx, STDOUT
mov ecx, message
mov edx, messageLength
int 0x80
mov eax, SYS_EXIT
xor ebx, ebx ; exit code 0
int 0x80
message:
db "Hello, world!",0xa
messageLength=$-message
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.