一个连接器的实现:「二」
Elf的文件格式
我实现的简单C链接器,MiniLinker下载:
git clone git@github.com:youzhonghui/MiniLinker.git
在windows或者在linux上的可执行文件,除了记录了程序运行需要的指令和数据以外,还需要提供一些载入的信息。比如文件中那个部分是指令,那个部分是数据等等。windows下的可执行文件是PE格式,linux下的则是Elf格式。
Elf格式在文件上是这个样子的:
对于Elf文件格式的解析,在《程序员的自我修养》里写的很清楚了,我就不再重复。记录一些对写链接器必要而书中没有涉及的东西。当然,没事闲着研究链接器的人也是少数,所以资料不好找,大多数是靠自己的实验得出的。
猜想与证明
链接器的最终目标是生成可执行文件,让我们想一想这个可执行文件的组成。
- Elf文件头
- 节表
- 段表
- 已经重新定位好的指令和数据
我们再猜想,转载Elf可执行文件的过程是什么。
- 读取文件头,进而得到表头,程序头
- 根据程序头的信息,将对应的文件片段载入到对应的内存地址
- 根据文件头中记录的入口地址,将ip指针指向它,开始运行程序
细节[1]
从以上的猜想可以看出,转载过程起作用的是程序头。那么节表是不是可有可无呢?
文件头里有一个e_shnum元素,记录文件中的节表数量。用二进制修改软件(例如ghex),把这个值修改为0,再运行程序,一切正常!
说明,在最后的可执行文件中,节表是可有可无的。那么,我们就不需要为生成节表设计算法了
还有两个重要的需要注意的细节,都是在我实现了一个链接器原型,发现链接出来的东西运行的时候,要不是
已杀死
就是
段错误
细节[2]
对于已杀死
的情况,用edb都无法加载。说明在载入初期就出现错误,甚至是没有载入,那么应该就是文件头或者段表出了问题。
将ld链接出来的正常程序与我的细细对比,发现要说什么差异,那么最大的可能就是我程序头的第一个offset并不是从0x0开始的。
如果真是这样,理论上,我用ghex修改了程序表的第一个offset之后,应该能够装载(当然不可能正确运行)
试了以后,edb果然装载了。
所以,这是第二个很重要的细节:程序头的第一条记录的offset应该是从0开始的
细节[3]
解决了一进来就已杀死
的问题,还有段错误
用edb运行,会在最后的几条语句,因为违法的地址访问(一个解决于0的地址),而弹出段错误。
当时这个错误郁闷了很久,因为这个现象可能是我的链接器算法写错误,但是我又找不到bug。
使用《程序员的自我修养》中 最小程序 的那个例子来调试,猛然醒悟。
这个例子中不但用int 0x80
写了一个print,还写了一个exit
也就是程序在执行main的ret是不能正常结束的。必须要有一个系统调用来终止程序。
把这个exit函数拷贝出来,链接上,正常了。这也就是为什么要_start在main之前运行的原因之一,之前竟然没有注意到。
第三个细节,程序需要系统调用来终止
大胆猜想,小心求证