4.3. 转移指令
转移指令用于实现有条件或无条件分支、函数调用、函数返回以及循环控制等操作。LoongArch支持的转移指令如表3-9所示。
表3-9中,转移指令助记符有两类命名方式:相对跳转的地址计算依赖PC值,称为“分支”(Branch),助记符通常以b开头;绝对跳转的地址计算不依赖PC值,称为“跳转”(Jump),助记符通常以j开头。
PC是程序计数器,用来控制程序中指令的执行顺序。程序正常运行时,PC指向CPU将要执行的下一条指令。CPU取出该指令后,会自动更新PC,使其指向再下一条指令,从而保证程序顺序执行。当程序需要改变执行顺序,也就是发生跳转时,就必须提前修改PC,使它指向目标指令地址。
表3-9中的转移指令可从有条件分支、无条件分支和跳转、跳转范围几个角度理解。
4.3.1. 有条件的分支指令
表3-9中的beq、bne、blt、bge、bltu、bgeu、beqz、bnez都是有条件相对跳转指令。条件成立时,程序跳转到目标地址;条件不成立时,继续顺序执行。目标地址的计算方式为:先将指令码中的立即数(offs16或offs21)逻辑左移2位并符号扩展,再把得到的偏移值加到该分支指令的PC上。各条件分别表示等于(eq)、不等于(ne)、小于(lt/ltu)、大于或等于(ge/geu)、等于零(eqz)和不等于零(nez);其中ltu、geu表示按无符号操作数进行比较。
有条件分支指令常用于函数内部控制程序流程,作用类似C语言中的if-else、do-while、for等语句。
4.3.2. 无条件分支指令和跳转指令
表3-9中的b和bl都是无条件分支指令,用于直接跳转到目标地址。目标地址的计算方式为:将指令码中的26位立即数offs26逻辑左移2位并符号扩展,再把得到的偏移值加到该分支指令的PC上。b和bl的区别在于,bl是带链接的无条件分支指令(Branch and Link),跳转前会把该指令PC值加4的结果写入寄存器r1。
表3-9中的jirl是无条件跳转指令,同样用于直接跳转到目标地址。它的目标地址计算方式为:先将指令码中的16位立即数offs16逻辑左移2位并符号扩展,再与寄存器rj中的值相加。指令名中的ir分别表示立即数(Immediate)和寄存器(Register),说明跳转目标基于“寄存器+立即数”,而不是基于PC。l表示带链接跳转(Jump and Link),即跳转时还会把该指令PC值加4的结果写入寄存器rd。
在LoongArch ABI中,寄存器r1被定义为返回地址寄存器ra。关于LoongArch ABI,后续章节会详细说明。
分支指令bl通常用于函数调用,跳转指令jirl通常用于函数返回。
4.3.3. 跳转范围
表3-9中,分支指令beq、bne、blt、bltu、bge、bgeu使用offs16作为偏移量。地址计算时还会左移2位,因此实际偏移宽度为18位,相对跳转范围为[PC-128K, PC+128K]。无条件分支指令b、bl使用offs26作为地址偏移量,左移2位后实际偏移宽度为28位,相对跳转范围为[PC-128M, PC+128M]。这一范围与ARM架构的相对跳转范围相同,比部分早期架构更大。例如,MIPS架构的无条件相对跳转范围为[-128K,128K]。
更大的跳转范围可以减少加载地址所需的额外指令。假设程序需要无条件跳转到[PC-128M, PC+128M]范围内的某个地址,例如偏移量为0x40,则只需一条分支指令b即可完成:
b 0x10 // 跳转到目标地址PC+0x40(0x10<<2)
如果目标地址偏移超出该范围,一条分支指令就无法完成跳转。此时需要先把目标地址(PC+偏移量)加载到寄存器,再用绝对跳转指令jirl跳转:
li $r7, (PC+offset) # 加载目标地址到寄存器r7
jirl $r0, $r7, 0 # 跳转到目标地址(r7+0)
根据常量(PC+offset)的大小不同,宏指令li最终会被编译器扩展为1~4条汇编指令。