Principios de composición informática | Comprensión profunda del formato ELF y enlaces estáticos

Análisis en profundidad del proceso de conversión de código de lenguaje C a código de máquina

第二阶段
Loader
Cache
CPU
第一阶段
Compile
Assemble
Link
Load
读取指令和数据
CPU
内存
装载器
链接
汇编
编译
C代码
可执行文件
Figura: Del código C al proceso de ejecución de código máquina

Desde una perspectiva amplia, se puede dividir en dos etapas:

  1. La primera etapa: Consta de tres etapas: Compilar, Ensamblar y Vincular, y genera un programa ejecutable (Programa Ejecutable).
  2. La segunda etapa: cargue el archivo ejecutable en la memoria a través del cargador, y luego la CPU lee las instrucciones y datos de la memoria para comenzar a ejecutar el programa.

La primera etapa: compilación, montaje y vinculación.

  1. Compilar: en esta etapa, se utiliza un compilador de lenguaje C (como GCC) para compilar el archivo de código fuente C (archivo .c) en un archivo de código ensamblador (archivo .s). El compilador realiza análisis léxico, análisis de sintaxis y análisis semántico en el código C y luego genera código intermedio para representar la estructura lógica del programa.
  2. Ensamblaje (ensamblado): en esta etapa, se utiliza un ensamblador (como el ensamblador GNU) para convertir el archivo de código ensamblador (archivo .s) en un archivo de instrucciones de código de máquina (archivo .o). El ensamblador traduce cada instrucción del código ensamblador a la instrucción del código de máquina correspondiente.
  3. Enlace: en esta etapa, se utiliza un enlazador (como el enlazador GNU) para vincular varios archivos de instrucciones de código de máquina (archivos .o) y los archivos de biblioteca necesarios para generar el archivo ejecutable final (programa ejecutable). El vinculador resuelve referencias a funciones y variables globales y asocia sus definiciones con las referencias correspondientes para crear el archivo ejecutable.

Segunda etapa: carga y ejecución.

  1. Cargar: En esta fase, el cargador del sistema operativo es responsable de cargar el archivo ejecutable en la ubicación adecuada de la memoria. El cargador asigna espacio de memoria y copia las instrucciones, datos y otros recursos del archivo ejecutable en la dirección de memoria correspondiente.
  2. Ejecución: una vez que el archivo ejecutable se carga exitosamente en la memoria, la CPU lee las instrucciones y los datos de la memoria y comienza a ejecutar el programa en el orden de las instrucciones. La CPU realizará operaciones aritméticas, juicios lógicos, acceso a la memoria y otras operaciones de acuerdo con las instrucciones, y finalmente realizará las funciones del programa.

Comprensión profunda del formato ELF: un papel importante en el sistema Linux

¿Qué son los ELF?

  • ELF (Formato ejecutable y vinculable, formato ejecutable y vinculable)

  • En sistemas Linux, utilice ELF para almacenar y organizar datos

Estructura de archivos ELF

Estructura del archivo principal ELF:

  1. .text Section: Segmento de código o segmento de instrucción (Sección de código), utilizado para guardar el código y las instrucciones del programa;
  2. .data Section: Sección de datos (Sección de datos), utilizada para guardar la información de datos de inicialización establecida en el programa;
  3. .rel.text Secion,: Tabla de Reubicación (Tabla de Reubicación). En la tabla de reubicación, lo que se retiene es la dirección de salto en el archivo actual, pero en realidad no sabemos qué direcciones de salto están allí.
  4. .symtab Section: Tabla de símbolos (Tabla de símbolos). La tabla de símbolos conserva lo que llamamos una libreta de direcciones de nombres de funciones y direcciones correspondientes definidas en el archivo actual.

imagen

Figura: Estructura clave del archivo ELF

El papel clave del formato ELF en el proceso de compilación

  1. Fase de compilación (compilar): el archivo objeto generado por el compilador generalmente usa el formato ELF para almacenar el código y los datos compilados.
  2. Etapa de ensamblaje (Ensamblar): El formato ELF se utiliza para almacenar instrucciones y datos de la máquina ensamblada en esta etapa.
  3. Fase de enlace (Link): La fase de enlace es el área de aplicación principal del formato ELF. Durante la fase de vinculación, el vinculador lee varios archivos de objetos y archivos de biblioteca, realiza la resolución y reubicación de símbolos en función de las relaciones de referencia de símbolos y, finalmente, genera un archivo ejecutable. El formato ELF proporciona estructuras como tablas de segmentos, tablas de símbolos y tablas de reubicación para describir la relación entre varias partes del archivo y los símbolos, lo que permite al vinculador manejar con precisión las operaciones de referencia y reubicación de símbolos.
  4. Fase de carga (Cargar): En esta fase, el formato ELF ayuda al sistema operativo (Sistema Operativo) a comprender los requisitos de diseño y reubicación del archivo ejecutable.

Ejemplo de ejecución de ELF

código C

Los dos archivos siguientes add_lib.cfuncionan link_example.cjuntos para implementar una función de suma.

// add_lib.c
int add(int a, int b)
{
    
    
    return a+b;
}
// link_example.c

#include <stdio.h>
int main()
{
    
    
    int a = 10;
    int b = 5;
    int c = add(a, b);
    printf("c = %d\n", c);
}

