Principio de vinculación de programas

Este artículo presenta brevemente el principio de vinculación del programa. Aprender el principio de vinculación ayuda a los programadores a comprender la naturaleza del programa y también puede sentar una base sólida para el futuro desarrollo de código de software a gran escala. Comprender el principio de enlace nos ayuda a resolver algunos problemas inexplicables en el desarrollo diario.

En pocas palabras, vincular es el proceso de recopilar varios códigos y datos parciales en un proyecto y combinarlos en un solo archivo ejecutable . El archivo combinado se puede cargar en la memoria para su ejecución.

La vinculación puede ocurrir en tres situaciones:

1. Tiempo de compilación: cuando el código fuente se traduce a código de máquina

2. Al cargar: cuando el programa se carga en la memoria y se ejecuta

3. Tiempo de ejecución: cuando se ejecuta la aplicación.

1. Enlace estático

1.1 Proceso de compilación del programa

//示例程序1

/* /code/link/main.c */
void swap();

int buf[2] = {1, 2};

int main()
{
    swap();
    return 0;
}

/* /code/link/swap.c */
extern int buf[];

int *bufp0 = &buf[0];
int *bufp1;

void swap()
{
    int temp;
    
    bufp1 = &buf[1];
    temp = *bufp0;
    *bufp0 = *bufp1;
    *bufp1 = temp;
}

Lo anterior es un programa simple de intercambio de dos números. El proceso de generación de un archivo de destino ejecutable es el siguiente:

Preprocesador de lenguaje C (cpp): traduce el programa fuente de lenguaje C *.c en un archivo intermedio de código ASCII *.i

Compilador de c (ccl): traduce *.i a un archivo de lenguaje ensamblador de código ASCII *.s

Ensamblador (as): traduce *.s en un archivo objeto reubicable*.o

Finalmente, el programa vinculador ld combina todos los archivos *.o y algunos archivos del sistema necesarios para crear un archivo objeto ejecutable.

 

 

1.2 Tareas del enlazador

El vinculador vincula varios archivos objeto en un archivo objeto completo, cargable y ejecutable. Su entrada es un conjunto de archivos de destino reubicables. Las dos tareas principales del enlace son las siguientes:

1. Resolución de símbolos : vincule referencias de símbolos y definiciones de símbolos en el archivo de destino. Cada función y cada variable pueden considerarse como un símbolo, y cada símbolo en el archivo objeto está asociado con la definición del símbolo.

2. Reubicación : el vinculador asocia la definición de cada símbolo con una ubicación de memoria (RAM) específica y luego modifica todas las referencias a estos símbolos para que todos apunten a esta ubicación de memoria.

1.3 Archivo de destino

​Tres formas de archivos de destino:

1. Archivos de destino reubicables

     Este tipo de archivo contiene código binario y datos que se han compilado y convertido en código y datos de instrucciones de máquina, pero que no se pueden ejecutar directamente. Debido a que estas instrucciones y datos a menudo hacen referencia a símbolos en otros módulos (archivos objeto), este módulo desconoce los símbolos de estos otros módulos. La resolución de estos símbolos requiere que el vinculador vincule todos los módulos. Esta operación se llama reubicación, por lo que este archivo de destino se denomina "archivo de destino reubicable" y el sufijo suele ser *.o
 

 2. Archivo de destino ejecutable

 Estos archivos también contienen código binario y datos. La diferencia es que este archivo ha sido vinculado y está vinculado a todos los módulos (archivos objeto). El vinculador concatena todos los archivos de objetos reubicables necesarios en un archivo de objetos ejecutable. En este punto, los símbolos de cada archivo de objeto que hacen referencia a otros archivos de objeto se han resuelto y reubicado. Por lo tanto, se conoce cada símbolo y la máquina puede ejecutar el archivo directamente.
 

3.  Compartir archivos de destino

  Este es un archivo objeto ubicable especial que se puede cargar dinámicamente en la memoria y ejecutarse cuando se ejecuta o carga el programa que lo necesita. El sufijo de estos archivos suele ser *.so. Los archivos de objetos compartidos a menudo se denominan archivos de "biblioteca dinámica" o archivos de "biblioteca compartida".

1.4 Archivos de objetos reubicables

Un archivo de destino reubicable típico y un archivo ejecutable en un entorno Linux suelen tener el formato ELF (archivo vinculable ejecutable). La estructura típica de un archivo ELF es la siguiente:

