DAY 0x17
关于链接器obj2bim
翻看源码发现,obj2bim其实就是个链接器,依据一个.rul规则文件来生成链接后的二进制文件。
输入文件
它接受的输入文件有obj和lib这两种文件,其中lib文件也有两种,一种是标准的pe文件,有一种是作者自定义的格式,在链接前会decode成标准的lib文件,然后从lib文件解压出obj文件来进行处理,所以这个链接器主要进行的是对obj文件的处理。 例如haribote / golibc.lib就是作者自定义的一种格式的文件,它原本是一个标准的win下的lib文件,编写工具将其转换出来后发现里头并没有什么有意思的东西,只是几个字符串相关的处理函数的实现。
处理obj
处理过程在loadobj
函数中,主要是用一个全局的数组存储所有的obj文件对象。
- 数据拷贝 对于每个obj,将各个段依次解析,拷贝其中的数据到一片开辟的空间中。
- 重定位表处理
对于每个段,都开辟了一块空间存储重定位表的表项数组,然后解析每个段指向的
relocation table
重定位表,obj文件中重定位表的结构如下:
typedef struct _IMAGE_RELOCATION {
union {
DWORD VirtualAddress;
DWORD RelocCount;
} M;
DWORD SymbolTableIndex;
WORD Type;
} IMAGE_RELOCATION;
其中VirtualAddress
指向的是需要被重定位的位置相对于当前段的偏移量。
每个重定位表项有一个指向符号表的索引号,这部分操作总的来说就是把重定位表里面的项解析成程序中的结构,最后把重定位表的第一个元素的指针以及重定位表项的数目记录到obj中的对应的段中
- 符号表 处理完段以后,处理符号表,排除一些符号比如调试用的符号,然后加载到之前的符号数组里,同时把bss段进行对齐。 符号表中包含两种组成,一种是定义在外部的符号,一种是定义在本obj文件的符号。 obj2bim这个链接器进程维护了一个包含所有符号的数组(相当于一个巨大的符号表),对于符号表中的每一项,先到该符号数组中去查找,如果找不到就添加一个新的符号。 每个符号项在程序中还带有一个指针,指向定义它的obj结构体实例,从而最终能区分一个符号是否被定义。 obj文件中符号表项的结构如下:
typedef struct _IMAGE_SYMBOL {
union {
BYTE ShortName[8];
struct {
DWORD Short; // if 0, use LongName
DWORD Long; // offset into string table
} Name;
DWORD LongName[2]; //two byte potioner
} N;
DWORD Value;
SHORT SectionNumber;
WORD Type;
BYTE StorageClass;
BYTE NumberOfAuxSymbols;
} IMAGE_SYMBOL;
开始链接
处理指定的label 加载完所有的obj以后,读取rul文件中的
label
字段,然后读取rul文件中指定的符号,如果找不到就会报错。 之后遍历所有的符号,对用到的符号进行标记。预链接 预处理完以后,三次调用
link0()
函数依次收集所有obj的代码段、所有obj的数据段、所有obj的bss段,组成三个大段。 收集过程中,将数据填充到filebuf
中,根据rul文件中规定的每个大段的起始逻辑地址算出obj中每个段的的逻辑地址,以便后面符号的确定时使用,最后计算出这三个大段的逻辑地址的结束位置符号值的确定 重定位主要是因为编译时单个obj不能确定代码所在的地址,要到链接时把所有obj放到一起才能确定符号的位置,而符号代表的就是地址,所以,我们要把符号的值进行修改。 具体操作是:每个符号原先的偏移量(符号表示的值)加上上一步计算出的它所在的obj文件的对应段的逻辑地址,变成该符号(表示的值)的逻辑地址
输出map文件(可选) 然后开始输出.map文件,输出三种段的大小以及处理过的全局的符号表(符号的逻辑地址和符号的字符串表示,吐槽一下作者由于不会写按地址进行排序函数,用了一种很慢的办法来排序)
link 这样搞了一通以后下面就是真的link过程了,之前已经把符号的值重新计算过了,现在就是要把符号的值应用到要重定位的地方。 其实代码中每个需要重定位的语句都会在重定位表里面生成一项。 这个link过程其实就是对重定位表项的处理,由于上面的上面一步中已经处理出了符号(表示的值)的逻辑地址了,那这里只需要在每个重定位表项里面把
VirtualAddress
处的值改成对应的符号(表示的值)就行了(注意这里不一定是直接写入符号的值(逻辑地址),因为有的指令比如call指令可能接受的不是绝对地址,而是相对于pc值的地址)。
输出
最后输出作者规定的一种格式的文件,将代码段和数据段的信息(大小,文件中的位置,逻辑地址)写到文件起始位置。
接下来是是写入了rul文件中指定的那个label符号表示的值(在文件中偏移量为24的位置)。例如rul中label
项写的是_HariStartup
,那么就是把_HariStartup
符号的入口地址写到这了。
然后是在指定的位置写入了代码段和数据段的内容,至此链接过程完成。
一些思考
- 重定位的时候究竟改了什么?
代码中有一个让我很疑惑的地方,就是在"link"步骤的时候,往
VirtualAddress
写东西(4字节)时,作者刻意用了小端模式的写法,而之前各个地方赋值的时候,都没有刻意用小端,为了探查究竟,用工具来解析一下这所谓的重定位表, 这是bootpack.obj
中HariMain
函数的一部分汇编。
0000047a <_HariMain>:
...
480: 81 ec dc 00 00 00 sub $0xdc,%esp
486: e8 00 00 00 00 call 48b <_HariMain+0x11> ; 这里
48b: e8 00 00 00 00 call 490 <_HariMain+0x16>
490: e8 00 00 00 00 call 495 <_HariMain+0x1b>
495: e8 00 00 00 00 call 49a <_HariMain+0x20>
49a: c7 44 24 04 f8 00 00 movl $0xf8,0x4(%esp)
...
注意0x486
那里第一个call
汇编应该是
call __Z7init_dtv ; init_dt(void)`
init_dt(void)
应该是dsctbl.obj
中的导出函数,
再看解析出来的重定位表的一部分
Symbol Symbol
Offset Type Applied To Index Name
-------- ---------------- ----------------- -------- ------
...
00000487 REL32 00000000 35 __Z7init_dtv
...
表中__Z7init_dtv
这个符号的Offset
是00000487
,这是什么意思呢?
注意看上面汇编中,0x486
的那条call
指令,长度为5个字节,后面从0x487
开始的四个字节都是十六进制的00
,我们可以大胆的猜测,这应该是留空给链接器填入链接后的地址的。
事实上查阅i386汇编手册就可以知道,这是
call
指令的一种五字节长的版本,在0xE8
后面接四字节的相对地址构成一条call
指令,这对链接器来说就很方便了,不知道对于其它架构的机器码链接器是怎么做链接的。
也就是说,最后我们把__Z7init_dtv
的地址填到这里就能call
了吧?其实不是这样的啦,call
需要的是相对地址,所以链接器得先根据重定位表中该项的type
进行个判断,把符号的值减去(当前指令的地址+4)的值写入到这个call
语句中,所以说,链接时的重定位是要修改.text
段的。
同理,
mov dword ptr ds:__ZN3sys8memtotalE, eax ; sys::memtotal
对应的是
4e4: a3 00 00 00 00 mov %eax,0x0
这里指令中也有四字节的空间是留出来让链接器时重定位__ZN3sys8memtotalE
这个外部引用用的
- 关于.bss段
全局变量存到.bss段里头,而这个段比较特殊,只在段的header里面存储它的大小,在运行加载时才为其初始化空间,但是显然裸机是不会帮我们分配
.bss
段的,而且这个操作系统我们还没有写加载器,那.bss
段只能由这个静态链接器来展开了,验证一番发现在link0
里面对.bss
段腾出了空间的逻辑。
关于bim2hrb
bim2hrb对比obj2bim就简单的多了,只是把bim的前面若干个字节进行了一些变动,加上了堆的空间的指定,然后调整了一下开头处存储的地址和大小什么的数据,还在前面加上了作者自己的魔术字Hari
。
参考
- Intel call指令 https://www.cnblogs.com/scu-cjx/p/6879041.html
- PE格式第七讲,重定位表 https://www.cnblogs.com/iBinary/p/7690069.html
- [原创]010edit模版(obj文件分析) https://bbs.pediy.com/thread-222934.htm
异常中断
x86中,从0x00到0x1f都是异常所使用的中断,IRQ的中断号都是从0x20之后开始的。 0x00 除零异常(当试图除以0时产生) 0x0c 栈异常 0x0d 非法内存访问 0x06 非法指令异常(当试图执行CPU无法理解的机器语言指令, 例如当试图执行一段数据时,有可能会产生)