内容简介 · · · · · ·
这本书主要介绍系统软件的运行机制和原理,涉及在Windows和Linux两个系统平台上,一个应用程序在编译、链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现,C/C++运行库的工作原理,以及操作系统提供的系统服务是如何被调用的。每个技术专题都配备了大量图、表和代码实例,力求将复杂的机制以简洁的形式表达出来。本书最后还提供了一个小巧且跨平台的C/C++运行库MiniCRT,综合展示了与运行库相关的各种技术。
对装载、链接和库进行了深入浅出的剖析,并且辅以大量的例子和图表,可以作为计算机软件专业和其他相关专业大学本科高年级学生深入学习系统软件的参考书。同时,还可作为各行业从事软件开发的工程师、研究人员以及其他对系统软件实现机制和技术感兴趣者的自学教材。
目录 · · · · · ·
第1章 温故而知新
1.1 从HELLO WORLD 说起
1.2 万变不离其宗
1.3 站得高,望得远
1.4 操作系统做什么
1.5 内存不够怎么办
1.6 众人拾柴火焰高
1.7 本章小结
第2部分 静态链接
第2章 编译和链接
2.1 被隐藏了的过程
2.2 编译器做了什么
2.3 链接器年龄比编译器长
2.4 模块拼装——静态链接
2.5 本章小结
第3章 目标文件里有什么
3.1 目标文件的格式
3.2 目标文件是什么样的
3.3 挖掘SIMPLESECTION.O
3.4 ELF 文件结构描述
3.5 链接的接口——符号
3.6 调试信息
3.7 本章小结
第4章 静态链接
4.1 空间与地址分配
4.2 符号解析与重定位
4.3 COMMON 块
4.4 C++相关问题
4.5 静态库链接
4.6 链接过程控制
4.7 BFD 库
4.8 本章小结
第5章 WINDOWS PE/COFF
5.1 WINDOWS 的二进制文件格式PE/COFF 134
5.2 PE 的前身——COFF
5.3 链接指示信息
5.4 调试信息
5.5 大家都有符号表
5.6 WINDOWS 下的ELF——PE
5.7 本章小结
第3部分 装载与动态链接
第6章 可执行文件的装载与进程
6.1 进程虚拟地址空间
6.2 装载的方式
6.3 从操作系统角度看可执行文件的装载
6.4 进程虚存空间分布
6.5 LINUX 内核装载ELF 过程简介
6.6 WINDOWS PE 的装载
6.7 本章小结
第7章 动态链接
7.1 为什么要动态链接
7.2 简单的动态链接例子
7.3 地址无关代码
7.4 延迟绑定(PLT)
7.5 动态链接相关结构
7.6 动态链接的步骤和实现
7.7 显式运行时链接
7.8 本章小结
第8章 LINUX 共享库的组织
8.1 共享库版本
8.2 符号版本
8.3 共享库系统路径
8.4 共享库查找过程
8.5 环境变量
8.6 共享库的创建和安装
8.7 本章小结
第9章 WINDOWS 下的动态链接
9.1 DLL 简介
9.2 符号导出导入表
9.3 DLL 优化
9.4 C++与动态链接
9.5 DLL HELL
9.6 本章小结
第4部分 库与运行库
第10章 内存
10.1 程序的内存布局
10.2 栈与调用惯例
10.3 堆与内存管理
10.4 本章小结
第11章 运行库
11.1 入口函数和程序初始化
11.2 C/C++运行库
11.3 运行库与多线程
11.4 C++全局构造与析构
11.5 FREAD 实现
11.6 本章小结
第12章 系统调用与API
12.1 系统调用介绍
12.2 系统调用原理
12.3 WINDOWS API
12.4 本章小结
第13章 运行库实现
13.1 C 语言运行库
13.2 如何使用MINI CRT
13.3 C++运行库实现
13.4 如何使用MINI CRT++
13.5 本章小结
附录A
A.1 字节序(BYTE ORDER)
A.2 ELF 常见段
A.3 常用开发工具命令行参考
索引
· · · · · · (收起)
豆瓣成员常用的标签(共331个) · · · · · ·
喜欢读"程序员的自我修养"的人也喜欢的电子书 · · · · · ·
喜欢读"程序员的自我修养"的人也喜欢 · · · · · ·
书评 · · · · · · (共58条)
我来评论这本书-
vivi (执子之手,将子拖走)
一、程序的编译过程,目标文件里究竟是什么 从源程序到目标文件的生成过程 最简单的编译命令是gcc helloworld.c,它包含了以下几个步骤: 预处理、编译、汇编、链接,下面分别简介。 预处理:处理#define宏定义、#if #ifdef等条件编译指令、#include预编译指令,删除注释,添加行号和文件名标识,保留所有的#pargma编译器指令,经过预编译后的文件为.i文件。预编译命令为:gcc -E hello.c -o hello.i或者cpp hello.c > ... (2回应)2011-05-06 17:24
一、程序的编译过程,目标文件里究竟是什么从源程序到目标文件的生成过程最简单的编译命令是gcc helloworld.c,它包含了以下几个步骤:预处理、编译、汇编、链接,下面分别简介。预处理:处理#define宏定义、#if #ifdef等条件编译指令、#include预编译指令,删除注释,添加行号和文件名标识,保留所有的#pargma编译器指令,经过预编译后的文件为.i文件。预编译命令为:gcc -E hello.c -o hello.i或者cpp hello.c > hello.i编译:把预处理完得文件进行一系列的词法分析、语法分析、语意分析及优化后产生的汇编代码文件。编译命令为gcc -S hello.i -o hello.s。现在版本的gcc把预编译和编译两个步骤合并成一个步骤,使用ccl程序来完成,命令为ccl hello.c。也可以使用gcc -S hello.c -o hello.s直接从.c文件生成.s汇编文件。汇编:将汇编代码转变成机器可以执行的指令,每一条汇编语句几乎都对应一条机器指令。命令为:as hello.s -o hello.o或者gcc -c hello.s -o hello.o或者我们最熟悉的gcc -c hello.c -o hello.o链接:当我们的程序模块调用a另一个模块中b的函数(foo())或变量时,在编译的阶段编译器并不知道函数foo的地址,所以暂时把调用foo的指令的目标地址搁置,等待最后链接的时候由连接器去将这些指令的目标地址修正。把目标文件和库一起链接成可执行文件。最常见的库时运行时库。目标文件中的格式目标文件就是源代码编译后但未进行链接的那些中间文件,它和可执行文件的内容和结构其实很相似,所以一般和可执行文件采用同一种格式存储,那就是ELF格式。与ELF格式相对应的是Windows平台下的PE格式,它们都是COFF格式的变种。不光是可执行文件和目标文件按照ELF格式存储,动态链接库(.so)和静态链接库(.a)都按照ELF格式存储。ELF格式的文件可分为以下4类:1 可重定位文件(Relocatable File):这类文件包含了代码和数据,可以被用来链接成可执行文件或共享目标文件,静态链接库也可以归为这一类,代表是Linux的.o文件2 可执行文件(Executable File):这类文件包含了可以直接执行的程序,它的代表就是ELF可执行文件,它们一般都没有扩展名,如/bin/bash文件3 共享目标文件(Shared Object File):这种文件包含了代码和数据,可以在以下两种情况下使用。一种是链接器可以使用这种文件跟其他的可重定位文件和共享目标文件链接,产生新的目标文件。第二种是动态链接器可以将这几个共享目标文件与可执行文件结合,作为进程影响的一部分来运行。4 核心转储文件(Core Dump File):当进程意外终止时,系统可以将该进程的地址空间的内容以终止时的一些其他信息转储带核心转储文件。上面几种文件在file命令下会显示出相应的类型。目标文件里有什么:ELF文件最重要的结构是ELF文件头(ELF Header):ELF文件头里定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段得数量等。三个最重要的段:代码段(.text)、数据段(.data)和只读数据段(.rodata)、BSS段(.bss)可以用objdump -s -x -d hello.o来分析各个段得内容,-d表示反汇编,-s表示把各段内容用16进制打印,-x表示详细数据。 顾名思义,.text段主要存放可执行的代码数据;.data段保存的是已经初始化了的全局变量和静态变量;.rodata段保存只读数据,一般是程序里的只读变量(如const修饰的变量)和字符串常量;bss保存未初始化的全局变量和静态变量。除此之外,还有一些常见的段,如.comment段存放编译器版本信息,比如字符串“GCC:(GNU)4.2.0”.dubug段存放调试信息.dynamic存放动态链接信息.hash段存放符号哈希表.line段存放调试时的行号表.note段存放额外的编译器信息.strtab段存放字符串表,用于存储ELF文件中用到的各种字符串,比如符号的名字.shstrtab段存放段表中用到的字符串,最常见的就是段名.symtab段是符号表,通过符号表就能知道这个符号在哪个段,以及在这个段的具体位置,还有这个符号在字符串表中的位置Section Table(段表)也是其中一个段,它保存了各个段的信息,如段名、段的长度、在文件中的偏移、读写权限及段的其他属性.rel.text段是一个重定位表,正如前面所说的,链接器在处理目标文件时,需要对目标文件的某些部位进行重定位,即代码段和数据段中那些对绝对地址的引用的位置,这些重定位的信息都记录在ELF文件的重定位表里通过ELF文件头的信息可以找到段表的位置,从而找到各个段得位置和信息。可以用readelf -S hello.o命令详细查看ELF文件中各段的信息。二、可执行文件的装载程序执行时所需要的指令和数据必须都在内存中才能正常运行,最简单的办法就是将程序运行时所需要的指令和数据全部都装入内存,这样程序就能顺利执行,这就是最简单的静态装入的方法。但是程序所需要的内存数量可能大于物理内存,静态装入就不太现实。 后来研究发现,程序运行时具有局部性原理,所以我们可以将程序最常用的部分驻留在内存中,而将一些不太常用的数据存放在磁盘上,这就是动态装载的原理。 覆盖装入和页映射是两种典型的动态装入方法。覆盖装入就是如果两个模块不会同时运行,则可以使这两个模块共用一块内存,需要哪个模块的时候就装入。现在基本已经淘汰了。 页映射是虚拟存储机制的一部分,它随着虚拟存储的发明而诞生。在页映射机制中,程序装载和操作的单位都是页。最常见的Inter IA32处理器一般都使用4KB大小的页。 假设程序所有的指令和数据总和为32kB,那么程序总共分为8页,将其编号为P0-O7,并假设物理内存只有16kB,编号为F0-F3。如果程序刚开始执行时的入口地址为P0,这时装载管理器发现程序的P0不在内存中,于是将F0分配给P0,并将P0的内容装入F0;运行一段时间后,程序需要用到P5,于是装载管理器将P5装入F1,就这样,当程序用到P3和P6时,它们分别装入到了F2和F3。 很明显,如果这时程序只需要P0,P3,P5和P6这4个页,那么程序就能一直运行下去。但是如果这时候需要访问P4,那么装载管理器需要最初选择,它必须放弃目前正在使用的4个物理内存页中的一个来装载P4。至于选择哪个页,可以有多种算法选择,比如FIFO先入先出算法,LRU最近最少使用算法等。 其实,上面所说的装载管理器就是操作系统的存储管理器。从操作系统的角度看可执行文件的装载 从操作系统的角度看,一个进程最关键的特征就是它拥有独立的虚拟地址空间,这使得它有别于其他进程。这在http://blog.csdn.net/vividonly/archive/2011/05/04/6393516.aspx一文中有详细解释。要使一个可执行程序得以执行,首先必须创建一个进程,然后装载相应的可执行文件并且执行。在有虚拟存储的情况下,上述过程最开始只要做三件事: 1 创建一个独立的虚拟地址空间。这时候并不设置虚拟地址页和物理地址页的映射关系,这些映射关系等到后面程序发生页错误的时候再进行设置。当发生页错误时,操作系统将从物理内存中分配一个物理页,然后将该“缺页”从磁盘中读取到内存中再设置缺页的虚拟页与物理页的映射关系。这都是通过CPU的MMU来实现的。 2 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。这个映射关系在进程中叫做VMA(虚拟内存区域)。比如对于.text段,在创建进程后,会在进程相应的数据结构中设置一个.text段的VMA,记录了它在虚拟空间的地址以及它在ELF文件中的偏移。当程序执行发生页错误,通过查找VMA结构来定位错误页在可执行文件的位置,把可执行文件的那部分内容装入刚分配的物理内存中,进程从刚才页错误的位置重新开始执行。 3 将CPU的指令寄存器设置为可执行文件的入口地址,启动运行。操作系统通过设置CPU的指令寄存器将控制权转交给进程。这里的入口地址就是ELF文件头中的入口地址。2回应 2011-05-06 17:24 -
邻水风竹 (惯看秋月春风)
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,大部分的中间层都是为了封装变化点 linux三个进程创建函数的区别 fork 复制当前进程 exec 使用新的可执行映像覆盖当前可执行映像 clone 创建子进程并从指定位置开始执行 几种锁机制 互斥量:获取互斥量的线程负责释放互斥量,且对系统中任何进程可见 信号量:信号量可由某个线程获取,由另外的线程释放,且对系统中任何进程可见 临界区:可看作只限于..2011-12-08 13:25
计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,大部分的中间层都是为了封装变化点linux三个进程创建函数的区别fork 复制当前进程exec 使用新的可执行映像覆盖当前可执行映像clone 创建子进程并从指定位置开始执行几种锁机制互斥量:获取互斥量的线程负责释放互斥量,且对系统中任何进程可见信号量:信号量可由某个线程获取,由另外的线程释放,且对系统中任何进程可见临界区:可看作只限于进程内的互斥量volatile 扩展,可防止编译器过度优化后指令执行顺序改变,导致程序异常,volatile 扩展可保证的两件事情1、阻止编译器为了提高速度将一个变量缓存到寄存器而不写回内存2、阻止编译器调整操作volatile变量的指令顺序内存屏障barrier()可阻止CPU将barrier前后的指令执行顺序交换,以解决乱序执行所带来的问题readelf:查看elf文件的工具editbin可更改DLL基地址修饰后符号名称解析binutils中的c++filt可用来解析被gcc修饰过的符号名称Windows API “UnDecorateSymbolName”可解析被Visual C++编译器的名称修饰规则修饰的符号C++ 代码会自动定义一个宏__cplusplus,可用来解决可能在 C 或 C++ 代码中使用的C库头文件的兼容问题,C++引用C库函数,须在函数声明前加extern "C"强符号:函数和初始化了的全局变量弱符号:未初始化的全局变量__attribute__(weak)将某符号强制性的变为弱符号强符号不能被重定义,但允许存在多个与其同名的弱符号。多个同名的弱符号可以并存,引用时实际使用强符号或占用空间最大的弱符号强引用必须有定义弱引用可没有定义,为定义的弱引用默认值为0,或一个特殊值ABI:指与符号修饰标准、变量内存布局、函数调用方式等跟可执行代码二进制兼容性相关的内容VC 编译预处理指令#pragma data_seg("foo")。。。pragma后定义的变量将全部放入foo段中,除非用另一条pragma预处理指令进行了段切换装载时重定位的动态链接方式,执行速度快,但共享对象无法实现进程间共享地址无关代码,执行速度慢,但可实现进程间共享,地址无关代码一般通过间接跳转的.got段实现动态加载存在同名符号覆盖的情况,此现象称为全局符号介入,因此可能引起程序逻辑错误程序运行中几个比较难发现的逻辑错误1、由于使用了不同版本的编译器,导致ABI不兼容,如不同版本编译器堆栈结构、符号修饰规则、调用习惯、数据结构内存分布、对齐方式等存在差异2、动态链接时发生了未预料的同名符号覆盖3、在多线程程序中使用了非线程安全的函数4、编译器过度优化,更改指令执行顺序(使用volatile解决)5、CPU 乱序执行导致程序异常(使用barrier解决)6、共享库内分配的内存在共享库外释放,或相反的方式,Linux 共享库命名规则:libname.so.x.y.zx:主版本号,表示重大升级,存在不兼容更新y:次版本号,表示增量升级,如添加新的接口功能,为兼容性更新z:分布版本号,表示只有一些错误修正,性能改进等,为兼容性更新libname.so.x又称为共享库的SO-NAMELinux系统为每一类兼容的共享库创建一个以SO-NAME为文件名的软链接命令行中使用的几个可改变动态链接器行为的环境变量添加共享库优先搜索路径LD_LIBRARY_PATH=/home/user cmd预先加载的共享库,可引起链接时同名符号覆盖LD_PRELOAD输出动态链接过程,可以去下列值:files、bindings、libs、versions、reloc、...、allLD_DEBUGgcc编译的共享库,默认情况下没有SO-NAME,可通过-Wl,-soname,myname参数指定DLL导出函数可通过在传统的函数定义前加__declspec(dllexport)来导出,而不用def文件,使用导出函数时在传统声明前加__declspec(dllimport)MSVC中C语言的默认调用规范为_cdecl,不对函数进行符号修饰,__stdcall为windows的通用调用规范,会对符号进行修饰,如:_Sub@16DLL的代码并非地址无关,因此不一定能被多个进程共享,即不一定支持一份代码多进程地址空间映射DLL优化如果知道DLL被加载的位置,使用更改DLL基地址的方法可以免去重定基地址,以提高程序的加载速度使用序号作为导入导出符号的手段,可免去动态链接时导出符号的二分查找导入函数DLL绑定,可免去动态链接时的符号解析,但DLL更新后或重定基地址后可能导致绑定失败。C++编写动态库注意事项:所有接口函数应该都为抽象的全局函数使用extern "C"来防止名字修饰,导出函数使用__stdcall调用规范不使用STL库、异常、虚析构函数不在DLL中申请内存而在DLL外释放内存或相反的方式,因为不同的DLL和可执行程序使用不同的堆接口不使用重载方法COM的主要目的之一是解决C++动态库的ABI兼容性问题不到万不得已,不要将大尺寸的对象或结构变量直接通过函数返回值返回,对象或结构变量的返回在实际实现上是通过两次内存拷贝完成的,效率不高线程的私有空间:栈、寄存器、TLS(线程局部存储,一般在进程堆中分配)静态链接C语言标准库crt时,如果通过ExitThread退出线程,线程私有数据_tidata将不会被释放,从而导致内存泄漏,因此最后使用经过包装后的线程函数,如C语言库的__beginthread,_endthreadLinux的每个进程都有自己的内核栈和用户栈回应 2011-12-08 13:25
-
赵赵 (爱菜酱)
构建Build过程:4个步骤 预处理prepressing、编译compilation、汇编assembly、链接linking ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 预编译:gcc -E 预编译命令的展开 #define #include 保留#pragma 生成.i文件 编译:gcc -S 生成.s文件 实际为cc1命令(包含预编译和编译) 词法分析:分割记号(tokens) lex程序 语法分析:语法树(syntax tree) yacc程序 语义分析:静...2013-06-06 14:56
构建Build过程:4个步骤预处理prepressing、编译compilation、汇编assembly、链接linking~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~预编译:gcc -E 预编译命令的展开 #define #include 保留#pragma 生成.i文件编译:gcc -S 生成.s文件 实际为cc1命令(包含预编译和编译) 词法分析:分割记号(tokens) lex程序 语法分析:语法树(syntax tree) yacc程序 语义分析:静态语义分析 声明和类型的匹配,类型的转换。语法树被标记类型(commented syntax tree)。 中间代码生成:源代码优化器将语法树转换成中间代码(Intermediate Code与机器无关,不包含数据尺寸、变量地址和寄存器的名字),例如:三地址码和P-代码。并对其进行优化,例如常量计算。为编译器前端。 目标代码的生成与优化:为编译器后端包括代码生成器(Code Generator)和目标代码优化器(Target Code Optimizer)。生成目标机器代码。CPU指令集的优化。汇编:gcc -c 生成.o目标文件 实际为as命令 将汇编代码转变成机器可以执行的指令链接:ld命令 包括:地址和空间分配、符号决议和重定向等步骤。回应 2013-06-06 14:56 -
赵赵 (爱菜酱)
临界区和互斥量与信号量的区别在于,互斥量和信号量在系统中任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质。2013-06-05 17:00
论坛 · · · · · ·
| 【bug】 来点干货。书中的一处错误。 | 来自lili | 3 回应 | 2013-04-27 |
| 知其然,更知其所以然——潘爱民 | 来自叶卡 | 3 回应 | 2011-11-21 |
| 博文视点致力原创——春播夏长,期待辉煌 | 来自博文视点 | 2009-06-05 | |
| 本书的勘误和参考文献已放在互动网 | 来自sunlight | 1 回应 | 2013-02-12 |
| 对于本科生来说看了绝对是质的变化。 | 来自叶卡 | 2009-05-14 |
> 浏览更多话题
在哪儿买这本书?
以下豆列推荐 · · · · · · ( 全部 )
- 运行时系统 (baozii)
- 博文视点原创精品图书 (博文视点)
- IT技术 (Divine)
- 3.linux内核 (葡萄呓语)
- 计算机理论 (mashan_snail)
谁读这本书?
喜欢这本书的人常去的小组 · · · · · ·

- LISP (3864)

- Vim (11965)

- Compiler (1142)

- 博文视点交流组 (391)

- 我们就爱复杂的编程语言 (834)

- Python编程 (33587)

- Emacs (3882)

- Haskell (1562)
二手市场
订阅关于程序员的自我修养的评论:
feed: rss 2.0











ziyoudefeng
2012-12-09 21:24 1人收藏
vivi (执子之手,将子拖走)
2011-05-06 17:24
赵赵 (爱菜酱)
2013-06-06 14:56
赵赵 (爱菜酱)
2013-06-05 17:00