Functions
Ya deberíamos entender las diferentes instrucciones de bifurcación y control utilizadas para manejar el flujo de ejecución del programa. También deberíamos tener un buen conocimiento sobre procedimientos y llamadas, y cómo utilizarlos para la bifurcación. Así que ahora enfoquémonos en llamar funciones.
Functions Calling Convention
Las funciones son una forma de procedures
. Sin embargo, las funciones tienden a ser más complejas y se espera que utilicen completamente la pila y todos los registros. Por lo tanto, no podemos simplemente llamar a una función como lo hicimos con los procedimientos. En su lugar, las funciones tienen un Calling Convention
para configurarse correctamente antes de ser llamadas.
Hay cuatro cosas principales que debemos considerar antes de llamar a una función:
Save Registers
en la pila (Caller Saved
)- Pasar
Function Arguments
(como syscalls) - Ajustar el
Stack Alignment
- Obtener el
Return Value
de la función (enrax
)
Esto es relativamente similar a llamar a un syscall, y la única diferencia con los syscalls es que tenemos que almacenar el número de syscall en rax
, mientras que podemos llamar funciones directamente con call function
. Además, con syscall no tenemos que preocuparnos por el Stack Alignment
.
Writing Functions
Todos los puntos anteriores son desde el punto de vista del caller
, es decir, quien llama a una función. Cuando se trata de escribir una función, hay diferentes puntos a considerar, que son:
- Guardar los registros
Callee Saved
(rbx
yrbp
) - Obtener argumentos desde los registros
- Alinear la pila
- Devolver el valor en
rax
Como podemos ver, estos puntos son relativamente similares a los puntos del caller
. El caller
está configurando las cosas, y luego el callee
(es decir, el receptor) debe recuperarlas y utilizarlas. Estos puntos generalmente se realizan al principio y al final de la función y se llaman prologue
y epilogue
de una función. Permiten que las funciones sean llamadas sin preocuparse por el estado actual de la pila o los registros.
En este módulo, solo estaremos llamando a otras funciones,
por lo que solo tenemos que centrarnos en configurar una llamada de función y no entraremos en escribir funciones.
Using External Functions
Queremos imprimir el número actual de Fibonacci en cada iteración del bucle loopFib
. Anteriormente, no pudimos usar un syscall write
ya que solo acepta caracteres ASCII
. Habríamos tenido que convertir nuestro número de Fibonacci a ASCII
, lo cual es un poco complicado.
Afortunadamente, hay funciones externas que podemos usar para imprimir el número actual sin tener que convertirlo. La biblioteca de funciones libc
utilizada para programas en C
proporciona muchas funcionalidades que podemos utilizar sin reescribir todo desde cero. La función printf
en libc
acepta el formato de impresión, por lo que podemos pasarle el número actual de Fibonacci y decirle que lo imprima como un entero, y hará la conversión automáticamente. Antes de poder usar una función de libc
, primero tenemos que importarla y luego especificar la biblioteca libc
para el enlace dinámico al enlazar nuestro código con ld
.
Importing libc Functions
Primero, para importar una función externa de libc
, podemos usar la instrucción extern
al comienzo de nuestro código, de la siguiente manera:
global _start
extern printf
Una vez hecho esto, deberíamos poder llamar a la función printf
. Así que podemos proceder con el Functions Calling Convention
que discutimos anteriormente.
Saving Registers
Definamos un nuevo procedimiento, printFib
, para contener nuestras instrucciones de llamada a función. El primer paso es guardar en la pila cualquier registro que estemos utilizando, que son rax
y rbx
, de la siguiente manera:
printFib:
push rax ; push registers to stack
push rbx
; function call
pop rbx ; restore registers from stack
pop rax
ret
Así que podemos proceder con el segundo punto, y pasar los argumentos requeridos a printf
.
Function Arguments
Ya hemos discutido cómo pasar argumentos de función en la sección de syscalls. El mismo proceso se aplica a los argumentos de función.
Primero, necesitamos averiguar qué argumentos acepta la función printf
usando man -s 3
para library functions manual
(como podemos ver en man man
):
man -s 3 printf
...SNIP...
int printf(const char *format, ...);
Como podemos ver, la función toma un puntero al formato de impresión (mostrado con un *
), y luego las cadenas a imprimir.
Primero, podemos crear una variable que contenga el formato de salida para pasarla como primer argumento. La página del manual de printf
también detalla varios formatos de impresión. Queremos imprimir un entero, por lo que podemos usar el formato %d
, de la siguiente manera:
global _start
extern printf
section .data
message db "Fibonacci Sequence:", 0x0a
outFormat db "%d", 0x0a, 0x00
Nota: Terminamos el formato con un carácter nulo 0x00
, ya que este es el terminador de cadena en printf
, y debemos terminar cualquier cadena con él.
Este puede ser nuestro primer argumento, y rbx
como nuestro segundo argumento, que printf
colocará como %d
. Así que movamos ambos argumentos a sus respectivos registros, de la siguiente manera:
printFib:
push rax ; push registers to stack
push rbx
mov rdi, outFormat ; set 1st argument (Print Format)
mov rsi, rbx ; set 2nd argument (Fib Number)
pop rbx ; restore registers from stack
pop rax
ret
Stack Alignment
Siempre que queramos hacer un call
a una función, debemos asegurarnos de que el Top Stack Pointer (rsp)
esté alineado en un límite de 16-byte
desde la pila de la función _start
.
Esto significa que debemos agregar al menos 16 bytes (o un múltiplo de 16 bytes) a la pila antes de hacer una llamada para garantizar que las funciones tengan suficiente espacio en la pila para ejecutarse correctamente. Este requisito existe principalmente para la eficiencia del rendimiento del procesador. Algunas funciones (como en libc
) están programadas para fallar si este límite no está corregido, con el fin de garantizar la eficiencia en el rendimiento. Si ensamblamos nuestro código y hacemos un break justo después del segundo push
, esto es lo que veremos:
───────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe3a0│+0x0000: 0x0000000000000001 ← $rsp
0x00007fffffffe3a8│+0x0008: 0x0000000000000000
0x00007fffffffe3b0│+0x0010: 0x00000000004010ad → <loopFib+5> add rax, rbx
0x00007fffffffe3b8│+0x0018: 0x0000000000401044 → <_start+20> call 0x4010bd <Exit>
0x00007fffffffe3c0│+0x0020: 0x0000000000000001 ← $r13
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401090 <initFib+9> ret
0x401091 <printFib+0> push rax
0x401092 <printFib+1> push rbx
→ 0x40100e <printFib+2> movabs rdi, 0x403039
Vemos que tenemos cuatro bloques de 8 bytes empujados a la pila, creando un límite total de 32 bytes. Esto se debe a dos cosas:
- Cada procedimiento
call
agrega una dirección de 8 bytes a la pila, la cual luego es eliminada conret
. - Cada
push
también agrega 8 bytes a la pila.
Así que estamos dentro de printFib
y dentro de loopFib
, y hemos empujado rax
y rbx
, para un total de un límite de 32 bytes. Dado que el límite es un múltiplo de 16, nuestra pila ya está alineada y no tenemos que corregir nada.
Si estuviéramos en un caso donde quisiéramos aumentar el límite a 16, podemos restar bytes de rsp
, de la siguiente manera:
sub rsp, 16
call function
add rsp, 16
De esta forma, estamos agregando 16 bytes adicionales en la parte superior de la pila y luego eliminándolos después de la llamada. Si tuviéramos 8 bytes empujados, podemos aumentar el límite a 16 restando 8 de rsp
.
Esto puede ser un poco confuso, pero lo más importante que debemos recordar es que debemos tener 16 bytes (o un múltiplo de 16) en la parte superior de la pila antes de hacer una llamada.
Podemos contar el número de instrucciones push
(no pop
ed) y las instrucciones call
(no ret
ornadas), y obtendremos cuántos bloques de 8 bytes se han empujado a la pila.
Function Call
Finalmente, podemos emitir un call printf
, y debería imprimir el número Fibonacci actual en el formato especificado, de la siguiente manera:
printFib:
push rax ; push registers to stack
push rbx
mov rdi, outFormat ; set 1st argument (Print Format)
mov rsi, rbx ; set 2nd argument (Fib Number)
call printf ; printf(outFormat, rbx)
pop rbx ; restore registers from stack
pop rax
ret
Ahora deberíamos tener nuestro procedimiento printFib
listo. Así que podemos agregarlo al inicio de loopFib
, de modo que imprima el número Fibonacci actual al comienzo de cada bucle:
loopFib:
call printFib ; print current Fib number
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
ret
Nuestro código final fib.s
debería ser el siguiente:
global _start
extern printf
section .data
message db "Fibonacci Sequence:", 0x0a
outFormat db "%d", 0x0a, 0x00
section .text
_start:
call printMessage ; print intro message
call initFib ; set initial Fib values
call loopFib ; calculate Fib numbers
call Exit ; Exit the program
printMessage:
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
ret
initFib:
xor rax, rax ; initialize rax to 0
xor rbx, rbx ; initialize rbx to 0
inc rbx ; increment rbx to 1
ret
printFib:
push rax ; push registers to stack
push rbx
mov rdi, outFormat ; set 1st argument (Print Format)
mov rsi, rbx ; set 2nd argument (Fib Number)
call printf ; printf(outFormat, rbx)
pop rbx ; restore registers from stack
pop rax
ret
loopFib:
call printFib ; print current Fib number
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
ret
Exit:
mov rax, 60
mov rdi, 0
syscall
Dynamic Linker
Ahora podemos ensamblar nuestro código con nasm
. Cuando enlacemos nuestro código con ld
, debemos indicarle que realice un enlace dinámico con la biblioteca libc
. De lo contrario, no sabría cómo obtener la función printf
importada. Podemos hacerlo con las flags -lc --dynamic-linker /lib64/ld-linux-x86-64.so.2
, de la siguiente manera:
nasm -f elf64 fib.s && ld fib.o -o fib -lc --dynamic-linker /lib64/ld-linux-x86-64.so.2 && ./fib
1
1
2
3
5
8
Como podemos ver, printf
facilitó mucho la impresión de nuestro número Fibonacci sin preocuparnos por convertirlo al formato adecuado, como teníamos que hacerlo con la syscall write
. A continuación, necesitamos revisar otro ejemplo sobre el uso de funciones externas de libc
para entender cómo llamar a funciones externas correctamente.