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.