¿Cómo funciona un linker? (III) – Tipos de símbolos

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.

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