程序员的自我修养总结_第1页
程序员的自我修养总结_第2页
程序员的自我修养总结_第3页
已阅读5页,还剩46页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、目录第一章温故而知新 6第二节万变不离其宗6第3节 站得高看得远 7第4节操作系统的功能7不要让CPU打盹7设备驱动81.5 内存不够怎么办? 8关于隔离81.5.2 分段91.5.3 分页91.6众人拾柴火焰高 10线程基础10线程安全 11多线程内部情况14第二章编译和链接 152.1被隐藏了的过程 15预编译152.1.2 编译152.1.3 汇编152.1.4 链接162.2编译器做了什么16词法分析16语法分析16语义分析16中间语言生成 17目标代码的生成与优化 172.3链接器年龄比编译器长 182.4模块拼接一一静态链接 18第三章目标文件里有什么 183.1目标文件的格式19

2、3.2目标文件是什么样的 193.3 挖掘 SimpleSection.o 203.3.3 BSS段 20其他段203.4 ELF文件结构描述 20文件头213.4.2 段表21重定位表 22字符串表 223.5链接的接口 一一符号 223.5.1 ELF符号表结构 23特殊符号 23符号修饰与函数签名 24弱符号和强符号 243.6调试信息25第4章静态链接254.1空间与地址分配25相似段合并25符号地址的确定 264.2符号解析与重定位 26重定位表26符号解析 27指令修正方式 274.3 COMMON 块27重复代码消除 284.4.2 全局构造与析构 294.4.3 C+与 ABI

3、 294.5静态库链接 304.6链接过程控制 30链接过程脚本30最“小”的程序 31使用Id链接脚本314.6.4 Id链接脚本语法简介314.7 BFD 库31第 5 章 WINDOWS PE/COFF315.1 Windows的二进制文件格式 PE/COFF315.2 PE 的前身 COFF325.3链接指示信息 325.4调试信息325.5大家都有符号表325.6 WINDOWS 下的 ELFPE32第6章可执行文件的装载与进程 336.1进程的虚拟地址空间 336.2装载的方式 33覆盖装入33页映射346.3从操作系统的角度看可执行文件的装载 34进程的建立 346.4 进程虚存

4、空间的分布 356.4.1 ELF文件链接视图和执行视图 35堆和栈36堆的最大申请数量 36段地址对齐36进程栈初始化376.5 Linux内核装载 ELF过程简介 376.6 Windows PE 的装载 38第7章动态链接387.1为什么要动态链接387.2简单的动态链接例子 397.3地址无关代码40固定装载地址的困扰 40装载时重定位 40地址无关代码 40共享模块的全局变量问题 42代码段地址无关性 437.4延迟绑定(PLT). 437.5动态链接相关结构 447.5.1 “ .in”i段457.5.2 “ dyna”段45动态符号表 45动态链接重定位表 45动态链接时进程堆栈

5、初始化信息 467.6动态链接的步骤和实现 46动态链接器自举46装载共享对象 47重定位和初始化 477.6.4 Linux动态链接器的实现 477.7显示运行时链接 48打开动态库 487.7.2 dlsym() 487.7.3 dlerror() 487.7.4 dlclose() 49第8章Linux共享库的组织 498.1共享库版本49共享库兼容性 49共享库版本命名 498.1.3 SO-NAME程序需要记录什么 508.2符号版本50基于符号的版本机制 508.2.3 Linux中的符号版本 518.3共享库系统路径518.4共享库的查找过程 518.5环境变量528.6共享库的

6、创建与安装 52共享库的创建52共享库的安装53共享库构造和析构函数 53共享库脚本53第 9 章 Windows 下的动态链接 549.1 dll 介绍54基地址和 RVA549.1.3 dll共享数据段549.1.4 dll的简单例子54使用模块定义文件 559.1.8 DLL显示运行时链接 559.2符号导出导入表55导出表559.2.2 EXP文件56导入表56导入函数的调用 569.3 DLL优化57重定基地址 579.3.2 序号58导入函数绑定 589.4 C+与动态链接589.5 DLL HELL59第4部分库与运行库60第10章内存6010.1程序的内存布局 6010.2栈与

7、调用惯例61什么是栈61调用惯例61函数返回值传递6310.3堆与内存管理63什么是堆6310.3.2 Linux进程堆管理 6310.3.3 Windows 进程堆管理64堆分配算法64第11章运行库6511.1入口函数和程序初始化 65程序从 main开始执行吗 65入口函数是如何实现的 65运行库与I/O 6611.1.4 MSVC CRT勺入口函数初始化 6611.2 C/C+运行库 6711.2.1 C语言运行库 6711.2.2 C语言标准库 6711.2.3 glibc 和 MSVC CRT6711.3 运行库与多线程 6811.3.1 CRT的多线程困扰 6811.3.2 CR

