上帝模式看程序从出生到死亡

529 查看

111862021-5e9d18823bc285bb

进程的一生

GitHub : Jerry4me


前言

我们中有许多程序员打码几年还没有搞清楚一个程序从源代码 -> 可执行程序 -> 执行 -> 死亡, 经历了什么变化. 他们只知道, 编译, 链接, 运行…由于强大的IDE已经帮我们把这些过程屏蔽掉了, 我们不知道底层他们干了什么. 但是我们只有明白这些运行机制和机理, 才能解决一些莫名其妙的错误, 提升性能瓶颈.

笔者在看了>这本书后决定把这些过程用比较简单易懂的文字叙述出来, 如有不对的地方还请各位指出, 谢谢~


目录

预编译
编译
汇编
目标文件
链接
可执行文件
装载
动态链接(需要的话)
运行
死亡
小知识


编译

编译又分为 预处理(Preprocessing), 编译(Compilation)汇编(Assembly).

预编译

预编译过程主要处理源代码文件那些#开头的预编译指令

编译

编译过程可分为6部 : 扫描, 语法分析, 语义分析, 源代码优化, 代码生成和目标代码优化.

汇编

汇编器将汇编代码转换成机器可以执行的指令, 输出目标文件. 该过程比较简单, 就是翻译代码.

经过上述多个步骤, 源代码终于被编译成了目标文件. 这个目标文件肚子里又卖的是什么药呢? 我们接着看~

目标文件

由于不同的操作系统下, 目标文件, 可执行文件等都有些出入. 本文是用Linux系统下的ELF文件作为例子

编译之后生成的目标文件内容肯定少不了机器指令代码, 数据等. 不过除了这些之外, 目标文件还包括了链接时所需的一些信息, 而目标文件将这些信息按照不同的属性, 以段(Section)来存储.

Question :
为什么要把数据和指令分开呢? 经典的冯诺依曼体系不是不分指令还是数据的吗?

Answer :
1 : 当程序被装载后, 数据和指令被映射到两个虚存区域. 数据区域对于进程而言, 是可读写的, 而指令区域则只可读. 这样方便分别设置他们的权限, 防止程序指令被恶意修改
2 : 把指令和数据分开有利于提高程序的局部性, 对于提高CPU缓存命中率有帮助
3 : 最重要的原因, 当系统中运行着多个该程序的副本时, 他们的指令都是一样的, 所以进程之间能共享指令和其他只读数据, 而数据区域则为进程私有. 如果系统中运行了数百个进程, 可以想象共享为我们节省了多少空间

这里插一句 : 其实不是可执行文件才才按照执行文件的格式存储. 什么意思呢? 除了可执行文件之外, 目标对象, 动态链接库, 静态链接库也按照可执行文件的格式存储. 某种程度上他们也是可执行文件. 所以我们可以把他们视为同一类文件

目标文件有什么

ELF文件头(ELF Header)

包含了整个文件的基本属性

段表(Section Header Table)

描述了ELF文件包含的所有段的信息

重定位表

链接器在处理目标文件时, 要对目标文件中某些符号进行重定位, 即代码段和数据段那些对绝对地址引用的符号. 这些重定位信息就记录在重定位表中.

符号表

在链接中, 我们将函数和变量统称为符号(Symbol), 函数名和变量名称为符号名(Symbol Name). 符号表记录着该目标文件所用到的所有符号, 每个符号都有一个对应的值, 符号值(Symbol Value), 对于函数和变量来说, 符号值就是他们的地址.

强符号与弱符号, 强引用与弱引用

如果在目标文件A和目标文件B都定义了一个全局变量global, 并将他们都初始化. 那么链接的时候就会报multiple definition of 'global'的错误. 这种符号就是强符号. 默认所有符号都是强符号, 可以使用GCC的__attribute__ ((weak))定义一个弱符号.

强符号与弱符号的规则 :

符号引用被最终链接的时候必须要被正确决议, 如果没有找到该符号的定义, 就会报符号未定义错误undefined symbol of xxx, 这种称为强引用(Strong Reference). 而弱引用(Weak Reference)则被处理的时候如果未定义, 不报错, 链接器会默认其为0或者是一个特殊值. 默认都是强引用, 可以使用GCC的__attribute__ ((weakref))定义一个弱引用.

