不同的处理器指令对于地址的格式和方式都不一样。《程序员的自我修养》一书在4.2章节介绍了x86平台的符号解析和指令修正方式。本文针对龙芯处理器介绍mips的两种重定位类型,绝对寻址和相对寻址。
首先你要知道MIPS中跳转指令有两种bal和jal。
bal指令格式如下:
jal指令格式如下:
两者的区别如下表:
指令 | 指令格式 | 寻址范围 | 寻址方式 | 重定位类型 |
---|---|---|---|---|
bal | I格式(6/5/5/16) | 128KB(2的16次方 ×4) | 相对寻址 | R_MIPS_PC16 |
jal | J格式(6/26) | 256MB(2的26次方×4) | 绝对寻址 | R_MIPS_26 |
下面通过一段代码分析bal和jal的跳转方式区别:
二、环境准备
为了说明bal和jal的指令修正算法和寻址过程,我准备main.c、test.S和 ld.lds 3个文件。具体代码如下:
## main.c
extern __export_parasite_head_start();
int main(){
__export_parasite_head_start();
return 0;
}
int test(){
char* str = "hello world\n";
int len = 12;
/*输出字符串str到终端*/
asm(
".set noreorder \n\t"
"li $2,5001\n\t" //sys_write syscall id is 5001 -> v0
"li $4,0\n\t" //stdio file id is 0 -> a0
"move $5,%0\n\t" // fixme: *str value -> a1
"move $6,%1\n\t" //length(*str) is 13 -> a2
"syscall \n\t"
"break \n\t"
:
:"r"(str),"r"(len)
:"$2","$4","$5","$6");
return 0;
}
注意:main.c里面没有引用任何gcc库函数。
在看看test.S文件的内容:
#define __ALIGN .align 4
#define ENTRY(name) \
.globl name; \
__ALIGN; \
.type name, @function; \
name:
#define END(sym) \
.size sym, . - sym
.section .head.text, "ax"
ENTRY(__export_parasite_head_start)
.set noreorder
bal test
jr $31
END(__export_parasite_head_start)
这里的bal test会跳转到main.c里面的test( )。
在看看链接脚本文件ld.lds
OUTPUT_ARCH(mips)
ENTRY(main) /*指定了程序入口函数*/
SECTIONS
{
. = 0x120000000; /* 指定了起始虚地址位置*/
tinytext : {
*(.head.text)
*(.text*)
*(.data)
*(.rodata)
}
/DISCARD/ : { /*释义:需要丢弃的输入段*/
*(.comment)
*(.pdr)
}
. = ALIGN(4);
}
关于链接脚本的语法和含义,在之前的文章中稍有提到,可以参考××
接下来我们把main.c和test.S分别用gcc编译成main.o和test.o文件。然后使用ld命令链接main.o和test.o文件生成最终可执行文件test,链接过程使用我写的ld.lds链接脚本,而非默认链接脚本。
$ gcc -c -fno-builtin -mno-abicalls main.c -o main.o
$ gcc -c -fno-builtin -mno-abicalls test.S -o test.o
$ ld -static -T ld.lds test.o main.o -o test
在分析bal和jal的寻址原理之前。我们可以运行一下test,看看效果
$ ./test
hello world
Trace/breakpoint trap (核心已转储)
可以看出,这个程序是可以在mips平台上运行的。这段代码的执行流程是:
1、ld.lds文件里面通过ENTRY(main)指定了程序的执行入口是main.c里面的 int main( )函数。
2、main函数里面调用__export_parasite_head_start(),它是在test.S里实现,ENTRY(__export_parasite_head_start)和END(__export_parasite_head_start)。
3、test.S里面通过 bal test 跳转到main.c里的int test()函数。
4、test函数里面通过一段内嵌汇编指令 打印出“hello world”后停住。在这里 Trace/breakpoint trap (核心已转储) 即为上面的break指令执行结果。
接下来我们就开始通过bal和jal指令分析MIPS的相对寻址和绝对寻址。
三、寻址分析
首先使用objdump命令来反汇编test
$ objdump -D test > test.dump
使用"vim test.dump"文件可以查看到如下信息(有些信息在此略过,没有显示)
(1)jal指令的绝对寻址
什么叫绝对寻址呢?绝对寻址就是在指令格式的地址的字段中直接指出操作数在内存的地址。由于操作数的地址直接给出而不需要经过某种变换,所以称这种寻址方式为直接寻址方式。MIPS文档中描述为not PC-relative,即PC无关寻址。
绝对地址寻址是要依赖基地址+偏移。在这里的基地址就是0x120000000 (在ld.lds 里面指定)。
看上面的jal跳转指令:
12000006c: 0c000014 jal 120000050 <__export_parasite_head_start>
当前jal寻址计算为: 0x120000000 + 0x14<<2 正好是__export_parasite_head_start的入口地址0x120000050。
再看指令0c000014是怎么回事?上面我们知道jal的指令格式是高6位代表jal指令码(0x0c),低26位存贮偏移地址右移2位后的结果值(0x50右移2位后结果为0x14)。
(2)bal指令的相对寻址
什么叫相对寻址呢?与基址变址寻址方式相类似,相对寻址以程序计数器PC的当前值(R15中的值)为基地址,指令中的地址标号作为偏移量,将两者相加后得到操作数的有效地址。MIPS文档中描述为PC-relative,即PC相关寻址。
相对寻址要依赖PC的值(PC就是当前指令的位置)。基本计算规则是加载延迟槽+偏移。加载延迟槽就是紧跟在PC加载指令后的指令位置。对于mips 的32位指令 加载延迟槽=PC+4。如果是64位指令加载延迟槽=PC+8。
看上面的bal跳转指令:
120000050: 0411000e bal 12000008c <test>
120000054: 03e00008 jr ra
当前bal跳转指令的寻址计算为:0x120000054 + 0xe<<2 正好是test函数的入口地址。
这里当前PC为0x120000050,所以加载延迟槽为0x120000054。bal用低6位存储偏移地址右移2位后的结果值。
(完)