El archivo de destino consta principalmente de dos partes: el encabezado del archivo ELF y el segmento del archivo de destino. Los primeros 16 bytes del encabezado del archivo ELF constituyen un orden de bytes que describe la longitud de las palabras y el orden de los bytes del sistema de archivos generado. La parte restante incluye otra información sobre el archivo ELF, incluido el tamaño del encabezado del archivo ELF, el tipo de archivo de destino, el tipo de máquina de destino, la posición de desplazamiento del archivo de la tabla de encabezado de segmento en el archivo de destino, etc. . Esta información es importante al vincular y cargar programas en formato ELF. 

/*ELF文件头*/
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4003e0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6736 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         31
  Section header string table index: 28

Además del encabezado del archivo ELF, la parte restante consta de segmentos del archivo objeto. Estas secciones son la parte central del archivo ELF. Consta de las siguientes secciones:

●.text : segmento de código , instrucciones binarias almacenadas de la máquina, que la máquina puede ejecutar directamente.

.rodata : segmento de datos de solo lectura , que almacena constantes complejas utilizadas en programas, como cadenas, etc.

.data : segmento de datos , que almacena datos globales que se han inicializado explícitamente en el programa. Incluyendo variables globales y variables estáticas en lenguaje C. Si estos datos globales se inicializan en 0, no se almacenan en el segmento de datos, sino en el segmento de almacenamiento de bloques. Las variables locales del lenguaje C se almacenan en la pila y no aparecen en el segmento de datos.

.bss : segmento de almacenamiento en bloque , que almacena datos globales que no se han inicializado explícitamente. Esta sección no ocupa espacio real en el archivo de destino, sino que es solo un marcador de posición para informar que el espacio para los datos globales debe reservarse en la ubicación especificada. La razón por la que existen segmentos de almacenamiento en bloque es para mejorar la utilización del espacio de almacenamiento en el disco.

.symtab : tabla de símbolos , que almacena funciones definidas y referenciadas y variables globales. Debe haber una tabla de este tipo en cada archivo de objeto reubicable. En esta tabla, todos los símbolos globales a los que se hace referencia (incluidas funciones y variables globales) en este módulo y los símbolos globales en otros módulos (archivos objeto) tendrán un registro. La operación de reubicación en el enlace es determinar la ubicación de estos símbolos globales referenciados.

.rel.text : información de que el segmento de código debe reubicarse (reubicarse) y almacena un resumen de los símbolos que deben modificarse mediante operaciones de reubicación. Estos símbolos están en el segmento de código y suelen ser el nombre y la etiqueta de una función.

.rel.data : información sobre los segmentos de datos que deben reubicarse y almacena un resumen de los símbolos que deben modificarse mediante operaciones de reubicación. Estos símbolos están en el segmento de datos y son variables globales.

.debug : información de depuración, que almacena una tabla de símbolos para la depuración. El uso de la opción -g del compilador gcc al compilar un programa generará esta sección. Esta tabla incluye las referencias y definiciones de todos los símbolos en el programa fuente. Con esta sección, puede imprimirla y observarla cuando use el depurador gdb para depurar el programa El valor de la variable.

.line : la asignación de número de línea del programa fuente, que almacena el número de línea de cada declaración en el programa fuente. Al compilar un programa, usar la opción -g del compilador gcc generará esta sección. Esta sección es muy útil al depurar el programa usando el depurador gdb.

.strtab : tabla de cadenas, que almacena los nombres de los símbolos en la tabla de símbolos .symtab y en la tabla de símbolos .debug. Estos nombres son cadenas y terminan en '\0'.

1.5 Símbolos y tablas de símbolos en archivos de objetos

La resolución de símbolos es una de las principales tareas del enlace. Solo después de que el símbolo se haya analizado correctamente se puede cambiar la ubicación del símbolo al que se hace referencia, completando así la reubicación y generando un archivo de destino ejecutable que la máquina puede cargar y ejecutar directamente. Cada archivo de objeto reubicable tiene una tabla de símbolos, que almacena símbolos. Estos símbolos se dividen en 3 categorías:

1. Símbolos globales definidos en este módulo

2. Símbolos globales definidos por otros módulos a los que se hace referencia en este módulo

