目录
编译和链接
预编译: gcc -E hello.c -o hello.i
预编译过程主要是处理#打头的预编译指令。
它会删除所有的#define,并展开所有宏定义
处理所有条件的预编译指令,例如#if,#elif, #ifdef, #else, #endif
处理#include 预编译指令,将被包含的头文件插入引用位置
删除所有注释
不过它会保留#pragma预编译指令,因为编译器必须要使用它们
(当你不清楚代码究竟引用了那个头文件时,可以通过查看预编译后的文件找到结果)
编译: gcc -S hello.c -o hello.s / gcc -S hello.i -o hello.s
编译就是将预编译完的文件进行一系列 词法分析,语法分析,语义分析以及优化后生成相应的汇编代码
词法分析:分析代码,识别每行代码中每个字符的意义,例如识别括号,运算符号,标识符(变量),数字等
语法分析:将代码转成一颗以表达式为节点的语法树。例如+为节点,左右两个节点则是相加的值。等于号为根节点的话,左右两子节点则是参与运算的两个子表达式。
语义分析:分为静态语义和动态语义,静态语义是在编译期就可以确定的,例如声明和类型的匹配,类型的转换等。动态语义是在运行过程中出现的语义问题,例如除0
中间语言生成:编译器有很多层次的优化,这里指的是源码级别的优化。不同的编译器会有不同的实现。中间代码使得编译器分为前端和后端。前端负责产生和机器无关的中间代码,而后端则将中间代码转换成目标机器代码。
目标代码生成与优化:这主要是编译器后端的工作。它由目标代码生成器和目标代码优化器组成。如果生成的目标代码里面有变量定义在其他的模块中,那么这些变量地址都必须要在链接时候才能确定。
汇编: gcc -c hello.c -o hello.o / as hello.s -o hello.o
汇编就是讲汇编代码转成机器可以执行的指令。它无需做其他处理和优化,只需要根据汇编-机器指令表一一翻译即可。
链接:
很多时候我们的工程都是由多个源文件组成,编译以后生成多个模块(.o),将这些模块拼接成目标文件的过程就是链接。
链接的主要过程包括:地址和空间分配(address and storage allocation),符号决议(symbol resolution),重定位(relocation)。
重定位:两个模块链接的时候,将本模块中引用的外部模块地址修正的过程就成为重定位。
ld -static /xxx/xxx/a1.o /xxx/xxx/a2.o -L/xxx/libpath1/ -L/xxx/libpath2/ -llib1 -llib2
目标文件里有什么
经过编译器编译但是还未经过链接的可执行文件格式成为目标文件。
ELF : excutable linkable format,.o,.a,.so,.exe, .coredump都属于ELF格式文件。
ELF文件可以分为两部分: ELF header和内容部分。
ELF内容部分有很多个段组成,比较重要的几个段包括:
1, .text(.code) :编译以后的机器指令就放在代码段。除数据以外的都算代码数据
2,.data:已经初始化的全局变量和局部静态变量都放在数据段
3,.bbs:未初始化的全局变量和静态局部变量放在bbs段。例如static int a=0也当成是未初始化的放在.bbs
4,.rodata:只读代码段。例如字符串常亮,或者const修饰的变量都会放在里面。它不允许程序去做修改,保证了程序的安全性。
-
可以通过 objdump -h hello.o 查看文件内部结构,通过size hello.o可以查看每个段的长度。
-
objdump -s(打印所有段的内容以16进制表示) -d(将所有包含指令的段的反汇编) hello.o
-
readelf -h hello.o 查看ELF文件header部分。
-
ELF中有很多段,这个段表就保存着这些段的基本结构。可以说ELF的段结构就是由段表决定的,而编译器,链接器,装载器都是通过段表来定位和访问各个段的树形的。
-
对于每一个需要重定位的代码段或者数据段都会包含一个.rel.XXX,例如.text的重定位表为.rel.text。
-
ELF文件中本身就用到了很多字符串,例如段名,变量名等。由于他们的长度是不固定的,所以很难用固定的结构去存放,所以引入一个新的概念,字符串表。常见的段名为.strtab。
-
命名空间主要用于解决多个模块符号冲突的问题。
-
C++下所有的符号都以_Z打头,后面接N然后是各个命名空间和类的名字,在每个名字之前还会有一个数字标识名字的长度。然后以E结尾,当然如果带参数的话,参数列表会紧跟在E后面。 正因为名字之前还有数字标识名字长度,所以要求命名时不能以数字打头。
-
C语言编译以后的符号不会增加下划线。所以一份C代码如果想提供给C++程序调用的话,需要在代码声明的外层包裹extern “C”{ ...define… },但是由于C语言不支持extern “C” { …define… } 语法,所以一般会通过编译宏来兼容。
-
#ifdef __cplusplus
-
extern “C” {
-
#endif
-
void *memset(void* , size_t t)
-
#ifdef __cplusplus
-
}
-
#endif
-
-
编译器规定函数和已经初始化的全局变量成为强符号,未初始化的全局变量称为弱符号。
-
不允许强符号定义多次
-
在强符号和若符号决议时,选择强符号
-
当所有目标符号都是弱符号时,选择占用空间最大的那个,例如int 和 double选择double
-
弱符号和弱引用的技巧一般使用在库中,即删除某些功能以后程序可以正常连接。
-
引入静态库中的某个函数a时,实际上会把静态库中的包含a的整个目标文件a.o包含进来。所以一些公共的库例如libc.a里面printf.o里面只包含了printf方法,这种做法就是为了避免引入一些不需要使用的方法。
可执行文件的装载和进程
-
可执行文件(程序)是一个静态的概念,他是一些预编译好的指令的集合。而进程是一个动态的概念,他是程序运行时的一个过程。
-
每个进程运行以后,都拥有自己的虚拟内存地址。VMA virtual memory address。
-
32未平台上,4G内存中,有1G是内核使用的0xC00000000-0xFFFFFFFFF,剩下3G 0x000000000-0xBFFFFFFFF可供用户使用。
-
动态装载的原理是将最常用的部分留在内存中,而将一些不常用的数据存放在磁盘中。
-
目前常用的装载方式为 页映射。他将内存和磁盘中的数据和指令按照页为单位划分成若干个子页,然后装载和操作的单位就是 页。 不同的处理器页大小不一致,一般有4k,8k,2m,4m等。
-
创建进程的过程:
-
创建一个独立的虚拟地址空间
-
读取可执行文件头,并建立虚拟地址空间和可执行文件的映射关系
-
将cpu的指令寄存器设置为可执行文件的入口,启动运行
-
PS:其实执行以上三步以后,并没有将可执行文件的指令都装载到内存中,它只是建立虚拟地址空间和可执行文件的映射关系,等程序时会发现找不到某指令的地址(因为是虚拟地址),所以会发生缺页错误(MMU),然后在物理空间中分配一个物理页面,将它和虚拟页映射起来,然后进程就可以从刚才报错的地方继续往下执行。
-
-
由于一个正常的进程可能会处理很多段(包括代码段数据段等),如果每个段都通过页映射的方式从内存中换入换出,则有可能产生空间浪费的问题,因为很多时候只需要加载1k的数据,结果段的大小可能是2m。所以在实际操作系统中一般会将段按照权限合并,对于有相同权限的段合并到一个段中执行。而多个section组成的一个segment。实际装载换页的时候是以segment为单位进行的。
-
segment在装载的时候,系统还有做一些优化,能够避免装载时空间利用率低的问题。
-
操作系统通过VMA来对进程的地址空间进行管理。另外进程还有可能用到堆和栈,事实上他们在进程的虚拟空间中也是以VMA的形式存在的。
-
进程在初始化时,会在堆栈内构造环境变量和命令行参数数据,然后将其传递给main函数。
-
linux内核装载ELF的过程:
-
fork( )系统调用创建一个新进程
-
新的进程调用execve( )系统调用执行制定的ELF文件
-
原先的bash进程继续返回等待刚才启动的新进程的结束
-
-
静态链接
-
链接:ld a.o b.o -e main -o ab.out
-e main表示将main函数作为函数入口,ld链接器默认的程序入口是_start。
-static 表示按照静态链接的方式链接程序,否则默认是用动态链接的方式。
-s 表示禁止产生符号表。它的效果和strip命令一致。
objdump -d a.o 查看a.o代码段反汇编的结果。
1,空间与地址分配
-
多个目标文件链接生成最终输出文件时,一般将文件的相同类型段合并到最终输出文件中。例如将a.o b.o c.o的.text .data合并到dst.out 的.text 和 .data中。
-
VMA即虚拟空间地址,在目标文件中(.o),VMS还没有被分配,所以默认都是0。通过objdump -h a.o可以查看。链接以后虚拟地址就分配了。
-
地址空间分配是指虚拟地址空间的分配。例如.bss段在目标文件和最终输出文件中并不占用文件的空间,但是他在装载时占用地址空间,所以链接器在合并各段的同事,也需要将.bss合并并分配虚拟空间。
-
在合并所有段的过程中,会获取各个段的长度,属性,位置,并将所有文件符号表中的符号定义和引用手机起来,统一放在一个全局符号表中,并建立映射。在收集完这个信息以后,就会进行符号的解析和重定位。
-
链接时会给各个段分配虚拟地址,但是段内的符号已经是确定了的,在分配地址后每个符号加上各自在段内的偏移即得到最终符号的地址。
-
2,符号解析和重定位
-
之前提过每个目标文件中,如果某个段有需要重定位的符号,那么在目标文件中会相应建立一张重定位表。例如a.out的.text中的重定位信息保存在.rel.text中。
-
通过objdump -r a.o 可以查看a.o中的重定位表。
-
符号解析是指连接器可能需要对某个符号的引用进行重定位时,他需要确定这个符号的目标地址。这是他就需要去查找全局符号表,找到相应的符号以后再做重定位。
-
重定位寻址修正方式在x86下可以分为绝对寻址修正和相对地址修正两种
-
重定位的过程是去不停地扫描每一个目标文件然后吧符号加到重定位表中,然后在扫描后面的目标文件时寻找目标undefine符号,所以被依赖的目标文件或者库需要放在右边。