自制玩具操作系统--week8

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文件中重定位表的结构如下:
    1
    2
    3
    4
    5
    6
    7
    8
    typedef struct _IMAGE_RELOCATION {
    union {
    DWORD VirtualAddress;
    DWORD RelocCount;
    } M;
    DWORD SymbolTableIndex;
    WORD Type;
    } IMAGE_RELOCATION;

其中VirtualAddress指向的是需要被重定位的位置相对于当前段的偏移量。
每个重定位表项有一个指向符号表的索引号,这部分操作总的来说就是把重定位表里面的项解析成程序中的结构,最后把重定位表的第一个元素的指针以及重定位表项的数目记录到obj中的对应的段中

  • 符号表
    处理完段以后,处理符号表,排除一些符号比如调试用的符号,然后加载到之前的符号数组里,同时把bss段进行对齐。
    符号表中包含两种组成,一种是定义在外部的符号,一种是定义在本obj文件的符号。
    obj2bim这个链接器进程维护了一个包含所有符号的数组(相当于一个巨大的符号表),对于符号表中的每一项,先到该符号数组中去查找,如果找不到就添加一个新的符号。
    每个符号项在程序中还带有一个指针,指向定义它的obj结构体实例,从而最终能区分一个符号是否被定义。
    obj文件中符号表项的结构如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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.objHariMain函数的一部分汇编。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    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汇编应该是

1
call    __Z7init_dtv    ; init_dt(void)`

init_dt(void)应该是dsctbl.obj中的导出函数,

再看解析出来的重定位表的一部分

1
2
3
4
5
6
                                               Symbol    Symbol
Offset Type Applied To Index Name
-------- ---------------- ----------------- -------- ------
...
00000487 REL32 00000000 35 __Z7init_dtv
...

表中__Z7init_dtv这个符号的Offset00000487,这是什么意思呢?
注意看上面汇编中,0x486的那条call指令,长度为5个字节,后面从0x487开始的四个字节都是十六进制的00,我们可以大胆的猜测,这应该是留空给链接器填入链接后的地址的。

事实上查阅i386汇编手册就可以知道,这是call指令的一种五字节长的版本,在0xE8后面接四字节的相对地址构成一条call指令,这对链接器来说就很方便了,不知道对于其它架构的机器码链接器是怎么做链接的。

也就是说,最后我们把__Z7init_dtv的地址填到这里就能call了吧?其实不是这样的啦,call需要的是相对地址,所以链接器得先根据重定位表中该项的type进行个判断,把符号的值减去(当前指令的地址+4)的值写入到这个call语句中,所以说,链接时的重定位是要修改.text段的。

同理,

1
mov     dword ptr ds:__ZN3sys8memtotalE, eax ; sys::memtotal

对应的是

1
4e4:   a3 00 00 00 00          mov    %eax,0x0

这里指令中也有四字节的空间是留出来让链接器时重定位__ZN3sys8memtotalE这个外部引用用的

  • 关于.bss段
    全局变量存到.bss段里头,而这个段比较特殊,只在段的header里面存储它的大小,在运行加载时才为其初始化空间,但是显然裸机是不会帮我们分配.bss段的,而且这个操作系统我们还没有写加载器,那.bss段只能由这个静态链接器来展开了,验证一番发现在link0里面对.bss段腾出了空间的逻辑。

关于bim2hrb

bim2hrb对比obj2bim就简单的多了,只是把bim的前面若干个字节进行了一些变动,加上了堆的空间的指定,然后调整了一下开头处存储的地址和大小什么的数据,还在前面加上了作者自己的魔术字Hari

参考

异常中断

x86中,从0x00到0x1f都是异常所使用的中断,IRQ的中断号都是从0x20之后开始的。
0x00 除零异常(当试图除以0时产生)
0x0c 栈异常
0x0d 非法内存访问
0x06 非法指令异常(当试图执行CPU无法理解的机器语言指令, 例如当试图执行一段数据时,有可能会产生)