Explicação detalhada do processo de compilação e vinculação da linguagem C
Arquivo fonte
principal.c
#include <stdio.h>
extern int data;
extern int add(int a,int b);
int a1;
int a2 = 0;
int a3 = 10;
static int b1;
static int b2 = 0;
static int b3 = 20;
int main()
{
int c1;
int c2 = 0;
int c3 = 30;
static int d1;
static int d2 = 0;
static int d3 = 40;
c1 = data;
c2 = add(a1,a2);
while(1);
return 0;
}
adicionar.c
int data = 3;
int add(int a,int b)
{
return a+b;
}
Dois processos principais: compilação e vinculação
1. Processo de compilação:
-
Pré-processamento (.i)
-
Processe instruções de pré-processamento começando com #: #include #define #ifndef #if #else, etc.
-
Remova comentários, adicione números de linha, gere índices de arquivos, etc.
Comando: gcc -E main.c -o main.i, gera arquivo .i
-
-
compilar (.s)
Compile o arquivo .i para gerar um arquivo assembly .s
Comando: gcc -S main.i gera arquivo .s
-
Montagem(.o)
Traduzir arquivos assembly em arquivos relocáveis de dois processos, ou seja, arquivos .o
Comando: gcc -c main.s gera arquivo .o
PS: O comando gcc é apenas um wrapper para alguns programas em segundo plano. Ele chama outros programas de acordo com parâmetros diferentes:
-
A pré-compilação e a compilação são combinadas em uma única etapa, usando o programa cc1 , ou você pode gerar um arquivo .s através do seguinte comando
cc1 olá.c
Equivalente a gcc -S olá.c -o olá.s
-
montador como
-
linkerld
Analise arquivos binários relocáveis
arquivo main.c
#include <stdio.h>
int a1;
int a2 = 0;
int a3 = 10;
static int b1;
static int b2 = 0;
static int b3 = 20;
int main(void)
{
int c1;
int c2 = 0;
int c3 = 30;
static int d1;
static int d2 = 0;
static int d3 = 40;
return 0;
}
Comando de compilação: compilar arquivos .o de 32 bits em uma máquina de 64 bits
* gcc -m32 -fno-PIC -c .c
-m32 especifica a compilação para gerar arquivos de 32 bits; -fno-PIC remove segmentos que são independentes da posição (deixando apenas .text.data.bss.comment, etc.)
1. Leia o cabeçalho do arquivo elf
$ readelf -h main.o
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: ARM
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 268 (bytes into file)
标志: 0x5000000, Version5 EABI
本头的大小: 52 (字节)
程序头大小: 0 (字节)
Number of program headers: 0
节头大小: 40 (字节)
节头数量: 10
字符串表索引节头: 7
(1) Número mágico
Magia: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
(2) REL (arquivo relocável)
(3) Endereço do ponto de entrada: 0x0
(4) Início dos cabeçalhos da seção: 268 (bytes no arquivo)
(5) Tamanho do cabeçalho: 52 (bytes)
2. Obtenha as informações dos cabeçalhos da seção do arquivo elf (para uso do link)
$ readelf -S main.o
There are 12 section headers, starting at offset 0x2ec:
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000044 00 AX 0 0 1
[ 2] .rel.text REL 00000000 00026c 000020 08 I 9 1 4
[ 3] .data PROGBITS 00000000 000078 00000c 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000084 000014 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 000084 00002a 01 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 00000000 0000ae 000000 00 0 0 1
[ 7] .eh_frame PROGBITS 00000000 0000b0 00003c 00 A 0 0 4
[ 8] .rel.eh_frame REL 00000000 00028c 000008 08 I 9 7 4
[ 9] .symtab SYMTAB 00000000 0000ec 000140 10 10 14 4
[10] .strtab STRTAB 00000000 00022c 000040 00 0 0 1
[11] .shstrtab STRTAB 00000000 000294 000057 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)
Existem 12 cabeçalhos de segmento e o deslocamento inicial do cabeçalho do segmento é 0x2ec
Você pode ver o deslocamento e o tamanho de cada segmento
3. Imprima o conteúdo do segmento
~ $ objdump -s main.o
main.o: 文件格式 elf32-i386
Contents of section .text:
0000 8d4c2404 83e4f0ff 71fc5589 e55183ec .L$.....q.U..Q..
0010 14c745ec 00000000 c745f01e 000000a1 ..E......E......
0020 00000000 8945f48b 15000000 00a10000 .....E..........
0030 000083ec 085250e8 fcffffff 83c41089 .....RP.........
0040 45ecebfe E...
Contents of section .data:
0000 0a000000 14000000 28000000 ........(...
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.
0010 352e302d 33756275 6e747531 7e31382e 5.0-3ubuntu1~18.
0020 30342920 372e352e 3000 04) 7.5.0.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 017c0801 .........zR..|..
0010 1b0c0404 88010000 20000000 1c000000 ........ .......
0020 00000000 44000000 00440c01 00471005 ....D....D...G..
0030 02750043 0f03757c 06000000 .u.C..u|....
4. Leia a tabela de símbolos do arquivo .o
~ $ objdump -t main.o
main.o: 文件格式 elf32-little
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .bss 00000004 b1
00000008 l O .bss 00000004 b2
00000004 l O .data 00000004 b3
00000008 l O .data 00000004 d3.1881
0000000c l O .bss 00000004 d2.1880
00000010 l O .bss 00000004 d1.1879
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000004 O *COM* 00000004 a1
00000000 g O .bss 00000004 a2
00000000 g O .data 00000004 a3
00000000 g F .text 00000044 main
00000000 *UND* 00000000 data
00000000 *UND* 00000000 add
Ele marca em qual segmento cada símbolo está e quanta memória ele ocupa. A1 é marcado como *COM* para indicar que é um símbolo fraco (uma variável global não estática não inicializada que pode ter o mesmo nome definido em outros arquivos).
Os dois símbolos data e add são marcados como *UND*, indicando símbolos indefinidos. A definição não pode ser encontrada neste arquivo e será encontrada em outros arquivos durante a vinculação.
5. Com base nas informações dos cabeçalhos da seção, desenhe a composição do arquivo binário relocável (arquivo .o)
Pode-se descobrir que a TV via satélite inicial do segmento bss e do segmento de comentários é a mesma, mas o cálculo real mostra que o segmento bss não está armazenado no arquivo .o, mas o segmento bss é registrado na tabela de símbolos.
Conclusão: A seção bss salva variáveis globais que não são inicializadas/inicializadas com 0 , e variáveis locais estáticas que não são inicializadas/inicializadas com 0 , portanto, seus valores padrão são todos 0, portanto, para economizar espaço no Arquivo .o, nenhum armazenamento é necessário, mas precisa ser registrado na tabela de símbolos.Depois que o arquivo executável é finalmente executado, os símbolos do segmento bss são armazenados no espaço de endereço virtual.
2. Processo de vinculação:
Compilando em uma máquina x86 de 64 bits - vinculando comandos que produzem arquivos de objeto e executáveis de 32 bits
编译:
gcc -m32 -fno-PIC -c *.c
手动链接:
ld -e main -melf_i386 *.o -o run
生成如下文件:
$ ls
add.c add.o main.c main.o run
PS:
-m32 especifica compilação para gerar arquivos de 32 bits;
-fno-PIC remove segmentos independentes da posição (deixando apenas .text.data.bss.comment, etc.)
-e especifica a entrada do programa, basta seguir -e com um símbolo, ou você pode usar a função add como a entrada do programa, ou seja, -e add
-melf_i386 especifica o link para gerar um arquivo executável de arquitetura x86 de 32 bits
A essência do processo de vinculação é "colar" vários arquivos de destino. Em essência, o que é costurado são as referências aos endereços entre os arquivos de destino, ou seja, nomes de funções e variáveis globais.
A tabela de símbolos é uma seção do arquivo .o, symtab , veja o comando da tabela de símbolos
readelf -s principal.o
objdump -t principal.o
nm principal.o
O que está incluído na tabela de símbolos, focando principalmente em 1 e 2 :
-
- Símbolos globais definidos neste arquivo objeto, como nomes de variáveis, nomes de funções, etc.
-
- Os símbolos referenciados em outros arquivos de destino não são definidos neste arquivo e geralmente são chamados de símbolos externos.
-
- Nome da seção, como ".text", ".data" etc.
-
- Os símbolos locais são visíveis apenas dentro da unidade de compilação. O depurador pode usar esses símbolos para analisar o programa ou o arquivo de dump principal quando ele trava. O vinculador geralmente os ignora durante o processo de vinculação.
$ objdump -t main.o
main.o: 文件格式 elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .bss 00000004 b1
00000008 l O .bss 00000004 b2
00000004 l O .data 00000004 b3
00000008 l O .data 00000004 d3.1877
0000000c l O .bss 00000004 d2.1876
00000010 l O .bss 00000004 d1.1875
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000004 O *COM* 00000004 a1
00000000 g O .bss 00000004 a2
00000000 g O .data 00000004 a3
00000000 g F .text 00000016 main
1. Mesclar segmentos de todos os arquivos .o
Conforme mostrado na figura acima, quando os segmentos de texto são mesclados, os segmentos de dados são mesclados e os segmentos bss são mesclados, os símbolos fracos precisam ser convertidos em símbolos fortes (ou os símbolos fracos são substituídos por símbolos fortes) e o tamanho do bss segmento aumenta.
E após descobrir o link, cada segmento do arquivo executável gerado recebe um endereço de memória (memória virtual)
2. Mesclar tabelas de símbolos , análise de símbolos e realocação
- Mesclar tabelas de símbolos
Pode-se ver que a tabela de símbolos de um arquivo executável é simplesmente uma combinação das tabelas de símbolos de vários arquivos .o.
- Análise de símbolos
Converta símbolos fracos (*COM*) em símbolos fortes
Símbolos indefinidos neste arquivo (*UND*) foram encontrados em outros arquivos
- reiniciar
Aloque um endereço de memória virtual para o símbolo. O endereço do símbolo é calculado com base no endereço do segmento mais seu próprio deslocamento.
Análise de arquivo executável
1. Veja o cabeçalho do arquivo
$ readelf -h run
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Intel 80386
版本: 0x1
入口点地址: 0x80480a1
程序头起点: 52 (bytes into file)
Start of section headers: 4676 (bytes into file)
标志: 0x0
本头的大小: 52 (字节)
程序头大小: 32 (字节)
Number of program headers: 3
节头大小: 40 (字节)
节头数量: 9
字符串表索引节头: 8
Endereço do ponto de entrada: 0x80480a1.
2. Visualize informações do segmento
$ readelf -S run
There are 9 section headers, starting at offset 0x1244:
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 08048094 000094 000051 00 AX 0 0 1
[ 2] .eh_frame PROGBITS 080480e8 0000e8 00005c 00 A 0 0 4
[ 3] .data PROGBITS 0804a000 001000 000010 00 WA 0 0 4
[ 4] .bss NOBITS 0804a010 001010 000018 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 001010 000029 01 MS 0 0 1
[ 6] .symtab SYMTAB 00000000 00103c 000170 10 7 14 4
[ 7] .strtab STRTAB 00000000 0011ac 000059 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 001205 00003f 00 0 0 1
Cada segmento recebe um endereço virtual.
3. Veja os cabeçalhos dos programas
$ readelf -l run
Elf 文件类型为 EXEC (可执行文件)
Entry point 0x80480a1
There are 3 program headers, starting at offset 52
程序头:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x00144 0x00144 R E 0x1000
LOAD 0x001000 0x0804a000 0x0804a000 0x00010 0x00028 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
Section to Segment mapping:
段节...
00 .text .eh_frame
01 .data .bss
02
Arquivos binários relocáveis têm apenas "cabeçalhos de seção" e apenas arquivos executáveis têm "cabeçalhos de programa" . "Cabeçalhos de programa" mostram o endereço virtual e os bytes de alinhamento de cada seção (uma página tem 4K)
Mesclar de acordo com atributos de segmento , somente leitura (texto+rodata), legível e gravável (dados+bss), etc.
Use readelf -l main para visualizar o "segmento" do ELF(para uso de carregamento)
PS: Como nós mesmos vinculamos e não vinculamos a biblioteca C, o conteúdo do parágrafo é relativamente pequeno.
* Se você executar gcc main.c -o main diretamente , a biblioteca C será vinculada por padrão e haverá muito conteúdo ao visualizar cada seção do arquivo executável.
* Arquivos executáveis são carregados no processo por execve
* A razão pela qual o arquivo executável pode ser executado é porque ele especifica o endereço de entrada (principal) e os cabeçalhos do programa (especifica o endereço virtual a ser carregado)
* A estrutura que descreve "Segmento" é chamada de "Cabeçalho do Programa" , que descreve como o arquivo ELF deve ser mapeado para o espaço virtual do processo pelo sistema operacional.