3. Símbolos locales definidos y referenciados en este módulo.

Nota: Las variables locales y los símbolos locales no son lo mismo. Las variables locales se almacenan en la pila y son un concepto que sólo aparece en la memoria; los símbolos locales incluyen variables estáticas y etiquetas locales, que también pueden aparecer en archivos de disco.

Estructura de la tabla de símbolos

typedef struct{
    int name;			//目标符号的名字
    int value;			//符号的地址。对于可重定位模块:该值是距定义目标节的起始位置的偏移;
    					//			对于可执行目标文件:该值是一个绝对运行时地址。
    int size;			//目标符号的大小(字节为单位)
    char type:4;		//目标符号的类型
    char binding:4;		//目标符号是本地的还是全局的
    char reserved;		//保留
    char section;		//表示目标符号和目标文件的某个节关联(符号表中的Ndx字段)
}Elf_Symbol;

 Tabla de símbolos en el programa de muestra 1main.c

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     8: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    3 buf
     9: 0000000000000000    21 FUNC    GLOBAL DEFAULT    1 main
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap

Explicación del significado de la tabla de símbolos:

**buf:** Un destino de 8 bytes ubicado en el desplazamiento 0 (valor) en la sección .data, un símbolo global

**principal:** Una función de 21 bytes ubicada en el desplazamiento 0 en la sección .text, una función global

**intercambio:**Referencia del intercambio de símbolo externo, símbolo externo

Se utilizan números enteros en la tabla de símbolos para identificar cada sección diferente: Ndx=1 representa la sección .text; Ndx=3 representa la sección .data; ABS representa símbolos que no deben reubicarse; UNDEF representa símbolos no definidos, es decir, en este Símbolos a los que se hace referencia en el módulo de destino y definidos en otro lugar; COMÚN representa un destino de datos no inicializado al que aún no se le ha asignado una ubicación, es decir, una variable estática global o local no inicializada. LOCAL representa símbolos locales y GLOBAL representa símbolos globales.

1.6 Análisis de símbolos

El vinculador resuelve referencias de símbolos asociando cada referencia con una definición de símbolo definida en la tabla de símbolos del archivo de objeto reubicable que ingresa.

1. Resolución de símbolos locales

 La resolución de símbolos es muy sencilla para aquellas referencias a símbolos locales definidos en el mismo módulo. El compilador permite sólo una definición de cada símbolo local en cada archivo de objeto local. Por supuesto, para las variables estáticas locales, el compilador les asignará un símbolo de enlazador local y tendrán un nombre único.
 

2. Resolución de símbolos globales 

 Al resolver símbolos globales, cuando el compilador encuentra un símbolo (variable o función) que no está definido en el módulo actual, asumirá que el símbolo está definido en algún otro módulo, generará una tabla de entrada de símbolos vinculador y lo dejará en manos del enlazador. Durante el proceso de reubicación del enlace posterior, si el vinculador no puede encontrar la definición del símbolo al que se hace referencia en ninguno de sus módulos de entrada, la compilación informará un error.
 

 3. Reglas de análisis del compilador para el mismo símbolo global definido en múltiples archivos de objetos

 Regla 1: No se permiten varios símbolos fuertes
 Regla 2: Si hay un símbolo fuerte y varios símbolos débiles, elija el símbolo fuerte
 Regla 3: Si hay varios símbolos débiles, elija cualquier símbolo fuerte de estos símbolos débiles
 : Símbolo global inicializado
 Símbolo débil: símbolo global no inicializado

 

1.7 Reubicación

Cuando se completa el análisis de símbolos, se conocen la posición de definición y el tamaño de cada símbolo. La operación de reubicación sólo requiere vincular estos símbolos. En este paso, el vinculador debe fusionar todos los archivos objeto que participan en el vínculo y asignar a cada símbolo una dirección de tiempo de ejecución para almacenar el contenido. La reubicación se realiza en dos pasos:

1. Sección de reubicación y definiciones de símbolos.

En este paso, el vinculador fusiona todas las secciones del mismo tipo en una nueva sección. Por ejemplo, todas las secciones .data en el módulo de objeto de entrada se fusionarán en secciones .data en el archivo de objeto ejecutable y luego el vinculador asigna la dirección de memoria de tiempo de ejecución a la nueva sección .data. El proceso de otras secciones es el mismo: cuando se completa este paso, cada instrucción y variable global del programa tiene una dirección de memoria de tiempo de ejecución única.

