Conceptos básicos de Linker

 

A veces puedes aprender conocimiento, pero no tiempo. -Zhong Yunlong


Básico: https://blog.csdn.net/qq_35865125/article/details/105214201


Resumen

En el sistema de compilación, el enlazador juega un papel similar al "pegamento". Pega y empalma el archivo objeto reubicable generado por el ensamblador que procesa en un archivo ELF ejecutable. Sin embargo, el vinculador no empalma el archivo de objetos mecánicamente, sino que también debe completar la asignación de la dirección del segmento, el cálculo de la dirección del símbolo y la corrección del contenido de datos / instrucciones que no se puede completar en la etapa de ensamblaje.

Estas tres tareas principales involucran el proceso central del trabajo del enlazador: asignación de espacio de direcciones, resolución de símbolos y reubicación.


En cada entrada de la tabla de encabezado de sección del archivo de objeto reubicable, la dirección virtual de la sección se establece en 0 de forma predeterminada. Esto se debe a que es imposible saber la dirección de carga del segmento durante la etapa de procesamiento del ensamblador. El objetivo principal de la operación de asignación de espacio de direcciones del vinculador es especificar la dirección de carga para el segmento ( es decir, determinar dónde se ubica cada sección del archivo de destino en el archivo ejecutable ).

 

Después de determinar la dirección de carga de la sección (denominada dirección base de la sección), la dirección del símbolo en el archivo ejecutable se puede calcular de acuerdo con la dirección de desplazamiento del símbolo en el archivo de destino ( denominada dirección del símbolo, como la dirección de la función definida ) . La operación de resolución de símbolos del enlazador no se detiene al calcular la dirección del símbolo, sino que también debe analizar la referencia del símbolo entre los archivos de destino y calcular la dirección del símbolo externo al que se hace referencia en el archivo de destino.

 

Después de la resolución del símbolo, se han determinado las direcciones simbólicas (por ejemplo, direcciones en el archivo ejecutable) de todos los archivos de destino. El enlazador corrige la dirección simbólica referenciada en el segmento de código o segmento de datos a través de la operación de reubicación ( por ejemplo, el segmento de código tiene call printf, que debe modificarse a la dirección de la función ) .

 

Finalmente, el vinculador exporta la información del archivo procesada por las operaciones anteriores como un archivo ELF ejecutable para completar el trabajo de vinculación.

 

Recopilación de información

Para el vinculador, la entrada es una serie de archivos de objetos reubicables. Para completar el trabajo de seguimiento, el vinculador debe escanear los archivos de destino uno por uno y extraer la información requerida para el procesamiento.

El vinculador necesita analizar las referencias de símbolos en el archivo objeto. La razón para analizar la información de referencia del símbolo es que en un archivo de objeto procesado por el enlazador, hay símbolos indefinidos, es decir, referencias a símbolos de otros archivos de objeto. Para facilitar el procesamiento de la resolución de símbolos del vinculador, generalmente se definen dos conjuntos de símbolos: uno es un conjunto de símbolos de exportación, que representa todos los conjuntos de símbolos globales definidos en todos los archivos de destino a los que otros objetivos pueden hacer referencia; el otro es un conjunto de símbolos de importación, que representa archivos de destino No está definido internamente y debe referirse al conjunto de símbolos de otros archivos de objetos.

 

Asignación de espacio de direcciones

Cuando el ensamblador genera el archivo objeto, debido a que la dirección de carga del segmento no se puede determinar, la dirección base del segmento se registra como 0 de forma predeterminada. El primer paso del enlazador es determinar la dirección base del segmento que se va a cargar. El proceso de especificar la dirección base del segmento para el segmento que se va a cargar se denomina asignación de espacio de direcciones.

El vinculador especifica la dirección base para el segmento, que debe considerarse desde tres aspectos.

1) Dirección de inicio de carga de segmento.

      Esta dirección es la posición inicial de todos los segmentos de carga. En los sistemas Linux de 32 bits, generalmente se establece en 0x08048000.

2) La secuencia de empalme de segmentos.

     El vinculador escanea secuencialmente los segmentos del mismo nombre en cada archivo de destino y "coloca" los datos binarios de los segmentos en secuencia.

