天天看点

MIPS架构下LW指令的重定位过程

通常我们不会去关心指令重定位(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文件反汇编文件的不同点了吗?我把区别标记下来并开始分析:

MIPS架构下LW指令的重定位过程

其中蓝色部分是做了重定向的结果。 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)

MIPS架构下LW指令的重定位过程

接下来我开始分析上面的每一条指令来了解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查看到一些(但不是全部),如下图:

MIPS架构下LW指令的重定位过程

在此我就上面的几个类型结合代码讲解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

继续阅读