Análisis en profundidad del proceso de conversión de código de lenguaje C a código de máquina
Desde una perspectiva amplia, se puede dividir en dos etapas:
- La primera etapa: Consta de tres etapas: Compilar, Ensamblar y Vincular, y genera un programa ejecutable (Programa Ejecutable).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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:
.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;.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;.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í..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.
El papel clave del formato ELF en el proceso de compilación
- 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.
- Etapa de ensamblaje (Ensamblar): El formato ELF se utiliza para almacenar instrucciones y datos de la máquina ensamblada en esta etapa.
- 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.
- 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.c
funcionan link_example.c
juntos 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.c
y : y .link_example.c
add_lib.o
link_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
main
llamada en la funciónadd
ya no es la dirección de la siguiente instrucción, sinoadd
la 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.
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.