8、T 改进 68线程局部存储实现 6911.4 C+全局构造和析构 6911.4.1 glibc全局构造和析构 691142 MSVC的全局构造和析构 7011.5 fread 的实现7111.5.1 缓冲7111.5.2 fread_s 7111.5.3 _fread_ no lock_s 7111.5.4 _read 71文本换行 7211.5.6 fread 回顾 72第12章系统调用与API 7212.1系统调用介绍72什么是系统调用 72系统调用的弊端 7212.2系统调用原理 73基于INT的Linux的经典系统调用实现 7312.2.3 Linux的新型系统调用机制 7312.3

9、Windows API 7412.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/O单独算作一类。当然这里说的I/O 是指I/O设备,并不是操作。随着发展CPU频率越来越高,处理速度越来越快,内存跟不上节奏了,它 们之间的

10、I/O也出现了速度不匹配的问题。因为I/O设备可分为高速设备和低速设备两种,所以为高速搭配北桥,低速 搭配南桥。它们之间的关系可用下图表示:CPU的频率只能达到4GHz无法提升,这是由CPU制造工艺决定的,是个 瓶颈,目前还无法突破。一个CPU能力有限,那就让多个CPU共同工作提升效率。但是这样的CPU 阵列各部件利用率不高,于是,发展出了多核心,其他部件共享的多核 CPU设 计。说白了,原来的CPU里面每个CPU一个核心,除此之外还有围绕这个核的其他部件。但是现在多核CPU除了核心彼此独立外,其他的部件是共享的 这一节就这么点内容。第3节站得咼看得远Applktlons:Web Browse

11、rVMeo PlayerWord ProcftssorEmail ClientImage Viewer从下图可以看出计算机的结构大概是这样的:Development Tools:C/C+ CompilerAssemblerLibrary ToolsDebug TootsDevelopment Libraries-Operating Systeni APIRuntime LibraryCalOperating System KernelHardware最底层是硬件,它提供硬件规格描述。再往上是操作系统内核,它提供系统 调用。再往上是运行库,它提供各种系统 API。再往上就是各种系统软件了。这种设

12、计具有上层屏蔽下层,上层提供接口的特点。这一节对接口的解释非常好。作者说接口是一种协议,协议二字比较贴切。当然这个协议不是计算机网络中的protocol。第4节操作系统的功能有二。1、提供抽象接口。2、管理硬件不要让CPU打盹操作系统经历了从多道程序设计、分时操作系统、到多任务操作系统等阶段 多道程序设计是指CPU空闲的时候出让CPU以提高CPU利用率的设计; 分时是指给每个程序固定的时间片执行, 时间片一到就停止的设计,不过这个时 间片是轮转着用的,不是一个程序用完了就没了;多任务就是现在操作系统设计 了,程序以进程的方式存在。抢占:OS对程序执行具有绝对的控制权,OS依据一定标准判断该剥夺

13、哪个 程序的执行就剥夺,想让哪个程序执行就让哪个程序执行。设备驱动GDI和directX等都是硬件的抽象,是一个中间层,它们屏蔽了硬件的具体 细节,提供了通用的操作接口。LBA(Logical Block Address):因为硬盘结构复杂,概念繁多,寻找一个扇区 要经过很多步骤,这个比较麻烦。与其如此,不如干脆为每个扇区配置一个逻辑 编号,这样找扇区就好像是哈希算法一样快。1.5内存不够怎么办?程序在内存中的地址空间是需要相互隔离的。这是为了防止一个程序在无意 间修改其他程序造成意料之外的结果,另外,这也是为了信息安全。内存利用率要高,要不然程序在内存和硬盘之间进行I/O操作所花费的时间 可

14、就多了。程序运行的地址应该是确定的。因为多数程序指令跳转的目标地址是固定的,如果运行地址不确定就不能保证每次都在目标地址上运行,这就需要重定向进行调整,浪费时间。解决上述问题的办法是使用中间层,即把程序的运行地址与目标地址建立一 种映射关系。关于隔离我们平时说的什么32位,64位CPU啥的都是指CPU的处理能力,从硬件 的角度讲,即,计算机的地址总线的条数。从 CPU的设计上讲就是CPU 次能 够处理的二进制位数,而这个位数还有一个学名叫字长。内存的物理地址空间就是真实的内存空间,虚拟地址空间则是应用于进程的逻辑地址空间。分段我在想如何从16进制的差值一下推断出地址空间的大小?以下是我的想法。

15、1位16进制数字代表4位2进制数字,换句话说16进制 数字转换为2进制数字是以24为单位进行换算的。那么根据某个16进制数字所 在位置乘以当前权值就可以得到该位置上的16进制数字所代表的2进制数字。而16进制某位的权值等于低一位的权值乘以 24,并且16进制最低位的权值是 2°,因此可以根据这个规律换算出相应的 2进制数字。来看个例子。书上说从0X00000000到0X00A00000的地址空间大小就等于 |0x00A00000-0x00000000|=|A00000因为 A 是 10 所以其等价于 |1000000|,现在按 照上述规律进行换算。10 X 220+0 >216