3) Alineación de segmentos.

      La alineación del segmento incluye dos niveles: la alineación del desplazamiento del archivo del segmento y la alineación de la dirección base del segmento.

En el archivo de destino reubicable, la alineación de desplazamiento del archivo del segmento generalmente se establece en 4 bytes, independientemente de la alineación de la dirección base del segmento (la dirección base del segmento es 0, no hay significado de alineación).

En el archivo ejecutable, la alineación de desplazamiento de archivo del segmento de código ".text" se establece en 16 bytes, y la alineación de desplazamiento de archivo de otros segmentos sigue siendo de 4 bytes por defecto. La alineación de la dirección base del segmento es más complicada. Es necesario asegurarse de que la dirección lineal del segmento y el desplazamiento del archivo correspondiente del segmento sean iguales al valor de alineación del segmento (es decir, el tamaño de página, que es 4096 bytes por defecto en Linux).

( El campo de alineación del segmento en la tabla de encabezado del programa p_align: p_ align indica la alineación del segmento, la regla de alineación es p_ vaddr% p_ align = 0, es decir, la dirección lineal del segmento debe ser un múltiplo entero de p_ align. En general, p_ align toma el valor 0x1000 = 4096, que es el tamaño de página predeterminado del sistema operativo Linux ).

 

La siguiente figura muestra un ejemplo de asignación de espacio de direcciones. El tamaño del segmento de código del archivo de destino ao es 0x4a bytes, el tamaño del segmento de datos es 0x08 bytes, el tamaño del segmento de código de bo es 0x21 bytes y el tamaño del segmento de datos es 0x04 bytes.

 

-No se requiere una tabla de encabezado de sección en el archivo ejecutable, esto solo se requiere en el archivo objeto.

 

Resolución de símbolos

La tabla de símbolos del archivo de destino almacena el desplazamiento de cada símbolo definido en relación con la dirección base del segmento. Cuando se asigna el espacio de direcciones del segmento, se determina la dirección base de cada segmento. Por lo tanto, la dirección del símbolo se puede calcular utilizando la siguiente fórmula:

Dirección del símbolo = dirección base del segmento + desplazamiento del símbolo de la dirección base del segmento

Sin embargo, antes de calcular la dirección simbólica, todavía se requiere algún trabajo de preparación.

Primero, debe escanear la tabla de símbolos en el archivo de destino para obtener la definición y la información de referencia de los símbolos, es decir, el conjunto de símbolos exportado y el conjunto de símbolos importado descrito anteriormente.

En segundo lugar, es necesario verificar la legalidad del conjunto de símbolos importado y el conjunto de símbolos exportado . La verificación de símbolos incluye dos aspectos:

1) Redefinición de símbolos: es decir, existe un símbolo con el mismo nombre en el conjunto de símbolos exportado. Cuando se vincula el archivo de destino, el símbolo se procesa mediante la recuperación del nombre, y la redefinición del símbolo hará que el archivo que se refiere al símbolo no pueda determinar qué símbolo debe usarse específicamente.

2) El símbolo no está definido: el conjunto de símbolos importado contiene símbolos que no existen en el conjunto exportado. Cuando el símbolo externo al que hace referencia el archivo de objeto no puede encontrar la definición correspondiente en otros archivos de objeto, no se puede determinar la dirección del símbolo. Una vez que el símbolo se redefine o no se define, el trabajo del vinculador no puede continuar.

 

Nota:

Hay una gran diferencia entre el archivo de destino y el archivo ejecutable: el campo de entrada del punto de entrada del programa e_ del encabezado del archivo de destino es 0, y el punto de entrada del programa del archivo ejecutable es una dirección lineal. Debemos suponer que la dirección de entrada del programa está registrada en un símbolo llamado "@start". Obviamente, este símbolo no puede ser el nombre del símbolo generado por el compilador. Para garantizar que el vinculador pueda encontrar el punto de entrada del programa, la fase de verificación de referencia de símbolo debe ser forzada a exportar el símbolo "@start" . En cuanto al proveedor del símbolo "@start", se puede suponer temporalmente que se origina en un archivo de objeto existente.

 

En términos generales, la resolución simbólica de direcciones se divide en dos pasos:

