Skip to content

LA32R中的用户态整型指令

数据传输类指令

前一节中我们介绍了LA32R中主要的运算类指令,那么如何为这些运算指令提供操作数呢,这就涉及到本节所关注的数据传输类指令。

向寄存器中加载立即数

除了addi.wandiorixorislli.wsrli.wsrai.w这些指令中以立即数形式存在的操作数外,最直接的提供操作数的方式就是向寄存器中加载一个立即数。为了实现高效的立即数加载,指令中定义了如下一条真实指令:

指令 汇编表达 功能简释
lu12i (load upper from bit12 immediate) lu12i $rx, si12 GR[x] = si20 << 12

不过,如果你不是在进行CPU硬件设计,那么建议你尽量记住并多使用汇编伪指令li.w

指令 汇编表达 功能简释
li.w (load immediate word) li.w $rx, imm32 GR[x] = imm32

作为一条伪指令,汇编器会根据immediate数值的范围自动生成一条指令或两条指令的序列。汇编器具体是如何实现的,感兴趣的可以自己写个汇编程序编译后再反汇编看一下。

li.w伪指令的立即数immediate的合法取值范围是[-231, 232-1],这个范围里面上界的指数是32,不是笔误。汇编器的这个设计考虑到了大家的日常使用习惯,毕竟当你想装载一个第31位等于1的二进制数时(例如一个IO设备寄存器的写掩码),你可以直接写这个立即数而不用算出它的负数。譬如,你可以直接写li.w $t0, 0xfefca147,而不用写成li.w $t0, -0x1035eb9

寄存器间的传输数据

LA32R中可以用伪指令move来完成寄存器间的数据传输。

指令 汇编表达 功能简释
move (move) move $rx, $ry GR[x] = GR[y]

在寄存器和内存之间传输数据

RISC型指令集仅通过load/store指令在寄存器和内存之间传输数据。LA32R中普通1的load/store指令如下:

指令 汇编表达 功能简释
ld.b (load byte signed) ld.b $rx, $ry, si12 GR[x] = sext32(MEM[GR[y] + sext32(si12)][7:0])
ld.bu (load byte unsigned) ld.bu $rx, $ry, si12 GR[x] = zext32(MEM[GR[y] + sext32(si12)][7:0])
ld.h (load halfword signed) ld.h $rx, $ry, si12 GR[x] = sext32(MEM[GR[y] + sext32(si12)][15:0])
ld.hu (load halfword unsigned) ld.hu $rx, $ry, si12 GR[x] = zext32(MEM[GR[y] + sext32(si12)][15:0])
ld.w (load word signed) ld.w $rx, $ry, si12 GR[x] = MEM[GR[y] + sext32(si12)][31:0]
st.b (store byte) st.b $rx, $ry, si12 MEM[GR[y] + sext32(si12)][7:0] = GR[x][7:0]
st.h (store halfword) st.h $rx, $ry, si12 MEM[GR[y] + sext32(si12)][15:0] = GR[x][15:0]
st.w (store word) st.w $rx, $ry, si12 MEM[GR[y] + sext32(si12)][31:0] = GR[x]

字节和半字load/store指令

指令集中定义字节和半字load/store指令的目的主要有两个。第一个目的是为了CPU访问一些只能接收字节或半字位宽访问的设备。像LA32R这类RISC指令基本上都采用的是Memory Mapped I/O的方式来访问外设的2,如果不通过load指令的类型来区分访问位宽,就无法访问只能接收字节或半字位宽访问的设备。第二个目的是为了加速从内存装载char型、short型数据,否则就只能从内存装载一个字到寄存器,然后再利用移位等操作提取出所需的数据,即需要用花费数条指令才能完成单条字节、半字load指令就可完成的功能。

字节和半字的load指令都有singed和unsigned两个,是为了适应char和unsigned char、short和unsigned short不同数据类型的访问对象。指令集这样设计是从性能优化的目的来考虑的。否则,只定义有符号扩展的或者无符号扩展的字节、半字load指令,再对取回到寄存器中的数据按照需求用指令完成相应的符号扩展,技术上也是可行的,只不过有的情况会多生成一些用于符号扩展的运算类指令,影响性能罢了。

TODO:对于接触硬件比较少的读者来说,Memory Mapped I/O可能有点难以理解,不过暂时不知道推荐什么参考材料合适。

汇编程序中load/store指令地址的计算

这一小节我们并不是要复习load/store指令采用“寄存器基址+立即数偏移”寻址方式这个知识点。我们主要是想讨论汇编编程时load/store指令基址寄存器和偏移量各自如何计算。

汇编中load/store指令访问的地址,绝大多数可以看作是C语言中变量的地址。我们知道C语言中有动态变量和静态变量。这两类变量在程序运行时处在不同的内存区域,其中动态变量分配在栈和堆上,而位于Data段和BSS段的静态变量则被系统分配在其它内存区域。我们接下来将按照动态变量和静态变量两种类型做进一步讨论。如果想进一步了解不同类型变量所在内存区域具体如何划分和管理,可以阅读本手册第2部分关于Linux/LA32R系统下进程地址空间划分的介绍内容。

动态变量的地址的计算

