StellatorOS: Compilación, linkado, gestión de la pila y variables en C


La programación en ensamblador nos permite un control absoluto sobre el hardware, pero avanzar en este lenguaje es un proceso muy lento además nuestro código es tan específico para la CPU que estemos utilizando que lo hace poco portable a otras arquitecturas. Por ese motivo utilizaremos C para la mayoría de código de nuestro SO, en este artículo veremos como C gestiona la pila y la definición de variables.

 

Antes de comenzar es recomendable que leas estos artículos anteriores:

En mi caso estoy utilizando FreeBSD, el compilador es el siguiente:

cc -v
FreeBSD clang version 8.0.1 (tags/RELEASE_801/final 366581) (based on LLVM 8.0.1)
Target: x86_64-unknown-freebsd12.1
Thread model: posix
InstalledDir: /usr/bin

La mejor forma de entender como funciona un compilador es compilando un pequeño programa y viendo el resultado:

vi basic.c
int my_function () {
 return 0xbaba ;
}

Compilamos el fichero objeto, este contendrá las etiquetas textuales:

cc -ffreestanding -c basic.c -o basic.o

Dumpeamos el código ensamblador del objeto:

objdump -M intel -d basic.o
basic.o:     file format elf64-x86-64-freebsd
Disassembly of section .text:
0000000000000000 <my_function>:
   0: 55                    push   rbp
   1: 48 89 e5              mov    rbp,rsp
   4: b8 ba ba 00 00        mov    eax,0xbaba
   9: 5d                    pop    rbp
   a: c3                    ret

Como podemos ver los pasos a ejecutar son:

  • Guardar el valor base de la pila de la función anterior
  • Ajustar nuestra base a la cima actual de la pila, de este modo estamos creando un stack frame nuevo para nuestra función
  • Movemos el valor 0xbaba al registro ax que es donde espera el resultado la función superior
  • Restauramos el valor de la base de la pila de la función anterior
  • Retornamos

Para generar un binario con todos los ficheros objeto necesitaremos un linker, este convertirá todas las direcciones de memoria relativa de los ficheros objeto a direcciones absolutas.

Por ejemplo call <etiqueta_X> será traducido a call 0x12345, donde 0x12345 es el offset dentro del binario donde el linker decidió poner el código de la rutina con la etiqueta_X, las etiquetas textuales desaparecen.

Linkamos los ficheros objeto formando el binario final:

ld -o basic.bin -Ttext 0x0 --oformat binary basic.o

El linker puede generar la salida en varios formatos, según el formato de salida el binario conservará meta información sobre como debe cargarse en memoria, información sobre etiquetas textuales para ayudar en el debugging del software. 

Mediante la opción -Ttext 0x0 estamos haciendo que todas las direcciones tengan como base la indicada por tal parámetro, es el equivalente a org que ya vimos en ensamblador. Por ahora esto no tiene mucha importancia pero si que la tendrá cuando carguemos nuestro kernel desde el bootloader.

Un binario puede ser desensamblado a código ensamblador, el único inconveniente de obtener el código ensamblador desde el binario es que algunas partes son datos y se mostrarán como instrucciones ASM, en nuestro código de ejemplo no tendremos ese problema.

ndisasm -b 32 basic.bin > basic.dis
cat basic.dis
00000000  55                push ebp
00000001  48                dec eax
00000002  89E5              mov ebp,esp
00000004  B8BABA0000        mov eax,0xbaba
00000009  5D                pop ebp
0000000A  C3                ret
0000000B  0000              add [eax],al
0000000D  0000              add [eax],al
0000000F  00                db 0x00

Básicamente es el mismo código que habiamos obtenido del fichero objeto, el linkado ha añadido algunos pasos extra, para los mas curiosos decir que la primera columna(00000000) mostrada es el offset donde residirán las instrucciones en memoria.

Pongamos ahora un ejemplo con variables locales a la función:

vi local_var.c
int my_function () {
 int my_var = 0xbaba ;
 return my_var ;
}
cc -ffreestanding -c local_var.c -o local_var.o
objdump -M intel -d local_var.o
local_var.o:     file format elf64-x86-64-freebsd
Disassembly of section .text:
0000000000000000 <my_function>:
   0: 55                    push   rbp
   1: 48 89 e5              mov    rbp,rsp
   4: c7 45 fc ba ba 00 00  mov    DWORD PTR [rbp-0x4],0xbaba
   b: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
   e: 5d                    pop    rbp
   f: c3                    ret    

El proceso es muy similar al anterior:

  • Guardamos el bp anterior
  • Ajustamos el nuevo bp a sp
  • Movemos el valor 0xbaba a la posición de memoria bp-4(la pila crece hacia abajo)
  • Guardamos el valor de retorno en el registro ax
  • Restauramos el bp anterior
  • Return

Para debugearlo de forma mas interactiva podemos programar un código completo con su función main() y cargarlo en GDB-gef:

vi test.c
int my_function();

int main(int argc, char *argv[])
{
 my_function();
}

int my_function() {
 int my_var = 0xbaba;
 return my_var;
}
cc -g test.c -o test
gdb -q ./test
GEF for freebsd ready, type `gef' to start, `gef config' to configure
80 commands loaded for GDB 9.2 [GDB v9.2 for FreeBSD] using Python engine 3.7
Reading symbols from ./test...

Veamos el código fuente en C:

gef➤  l
1 int main(int argc, char *argv[])
2 {
3  my_function();
4 }
5 
6 int my_function() {
7  int my_var = 0xbaba;
8  return my_var;
9 }
10

Veamos el código ASM de nuestra función:

gef➤  disassemble my_function
Dump of assembler code for function my_function:
   0x0000000000201310 <+0>: push   rbp
   0x0000000000201311 <+1>: mov    rbp,rsp
   0x0000000000201314 <+4>: mov    DWORD PTR [rbp-0x4],0xbaba
   0x000000000020131b <+11>: mov    eax,DWORD PTR [rbp-0x4]
   0x000000000020131e <+14>: pop    rbp
   0x000000000020131f <+15>: ret    
End of assembler dump.

Ponemos un breakpoint justo antes de llamar a nuestra función, línea 3 en el código en C:

gef➤  break 3

Corremos el programa:

gef➤  r

Veamos en ASM en que punto del código estamos:

gef➤  disassemble 
Dump of assembler code for function main:
   0x00000000002012e0 <+0>: push   rbp
   0x00000000002012e1 <+1>: mov    rbp,rsp
   0x00000000002012e4 <+4>: sub    rsp,0x20
   0x00000000002012e8 <+8>: mov    DWORD PTR [rbp-0x4],edi
   0x00000000002012eb <+11>: mov    QWORD PTR [rbp-0x10],rsi
=> 0x00000000002012ef <+15>: call   0x201310 <my_function>
   0x00000000002012f4 <+20>: xor    edi,edi
   0x00000000002012f6 <+22>: mov    DWORD PTR [rbp-0x14],eax
   0x00000000002012f9 <+25>: mov    eax,edi
   0x00000000002012fb <+27>: add    rsp,0x20
   0x00000000002012ff <+31>: pop    rbp
   0x0000000000201300 <+32>: ret    
End of assembler dump.

Ejecutamos un paso en ASM:

=> 0x00000000002012ef <+15>: call   0x201310 <my_function>
gef➤  si

Consultamos el valor de bp y sp:

gef➤  registers $rbp $rsp
$rsp   : 0x7fffffffe0e8    
$rbp   : 0x7fffffffe110 

Consultamos los valores en memoria cerca de sp:

gef➤  x/10x $rsp-10
0x7fffffffe0de: 0x00000000 0x00000000 0x12f40000 0x00000020
0x7fffffffe0ee: 0xe1800000 0x7fffffff 0x00000000 0x00000000
0x7fffffffe0fe: 0xe1880000 0x7fffffff