1) Escanee los símbolos locales de todos los archivos de destino ELF para calcular la dirección de los símbolos locales.

2) Escanee todos los símbolos del conjunto importado (es decir, un archivo debe usar los símbolos definidos por otros archivos de destino) y pase la dirección del símbolo a la tabla de símbolos del archivo de destino que hace referencia al símbolo.

 

Reubicación

 

( https://blog.csdn.net/qq_35865125/article/details/105214201

Los símbolos que deben reubicarse se almacenan en la tabla de reubicación en cada archivo de destino, correspondiente a la sección denominada " .rel" al principio. Las secciones donde los archivos ELF necesitan ser reubicados generalmente corresponden a una tabla de reubicación. Por ejemplo, la sección de código, es decir, la tabla de reubicación de la sección ". Text" se almacena en la sección ". Rel. Text", y la tabla de reubicación de ". Data" se almacena en ". Rel. Data")

 

La información de reubicación del archivo de destino contiene tres elementos clave:

# Símbolo de reubicación: la dirección del símbolo que se usa para la reubicación ;-( en la tabla de reubicación en cada archivo de destino)

# Ubicación de reubicación: dónde reubicar; ( Esta información también se puede obtener de la tabla de reubicación del archivo de destino, que almacena el nombre del símbolo que debe reubicarse, y también guarda a qué sección del archivo de destino pertenece el símbolo , Y el desplazamiento en esta sección, después de vincular para completar la asignación del espacio de direcciones, también se determina la dirección de esta sección en el archivo de destino, por lo que la posición del símbolo en el archivo ejecutable se puede ubicar de acuerdo con el desplazamiento ).

# Tipo de reubicación: qué método utilizar para la reubicación.

 

Primero, debido a que la operación de reubicación se basa en la dirección del símbolo reubicado, no puede reubicarse hasta que se complete la resolución del símbolo.

 

Hay dos tipos de reubicación:

Reubicación de dirección absoluta y reubicación de dirección relativa. La corrección de los datos del segmento de acuerdo con los diferentes tipos de reubicación es el núcleo de la reubicación.

1) La operación de reubicación de dirección absoluta es relativamente simple, donde la reubicación de dirección absoluta generalmente se deriva de la referencia directa a la dirección del símbolo. Dado que el ensamblador no puede determinar la dirección virtual del símbolo, el símbolo de referencia finalmente se llena con 0 como marcador de posición Dirección del lugar. Por lo tanto, la operación de reubicación de dirección absoluta solo necesita completar directamente la dirección virtual del símbolo de reubicación en la posición de reubicación.

Dirección de reubicación absoluta = dirección del símbolo de reubicación

 

2) La reubicación de la dirección relativa es un poco más complicada. El lugar donde se necesita la reubicación de la dirección relativa se deriva generalmente de la instrucción de dirección de salto que hace referencia a la dirección simbólica de otros archivos .

Aunque el ensamblador no puede determinar la dirección virtual del símbolo referenciado, no usa 0 como marcador de posición para llenar la dirección del símbolo de referencia, sino que usa la "posición de desplazamiento relativa a la dirección de la siguiente instrucción" para llenar la posición. Cuando el vinculador realiza una operación de reubicación de dirección relativa, calcula el desplazamiento de la dirección del símbolo en relación con la posición de reubicación, y luego agrega el desplazamiento al contenido almacenado en la posición de reubicación.

Dirección de reubicación relativa = dirección del símbolo de reubicación - ubicación de reubicación + contenido de datos de ubicación de reubicación

                           = (Dirección de símbolo de reubicación-posición de reubicación) + (posición de reubicación-siguiente dirección de instrucción)

                           = Dirección del símbolo de reubicación - siguiente dirección de instrucción

De acuerdo con el cálculo anterior, se puede ver claramente que la dirección de reubicación relativa calculada final es el desplazamiento de la dirección del símbolo de la dirección de la siguiente instrucción, y también está en línea con los requisitos de la instrucción de salto para el operando . En cuanto a por qué un cálculo tan "engorroso" de la reubicación de la dirección relativa, el autor cree que de esta manera, para instrucciones de diferentes longitudes y estructuras de diseño, siempre que los datos de la posición de reubicación se corrijan de acuerdo con el método de la dirección relativa, entonces la dirección de reubicación relativa El método de cálculo no cambia, la diferencia es solo el valor de los datos en la posición de reubicación. Por ejemplo, para las instrucciones de salto de Intel de 32 bits, el valor de los datos de posición es –4, para las instrucciones de salto de Intel de 64 bits, el valor de los datos de posición es –8.

 

