注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

秒大刀 博客

好好学习 天天向上

 
 
 

日志

 
 
 
 

读《程序员的自我修养-链接装载与库》  

2010-11-05 22:30:39|  分类: 读书 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
读《程序员的自我修养-链接装载与库》 - 秒大刀 - 秒大刀的城堡
 
    本书内容宛如其副标题——链接、装载与库。比较清晰的介绍了程序从编译到链接到最后由操作系统加载并结合运行时库执行起来的过程,对程序的产生和运行流程剖析的可算是入木三分。计算机组成原理讲的是硬件如何转起来的,而本书则讲的是软件如何转起来的。
    不喜欢主标题扣上“程序员的自我修养”这顶大帽子,貌似不懂本书所讲的就不是程序员了?或者说至多算个没修养的程序员?书中对某些细节过于追究,当然也反映出了作者可能对这些细节比较熟悉,但并非所有的读者都对这些细节感兴趣。或者说有点目标读者群体定位不精准,知识层面划分不明确的嫌疑。
    不推荐群体:新人;非C/C++程序员;应用层程序员
    推荐群体:C/C++偏底层程序员;有2年以上的C/C++经验并且对底层想澄清下的应用层程序员
    总体感受:还行,毕竟是国人自己写的书,能到这程度已经很欣慰,学术味淡点会更好
    感谢宋传波大侠赐宝一阅!

正文之前
  • 很简单的道理,本以为自己明白的很,但要写出来让人明白,却是件非常不容易的事
  • 你可以不自己造轮子,但应该了解轮子的构造,而且越详尽越好,中恶就是程序的自我修养吧

第1部分 简介
第1章 温故而知新
  • 计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决
  • 在尽可能少甚至不改变其他层的情况下,新增加一个层次就可以提供前所未有的功能
  • 相对于多进程应用,多线程在数据共享方面的效率要高得多
  • IO密集型线程总是比CPU密集型线程更容易得到优先级的提升
  • Linux对多线程的支持颇为贫乏,在Linux内核中并不存在真正意义上的线程概念
  • fork产生新任务的速度非常快,它并不复制原任务的内存空间,而是和原任务一起共享一个写时拷贝(Copy on Write, COW)的内存空间
  • 函数可重入的特点:
    • 不使用任何(局部)静态或全局的非const变量
    • 不返回任何(局部)静态或全局的非const变量的指针
    • 仅依赖于调用方提供的参数
    • 不依赖任何单个资源的锁
    • 不调用任何不可重入的函数
  • volatile关键字的作用:
    • 阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回
    • 组织编译器调整操作volatile变量的指令顺序(但无法阻止CPU在执行时对指令的动态调度换序)
  • barrier指令可以阻止CPU对该指令前后的指令进行乱序执行
  • 用户态线程并不一定在操作系统内核里对应同等数量的内核线程。存在“一对一”、“一对多”或“多对多”模型

第2部分 静态链接
第2章 编译和链接
  • 预编译主要处理规则
    • 将所有的#include删除,并且展开所有的宏定义
    • 处理所有的条件编译指令,比如#if、#elif、#else、#endif
    • 处理#include预编译指令,将被包含的文件插入到预编译指令的位置。此过程递归执行
    • 删除所有的注释
    • 添加行号和文件名标识,以便于编译时产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号
    • 保留所有的#pragma指令,因为编译器要使用它们
  • 直接在语法树上作优化比较困难,所有源代码优化器往往将整个语法树转换成中间代码。中间代码的存在也使得编译器被拆分为前端和后端
  • 运行时库是支持程序运行的基本函数的集合

第3章 目标文件里有什么
  • obj目标文件从结构上讲,它已经是可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同
  • 现在PC平台上流行的可执行文件格式主要是Windows下的PE(Portable Executable)和Linux上的ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种
  • 很多问题在表面上看似简单,其实深入内部会发现很多鲜为人知的秘密,或者发现以前自己认为理所当然的东西居然是错误的,或者是有偏差的
  • 真正了不起的程序员对自己程序的每一个字节都了如指掌
  • 由于不同的编译器采用不同的名字修饰方法,必然会导致由不同编译器编译产生的目标文件无法正常相互链接,这是导致不同编译器之间不能互操作的主要原因之一
  • 编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号
  • 链接器对强符号和弱符号的处理规则
    • 不允许强符号被多次定义
    • 如果一个符号在某个目标文件中为强符号,在其他文件中都是弱符号,那么选择强符号
    • 如果一个符号在所有目标文件中都是弱符号,那么选择其中占用空间最大的一个
  • 调试信息在目标文件和可执行文件中占用很大的空间,往往比程序的代码和数据本身大好几倍

第5章 Windows PE/COFF
  • PE文件在装载时被直接映射到进程的虚拟空间中运行,是进程虚拟空间的映像。PE可执行文件很多时候也被叫做“映像文件”
  • PE/COFF格式与ELF文件格式非常相似,都是基于段结构的二进制文件
  • Windows下最常见的目标文件格式为COFF格式,COFF中有个“.drectve段”,该段中保存的是编译器传递给链接器的命令行参数,可以通过这个段实现指定运行时库等功能
  • Windows下的exe、dll等都是PE格式,PE是COFF的改进版,增加了PE文件头、数据目录等一些结构

