Syscalls
Aunque estamos hablando directamente con la CPU a través de instrucciones de máquina en Assembly, no tenemos que invocar cada tipo de comando utilizando solo instrucciones de máquina básicas. Los programas utilizan regularmente muchos tipos de operaciones. El Sistema Operativo puede ayudarnos a través de syscalls para no tener que ejecutar estas operaciones manualmente cada vez.
Por ejemplo, supongamos que necesitamos escribir algo en la pantalla sin syscalls. En ese caso, necesitaríamos comunicarnos con la Memoria de Video y la Entrada/Salida de Video, resolver cualquier codificación requerida, enviar nuestra entrada para que sea impresa y esperar la confirmación de que ha sido impresa. Como era de esperar, si tuviéramos que hacer todo esto para imprimir un solo carácter, los códigos en Assembly serían mucho más largos.
Linux Syscall
Un syscall
es como una función globalmente disponible escrita en C
, proporcionada por el Kernel del Sistema Operativo. Un syscall toma los argumentos necesarios en los registros y ejecuta la función con los argumentos proporcionados. Por ejemplo, si quisiéramos escribir algo en la pantalla, podríamos usar el syscall write
, proporcionar la cadena que se va a imprimir y otros argumentos necesarios, y luego llamar al syscall para emitir la impresión.
Hay muchos syscalls disponibles proporcionados por el Kernel de Linux, y podemos encontrar una lista de ellos y el syscall number
de cada uno leyendo el archivo del sistema unistd_64.h
:
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
El archivo anterior establece el número de syscall para cada syscall para referirse a ese syscall utilizando este número.
Nota: Con procesadores 32-bit
x86, los números de syscall están en el archivo unistd_32.h
.
Practiquemos el uso de syscalls con el syscall write
que imprime en pantalla. No imprimiremos los números de Fibonacci todavía, sino que comenzaremos imprimiendo un mensaje de introducción, Fibonacci Sequence
, al principio de nuestro programa.
Syscall Function Arguments
Para usar el syscall write
, primero debemos saber qué argumentos acepta. Para encontrar los argumentos aceptados por un syscall, podemos usar el comando man -s 2
con el nombre del syscall de la lista anterior:
man -s 2 write
...SNIP...
ssize_t write(int fd, const void *buf, size_t count);
Como podemos ver en el resultado anterior, la función write
tiene la siguiente sintaxis:
ssize_t write(int fd, const void *buf, size_t count);
Vemos que la función syscall espera 3
argumentos:
- Descriptor de archivo
fd
al que se imprimirá (usualmente1
parastdout
) - El puntero a la dirección de la cadena que se imprimirá
- La longitud que queremos imprimir
Una vez que proporcionemos estos argumentos, podemos usar la instrucción syscall para ejecutar la función e imprimir en pantalla. Además de estos métodos manuales para localizar syscalls y argumentos de funciones, existen recursos en línea que podemos utilizar para buscar rápidamente syscalls, sus números y los argumentos que esperan, como esta tabla. Además, siempre podemos referirnos al código fuente de Linux
en Github.
Consejo: El flag -s 2
especifica las páginas man de syscall
. Podemos consultar man man
para ver varias secciones de cada página man.
Syscall Calling Convention
Ahora que entendemos cómo localizar varios syscalls y sus argumentos, comencemos a aprender cómo llamarlos. Para llamar a un syscall, debemos:
- Guardar los registros en la pila
- Establecer su número de syscall en
rax
- Configurar sus argumentos en los registros
- Usar la instrucción de ensamblado syscall para llamarlo
Generalmente deberíamos guardar cualquier registro que utilicemos en la pila antes de cualquier llamada a función o syscall.
Sin embargo, como estamos ejecutando este syscall al principio de nuestro programa antes de usar cualquier registro, no tenemos valores en los registros, por lo que no deberíamos preocuparnos por guardarlos. Discutiremos cómo guardar registros en la pila cuando lleguemos a Function Calls
.
Syscall Number
Comencemos moviendo el número del syscall al registro rax
. Como vimos antes, el syscall write
tiene el número 1
, por lo que podemos comenzar con el siguiente comando:
mov rax, 1
Ahora, si llegamos a la instrucción syscall, el Kernel sabría qué syscall estamos llamando.
Syscall Arguments
A continuación, debemos colocar cada uno de los argumentos de la función en su registro correspondiente. La convención de llamada de la arquitectura x86_64
especifica en qué registro debe colocarse cada argumento (por ejemplo, el primer argumento debe estar en rdi
). Todas las funciones y syscalls deben seguir este estándar y tomar sus argumentos de los registros correspondientes. Hemos discutido la siguiente tabla en la sección Registers
:
Descripción | Registro 64-bit | Registro 8-bit |
---|---|---|
Número de Syscall/Valor de retorno | rax |
al |
Guardado por el Callee | rbx |
bl |
1er argumento | rdi |
dil |
2do argumento | rsi |
sil |
3er argumento | rdx |
dl |
4to argumento | rcx |
cl |
5to argumento | r8 |
r8b |
6to argumento | r9 |
r9b |
Como podemos ver, tenemos un registro para cada uno de los primeros 6
argumentos. Cualquier argumento adicional puede almacenarse en la pila (aunque no muchos syscalls usan más de 6
argumentos).
Nota: rax
también se utiliza para almacenar el valor de retorno
de un syscall o función. Por lo tanto, si esperamos obtener un valor de un syscall/función, estará en rax
.
Con eso, deberíamos conocer nuestros argumentos y en qué registro debemos almacenarlos. Volviendo a la función syscall write
, debemos pasar: fd
, pointer
y length
. Podemos hacerlo de la siguiente manera:
rdi
->1
(para stdout)rsi
->'Fibonacci Sequence:\n'
(puntero a nuestra cadena)rdx
->20
(longitud de nuestra cadena)
Podemos usar mov rcx, 'string'
. Sin embargo, solo podemos almacenar hasta 16 caracteres en un registro (es decir, 64 bits), por lo que nuestra cadena de introducción no cabría. En su lugar, creemos una variable con nuestra cadena (como aprendimos en la sección Assembly File Structure
):
global _start
section .data
message db "Fibonacci Sequence:", 0x0a
Note cómo agregamos 0x0a
después de nuestra cadena, para añadir un carácter de nueva línea.
La etiqueta message
es un puntero a donde se almacenará nuestra cadena en la memoria. Por lo tanto, podemos usarla como nuestro segundo argumento. Así que, nuestro código final de syscall debería ser el siguiente:
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
Consejo: Si alguna vez necesitamos crear un puntero a un valor almacenado en un registro, podemos simplemente empujarlo a la pila, y luego usar el puntero rsp
para apuntar a él.
También podemos usar una variable length
calculada dinámicamente usando equ
, de manera similar a lo que hicimos con el programa Hello World
.
Calling Syscall
Ahora que tenemos nuestro número de syscall y argumentos en su lugar, lo único que queda es ejecutar la instrucción syscall. Así que, agreguemos una instrucción syscall y agreguemos las instrucciones al comienzo de nuestro código fib.s
, que debería verse así:
global _start
section .data
message db "Fibonacci Sequence:", 0x0a
section .text
_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
Ahora ensamblaremos nuestro código y lo ejecutaremos, para ver si nuestro mensaje introductorio se imprime:
./assembler.sh fib.s
Fibonacci Sequence:
[1] 107348 segmentation fault ./fib
Vemos que, efectivamente, nuestra cadena se imprime en la pantalla. Ejecutémoslo a través de gdb
, y hagamos un break en la syscall para ver cómo están configurados todos los argumentos antes de llamar a la syscall, de la siguiente manera:
$ 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:
"
$rdi : 0x1
gef➤ si
Secuencia de Fibonacci:
Vemos un par de cosas que esperábamos:
- Nuestros argumentos están correctamente configurados en los registros correspondientes antes de cada syscall.
- Un puntero a nuestro mensaje está cargado en
rsi
.
Ahora, hemos utilizado exitosamente la syscall write
para imprimir nuestro mensaje introductorio.
Exit Syscall
Finalmente, dado que hemos entendido cómo funcionan las syscalls, repasemos otra syscall esencial utilizada en los programas: Exit syscall
. Podemos haber notado que hasta ahora, cada vez que nuestro programa termina de ejecutarse, sale con un segmentation fault
, como vimos al ejecutar ./fib
. Esto se debe a que estamos finalizando nuestro programa abruptamente, sin pasar por el procedimiento adecuado de salida de programas en Linux, llamando a la exit syscall
y pasando un código de salida.
Así que, agreguemos esto al final de nuestro código. Primero, necesitamos encontrar el número de exit syscall
, de la siguiente manera:
grep exit /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#define __NR_exit 60
#define __NR_exit_group 231
Necesitamos usar el primero, con un número de syscall 60
. A continuación, veamos si la exit syscall
necesita algún argumento:
man -s 2 exit
...SNIP...
void _exit(int status);
Vemos que solo necesita un argumento entero, status
, que se explica como el código de salida. En Linux, cada vez que un programa termina sin errores, pasa un código de salida de 0
. De lo contrario, el código de salida es un número diferente, usualmente 1
. En nuestro caso, como todo salió según lo esperado, pasaremos el código de salida de 0
. Nuestro código de exit syscall
debería ser el siguiente:
mov rax, 60
mov rdi, 0
syscall
Ahora, agreguémoslo al final de nuestro código:
global _start
section .data
message db "Fibonacci Sequence:", 0x0a
section .text
_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
mov rax, 60
mov rdi, 0
syscall
Ahora podemos ensamblar nuestro código y ejecutarlo nuevamente:
./assembler.sh fib.s
Fibonacci Sequence:
¡Genial! Vemos que esta vez nuestro programa terminó correctamente sin un segmentation fault
. Podemos verificar el código de salida que fue pasado de la siguiente manera:
echo $?
0
Como se esperaba, el código de salida fue 0
, tal como especificamos en nuestra syscall.
Práctica: Para comprender completamente cómo funcionan las syscalls, intenta implementar la syscall write
para imprimir el número de Fibonacci, y colócala después de 'xchg rax, rbx
'.
Spoiler: No funcionará. Intenta descubrir por qué, y trata de arreglarlo para imprimir los primeros números de Fibonacci menores a 10
(pista: usa ASCII
).