《程序员的自我修养——链接、装载与库》读书笔记(一)
在被各种编译问题折磨了一个假期后,突然想起了曾经借过的程序员神书——《程序员的自我修养——链接、装载与库》。初见这本书还是个没见过Linux萌新,看的云里雾里。今天正好跟着新开的编译原理课,好好捋顺一下这一大堆曾经视若不见的底层。
0x0 导读
有故事的书要先读序言,有技术的书要先看目录。作者很贴心的给出了导读,也算是给这本书划了一道门槛。
0x01 应具备的知识
- 了解C/C++编程
- 有一定x86汇编基础
- 了解操作系统、编程技巧和计算机系统结构基本概念
- 最好有Linux的编译体验(斗胆新添)
0x02 大致涉及内容
- 静态链接过程
- 装载与动态链接
- 库与运行库
0x03 想要达到的目的
- 了解Win、Linux下各自的可执行文件、目标文件格式
- C/C++程序编译详情
- 目标文件链接过程详情
- 可执行文件装载与执行
- 可执行文件与进程虚拟空间映射
- 动态链接
- 堆栈
- 函数调用
- 运行库(Glibc和MSVC CRT实现分析)
- 实现一个 Mini CRT
应当掠过的内容
本书第一章给出的是一些基础内容的回顾,也是作者出于“降低阅读门槛”目的下的给出的知识补丁。由于不是笔记的重点内容,这里简单列举一下涉及的内容,有兴趣的朋友可以选择了解:
- 计算机硬件发展简述(SMP与多核)
- 计算机软件体系结构(API、系统调用接口)
- 操作系统发展(CPU分配策略发展、设备驱动意义)
- 虚拟内存(隔离、分段、分页)
- 线程(意义、分配策略与调度、抢占、安全、优化)
1 静态链接
1.1 高级语言程序处理过程
高级语言程序处理过程-
预编译:展开所有的宏定义、处理所有的预编译指令、递归包含头文件并将其中逻辑插入需要的地方、删除所有注释、添加行号和文件名标识、保留所有#pragma指令
-
编译:将高级语言的源程序变换成低级语言的目标程序的过程,可以使编程者不必过多考虑与机器有关的细节。
-
汇编:将汇编代码转换成机器可以执行的命令。
-
链接:把一大堆用到的文件拼接到一起,通过符号表解析和重定位等最终输出可加载、可执行的目标文件。
1.2 编译过程
编译过程☆中间每个步骤都与表格管理、出错处理相关联☆
-
词法分析:在扫描器内利用有限状态机的算法将源代码中的字符序列分割成一系列的符号(Token)。
一般分为:关键字、标识符、常量、特殊符号 -
语法分析:由语法分析器利用上下文无关算法,将这些符号生成语法树。
语法树以表达式为节点,表达式只能为常量、标识符、(A)、A※B 之一,其中A、B为表达式,※为一个运算符
eg:id1:=id2+id3*10 生成语法树:
id1:=id2+id3*10 生成语法树 简化后的语法树-
语义分析:对表达式语法层面的分析,同时为代码生成阶段收集类型信息。
该阶段并未动态装配,所以只是静态分析 -
中间代码生成:一般是三地址编码或者四元算式,转换成内部的表达方式。
( IntToReal ,10 , -, t1),
( *, id3, t1, t2 ),
( +, id2, t2, t3 ),
( :=, t3 , - , id1 ) -
代码优化:对中间代码进行优化。
( *, id3, 10.0, t1 ),
( +, id2, t1, id1 )
1.3 链接过程
程序并不是一成不变的,修改后的地址变化问题非常繁琐。为了简化编程,人们开始使用符号来代指位置,其地址在使用过程中动态插入到需要的位置。重新计算各个目标位置的过程即为重定位。
运行一个程序所需要的代码量可能非常庞大,而且很大一部分代码可重用性很高,而且模块之间耦合度很低。于是人们便将代码分割成了很多部分,使用时再将各个部分拼接起来,这个过程就是链接,这些部分在不同的语言中有不同的形式,比如引用、包、或者库。
可想而知,链接过程中必定存在很多内部或外部的函数或变量,所以这个过程包括了很多诸如地址空间分配、符号决议、重定位等步骤。
1.4 目标文件
可执行文件:主要是Windows下的PE(Portable Executable)和Linux下的ELF(Executable Linkable Format),他们都是COFF(Common file format)格式的变种。
目标文件:源码编译后还未经过链接的中间文件,内容与结构与可执行文件相似,所以一般采取同种格式存储。
-
可重定位文件:代码+数据,可用来链接成可执行文件或共享目标文件。
如 Linux .o 和 Windows .obj -
可执行文件:可以直接执行。
如 Linux .sh 和 Windows .exe -
共享目标文件:代码+数据,链接器使用它们可重定位或共享目标文件链接生成新的目标文件,或者与可执行文件结合,作为映像一部分运行。
如 Linux .so 和 Windows .dll -
核心转储文件:程序意外终止时转储一些信息。
如 Linux core dump
目标文件格式:目标文件根据信息的不同属性将其按“段”存储。
段存储举例需要特别说明的几点:
- 文件头:存储了很多文件信息,后面会专门分析。
- 代码与数据分离:便于读写权限分配、数据独立逻辑共享、提高命中率。
- 预留段:未赋值或值为0的标识符将暂时不被分配内存,只在此处声明预留空间。
- 必要部分:文件头、代码段、数据段、预留段。
- 非必要部分:还有很多。
Ps: 有关ELF内部数据的详细格式这里有篇不错的博客 => 传送门
1.5 符号问题
无论是源代码还是依赖文件,大量的函数和变量链接到一起后在全局范围内很容易造成冲突。早期C语言与Fortran语言通过在前后添加“_”来进行区别,但在越来越多语言的后来并未很好的解决问题。
1.5.1 C++的符号修饰
C++本身的继承和重载就对函数区分有很高要求,于是C++引入了函数签名的机制。所有符号都以“_Z”开始,对于嵌套的名字,后面紧跟“N”并以“名字字符串长度+E”结尾,中间添加名称空间和类的名字。最后再添加参数类型的简写完成名称改造。
eg: N::C::func(int) ==> _ZN1N1C4funcEi
1.5.2 强与弱
强符号与弱符号:全局范围内重复定义符号时,若是强符号则报错。选择时优先选择强符号,若全是弱符号则选择长度最大的一个。
可以使用__attribute__((weak)) symbool = 2; 定义弱符号
强引用与弱引用:引用在被链接成可执行文件是需要被正确决议,若强引用丢失则报错,弱引用丢失则忽略。
小结
尽管书刚读了五分之一,只看完了一点点定义与格式,但也是很多次心里过瘾的大呼“原来如此”。与第一次读书时的感觉截然不同,在实际问题中的挣扎让现在的阅读变得更有目的性和体验感,期待接下来继续读下去的丰富收获。
PS:有兴趣的同学可以去查查书里讲到一个很好玩的故事。搜索的关键词:马屁股 航天飞机。