C++ | 程序编译连接原理
文章目录
c/c++程序编译链接一共分四个过程:
源文件 a.c
预编译(生成*.i文件)
命令 :gcc -E a.c
或 gcc -E a.c -o a.i
生成 a.i 文件
- 将所有的“#define”删除,并且展开所有宏;
- 处理掉所有条件预编译指令,如:“#if”、“#ifdef”、“#elif”、“#else”、“#endif”;
- 处理“#include”指令,这是一个递归过程;
- 删除所有的注释“//”和“/* */”;
- 添加行号和文件名标识;
- 保留所有的#pragma编译器指令,待编译器使用;
编译(生成*.s文件)
命令 :gcc -S a.i
或 gcc -S a.i -o a.s
生成 a.s 文件
把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相对应的汇编代码文件。
汇编(生成*.o文件,也叫目标文件)
命令 :gcc -c a.s
或 gcc -c a.c -o a.o
生成 a.o 文件
汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。
链接(生成*.exe文件,也叫可执行文件)
命令 :gcc -o a.o
其中 -l
指定连接文件路径 -L
指定头文件路径
- 合并段和符号表
- 符号解析
- 地址和空间分配
- 符号重定位
a.o 文件也叫可重定位文件,虽然这个目标文件中包含了机器语言代码,但并不是一个完整的程序,由于缺少启动代码与库代码所以暂时不能运行因此我们在运行程序时还需要在进一步链接,通过链接器把启动代码,库代码,和目标代码结合在一起,并将它们放入单个文件,即可执行文件。
汇编——目标文件
通过下面两个代码探究目标文件的组成。
/* main.cpp */
extern int gdata;
int sum(int, int);
int data = 20;
int main()
{
int a = gdata;
int b = data;
int ret = sum(a,b);
return 0;
}
/* sum.cpp */
int gdata = 10;
int sum(int a, int b)
{ return a+b;
}
在Linux下通过 g++ -c main.cpp sum.cpp
命令,把两个文件编译为目标文件。
在 .o 目标文件中存在若干段表,我们主要需要了解其中的 elf文件头、.text指令段、 .data/.bss 数据段、.symbal符号表段 … 等。
查看文件头
使用 readelf -h main.o
查看elf文件头部信息。
[stu@tr blog]$ readelf -h main.o
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: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 736 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 12
Section header string table index: 11
[stu@tr blog]$ readelf -h sum.o
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: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 568 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 11
Section header string table index: 10
类型:可重定向
Type: REL (Relocatable file)
程序入口地址:没有指定
Entry point address: 0x0
查看符号表
通过 objdump -t main.o
命令可以查看符号表。
[stu@tr blog]$ objdump -t main.o
main.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 main.cpp
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g O .data 0000000000000004 data
0000000000000000 g F .text 0000000000000033 main
0000000000000000 *UND* 0000000000000000 gdata
0000000000000000 *UND* 0000000000000000 _Z3sumii
[stu@tr blog]$ objdump -t sum.o
sum.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 sum.cpp
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 g O .data 0000000000000004 gdata
0000000000000000 g F .text 0000000000000014 _Z3sumii
我们可以看到main.o 和 sum.o 的符号表中*gdata
与_Z3sumii
都是*UND*
未定义,第二列的 l (local)表示只能在当前文件中可见,g(global)表示其他文件中可见。对于链接器来说只能看见 g 属性的符号。
另外,我们可以看到在符号表中的符号没有分配地址(第一列),只有在进行链接后才会给符号表分配虚拟地址。
查看 .o 文件段表
使用 objdump -s main.o
命令查看目标文件中常用的段表
[stu@tr blog]$ objdump -s main.o
main.o: file format elf64-x86-64
Contents of section .text:
0000 554889e5 4883ec10 8b050000 00008945 UH..H..........E
0010 fc8b0500 00000089 45f88b55 f88b45fc ........E..U..E.
0020 89d689c7 e8000000 008945f4 b8000000 ..........E.....
0030 00c9c3 ...
Contents of section .data:
0000 14000000 ....
Contents of section .comment:
0000 00474343 3a202847 4e552920 342e382e .GCC: (GNU) 4.8.
0010 35203230 31353036 32332028 52656420 5 20150623 (Red
0020 48617420 342e382e 352d3339 2900 Hat 4.8.5-39).
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 33000000 00410e10 8602430d ....3....A....C.
0030 066e0c07 08000000 .n......
[stu@tr blog]$ objdump -s sum.o
sum.o: file format elf64-x86-64
Contents of section .text:
0000 554889e5 897dfc89 75f88b45 f88b55fc UH...}..u..E..U.
0010 01d05dc3 ..].
Contents of section .data:
0000 0a000000 ....
Contents of section .comment:
0000 00474343 3a202847 4e552920 342e382e .GCC: (GNU) 4.8.
0010 35203230 31353036 32332028 52656420 5 20150623 (Red
0020 48617420 342e382e 352d3339 2900 Hat 4.8.5-39).
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 14000000 00410e10 8602430d .........A....C.
0030 064f0c07 08000000 .O......
查看汇编代码
g++ -c main.cpp -g ; objdump -S main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
int sum(int, int);
int data = 20;
int main()
{
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
int a = gdata;
8: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # e <main+0xe>
e: 89 45 fc mov %eax,-0x4(%rbp)
int b = data;
11: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 17 <main+0x17>
17: 89 45 f8 mov %eax,-0x8(%rbp)
int ret = sum(a,b);
1a: 8b 55 f8 mov -0x8(%rbp),%edx
1d: 8b 45 fc mov -0x4(%rbp),%eax
20: 89 d6 mov %edx,%esi
22: 89 c7 mov %eax,%edi
24: e8 00 00 00 00 callq 29 <main+0x29>
29: 89 45 f4 mov %eax,-0xc(%rbp)
return 0;
2c: b8 00 00 00 00 mov $0x0,%eax
}
31: c9 leaveq
32: c3 retq
我们可以看到其中的两条指令,他们在执行时对 0x0 地址进行了相关操作,很明显这两句指令不可能执行成功,原因就在于该处指令中用到的符号地址不确定,暂时用 0x0 代替。而在链接后通过符号重定位这一步可以把这些暂时不确定的符号地址重新改写为确定的地址。这也是 obj 文件无法运行的原因之一。
mov 0x0(%rip),%eax // int a = gdata;
mov 0x0(%rip),%eax // int b = data;
链接——可执行文件
链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。它的工作主要就是把一些指令对其他符号地址的引用加以修正。
链接: g++ -o main main.o sum.o
链接可分四步:
- 合并段和符号表
合并多个文件的符号表及各段内容,放入一个新的文件中。 - 符号解析 // 符号表
在每个文件符号引用(引用外部符号)的地方找到符号的定义。这就是符号解析。 - 地址和空间分配
符号解析成功后,为程序分配虚拟地址空间。 - 符号重定位 // 指令段
符号重定向就是对.o文件中.text段指令中的无效地址给出具体的虚拟地址或者相对位移偏移量。
查看文件头
使用readelf -h main
命令查看文件头部信息。
[stu@tr blog]$ readelf -h main
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: 0x400420
Start of program headers: 64 (bytes into file)
Start of section headers: 7208 (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: 35
Section header string table index: 34
类型:可执行
Type: REL (Relocatable file)
程序入口地址:0x400420
Entry point address: 0x400420
查看符号表
objdump -t main
main: file format elf64-x86-64
SYMBOL TABLE:
0000000000400238 l d .interp 0000000000000000 .interp
0000000000400254 l d .note.ABI-tag 0000000000000000 .note.ABI-tag
0000000000400274 l d .note.gnu.build-id 0000000000000000 .note.gnu.build-id
0000000000400298 l d .gnu.hash 0000000000000000 .gnu.hash
00000000004002b8 l d .dynsym 0000000000000000 .dynsym
0000000000400300 l d .dynstr 0000000000000000 .dynstr
0000000000400360 l d .gnu.version 0000000000000000 .gnu.version
0000000000400368 l d .gnu.version_r 0000000000000000 .gnu.version_r
0000000000400388 l d .rela.dyn 0000000000000000 .rela.dyn
00000000004003a0 l d .rela.plt 0000000000000000 .rela.plt
00000000004003d0 l d .init 0000000000000000 .init
00000000004003f0 l d .plt 0000000000000000 .plt
0000000000400420 l d .text 0000000000000000 .text
00000000004005d4 l d .fini 0000000000000000 .fini
00000000004005e0 l d .rodata 0000000000000000 .rodata
00000000004005f0 l d .eh_frame_hdr 0000000000000000 .eh_frame_hdr
0000000000400630 l d .eh_frame 0000000000000000 .eh_frame
0000000000600de0 l d .init_array 0000000000000000 .init_array
0000000000600de8 l d .fini_array 0000000000000000 .fini_array
0000000000600df0 l d .jcr 0000000000000000 .jcr
0000000000600df8 l d .dynamic 0000000000000000 .dynamic
0000000000600ff8 l d .got 0000000000000000 .got
0000000000601000 l d .got.plt 0000000000000000 .got.plt
0000000000601028 l d .data 0000000000000000 .data
0000000000601034 l d .bss 0000000000000000 .bss
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 l d .debug_aranges 0000000000000000 .debug_aranges
0000000000000000 l d .debug_info 0000000000000000 .debug_info
0000000000000000 l d .debug_abbrev 0000000000000000 .debug_abbrev
0000000000000000 l d .debug_line 0000000000000000 .debug_line
0000000000000000 l d .debug_str 0000000000000000 .debug_str
0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
0000000000600df0 l O .jcr 0000000000000000 __JCR_LIST__
0000000000400450 l F .text 0000000000000000 deregister_tm_clones
0000000000400480 l F .text 0000000000000000 register_tm_clones
00000000004004c0 l F .text 0000000000000000 __do_global_dtors_aux
0000000000601034 l O .bss 0000000000000001 completed.6355
0000000000600de8 l O .fini_array 0000000000000000 __do_global_dtors_aux_fini_array_entry
00000000004004e0 l F .text 0000000000000000 frame_dummy
0000000000600de0 l O .init_array 0000000000000000 __frame_dummy_init_array_entry
0000000000000000 l df *ABS* 0000000000000000 main.cpp
0000000000000000 l df *ABS* 0000000000000000 sum.cpp
0000000000000000 l df *ABS* 0000000000000000 crtstuff.c
0000000000400740 l O .eh_frame 0000000000000000 __FRAME_END__
0000000000600df0 l O .jcr 0000000000000000 __JCR_END__
0000000000000000 l df *ABS* 0000000000000000
00000000004005f0 l .eh_frame_hdr 0000000000000000 __GNU_EH_FRAME_HDR
0000000000601000 l O .got.plt 0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000600de8 l .init_array 0000000000000000 __init_array_end
0000000000600de0 l .init_array 0000000000000000 __init_array_start
0000000000600df8 l O .dynamic 0000000000000000 _DYNAMIC
0000000000601028 w .data 0000000000000000 data_start
00000000004005d0 g F .text 0000000000000002 __libc_csu_fini
0000000000400420 g F .text 0000000000000000 _start
0000000000000000 w *UND* 0000000000000000 __gmon_start__
00000000004005d4 g F .fini 0000000000000000 _fini
0000000000000000 F *UND* 0000000000000000 __libc_start_main@@GLIBC_2.2.5
0000000000601030 g O .data 0000000000000004 gdata
00000000004005e0 g O .rodata 0000000000000004 _IO_stdin_used
000000000060102c g O .data 0000000000000004 data
0000000000601028 g .data 0000000000000000 __data_start
0000000000601038 g O .data 0000000000000000 .hidden __TMC_END__
00000000004005e8 g O .rodata 0000000000000000 .hidden __dso_handle
0000000000400560 g F .text 0000000000000065 __libc_csu_init
0000000000601034 g .bss 0000000000000000 __bss_start
0000000000400540 g F .text 0000000000000014 _Z3sumii
0000000000601038 g .bss 0000000000000000 _end
0000000000601034 g .data 0000000000000000 _edata
000000000040050d g F .text 0000000000000033 main
00000000004003d0 g F .init 0000000000000000 _init
可以看到所有的符号都已经分配了虚拟地址。
查看汇编代码
objdump -S main
main: file format elf64-x86-64
Disassembly of section .init:
00000000004003d0 <_init>:
4003d0: 48 83 ec 08 sub $0x8,%rsp
4003d4: 48 8b 05 1d 0c 20 00 mov 0x200c1d(%rip),%rax # 600ff8 <__gmon_start__>
4003db: 48 85 c0 test %rax,%rax
4003de: 74 05 je 4003e5 <_init+0x15>
4003e0: e8 1b 00 00 00 callq 400400 <__gmon_start__@plt>
4003e5: 48 83 c4 08 add $0x8,%rsp
4003e9: c3 retq
.... 省略部分内容 .....
000000000040050d <main>:
int sum(int, int);
int data = 20;
int main()
{
40050d: 55 push %rbp
40050e: 48 89 e5 mov %rsp,%rbp
400511: 48 83 ec 10 sub $0x10,%rsp
int a = gdata;
400515: 8b 05 15 0b 20 00 mov 0x200b15(%rip),%eax # 601030 <gdata>
40051b: 89 45 fc mov %eax,-0x4(%rbp)
int b = data;
40051e: 8b 05 08 0b 20 00 mov 0x200b08(%rip),%eax # 60102c <data>
400524: 89 45 f8 mov %eax,-0x8(%rbp)
int ret = sum(a,b);
400527: 8b 55 f8 mov -0x8(%rbp),%edx
40052a: 8b 45 fc mov -0x4(%rbp),%eax
40052d: 89 d6 mov %edx,%esi
40052f: 89 c7 mov %eax,%edi
400531: e8 0a 00 00 00 callq 400540 <_Z3sumii>
400536: 89 45 f4 mov %eax,-0xc(%rbp)
return 0;
400539: b8 00 00 00 00 mov $0x0,%eax
}
40053e: c9 leaveq
40053f: c3 retq
0000000000400540 <_Z3sumii>:
400540: 55 push %rbp
400541: 48 89 e5 mov %rsp,%rbp
400544: 89 7d fc mov %edi,-0x4(%rbp)
400547: 89 75 f8 mov %esi,-0x8(%rbp)
40054a: 8b 45 f8 mov -0x8(%rbp),%eax
40054d: 8b 55 fc mov -0x4(%rbp),%edx
400550: 01 d0 add %edx,%eax
400552: 5d pop %rbp
400553: c3 retq
400554: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40055b: 00 00 00
40055e: 66 90 xchg %ax,%ax
.........
我们可以看到,在 .o 文件中没有确定的地址已经被重定向至正确的地址。
mov 0x200b15(%rip),%eax // int a = gdata;
mov 0x200b08(%rip),%eax // int b = data;
程序头部信息
readelf -l main
显示程序头表信息,包扩有几个段,每个段的属性,以及每个段中包含有哪几个节(Section)
Elf file type is EXEC (Executable file)
Entry point 0x400420
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000744 0x0000000000000744 R E 200000
LOAD 0x0000000000000de0 0x0000000000600de0 0x0000000000600de0
0x0000000000000254 0x0000000000000258 RW 200000
DYNAMIC 0x0000000000000df8 0x0000000000600df8 0x0000000000600df8
0x0000000000000200 0x0000000000000200 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000005f0 0x00000000004005f0 0x00000000004005f0
0x000000000000003c 0x000000000000003c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000de0 0x0000000000600de0 0x0000000000600de0
0x0000000000000220 0x0000000000000220 R 1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
可以看到在程序的头部信息中,有两个 LOAD(加载器),在执行程序时分别对指令和数据进行加载。
Entry point 0x400420
是程序执行的入口点,也就是程序开始执行的地方。
总结
总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。
在程序运行时,通过两个加载器把程序的指令和数据加载至虚拟内存空间中。虚拟空间的用户程序并不直接被加载到真实的物理内存上,而是把该程序需要被访问的部分映射到真实的物理地址上,而暂时没有被访问到的部分任然存在于虚拟内存中。通过这样的方式我们发现每个程序的虚拟地址空间可以远大于实际的物理地址空间,使之可以运行远大于自身内存的程序,同时虚拟地址空间也可以小于实际的物理地址空间,使之可以同时运行多个程序。