16、+0 >212+0 >28+0 >24+0 >2°=10M(byte)。分段的方法可以使各进程彼此隔离,并且可以使程序运行的地址确定。分段的缺点就是它以程序为单位进行处理,但是根据程序运行的局部性原 理,程序通常情况下只有一少部分需要常驻内存,因此以程序为单位换进换出严 重影响了内存的利用率和处理速度。分页页面有3种:1、虚拟页;2、内存页;3、磁盘页。MMU(Memory Ma nageme nt Un it)负责把虚拟地址转换成物理地址。1.6众人拾柴火焰高线程基础使用线程的好处?1、多线程可以有效利用等待时间。因为某线程陷入等待状态后别的线程可 以继续执

17、行;2、多线程不会使与用户的交互中断。因为可以一个线程负责与用户交互,另一个线程负责计算;3、能够实现程序内部并发执行操作;4、多核CPU等硬件的潜力只有多线程才能使其充分发挥;5、在数据共享方面更高效。线程的私有存储空间?1、栈;2、线程局部存储(Thread Local Storage,TLS; 3、寄存器。线程真正的并发执行和非真正并发执行?在同一时间只有处理器核心数量大于等于执行线程数量的时候才是真并发 执行,除此之外都是模拟出来的。线程调度:在同一时间处理器的核心数量小于执行线程的数量时就需要在同 一核心不断切换来执行线程。改变线程优先级的3种方式1、用户指定优先级;2、根据等待状态

18、的频繁程度调整优先级;3、长时间 得不到执行而被提升优先级。可抢占执行线程和不可抢占执行线程:线程的各种状态完全由操作系统来控 制这就叫可抢占,就像某线程的时间片用完进入就绪态一样, 这就是由操作系统 来控制的。除此之外的就是不可抢占线程。不可抢占线程主动放弃执行的时机:1、线程等待某事件发生时。2、线程主 动放弃时间片。因为就这俩条件所以不可抢占线程调度的时机是确定的Linux下的多线程:不像 Windows那样把线程和进程分得那样清楚, Linux 是以任务为单位的,如果某几个任务的执行是做同一件事的各个部分, 那么这几 个任务就可以看成是线程,而这件事就可以看成是进程。 所以Linux下

19、的线程和 进程是动态的概念。Linux下的fork函数:fork是叉子的意思,我不知道为啥 Linux用它来给函 数命名。它的作用就是复制任务,新任务和原任务共享同一块内存空间, 并且是 写时复制。所谓写时复制就是写的时候才从内存空间里面复制出一块给你写, 原 内存空间内容不变。读的时候新旧任务读同一块内存空间。Linux下的exec函数:fork产生的是本任务的镜像,也就是复制品。两个同 样的任务完成同样的功能是浪费啊,所以fork是个半成品函数,必须搭配别的函数才有用,这个函数就是exec函数。Exec函数用来执行别的可执行文件,换 句话说就是干别的事。所以可以把fork理解成在一块内存空

20、间上创造出个接口 给exec执行新任务。Linux下的clone函数:我对它的理解就是fork和exec二合一,clone的作 用就是产生新线程。线程安全要知道线程安全就得知道啥叫线程不安全。所谓线程不安全就是指多个线程 同时访问共享数据造成结果的不确定性。原子操作:绝对不会被打断的操作。因为原子是化学反应中的最小微粒不可 再分所以拿这个来比拟原子操作。它适用于简单应用环境。解决线程不安全的通用方法是锁。线程同步:一开始我还以为是多个线程一起访问某个资源呢,其实不然,线程同步是解决线程访问同一数据资源的解决方式,保证了同一时间只有同一线程访问数据资源,从而保证了线程安全。锁一一二元信号量:最简

21、单的锁机制。只允许一个线程独占,一旦有线程占 用,锁就呈现占用状态,其他线程无法访问资源。否则,非占用状态,可以接受 线程。锁一一多元信号量:就是它允许多个线程同步访问资源,比二元信号量高能 一些。我感觉信号量就像管道。一个线程想访问资源它就必须首先获取一个管道, 这样原来的管道数就少 1于是信号量首先减1。但是如果信号量减1以后成为 负值,说明原来的管道数为0,即原来就已经没有管道了,那么此时信号量机制 就只能让该线程等待了,这就是 P原语。而如果一个线程用完了资源想要释放, 那么它必须归还它所使用的管道,那么管道总数应该加1,即信号量加1。正因为信号量已经加1,如果此时的信号量值为小于1,

22、那说明在加1之前管道总量 就已经透支了,而且先前那些因为没有获得管道的线程还在那等着呢。正好有个 线程归还了管道,V原语赶紧从那些等待的线程中找一个出来把管道给它,这就是在信号量值小于1的情况下唤醒线程的意思。锁一一互斥量(Mutex):信号量与互斥量的区别是一个信号量可以被一个线 程获取并释放给另一个线程使用,正如 V原语的操作。而互斥量始终都是一个 线程,上锁是这个线程,这个线程不执行完就不解锁。锁一一临界区:获取临界区的锁为进入临界区,释放锁为离开临界区。它的 作用对象是某一位以进程,一旦某进程进入临界区,其他进程就无法进入。除此 之外,临界区与互斥量相同。锁一一读写锁:互斥量、临界区和

23、信号量适用于读写都非常频繁的场合,而读写锁适用于读频繁而写不频繁的场合。它的工作规律可用下表表示:锁写锁状态以共享方式获取以独占方式获取自由成功成功卄享/、成功等待独占等待等待锁一一条件变量:相当于一个开关,它可以让等待它的线程继续等待也可以 让它们继续执行。而这个开关需要一些其他的线程打开或关闭它。可重入函数:一个函数没有执行完全,但是由于内部因素或者外部调用, 又 一次开始执行该函数。它不产生任何不良后果。产生可重入的条件:1多线程共同执行该函数。2、函数自己直接或者间接 调用自身。可重入函数的特点:1不使用任何(局部)静态或全局的非 const变量。 因为如果使用的话它就涉嫌操纵共享数据

24、,这样会导致线程不安全。2、不返回任何(局部)静态或全局的非const变量的指针。因为这同样涉及到共享数据。3、仅依赖于调用方提供的参数。因为这样可以把函数的执行过程局限在局部。4、 不依赖于任何单个资源的锁。单个资源的锁不允许被中断,这不符合可重入函数 的定义。5、不调用任何不可重入函数。这个没啥好说的,如果调用了,可重入 函数就成了不可重入函数。可重入性质是并发安全的强力保证可在多线程环境下 大胆使用。过度优化:P53这个例子就是说本来2个x+结果是2,但是经过上锁以后却是1,这证明即使通过锁机制也不能完全保障计算正确,这是计算机内部工作 机制造成的线程不安全。CPU对程序的优化可能导致线

25、程不安全,因为它会调整程序语句执行顺序 以达到CPU所谓的优化,这有时候很麻烦。Volatile关键字可以阻止这种优化。1、它阻止编译器为提高程序执行速度将一个变量缓存到寄存器内而不写回。2、它阻止编译器调整语句执行顺序。这两件事就是volatile所做的具体工作。但是, volatile能管住编译器管不了 CPU,CPU还是能对指令进行动态调整。P54举了一个double-check的例子,虽然现在我对这个没有多深的理解,但 是从这个例子中我看到作者是怎么分析的。它是将各个语句内部实际所进行的操 作都列出来进行分析的,这个值得我学习。虽然volatile管不了 CPU,但是CPU有CPU相当

26、于volatile的指令,一般这个指令叫做barrier。163多线程内部情况线程分为内核级线程和用户级线程,内核级线程是用户直接接触不到的,用 户只能接触到用户级线程。3种内核级线程与用户级线程的模型。1、一对一模型:就是每个用户级线程都对应一个内核级线程,但反过来不是,因为内核级线程可能没有用户级线程与之对应。一般直接使用API或者系统调用创建的线程均为一对一模型。它的优点:真正实现线程的并发执行,线程之间彼此互不影响。它的缺点:1、许多操作系统限制了内核级线程的数量导致用户级线程数量 受限。2、许多操作系统用在内核级线程调度上的开销较大,主要为上下文切换 开销,致使用户级线程执行效率低下

27、。2、多对一模型:多个用户级线程对应同一个内核级线程,线程的切换由用户级代码决定。作者说多处理器对提升处理速度没有明显帮助,这是当然的了, CPU处理的是内核级线程,而这个模型就在那摆着,CPU也只能按照这个模式来处理。再说了,一个线程只能在一个核上跑,你再多给几个核也没用啊。它的优点:它比一对一模型快,还有高效的上下文切换和近似无限制的线程 数量。它的缺点:只要有一个线程阻塞,对应于同一个内核级线程的其他线程也无 法执行,该内核级线程也阻塞,这很好理解,因为只有一条通路。3、多对多模型:是上面二者的合体。很显然它能克服上述二者的缺点,同 理多处理器也无法显著提升它的执行效率。第二章编译和链接

28、2.1被隐藏了的过程以前学的程序的执行过程是编辑、编译、链接、执行。今天这本书把这个过 程更加细化了,它以C语言中的helloworld程序为例进行说明,讲的大概是从编 译到链接的过程。也是包括4步:1、预处理;2、编译;3、汇编;4、链接。从这个顺序可以 看出在C语言中预处理是在编译之前。预编译预编译是个独立的过程,不同于源文件的.cpp格式和头文件的.h格式,预编 译得到的文件后缀是.i或者.ii。预编译的主要动作就是处理代码中以#开头的指令,具体可见P64这些步骤。 因为宏已经展开所以.i文件不包含任何宏定义。可以根据.i文件查看宏定义和文 件包含是否正确。预编译需要预编译器。编译编译的

29、过程是把预处理得到的文件进行词法分析、语法分析、语义分析和优 化后生成相应的汇编代码文件。汇编汇编阶段是通过汇编器完成的,其作用就是把汇编指令转换成机器指令。汇 编结束以后生成目标文件.obj链接链接简而言之就是把目标文件链接在一起生成可执行文件的过程,但是实际上这是一个非常复杂的过程,并不像看上去那么简单。2.2编译器做了什么编译的过程可以分为扫描、语法分析、语义分析、源代码优化、代码生成、 目标代码优化等6步。词法分析这一过程是交给扫描器执行的,目的是把程序语句划分成若干记号。这些记号一般包括:1、关键字;2、标识符;3、字面量(数字,字符串等);4、特殊符号(加号,等号等)。此外,扫描器

30、还将标识符放到符号表,将字面量放到文字表中以备后用。词法分析需要此法扫描器。语法分析它是对词法分析产生的各种记号进行语法分析,并产生一颗语法树语句内容含义的区分,语法的检查等都是在此阶段完成的。语法分析需要语法分析器。语义分析语义分析需要语义分析器。语义分析就是分析该语句的意思,就是它能做什么,有啥用。编译器所能做的包括静态语义分析和动态语义分析。静态语义:编译期能够确定的语义,它主要包括类型和声明的匹配,类型的 转换等。我想C+中的静态绑定应该也属于静态语义吧。动态语义:运行期能够确定的语义以及相关问题, 比如说异常处理。我同时 在想C+中的动态绑定应该属于动态语义。语义分析对语法树各节点进

31、行了类型标记和类型转换,还更新了符号表里的 符号类型。224中间语言生成编译器有很多层次的优化,源码级别的优化是其中一个层次。源码级的优化需要源码级优化器。这个优化是把语法树转换成中间代码,并在中间代码上进行的。常见的中间代码有三地址码和 P代码。中间代码将编译器分成了前端和后端,前端负责产生与机器无关的中间代 码,后端负责把中间代码转换成目标代码。跨平台的编译器并不是放在任意一个平台上都绝对能用,只不过它能支持的 平台很多而已。这是因为编译器使用同一个前端,而针对不同的平台使用不同的 后端。目标代码的生成与优化编译器的后端包括代码生成器和目标代码优化器。代码生成器将中间代码转换成目标代码,该

32、过程依赖于目标机器。目标代码优化器对目标代码进行优化, 比如选择合适的寻址方式,以移位代 替数乘等。现在的编译器非常复杂,上述提到的这些方面也变得非常复杂。变量和函数的地址都是在最终链接的时候才确定的,然后变成可执行文件。2.3链接器年龄比编译器长作者把链接比喻为拼图的拼接。2.4模块拼接一一静态链接将源代码模块组装起来的过程就是链接。链接的过程包括:1、地址和空间分配;2、符号决议;3、重定位等。.obj文件即目标文件和库一起链接成可执行文件。库是由一些常用的代码编译成的目标文件的包, 是一个集合。最常见的库是 运行时库,是支持程序运行的基本函数的集合。每个目标文件都是单独编译的。模块A想要

33、调用模块B的C函数,A必须要知道C的地址,但是现在A不 知道C的地址,但是A给C留了位置,等到链接器链接时再在这个位置上填上 C的地址。如果C的地址被改动了, A中所有调用C的地方都需要进行相应的 更改,这些都可藉由链接器完成。这是静态链接的基本功能和作用。在链接的过程中需要对目标文件中定义在其他目标文件中的函数和变量的调用指令进行重新调整,注意这里说的是指令!书中举的例子意在说明,当目标 文件A调用目标文件B中的变量C时,因为暂时无法知道C的位置,所以指令 先把表示C的位置置为某一值,等到链接的时候再把这值修正为 C的地址,这 一过程叫做重定位,像C这样的位置被称为重定位入口。第三章目标文件

34、里有什么.obj是目标文件,所以可以知道目标文件是指编译后生成的文件,目标文件几乎和可执行文件相同只是稍微有点不同而已。其不同之处在于有些符号和地址没有被调整。3.1目标文件的格式正是因为目标文件与可执行文件几乎相同,所以它们的存储格式是一样的, 可以把它们近似看成同一种文件。Linux下的动态链接库格式为.so,Windows和Linux下的静态链接库格式分 别为.lib和a静态链接库是一个文件,该文件包含了很多目标文件,它是一个整体。Linux下的可执行文件是按照ELF格式存储的,ELF标准包含4种文件,请 看P81。我所熟悉的 Windows下的DLL就属于共享目标文件。3.2目标文件是

35、什么样的目标文件一般包含了哪些内容?编译后的机器指令代码、数据、连接所需的信息、符号表、调试信息、字符串等。目标文件把信息按照属性的不同分段存储。写到这里我感觉这书上说的与老 师课上讲的程序在内存中的分段方法有些相似。在目标文件中,编译后的机器指令代码放在代码段(Code Section)中,段名一般为.code和.text。全局变量和静 态变量放在数据段(Data Section)中,段名一般为.data。BSS段(Block Started By Symbo I)用来存储未初始化的静态变量和全局变量。话虽如此bss中并没有这些变量的内容,它只是为这些变量按照所占空间大小预 留空间而已。由于

36、这些变量默认就是 0,所以压根没必要再为它们分配一个数据0,也没有必要让它们待在data段中。因此bss的作用是为这些变量预留空间。另外目标代码还有一个文件头用来保存该目标文件的信息,它里面还有一个段表。源代码被编译以后生成两种段数据段和指令段,.code.text属于指令 段.data.bss属于数据段。这样分主要有3点好处:1、防止程序被有意无意篡改。这是因为指令段只读,数据段可读写。2、提高了缓存命中率。3、节省内存空间。因为指令段可被多个副本共享,但是副本可以拥有自己 的数据段。3.3 挖掘 SimpleSection.o原来目标文件中的段还有只读数据段(.rodata)、注释信息段(

37、.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段。虽说bss存放的是未初始化的静态和全局变量,但是有些变量如果被初始化为0,它也会被放在bss中,这是编译器的优化,有时候这种优化会带来麻

38、烦。其他段表3-2列出了其他段及意义。此外,这个段还可以自定义。3.4 ELF文件结构描述图3-4展示了 ELF的层次结构。最重要的两个部分就是ELF文件头和段表。ELF文件头描述整个文件的基 本属性,段表描述各段的信息。文件头清单3-2清楚地描述了 ELF文件头的信息,P95黑体部分列举了 ELF文件 头包含的信息。ELF文件兼容各平台,它的文件结构和相关参数定义在” /usr/include/elf.h里, 它有32位和64位两种。表3-3展示了 elf.h的自定义变量体系。表3-4展示了 ELF文件头结构成员含义。ELF魔数:ELF文件头的第一个字段是Magic,包含16bytes,对应

39、于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文件类型,ELF总共有三种文件类型 如表3-5所示。操作系统是通过判断文件类型而不是扩展名来确定 ELF文件类 型的。Elf32_Ehdr中

40、的e_machine成员表示ELF文件的平台属性。虽然 ELF遵循 统一标准但不代表同一 ELF文件可以在不同平台上使用。段表它用来表示各个段的信息,ELF文件中的段是由段表决定的。一个ELF文件不仅仅包含像data text、bss这样的段,还包括其他的辅助性段。段表是一个Elf32_Shdr类型的结构体数组,元素的个数代表段的个数,每 个元素对应一个段。这个Elf32_Shdr被称为段描述符。表3-7描述了 Elf32_Shdr中各字段的意义。段的名称对于编译和链接有意义,对操作系统无意义。决定段的类型的是段 的类型字段,并不是段的后缀名和名称。段的类型和段的标志位字段决定了段的属性。表3

41、-8展示了段的各种类型。段的标志位表示该段在进程虚拟地址空间中的属性,如是否可读。表3-9列出了段的各种属性。表3-10列出了系统保留段的各种属性。段的连接信息包括sh_link和sh_info,它们与链接相关,如表3-11所示。重定位表目标文件中有一个SHT_REL的.rel.text字段,它是重定位表。重定位发生在连接的过程中,这个在前面已经讲过,重定位表记录了重定位相关信息。字符串表顾名思义,就是用来表示各种名称的字符串的表。它是一个装有各种字符串的表格,每个字符在表中都有一个固定的位置。这种表在ELF文件中保存为2种形式一一.strtab和.shstrtab,它们分别是字 符串表和段字

42、符串表,它们在ELF文件中都以独立的段而存在。为了轻松地找到这个段,在ELF文件头中包含了这两个段的下标,名为e_shstrndx)3.5链接的接口符号链接是组合目标文件的过程,目标文件是根据彼此之间的地址相互引用, 从 而组合成可执行文件的。而,这个地址可以简单地理解为目标文件中的函数和变 量。在这里,函数和变量统称为符号,函数名和变量名统称为符号名。链接器的着眼点主要在定义在本目标文件和定义在其他目标文件的全局性 符号,因为只有这些涉及到目标文件之间的组合。3.5.1 ELF符号表结构ELF文件的符号表是一个段,段名为“ .symtab”它是一个Elf32_sym类型 的数组,每个数组元素

43、代表一个符号。在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的意义有如下几种:1、如果符号定义在目标文件中,并且它不是COMMON 块类型, 则st_value代表符号在段中的偏移。2、 如果符号定义在

44、目标文件中并且是COMMON块类型,则st_value表示符号的对齐属性。3、在可执行文件中st_value表示符号的虚拟地址。特殊符号链接器本身自带的,不是你定义的,定义在链接脚本中的,但是你可以用的, 这样的符号是特殊符号。它们存在的时机是链接器链接生成可执行文件时,此时 链接器会将它们解析成正确的值,书中P110举了几个具有代表性的特殊符号。符号修饰与函数签名本小节明确了函数签名的概念。函数签名:主要是指函数名和参数类型,其次是所在类和命名空间等。它用 于区分不同函数。编译器和连接器会使用名称修饰的办法加工函数签名使之成为修饰后名称, 在C+中为符号名。不同的编译器对函数签名的修饰方法不

45、同, 这导致不同种类的目标文件无法 互连。原来C+编译器已经默认定义了宏 cplusplus来兼容C语言和C+。弱符号和强符号在不同目标文件中含有相同全局性符号定义,这种情况被称为强符号,它会引起符号重定义。C/C+编译器认为未初始化的全局变量是弱符号。这个强弱符号是可以被定义的,所以强弱之别是根据定义来划分的, 并不针 对符号的引用,P117代码说明了这一点。链接器根据符号的强弱来处理和选择定义的全局变量:1、不允许多次定义强符号,否则报错。2、同一个符号在各目标文件中出现了多次,但只有一个是强符号,那么编 译器选择强符号的那个。3、如果一个符号在所有目标文件中都是弱符号,那么编译器选择占用

46、空间最大的一个。由此可见编译器对于弱符号的选择并不明显,所以由弱符号造成的错误也相对难以发现。强引用:目标文件对于非本目标文件的符号引用, 在链接成可执行文件的过 程中,如果找不到该符号的定义,就报未定义错误。弱引用:与强引用差不多,只不过在找不到符号时不报错。强弱引用主要用于库的链接。对于未定义的弱引用,编译器为便于识别把它 看作是某一值,一般为0。弱符号与COMMON块联系较密切。弱引用是可以手动声明的,如 P118第一段代码所示。弱符号的作用在于提供一个默认的库符号, 但是当用户想要自定义该符号的 时候,该自定义符号就获得了更高的优先级。 而弱引用的作用在于增强了程序的 可扩展性,因为有

47、了弱引用程序功能更强,没有弱引用程序也能正常运行。3.6调试信息目标文件和可执行文件中都可能保存调试信息,ELF文件采用DWARF格式 保存调试信息。由于调试信息与可执行文件最终结果无关, 而且占用大量空间,所以在发布 软件时应该去掉这些调试信息。第4章静态链接静态链接是指将目标文件链接在一起形成可执行文件的过程。4.1空间与地址分配相似段合并静态链接过程是把各目标文件中的各段合并到可执行文件中的相应段中。链接器为目标文件分配地址和空间。 这个空间有两层含义,既包括在可执行 文件中占有的空间也包括在虚拟地址中分配的空间。其中虚拟地址空间的分配关 系重大。静态链接的过程一般分两步一一1、空间与地

48、址分配。2、符号解析与重定位。第一步就是获取段信息,合并段将它们映射到可执行文件的段表信息中。 整 理符号和引用并放入全局符号表中。第二步,实际上就是链接,把目标文件中的地址呀、符号呀、数据等进行重 定位然后链接。VMA:Virtual Memory AddressLMA : Load Memory Address链接前的VMA都是0,链接后就有实实在在的地址了。符号地址的确定符号地址在原来的目标文件中的每个段中都有一个偏移量,这个偏移量是固定的,所以在链接的过程中只要在虚拟地址的基础上再加上这个偏移量就是某符 号在虚拟地址空间中的地址。4.2符号解析与重定位在空间和地址分配完成以后,链接器即

49、将进行符号解析与重定位。本小节举 了个例子,用了很多汇编代码,有些晦涩难懂。目标文件中使用的都是虚拟地址不是物理地址,这一点很重要。目标文件的起始地址都是0。重定位表它存储着与重定位相关的信息每个要被重定位的ELF段都对应一个重定位表,重定位表本身也是一个段, 所以你也可以叫重定位表为重定位段。每一个要被重定位的地方叫做重定位入口。重定位入口的偏移表示入口在要被重定位的段中的位置。重定位表的实质是一个Elf32_Rel的结构体数组,每个数组元素对应一个重 定位入口423符号解析重定位的过程伴随着符号解析的过程。每个重定位的入口对应一个符号引用,链接器会查找有所有目标文件的符号 表所组成的全局符

50、号表,然后根据这个全局符号表进行重定位。指令修正方式32位x86平台下的ELF文件的重定位入口所修正的指令寻址方式只有2种:绝对近址32位寻址和相对近址32位寻址。修正的位置长度为4byte&经过绝对地址修正方式修正得到的地址是该符号的实际地址,而相对地址寻址方式得到的是符号与被修正位置的距离。4.3 COMMON 块相同的符号定义在多个不同的目标文件中, 但是类型各不相同,这说明它们 不是同一个变量或者函数,因此不能对它们进行相同的操作。但是链接器只认符 号不认类型,它认为它们都一样。这种情况主要分为3种:1、至少2个强符号类型不一致。2、一个强符号和多个弱符号类型不一致。3、至少2

51、个弱符号类型不一致。强符号是指定义在目标文件中全局性符号, 包括函数和变量,显然它们如果 有相同的多个,那就是重定义,这本身就会报错。现在的编译器和链接器都支持 COMMON块机制。它主要针对的对象是弱 符号。如果在众多符号之中有一个符号是强符号,那么符号所占空间与强符号相 同。如果弱符号大小超过强符号,编译器会发出警告。编译器为什么不把未初始化的全局变量当做未初始化的局部静态变量处理?为什么不在bss中给它们分配空间,而非要把它们标记为 COMMON类型 呢?因为编译时编译器不知道弱符号需要多大空间,所以这时无法为其在BSS中分配空间,只能当做局部静态变量处理。 但是在链接的时候可以确定,所

52、以链 接以后才在BSS中分配空间。编译器把所有未初始化的全局变量都当成 COMMON类型处理,这样做是 为了与强类型分开,凡是非 COMMON类型的都是强类型。多个强类型的符号 会发生重复定义的错误。重复代码消除C+在很多时候会产生重复代码,模版是其中最具代表性的一个。模版可以 在不同的编译单元被实例化成相同的类型,两个完全一样的类是完全没有必要 的,一个足矣。不解决代码重复问题会导致:1、空间浪费。这个根本就不用解释。2、地址容易出错。因为是多个相同的实例嘛,就会有多个指针分别指向这些实例,但是这些实例之间没差别,它们在逻辑上是同一函数,这就容易造成指 针的误指。3、指令运行效率较低。缓存机

53、制会缓存多份重复的代码,但是程序只会用 特定的一份,在这么多份相同的代码中找特定的一份不好找,成功率较低,即, 缓存命中率低。解决方案:把每个编译单元中的每个模版的不同实例分别放进不同的段中, 并且对不同的单元都这样做,这样在最后链接的时候不同编译单元中的相同实例 段就合并从而消除多份相同的实例。缺点:不同的编译单元可能使用了不同的编译器版本或者优化选项,这会导 致实际产生的代码不同,链接器必须选择其中一个副本。函数级别链接:默认情况下链接器会把所有的目标文件链接在一起,不管有用的代码还是没用的代码,这会导致可执行文件很大。所谓函数级别链接就是每个编译单元也把函数单独放进一个段中,在链接的时候

54、只链接那些有用的函数段。这种做法会减慢编译和链接的过程,因为段的数量增加了。442全局构造与析构在C+中全局对象的构造在main之前完成,析构在main之后完成。在ELF文件中有.init和.fini两个段。init段包含了进程的初始化代码,在 main之前执行。fini段包含了进程的终止代码,在 main之后执行。C+的全局构造和析构由此实现。4.4.3 C+与 ABI把不同编译器产生的目标文件链接在一起需要特定的条件一一相同的ABI(Applicati on Binary In terface)。ABI :符号修饰标准、变量内存布局、函数调用方式等与二进制兼容性相关 的内容。C语言间的目标

55、文件能否互相兼容具体决定于如下几个方面:1、内置类型大小和存储方式。2、组合类型大小和存储方式。3、外部符号与用户定义的符号之间的命名方式和解析方式。4、函数调用方式。5、堆栈分布方式。6寄存器使用方式。C+在这方面的决定因素 P141+P142介绍。C+代码不仅对于由不同编译器编译得到的目标文件不兼容,而且就算是同一编译器的不同版本编译得到的目标文件也不兼容。这都是ABI闹的。4.5静态库链接开发环境往往附带语言库,这些库是对系统 API的封装。大部分的C语言 库函数都调用了系统API,少数除外。静态库实际上可以看成是一组目标文件的集合。C语言中看似简单的库函数和系统中众多的 API存在着依赖关系。静态链接的过程分为三步:1、调用C语言

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论