通常我们不会去关心指令重定位(relocation)的细节,编译器的ld过程已经帮助我们做好了。由于最近在移植CRIU,涉及到指令的重定位计算,不得不细细研究代码重定位的细节知识。之前的文章介绍了MIPS架构下函数跳转指令bal和jal和重定位过程,感兴趣的同学可以跳转到https://blog.csdn.net/weixin_38669561/article/details/100536803查看,本章通过分析lw 指令来分析从内存加载数据到寄存器的重定位的过程。
本文有点烧脑,看完注意休息 “_”
一、准备工作和基础知识
可以跳过
首先看下面的示例汇编语句:
//test.S
ENTRY(__export_parasite_head_start)
.set noreorder
lw a0, __value
jr ra
__value:
.long 0
END(__export_parasite_head_start)
这里lw a0,__value 就是我们要分析的汇编指令。我们用gcc编译
$ gcc -save-temps -g -c -fno-builtin -mno-abicalls test.S
其中 -save-temps 参数意味着保留中间临时文件,所以这条命令执行完成会发现当前目录多了两个文件,test.s和test.o。其中test.s就是GCC编译生成的中间编译文件,而test.o是汇编文件。编译文件和汇编文件有什么区别呢?首先要了解GCC编译是有4个过程,预编译(生成.i文件)-> 编译(生成.s文件)->汇编(生成.o文件)->链接(生成可执行文件,默认为a.out)。这里的test.s就是编译阶段产生的文件,test.o就是汇编阶段产生的文件,编译和汇编的区别可以通过查看文件内容得知:
//test.s
.section .head.text, "ax"
.globl __export_parasite_head_start; .align 4; .type __export_parasite_head_start, @function; __export_parasite_head_start:
.set noreorder
lw $4, __export_parasite_cmd
jr $31
__export_parasite_cmd:
.long 2
.size __export_parasite_head_start, . - __export_parasite_head_start
可以看出test.s和test.S几乎没有差别,都是汇编指令。
test.o已经是ELF格式的文件了,所以不能直接打开,需要使用objdump命令
$ objdump -D test.o > test.o.dump
然后打开文件test.o.dump
test.o: 文件格式 elf64-tradlittlemips
...
Disassembly of section .head.text:
0000000000000000 <__export_parasite_head_start>:
0: 3c040000 lui a0,0x0
4: 3c010000 lui at,0x0
8: 64840000 daddiu a0,a0,0
c: 0004203c dsll32 a0,a0,0x0
10: 0081202d daddu a0,a0,at
14: 8c840000 lw a0,0(a0)
18: 03e00008 jr ra
000000000000001c <__export_parasite_cmd>:
1c: 00000002 srl zero,zero,0x0
...
汇编过程就是将汇编代码(test.s)转变成机器可以执行的指令(test.o),有些汇编语句和机器指令一一对应,不需要扩展,比如上面的 "jr ra"
指令。有些汇编指令可能扩展成多条机器指令,比如test.S里里面的 "lw a0, __value"
扩展成了6条指令
0: 3c040000 lui a0,0x0
4: 3c010000 lui at,0x0
8: 64840000 daddiu a0,a0,0
c: 0004203c dsll32 a0,a0,0x0
10: 0081202d daddu a0,a0,at
14: 8c840000 lw a0,0(a0)
同时我们有知道,此时的test.o还是没有做重定位的指令集,从起始地址"0000000000000000"
就可以看出,或者使用file命令
$ file test.o
test.o: ELF 64-bit LSB relocatable, MIPS, MIPS64 rel2 version 1 (SYSV), not stripped
这里 "relocatable"
就意味着这是需要重定位的文件,或者说需要做指令修正的文件。
那么重定位什么时候做呢?链接阶段。上面我在使用gcc工具时有一个参数 "-c"
。这个值意思是制作编译、汇编,不进行链接。链接过程主要包括了地址和空间分配、符号决议和重定位。
下面我们使用ld工具来进行test.o的链接过程。为了便于分析LW指令的重定位的过程,ld使用自定义的链接脚本,内容如下:
// ld.lds
OUTPUT_ARCH(mips)
ENTRY(__export_parasite_head_start) /*指定了程序入口函数*/
SECTIONS
{
. = 0x120000000; /*0xfff70c4000; 指定当前虚拟地址*/
tinytext : {
*(.head.text)
*(.text*)
*(.data)
*(.rodata)
}
/DISCARD/ : { /*释义:需要丢弃的输入段*/
*(.comment)
*(.pdr)
}
/* Parasite args should have 4 bytes align, as we have futex inside. */
. = ALIGN(4);
__export_parasite_args = .;
}
在这个文件中,你只要关注两点, 一、"ENTRY(__export_parasite_head_start)"
指定了程序运行的入口地址,没有它,接下来的ld命令会失败 。二、". = 0x120000000"
指定了当前虚拟起始地址。接下来执行l命令。
$ ld -static -T ld.lds -o test test.o
这时查看生成的test文件已经是可执行的,relocation已经完成。
$ file test
test: ELF 64-bit LSB executable, MIPS, MIPS64 rel2 version 1 (SYSV), statically linked, not stripped
二、relocation分析
敲黑板上面的内容都是准备工作,接下来开始分析重定位(relocation)的过程。
首先反汇编test文件
$ objdump -D test > test.dump
打开test.dump文件
test: 文件格式 elf64-tradlittlemips
...
Disassembly of section tinytext:
000000fff70c4030 <__export_parasite_head_start>:
fff70c4030: 3c040000 lui a0,0x0
fff70c4034: 3c01f70c lui at,0xf70c
fff70c4038: 64840100 daddiu a0,a0,256
fff70c403c: 0004203c dsll32 a0,a0,0x0
fff70c4040: 0081202d daddu a0,a0,at
fff70c4044: 8c84404c lw a0,16460(a0)
fff70c4048: 03e00008 jr ra
000000fff70c404c <__export_parasite_cmd>:
fff70c404c: 00000002 srl zero,zero,0x0
发现和上面的test.o文件反汇编文件的不同点了吗?我把区别标记下来并开始分析:
其中蓝色部分是做了重定向的结果。 R_MIPS_HIGHEST、R_MIPS_HI16 、R_MIPS_HIGHER、R_MIPS_LO16是重定位入口类型。每种类型的指令修正方式可以通过查看mipsabi文档可以找到
目前我从https://elinux.org网站上下载下来的mipsabi.pdf文档里对重定向入口类型介绍的也不全,最好的分析办法是看 binutils 的源码
还要明确一下,我当前运行的是在龙芯处理器上。mips寄存器为64位,指令32位,寻址48位(我还不确定)。lw指令的描述是 lw rt,offset(base) ,这里base可以寻址64位。按上面的"lw a0,16460(a0)"
为例,base值应该是0xfff70c0000(等于ld.lds里面设置的初始虚拟地址) 。16460是10进制数,对应的16进制数为0x404c。那么寻址后的a0值应该为0xfff70c0000+0x404c = 0xfff70c404c。base值应该是(也就是上面的a0)
接下来我开始分析上面的每一条指令来了解lw a0,__export_parasite_cmd是怎么加载上来的。
第一条指令 lui a0,0x0
lui 功能为上位加载立即数,描述为a0 = 0x0<<16位,操作后的a0值为:
a0 :0x0000 0000 0000 0000
第二条指令 lui at,0xf70c
这里使用了at寄存器做中间变量,描述为at = 0xf70c<<16,操作后的at值为:
at:0xffff ffff f70c 0000
这里是最让人费解的地方,f70c左移16位后应该是0x0000 0000 f70c 0000。而这里却是0xffff ffff f70c 0000 这是我通过gdb调试确认过的结果,可能是MIPS实现48位内存寻址的策略
第三条指令 daddiu a0,a0,256
daddiu 功能为64位立即数加法,描述为a0 = a0+256 ,256为10进制对应0x0100,操作后的a0值为:
a0:0x0000 0000 0000 0100
第四条指令 dsll32 a0,a0,0x0
dsll32 功能为32位左移,描述为a0 = a0<<(32+0x0),操作后的a0值为:
a0:0x0000 0000 0100 0000
第五条指令 daddu a0,a0,at
daddu功能同daddiu,为64位加法,但是操作数不是立即数而是寄存器。描述为a0 = a0+at,结果为:
a0:0x0000 00ff f70c 0000
此时你可能明白点为啥at值的高32位填充全f的原因没?
分析的最后一条 lw a0,16460(a0)
lw 功能为32位加载,64位CPU上进行符号扩展。 描述为a0 = memory(0xff f70c 0000+0x404c),a0结果为:
a0 = 2 //如果不信,你可以使用gdb调试
也就是lw执行后,a0可以加载到内存地址为fff70c404c上的数据。
到这里,我们已经通过重定位的指令分析了lw的基址计算过程。如果让你去实现relocation过程,你该怎么做?可能我们不被逼到死路是不会考虑这个问题。此刻,我就在死路上。
敲黑板:指令修正方式
上面提到了重定位入口类型R_MIPS_HIGHEST、R_MIPS_HI16 、R_MIPS_HIGHER、R_MIPS_LO16,但是分析过程一点没有用到,那是由于ld已经帮我们根据这几个类型实现了指令修正。重定位入口类型只有在test.o 到可执行文件test 的汇编过程才会用到。test.o是ELF格式的文件,ELF格式中会存储哪些段需要重定位以及指令修正的类型等信息。我们可以通过readelf命令查看
$ readelf -r test.o
重定位节 '.rela.head.text' 位于偏移量 0x4b0 含有 4 个条目:
Offset Info Type Sym. Value Sym. Name + Addend
000000000000 00040000001d R_MIPS_HIGHEST 0000000000000000 .head.text + 1c
Type2: R_MIPS_NONE
Type3: R_MIPS_NONE
000000000004 000400000005 R_MIPS_HI16 0000000000000000 .head.text + 1c
Type2: R_MIPS_NONE
Type3: R_MIPS_NONE
000000000008 00040000001c R_MIPS_HIGHER 0000000000000000 .head.text + 1c
Type2: R_MIPS_NONE
Type3: R_MIPS_NONE
000000000014 000400000006 R_MIPS_LO16 0000000000000000 .head.text + 1c
Type2: R_MIPS_NONE
Type3: R_MIPS_NONE
offset是指当前指令在elf文件中的位置,Type即为重定位入口类型,Addend会参与到指令修正的运算。
MIPS上的重定位类型对应的计算方式可以通过mipsabs.pdf查看到一些(但不是全部),如下图:
在此我就上面的几个类型结合代码讲解test.o中relocation的计算过程。
重定位类型 R_MIPS_HIGHEST
通过elf格式解析过程能够看到test.o中第一条指令 lui a0,0x0 ,指令码为 3c040000,重定位类型为R_MIPS_HIGHEST。怎么计算呢?mipsabi上还真没有,我参考了binutils的源码中mips.cc得出计算过程为:
((vbase(0xfff70c4000)+ 0x800080008000llu)>>48) & 0xffff
上述的计算结果(0x0)放在lui指令的低16位。修正后的lui指令码还是 3c040000
重定位类型 R_MIPS_HI16
test.o中第二条指令 lui at,0x0,指令码为 3c010000,重定位类型 R_MIPS_HI16,计算方式根据不同的符号类型有不同的计算方式,此处的符号类型为Local,计算过程为:
((vbase(0xfff70c4000)+ 0x8000)>>16) & 0xffff
上述的计算结果(0xf70c)放在lui指令的低16位,修正后的lui指令码为 3c01f70c
重定位类型 R_MIPS_HIGHER
第三条指令 daddiu a0,a0,256 ,指令码为 64840000 ,重定位类型为R_MIPS_HIGHER,计算方式也只能参考binutils的源码中mips.cc文件
((vbase(0xfff70c4000)+ 0x80008000)>>32) & 0xffff
上述计算结果(0x0100)放在daddiu指令的低16位,修正后的daddiu指令码为64840100
重定位类型 R_MIPS_LO16 ( fixme)
第六条指令 lw a0,0(a0) ,指令码为 8c840000 ,重定位类型为R_MIPS_LO16,计算方式根据不同的符号类型有不同的计算方式,此处的符号类型为Local,计算过程为:
(vbase(0xfff70c4000)& 0xffff)+A (0x4c)
上述计算结果(0x404c)放在lw指令的低16位,修正后的lw指令码为8c84404c