程序的编译链接过程复习
//参考《程序员的自我修养》
本文将以下面代码为例说明问题:
#include <stdio.h>
//g是global的意思,全局
int gdata1 = 10;
int gdata2 = 0;
int gdata3 ;
//gdata3是弱符号,编译的时候不知道要不要用到这个符号,因为不知道有没有同名的强符号
//所以暂且放到comments段,链接合并完符号表,发现并没有强符号,就会把这个数据放回
//到bss段
//以上部分是数据
static int gdata4 = 1;
static int gdata5 = 0;
static int gdata6;
//以上部分是数据
int main()
{
#l是local的意思,代表局部
int ldata7 = 100;
int ldata8 = 0;
int ldata9;
//从main(包含main)到这里,这一部分是指令
static int ldata10 = 8;
static int ldata11 = 0;
static int ldata12;
//以上部分是数据
//从这里以下的部分都是指令
return 0;
}
//函数名也会产生符号
第一部分内容:虚拟内存的概念
程序的加载过程:磁盘 ——> 内存——> cpu
加载什么东西:指令 (instructions)+ 数据(data)
操作系统作用的简述:(厂家不同,会有差异)
屏蔽I/O之间的差异,为了调用统一的接口,操作系统有VFS:虚拟文件管理系统
屏蔽内存和I/O间的差异,为了统一,操作系统有虚拟存储器(虚拟内存)来管理物理内存的方案
屏蔽cpu、内存和IO间的差异,为了统一资源调度单位,出现了 进程概念
虚拟内存: x86 32 linux内核
多大:·2^32
与CPU的位数有关,也就是ALU算术逻辑单元的最大宽度,即就是数据总线的条数
在哪里:
不存在,是虚拟的 ~ 逻辑上抽象出来的 ~
什么用:
通过虚拟内存来管理物理内存,每个进程都会有
每个程序只要一运行,就会给分配虚拟内存,而不是真正的物理内存
这个虚拟内存我们一般又叫做虚拟地址空间:
bss和data有什么区别:
初始化了的数据,且初始化不为0的,放在data段
没有初始化和初始化为0数据存放在bss
bss节省了谁的空间:
虚拟地址的空间还是文件的空间?节省的是文件的空间,bss段在虚拟空间一定占
bss段其实没需要存,因为这些值不论是0还是没有初始化,值永远是0,所有根本不需要存储
但是为了让操作系统知道有这种数据,就将这些数据的信息纪录在执行文件的section table中
我们通过ELF Header找到section table然后找到bss数据的信息即可
用户空间和内核空间哪个是共享的
用户空间是每个进程各自拥有各自的,而内核空间只有一份,所有进程共享同一个内核空间
int main(int argc , char ** argv , char ** environ)
{
//argc:命令行参数的个数,默认是一个,当前可执行程序的绝对路径
for (int i = 0, i < argc;++i)
{
puts(argv[i])
//这里链接了内存中的动态库
}
//argv 就是我们的命令行参数
//environ存放的是环境变量,如我们使用的库,头文件等的路径
//argv environ在stack的高内存中存储
}
第二部分内容:编译和链接的过程
预编译:文本替换,删除注释等 ——> main.i
编译 :语法、语义、代码优化、汇总所有的符号到符号表——> main.s (汇编指令)
汇编 :将汇编码翻译为01机器码——>main.o(机器指令) 即:二进制可重定位的目标文件
链接 :
1.合并所有的obj段,并调整段偏移,合并符号表
2.核心:符号重定位——>可执行文件
main point:
1.为什么汇编得到的文件不能执行?
2.链接的意义何在?
3.符号表是做什么的?
4.可执行文件格式,cpu如何知道从哪里开始执行?
查看main.o文件都有什么:
1.生成目标文件
gcc -o main.cpp
2.查看目标文件有什么
objdump -h maim.o
main.o: 文件格式 elf64-x86-64 // 由于博主装了64位的系统,但是理论上基本一样
字节:
Idx Name Size VMA LMA File off Algn
0 .text 00000019 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 0000000c 0000000000000000 0000000000000000 0000005c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000018 0000000000000000 0000000000000000 00000068 2**2
ALLOC
3 .comment 0000002b 0000000000000000 0000000000000000 00000068 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 0000000000000000 0000000000000000 00000093 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 0000000000000000 0000000000000000 00000098 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
我们关注目标文件的一个点:
所有数据的地址为0,因此我们从这里可以得到mian.o中我们没有给变量分配地址
(其实是链接的时候才会给分配内存地址,不过这个是虚拟地址空间上的地址)
我们看一下main.o文件的大概组成:
查看elf文件中保存的内容:
readelf -h main.o
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 880 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 0 (字节)
Number of program headers: 0
节头大小: 64 (字节)
节头数量: 11
字符串表索引节头: 10
关注两个点:
1.类型:REL 可重定位文件
2.入口地址:0x0
(单单从以上两点就足以说明,可重定位的文件REL不可以执行,即main.o文件不能执行原因)
原因:入口地址是不可访问的,变量还没有给分配虚拟地址
我们再来看main.o中的具体内容:
我们可以通过进制转换看到我们数据的内容
objdump -s main.o
main.o: 文件格式 elf64-x86-64
Contents of section .text:
0000 554889e5 c745f864 000000c7 45fc0000 UH...E.d....E...
0010 0000b800 0000005d c3 .......].
Contents of section .data:
0000 0a000000 01000000 08000000 ............
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.
0010 332e302d 32377562 756e7475 317e3138 3.0-27ubuntu1~18
0020 2e303429 20372e33 2e3000 .04) 7.3.0.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 19000000 00410e10 8602430d .........A....C.
0030 06540c07 08000000 .T......
其中,text可读可执行,data可读可写的,
rodata是只读不可写的,保存着如常量字符串等
查看段表中的内容:section table
readelf -S main.o
There are 11 section headers, starting at offset 0x370:
节头:
[号] 名称 类型 地址 偏移量
大小 全体大小 旗标 链接 信息 对齐
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000019 0000000000000000 AX 0 0 1
[ 2] .data PROGBITS 0000000000000000 0000005c
000000000000000c 0000000000000000 WA 0 0 4
[ 3] .bss NOBITS 0000000000000000 00000068
0000000000000018 0000000000000000 WA 0 0 4
[ 4] .comment PROGBITS 0000000000000000 00000068
000000000000002b 0000000000000001 MS 0 0 1
第三部分内容:链接的过程
comments段是干什么:存放未决定的符号(弱符号)
链接的时候我们只链接全局符号,如果一个普通全局的变量不初始化,为弱符号,编译的时候不知道其他的问是否要用,所以干脆放到comments段中。
(有的较新的系统在这一方面有着不同的处理,这里只是参考)
链接的2个过程:重点
1.合并各个obj文件中的各个相同属性的段,组织在一个页面上,调整段起始偏移和段长度,合并各个.obj对应的符号表,最终进行符号解析
(A文件引用了B文件的符号,符号解析就是找到B引用A符号的定义的地方,
如果找不到,就发生链接错误:undefined symbol)
我们先来看一下符号表:数据是哪些一目了然,也可以看出局部变量不是数据
obidump -t main.o
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
0000000000000004 l O .data 0000000000000004 _ZL6gdata4
0000000000000008 l O .bss 0000000000000004 _ZL6gdata5
000000000000000c l O .bss 0000000000000004 _ZL6gdata6
0000000000000008 l O .data 0000000000000004 _ZZ4mainE7ldata10
0000000000000010 l O .bss 0000000000000004 _ZZ4mainE7ldata11
0000000000000014 l O .bss 0000000000000004 _ZZ4mainE7ldata12
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 gdata1
0000000000000000 g O .bss 0000000000000004 gdata2
0000000000000004 g O .bss 0000000000000004 gdata3
0000000000000000 g F .text 0000000000000019 main
2.进行符号的重定位,即就是给每个符号分配内存地址(虚拟空间上的地址)
(编译过程中,数据的地址都是0,函数的入口地址都是一些不可访问的地址,也就是说编译过程中的地址都是无效的,这也就是为什么需要链接的第二步过程)
第四部分:二进制可执行文件的组成
我们来看一下ELF Header中的内容:
readelf -h main
ELF 头:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
类别: ELF64
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: DYN (共享目标文件)
系统架构: Advanced Micro Devices X86-64
版本: 0x1
入口点地址: 0x4f0
程序头起点: 64 (bytes into file)
Start of section headers: 6704 (bytes into file)
标志: 0x0
本头的大小: 64 (字节)
程序头大小: 56 (字节)
Number of program headers: 9
节头大小: 64 (字节)
节头数量: 28
字符串表索引节头: 27
同样的我们只需要关注两个点:
程序的入口地址:0x4f0 不再是可重定位文件中的0地址
类别:为ELF
我们再来看一下main文件的内容:
objdump -d main
//我们将截取一部分主要的内容来说明问题:
Disassembly of section .text:
00000000000004f0 <_start>:
4f0: 31 ed xor %ebp,%ebp
4f2: 49 89 d1 mov %rdx,%r9
......
结合main主要内容和EFL Header内容来看:
程序的入口地址 == text指令开始执行的地方
换一种说法也就是说为什么程序每次从main函数开始执行,因为ELF Header中保存的就是main函数的入口地址
可执行文件内容:
ELF问 比. obj 文件多了个program header段,该段存储这两个Load项,将属性相同的段映射到虚拟地址空间。
第五部分:程序的运行
./main
1.创建虚拟地址空间到物理内存的映射(创建内核地址映射结构体),创建页目录和页表
2.加载代码段 和 数据段到内存中
3.将可执行文件的入口地址写到cpu的pc寄存器即可。
磁盘上文件映射到物理内存的过程:
DP磁盘数据通过(mmap函数) ——> VP 虚拟地址空间页面 (通过多级页表映射) ——> PP物理内存
跟踪一个进程的执行过程:
strace ./main 跟踪程序执行过程
execve("./main", ["./main"], 0x7ffd38b10830 /* 64 vars */) = 0
brk(NULL) = 0x562fc2b87000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=83928, ...}) = 0
mmap(NULL, 83928, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f87ae55f000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f87ae55d000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f87adf5c000
mprotect(0x7f87ae143000, 2097152, PROT_NONE) = 0
mmap(0x7f87ae343000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f87ae343000
mmap(0x7f87ae349000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f87ae349000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7f87ae55e4c0) = 0
mprotect(0x7f87ae343000, 16384, PROT_READ) = 0
mprotect(0x562fc10d6000, 4096, PROT_READ) = 0
mprotect(0x7f87ae574000, 4096, PROT_READ) = 0
munmap(0x7f87ae55f000, 83928) = 0
exit_group(0) = ?
+++ exited with 0 +++
从上面的执行结果来看:
有我们熟悉的进程替换函数execve,进程的起源
也有我们熟悉mmap函数,在这里mmap作用就是将磁盘上的执行文件给虚拟地址空间上映射
包括open、read以及close等函数