一.编译和链接
1.预处理
命令:gcc -E hello.c -o hello.i
主要处理.c文件中以“#”开头的预编译指令
2.编译
命令:gcc -S hello.i -o hello.s
[1]词法分析
[2]语法分析
[3]语义分析
编译器只能分析静态语义(编译期确定的语义)
静态语义有声明,类型转换,类型匹配
[4]优化后生成相应的汇编代码文件
中间语言生成,目标代码生成与优化。
3.汇编
命令:gcc -c hello.s -o hello.o
汇编器是将汇编代码转化成机器可以执行的指令。
4.链接
重定位:绝对地址引用的位置“打补丁”,使其指向正确的地址
符号:函数或变量的起始地址
[1]地址和空间分配
[2]符号决议
[3]重定位
二.目标文件
基础知识:
1】可执行文件格式有windows下的PE和linux下的ELF,都是COFF格式的变种。
2】静态链接库(windows下的.lib,linux下的.a)动态链接库(windows下的.dll,linux下的.so)都按可执行文件格式存储。
为何将可执行文件的代码段和数据段分开存放?
1】代码段只读,数据段可读可写,有利于分别保护
2】现代cpu的缓存被设计为指令缓存和数据缓存分离,分开存放可提高cpu的缓存命中率。
3】运行多个进程时,有各自的数据段,共享代码段,节省内存
程序示例:
查看目标文件的结构和内容:
目标文件 段的基本分布
1.代码段(.text)
存放机器指令
2.数据段(.data)
存放已经初始化的静态变量,全局变量
3.只读数据段(.rodata)
存放只读数据,一般为只读变量(const修饰的变量)和常量字符串
4.数据段(.bss)
存放未初始化或初始化为0的静态变量,全局变量
因为数据全为0,.data段存储数据0是没有必要的,因此在目标文件中.bss是预留的,没有内容,不占内存空间,运行时的确占内存空间
注:未初始化的全局变量在.comment段,故.bss的大小为0x14=20字节,并非24字节。
ELF文件结构描述
1.文件头
2.段表:描述每个段的基本信息
编译器,链接器,装载器都是通过段表来访问和定位段的属性的
ELF32_Shdr段描述符结构:每一个ELF32_Shdr结构体对应一个段
mian.o的段表及所有段的位置和长度
注:以2^2=4字节对齐,故有一小部分空余。
3.重定位表
.rel.text是针对.text的重定位表。在.text段有绝对地址的引用,那就是printf函数。.data段包含几个常量,没有绝对地址的引用。
4.字符串表
字符串表(.strtab):保存普通的字符串,比如符号名。
段表字符串表(.shstrtab):保存段表中的字符串,比如段名。
链接的接口----符号
1】在链接中,目标文件的相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。
比如目标文件B用到了目标文件A中的foo函数,则称目标文件A定义了foo函数,目标文件B引用了目标文件A中的foo函数。(同样适用于变量)
2】在链接中,将函数和变量统称为符号,函数名和变量命为符号名。
3】每一个目标文件有一个相对应的符号表。
符号表记录了目标文件的所有符号,每一个符号对应一个符号值。对函数和变量来说,符号值就是它们的地址。
符号的类型
1】全局符号 @@@@@@链接过程只关心全局符号的相互粘合。
1)定义在本目标文件的,可以被其他目标文件引用。eg:main,gdata1,gdata2,gdata3
2)外部符号:没有定义在本目标文件,在本目标文件中引用。eg:printf
2】局部符号
只在编译单元内部可见,对于链接过程没有作用。eg:d.e.f,gdata4,gdata5,gdata6
5.符号表(.symtab)
符号修饰与函数签名
1】为了避免库文件中的函数和全局变量名与目标文件中的名字起冲突,函数经编译后要在符号名前加"_"。eg:foo----->_foo (C语言)
2】名称空间:解决多模块的符号冲突问题 (c++)
3】c++符号修饰
函数签名:用于识别不同的函数。包含了函数的所有信息,包括函数名,参数列表,它所在的名称空间和类。
c++编译器在编译时会将函数(函数签名)和变量的名字进行修饰,形成符号名。
extern"c" 符号的引用
c++编译器会将 extern"c" 大括号内部的代码当作C语言代码处理。
C语言不支持 extern "c" 语法,为兼容C语言和c++定义两套头文件,c++的宏"_cplusplus",c++编译器在c++编译程序时默认调用该宏。
弱符号与强符号-----》针对符号的定义,并非符号的引用。 (只适用于C语言)
1】 强符号:函数和初始化了的全局变量。弱符号:未初始化的全局变量。(在.COMMON块)
2】链接器按如下规则处理不同目标文件中重复定义的符号:
1)同名强符号,编译错误。
2)同名强,弱符号,选择强符号。
3)同名弱符号,选择占用内存大的。
3】强引用和弱引用:
强引用:若没有找到符号的定义,链接器会报符号未定义的错误。
弱引用:若符号有定义,链接器将该符号的引用决议。若没有定义,链接器不会报错。主要用于库的链接过程。
为何将未初始化的全局变量放在.comment段,不放在.bss段?????
答:未初始化的全局变量放在.comment段只针对编译后的目标文件。在链接时,两个目标文件链接为一个可执行文件,若两个目标文件出现了同名的弱符号,则选择内存占用大的,实际上未初始化的全局变量在链接后是放在.bss段的(此时已经选择出了占用内存大的弱符号)。而在编译时,并不确定在别的源文件中是否有同名的弱符号,不可确定其最终的大小,因此将未初始化的全局变量暂时存放在.comment段。
三.静态链接
空间与地址分配
1】相似段合并:相同性质的段进行合并,obj文件以2^2=4字节对齐,合并后以页面(4k)对齐。
.bss段不占目标文件和可执行文件的空间,装载时为其分配空间,其只有虚拟地址空间。
2】调整段偏移和段长度,合并符号表。
程序示例:
a.c b.c编译为目标文件a.o b.o
a.o b.o 链接为ab可执行文件
查看链接前后地址分配情况:
符号解析与重定位
1】重定位
2】重定位表
3】符号解析
链接时符号未定义的原因:1)链接时缺少了某个库。2)输入目标文件路径不正确。3)符号的声明与定义不一样。
链接器扫描完所有输入目标文件后,目标文件中未定义的符号应该能够在全局符号表中找到,否则链接器报符号未定义错误。
(所有obj符号表中对符号引用的地方要找到符号定义的地方)
四.可执行文件的装载与进程
可执行文件只有装载到内存才能被CPU执行。
1.进程虚拟地址空间
1】进程和程序的区别:
程序:静态的概念,预先编译好的数据和指令的集合的文件。
进程:动态的概念,运行中的程序。
2】虚拟地址空间的大小与CPU的位数有关。
32位CPU大小为2^32=4G
硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小。
2.装载的方式
1】静态装入:将程序运行时需要的指令和数据全部加载到内存中执行。
2】动态装入:程序所需要的内存大于物理内存。
思想:程序需要哪个模块,就把哪个模块装入内存,如果不需要,就将其存放在磁盘。
1)覆盖装入
2)页映射 页面置换算法:FIFO LRU
3.从操作系统的角度看可执行文件的加载
1】进程的建立
1)创建独立的虚拟地址空间。
页映射函数:创建虚拟地址空间到物理空间的映射关系。
2)读取可执行文件头,建立可执行文件与虚拟地址空间的映射。
程序执行发生页错误时,操作系统在物理内存中分配一个物理页,将缺页从磁盘读取到物理内存,建立虚拟页到物理页的映射关系。同时操作系统要知道缺页位于可执行文件的哪个位置,于是建立可执行文件与虚拟地址空间的映射关系。
可执行文件又叫映像文件。
Linux中将虚拟地址空间的一个段叫做虚拟内存区域(VMA)。
3)将CPU的指令寄存器设置为可执行文件的入口地址,启动运行。
2】页错误
4.进程虚存空间分布
1】ELF文件链接视图和执行视图
段数量增多时,为减少空间浪费,可执行文件到虚拟地址空间的映射时,对于相同权限的段,合并到一起当作一个段进行映射。映射到同一个VMA。
合并后的一个段叫做"segment",其中包含一个或多个属性类似的"section"。
从链接的角度,可执行文件按“section”存储,可执行文件为链接视图。从装载的角度,可执行文件按“segment”划分,可执行文件为执行视图。
目标文件链接成可执行文件时,链接器尽量将相同权限属性的段分配在同一空间。可执行文件映射时,是以“segment”来映射的。
正如描述“section”属性的结构叫做段表,而描述“segment”属性的结构叫做程序头(program header)。描述了ELF文件该如何被操作系统映射到进程的虚拟空间。
ELF可执行文件与进程虚拟空间的映射关系
2】堆和栈
通过查看/proc来查看进程虚拟空间分布:
进程虚拟地址空间的概念:操作系统通过将进程划分成一个个VMA来管理进程的虚拟空间,基本原则是将相同属性的,有相同映像文件的映射成一个VMA。
3】堆的最大申请数量
4】段地址对齐
各个段接壤的部分共享一个物理页面,然后将该物理页面分别映射两次。
4】进程栈初始化
5】Linux内核装载ELF过程简介
详情请参见《程序员的自我修养-链接,装载与库》