2. Referencias de símbolos en secciones de reubicación

En este paso, el vinculador modifica las referencias a cada símbolo en las secciones de código y datos para que apunten a la dirección de memoria de tiempo de ejecución correcta.

Cuando el compilador genera un archivo objeto, no conoce la ubicación de almacenamiento final del código y las variables, ni conoce los símbolos externos definidos en otros archivos. Por lo tanto, cada vez que el ensamblador encuentra una referencia de destino cuya ubicación final se desconoce, el compilador genera una entrada de reubicación que almacena información sobre cada símbolo. Esta entrada le dice al vinculador cómo modificar las referencias de símbolos en cada archivo de objeto al fusionar los archivos de objeto. Esta entrada de reubicación se almacena en el segmento **.rel.text** y en el segmento .rel.data . Esta entrada puede entenderse como una estructura que almacena la información de reubicación de cada símbolo.

typedef struct {
    int offset;/*偏移值*/
    int symbol;/*所代表的符号*/
    int type;/*符号的类型*/  
}symbol_rel;
/*
offset表示该符号在存储的段中的偏移值。symbol代表该符号的名称,字符串实际存储在.strtab段中,这里存储的是该字符串首地址的下标。type表示重定位类型,链接器只关心两种类型,一种是与PC相关的重定位引用,另一种是绝对地址引用。
*/

La referencia de reubicación relacionada con la PC significa sumar el valor actual de la PC (este valor suele ser la ubicación de almacenamiento de la siguiente instrucción de salto) más el valor de compensación del símbolo. La referencia de dirección absoluta significa que la referencia de dirección especificada en la instrucción actual se utiliza directamente como dirección de salto sin ninguna modificación.

Con esta información, el vinculador puede agregar el valor de desplazamiento del símbolo en el segmento de almacenamiento a la nueva dirección del segmento después de la reubicación, obteniendo así una nueva dirección de referencia, y esta dirección de referencia es la dirección final del símbolo. Asimismo, todas las partes del programa que hacen referencia a esta dirección deben modificarse para utilizar esta nueva dirección absoluta en lugar de la antigua dirección de desplazamiento. Cuando se modifica la nueva dirección del símbolo, el trabajo del vinculador termina.

1.8 Archivos objeto ejecutables

​El formato de un archivo objeto ejecutable (ELF):

 

El encabezado ELF describe el formato general del archivo, que es similar al formato de un archivo objeto reubicable, pero incluye el punto de entrada del programa.

Tabla de encabezados de segmentos: describe qué segmentos contiguos de memoria se asignan a sectores contiguos del archivo ejecutable.

.init define una función: _init, que llamará el código de inicialización del programa.

.text, .rodata y .data son similares a las secciones anteriores del archivo de objeto reubicable, pero estas secciones se han reubicado en su dirección de memoria de tiempo de ejecución final.

​ Ejemplo de tabla de encabezado de segmento:

 

off: desplazamiento del archivo; vaddr: dirección virtual; paddr: dirección física; align: alineación del segmento;

filesz: tamaño del segmento en el archivo de destino; memsz: tamaño del segmento en la memoria; flags: permisos de operación

explicar:

Las líneas 1 y 2 nos dicen que el primer segmento (segmento de código) está alineado con un límite de 4 KB, tiene permisos de lectura/ejecución, comienza en la dirección de memoria 0x08048000, el tamaño total de la memoria es 0x448 bytes y se inicializa. Son los primeros 0x448 bytes. del archivo objeto ejecutable, incluido el encabezado ELF, la tabla de encabezados de segmento y las secciones .init, .text y .rodata.

Las líneas 3 y 4 nos dicen que el segundo segmento (segmento de datos) está alineado con un límite de 4 KB, tiene permisos de lectura/escritura, comienza en la dirección de memoria 0x08049448, tiene un tamaño de memoria total de 0x104 bytes y utiliza los bytes 0xe8 que se inicializan a partir en el desplazamiento del archivo 0x448, que en este caso es el comienzo de la sección .data. Los bytes restantes en esta sección corresponden a datos .bss que se inicializarán a cero en tiempo de ejecución.

Supongo que te gusta

Origin blog.csdn.net/qq_40648827/article/details/128021316
Recomendado
Clasificación