第3部分 装载与动态链接
第6章 可执行文件的装载与进程
  • 进程最关键的特征是它拥有独立的虚拟地址空间
  • 在将目标文件链接成可ELF执行文件的时候,链接器会尽量把相同权限属性(读写属性)的段Section合并成Segment以分配在同一空间减少内存浪费
  • 随机地址空间分布技术可以防止程序受恶意攻击,但同时可能使得进程的堆空间变小
  • 在32位的PE文件中,段的起始地址和长度都是4096字节的整倍数
  • linux启动进程时会先读取可执行文件的前128个字节,然后根据其中的魔数来判断可执行文件的类型(比如ELF、bash等),并调用支持相应格式的装载处理过程
  • PE文件加载流程:
    • 先读取一个页,其中包含了DOS头、PE文件头和段表
    • 检查进程空间中,目标地址是否可用,如果不可用则另外选一个装载地址(Rebasing)。因exe文件往往是第一个装入进程空间的模块,该问题基本不存在。但对dll而言,该过程非常重要
    • 使用段表中提供的信息,将PE文件中所有的段一一映射到地址空间中相应的位置
    • 如果装在地址不是目标地址,则进行Rebasing
    • 装载所有PE文件所需要的dll文件
    • 对PE文件中所有导入符号进行解析
    • 根据PE头中指定的参数,建立初始化堆和栈
    • 建立主线程并且启动进程
  • PE文件中,与装载相关的主要信息都包含在PE扩展头和段表中。

第7章 动态链接
  • Linux ELF *.so中所有使用全局变量的指令都指向位于可执行文件中的那个副本。ELF共享库在编译时,默认都把定义在模块内部的全局变量当作定义在其他模块的全局变量,通过GOT(Global Offset Table,全局偏移表)来实现变量的访问。当共享库被装载时,如果某个全局变量在可执行文件中拥有副本,那么动态连接器就会把GOT中的相应地址指向该副本,这样该变量在运行时实际上最终就只有一个实例。如果变量在共享模块中被初始化,那么动态连接器还需要将该初始化值复制到程序主模块中的变量副本,如果该全局变量在程序主模块中没有副本,那么GOT中的相应地址就指向模块内部的该变量副本。
  • 据统计ELF程序在静态链接下要比动态库稍微快点,大约为1%~5%

第8章 Linux共享库的组织
  • 二进制接口ABI(Application Binary Interface)的兼容性跟程序语言有着很大的关系
  • 用C++作为共享库接口并且维持兼容性是非常困难的

第9章 Windows下的动态链接
  • Dll的设计目的与共享对象有些出入,dll更强调模块化
  • ELF中,共享库中所有的全局函数和变量在默认情况下都可以被其他模块使用,ELF默认导出了所有的全局符号;dll中需要显式的告诉编辑器我们需要导出某个符号,否则编译器默认所有符号都不导出
  • .def文件也可以用来指示导入导出符号,该方法对C++语言之外的其他语言也有效
  • .def文件可以将导出函数重新命名
  • __stdcall是Windows下的编程语言所支持的通用规范,作为一个能够被广泛使用的dll最好采用__stdcall的函数调用规范。而MSVC默认采用的是__cdecl的调用规范。可以采用.def文件可以轻松实现导出符号的重命名。
  • .def文件还可以控制一些高级的链接过程
  • 在创建dll的同时会得到一个exp文件,这个文件实际上是链接器在创建dll时的临时文件
  • dll支持导出重定向(Export Forwarding),可以通过def文件实现
  • 链接器在一般情况下是不会产生指令的
  • dll的代码段和数据段本身并不是地址无关的,如果默认加载地址被占用,则需要加载到其他地址并进行整个dll的rebase,这会在一定程度上影响dll性能
  • 对拥有大量符号的dll进行字符串比对的符号解析是影响dll性能的另外一个原因
  • exe文件的默认加载地址为0x00400000,而dll的默认加载地址为0x10000000
  • 一个进程中,多个dll不可以被装在到同一个虚拟地址,每个dll所占用的虚拟地址区域之间都不可以重叠
  • dll的重定基地址(rebasing)方法导致的一个问题:如果一个dll被多个进程共享,且该dll被这些进程装载到不同的位置,那么每个进程都需要有一份单独的dll代码段的副本。如此相比ELF的共享对象代码段地址无关方案更耗内存,但可能有着更高的运行速度。
  • 可以给链接器指明BASE参数来改变dll的默认加载地址,以此来避免应用程序加载dll时的Rebasing。另外有个editbin工具可以直接修改dll的默认加载地址。Windows系统的核心dll都是经过严密的加载地址排布的,防止运行时不必要的Rebasing。
  • 对于仅在内部使用并防止外部使用者误用的函数,可以仅采用导出序号而无导出符号名的方式。采用序号导出而不是符号导出的方式,在如今的硬件平台上获得的性能提升非常有限。
  • 可以通过链接器或者editbin工具将exe绑定到所依赖的dll上,这样就能避免每次程序启动时重复的dll符号解析。exe启动时会自动校验依赖,只有当依赖未发生改变时才调用绑定的地址,否则按照普通的符号解析流程执行。在编译或安装时对exe进行绑定会加快程序的加载速度,并且至少不会带来坏处。事实上,Windows系统所附带的程序都是与它所在的Windows版本的系统dll绑定的。
  • 每个CRT都会有自己独立的堆,在一个CRT中申请内存而在另外一个CRT中释放内存会出错
  • 用C++编写的dll将很难维护其兼容性
  • Windows下采用C++编写dll的建议:
    • 所有的接口函数都应该是抽象的,所有的方法都应该是纯虚的。或者是inline的方法也可以
    • 所有的全局函数都应该使用extern “C”来防止名字修饰的不兼容。并且导出函数都是__stdcall调用规范(COM的dll都使用该规范)。这样即使用户本身的程序默认以__cdecl方式编译的,对于dll的调用也能够正确
    • 不要使用C++标准库STL
    • 不要使用异常
    • 不要使用虚析构函数。可以创建一个destroy()方法,并重载delete操作符来调用destroy。
    • 不要在dll里面申请内存,而且在dll外释放(或者相反),不同的dll和可执行文件可能使用不同的堆。对于内存分配相关的函数不应该是inline的,以防止编译时被展开到不同的dll和可执行文件
    • 不要在接口中重载方法。因为不同的编译器对于vtable的安排可能不同