堆上数据的分配和释放,全部由程序员自行完成,即堆上的动态变量load/store的地址完全是由你自己来定的,只要你能确保堆上每个变量所用的地址:(1)装载后位于内存中给堆分配的区域,即不会侵入到栈空间或其它代码、数据空间;(2)不会和堆上其它变量的地址冲突。这两点要求在Linux这样的系统上随着你所调试的应用规模增大将变得很有难度,所以除非是一些嵌入式应用场景下程序直接运行在裸金属(baremetal)执行环境中,我们不建议大家在汇编下操作堆上的动态变量。

存放在栈空间上的动态变量,其load/store的基址寄存器通常就采用栈指针寄存器($sp)或帧指针寄存器($fp),即使你因为某些原因而另选其它寄存器做基址寄存器,所用的基址寄存器的值也是需要从这两个寄存器(之一)中推算出来的。编程中需要重点注意的地方是,因为$sp$fp指向的位置在程序运行过程中会变化,所以编程人员必须清楚当这条load/store指令执行时,其访问变量的地址与此时$sp$fp的具体相对位置,对应到指令中就是立即数偏移量的值。

静态变量的地址的确定

静态变量在程序运行时的地址,与链接和装载两个环节有关。为了避免受到装载过程中数据段具体被装载到什么地址这一运行时才能确定的因素的影响,我们强烈建议访问静态变量的寻址采用位置无关代码(Position Independent Code, 简称PIC)的风格。在LA32R所用的ABI中,我们主要通过相对指令PC寻址的方式来实现位置无关的寻址。对应到汇编开发中,则是利用伪指令la.local来获取地址。我们可以通过下面所列C代码和汇编代码之间的对应关系来看一下la.local的具体使用方法。

int data_var_a = 1, data_var_b = 2;
int bss_var_c;

int foo() {
    bss_var_c = data_var_a + data_var_b;
    return 0;
}
    .data
data_var_a:     .word   1
data_var_b:     .word   2

    .bss
bss_var_c:      .skip   4

    .text
    ......
    la.local    $t0, data_var_a         #获取data_var_a的地址
    ld.w        $t1, $t0, 0             #取回data_var_a的值
    la.local    $t0, data_var_b         #获取data_var_b的地址
    ld.w        $t2, $t0, 0             #取回data_var_b的值
    add.w       $t3, $t1, $t2           #data_var_a+data_var_b
    la.local    $t0, bss_var_c          #取回bss_var_c的地址
    st.w        $t3, $t0, 0             #结果写入bss_var_c中
    ......

上面例子中la.local $t0, data_var_a这条伪指令会被汇编器翻译成如下的两条指令:

    pcaddu12i   $t0, pcrel(adr(data_var_a)+0x800)<<32>>44;
    addi.w      $t0, $t0, pcrel(adr(data_var_a)+4)-(pcrel(adr(data_var_a)+4+0x800)>>12<<12);

上面的表达式中,adr(L)表示链接后标签L的地址,pcrel(A)表示地址A相对于这条指令PC的偏移量。pcaddu12i指令的速查内容如下:

指令 汇编表达 功能简释
pcaddu12i (pc add upper from bit12 immediate) pcaddu12i $rx, si20 GR[x] = PC + sext32(si20 << 12)

可能有的读者已经发现了,原有汇编程序中的

    la.local    $t0, data_var_a         #获取data_var_a的地址
    ld.w        $t1, $t0, 0             #取回data_var_a的值

可以直接用如下两条指令代替:

    pcaddu12i   $t0, pcrel(adr(data_var_a)+0x800)<<32>>44;
    ld.w        $t1, $t0, pcrel(adr(data_var_a)+4)-(pcrel(adr(data_var_a)+4+0x800)>>12<<12);

不过,由于目前LA32R的汇编器还不支持形如ld.w.local $t1, data_var_a这样的伪指令,所以还无法在汇编开发时获得上述的高效代码。

既然有la.local这样的伪指令,那么有没有la.global这样的伪指令呢?答案是有的。不过,LA32R的工具链对于la.global伪指令展开的位置无关寻址指令序列是基于GOT表的。因为访问GOT表需要一条load指令,所以基于GOT表的PIC寻址的性能开销通常大于相对指令PC寻址方式。在没有装载器(Loader)的裸金属执行环境中开发程序,就更要慎重使用la.global,除非你能确保在其需要访问的GOT表表项位置上已存入正确的数值。la.global通常用来加载位于不同汇编文件中的标签的地址。如果不是十分必要,尽量不要用la.global

熟悉MIPS或RISC-V汇编编程的读者,应该经常用la这个宏指令/伪指令。请你们在LA32R下开发汇编时注意不要习惯性的写了la而不是我们推荐的la.local。因为LA32R的工具链同时也支持la这条伪指令(意味编译过程不会有报错提醒你注意),但是la是与la.global等价的,所以很可能会花费你不少时间去de出这个bug。


  1. 这里说普通是为了与支持原子访存的ll.wsc.w指令区别开来。 

  2. X86指令集有专门用于访问I/O端口空间的I/O指令。不过这并意味着基于X86架构的系统中不可以采用Memory Mapped I/O方式来进行I/O访问。