¿Cómo funciona un linker? (I)

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.

Irene Díez
Acerca de
Investigadora de DT
Expertise: Operating systems, program analysis