A continuación se describe el proceso de reubicación con un ejemplo.

 

 

Punto de entrada del programa y biblioteca de tiempo de ejecución

Como se mencionó en la sección anterior, la dirección del punto de entrada del programa se almacena en un símbolo especial llamado "@start", y el compilador no genera el archivo de objeto que define el símbolo en función del código fuente. Luego hay dos problemas que deben aclararse:

1) ¿Por qué introducir nuevos símbolos en lugar de la función principal como punto de entrada del programa?

2) ¿Cómo obtengo el archivo de destino que define el nuevo símbolo?

Primero explique la primera pregunta. La forma de los fragmentos de código de ensamblaje generados para la función principal es la siguiente:

 

Esencialmente, la función principal no es muy diferente de las funciones ordinarias: contiene el código de la pila de funciones (líneas 3 ~ 5), el código del cuerpo de la función (se omite la línea 6) y el código de la pila de funciones (líneas 7 ~ 9) OK) Suponiendo que la función principal se usa como el punto de entrada del programa, es decir, la dirección lineal del símbolo principal se escribe en el campo e_entry en el encabezado del archivo ELF , luego, después de cargar y ejecutar el programa, la instrucción se leerá desde la posición de la dirección del símbolo principal para comenzar la ejecución. No habrá problemas durante la ejecución de la función principal hasta después de la ejecución de la instrucción ret. De acuerdo con la semántica de la instrucción ret , el programa tomará los datos de 32 bits de la parte superior de la pila como la dirección de retorno, ¡y luego saltará a esa dirección para continuar la ejecución ! Sin embargo, antes de que el programa ejecute la función principal, los datos almacenados en la parte superior de la pila son desconocidos, por lo que no se puede predecir el comportamiento final del programa. La consecuencia más común es activar el proceso "SegmentFault".

Por lo tanto, para que el programa salga correctamente, se debe construir un llamador de la función principal para completar el trabajo de "limpieza" después de la llamada a la función. Esto también proporciona una solución al segundo problema.

En la llamada al sistema de Linux, la llamada del sistema con el número de llamada 1. es salir. Usar la salida puede hacer que el proceso salga normalmente. El código de ensamblaje para llamar a la salida se muestra en las líneas 6 a 8, donde el registro eax contiene la llamada al sistema de salida número 1, ebx contiene el parámetro 0 de la llamada al sistema de salida y la instrucción int activa la llamada al sistema de salida para salir del proceso. El código en el símbolo "@start" llamará a la función principal y utilizará la llamada al sistema de salida para salir del proceso. Antes y después de llamar a la función principal, puede realizar algunos trabajos de inicialización (contenido omitido en la línea 3) y limpieza (contenido omitido en la línea 5).

Si el compilador guarda el código anterior en start.s, después de ser procesado por el ensamblador, se puede obtener el archivo de objeto start.o. Luego, use la herramienta readelf para ver la tabla de símbolos de start.o:

 

Desde la perspectiva del flujo de trabajo de todo el sistema de compilación, el archivo start.o es el archivo de destino necesario para el funcionamiento normal del sistema de compilación. No importa cómo se defina el código fuente procesado por el sistema de compilación, start.o y otros archivos de objeto deben estar vinculados en la etapa final de vinculación para generar un archivo ejecutable normalmente. Para dicho archivo de objeto, hay un nombre unificado: "biblioteca de tiempo de ejecución de lenguaje" . Obviamente, start.o debería ser la biblioteca de tiempo de ejecución más simple, solo es responsable de guiar y llamar a la función principal, y no hace nada más.

 

  ------ Gran conocimiento -----------

Según un método similar, las funciones de la biblioteca de tiempo de ejecución del lenguaje de programación se pueden ampliar fácilmente .

