¿Cómo funciona un linker? (II) – La tabla de símbolos

En entregas anteriores hablábamos de que los linkers son los encargados de combinar diferentes ficheros de código objeto trasladable en un ejecutable. Para ello tenían dos tareas fundamentales, la resolución de símbolos y el traslado; en esta entrega vamos a empezar a hablar de la resolución de símbolos, que recordemos que consiste en asociar cada referencia de un símbolo con su definición.

Para la resolución de símbolos al linker le interesa la sección symtab (symbol table), la tabla de símbolos. Esta tabla ha sido creada por el assembler, y contiene información sobre las referencias y definiciones de símbolos. Vamos a utilizar test2.c para ver los diferentes tipos de símbolos que se pueden definir y referenciar.

$ cat test2.c
static int add(int num1, int num2)
{
    return num1 + num2;
}

float subtract(float num1, float num2)
{
    return num1 - num2;
}

int multiply(int, int);

int main()
{
    int i = 1, j = 2;
    int k = add(i, j);
    float a = 2.0, b = 4.0;
    float c = substract(a, b);
    int x = 2, y = 2;
    int z = multiply(x, y);
}
$ gcc -c -m32 test2.c -o test2.o
$ readelf -S test2.o
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 0000a0 00  AX  0   0  1
  [ 2] .rel.text         REL             00000000 00028c 000020 08   I 11   1  4
  [ 3] .data             PROGBITS        00000000 0000d4 000000 00  WA  0   0  1
  [ 4] .bss              NOBITS          00000000 0000d4 000000 00  WA  0   0  1
  [ 5] .rodata           PROGBITS        00000000 0000d4 000008 00   A  0   0  4
  [ 6] .comment          PROGBITS        00000000 0000dc 000035 01  MS  0   0  1
  [ 7] .note.GNU-stack   PROGBITS        00000000 000111 000000 00      0   0  1
  [ 8] .eh_frame         PROGBITS        00000000 000114 000084 00   A  0   0  4
  [ 9] .rel.eh_frame     REL             00000000 0002ac 000018 08   I 11   8  4
  [10] .shstrtab         STRTAB          00000000 0002c4 00005f 00      0   0  1
  [11] .symtab           SYMTAB          00000000 000198 0000d0 10     12  10  4
  [12] .strtab           STRTAB          00000000 000268 000024 00      0   0  1

La sección 11 (.symtab) es la que nos interesa, vamos a ver su contenido.

$ readelf -s test2.o
Symbol table '.symtab' contains 13 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS test2.c
     2: 00000000     0 SECTION LOCAL  DEFAULT    1 
     3: 00000000     0 SECTION LOCAL  DEFAULT    3 
     4: 00000000     0 SECTION LOCAL  DEFAULT    4 
     5: 00000000    13 FUNC    LOCAL  DEFAULT    1 add
     6: 00000000     0 SECTION LOCAL  DEFAULT    5 
     7: 00000000     0 SECTION LOCAL  DEFAULT    7 
     8: 00000000     0 SECTION LOCAL  DEFAULT    8 
     9: 00000000     0 SECTION LOCAL  DEFAULT    6 
    10: 0000000d    11 FUNC    GLOBAL DEFAULT    1 subtract
    11: 00000018   136 FUNC    GLOBAL DEFAULT    1 main
    12: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND multiply

La tabla de símbolos es una estructura que define el nombre, valor, tamaño, tipo, atributos de binding, visibilidad y sección a la que pertenece cada símbolo. Los símbolos con nombre tienen una entrada en ‘Name’, que representa un índice en la tabla de strings (sección 12, .strtab). readelf nos da el trabajo hecho y nos pone directamente el nombre del símbolo en vez de tener que buscarlo nosotros en la tabla de strings. De todas formas vamos a ver el contenido de la tabla de símbolos y la tabla de strings en hexadecimal.