Damos un paso mas:

 →   0x201310 <my_function+0>  push   rbp
gef➤  si

Volvemos a consultar los valores cercanos a sp:

gef➤  x/10x $rsp-10
0x7fffffffe0d6: 0x3e020000 0x0008003e 0xe1100000 0x7fffffff
0x7fffffffe0e6: 0x12f40000 0x00000020 0xe1800000 0x7fffffff
0x7fffffffe0f6: 0x00000000 0x00000000

Podemos ver el valor de bp en la RAM:

$rbp   : 0x7fffffffe110  -> 0xe1100000 0x7fffffff

El sp habrá decrementado:

gef➤  registers $rsp
$rsp   : 0x7fffffffe0e0    

La próxima instrucción es crear el stack frame de nuestra función, consultamos el valor de los registros bp/sp:

gef➤  registers $rbp $rsp
$rsp   : 0x7fffffffe0e0    
$rbp   : 0x7fffffffe110   

Ejecutamos un paso:

=> 0x0000000000201311 <+1>: mov    rbp,rsp
gef➤  si

Consultamos el valor de los registros bp/sp:

gef➤  registers $rbp $rsp
$rsp   : 0x7fffffffe0e0    
$rbp   : 0x7fffffffe0e0   

La próxima instrucción es meter la variable en la pila, posición de memoria bp-4:

=> 0x0000000000201314 <+4>: mov    DWORD PTR [rbp-0x4],0xbaba

Como vemos no utiliza sp si no que calcula las posiciones restándoles lo que ocupan las variables al valor de bp, de esto podemos extraer varias conclusiones muy interesantes:

  • sp solo se utiliza para ajustar el valor de bp cuando definimos el stack frame
  • sp no cambia de valor cuando se definen variables, solo cuando se pushean/popean

Según el compilador o la versión de este puede que el código en ASM varíe, hay ocasiones en las que se decrementa el sp por temas de alineamiento de memoria de este modo se desperdicia parte de la RAM pero se accede al dato con un solo acceso, hay que elegir entre velocidad u optimización del uso de la RAM.

Antes de ejecutar la orden consultamos el valor de esa posición de memoria:

gef➤  x/4x $rbp-4
0x7fffffffe0dc: 0x00000008 0xffffe110 0x00007fff 0x002012f4

Definimos la variable en RAM:

=> 0x0000000000201314 <+4>: mov    DWORD PTR [rbp-0x4],0xbaba
gef➤  si

Comprobamos que se haya guardado:

gef➤  x/4x $rbp-4
0x7fffffffe0dc: 0x0000baba 0xffffe110 0x00007fff 0x002012f4

Correcto "baba" aparece en la posición adecuada.

La próxima instrucción es copiar "baba" al registro eax a modo de valor de retorno. consultamos el valor actual:

gef➤  registers $rax
$rax   : 0x0000000000203028  →  0x0000000000000001  →  0x0000000000000001

Ejecutamos la instrucción:

=> 0x000000000020131b <+11>: mov    eax,DWORD PTR [rbp-0x4]
gef➤  si

Volvemos a consultarlo:

gef➤  registers $rax
$rax   : 0xbaba        

La próxima instrucción es recuperar el valor pusheado anteriormente antes de volver a la función main, recordad que antes de retornar hay que dejar el stack frame tal como estaba antes de llamar a la función, habrá que ajustar tanto sp como bp a los valores originales, en nuestro caso sp no se ha tocado tan solo lo hacemos con bp.

Consultamos el estado de bp:

gef➤  registers $rbp
$rbp   : 0x7fffffffe0e0    

Ejecutamos la instrucción:

     0x20131e <my_function+14> pop    rbp
gef➤  si

Consultamos el estado de bp:

gef➤  registers $rbp
$rbp   : 0x7fffffffe110    

Correcto, ese era el valor de bp justo antes de ejecutar la función.

Si te ha gustado el artículo puedes invitarme a un redbull aquí.
Autor: kr0m -- 15/06/2020 02:09:39