3.4 PC相对寻址
程序计数器(Program Counter,PC)用来指示下一条指令的地址。为了保证CPU正确地执行程序的指令代码,CPU必须知道下一条指令的地址,这就是程序计数器的作用,程序计数器通常是一个寄存器。例如,在程序执行之前,把程序的入口地址(即第一条指令的地址)设置到PC寄存器中,CPU从PC寄存器指向的地址取值,然后依次执行。CPU执行完一条指令后会自动修改PC寄存器的内容,使其指向下一条指令的地址。
RISC-V指令集提供了一条PC相对寻址的指令AUIPC,格式如下。
auipc rd, imm
这条指令把imm(立即数)左移12位并带符号扩展到64位后,得到一个新的立即数。这个新的立即数是一个有符号的立即数,再加上当前PC值,然后存储到rd寄存器中。由于新的立即数表示的是地址的高20位部分,并且是一个有符号的立即数,因此这条指令的寻址范围为基于当前的PC偏移量±2 GB,如图3.6所示。另外,由于这个新的立即数的低12位都是0,因此它只能寻址到与4 KB对齐的地址。涉及4 KB以内的寻址,则需要结合其他指令(如ADDI指令)来完成。
图3.6 AUIPC指令寻址范围
另外,还有一条指令(LUI指令)与AUIPC指令类似。不同点在于LUI指令不使用PC相对寻址,它仅仅把立即数左移12位,得到一个新的32立即数,再带符号扩展到64位,将其存储到rd寄存器中。AUIPC和LUI指令的编码如图3.7所示。
图3.7 AUIPC和LUI指令的编码
【例3-5】 假设当前PC值为0x8020 0000,分别执行如下指令,a5和a6寄存器的值分别是多少?
auipc a5,0x2
lui a6,0x2
a5寄存器的值为PC + sign_extend(0x2 << 12) = 0x8020 0000 + 0x2000 = 0x8020 2000。
a6寄存器的值为0x2 << 12 = 0x2000。
AUIPC指令通常和ADDI指令联合使用来实现32位地址空间的PC相对寻址。AUIPC指令可以寻址与被访问地址按4 KB对齐的地方,即被访问地址的高20位。ADDI指令可以在[−2048, 2047]范围内寻址,即被访问地址的低12位。
如果知道了当前的PC值和目标地址,如何计算AUIPC和ADDI指令的参数呢?在图3.8中,offset为地址B与当前PC值的偏移量,地址B与4 KB对齐的地方为地址A,地址A与地址B的偏移量为lo12。lo12是有符号数的12位数值,取值范围为[−2048, 2047]。
图3.8 使用AUIPC和ADDI指令寻址
根据上述信息,可以得出计算hi20和lo12的公式。
hi20 = (offset >> 12) + offset[11]
lo12 = offset & 0xfff
这里特别需要注意如下几点。
● hi20表示地址的高20位,用于AUIPC指令的imm操作数中。
● lo12表示地址的低12位,用于ADDI指令的imm操作数中。
● 计算hi20时需要加上offset[11],用于抵消低12位有符号数的影响(见例3-6)。
● lo12是一个12位有符号数,取值范围为[−2048, 2047]。
使用AUIPC和ADDI指令对地址B进行寻址。
auipc a0,hi20
addi a1,a0,lo12
【例3-6】 假设PC值为0x8020 0000,地址B为0x8020 1800,地址B正好在4 KB的正中间,地址B与地址A的偏移量为2048字节,与地址C的偏移量为−2048字节,如图3.9所示。
图3.9 地址之间的关系
那我们应该使用地址A还是地址C来计算lo12呢?
应该使用地址C来计算lo12。因为lo12是一个12位的有符号数,取值范围为[−2048, 2047]。若使用地址A来计算,就会超过lo12的取值范围。
地址B与PC值的偏移量均为0x1800。根据前面列出的计算公式,计算hi20和lo12。
hi20 = (0x1800 >> 12) + offset[11] = 2
lo12 = 0x800
因为lo12为12位的有符号数,所以0x800表示的十进制数为−2048。下面是访问地址B的汇编指令。
auipc a0, 2
addi a1, a0, -2048
如果把ADDI指令写成如下形式,汇编器将报错。(因为汇编器把字符“0x800”当成了64位数值(即2048)解析,它已经超过了ADDI指令中立即数的取值范围。)
addi a1, a0, 0x800
报错日志如下。
AS build_src/boot_s.o
src/boot.S: Assembler messages:
src/boot.S:6: Error: illegal operands 'addi a1,a0,0x800'
make: *** [Makefile:28: build_src/boot_s.o] Error 1
通常很少直接使用AUIPC指令,因为编写汇编代码时不知道当前PC值是多少。计算上述hi20和lo12的过程通常由链接器在重定位时完成。不过RISC-V定义了几条常用的伪指令,这些伪指令是基于AUIPC指令的。伪指令是对汇编器发出的命令,它在源程序汇编期间由汇编器处理。伪指令可以完成选择处理器、定义程序模式、定义数据、分配存储区、指示程序结束等功能。总之,伪指令可以分解为几条指令的集合。与PC相关的加载与存储伪指令如表3.3所示。
表3.3 与PC相关的加载和存储伪指令
表3.3中的PIC表示生成与位置无关的代码(Position Independent Code),GOT表示全局偏移量表(Global Offset Table)。GCC有一个“-fpic”编译选项,它在生成的代码中使用相对地址,而不是绝对地址。所有对绝对地址的访问都需要通过GOT实现,这种方式通常运用在共享库中。无论共享库被加载器加载到内存的什么位置,代码都能正确执行,而不需要重定位(relocate)。若没有使用“-fpic”选项编译共享库,则当有多个程序加载此共享库时,加载器需要为每个程序重定位共享库,即根据加载到的位置重定位,这中间可能会触发写时复制机制。
【例3-7】 观察LA和LLA指令在PIC与非PIC模式下的区别。下面是main.c文件和asm.S文件。
<main.c>
extern void asm_test(void);
int main(void)
{
asm_test();
return 0;
}
<asm.S>
.globl my_test_data
my_test_data:
.dword 0x12345678abcdabcd
.global asm_test
asm_test:
la t0, my_test_data
lla t1, my_test_data
ret
首先,观察非PIC模式。在QEMU+RISC-V+Linux平台[2]上编译,使用“-fno-pic”关闭PIC。
[2]QEMU+RISC-V+Linux平台的搭建方法见第2.4节。
# gcc main.c asm.S -fno-pic -O2 -g -o test
使用OBJDUMP命令反汇编。
root:example_pic# objdump -d test
00000000000005f4 <my_test_data>:
5f4:abcd j be6 <__FRAME_END__+0x53e>
5f6:abcd j be8 <__FRAME_END__+0x540>
5f8:5678 lw a4,108(a2)
5fa:1234 addi a3,sp,296
00000000000005fc <asm_test>:
5fc:00000297 auipc t0,0x0
600:ff828293 addi t0,t0,-8 # 5f4 <my_test_data>
604:00000317 auipc t1,0x0
608:ff030313 addi t1,t1,-16 # 5f4 <my_test_data>
60c:8082 ret
通过反汇编可知,在非PIC模式下,LA和LLA伪指令都是AUIPC与ADDI指令,并且都直接获取了my_test_data符号的绝对地址。
接下来,使用“-fpic”重新编译test程序。
# gcc main.c asm.S -fpic -O2 -g -o test
使用OBJDUMP命令反汇编。
root:example_pic# objdump -d test
0000000000000634 <my_test_data>:
634:abcd j c26 <__FRAME_END__+0x53e>
636:abcd j c28 <__FRAME_END__+0x540>
638:5678 lw a4,108(a2)
63a:1234 addi a3,sp,296
000000000000063c <asm_test>:
63c:00002297 auipc t0,0x2
640:9f42b283 ld t0,-1548(t0) # 2030 <_GLOBAL_OFFSET_TABLE_+0x10>
644:00000317 auipc t1,0x0
648:ff030313 addi t1,t1,-16 # 634 <my_test_data>
64c:8082 ret
通过反汇编可知,在PIC模式下,LA伪指令是AUIPC和LD指令的集合,它会访问GOT,然后从GOT中获取my_test_data符号的地址;而LLA伪指令是AUIPC和ADDI指令的集合,可直接获取my_test_data符号的绝对地址。
总之,在非PIC模式下,LLA和LA伪指令的行为相同,都是直接获取符号的绝对地址;而在PIC模式下,LA指令是从GOT中获取符号的地址,而LLA伪指令则是直接获取符号的绝对地址。
【例3-8】 在例3-7的基础上修改asm.S汇编文件,目的是观察LI伪指令。
<asm.S>
.global asm_test
asm_test:
li t0, 0xfffffff080200000
ret
在QEMU+RISC-V+Linux平台上编译。
# gcc main.c asm.S -O2 -g -o test
使用OBJDUMP命令反汇编。
root:example_pic# objdump -d test
00000000000005fc <asm_test>:
5fc:72e1 lui t0,0xffff8
5fe:4012829b addiw t0,t0,1025
602:02d6 slli t0,t0,0x15
604:8082 ret
从上面的反汇编结果可知,上述的LI伪指令由LUI、ADDIW和SLLI这3条指令组成。