MIPS架构下LW指令的重定位过程
_
本⽂有点烧脑,看完注意休息 “”
⼀、准备⼯作和基础知识
可以跳过
⾸先看下⾯的⽰例汇编语句:
//test.S
ENTRY(__export_parasite_head_start)
.set noreorder
lw a0, __value
jr ra
__value:
.long0
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 .,"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:
.long2
.size __export_parasite_head_start,.- __export_parasite_head_start
可以看出test.s和test.S⼏乎没有差别,都是汇编指令。
test.o已经是ELF格式的⽂件了,所以不能直接打开,需要使⽤objdump命令
$ objdump -D test.o > dump
然后打开⽂件dump
test.o:⽂件格式 elf64-tradlittlemips
...
Disassembly of section .:
0000000000000000<__export_parasite_head_start>:
0:3c040000  lui a0,0x0
4:3c010000  lui at,0x0
8:64840000  daddiu a0,a0,0
offset指令是什么意思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 :{
*(.)
*(.text*)
*(.data)
*(.rodata)
}
/DISCARD/:{/*释义:需要丢弃的输⼊段*/
*(ment)
*(.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⽂件反汇编⽂件的不同点了吗?我把区别标记下来并开始分析:
接下来我开始分析上⾯的每⼀条指令来了解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
重定位节'.'位于偏移量0x4b0含有4个条⽬:
Offset          Info          Type          Sym. Value    Sym. Name + Addend
00000000000000040000001d R_MIPS_HIGHEST    +1c
Type2: R_MIPS_NONE
Type3: R_MIPS_NONE
000000000004000400000005 R_MIPS_HI16      +1c
Type2: R_MIPS_NONE
Type3: R_MIPS_NONE
00000000000800040000001c R_MIPS_HIGHER    +1c
Type2: R_MIPS_NONE
Type3: R_MIPS_NONE
000000000014000400000006 R_MIPS_LO16      +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的源码中得出计算过程为:
((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的源码中⽂件
((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)

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