第4部分 库与运行库
第10章 内存
  • 管理着堆空间分配的往往是运行库而不是操作系统内核
  • 每个线程的栈都是独立的,一个进程中有多少个线程,就应该有多少个对应的栈。Windows上每个线程默认的栈大小为1MB,可以在线程创建的CreateThread函数中指定
  • VirtualAlloc是OS提供的最底层内存批发函数
  • Windows用户层面的堆管理器通过HeapCreate、HeapAlloc、HaapFree、HeadDestroy来转接调用VirtualAlloc来实现的。而运行时库中的malloc则是对HeapXXX系列函数的再次封装
  • Windows进程创建时都会有一个默认大小为1M的默认堆,可以通过链接器的/HEAP参数来调整
  • Windows下一个进程中可能存在多个堆,一次能够分配的最大堆空间大小由最大的堆剩余空间决定

第11章 运行库
  • alloc是唯一可以不使用堆的动态分配机制,alloc可以在栈上分配任意大小的空间(只要栈的大小允许),并且在函数返回的时候自动释放
  • MSVC的CRT默认的入口函数名为mainCRTStartup
  • OS API上设计一层句柄可以防止用户随意读写操作系统内核对象。句柄和内核对象是相关联的,但如何关联的细节用户并不可见。OS内核可以通过句柄计算出内核对象的地址,但此能力并不对用户开放
  • 变长参数宏:
    • GCC:#define printf(args…) fprintf(stdout, ##args)
    • MSVC:#define printf(…) fprintf(stdout, __VA_ARGS__)
  • Linux上的C运行时库为glibc(GNC C Library),Windows上的C运行时库为MSVCRT(Microsoft Visual C Run-time)
  • 线程局部存储(Thread Local Storage, TLS)是OS为线程单独提供的私有空间,但通常只具有很有限的容量。GCC的TLS关键字为__thread,MSVC的TLS关键字为__declspec(thread)
  • Windows Vista和2008之前的OS,如果TLS的全局变量被定义在一个dll中,并且该dll是由LoadLibrary显示加载的,则该全局变量将无法使用
  • 当一个进程/线程开始或退出的时候,每个dll的DllMain都会被调用一次
  • Windows下当时用CRT时,尽量使用_beginthread/_beginthreadex/_endthread/_endthreadex,而不是直接用CreateThread/ExitThread
  • 全局对象的构造和析构都是由运行库完成的
  • 如果频繁进行系统调用,将会严重影响系统性能
  • Windows的回车为“\r\n”(0D0A);Linux为“\n”(0A);Max OS为“\r”(0D)。而在C语言中,回车始终用“\n”表示。在以文本模式读取文件时,不同的操作系统会将各自的回车符转换成C语言的表示形式

第12章 系统调用与API
  • Linux操作系统直接暴露给用户的是系统调用,而Windows并没有公开其系统调用接口,而是在上面再封装了一层的Windows API,其他程序均是通过API来和Windows内核的系统调用进行交互的。这种中间层的设计为内核升级调整的兼容性做好了保障。
  • Windows NT系列和Windows 9x系列是两个完全不同的操作系统,他们分别属于两个不同的Windows产品线

第13章 运行库实现
  • atexit函数可注册一个进程正常结束前的回调
  评论这张
 
阅读(3508)| 评论(1)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017