拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

【risc-v专栏 09】 工具链深挖:relaxation / ELF / 软算术

【risc-v专栏 09】 工具链深挖:relaxation / ELF / 软算术

本篇定位: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只读数据(常量/字符串)FlashFlash 读
.data已初始化全局/静态Flash(拷贝源)RAM
.bss未初始化/零初始化全局不占 FlashRAM(启动清零)
.sdata小数据(≤N字节,gp寻址)FlashRAM
.sbss小 bss不占 FlashRAM
.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 扩展)

函数对应运算备注
__mulsi332位乘法无 M
__muldi364位乘法32位核做64位乘
__divsi3有符号32位除无 M
__udivsi3无符号32位除—
__modsi3有符号余数—
__umodsi3无符号余数—
__divdi364位除—
__udivdi364位无符号除—
__moddi364位余数—

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 软算术名

ARMRISC-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 关键符号

符号用途
_startENTRY,启动入口
_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
转 binobjcopy -O binary
64位除法软实现__divdi3(可固化)
单精度乘软实现__mulsf3
C++ 构造在__libc_init_array

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的点赞 👍和收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在评论区 💬留言,我们一起探讨。也欢迎关注 👀我,一起交流底层开发的那些事儿。


返回列表