程序员的⾃我修养--链接、装载与库
 中国科学技术⼤学软件学院周艾亭原创作品版权所有转载请注明出处
第⼀次接触《程序员的⾃我修养》的时候,的确怀有⼀种疑惑的态度的。因为潜意识告诉我:在计算机这⼀⾏,更强调的是实践动⼿,⽽XXX修养的显然不属于动⼿操作类,⾄少不是太适合我的需求。但是,当我以⼀种随意的⼼态翻阅的时候,我才发现我的判断是多么的幼稚!
这是⼀本深⼊浅出、通俗易懂的权威教材,特别是当我了解到写书的时候,作者还是在校的研究⽣?!我就深深的惊呆了。。。好吧,还是那句台词:⼈和⼈的差别怎么这么⼤呢。这时候才明⽩:难怪书中⾏⽂的⼝⽓太接近⽣活⽤语了,读书的过程感觉⾮常亲切,就像与学长聊天。难得的是他们以这种⽅式讲解计算机中底层晦涩的原理,把枯燥的知识讲得精确且有趣⽣动,真正的⼤师风范啊。废话不多说,下⾯进⼊正题。
全书共分为四个部分,⼗三的章节:
⼀.简介
⼆.静态链接
三.装载与动态链接
四.库与运⾏库
 在简介部分
 作者解释了⼀些计算机领域的概念,如:简单的Hello world程序到底是怎么运⾏的;操作系统在幕后为我们做了哪些⼯作;内存不够了,会有哪些解决⽅式,诸如此类。作者在这⾥只是为了提出问题,引发读者的思考。毕竟,兴趣才是最好的⽼师。也难怪,我在读了这⾥内容后,就决定:那天下午其他什么作业都放下,只读书。⼀个下午,读了⼀百多页多内容(个⼈认为这速度对于计算机放⽅⾯多书籍来说已经够快了,全书共计四百多页)。他们似乎总能够知道难点、疑问点在哪,然后⼀步⼀步深⼊,在读书的过程中,你会发现这其实是⼀个与作者互动的过程。有哪么⼀种互动的感觉。在以前,我⼏乎不怎么读书,更喜欢多学习⽅式是查⽹络资源。但是读了这本书以后,我的好多观点被撼动了,是不是已经错过了好多好书了呢?不得⽽知,也不敢想了。。。所以在这⾥奉劝⼤家,不要轻易的给某种事物贴标签,如读书。即使看到99.9%的⽆趣教材,也不要对那仅剩下的那⼀本放弃希望。
 第⼆部分是静态链接
 在这⼀部分,主要的篇幅放在ELF⽂件格式的解释上。linux下的⽬标⽂件、静态链接库⽂件、动态链接库⽂件采⽤的⽂件格式都是ELF⽂件格式标准。对⽐下,windows下的程序⽂件符合的标准是PE⽂件。ELF⽂件有固定的格式控制,有52个字节长度的⽂件头。它描述了整个⽂件的⽂件属性,包括⽂件
是否可执⾏、是静态链接还是动态链接及⼊⼝地址(如果是可执⾏⽂件)、⽬标硬件、⽬标操作系统等信息。⽽且同⼀份⽂件有两种视图,分别对应于链接的过程、加载的过程。链接的过程,可以⽤节区的⽅式来看待⽂件内容。整个⽂件由不同的节区组成,在⽂件头有⼀个字段标识节区表的位置,通过节区表可以⽅便的到不同的节区。那么节区⾥⾯放的是什么呢?⽐如:
.text(代码段)、
.bss(未初始化的全局变量或者静态局部变量)、
.data(初始化的全局变量或者局部静态变量)
.rodata(只读数据段,放⼀些字符串常量等信息)、
ment(注释信息段)、
.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的组合跳转⽅式。
 虽然第⼀次写这么长的读后感(⾜有4135字,远超出2000的字数),也感觉很累啊,但是还是希望能有来⾃前辈的指点,周某先⾏谢过。。。 

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。