1、目录第一章 温故而知新6第二节 万变不离其宗6第3节 站得高看得远7第4节 操作系统的功能71.4.1 不要让CPU打盹71.4.2 设备驱动81.5 内存不够怎么办?81.5.1 关于隔离91.5.2 分段91.5.3 分页91.6 众人拾柴火焰高101.6.1 线程基础101.6.2 线程安全111.6.3 多线程内部情况14第二章 编译和链接152.1 被隐藏了的过程152.1.1 预编译152.1.2 编译152.1.3 汇编152.1.4 链接162.2 编译器做了什么162.2.1 词法分析162.2.2 语法分析162.2.3 语义分析162.2.4 中间语言生成172.2.5
2、目标代码的生成与优化172.3 链接器年龄比编译器长182.4 模块拼接静态链接18第三章 目标文件里有什么183.1 目标文件的格式193.2 目标文件是什么样的193.3 挖掘SimpleSection.o203.3.3 BSS段203.3.4 其他段203.4 ELF文件结构描述203.4.1 文件头213.4.2 段表213.4.3 重定位表223.4.4 字符串表223.5 链接的接口符号223.5.1 ELF符号表结构233.5.2 特殊符号233.5.3 符号修饰与函数签名243.5.5 弱符号和强符号243.6 调试信息25第4章 静态链接254.1 空间与地址分配254.1.
3、2 相似段合并254.1.3 符号地址的确定264.2 符号解析与重定位264.2.2 重定位表264.2.3 符号解析274.2.4 指令修正方式274.3 COMMON块274.4.1 重复代码消除284.4.2 全局构造与析构294.4.3 C+与ABI294.5 静态库链接304.6 链接过程控制304.6.1 链接过程脚本304.6.2 最“小”的程序314.6.3 使用ld链接脚本314.6.4 ld链接脚本语法简介314.7 BFD库31第5章 WINDOWS PE/COFF315.1 Windows的二进制文件格式PE/COFF315.2 PE的前身COFF325.3 链接指示
4、信息325.4 调试信息325.5 大家都有符号表325.6 WINDOWS下的ELFPE32第6章 可执行文件的装载与进程336.1 进程的虚拟地址空间336.2 装载的方式336.2.1 覆盖装入336.2.2 页映射346.3 从操作系统的角度看可执行文件的装载346.3.1 进程的建立346.4 进程虚存空间的分布356.4.1 ELF文件链接视图和执行视图356.4.2 堆和栈366.4.3 堆的最大申请数量366.4.4 段地址对齐366.4.5 进程栈初始化376.5 Linux内核装载ELF过程简介376.6 Windows PE的装载38第7章 动态链接387.1 为什么要动
5、态链接387.2 简单的动态链接例子397.3 地址无关代码407.3.1 固定装载地址的困扰407.3.2 装载时重定位407.3.3 地址无关代码407.3.4 共享模块的全局变量问题427.3.5 代码段地址无关性437.4 延迟绑定(PLT)437.5 动态链接相关结构447.5.1 “.interp”段457.5.2 “dynamic”段457.5.3 动态符号表457.5.4 动态链接重定位表457.5.5 动态链接时进程堆栈初始化信息467.6 动态链接的步骤和实现467.6.1 动态链接器自举467.6.2 装载共享对象477.6.3 重定位和初始化477.6.4 Linux动
6、态链接器的实现477.7 显示运行时链接487.7.1 打开动态库487.7.2 dlsym()487.7.3 dlerror()487.7.4 dlclose()49第8章 Linux共享库的组织498.1 共享库版本498.1.1 共享库兼容性498.1.2 共享库版本命名498.1.3 SO-NAME程序需要记录什么508.2 符号版本508.2.1 基于符号的版本机制508.2.3 Linux中的符号版本518.3 共享库系统路径518.4 共享库的查找过程518.5 环境变量528.6 共享库的创建与安装528.6.1 共享库的创建528.6.3 共享库的安装538.6.4 共享库构
7、造和析构函数538.6.5 共享库脚本53第9章 Windows下的动态链接549.1 dll介绍549.1.2 基地址和RVA549.1.3 dll共享数据段549.1.4 dll的简单例子549.1.7 使用模块定义文件559.1.8 DLL显示运行时链接559.2 符号导出导入表559.2.1 导出表559.2.2 EXP文件569.2.4 导入表569.2.5 导入函数的调用569.3 DLL优化579.3.1 重定基地址579.3.2 序号589.3.3 导入函数绑定589.4 C+与动态链接589.5 DLL HELL59第4部分 库与运行库60第10章 内存6010.1 程序的内
8、存布局6010.2 栈与调用惯例6110.2.1 什么是栈6110.2.2 调用惯例6110.2.3 函数返回值传递6210.3 堆与内存管理6310.3.1 什么是堆6310.3.2 Linux进程堆管理6310.3.3 Windows进程堆管理6410.3.4 堆分配算法64第11章 运行库6411.1 入口函数和程序初始化6411.1.1 程序从main开始执行吗6411.1.2 入口函数是如何实现的6511.1.3 运行库与I/O6611.1.4 MSVC CRT的入口函数初始化6611.2 C/C+运行库6711.2.1 C语言运行库6711.2.2 C语言标准库6711.2.3 g
9、libc和MSVC CRT6711.3 运行库与多线程6811.3.1 CRT的多线程困扰6811.3.2 CRT改进6811.3.3 线程局部存储实现6811.4 C+全局构造和析构6911.4.1 glibc全局构造和析构6911.4.2 MSVC的全局构造和析构7011.5 fread的实现7111.5.1 缓冲7111.5.2 fread_s7111.5.3 _fread_nolock_s7111.5.4 _read7111.5.5 文本换行7111.5.6 fread回顾72第12章 系统调用与API7212.1 系统调用介绍7212.1.1 什么是系统调用7212.1.3 系统调用
10、的弊端7212.2 系统调用原理7312.2.2 基于INT的Linux的经典系统调用实现7312.2.3 Linux的新型系统调用机制7312.3 Windows API7312.3.1 Windows API概览7412.3.2 为什么要使用Windows API?74第13章 运行库的实现7413.1 C语言运行库74A.1 字节序74第一章 温故而知新第二节 万变不离其宗凡是单纯讲史的章节我所有略去。本节讲的重要是由CPU、内存和I/O之间速度不匹配而设计的硬件架构及其发展。这个就不用细说了CPU最快,内存次之,I/O更慢。因为CPU和内存速度还算接近,因此把CPU和内存算作一类,I/
11、O单独算作一类。当然这里说的I/O是指I/O设备,并不是操作。伴随发展CPU频率越来越高,处理速度越来越快,内存跟不上节奏了,它们之间的I/O也出现了速度不匹配的问题。因为I/O设备可分为高速设备和低速设备两种,因此为高速搭配北桥,低速搭配南桥。它们之间的关系可用下图表示:CPU的频率只能达成4GHz无法提升,这是由CPU制造工艺决定的,是个瓶颈,目前还无法突破。一个CPU能力有限,那就让多个CPU共同工作提升效率。不过这么的CPU阵列各部件利用率不高,于是,发展出了多核心,其他部件共享的多核CPU设计。说白了,本来的CPU里面每个CPU一个核心,除此之外尚有围绕这个核的其他部件。不过目前多核
12、CPU除了核心彼此独立外,其他的部件是共享的。这一节就这么点内容。第3节 站得高看得远从下图能够看出计算机的结构大约是这么的:最底层是硬件,它提供硬件规格描述。再往上是操作系统内核,它提供系统调用。再往上是运行库,它提供各种系统API。再往上就是各种系统软件了。这种设计具备上层屏蔽下层,上层提供接口的特点。这一节对接口的解释非常好。作者说接口是一个协议,协议二字比较贴切。当然这个协议不是计算机网络中的protocol。第4节 操作系统的功效有二。1、提供抽象接口。2、管理硬件。1.4.1 不要让CPU打盹操作系统经历了从多道程序设计、分时操作系统、到多任务操作系统等阶段。多道程序设计是指CPU
13、空闲的时候出让CPU以提升CPU利用率的设计;分时是指给每个程序固定的时间片执行,时间片一到就停止的设计,不过这个时间片是轮转着用的,不是一个程序用完了就没了;多任务就是目前操作系统设计了,程序以进程的方式存在。抢占:OS对程序执行具备绝正确控制权,OS依据一定标准判断该剥夺哪个程序的执行就剥夺,想让哪个程序执行就让哪个程序执行。1.4.2 设备驱动GDI和directX等都是硬件的抽象,是一个中间层,它们屏蔽了硬件的详细细节,提供了通用的操作接口。LBA(Logical Block Address):因为硬盘结构复杂,概念繁多,寻找一个扇区要通过诸多步骤,这个比较麻烦。与其如此,不如干脆为每
14、个扇区配备一个逻辑编号,这么找扇区就仿佛是哈希算法同样快。1.5 内存不够怎么办?程序在内存中的地址空间是需要相互隔离的。这是为了预防一个程序在无意间修改其他程序导致意料之外的成果,另外,这也是为了信息安全。内存利用率要高,要否则程序在内存和硬盘之间进行I/O操作所花费的时间可就多了。程序运行的地址应当是确定的。因为多数程序指令跳转的目标地址是固定的,假如运行地址不确定就不能确保每次都在目标地址上运行,这就需要重定向进行调整,浪费时间。处理上述问题的措施是使用中间层,即把程序的运行地址与目标地址建立一个映射关系。1.5.1 有关隔离我们平时说的什么32位,64位CPU啥的都是指CPU的处理能力
15、,从硬件的角度讲,即,计算机的地址总线的条数。从CPU的设计上讲就是CPU一次能够处理的二进制位数,而这个位数尚有一个学名叫字长。内存的物理地址空间就是真实的内存空间,虚拟地址空间则是应用于进程的逻辑地址空间。1.5.2 分段我在想怎样从16进制的差值一下推断出地址空间的大小?如下是我的想法。1位16进制数字代表4位2进制数字,换句话说16进制数字转换为2进制数字是以24为单位进行换算的。那么依照某个16进制数字所在位置乘以目前权值就能够得到该位置上的16进制数字所代表的2进制数字。而16进制某位的权值等于低一位的权值乘以24,并且16进制最低位的权值是20,因此能够依照这个规律换算出对应的2
16、进制数字。来看个例子。书上说从0X00000000到0X00A00000的地址空间大小就等于|0x00A00000-0x00000000|=|A00000|因为A是10因此其等价于|1000000|,目前按照上述规律进行换算。10220+0216+0212+028+024+020=10M(byte)。分段的措施能够使各进程彼此隔离,并且能够使程序运行的地址确定。分段的缺陷就是它以程序为单位进行处理,不过依照程序运行的局部性原理,程序一般情况下只有一少部分需要常驻内存,因此以程序为单位换进换出严重影响了内存的利用率和处理速度。1.5.3 分页页面有3种:1、虚拟页;2、内存页;3、磁盘页。MMU
17、(Memory Management Unit)负责把虚拟地址转换成物理地址。1.6 众人拾柴火焰高1.6.1 线程基础使用线程的好处?1、多线程能够有效利用等候时间。因为某线程陷入等候状态后别的线程能够继续执行;2、多线程不会使与用户的交互中断。因为能够一个线程负责与用户交互,另一个线程负责计算;3、能够实现程序内部并发执行操作;4、多核CPU等硬件的潜力只有多线程才能使其充足发挥;5、在数据共享方面更高效。线程的私有存储空间?1、 栈;2、线程局部存储(Thread Local Storage,TLS);3、存储器。线程真正的并发执行和非真正并发执行?在同一时间只有处理器核心数量不小于等于
18、执行线程数量的时候才是真并发执行,除此之外都是模拟出来的。线程调度:在同一时间处理器的核心数量小于执行线程的数量时就需要在同一核心不停切换来执行线程。变化线程优先级的3种方式1、用户指定优先级;2、依照等候状态的频繁程度调整优先级;3、长时间得不到执行而被提升优先级。可抢占执行线程和不可抢占执行线程:线程的各种状态完全由操作系统来控制这就叫可抢占,就像某线程的时间片用完进入就绪态同样,这就是由操作系统来控制的。除此之外的就是不可抢占线程。不可抢占线程积极放弃执行的时机:1、线程等候某事件发生时。2、线程积极放弃时间片。因为就这俩条件因此不可抢占线程调度的时机是确定的。Linux下的多线程:不像
19、Windows那样把线程和进程分得那样清楚,Linux是以任务为单位的,假如某几个任务的执行是做同一件事的各个部分,那么这几个任务就能够当作是线程,而这件事就能够当作是进程。因此Linux下的线程和进程是动态的概念。Linux下的fork函数:fork是叉子的意思,我不懂得为啥Linux用它来给函数命名。它的作用就是复制任务,新任务和原任务共享同一块内存空间,并且是写时复制。所谓写时复制就是写的时候才从内存空间里面复制出一块给你写,原内存空间内容不变。读的时候新旧任务读同一块内存空间。Linux下的exec函数:fork产生的是本任务的镜像,也就是复制品。两个同样的任务完成同样的功效是浪费啊,
20、因此fork是个半成品函数,必须搭配别的函数才有用,这个函数就是exec函数。Exec函数用来执行别的可执行文献,换句话说就是干别的事。因此能够把fork了解成在一块内存空间上创造出个接口给exec执行新任务。Linux下的clone函数:我对它的了解就是fork和exec二合一,clone的作用就是产生新线程。1.6.2 线程安全要懂得线程安全就得懂得啥叫线程不安全。所谓线程不安全就是指多个线程同时访问共享数据导致成果的不确定性。原子操作:绝对不会被打断的操作。因为原子是化学反应中的最小微粒不可再分因此拿这个来比拟原子操作。它适合用于简单应用环境。处理线程不安全的通用措施是锁。线程同时:一开
21、始我还以为是多个线程一起访问某个资源呢,其实否则,线程同时是处理线程访问同一数据资源的处理方式,确保了同一时间只有同一线程访问数据资源,从而确保了线程安全。锁二元信号量:最简单的锁机制。只允许一个线程独占,一旦有线程占用,锁就展现占用状态,其他线程无法访问资源。否则,非占用状态,能够接收线程。锁多元信号量:就是它允许多个线程同时访问资源,比二元信号量高能某些。我感觉信号量就像管道。一个线程想访问资源它就必须首先获取一个管道,这么本来的管道数就少1,于是信号量首先减1。不过假如信号量减1以后成为负值,阐明本来的管道数为0,即本来就已经没有管道了,那么此时信号量机制就只能让该线程等候了,这就是P原
22、语。而假如一个线程用完了资源想要释放,那么它必须偿还它所使用的管道,那么管道总数应当加1,即信号量加1。正因为信号量已经加1,假如此时的信号量值为小于1,那阐明在加1之前管道总量就已经透支了,并且先前那些因为没有取得管道的线程还在那等着呢。恰好有个线程偿还了管道,V原语赶紧从那些等候的线程中找一个出来把管道给它,这就是在信号量值小于1的情况下唤醒线程的意思。锁互斥量(Mutex):信号量与互斥量的区分是一个信号量能够被一个线程获取并释放给另一个线程使用,正如V原语的操作。而互斥量一直都是一个线程,上锁是这个线程,这个线程不执行完就不解锁。锁临界区:获取临界区的锁为进入临界区,释放锁为离开临界区
23、。它的作用对象是某一位以进程,一旦某进程进入临界区,其他进程就无法进入。除此之外,临界区与互斥量相同。锁读写锁:互斥量、临界区和信号量适合用于读写都非常频繁的场所,而读写锁适合用于读频繁而写不频繁的场所。它的工作规律可用下表表示:锁写锁状态以共享方式获取以独占方式获取自由成功成功共享成功等候独占等候等候锁条件变量:相称于一个开关,它能够让等候它的线程继续等候也能够让它们继续执行。而这个开关需要某些其他的线程打开或关闭它。可重入函数:一个函数没有执行完全,不过因为内部原因或者外部调用,又一次开始执行该函数。它不产生任何不良后果。产生可重入的条件:1、多线程共同执行该函数。2、函数自己直接或者间接
24、调用自身。可重入函数的特点:1、不使用任何(局部)静态或全局的非const变量。因为假如使用的话它就涉嫌操纵共享数据,这么会导致线程不安全。2、不返回任何(局部)静态或全局的非const变量的指针。因为这同样包括到共享数据。3、仅依赖于调用方提供的参数。因为这么能够把函数的执行过程局限在局部。4、不依赖于任何单个资源的锁。单个资源的锁不允许被中断,这不符合可重入函数的定义。5、不调用任何不可重入函数。这个没啥好说的,假如调用了,可重入函数就成了不可重入函数。可重入性质是并发安全的强力确保可在多线程环境下大胆使用。过度优化:P53这个例子就是说本来2个x+成果是2,不过通过上锁以后却是1,这证明
25、虽然通过锁机制也不能完全保障计算正确,这是计算机内部工作机制导致的线程不安全。CPU对程序的优化也许导致线程不安全,因为它会调整程序语句执行次序以达成CPU所谓的优化,这有时候很麻烦。Volatile核心字能够制止这种优化。1、它制止编译器为提升程序执行速度将一个变量缓存到存储器内而不写回。2、它制止编译器调整语句执行次序。这两件事就是volatile所做的详细工作。不过,volatile能管住编译器管不了CPU,CPU还是能对指令进行动态调整。P54举了一个double-check的例子,虽然目前我对这个没有多深的了解,不过从这个例子中我看到作者是怎么分析的。它是将各个语句内部实际所进行的操
26、作都列出来进行分析的,这个值得我学习。虽然volatile管不了CPU,不过CPU有CPU相称于volatile的指令,一般这个指令叫做barrier。1.6.3 多线程内部情况线程分为内核级线程和用户级线程,内核级线程是用户直接接触不到的,用户只能接触到用户级线程。3种内核级线程与用户级线程的模型。1、一对一模型:就是每个用户级线程都对应一个内核级线程,但反过来不是,因为内核级线程也许没有用户级线程与之对应。一般直接使用API或者系统调用创建的线程均为一对一模型。它的优点:真正实现线程的并发执行,线程之间彼此互不影响。它的缺陷:1、许多操作系统限制了内核级线程的数量导致用户级线程数量受限。2
27、、许多操作系统用在内核级线程调度上的开销较大,重要为上下文切换开销,致使用户级线程执行效率低下。2、多对一模型:多个用户级线程对应同一个内核级线程,线程的切换由用户级代码决定。作者说多处理器对提升处理速度没有明显协助,这是当然的了,CPU处理的是内核级线程,而这个模型就在那摆着,CPU也只能按照这个模式来处理。再说了,一个线程只能在一个核上跑,你再多给几个核也没用啊。它的优点:它比一对一模型快,尚有高效的上下文切换和近似无限制的线程数量。它的缺陷:只要有一个线程阻塞,对应于同一个内核级线程的其他线程也无法执行,该内核级线程也阻塞,这很好了解,因为只有一条通路。3、多对多模型:是上面二者的合体。
28、很显然它能克服上述二者的缺陷,同理多处理器也无法明显提升它的执行效率。第二章 编译和链接2.1 被隐藏了的过程此前学的程序的执行过程是编辑、编译、链接、执行。今日这本书把这个过程愈加细化了,它以C语言中的helloworld程序为例进行阐明,讲的大约是从编译到链接的过程。也是包括4步:1、预处理;2、编译;3、汇编;4、链接。从这个次序能够看出在C语言中预处理是在编译之前。2.1.1 预编译预编译是个独立的过程,不一样于源文献的.cpp格式和头文献的.h格式,预编译得到的文献后缀是.i或者.ii。预编译的重要动作就是处理代码中以#开头的指令,详细可见P64这些步骤。因为宏已经展开因此.i文献不
29、包括任何宏定义。能够依照.i文献查看宏定义和文献包括是否正确。预编译需要预编译器。2.1.2 编译编译的过程是把预处理得到的文献进行词法分析、语法分析、语义分析和优化后生成对应的汇编代码文献。2.1.3 汇编汇编阶段是通过汇编器完成的,其作用就是把汇编指令转换成机器指令。汇编结束以后生成目标文献.obj。2.1.4 链接链接简而言之就是把目标文献链接在一起生成可执行文献的过程,不过实际上这是一个非常复杂的过程,并不像看上去那么简单。2.2 编译器做了什么编译的过程能够分为扫描、语法分析、语义分析、源代码优化、代码生成、目标代码优化等6步。2.2.1 词法分析这一过程是交给扫描器执行的,目标是把
30、程序语句划提成若干记号。这些记号一般包括:1、核心字;2、标识符;3、字面量(数字,字符串等);4、特殊符号(加号,等号等)。另外,扫描器还将标识符放到符号表,将字面量放到文字表中以备后用。词法分析需要此法扫描器。2.2.2 语法分析它是对词法分析产生的各种记号进行语法分析,并产生一颗语法树。语句内容含义的辨别,语法的检查等都是在此阶段完成的。语法分析需要语法分析器。2.2.3 语义分析语义分析需要语义分析器。语义分析就是分析该语句的意思,就是它能做什么,有啥用。编译器所能做的包括静态语义分析和动态语义分析。静态语义:编译期能够确定的语义,它重要包括类型和申明的匹配,类型的转换等。我想C+中的
31、静态绑定应当也属于静态语义吧。动态语义:运行期能够确定的语义以及有关问题,例如说异常处理。我同时在想C+中的动态绑定应当属于动态语义。语义分析对语法树各节点进行了类型标识和类型转换,还更新了符号表里的符号类型。2.2.4 中间语言生成编译器有诸多层次的优化,源码级别的优化是其中一个层次。源码级的优化需要源码级优化器。这个优化是把语法树转换成中间代码,并在中间代码上进行的。常见的中间代码有三地址码和P代码。中间代码将编译器提成了前端和后端,前端负责产生与机器无关的中间代码,后端负责把中间代码转换成目标代码。跨平台的编译器并不是放在任意一个平台上都绝对能用,只不过它能支持的平台诸多而已。这是因为编
32、译器使用同一个前端,而针对不一样的平台使用不一样的后端。2.2.5 目标代码的生成与优化编译器的后端包括代码生成器和目标代码优化器。代码生成器将中间代码转换成目标代码,该过程依赖于目标机器。目标代码优化器对目标代码进行优化,例如选择适宜的寻址方式,以移位替代数乘等。目前的编译器非常复杂,上述提到的这些方面也变得非常复杂。变量和函数的地址都是在最后链接的时候才确定的,然后变成可执行文献。2.3 链接器年龄比编译器长作者把链接比方为拼图的拼接。2.4 模块拼接静态链接将源代码模块组装起来的过程就是链接。链接的过程包括:1、地址和空间分派;2、符号决议;3、重定位等。.obj文献即目标文献和库一起链
33、接成可执行文献。库是由某些常用的代码编译成的目标文献的包,是一个集合。最常见的库是运行时库,是支持程序运行的基本函数的集合。每个目标文献都是单独编译的。模块A想要调用模块B的C函数,A必须要懂得C的地址,不过目前A不懂得C的地址,不过A给C留了位置,等到链接器链接时再在这个位置上填上C的地址。假如C的地址被改动了,A中所有调用C的地方都需要进行对应的更改,这些都可藉由链接器完成。这是静态链接的基本功效和作用。在链接的过程中需要对目标文献中定义在其他目标文献中的函数和变量的调用指令进行重新调整,注意这里说的是指令!书中举的例子意在阐明,当目标文献A调用目标文献B中的变量C时,因为暂时无法懂得C的
34、位置,因此指令先把表示C的位置置为某一值,等到链接的时候再把这值修正为C的地址,这一过程叫做重定位,像C这么的位置被称为重定位入口。第三章 目标文献里有什么.obj是目标文献,因此能够懂得目标文献是指编译后生成的文献,目标文献几乎和可执行文献相同只是稍微有点不一样而已。其不一样之处在于有些符号和地址没有被调整。3.1 目标文献的格式正是因为目标文献与可执行文献几乎相同,因此它们的存储格式是同样的,能够把它们近似当作同一个文献。Linux下的动态链接库格式为.so,Windows和Linux下的静态链接库格式分别为.lib和.a。静态链接库是一个文献,该文献包括了诸多目标文献,它是一个整体。Li
35、nux下的可执行文献是按照ELF格式存储的,ELF标准包括4种文献,请看P81。我所熟悉的Windows下的DLL就属于共享目标文献。3.2 目标文献是什么样的目标文献一般包括了哪些内容?编译后的机器指令代码、数据、连接所需的信息、符号表、调试信息、字符串等。目标文献把信息按照属性的不一样分段存储。写到这里我感觉这书上说的与老师课上讲的程序在内存中的分段措施有些相同。在目标文献中,编译后的机器指令代码放在代码段(Code Section)中,段名一般为.code和.text。全局变量和静态变量放在数据段(Data Section)中,段名一般为.data。BSS段(Block Started
36、By Symbol)用来存储未初始化的静态变量和全局变量。话虽如此bss中并没有这些变量的内容,它只是为这些变量按照所占空间大小预留空间而已。因为这些变量默认就是0,因此压根没必要再为它们分派一个数据0,也没有必要让它们待在data段中。因此bss的作用是为这些变量预留空间。另外目标代码尚有一个文献头用来保存该目标文献的信息,它里面尚有一个段表。源代码被编译以后生成两种段数据段和指令段,.code.text属于指令段.data.bss属于数据段。这么分重要有3点好处:1、预防程序被故意无意篡改。这是因为指令段只读,数据段可读写。2、提升了缓存命中率。3、节约内存空间。因为指令段可被多个副本共享
37、,不过副本能够拥有自己的数据段。3.3 挖掘SimpleSection.o本来目标文献中的段尚有只读数据段(.rodata)、注释信息段(.comment)、堆栈提示段(.note.GNU-stack)。从书中所给的例子来看一个ELF文献只有4个段是由内容的,即.data、.text、.rodata、.comment。从图3-3能够看出在内存中,从低地址到高地址是按照ELF header、text、data、rodata、comment、other data的次序存储的。3.3.3 BSS段由本小节可知,全局变量也许因为语言和编译器的不一样不一定存储在bss段,不过静态变量一定存储在bss段。
38、虽说bss存储的是未初始化的静态和全局变量,不过有些变量假如被初始化为0,它也会被放在bss中,这是编译器的优化,有时候这种优化会带来麻烦。3.3.4 其他段表3-2列出了其他段及意义。另外,这个段还能够自定义。3.4 ELF文献结构描述图3-4展示了ELF的层次结构。最重要的两个部分就是ELF文献头和段表。ELF文献头描述整个文献的基本属性,段表描述各段的信息。3.4.1 文献头清单3-2清楚地描述了ELF文献头的信息,P95黑体部分列举了ELF文献头包括的信息。ELF文献兼容各平台,它的文献结构和有关参数定义在”/usr/include/elf.h”里,它有32位和64位两种。表3-3展示
39、了elf.h的自定义变量体系。表3-4展示了ELF文献头结构组员含义。ELF魔数:ELF文献头的第一个字段是Magic,包括16bytes,对应于Elf32_Ehdr中的e_ident组员。Magic用来表示平台的各种属性。14个字节是所有ELF文献都相同的标识码,分别对应del、E、L、F,这四个字节就是ELF魔数。操作系统通过确认魔术是否正确以决定是否加载可执行文献。第5个字节用来表示ELF文献是32位的还是64位的。第6个字节用来表示ELF字节序。第7个字节用来表示ELF文献版本号。背面的9个字节用来预留,有些平台也许用来作为扩展标志。Elf32_Ehdr中的e_type组员表示ELF文
40、献类型,ELF总共有三种文献类型如表3-5所示。操作系统是通过判断文献类型而不是扩展名来确定ELF文献类型的。Elf32_Ehdr中的e_machine组员表示ELF文献的平台属性。虽然ELF遵照统一标准但不代表同一ELF文献能够在不一样平台上使用。3.4.2 段表它用来表示各个段的信息,ELF文献中的段是由段表决定的。一个ELF文献不但仅包括像data、text、bss这么的段,还包括其他的辅助性段。段表是一个Elf32_Shdr类型的结构体数组,元素的个数代表段的个数,每个元素对应一个段。这个Elf32_Shdr被称为段描述符。表3-7描述了Elf32_Shdr中各字段的意义。段的名称对于
41、编译和链接故意义,对操作系统无意义。决定段的类型的是段的类型字段,并不是段的后缀名和名称。段的类型和段的标志位字段决定了段的属性。表3-8展示了段的各种类型。段的标志位表示该段在进程虚拟地址空间中的属性,如是否可读。表3-9列出了段的各种属性。表3-10列出了系统保存段的各种属性。段的连接信息包括sh_link和sh_info,它们与链接有关,如表3-11所示。3.4.3 重定位表目标文献中有一个SHT_REL的.rel.text字段,它是重定位表。重定位发生在连接的过程中,这个在前面已经讲过,重定位表统计了重定位有关信息。3.4.4 字符串表顾名思义,就是用来表示各种名称的字符串的表。它是一
42、个装有各种字符串的表格,每个字符在表中都有一个固定的位置。这种表在ELF文献中保存为2种形式.strtab和.shstrtab,它们分别是字符串表和段字符串表,它们在ELF文献中都以独立的段而存在。为了轻松地找到这个段,在ELF文献头中包括了这两个段的下标,名为e_shstrndx。3.5 链接的接口符号链接是组合目标文献的过程,目标文献是依照彼此之间的地址相互引用,从而组合成可执行文献的。而,这个地址能够简单地了解为目标文献中的函数和变量。在这里,函数和变量统称为符号,函数名和变量名统称为符号名。链接器的着眼点重要在定义在本目标文献和定义在其他目标文献的全局性符号,因为只有这些包括到目标文献
43、之间的组合。3.5.1 ELF符号表结构ELF文献的符号表是一个段,段名为“.symtab”,它是一个Elf32_sym类型的数组,每个数组元素代表一个符号。在Elf32_sym结构体中有一个32bit组员叫st_info,低4bit表示符号的类型,高28bit符号的绑定信息。绑定信息详细可见表3-15,符号类型可参见表3-16。Elf32_sym.st_shndx:假如符号定义在本目标文献中,它表示该符号所在的段在段表中的下标,否则它具备其他意义。st_shndx详细信息可见表3-17。Elf32_sym.st_value:每个符号都有一个对应值,它一般为变量和函数的地址。st_value的
44、意义有如下几个:1、假如符号定义在目标文献中,并且它不是COMMON块类型,则st_value代表符号在段中的偏移。2、假如符号定义在目标文献中并且是COMMON块类型,则st_value表示符号的对齐属性。3、在可执行文献中st_value表示符号的虚拟地址。3.5.2 特殊符号链接器自身自带的,不是你定义的,定义在链接脚本中的,不过你能够用的,这么的符号是特殊符号。它们存在的时机是链接器链接生成可执行文献时,此时链接器会将它们解析成正确的值,书中P110举了几个具备代表性的特殊符号。3.5.3 符号修饰与函数署名本小节明确了函数署名的概念。函数署名:重要是指函数名和参数类型,其次是所在类和
45、命名空间等。它用于辨别不一样函数。编译器和连接器会使用名称修饰的措施加工函数署名使之成为修饰后名称,在C+中为符号名。不一样的编译器对函数署名的修饰措施不一样,这导致不一样种类的目标文献无法互连。本来C+编译器已经默认定义了宏_cplusplus来兼容C语言和C+。3.5.5 弱符号和强符号在不一样目标文献中含有相同全局性符号定义,这种情况被称为强符号,它会引起符号重定义。C/C+编译器以为未初始化的全局变量是弱符号。这个强弱符号是能够被定义的,因此强弱之别是依照定义来划分的,并不针对符号的引用,P117代码阐明了这一点。链接器依照符号的强弱来处理和选择定义的全局变量:1、不允许数次定义强符号
46、,否则报错。2、同一个符号在各目标文献中出现了数次,但只有一个是强符号,那么编译器选择强符号的那个。3、假如一个符号在所有目标文献中都是弱符号,那么编译器选择占用空间最大的一个。由此可见编译器对于弱符号的选择并不明显,因此由弱符号导致的错误也相对难以发觉。强引用:目标文献对于非本目标文献的符号引用,在链接成可执行文献的过程中,假如找不到该符号的定义,就报未定义错误。弱引用:与强引用差不多,只不过在找不到符号时不报错。强弱引用重要用于库的链接。对于未定义的弱引用,编译器为便于识别把它看作是某一值,一般为0。弱符号与COMMON块联系较亲密。弱引用是能够手动申明的,如P118第一段代码所示。弱符号的作用在于提供一个默认的库符号,不过当用户想要自定义该符号的时候,该自定义符号就取得了更高的优先级。而弱引用的作用在于增强了程序
©2010-2025 宁波自信网络信息技术有限公司 版权所有
客服电话:4008-655-100 投诉/维权电话:4009-655-100