2026年01月23日/ 浏览 6
la是risc-v指令集中的一条伪指令。伪指令,一般会被汇编器翻译成一条或者多条等价的实际指令。伪指令存在的意义就类似于C语言中的typedef或者#define等,完全是出于方便程序员编程、阅读源代码的目的,它本身并没有扩展整个指令集的表达能力。
la(load address),顾名思义,它是一条地址加载指令。

最左一栏是la指令的格式,它将一个内存地址(symbol是一个地址标记)的值加载到rd寄存器中。第二栏是la为指令被翻译成的等价的实际指令,可以看出来,la指令在非位置无关码(non-PIC)和位置无关码(PIC,position independance code)中,具有不同的解释方式——这一篇我们主要聊聊non-pic的情况下la的地址加载过程,下一篇我们再聊pic的情形。第三栏是简要的含义说明。
la会被汇编器解释成两条等价的实际指令,可以大胆猜测,risc-v是分了两步才将一个地址加载到寄存器中的。
既然la指令是分两步完成的,那我们也分两步来理解它——第一步,我先给出一个有漏洞的版本,第二步我们再修正它。
有漏洞的版本如下图所示。这时候指令 la symbol 会被解释成下面的两条语句
auipcrd,delta[31:12]
addird,rd,delta[11:0]
其中delta是目标地址symbol和当前指令地址pc之间的偏移:delta=symbol-pc
先来解释一下第一步的auipc指令,这条指令全称叫add upper immediate to PC,它的行为有点奇怪,它会把立即数的高20位和pc相加(或者说,把立即数的低12位抹0,然后和pc相加。可能这样说更直观),结果写入rd寄存器。
auipc rd,immediate
~~rd = pc + sext(immediate[31:12]<<12)~~
rd=pc+immediate[31:12]<<12
sext()表示符号扩展(sign extension),它是针对64位指令集对,即把最高位,即第三十一位的值填充到64位寄存器的高32位。这里我们讨论32位指令集RV32,这个sext()可以忽略。
接着第二步,把第一步得到的结果rd再和偏移delta的第12位相加,即可得到symbol的地址了。
说白了,这两步的过程其实就是像是一个加法结合律的应用。
delta=(delta[31:12]<<12)+delta[11:0]
symbol=pc+delta
symbol=pc+ ( (delta[31:12]<<12)+delta[11:0] )
symbol=( pc+(delta[31:12]<<12) ) +delta[11:0]
上面的第四个式子就是伪指令la的操作。
如果la就是这样实现的话,其实有个小小的漏洞。我们回顾一下第二步的addi指令addi rd,rd,delta[11:0]。 先来看一下addi指令的格式。 在32位的risc-v指令集中,它的样子如下图所示。

可以看到,addi只用了12位来存放立即数。可能有小伙伴会有疑问了,寄存器rd是32位的(前面提到过,我们现在讨论的是32位指令集),位数都不一样,它们是怎么相加的呢?其实addi会对立即数先进行符号扩展,也就是用最高位、第11位的值把立即数填充至32位,再与rd相加。
为什么要进行符号扩展而不是零扩展(Zero extension)? 因为如果进行的是零扩展,就不存在那个“小小的漏洞”。原因在于risc-v中并没有和addi对应的subi(立即数减)指令,所以减掉一个立即数都是用加上它的负数来等价实现的,这就要求addi的立即数必须是一个(二进制补码表示的)有符号数。
说到这里可能有小伙伴反应过来了——是的,如果我们的delta[11]是1而不是0的话,进行符号扩展后,我们la的第二步addi指令就会将一个负数加到rd上,这明显是不对的,这样做的结果会比正确答案多减掉一个2^12。
改正办法如下所示
如果第11位是1的话,我们就给auipc的immediate加上1。前面已经介绍过,auipc中的immediate会左移12位再和pc相加。给immdiate加1,就相当与给rd加2^12。第一步多加的刚好弥补第二步多减的,这样就修正了答案。
与la类似,risc-v中还有一条叫做li(load immediate)的伪指令。
li,rd,imm
它把一个立即数imm加载到rd寄存器中。前面提到的addi指令,用12位表示一个有符号数,因此当imm在-2^12~2^12-1范围内(也就是[-2048~2047])的时候,li被转化成下面这条实际指令:
addi rd, x0,imm #rd=imm+0
注意这里的x0是risc-v体系中一个特殊的设计,这个x0寄存器永远都是0,不能被改变。
当imm不在这个范围,但在32位有符号数的范围内(也就是[-2147482648~-2048)以及(+2047~+2147482647])的时候,一条addi指令显然是不够了。 这时候需要用到一条和auipc类似的指令: lui,
lui rd, imm #rd=imm[31:12]<<12
它把大立即数的高20位(也就是imm[31:12],可以通过imm>>12得到)左移12位,然后放到rd中(效果上,就是把imm的低12位抹0放到rd中,可能这样说更直观)。
最后,li指令的加载一个大立即数的步骤就是这样:
lui rd, imm[31:12]+imm[11] addi, rd,rd,imm[11:0]
至于为什么要在第一步lui中加上imm[11], 和前面auipc中是一样的道理,这就不再重复解释了。