弱符号和弱引用的作用 :

符号修饰和函数签名

很久之前, 编译器编译源代码产生目标文件时, 符号名与相应的变量和函数的名字是一样的, 例如函数foo, 经过编译后对应的符号名也是foo, 那么久会产生冲突, 例如要使用Fortran语言编写的目标文件, 一链接就会报错. 为了解决这种冲突, 规定C语言的全局变量和函数经编译后, 符号名前加上_, 此时foo编译后符号名为_foo. 但是还是不能完全解决C语言源文件之间链接产生的问题, 因为大家都有下划线啊! 于是C++开始设计的时候就考虑到了这个问题, 衍生出了命名空间(Name Space).

在C++中, int func()int func(int)int func(float)是三个不一样的函数, 这里我们引用一个术语函数签名(Function Signature), 函数签名包括一个函数的信息, 包括函数名, 参数类型, 所在的类和命名空间等其他信息. 于是, 以上三个函数编译后各自的符号名均不一样但是有规律可循.

链接

很久很久以前, 人们把所有代码写在一个文件中, 到后来, 人类已经没有能力维护这个程序了. 于是人们把代码根据功能或性质划分为不同的模块. 于是, 将这些模块拼接起来的过程就叫 : 链接

不知道大家看完上述的编译过程有没有这么一个疑问 : 如果编译的时候编译器不知道一个外部符号的地址, 怎么办? 答案就是不管, 先放一边, 等到链接的时候再把地址修正, 这就是重定位该做的事.

链接过程包括 : 地址和空间分配(Address and Storage Allocation), 符号决议(Symbol Resolution)重定位(Relocation).

静态链接

最基本的静态链接过程 : 把各个目标文件(.o文件)和库(Library)一起链接形成可执行文件.

那么他们每个文件中的段是怎么合并起来呢?

ELF用的就是相似段合并 : a的16/10/391e9426e1dbf10f5b41b71189bd1dc6.png" alt="111862021-5e9d18823bc285bb">

进程的一生

GitHub : Jerry4me


前言

我们中有许多程序员打码几年还没有搞清楚一个程序从源代码 -> 可执行程序 -> 执行 -> 死亡, 经历了什么变化. 他们只知道, 编译, 链接, 运行…由于强大的IDE已经帮我们把这些过程屏蔽掉了, 我们不知道底层他们干了什么. 但是我们只有明白这些运行机制和机理, 才能解决一些莫名其妙的错误, 提升性能瓶颈.

笔者在看了>这本书后决定把这些过程用比较简单易懂的文字叙述出来, 如有不对的地方还请各位指出, 谢谢~


目录

预编译
编译
汇编
目标文件
链接
可执行文件
装载
动态链接(需要的话)
运行
死亡
小知识


编译

编译又分为 预处理(Preprocessing), 编译(Compilation)汇编(Assembly).

预编译

预编译过程主要处理源代码文件那些#开头的预编译指令

编译

编译过程可分为6部 : 扫描, 语法分析, 语义分析, 源代码优化, 代码生成和目标代码优化.

汇编

汇编器将汇编代码转换成机器可以执行的指令, 输出目标文件. 该过程比较简单, 就是翻译代码.

经过上述多个步骤, 源代码终于被编译成了目标文件. 这个目标文件肚子里又卖的是什么药呢? 我们接着看~

目标文件

由于不同的操作系统下, 目标文件, 可执行文件等都有些出入. 本文是用Linux系统下的ELF文件作为例子

编译之后生成的目标文件内容肯定少不了机器指令代码, 数据等. 不过除了这些之外, 目标文件还包括了链接时所需的一些信息, 而目标文件将这些信息按照不同的属性, 以段(Section)来存储.

Question :
为什么要把数据和指令分开呢? 经典的冯诺依曼体系不是不分指令还是数据的吗?

Answer :
1 : 当程序被装载后, 数据和指令被映射到两个虚存区域. 数据区域对于进程而言, 是可读写的, 而指令区域则只可读. 这样方便分别设置他们的权限, 防止程序指令被恶意修改
2 : 把指令和数据分开有利于提高程序的局部性, 对于提高CPU缓存命中率有帮助
3 : 最重要的原因, 当系统中运行着多个该程序的副本时, 他们的指令都是一样的, 所以进程之间能共享指令和其他只读数据, 而数据区域则为进程私有. 如果系统中运行了数百个进程, 可以想象共享为我们节省了多少空间

