大数跨境

x86-64/ARM64/RISC-V 跳转及调用指令的链接器视角

x86-64/ARM64/RISC-V 跳转及调用指令的链接器视角 ai算法芯片与系统
2026-07-28
10
导读:三种架构跳转范围差异大,链接器承担不同补偿工作:x86-64最轻,ARM64用跳板,RISC-V强制松弛。调用比跳转多保存返回地址,影响PLT、尾调用和RAS配对。理解这些是优化代码密度和性能的关键。

 

x86-64、ARM64 与 RISC-V 跳转与调用指令:编码范围、设计动机与链接器策略 🧩

跳转指令(Jump)和调用指令(Call)是 CPU 控制流转移的两大基石。调用指令在跳转的同时保存返回地址,因此链接器对其处理有额外考量(如 PLT 生成与尾调用优化)。本文从指令编码出发,系统分析无条件跳转(JMP/B/J)、调用(CALL/BL/JAL)、条件分支(Jcc/B.cond/BEQ)及间接跳转(BR/JALR)的硬件设计与链接器策略。


📑 目录

  1. 1. 引言:跳转指令的分类与编码约束
  2. 2. 基础概念:重定位、PLT 与链接器松弛
  3. 3. 无条件直接跳转(JMP / B / J
  4. 4. 调用指令(CALL / BL / JAL
  5. 5. 条件分支指令
  6. 6. 间接跳转与间接调用
  7. 7. 比较-跳转融合指令
  8. 8. 不同类型跳转/调用对链接器的影响
  9. 9. 链接器松弛与跳板生成策略
  10. 10. 链接过程中的内存布局变化示例
  11. 11. 综合对比总览
  12. 12. 总结与实用建议

1. 引言:跳转指令的分类与编码约束 🔍

跳转指令控制程序执行流的转移。根据目标地址的指定方式,主要分为四类:

  • • 无条件直接跳转:目标地址由指令中的立即数偏移量直接给出(如 JMP label),不保存返回地址。
  • • 调用指令:与无条件跳转类似,但额外保存返回地址(如 CALL label),用于函数调用。
  • • 条件分支:根据状态码(如 ZF、CF)决定是否跳转(如 JE label)。
  • • 间接跳转/调用:目标地址来自寄存器或内存(如 JMP RAX),范围不受限制。

在直接跳转(包括调用和条件分支)中,偏移量的位数( )直接决定了最大跳转距离。设指令对齐要求为   字节,则跳转范围为:

例如,ARM64 的 B 指令使用 26 位偏移,且要求 4 字节对齐,因此范围为 

设计者确定   时需权衡三个因素:

  • • 硬件解码复杂度:立即数越宽,解码逻辑越复杂。
  • • 指令长度约束:在 32 位定长指令中,立即数字段与操作码、寄存器字段争抢位宽。
  • • 工具链补偿能力:链接器能否通过松弛(Relaxation)或跳板弥补硬件范围的不足。

下图总结了指令分类与各架构的跳转范围概览:

下面我们逐一剖析三大架构的具体实现。👇


2. 基础概念:重定位、PLT 与链接器松弛 📚

在深入分析各架构之前,先明确三个链接器层面的核心概念。

2.1 重定位(Relocation)🔗

是什么:编译器生成目标文件(.o)时,外部符号的最终地址未知。编译器在指令的立即数字段预留占位(填 0),并生成一个重定位表项,告诉链接器:“这里需要修正!”链接器在合并所有目标文件后,根据符号的最终地址计算正确的偏移量并填入。

一个具体例子:x86-64 的 call foo 汇编后生成 E8 00 00 00 00E8 是操作码,后 4 字节是占位),并伴随重定位表项 R_X86_64_PC32 指向符号 foo。链接时若 foo 位于 0x10000,当前指令地址为 0x200,则计算偏移量 0x10000 - (0x200 + 5)(因为 CALL 长度为 5 字节),将结果填入后 4 字节。✅

不同指令的重定位差异

  • • 调用和跳转使用 PC 相对重定位。
  • • 条件分支的重定位需处理立即数位宽和编码格式。
  • • 间接跳转通常不产生重定位(目标来自寄存器)。

2.2 过程链接表(PLT)🧩

是什么:在动态链接中,外部函数(如 printf)的地址在加载前未知。PLT 是一段桩代码,用于在首次调用时解析实际地址并缓存。链接器为每个外部函数生成一个 PLT 条目,并将 CALL 重定位指向该条目,而非直接指向目标。

典型 PLT 条目(x86-64):


   
   
   
    
   
   
   foo@plt:
    jmp *foo@got(%rip)   ; 跳转到 GOT 中存储的地址
    push 0               ; 压入标识符
    jmp resolver         ; 调用动态链接器解析

注意:只有调用指令才会触发 PLT 生成,无条件跳转(JMP)通常不会。因为调用需要返回地址,动态链接器只需拦截调用即可完成符号解析。🔑

2.3 链接器松弛(Linker Relaxation)✨

是什么:链接器在知道最终地址后,可以用更短或更高效的指令序列替换原来的较长序列,前提是语义完全等价。松弛的目的是缩减代码体积、提高缓存命中率。

常见松弛场景

  • • x86-64:JMP rel32(5 字节)在目标距离 ±127 字节内可替换为 JMP rel8(2 字节)。
  • • RISC-V:AUIPC + JALR(8 字节)在目标距离 ±1MB 内可替换为单条 JAL(4 字节)——这是强制执行的。

松弛的代价:缩短指令会改变后续指令的地址,可能引发级联修正,增加链接时间。但这换来了更小的二进制体积和更快的执行速度。🚀

下图展示了三种架构的链接器在重定位和松弛上的总体负担对比:


3. 无条件直接跳转(JMP / B / J)🏃

纯粹的跳转只改变 PC,不保存任何返回信息。

3.1 x86-64 的 JMP rel32(±2GB)🦅

属性
说明
编码
E9 cd
(cd 为 4 字节偏移)
短跳变体
EB cb
(±128B,rel8
重定位
R_X86_64_PC32
示例
jmp label

设计动机:x86 是 CISC(复杂指令集),指令长度可变,可以轻松容纳 4 字节立即数。±2GB 覆盖了几乎所有应用程序,链接器几乎无需担心越界问题。🎯

3.2 ARM64 的 B(±128MB)⚖️

属性
说明
编码
0x14 + imm26
(左移 2 位)
重定位
R_AARCH64_JUMP26
示例
b loop

设计动机:ARM64 采用固定 32 位指令长度,26 位已是立即数字段的上限(需保留操作码和寄存器位)。基于统计,95% 以上的跳转在 ±128MB 内。对于少数越界情况,链接器可插入跳板。📊

3.3 RISC-V 的 J(±1MB)🧗

属性
说明
编码
JAL rd, label
rd=x0 时等价于跳转
范围
20 位立即数,左移 1 位,±1MB
重定位
R_RISCV_JAL
示例
jal x0, exit

设计动机:RISC-V 追求极简硬件,20 位立即数被拆成 4 个不连续字段,解码器只需导线重排即可提取,功耗极低。软件通过链接器松弛来弥补硬件范围的不足。⚡


4. 调用指令(CALL / BL / JAL)📞

调用指令与跳转的唯一区别:它在跳转前将返回地址保存到特定位置——x86-64 压入栈,ARM64 写入 X30,RISC-V 写入 rd(通常为 x1)。

4.1 x86-64 的 CALL rel32(±2GB)

属性
说明
编码
E8 cd
(4 字节偏移)
返回地址保存
压入栈
重定位
R_X86_64_PC32
(直接)/ R_X86_64_PLT32(动态链接)
示例
call printf

4.2 ARM64 的 BL(±128MB)

属性
说明
编码
0x94 + imm26
(左移 2 位)
返回地址保存
写入 X30
重定位
R_AARCH64_CALL26
示例
bl foo

4.3 RISC-V 的 JAL(±1MB)

属性
说明
编码
JAL rd, label
rd 通常为 x1
返回地址保存
写入 rd
重定位
R_RISCV_JAL
(单条)/ R_RISCV_CALL(组合)
示例
jal x1, foo

远距离调用组合:当距离超过 ±1MB,RISC-V 编译器生成 AUIPC x1, hi20 + JALR x1, x1, lo12(8 字节),由链接器决定是否松弛为单条 JAL。📏


5. 条件分支指令 🌿

用于 ifforwhile,通常具有局部性。

5.1 各架构条件分支对比

架构
指令
立即数位宽
对齐
范围
重定位类型
x86-64
Jcc rel8
 / rel32
8 / 32
1B
±128B / ±2GB
R_X86_64_PC8
 / PC32
ARM64
B.cond
19
4B
±1MB
R_AARCH64_CONDBR19
RISC-V
BEQ
/BNE/BLT
12
2B
±4KB
R_RISCV_BRANCH

5.2 指令示例

x86-64


   
   
   
    
   
   
   cmp eax, ebx
je label

ARM64


   
   
   
    
   
   
   cmp x0, x1
b.eq exit

RISC-V


   
   
   
    
   
   
   beq a0, a1, loop
blt a0, zero, exit

5.3 设计动机

条件分支大多局限于循环和条件语句内部,具有强局部性,短偏移足以覆盖。短偏移节省了指令编码位宽,利于提高指令密度。🎯


6. 间接跳转与间接调用 🔄

目标地址来自寄存器或内存,范围不受限制

6.1 各架构间接指令对比

架构
跳转指令
调用指令
返回指令
特点
x86-64
JMP r/m CALL r/m RET
支持内存间接寻址
ARM64
BR Xn BLR Xn RET
固定寄存器间接
RISC-V
JALR x0, rs1, imm12 JALR rd, rs1, imm12 JALR x0, x1, 0
自带 12 位偏移

6.2 RISC-V JALR 的独特优势

JALR 自带 12 位立即数偏移(±2KB),可用于:

  • • 跳转到 rs1 + 偏移 的地址(如函数表项 + 常量偏移)。
  • • 减少额外 ADD 指令,提升代码密度。

6.3 指令示例

x86-64


   
   
   
    
   
   
   jmp *rax          ; 跳转到 rax 指向的地址
call *[rdi+8]     ; 调用虚函数表第二项

ARM64


   
   
   
    
   
   
   br x16            ; 跳转
blr x1            ; 调用

RISC-V


   
   
   
    
   
   
   jalr x0, x1, 0    ; 返回
jalr x1, a0, 64   ; 调用 a0+64 地址处的函数

6.4 间接跳转/调用的必要性

  • • 实现运行时多态(虚函数)、回调函数、动态链接的延迟绑定。
  • • 不受范围限制,但代价是分支预测准确率下降。🎭

7. 比较-跳转融合指令 🎯

这类指令将比较和跳转合并为一条指令,减少指令数,提高密度。ARM64 提供丰富支持,x86 和 RISC-V 基础指令集无直接对应。

7.1 ARM64 的 CBZ / CBNZ(±1MB)


   
   
   
    
   
   
   cbz x0, exit      ; if (x0 == 0) goto exit
cbnz x1, loop     ; if (x1 != 0) goto loop
  • • 重定位类型:R_AARCH64_CBZ26(实际 19 位)
  • • 范围:±1MB(左移 2 位)

7.2 ARM64 的 TBZ / TBNZ(±16KB)

测试指定位是否为 0/1,偏移 14 位,范围 ±16KB。

7.3 RISC-V C 扩展

提供 C.BEQZ / C.BNEZ(16 位,±256B),属于压缩指令而非融合比较,但在短距条件跳转中效果类似。📦


8. 不同类型跳转/调用对链接器的影响 ⚙️

本章系统分析每种指令在链接器层面的不同行为。下图给出了总体影响框架:

8.1 重定位类型与计算方式差异 📌

指令类型
x86-64 重定位
ARM64 重定位
RISC-V 重定位
无条件跳转
R_X86_64_PC32 R_AARCH64_JUMP26 R_RISCV_JAL
调用
R_X86_64_PC32
 / PLT32
R_AARCH64_CALL26 R_RISCV_CALL
(组合)
条件分支
R_X86_64_PC8
 / PC32
R_AARCH64_CONDBR19 R_RISCV_BRANCH
间接跳转/调用
无重定位
无重定位
无重定位

关键差异

  • • 调用指令的重定位类型常与跳转分开(如 x86 的 PLT32 vs PC32),以便链接器决定是否生成 PLT。
  • • RISC-V 调用使用组合重定位 R_RISCV_CALL,链接器必须同时处理两条指令。
  • • RISC-V 条件分支需特殊编码立即数(拆分字段),重定位需处理位段重组。🧩

8.2 松弛能力与优化方向 🧘

指令类型
x86-64 松弛
ARM64 松弛
RISC-V 松弛
无条件跳转
JMP rel32
rel8(省 3B)
无(但可插跳板)
JAL x0
C.J(省 2B,需 C 扩展)
调用
不可松弛
不可松弛(只能插跳板)
AUIPC+JALR
JAL(省 4B),→C.JAL(省 6B)
条件分支
Jcc rel32
rel8(省 4B)
不可松弛
不可松弛(C 扩展需编译器生成)

松弛收益排序:RISC-V 调用 > x86 条件分支 > x86 跳转 > ARM64 无。

链接器实现复杂度

  • • x86-64:只需检查距离,简单替换操作码和立即数。
  • • ARM64:无松弛,但需扫描越界调用并分配跳板。
  • • RISC-V:必须遍历所有 R_RISCV_CALL 重定位,尝试两级松弛,并级联修正后续地址(因为指令长度变化)。🔧

8.3 PLT 与动态链接的特殊要求 🌐

  • • 调用指令是唯一需要 PLT 的类别。
  • • 跳转指令通常不生成 PLT。
  • • 在尾调用优化中,链接器可将 CALL 重定位转换为 JMP 重定位,跳过 PLT,提高性能。🏃

8.4 返回地址栈(RAS)与链接器的约束 🧠

  • • 调用指令必须与返回指令配对,否则 RAS 会预测错误。
  • • 跳转指令不影响 RAS。
  • • RISC-V 的 JAL 返回寄存器可配置(rd 可指定),链接器需识别调用约定才能进行尾调用优化。

9. 链接器松弛与跳板生成策略 🛠️

9.1 RISC-V 的强制松弛流程(Mermaid 图)

9.2 ARM64 的越界跳板(Veneer)插入

当 B / BL 超出 ±128MB,链接器插入跳板(12~16 字节):


   
   
   
    
   
   
   ADRP X16, target_hi
ADD  X16, X16, target_lo
BR   X16          ; 或 BLR X16(用于调用)

原始 B/BL 指向跳板(距离在范围内)。多个调用可共享同一跳板。🔄

9.3 x86-64 的可选短跳优化

若 JMP rel32 距离在 ±127B,替换为 JMP rel8(2B 代替 5B),省 3 字节。同样适用于条件分支 Jcc(6B→2B,省 4B)。✨

9.4 调用与跳转在链接器中的差异化处理 ⚙️

操作
CALL
 / BL / JAL
JMP
 / B / J
动态链接
默认生成 PLT
通常不生成 PLT
尾调用优化
可改写为 JMP
无需改写
越界处理
跳板中需用 BLR/CALL 匹配
跳板可用 BR/JMP
RAS 影响
必须保证配对
不影响 RAS

10. 链接过程中的内存布局变化示例 📂

假设目标文件有三个指令指向外部 foo(地址 0x80000000),最终布局如下(每一行是一个地址):

最终地址
指令内容
处理方式
0x10000
E8 FB EF FF 7F
 (call rel32)
x86-64 直接调用
0x10005
E9 F6 EF FF 7F
 (jmp rel32)
直接跳转
0x20000
0x02 0x00 0x00 0x94
 (BL 0x20004)
ARM64 转向跳板
0x20004
(跳板用于 BL)

0x20004
ADRP X16, 0x80000
构造地址
0x20008
ADD X16, X16, 0x000

0x2000C
0x00 0x02 0x1F 0xD6
 (BLR X16)
调用并保存 X30
0x30000
AUIPC x1, 0x80000
 (RISC-V call)
8B 组合(超 1MB)
0x30004
JALR x1, x1, 0x000
保存返回地址
0x40000
0x6F 0x00 0x00 0x00
 (JAL x0, near)
近距跳转(松弛后)

注意:ARM64 的跳板使用 BLR X16(调用)而非 BR,确保返回地址正确存入 X30。✅


11. 综合对比总览 📊

下面的思维导图汇总了三种架构在各维度上的差异:

指标
x86-64
ARM64
RISC-V
调用范围(直接)
±2GB
±128MB
±1MB
跳转范围(直接)
±2GB
±128MB
±1MB
条件分支范围
±2GB / ±128B
±1MB
±4KB
调用最短体积(近距)
5B
4B
4B(松弛后)
调用最远体积(超距)
5B
4B + 跳板(12~16B)
8B
PLT 条目典型大小
16B
16B
16B
链接器松弛方向
JMP rel32
rel8
不松弛(插跳板)
AUIPC+JALR
JALC.JAL
尾调用优化
CALL
JMP
BL
B
JAL
(rd=x1)→JAL x0
硬件 RAS 支持
强(调用/返回配对)
强(BL/RET
中(JAL/JALR x0,x1
链接器负担
极低 ✅
中等 ⚖️
高 🔧

12. 总结与实用建议 🧘

12.1 各架构设计哲学回顾

设计选择
x86-64
ARM64
RISC-V
对调用范围的态度
硬件承担大范围
硬件承担中范围,软件补跳板
硬件承担小范围,软件强制松弛
对 PLT 的态度
常规支持
常规支持
常规支持,松弛可跳过 PLT
对 RAS 的要求
隐式(栈)
显式(X30)
显式(rd 可配置)
压缩指令的作用
短跳 rel8
无(固定长度)
C 扩展压缩调用

12.2 给开发者的实用建议 💡

  • • 性能敏感代码:优先使用直接调用(CALL/BL/JAL),避免间接调用(破坏预测)。
  • • 大型 C++ 项目:启用链接时优化(LTO)和函数重排,使频繁调用的函数保持在 ±1MB(RISC-V)或 ±128MB(ARM64)内,避免跳板。
  • • 动态链接:使用 -fno-plt 让 CALL 直接通过 GOT 间接跳转(减少 PLT 开销),但会增加一次内存加载。
  • • 嵌入式开发:RISC-V 需强制开启 -mrelax,否则所有调用都是 8B,无法利用短跳优势。

12.3 核心要点 🎯

  • • 调用与跳转的唯一区别在于是否保存返回地址,这直接影响了 PLT、RAS 和尾调用优化。
  • • 条件分支的范围普遍短于调用,利用了程序的局部性原理
  • • 间接跳转/调用实现了运行时多态,但代价是分支预测准确率下降
  • • 三种架构对“硬件 vs 软件”的权衡各有侧重:x86 硬件扛下所有,RISC-V 把包袱甩给链接器,ARM64 折中。

理解每条跳转/调用指令的位宽限制与链接器行为,是写出高性能、高密度机器码的基础。🌱

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 216
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读3.7k
粉丝0
内容216