Por ejemplo, puede definir printf.s para implementar la función de salida estándar printf y generar el archivo de objeto printf.o después del procesamiento por parte del ensamblador. Siempre que la declaración del código fuente utilice la función printf, vincule printf.o al archivo ejecutable al vincular, y luego la función de salida estándar se puede realizar en el lenguaje de alto nivel. Además, el archivo math.c se puede definir directamente para implementar funciones relacionadas con las matemáticas, y el archivo objeto math.o se puede generar después del procesamiento por el compilador y el ensamblador, de modo que los lenguajes de alto nivel puedan realizar cálculos matemáticos complejos.

Si el preprocesador se implementa en el sistema de compilación y las instrucciones incluyen instrucciones, las declaraciones de declaración de funciones como la función printf o math.c se pueden colocar en un archivo de encabezado como "stdio.h" o "math.h" .  

Si los soportes de entrada de enlace de formato de paquete de archivos comprimidos, el archivo de destino como printf.o y math.o puede ser empaquetado en una situación similar "libc.a" Este archivo (biblioteca) , el enlazador sólo necesita antes del enlace Descomprima el paquete comprimido. Al escribir programas de lenguaje de alto nivel, siempre que se incluyan los archivos de encabezado requeridos y los archivos de biblioteca correspondientes se incluyan en la fase de enlace , se pueden usar características de lenguaje más potentes.

 

 

En comparación, la Biblioteca de tiempo de ejecución C (CRT) de GCC es mucho más complicada. Recordemos el ejemplo en el Capítulo 1:

(Cuando está vinculado estáticamente, GCC copiará cinco archivos de objetos importantes crt1. O, crti. O, crtbeginT. O, crtend. O, crtn. O y 3 bibliotecas estáticas libgcc. A en la biblioteca de tiempo de ejecución de lenguaje C (CRT). , Libgcc_ eh. A, libc. Un enlace al archivo ejecutable hola. )

 

5 archivos de destino crt1.o, crti.o, crtbeginT.o, crtend.o, crtn.o y 3 bibliotecas estáticas libgcc.a, libgcc_eh.a, libc.a involucradas en la descripción del flujo de trabajo de enlace estático GCC Las funciones de estos archivos son:

1) crt1.o: defina el punto de entrada del programa "_start", llame al código ".init" para ejecutar la inicialización del programa, llame a la función principal y llame al código ".finit" para realizar la limpieza del programa. La versión anterior era crt0.o, y las secciones ".init" y ".finit" no eran compatibles.

2) crti.o: defina la función de la sección ".init" en el código de la pila, llame al código de construcción global de C ++.

3) crtn.o: defina la función de la sección ".finit" del código de la pila, llame al código destructor global de C ++.

4) crtbeginT.o: define el código de construcción global de C ++.

5) crtend.o: define el código destructor global de C ++.

6) libc.a: defina el código de biblioteca estándar del lenguaje C. --- Debe ser un archivo listo para usar de gcc. Tráigalo cuando instale gcc. Para usar las funciones, #incluya los archivos de encabezado correspondientes en el código. El propósito de los archivos de encabezado es solo declarar la existencia de funciones. Durante la fase de vinculación, el vinculador obtiene estas funciones de libc.a. Hey http://www.delorie.com/djgpp/doc/libc/libc_1.html

7) libgcc.a: defina el código de función auxiliar debido a diferencias de plataforma.

8) libgcc_eh.a: define el código relacionado con la plataforma para el manejo de excepciones de C ++.

Se puede ver que, para un lenguaje de alto nivel, además del compilador, el ensamblador y el enlazador son partes esenciales, la biblioteca de tiempo de ejecución del lenguaje también es una parte indispensable . La biblioteca de tiempo de ejecución rica en funciones puede hacer que la expresión de lenguajes de alto nivel sea más poderosa.

 

Generación de archivos ELF

 

 


Árbitro:

Fan Zhidong; Zhang Qiongsheng. "Construyendo un sistema de compilación por uno mismo: compilación, compilación y vinculación" Press Industry Industry.

Publicado 374 artículos originales · 95 alabanzas · 260,000+ visitas

Supongo que te gusta

Origin blog.csdn.net/qq_35865125/article/details/105458421
Recomendado
Clasificación