Tras aprender qué información contiene la tabla de símbolos en la entrega anterior, ahora podemos hablar más detenidamente de los atributos que puede tener un símbolo y qué papel tienen en la resolución de símbolos desde el punto de vista del linker. Recordemos que los símbolos pueden ser globales (global), locales (local) o débiles (weak). Los globales son las funciones definidas sin static además de las variables inicializadas definidas sin static. Un símbolo local es aquel definido con un static, y finalmente los símbolos débiles son las variables globales no inicializadas.
Utilizando la información que nos dan las tablas de símbolos de los diferentes ficheros a convertir en un ejecutable, el linker trata de asociar cada referencia con una única definición de símbolo. Siguiendo el ejemplo que vimos en la entrega anterior, nuestro programa test2.c tenía una función ‘multiply’ que aparecía en la tabla de símbolos como símbolo global y no definido:
$ readelf -s test2.o Symbol table '.symtab' contains 13 entries: Num: Value Size Type Bind Vis Ndx Name ... 12: 00000000 0 NOTYPE GLOBAL DEFAULT UND multiply
Para definir este símbolo vamos a implementar la función multiply.
$ cat test2-multiply.c int multiply(int x, int y) { return x * y; }
Si inspeccionamos la tabla de símbolos de test2-multiply.o veremos que tenemos el símbolo de la función que le falta a test2.o
$ readelf -s test2-multiply.o Symbol table '.symtab' contains 9 entries: Num: Value Size Type Bind Vis Ndx Name 0: 00000000 0 NOTYPE LOCAL DEFAULT UND 1: 00000000 0 FILE LOCAL DEFAULT ABS test2-multiply.c 2: 00000000 0 SECTION LOCAL DEFAULT 1 3: 00000000 0 SECTION LOCAL DEFAULT 2 4: 00000000 0 SECTION LOCAL DEFAULT 3 5: 00000000 0 SECTION LOCAL DEFAULT 5 6: 00000000 0 SECTION LOCAL DEFAULT 6 7: 00000000 0 SECTION LOCAL DEFAULT 4 8: 00000000 12 FUNC GLOBAL DEFAULT 1 multiply
Ahora ya podríamos generar un ejecutable en el que todos los símbolos estuvieran resueltos:
$ gcc -m32 test2.o test2-multiply.o -o program $ readelf -s program ... Symbol table '.symtab' contains 72 entries: Num: Value Size Type Bind Vis Ndx Name ... 37: 00000000 0 FILE LOCAL DEFAULT ABS test2.c 38: 080483db 13 FUNC LOCAL DEFAULT 14 add 39: 00000000 0 FILE LOCAL DEFAULT ABS test2-multiply.c ... 53: 0804847b 12 FUNC GLOBAL DEFAULT 14 multiply ... 66: 080483f3 136 FUNC GLOBAL DEFAULT 14 main 67: 080483e8 11 FUNC GLOBAL DEFAULT 14 subtract ...
El linker ha utilizado la tabla de símbolos para dar sentido al símbolo global ‘multiply’ que antes no estaba definido. Sin embargo no siempre el proceso de resolución de símbolos es tan obvio, ya que por ejemplo puede haber varios símbolos globales con el mismo nombre. Para ello existen dos prioridades que pueden tener los símbolos, fuerte (strong) y débil (weak) que son dadas por el assembler.
Teniendo en cuenta esto, el linker tiene en cuenta que: es un error si hay dos símbolos fuertes iguales, tiene más prioridad un símbolo fuerte frente a los débiles, y entre varios símbolos débiles iguales no hay una norma sobre cual elegir. Por ejemplo, si cambiamos test2-multiply.c de esta forma:
$ cat test2-multiply-modified.c int multiply(int x, int y) { return x * y; } float subtract(float x, float y) { return y - x; }
Al tratar de generar el ejecutable final el linker se quejaría diciendo que hay múltiples definiciones de ‘subtract’, ya que el linker no puede elegir entre dos símbolos fuertes iguales. Una de los fallos que suele costar más encontrar es cuando tenemos un error en nuestro programa porque dos variables globales se están sobrescribiendo:
$ cat strongvsweak-1.c #include int x; int main() { printf("%d\n", x); } $ cat strongvsweak-2.c int x = 100;
En este caso nuestro programa imprimiría 100, ya que las variables globales inicializadas son fuertes y las no inicializadas débiles, y por tanto el linker da prioridad a int x = 100 en vez del valor no inicializado (que podría ser cualquier cosa). Podemos tener otro fallo sútil similar cuando hay varias definiciones débiles sobre las que el linker tiene que elegir. Para evitar esto, podemos hacer que el compilador nos avise como si fuera un error cuando se están sobrescribiendo definiciones utilizando la opción -fno-common de GCC.