En el proceso de convertir el código de un lenguaje de alto nivel a un ejecutable que entienda nuestro sistema, los linkers tienen un rol que muchas veces pasa desapercibido, pero que es importante de conocer. Cuando queremos convertir un archivo .c en un ejecutable entran en juego varias herramientas: primero el preprocesador de C (llamado cpp) genera un archivo intermedio, donde entre otras cosas se expanden las directivas #define y se borran los comentarios. Después, como hemos visto en anteriores entradas del blog, este archivo es leído por el compilador de C (cc1), y produce un archivo .s, que es código en ensamblador. Seguidamente el assembler (as) lee este .s y genera un .o, un “archivo de código objeto trasladable” (relocatable object file) que finalmente es procesado por el linker para crear un ejecutable que puede ser cargado en memoria.
En general el linker es el encargado de combinar diferentes archivos con código objeto en un único archivo, y para esto hay dos tareas fundamentales, la resolución de símbolos y el “traslado” (relocation). Por un lado, los archivos de código objeto referencian y usan símbolos, el objetivo de la resolución de símbolos es asociar cada referencia con una única definición del símbolo. Por otro lado, en la tarea de relocation, ya que los compiladores y assemblers generan código objeto en el que las secciones empiezan en la dirección 0, el linker traslada las secciones que empiezan en la dirección 0 asociando una dirección a cada definición de un símbolo y después haciendo referencia a esa dirección en cada referencia del símbolo.
Vamos a ver esto con un pequeño ejemplo:
$ cat test.c #include int main() { printf("hello world\n"); }
Compilamos para generar un relocatable object file
$ gcc -c -m32 test.c -o test.o $ readelf -S test.o There are 13 section headers, starting at offset 0x11c: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .text PROGBITS 00000000 000034 000017 00 AX 0 0 1 [ 2] .rel.text REL 00000000 0003e8 000010 08 11 1 4 [ 3] .data PROGBITS 00000000 00004b 000000 00 WA 0 0 1 [ 4] .bss NOBITS 00000000 00004b 000000 00 WA 0 0 1 [ 5] .rodata PROGBITS 00000000 00004b 00000c 00 A 0 0 1 [ 6] .comment PROGBITS 00000000 000057 00002c 01 MS 0 0 1 [ 7] .note.GNU-stack PROGBITS 00000000 000083 000000 00 0 0 1 [ 8] .eh_frame PROGBITS 00000000 000084 000038 00 A 0 0 4 [ 9] .rel.eh_frame REL 00000000 0003f8 000008 08 11 8 4 [10] .shstrtab STRTAB 00000000 0000bc 00005f 00 0 0 1 [11] .symtab SYMTAB 00000000 000324 0000b0 10 12 9 4 [12] .strtab STRTAB 00000000 0003d4 000012 00 0 0 1
Y podemos ver que todas las secciones empiezan en la dirección 0, así que vamos a seguir con la fase de linkado ( no hace falta que le pasemos al linker la biblioteca estándar de C porque se linka por defecto)
$ gcc -m32 test.o -o test $ readelf -S test There are 30 section headers, starting at offset 0x117c: Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [ 0] NULL 00000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4 [ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4 [ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4 [ 5] .dynsym DYNSYM 080481cc 0001cc 000050 10 A 6 1 4 [ 6] .dynstr STRTAB 0804821c 00021c 00004a 00 A 0 0 1 [ 7] .gnu.version VERSYM 08048266 000266 00000a 02 A 5 0 2 [ 8] .gnu.version_r VERNEED 08048270 000270 000020 00 A 6 1 4 [ 9] .rel.dyn REL 08048290 000290 000008 08 A 5 0 4 [10] .rel.plt REL 08048298 000298 000018 08 A 5 12 4 [11] .init PROGBITS 080482b0 0002b0 000023 00 AX 0 0 4 [12] .plt PROGBITS 080482e0 0002e0 000040 04 AX 0 0 16 [13] .text PROGBITS 08048320 000320 000192 00 AX 0 0 16 [14] .fini PROGBITS 080484b4 0004b4 000014 00 AX 0 0 4 [15] .rodata PROGBITS 080484c8 0004c8 000014 00 A 0 0 4 [16] .eh_frame_hdr PROGBITS 080484dc 0004dc 00002c 00 A 0 0 4 [17] .eh_frame PROGBITS 08048508 000508 0000b0 00 A 0 0 4 [18] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4 [19] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4 [20] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4 [21] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4 [22] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4 [23] .got.plt PROGBITS 0804a000 001000 000018 04 WA 0 0 4 [24] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4 [25] .bss NOBITS 0804a020 001020 000004 00 WA 0 0 1 [26] .comment PROGBITS 00000000 001020 000056 01 MS 0 0 1 [27] .shstrtab STRTAB 00000000 001076 000106 00 0 0 1 [28] .symtab SYMTAB 00000000 00162c 000430 10 29 45 4 [29] .strtab STRTAB 00000000 001a5c 00024f 00 0 0 1
Una vez el código objeto ha sido linkado, tenemos un ejecutable que puede ser copiado para su ejecución en memoria por el loader. Por otro lado el linker también se ha encargado de asignar una dirección al símbolo de printf antes no definido:
$ objdump -d test.o -j .text test.o: file format elf32-i386 Disassembly of section .text: 00000000 : 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 e4 f0 and $0xfffffff0,%esp 6: 83 ec 10 sub $0x10,%esp 9: c7 04 24 00 00 00 00 movl $0x0,(%esp) 10: e8 fc ff ff ff call 11 <main+0x11> 15: c9 leave 16: c3 ret
con una dirección válida, que hace referencia al símbolo de printf.
$ gdb -batch -ex 'file test' -ex 'disassemble main' Dump of assembler code for function main: 0x0804841d <+0>: push %ebp 0x0804841e <+1>: mov %esp,%ebp 0x08048420 <+3>: and $0xfffffff0,%esp 0x08048423 <+6>: sub $0x10,%esp 0x08048426 <+9>: movl $0x80484d0,(%esp) 0x0804842d <+16>: call 0x80482f0 <puts@plt> 0x08048432 <+21>: leave 0x08048433 <+22>: ret End of assembler dump.
En siguientes entregas hablaremos de cómo el linker organiza las secciones para resolver las dependencias y los tipos de símbolos que reconoce el linker para comprender mejor el ejemplo anterior.