$ readelf -x .symtab test2.o
Hex dump of section '.symtab':
  0x00000000 00000000 00000000 00000000 00000000 ................
  0x00000010 01000000 00000000 00000000 0400f1ff ................
  0x00000020 00000000 00000000 00000000 03000100 ................
  0x00000030 00000000 00000000 00000000 03000300 ................
  0x00000040 00000000 00000000 00000000 03000400 ................
->0x00000050 09000000 00000000 0d000000 02000100 ................
  0x00000060 00000000 00000000 00000000 03000500 ................
  0x00000070 00000000 00000000 00000000 03000700 ................
  0x00000080 00000000 00000000 00000000 03000800 ................
  0x00000090 00000000 00000000 00000000 03000600 ................
  0x000000a0 0d000000 0d000000 0b000000 12000100 ................
  0x000000b0 16000000 18000000 88000000 12000100 ................
  0x000000c0 1b000000 00000000 00000000 10000000 ................

Nos vamos a fijar en la entrada 5. Los primeros 8 bytes (0x09000000) son el índice de la tabla de strings que tenemos que mirar, en este caso el 9.

$ readelf -x .strtab test2.o
Hex dump of section '.strtab':
  0x00000000 00746573 74322e63 00616464 00737562 .test2.c.add.sub
  0x00000010 74726163 74006d61 696e006d 756c7469 tract.main.multi
  0x00000020 706c7900                            ply.

El campo ‘Type’ denota el tipo de símbolo, los valores posibles son NOTYPE: no especificado, OBJECT: variable, array etc. datos en general, FUNC: función o código ejecutable, FILE: nos dice el nombre del archivo del código fuente asociado con el código objeto en el que está esta tabla de símbolos, en nuestro caso test2.c, SECTION: denota que el símbolo está asociado con una sección (nos preocuparemos de esto cuando hablemos de la fase de relocation en siguientes entradas del blog), además existen varios valores reservados. En nuestro ejemplo ‘add’, ‘subtract’ y ‘main’ son definidos como funciones, como ‘multiply’ no está definido en este archivo, y por tanto el linker no sabe lo que es, tiene un valor de NOTYPE.

El siguiente campo interesante de la tabla de símbolos es ‘Ndx’, que nos dice a qué sección está asociado un símbolo. Por ejemplo, el símbolo 5 ‘add’ pertenece a la sección 1, que es .text. Si miramos nuestro código, ‘add’ es una función, y por tanto tiene sentido que pertenezca a la sección de código.

Si nos fijamos en el símbolo 12, ‘multiply’; la tabla nos dice que no está definido en ninguna sección (UND), ya que efectivamente no lo hemos definido en test2.c y será el linker el encargado de buscar una definición. Si tratáramos de compilar el programa en este estado el linker se quejaría diciéndonos que la referencia ‘multiply’ no está definida.

‘Bind’ se refiere al tipo de binding (atadura u adhesión) que tiene el símbolo, esto afecta a su visibilidad y qué pasará en la fase de relocation. Los símbolos pueden ser globales (GLOBAL), locales (LOCAL) o débiles (WEAK). En nuestro ejemplo hemos definido la función ‘add’ con un ‘static’ por delante, que en C se utiliza para que esa función solamente sea visible dentro del archivo en el que está definida (utilizaríamos los modificadores public y private en C++), por tanto, en la tabla de símbolos tendrá un binding LOCAL. Por otro lado, tanto ‘main’, ‘subtract’ y ‘multiply’ tienen binding GLOBAL, y son visibles por todos los archivos que linkemos de forma conjunta. Finalmente, un símbolo WEAK es equivalente a un GLOBAL, solo que tiene una prioridad menor que los GLOBAL. Estas prioridades serán necesarias para saber qué hacer cuando tengamos varios símbolos definidos con un mismo nombre.

En la siguiente entrada de esta serie hablaremos de cómo se resuelven los símbolos con la tabla de símbolos. Consultar “Executable and Linkable Format (ELF)” para mas detalles de la tabla de símbolos.

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