本篇定位:RISC-V 专题第 9 篇。本篇系统化讲 RISC-V GCC 工具链的内部:linker relaxation(RISC-V 独有)、ELF 段布局、软算术函数族、启动 runtime。读完能调链接脚本到指令级、看懂 relaxation 优化、固化 ROM 函数。
目录
- 一、RISC-V 工具链组成
- Linux 版 vs 裸机版
- 二、Linker Relaxation
- 2.1 什么是 relaxation
- 2.2 典型 relaxation
- (1) lui+addi → c.lui 或 gp 相对
- (2) auipc+jalr → jal(若目标在 ±1MB 内)
- (3) 多字节压栈 → c.push
- 2.3 relaxation 的开关
- 2.4 relaxation 的副作用
- 2.5 什么时候关 relaxation
- 三、ELF 段布局详解
- 3.1 标准段
- 3.2 RISC-V 特有段
- 3.3 -msmall-data-limit
- 3.4 看段大小和符号
- 四、软算术函数族(libgcc)
- 4.1 整数软算术(无 M 扩展)
- 4.2 浮点软算术(无 F/D 扩展)
- 4.3 ARM vs RISC-V 软算术名
- 4.4 `__moddi3` 优化
- 五、启动 runtime:_start → main
- 5.1 __libc_init_array
- 谁会被放进 .init_array
- 5.2 C++ 全局对象的代价
- 5.3 exit
- 六、链接脚本实战要点
- 6.1 关键符号
- 6.2 .data 的 AT 语法
- 6.3 内存区定义
- 6.4 gc-sections 配合
- 七、调试工具速查
- 八、ARM vs RISC-V 工具链对照
- 九、本篇小结
- 速查表
一、RISC-V 工具链组成
riscv64-unknown-elf-gcc # 编译器(驱动) ├─ cc1 # C 编译器(生成汇编) ├─ as # 汇编器(生成 .o) ├─ ld # 链接器(生成 .elf) ├─ libgcc # 软算术库(__divsi3 等) ├─ libc(newlib/picolibc) # C 标准库 └─ libm # 数学库 riscv64-unknown-elf-objdump # 反汇编 riscv64-unknown-elf-objcopy # 格式转换(elf→bin/hex) riscv64-unknown-elf-nm # 符号表 riscv64-unknown-elf-size # 段大小 riscv64-unknown-elf-addr2line # 地址→代码行 riscv64-unknown-elf-readelf # ELF 信息Linux 版 vs 裸机版
riscv64-unknown-elf-gcc:裸机(newlib),你 MCU 用riscv64-linux-gnu-gcc:Linux(glibc),跑应用用- 交叉编译(64位 Linux)用后者,MCU 用前者
二、Linker Relaxation
这是 RISC-V 工具链最独特的优化,ARM 没有。
2.1 什么是 relaxation
链接器在链接时,把一些指令序列替换成更短的等价序列,省代码体积。
2.2 典型 relaxation
(1) lui+addi → c.lui 或 gp 相对
# 编译器生成(保守,假设符号远): lui a0, %hi(var) # 4字节 lw a0, %lo(var)(a0) # 4字节 # 共 8 字节 # linker relax(若 var 在 gp ±2KB 内): lw a0, %gprel(var)(gp) # 4字节(或 c.lw 2字节) # 省 4-6 字节(2) auipc+jalr → jal(若目标在 ±1MB 内)
# 编译器生成(call 伪指令,假设目标远): auipc ra, %hi(func) jalr ra, %lo(func)(ra) # 8 字节 # linker relax(若 func 在 ±1MB): jal ra, func # 4 字节,省 4 字节(3) 多字节压栈 → c.push
RISC-V 的 save-restore(⑧讲过)配合 relaxation,把 prologue 压成单条call __riscv_save_*。
2.3 relaxation 的开关
-mrelax# 启用(默认)-mno-relax# 禁用2.4 relaxation 的副作用
relaxation 改变代码大小和地址
relaxation 后代码体积变小,所有符号地址在链接后才固定。影响:
- 反汇编看的是 relax 后的代码(指令数变了)
- 调试符号/DWARF 信息要带 relax 后的地址
- gp 设值必须防 relaxation:
la gp, __global_pointer$可能被 relax 成mv gp,gp(无意义),用.option norelax禁掉(⑦讲过)
2.5 什么时候关 relaxation
- 调试启动死机时(代码地址变化难跟踪)
- 栈回溯机器码扫描想简化时
- 代码大小不敏感、要确定性时
ARM 的对应能力
ARM 工具链(armclang/armlink)通过以下机制实现类似效果:
链接时优化(LTO):编译时保留 LLVM 中间表示(IR),链接阶段进行跨文件全局分析,实现函数内联、死代码消除等。
消除公共部分组(COMDAT):检测并丢弃内联函数/模板的重复副本,保留最佳变体。
消除未使用部分:通过 --remove 删除不可达代码和数据。
核心区别
RISC-V 的 relaxation 是指令级别的代码序列替换(链接器直接改机器码);ARM 的 LTO 是IR 级别的全局优化(链接器看到的是中间表示而非最终指令),两者实现层面不同。
三、ELF 段布局详解
你天天看 size 输出,这里系统讲段。
3.1 标准段
riscv64-unknown-elf-size firmware.elf text data bss dec hex 12340 120 8200 20660 50b4| 段 | 内容 | 存放 | 运行时 |
|---|---|---|---|
| .text | 代码 | Flash(只读) | Flash 执行 |
| .rodata | 只读数据(常量/字符串) | Flash | Flash 读 |
| .data | 已初始化全局/静态 | Flash(拷贝源) | RAM |
| .bss | 未初始化/零初始化全局 | 不占 Flash | RAM(启动清零) |
| .sdata | 小数据(≤N字节,gp寻址) | Flash | RAM |
| .sbss | 小 bss | 不占 Flash | RAM |
| .init/.fini | 构造/析构函数表 | Flash | — |
| .eh_frame | 异常展开表(关异常可去) | Flash | — |
3.2 RISC-V 特有段
- .sdata/.sbss:小数据段,gp 相对寻址(±2KB),2 条指令变 1 条
- .srodata:小只读数据(部分工具链)
.text.init:启动代码,链接脚本常放最前.init_array:C++ 全局对象构造函数指针表
3.3 -msmall-data-limit
-msmall-data-limit=8# ≤8 字节的全局/静态放 .sdata/.sbss- N 别太大(gp 范围 ±2KB,超了寻址不到)
- 省 Flash 代码(gp 寻址省指令),不直接减 SRAM 总量
3.4 看段大小和符号
# 段总览riscv64-unknown-elf-size firmware.elf# 段详细riscv64-unknown-elf-size-Afirmware.elf# 符号大小排序(找大头)riscv64-unknown-elf-nm-S--size-sort firmware.elf|tail-30# 反汇编某段riscv64-unknown-elf-objdump-d-j.text firmware.elf# ELF 头信息riscv64-unknown-elf-readelf-hfirmware.elf四、软算术函数族(libgcc)
无硬件扩展时,编译器调用 libgcc 的软实现。
4.1 整数软算术(无 M 扩展)
| 函数 | 对应运算 | 备注 |
|---|---|---|
__mulsi3 | 32位乘法 | 无 M |
__muldi3 | 64位乘法 | 32位核做64位乘 |
__divsi3 | 有符号32位除 | 无 M |
__udivsi3 | 无符号32位除 | — |
__modsi3 | 有符号余数 | — |
__umodsi3 | 无符号余数 | — |
__divdi3 | 64位除 | — |
__udivdi3 | 64位无符号除 | — |
__moddi3 | 64位余数 | — |
4.2 浮点软算术(无 F/D 扩展)
| 函数 | 运算 |
|---|---|
__mulsf3/__muldf3 | 单/双精度乘 |
__addsf3/__adddf3 | 加 |
__subsf3/__subdf3 | 减 |
__divsf3/__divdf3 | 除 |
__negsf2/__negdf2 | 取负 |
__floatsisf/__floatsidf | 整数→浮点 |
__fixsfsi/__fixdfsi | 浮点→整数 |
__extendsfdf2 | 单→双 |
__truncdfsf2 | 双→单 |
4.3 ARM vs RISC-V 软算术名
| ARM | RISC-V | |
|---|---|---|
| 32位除 | __aeabi_idiv | __divsi3 |
| 64位除 | __aeabi_ldivmod | __divdi3 |
| 单精度乘 | __aeabi_fmul | __mulsf3 |
4.4__moddi3优化
嵌入式视角:
__moddi3优化的底层
sprintf 的%U(无符号64位)内部调__moddi3做"除10取余"逐位输出。原始__moddi3是通用 64 位除法,慢。可优化成"特化除10"用移位加法,快几倍。这是把通用软算术替换成特化实现的标准优化,完全建立在你懂工具链软算术这层上。
五、启动 runtime:_start → main
链接脚本ENTRY(_start)指定入口,_start 后的 runtime 流程:
_start(启动汇编) │ ├─ sp/gp/.data/.bss/mtvec(⑦的5件事) │ ├─ (可选)__libc_init_array ← 调用 .init_array 里的 C++ 构造函数 │ 和 C 的 __attribute__((constructor)) 函数 │ ├─ call main │ └─ main 返回 → exit(或死循环)5.1 __libc_init_array
// newlib 提供的,遍历 .init_array 调每个函数指针void__libc_init_array(void){for(size_ti=0;i<__init_array_end-__init_array_start;i++){__init_array_start[i]();}}- C++ 全局对象的构造函数在这里调
- C 的
__attribute__((constructor))也在这 - 必须在 .data/.bss 初始化之后、main 之前
谁会被放进 .init_array
| 来源 | 示例 |
|---|---|
| C++ 全局对象构造函数 | MyClass globalObj;→ 构造函数被放入 |
__attribute__((constructor)) | 用户手动标记的初始化函数 |
| 动态初始化全局变量 | int x = func();→ 编译器生成构造函数放入 |
| C++ 局部静态变量(部分实现) | 首次使用时构造 |
5.2 C++ 全局对象的代价
- 每个全局对象:.bss 占实例 + .init_array 占构造函数指针 + 构造函数代码
__libc_init_array遍历调用,启动变慢- 建议"全局对象 → 全局 POD + 显式 init()",就是为了避开这层代价
用"零初始化成本的全局数据 + 手动调用 init()“替代"带构造函数/析构函数的全局对象”,从而彻底绕开 __libc_init_array 的遍历开销和跨编译单元初始化顺序的不确定性。
| 维度 | 全局对象(隐式构造) | 全局 POD + 显式 init() |
|---|---|---|
| 启动速度 | __libc_init_array逐个调用构造函数,对象越多越慢 | 无.init_array遍历,启动几乎零开销 |
| 初始化顺序 | 跨编译单元顺序未定义,容易踩坑 | 在main()中手动控制,顺序 100% 确定 |
| 可预测性 | 构造函数中若依赖其他全局对象,可能读到垃圾值 | 依赖关系由调用顺序保证,不会出错 |
| 调试难度 | 崩溃堆栈指向构造函数,难以定位是哪个全局对象 | 崩溃堆栈指向显式init()调用,一目了然 |
此处不展开
5.3 exit
void_exit(intstatus){while(1);// MCU 通常死循环}- MCU 上 main 不应返回,返回了就死循环
- newlib 的 exit 会调 atexit + 析构,MCU 通常不需要
六、链接脚本实战要点
6.1 关键符号
| 符号 | 用途 |
|---|---|
_start | ENTRY,启动入口 |
_stack_top | 栈顶(sp 初始值) |
__global_pointer$ | gp 值(.sdata 中点) |
_sidata | .data 的 LMA(Flash 源) |
_sdata/_edata | .data 的 VMA(RAM) |
_sbss/_ebss | .bss 范围 |
__heap_start/__heap_end | 堆范围 |
6.2 .data 的 AT 语法
.data : { _sdata = .; *(.data .data.*) _edata = .; } > RAM AT > FLASH _sidata = LOADADDR(.data);> RAM:VMA 在 RAM运行AT > FLASH:LMA 在 Flash加载LOADADDR(.data):取 LMA,启动汇编从这拷贝
6.3 内存区定义
MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K ROM (rx) : ORIGIN = 0x1FFF0000, LENGTH = 32K }6.4 gc-sections 配合
-ffunction-sections -fdata-sections# 每函数/变量单独段-Wl,--gc-sections# 链接器丢未引用段链接脚本里.text : { *(.text.*) }会自动收集,gc 掉未用的。
七、调试工具速查
# 反汇编riscv64-unknown-elf-objdump-dfirmware.elf>disasm.txt riscv64-unknown-elf-objdump-d-Mno-aliases firmware.elf# 不显示伪指令# 看某函数riscv64-unknown-elf-objdump-dfirmware.elf|grep-A50"<func>:"# 地址→代码行(死机调试)riscv64-unknown-elf-addr2line-efirmware.elf 0x08001234# 符号表riscv64-unknown-elf-nm firmware.elf|grepfunc riscv64-unknown-elf-nm-S--size-sort firmware.elf|tail-30# 段大小riscv64-unknown-elf-size firmware.elf riscv64-unknown-elf-size-Afirmware.elf# ELF 信息riscv64-unknown-elf-readelf-hfirmware.elf# 头riscv64-unknown-elf-readelf-Sfirmware.elf# 段表riscv64-unknown-elf-readelf-lfirmware.elf# 程序头# 转 bin/hexriscv64-unknown-elf-objcopy-Obinary firmware.elf firmware.bin riscv64-unknown-elf-objcopy-Oihex firmware.elf firmware.hex# 看 relaxation 效果(对比 -mrelax vs -mno-relax 的 size)八、ARM vs RISC-V 工具链对照
| ARM(arm-none-eabi) | RISC-V(riscv64-unknown-elf) | |
|---|---|---|
| 软除法名 | __aeabi_idiv | __divsi3 |
| Linker relaxation | 无 | 有(独有) |
| 小数据段 | 无 | .sdata/.sbss + gp |
| 启动入口 | 复位向量(向量表) | _start(ENTRY) |
| 初始 SP | 硬件从向量表加载 | 软件设 |
| ELF 段 | 基本相同 | 多 .sdata/.sbss |
| 工具前缀 | arm-none-eabi- | riscv64-unknown-elf- |
九、本篇小结
- RISC-V 工具链:gcc 驱动 cc1/as/ld + libgcc + libc + libm
- linker relaxation 是 RISC-V 独有:链接时把 lui+addi 压成 gp 相对、auipc+jalr 压成 jal,省代码
- relaxation 改变代码大小和地址,栈回溯/gp 设值要处理
- ELF 段:.text/.rodata/.data/.bss + RISC-V 特有 .sdata/.sbss(gp 寻址)
- 软算术函数族:无 M 则
__divsi3等,无 F/D 则__mulsf3等(名字和 ARM 的__aeabi_*不同) - 启动 runtime:_start → __libc_init_array → main
- 调试:objdump/addr2line/nm/size/readelf 标配
速查表
| 想干啥 | 命令 |
|---|---|
| 反汇编 | objdump -d |
| 地址定位代码 | addr2line -e elf 0xADDR |
| 找大符号 | nm -S --size-sort |
| 看段大小 | size -A |
| 关 relaxation | -mno-relax |
| 看 relax 前后 | 对比 size |
| 转 bin | objcopy -O binary |
| 64位除法软实现 | __divdi3(可固化) |
| 单精度乘软实现 | __mulsf3 |
| C++ 构造在 | __libc_init_array |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的点赞 👍和收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在评论区 💬留言,我们一起探讨。也欢迎关注 👀我,一起交流底层开发的那些事儿。