程序员的自我修养--链接、装载与库

来源:互联网 发布:docker nginx 镜像 编辑:程序博客网 时间:2024/06/06 02:38

第一次接触《程序员的自我修养》的时候,的确怀有一种疑惑的态度的。因为潜意识告诉我:在计算机这一行,更强调的是实践动手,而XXX修养的显然不属于动手操作类,至少不是太适合我的需求。但是,当我以一种随意的心态翻阅的时候,我才发现我的判断是多么的幼稚!
    这是一本深入浅出、通俗易懂的权威教材,特别是当我了解到写书的时候,作者还是在校的研究生?!我就深深的惊呆了。。。好吧,还是那句台词:人和人的差别怎么这么大呢。这时候才明白:难怪书中行文的口气太接近生活用语了,读书的过程感觉非常亲切,就像与学长聊天。难得的是他们以这种方式讲解计算机中底层晦涩的原理,把枯燥的知识讲得精确且有趣生动,真正的大师风范啊。废话不多说,下面进入正题。
    全书共分为四个部分,十三的章节:
    一.简介
    二.静态链接
    三.装载与动态链接
    四.库与运行库   

 在简介部分

 作者解释了一些计算机领域的概念,如:简单的Hello world程序到底是怎么运行的;操作系统在幕后为我们做了哪些工作;内存不够了,会有哪些解决方式,诸如此类。作者在这里只是为了提出问题,引发读者的思考。毕竟,兴趣才是最好的老师。也难怪,我在读了这里内容后,就决定:那天下午其他什么作业都放下,只读书。一个下午,读了一百多页多内容(个人认为这速度对于计算机放方面多书籍来说已经够快了,全书共计四百多页)。他们似乎总能够知道难点、疑问点在哪,然后一步一步深入,在读书的过程中,你会发现这其实是一个与作者互动的过程。有哪么一种互动的感觉。在以前,我几乎不怎么读书,更喜欢多学习方式是查网络资源。但是读了这本书以后,我的好多观点被撼动了,是不是已经错过了好多好书了呢?不得而知,也不敢想了。。。所以在这里奉劝大家,不要轻易的给某种事物贴标签,如读书。即使看到99.9%的无趣教材,也不要对那仅剩下的那一本放弃希望。

  第二部分是静态链接

 在这一部分,主要的篇幅放在ELF文件格式的解释上。linux下的目标文件、静态链接库文件、动态链接库文件采用的文件格式都是ELF文件格式标准。对比下,windows下的程序文件符合的标准是PE文件。ELF文件有固定的格式控制,有52个字节长度的文件头。它描述了整个文件的文件属性,包括文件是否可执行、是静态链接还是动态链接及入口地址(如果是可执行文件)、目标硬件、目标操作系统等信息。而且同一份文件有两种视图,分别对应于链接的过程、加载的过程。链接的过程,可以用节区的方式来看待文件内容。整个文件由不同的节区组成,在文件头有一个字段标识节区表的位置,通过节区表可以方便的找到不同的节区。那么节区里面放的是什么呢?比如:
    .text(代码段)、
    .bss(未初始化的全局变量或者静态局部变量)、
    .data(初始化的全局变量或者局部静态变量)
    .rodata(只读数据段,放一些字符串常量等信息)、
    .comment(注释信息段)、
    .note.GNU-stack(堆栈提示段)
    ... ...

  
    这些程序运行需要的信息被分们别类放在不同的节区当中,但是加载进内存后,则是另外一番景象。加载后,ELF的视图是以一种称为段表的方式管理的。不同的部分以段为单位进行管理,且有一个称为段表的数据结构辅助段的管理。因为节区表的信息是以一种模型的方式固定产生的,有的程序可能并没有.bss。所以这时候就需要去掉一些无用的信息,同时进行一些节区的合并,以产生一个段。以便OS以段的方式进行权限管理,如:只读的内容放在只读段、可读写的信息放在另外一个段(注意,OS是以页为最小粒度进行权限管理的)。合并节区的好处不言而喻:可以减少内存碎片、提高内存利用率且方便OS进行权限管理。
    第二部分的其他内容所占篇幅不是很大,还讲解了编译器和链接器。编译源代码为可执行文件的过程分为以下几个步骤:
  

  1.预处理
    2.编译源代码
    3.汇编
    4.链接

    1.预处理是解决一些宏定义的替换等工作,为编译做准备,对应的gcc操作为:gcc -E xx.c -o xx.i(xx为源文件名)。
    2.编译是将源码编译为汇编语言的过程,对应的gcc操作为:gcc -S xx.i -o xx.s。由xx.i 产生xx.s文件。
    3.汇编是将汇编代码的文件汇编为机器语言的过程,对应的gcc操作为:gcc -c xx.s -o xx.o
    4.链接是将目标文件链接为一个整的可执行文件的过程,对应的gcc操作为 gcc xx.o -o xx(xx成为可执行,运行时候可以用 "./xx" 的方式运行)。
    当然,平时常用的方式是直截了当的 " gcc xx.c ",做完以上所有过程生成可执行文件 " a.out "。在这里,我将重点阐述链接的过程。链接的过程是不可避免的,比如我们很有可能用到系统函数printf()、scanf()...此时,也许有人会反驳:“我的程序没用到这些个函数,所以链接的过程在这样的程序中不存在!!”。真的是这回事吗?!显然不是嘛...要是那样的话,我岂不是自己打自己的脸吗?对不,呵呵。。。其实,在程序运行的时候,main()函数一定不是第一个被执行的函数,那第一个被执行的函数是什么捏?start()函数(不信的话你 " objdump -d a.out " 一下看看呗)。一个被忽视但又一直存在的函数,它才是第一个被调用执行的函数。main()只不过是它调用的子函数而已。好吧,这家伙是必须存在的,但显然不在你写的程序中吧,由此看来,链接过程是不是肯定存在?!
    好吧,扯的有点多了啊。那么链接过程肯定是有的,究竟链接干了什么呢?这里我再举一个例子。两个文件 " A.C 、 B.C ",在A中有一个外部变量x,在B中用到A中的这个变量怎么办呢?c语言提供支持,方法如下:“extern x ”,标识x在外部申明。这样编译的时候就不会报错了。那么编译但不链接时候,B中用到A的X,地址如何确定呢?此时,编译器充分发挥自己的主观能动性:随便给它一个地址就行了。于是B中用到A中的x地址就被规定为一个数值了,多少呢?不是其它值,就是0(数据地址被定为0,外部函数地址被定为一个特定的之的值,如:call 0x08048900 ,跳到地址的最低位08)。这些假地址在运行的时候肯定是不行的,所以必须有一个链接的过程来修正这些地址为正确的地址。

 第三部分是装载与动态链接

 这部分的重点当然是虚拟空间的分配与管理啦。虚拟地址空间有4G大小(32位地址空间)!但是能用的空间才多大呢?linux有3G左右,而windows才2G左右(windows你为什么总这么的奇怪...省略一百字,呵呵)。4G的虚拟地址空间,理论上虽然都可以分配给进程,但是操作系统为了方便管理,有些空间被强制用作其它用途。逻辑地址空间(4G),典型的被分为4部分(windows分配方式):核心区(0xc0000000-0xffffffff)、隔离区(0xbfff0000-0xbfffffff)、用户区(0x00010000-0xbffeffff)、null区(0x00000000-0x0000ffff)。核心区一般1G左右,所有进程必须映射物理的OS所在的区域,即物理的1G空间被所有进程共享哦。紧接着核心区的是隔离区,这部分区域是OS用来保护自己的黑色地带,但凡有进程试图通过缓冲区溢出等方式攻击时,都将遭到OS的阻止(理论上),这片区域就是OS的外围城墙。用户区才是各进程区分自己的地方,其他区域都是一样的。用户区存放的是:代码段、数据段、堆栈段...null区用来干什么的呢?是用来保证接口的一致性的的吧(仅仅是个人观点。当然,估计还有其他原因,但是目前还未找到)。怎么来保证一致性呢?在程序中,你也许会用到malloc()函数来为某个数据结构分配空间。大部分情况下,都可以正常分配,但是总有那么些个情况它就是分配不成功-_-!!c语言有必要专门为申请空间不成功保存状态信息吗?如哪种情况导致的不成功。愚认为没必要,一方面,分配失败毕竟在概率上只是少数。另外一方面,程序关心的只是成功与否,至于原因,或许并不在意(其实空间不足或许是绝大部分原因,所以也就没必要判断分配失败原因了)。分配不成功时候,os将指针指向null区。用户只需要判断是否分配在null区,就可以得到分配是否成功。如果失败,至于原因如何,c语言未提供支持(分析见上)。


 第四部分是库与运行库。


    这部分则关于动态库知识的介绍,如API、公共运行库等,这部分知识是关于规则的介绍及使用,较为琐碎。其他一些内容如:栈的构造、堆的分配算法、运行时多线程的困扰、CRT的改进、中断与系统调用...其中颇为感兴趣的是微软的Hot Patch Prologue技术,详细内容见《程序员的自我修养》的P292页。本人另外一篇博客园博客就是介绍这项技术的ppt截图,感兴趣的话可以翻翻看,应该很快就可以找到吧,总共没几篇博文。微软在编译现代的程序的时候,会人为的加上7个字节的内容,就这7个字节的内容可以实现热修复(不需要宕机就可以实现dll的替换哦)。当然,实现这项技术的基础是APi Hook。APi Hook可以改变函数的执行流程,微软是这项技术的狂热爱好者,并在OS层面提供支持。这是为什么呢?我们知道微软的OS有很多版本,应用程序开发时,只能对某一款OS提供最完美的支持。如:有很多游戏都是针对经典的XP开发的,但是在Win7上出现兼容性问题(很多人都会遇到吧,呵呵)。这时候就体现热修复的威力了,它可以同时保存两个版本的Dll。出现兼容性问题后,改变DLL的版本,当然执行完还需要改回来。这个过程就需要那7个字节来实现跳转到目标DLL,7个字节的内容包含两个跳转:2(短跳)+5(长跳)。5个字节的长跳就可以跳到32位地址空间的任意位置,那为什么还需要2个字节的短跳呢?原因很简单,概率问题。因为实现的时候并不仅仅需要理论,更需要实践的支持。实践概率表明:大部分的情况不需要跳转(即不需要版本兼容)。这种情况下,5个字节的长跳就会被忽略,就是浪费5个字节的空间。但是通过2+5的方式(短跳的目的地址是长跳的位置,它俩紧挨着),浪费的空间只有两个字节。所以出现这种2+5的组合跳转方式。

0 0
原创粉丝点击