SYSTEM CALL

A syscall is the mechanism that allows a program running in user space to request services from the operating system kernel, such as reading from a file, writing to the console, or allocating memory. Instead of interacting directly with hardware, the program places specific values into designated registers to indicate the system call number and its arguments. Without syscalls, developers would need to manually communicate with hardware, for example writing to video memory and video I/O, handling character encoding, sending output to be printed, and waiting for confirmation of completion. Performing all these steps just to print a single character would make assembly code far longer and more complex, which is why syscalls provide a critical abstraction layer between software and hardware. On Linux x86 64, the system call number is placed in the rax register, while arguments are passed in rdi, rsi, rdx, r10, r8, and r9. Once the registers are prepared, the syscall instruction is executed, switching the processor into kernel mode and transferring control to the operating system. After the kernel completes the requested operation, it places the result or return value back into rax and returns execution to user space. Conceptually, a syscall can be thought of as a globally available function provided by the operating system kernel. It takes the required arguments via registers and executes the requested operation on behalf of the program. Linux syscalls and their corresponding numbers can be found in the system header file unistd_64.h.

root@dev:~$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h
 #ifndef _ASM_X86_UNISTD_64_H
 #define _ASM_X86_UNISTD_64_H 1

 #define __NR_read 0
 #define __NR_write 1
 #define __NR_open 2
 #define __NR_close 3
 #define __NR_stat 4
 #define __NR_fstat 5

 * this file sets the syscall number for each syscall to refer to that syscall 
   using this number
    - With 32-bit x86 processors, the syscall numbers are in the unistd_32.h file.

REFERENCE: LINUX SYSCALLS (LOCAL)

//view how to use the write syscall
root@dev:~$ man -s 2 write
 ...SNIP...
 ssize_t write(int fd, const void *buf, size_t count);
 
 * the write syscall expects three arguments
    - File Descriptor fd to be printed to (usually 1 for stdout)
    - The address pointer to the string to be printed
    - The length to be printed
    
 * the -s 2 flag specifies syscall man pages.

REFERENCE: LINUX SYSCALL TABLE (ONLINE)

SYSCALL CALLING CONVENTION

Save registers to the stack before any function call or syscall. However, if the call occurs at the start of the program when no registers hold meaningful values, saving them is not necessary.

1.identify the corresponding number for the syscall (write)
   - cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h
      #ifndef _ASM_X86_UNISTD_64_H
      #define _ASM_X86_UNISTD_64_H 1

      #define __NR_read 0
      #define __NR_write 1
      #define __NR_open 2
      #define __NR_close 3
      #define __NR_stat 4
      #define __NR_fstat 5

2.research how to use the required syscall (write)
   - root@dev:~$ man -s 2 write
      ...SNIP...
      ssize_t write(int fd, const void *buf, size_t count);
       - File Descriptor fd to be printed to (usually 1 for stdout)
       - The address pointer to the string to be printed
       - The length to be printed
 
3.Save registers to stack
   - push ...
   
4.Set its syscall number in rax
   - mov rax, 1
      - rax is also used for storing the return value of a syscall or a function. So, 
        if we were expecting to get a value back from a syscall/function, it will be 
        in rax.
        
5.Set its arguments in the registers
   - rdi -> 1 (for stdout)
     rsi -> 'Fibonacci Sequence:\n' (pointer to our string)
     rdx -> 20 (length of our string)

6.Use the syscall assembly instruction to call it

7.gracefully exit syscalls
   - root@dev:~$ grep exit /usr/include/x86_64-linux-gnu/asm/unistd_64.h
      #define __NR_exit 60
      #define __NR_exit_group 231
   - root@dev:~$ man -s 2 exit
      ...SNIP...
      void _exit(int status);
   
   - mov rax, 60
     mov rdi, 0
     syscall

All functions and syscalls should follow this standard and take their arguments from the corresponding registers.

section .data
  ; We can use mov rcx, 'string'. However, we can only store up to 8 bytes in a 
  ; register (i.e., 64 bits), which is equivalent to 8 ASCII characters, assuming each 
  ; character is 1 byte, so our intro string would not fit.
  ; this creates a variable with the specified string
  message db "Fibonacci Sequence:", 0x0a                ; 0x0a refers to a newline character
  msg_len equ $ - message                               ; $ is the current address, so $ - message = length

section .text
  global  _start
  
  _start:
    mov rax, 1           ; rax: syscall number 1
    mov rdi, 1           ; rdi: fd 1 for stdout
    mov rsi,message      ; rsi: pointer to message

    mov rdx, msg_len     ; length calculated dynamically with equ
    ;mov rdx, 20         ; rdx: print length of 20 bytes (manual calculation)
    syscall              ; call write syscall to the intro message
    xor rax, rax         ; initialize rax to 0
    xor rbx, rbx         ; initialize rbx to 0
    inc rbx              ; increment rbx to 1

  loopFib:
    add rax, rbx         ; get the next number
    xchg rax, rbx        ; swap values
    cmp rbx, 10	         ; do rbx - 10
    js loopFib	         ; jump if result is <0

DEBUGGING

root@dev:~$ gdb -q ./fib
gef>  disas _start
 Dump of assembler code for function _start:
 ..SNIP...
 0x0000000000401011 <+17>:	syscall 
gef>  b *_start+17
 Breakpoint 1 at 0x401011
gef>  r
 ───────────────────────────────────────────────────────────────────────────────────── registers ────
 $rax   : 0x1               
 $rbx   : 0x0               
 $rcx   : 0x0               
 $rdx   : 0x14              
 $rsp   : 0x00007fffffffe410  →  0x0000000000000001
 $rbp   : 0x0               
 $rsi   : 0x0000000000402000  →  "Fibonacci Sequence:\n"
 $rdi   : 0x1 
               
gef>  si
 Fibonacci Sequence:
 

GRACEFULLY EXITING SYSCALL

A graceful exit syscall allows a program to terminate cleanly by informing the operating system that it has completed execution and providing an optional exit code. In Linux x86-64 assembly, this is typically done using the exit syscall (number 60) with the return value placed in the rdi register. Failing to properly exit a program can lead to undefined behavior, including segmentation faults or other runtime errors, because the operating system may not correctly release resources or restore the process state. Using the exit syscall ensures that the program ends in a controlled manner, cleaning up resources, closing open file descriptors, and returning an appropriate status code to the operating system or calling process. This practice is essential in assembly programming, where there is no automatic runtime to manage process termination.

root@dev:~$ grep exit /usr/include/x86_64-linux-gnu/asm/unistd_64.h
 #define __NR_exit 60
 #define __NR_exit_group 231
 
root@dev:~$ man -s 2 exit
 ...SNIP...
 void _exit(int status);


section .data
    message db "Fibonacci Sequence:", 0x0a

section .text
  global  _start

  _start:
    mov rax, 1       ; rax: syscall number 1
    mov rdi, 1      ; rdi: fd 1 for stdout
    mov rsi,message ; rsi: pointer to message
    mov rdx, 20      ; rdx: print length of 20 bytes
    syscall         ; call write syscall to the intro message
    xor rax, rax    ; initialize rax to 0
    xor rbx, rbx    ; initialize rbx to 0
    inc rbx         ; increment rbx to 1

  loopFib:
    add rax, rbx    ; get the next number
    xchg rax, rbx   ; swap values
    cmp rbx, 10		; do rbx - 10
    js loopFib		; jump if result is <0
    
    ;graceful syscall exit
    mov rax, 60
    mov rdi, 0
    syscall

Last updated