这里插一句 : 其实不是可执行文件才才按照执行文件的格式存储. 什么意思呢? 除了可执行文件之外, 目标对象, 动态链接库, 静态链接库也按照可执行文件的格式存储. 某种程度上他们也是可执行文件. 所以我们可以把他们视为同一类文件

目标文件有什么

ELF文件头(ELF Header)

包含了整个文件的基本属性

段表(Section Header Table)

描述了ELF文件包含的所有段的信息

重定位表

链接器在处理目标文件时, 要对目标文件中某些符号进行重定位, 即代码段和数据段那些对绝对地址引用的符号. 这些重定位信息就记录在重定位表中.

符号表

在链接中, 我们将函数和变量统称为符号(Symbol), 函数名和变量名称为符号名(Symbol Name). 符号表记录着该目标文件所用到的所有符号, 每个符号都有一个对应的值, 符号值(Symbol Value), 对于函数和变量来说, 符号值就是他们的地址.

强符号与弱符号, 强引用与弱引用

如果在目标文件A和目标文件B都定义了一个全局变量global, 并将他们都初始化. 那么链接的时候就会报multiple definition of 'global'的错误. 这种符号就是强符号. 默认所有符号都是强符号, 可以使用GCC的__attribute__ ((weak))定义一个弱符号.

强符号与弱符号的规则 :

符号引用被最终链接的时候必须要被正确决议, 如果没有找到该符号的定义, 就会报符号未定义错误undefined symbol of xxx, 这种称为强引用(Strong Reference). 而弱引用(Weak Reference)则被处理的时候如果未定义, 不报错, 链接器会默认其为0或者是一个特殊值. 默认都是强引用, 可以使用GCC的__attribute__ ((weakref))定义一个弱引用.

弱符号和弱引用的作用 :

符号修饰和函数签名

很久之前, 编译器编译源代码产生目标文件时, 符号名与相应的变量和函数的名字是一样的, 例如函数foo, 经过编译后对应的符号名也是foo, 那么久会产生冲突, 例如要使用Fortran语言编写的目标文件, 一链接就会报错. 为了解决这种冲突, 规定C语言的全局变量和函数经编译后, 符号名前加上_, 此时foo编译后符号名为_foo. 但是还是不能完全解决C语言源文件之间链接产生的问题, 因为大家都有下划线啊! 于是C++开始设计的时候就考虑到了这个问题, 衍生出了命名空间(Name Space).

在C++中, int func()int func(int)int func(float)是三个不一样的函数, 这里我们引用一个术语函数签名(Function Signature), 函数签名包括一个函数的信息, 包括函数名, 参数类型, 所在的类和命名空间等其他信息. 于是, 以上三个函数编译后各自的符号名均不一样但是有规律可循.

链接

很久很久以前, 人们把所有代码写在一个文件中, 到后来, 人类已经没有能力维护这个程序了. 于是人们把代码根据功能或性质划分为不同的模块. 于是, 将这些模块拼接起来的过程就叫 : 链接

不知道大家看完上述的编译过程有没有这么一个疑问 : 如果编译的时候编译器不知道一个外部符号的地址, 怎么办? 答案就是不管, 先放一边, 等到链接的时候再把地址修正, 这就是重定位该做的事.

链接过程包括 : 地址和空间分配(Address and Storage Allocation), 符号决议(Symbol Resolution)重定位(Relocation).

静态链接

最基本的静态链接过程 : 把各个目标文件(.o文件)和库(Library)一起链接形成可执行文件.

那么他们每个文件中的段是怎么合并起来呢?

ELF用的就是相似段合并 : a的>.text和b的.text合并, a的.data与b的.data合并, 其他段类似.

符号决议和重定位

符号地址的确定

121862021-3ea8d5f769171c0b
符号地址的确定.png

重定位表

每个需要被重定位的段都有一个与之相对应的重定位表, 如.text段对应.rel.text

根据重定位表中每个符号的信息, 找到每个符号对应的目标对象文件, 再根据偏移(offset)确定其绝对地址(或相对地址).

动态链接

为什么有了静态链接还需要动态链接?