Compilacion

El siguiente es el archivo objeto (archivo objeto) generado por add_lib.cy : y .link_example.cadd_lib.olink_example .o

Compilar con gcc:

$ gcc -g -c add_lib.c link_example.c
$ objdump -d -M intel -S add_lib.o
$ objdump -d -M intel -S link_example.o

El código ensamblador que obtenemos después de compilar:

# add_lib函数的汇编代码

add_lib.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <add>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
   7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
   a:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
   d:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  10:   01 d0                   add    eax,edx
  12:   5d                      pop    rbp
  13:   c3                      ret    
# link_example函数的汇编代码

link_example.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   48 83 ec 10             sub    rsp,0x10
   8:   c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa
   f:   c7 45 f8 05 00 00 00    mov    DWORD PTR [rbp-0x8],0x5
  16:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
  19:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  1c:   89 d6                   mov    esi,edx
  1e:   89 c7                   mov    edi,eax
  20:   b8 00 00 00 00          mov    eax,0x0
  25:   e8 00 00 00 00          call   2a <main+0x2a>
  2a:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
  2d:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
  30:   89 c6                   mov    esi,eax
  32:   48 8d 3d 00 00 00 00    lea    rdi,[rip+0x0]        # 39 <main+0x39>
  39:   b8 00 00 00 00          mov    eax,0x0
  3e:   e8 00 00 00 00          call   43 <main+0x43>
  43:   b8 00 00 00 00          mov    eax,0x0
  48:   c9                      leave  
  49:   c3                      ret    

Enlace

gcc -c add_lib.s
gcc -c link_example.s

código ejecutable

gcc -o executable add_lib.o link_example.o
$ ./executable
c = 15 # 运行结果为15
  • Nota: La dirección de salto mainllamada en la función addya no es la dirección de la siguiente instrucción, sino addla dirección de entrada de la función.

link_example:     file format elf64-x86-64
Disassembly of section .init:
...
Disassembly of section .plt:
...
Disassembly of section .plt.got:
...
Disassembly of section .text:
...

 6b0:   55                      push   rbp
 6b1:   48 89 e5                mov    rbp,rsp
 6b4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
 6b7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
 6ba:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
 6bd:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
 6c0:   01 d0                   add    eax,edx
 6c2:   5d                      pop    rbp
 6c3:   c3                      ret    
00000000000006c4 <main>:
 6c4:   55                      push   rbp
 6c5:   48 89 e5                mov    rbp,rsp
 6c8:   48 83 ec 10             sub    rsp,0x10
 6cc:   c7 45 fc 0a 00 00 00    mov    DWORD PTR [rbp-0x4],0xa
 6d3:   c7 45 f8 05 00 00 00    mov    DWORD PTR [rbp-0x8],0x5
 6da:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
 6dd:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
 6e0:   89 d6                   mov    esi,edx
 6e2:   89 c7                   mov    edi,eax
 6e4:   b8 00 00 00 00          mov    eax,0x0
 6e9:   e8 c2 ff ff ff          call   6b0 <add>  # 直接在main函数中调用add函数的入口地址
 6ee:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
 6f1:   8b 45 f4                mov    eax,DWORD PTR [rbp-0xc]
 6f4:   89 c6                   mov    esi,eax
 6f6:   48 8d 3d 97 00 00 00    lea    rdi,[rip+0x97]        
 6fd:   b8 00 00 00 00          mov    eax,0x0
 702:   e8 59 fe ff ff          call   560 <printf@plt>
 707:   b8 00 00 00 00          mov    eax,0x0
 70c:   c9                      leave  
 70d:   c3                      ret    
 70e:   66 90                   xchg   ax,ax
...
Disassembly of section .fini:
...

El vinculador escanea todos los archivos de objetos de entrada y luego recopila la información en todas las tablas de símbolos para formar una tabla de símbolos global. Luego, según la tabla de reubicación, todos los códigos cuyas direcciones de salto son inciertas se corrigen según las direcciones almacenadas en la tabla de símbolos. Finalmente, las secciones correspondientes de todos los archivos de destino se fusionan en el código ejecutable final.

imagen

Figura: Diagrama esquemático del proceso de generación de archivos ejecutables

Sistema operativo Windows: PE

  • El formato de archivo ejecutable de Windows se llama PE (formato ejecutable portátil).
  • El cargador en Linux solo puede analizar el formato ELF pero no el formato PE.

¿Cómo hacer que el formato sea compatible entre el sistema Windows y el sistema Linux?

  • Wine, un conocido proyecto de código abierto en Linux, admite un cargador compatible con el formato PE, lo que nos permite ejecutar programas de Windows directamente en Linux.
  • Windows también proporciona WSL, que es el subsistema de Windows para Linux, que puede analizar y cargar archivos en formato ELF.
  • Aunque existen varias herramientas para lograr la compatibilidad del formato de archivo ejecutable, el programa también se basa en bibliotecas de enlaces dinámicos, llamadas al sistema, etc. proporcionadas por varios sistemas operativos, y aún debe adaptarse y probarse para plataformas específicas. En otras palabras, la compatibilidad de formatos es sólo el primer paso.

referencias

Supongo que te gusta

Origin blog.csdn.net/YuvalNoah/article/details/131183719
Recomendado
Clasificación