第一章:RISC-V平台Go cgo调用失败的典型现象与诊断框架
在 RISC-V 架构(如 QEMU 模拟的 rv64gc 或香山/曳影等硬件平台)上启用 cgo 时,Go 程序常出现静默崩溃、SIGSEGV、runtime/cgo: pthread_create failed 或链接阶段 undefined reference to 'xxx' 等非预期行为。这些失败往往并非代码逻辑错误,而是源于 ABI 不匹配、工具链缺失或运行时环境约束。
典型失败现象
- Go 进程启动即 panic,日志显示
fatal error: unexpected signal during runtime execution,且signal SIGSEGV地址位于_cgo_init或pthread_create调用路径; go build -buildmode=c-shared成功,但加载.so时dlopen()返回undefined symbol: __cxa_thread_atexit_impl;CGO_ENABLED=1 go run main.go报错gcc: error: unrecognized command-line option '-march=rv64gc',表明 GCC 未正确识别目标架构;- 使用 musl libc(如
riscv64-linux-musl-gcc)编译 C 代码后,Go 链接时报relocation R_RISCV_PCREL_HI20 against symbol 'xxx' can not be used when making a shared object。
必备诊断工具链检查
确保以下组件版本兼容并显式指定:
# 检查交叉工具链是否支持 RISC-V GNU ABI(非 bare-metal)
riscv64-unknown-linux-gnu-gcc --version # 应 ≥ 12.2.0,含 `-mabi=lp64d` 支持
go version # Go ≥ 1.21(原生 RISC-V 支持稳定)
go env GOOS GOARCH CGO_ENABLED # 必须为 linux/riscv64/1
最小可复现诊断流程
- 创建
main.go:package main /* #include <stdio.h> void hello() { printf("Hello from C!\n"); } */ import "C" func main() { C.hello() } - 执行:
CGO_ENABLED=1 CC=riscv64-unknown-linux-gnu-gcc go run -ldflags="-v" main.go
观察链接器输出中是否含libgcc和libc的 RISC-V 符号解析过程;若失败,-ldflags="-v"将暴露缺失的--sysroot或--dynamic-linker路径。
关键依赖对照表
| 组件 | 推荐值 | 检查命令 |
|---|---|---|
| C 标准库 | glibc ≥ 2.35 或 musl ≥ 1.2.4 | riscv64-unknown-linux-gnu-readelf -d /path/to/libc.so \| grep NEEDED |
| Go 构建标志 | GOOS=linux GOARCH=riscv64 |
go env GOOS GOARCH |
| 动态链接器路径 | /lib/ld-linux-riscv64-lp64d.so.1 |
riscv64-unknown-linux-gnu-gcc -print-sysroot + 查找 ld-linux* |
所有诊断必须在目标环境(QEMU 或真机)中复现,模拟器需启用 +ext_zicsr,+ext_zifencei 扩展以满足 Go 运行时原子指令要求。
第二章:底层工具链协同失效类问题
2.1 libgcc链接顺序错位导致_aeabi*符号未解析(理论剖析+RISC-V ld脚本调试实践)
当 RISC-V 工具链链接裸机固件时,若 -lgcc 出现在 --start-group ... --end-group 外部或晚于目标对象文件,链接器无法回溯解析 __aeabi_idiv 等 ABI 辅助符号。
符号依赖关系
/* 链接脚本关键片段 */
SECTIONS {
.text : {
*(.text.startup)
*(.text) /* 早于 libgcc.a 中的 __aeabi_* 定义 */
}
}
此处
.text段未预留对libgcc的前向引用窗口;RISC-Vld默认单遍扫描,__aeabi_idiv被引用后才定义即报undefined reference。
典型修复策略
- ✅ 将
-lgcc置于链接命令末尾(强制后置提供) - ✅ 使用
--undefined=__aeabi_idiv触发符号保留 - ❌ 避免
-Wl,--as-needed干扰隐式依赖
| 场景 | 链接命令片段 | 是否触发错误 |
|---|---|---|
| 错位 | main.o -lc -lgcc |
否(libgcc 在后) |
| 错位 | -lgcc main.o -lc |
是(引用时未见定义) |
riscv64-unknown-elf-gcc -T link.ld -o fw.elf main.o -lc -lgcc
-lgcc必须位于所有引用其符号的目标文件之后,因 RISC-Vld不支持循环依赖自动解耦。
2.2 RISC-V GCC多版本混用引发libgo与libgcc ABI不兼容(ABI差异分析+nm/objdump交叉验证)
当项目中同时链接 GCC 12 编译的 libgo.a 与 GCC 13 编译的 libgcc.a,运行时出现 undefined symbol: __riscv_save_56 —— 这是典型的跨版本 ABI 断裂。
根本诱因:寄存器保存约定变更
GCC 13 引入新 ABI 扩展(zicsr+zifencei 下的 __riscv_save_N 系列符号),而 GCC 12 仅提供 __riscv_save_32。libgo(Go 运行时)调用该符号进行协程栈切换,但 libgcc 未导出匹配版本。
交叉验证命令链
# 检查 libgo 依赖的符号
nm -C libgo.a | grep riscv_save
# 输出:U __riscv_save_56
# 检查 libgcc 提供的符号
nm -C libgcc.a | grep riscv_save
# 输出:T __riscv_save_32
U 表示未定义引用,T 表示已定义文本段符号——二者位宽不匹配即 ABI 不兼容。
关键 ABI 差异对照表
| 特征 | GCC 12 | GCC 13 |
|---|---|---|
| 默认 save size | 32 reg (x1–x31) | 56 reg (x1–x55) |
-mabi 默认值 |
ilp32d | ilp32d + zicsr 扩展 |
graph TD
A[libgo.o calls __riscv_save_56] --> B{libgcc.a contains?}
B -->|No| C[Linker error: undefined reference]
B -->|Yes| D[Runtime OK]
2.3 -march/-mabi参数未全局对齐造成cgo对象段属性冲突(指令集语义解析+readelf -S实证)
当 Go 项目混合 C 代码(cgo)时,若 Go 编译器与 C 编译器使用的 -march(如 x86-64-v3)或 -mabi(如 sysv vs gnu)不一致,目标文件 .text 和 .rodata 段将携带不同 SHF_X86_64_LARGE 或 ABI 特化标志,导致链接期段合并失败。
指令集语义差异实证
# 查看 cgo 生成的 .o 文件段属性
readelf -S hello.o | grep -E "(Name|flags)"
输出中可见 SHF_X86_64_LARGE 标志仅存在于启用 x86-64-v3 的 C 对象中,而 Go 默认生成的 .text 无此标志。
关键冲突点对比
| 段名 | C 对象(-march=x86-64-v3) | Go 对象(默认) | 冲突类型 |
|---|---|---|---|
.text |
AX + SHF_X86_64_LARGE |
AX |
段属性不兼容 |
.rodata |
A + SHF_X86_64_LARGE |
A |
链接器拒绝合并 |
修复路径
- 统一构建链:
CGO_CFLAGS="-march=x86-64-v3 -mabi=sysv" - 或禁用扩展:
GOAMD64=v3(Go 1.21+)与 C 编译器对齐
graph TD
A[Go源码] -->|go build| B[Go对象:默认ABI]
C[C源码] -->|gcc -march=x86-64-v3| D[C对象:v3+LARGE]
B --> E[链接器ld]
D --> E
E -->|段标志冲突| F[link: error: section attributes mismatch]
2.4 RISC-V ELF重定位类型(R_RISCV_CALL/R_RISCV_PCREL_HI20)在cgo stub中被错误裁剪(重定位表逆向追踪+patchelf注入测试)
cgo生成的stub目标文件常因--gc-sections误删含R_RISCV_PCREL_HI20的.rela.dyn节,导致链接时auipc+jalr跳转地址错位。
重定位截断现象
# objdump -dr libfoo.a(_cgo_export.o) | grep -A2 'R_RISCV_PCREL_HI20'
12: 0000000000000000 auipc t0,0x0 # R_RISCV_PCREL_HI20 .text.func
13: 0000000000000000 jalr ra,0(t0) # R_RISCV_PCREL_LO12_I .text.func
→ auipc的HI20重定位项被strip后,链接器无法修正高位偏移,跳转目标变为零页。
patchelf复现实验
| 工具 | 操作 | 结果 |
|---|---|---|
patchelf --remove-section .rela.dyn |
强制移除重定位节 | 运行时SIGSEGV于stub入口 |
readelf -r |
验证R_RISCV_CALL是否残留 | 仅LO12存在,HI20丢失 |
修复路径
- 编译时添加
-Wl,--no-gc-sections保全stub节; - 或用
objcopy --keep-section=.rela.dyn显式保留重定位表。
2.5 Go linker未识别RISC-V特定section flags(SHF_RISCV_VARIANT_CC等)导致cgo初始化段丢弃(linker源码级断点+–buildmode=plugin复现)
当使用 --buildmode=plugin 构建 RISC-V 目标时,Go linker 因未注册 SHF_RISCV_VARIANT_CC 等 ELF section flag,在 ldelf.go 的 shouldKeepSection 判断中默认返回 false,致使 .init_array 中依赖该 flag 的 cgo 初始化节被静默丢弃。
核心判断逻辑缺陷
// src/cmd/link/internal/ld/ldelf.go:shouldKeepSection
func shouldKeepSection(s *sym.Section) bool {
// 缺失对 RISC-V 特定 flag 的检查 → 此处跳过 SHF_RISCV_VARIANT_CC
if s.Flag&elf.SHF_ALLOC == 0 {
return false
}
return true
}
该函数仅校验 SHF_ALLOC,忽略 SHF_RISCV_VARIANT_CC(值为 0x20000000),导致含此 flag 的可加载节被误判为“非必要”。
影响范围对比
| 场景 | 是否保留 .init_array 条目 |
原因 |
|---|---|---|
amd64 + --buildmode=plugin |
✅ | 无 RISC-V flag,走通用路径 |
riscv64 + --buildmode=plugin |
❌ | SHF_RISCV_VARIANT_CC 触发误过滤 |
复现关键步骤
- 在
ldelf.go:shouldKeepSection设置断点 - 运行
go build -buildmode=plugin -ldflags="-v" -o p.so . - 观察
s.Flag值含0x20000000但返回false
graph TD
A[读取ELF Section] --> B{has SHF_ALLOC?}
B -->|No| C[丢弃]
B -->|Yes| D{is RISC-V variant flag?}
D -->|Missing check| C
D -->|Fixed| E[保留并链接]
第三章:内存模型与段布局异常
3.1 attribute((section(“.rodata.cst16”)))在RISC-V上因16字节对齐强制触发非法地址访问(段对齐语义+GDB watchpoint内存越界捕获)
RISC-V 的 .rodata.cst16 段要求严格 16 字节对齐,否则 lw/flw 等指令在访问未对齐常量时可能触发 misaligned address 异常(scause=0x7)。
数据同步机制
GCC 插入 .balign 16 指令确保段起始对齐,但若链接脚本未显式约束段地址边界,或 .rodata.cst16 被合并进非对齐 .rodata 区域,则实际加载地址可能违反对齐契约。
// 示例:隐式越界风险
static const float coeffs[4] __attribute__((section(".rodata.cst16"))) = {1.0, 2.0, 3.0, 4.0};
// → 编译器生成 .quad 0x3ff0000000000000, ...(8-byte aligned only)
该声明不保证数组首地址 16B 对齐——仅将符号归入 .rodata.cst16 段,而段本身对齐由链接器决定。若段基址为 0x80001234(低4位非零),则 lw t0, 0(a0) 访问 coeffs[0] 将触发非法地址异常。
GDB 捕获路径
启用硬件 watchpoint 可精准定位越界访问点:
| Watchpoint 类型 | 触发条件 | RISC-V CSR |
|---|---|---|
watch *0x80001234 |
地址未对齐读取 | stval = 访问地址 |
catch syscall |
捕获 ecall 进入异常处理 |
scause = 0x7 |
graph TD
A[CPU 执行 lw t0, 0(a0)] --> B{a0 % 16 == 0?}
B -- No --> C[Trap: scause=0x7]
B -- Yes --> D[成功加载]
C --> E[GDB 拦截 stval]
根本解法:在链接脚本中强制段对齐:
.rodata.cst16 : ALIGN(16) { *(.rodata.cst16) }
3.2 .init_array节在RISC-V裸机环境缺少__libc_start_main调度导致cgo init函数静默跳过(启动流程图解+objdump反汇编跟踪)
在RISC-V裸机环境中,_start直接跳转至main,绕过glibc的__libc_start_main——而该函数正是遍历.init_array并调用各INIT_ARRAY条目(含cgo生成的_cgo_init)的唯一入口。
启动流程关键差异
# objdump -d hello.elf | grep -A5 "<_start>:"
0000000080000000 <_start>:
80000000: 4505 li a0,0
80000002: 00000097 auipc ra,0x0
80000006: 000080e7 jalr ra,0(ra) # → 直接 call main
此反汇编表明:无call __libc_start_main指令,.init_array未被扫描。
.init_array未触发的后果
- cgo初始化函数(如
_cgo_setenv,_cgo_notify_runtime_init_done)完全不执行 - Go运行时无法感知C环境就绪,
runtime/cgo相关逻辑处于未定义状态
启动流程对比(mermaid)
graph TD
A[_start] -->|裸机| B[main]
A -->|Linux/glibc| C[__libc_start_main]
C --> D[遍历.init_array]
D --> E[调用_cgo_init等]
| 环境 | .init_array 执行 | cgo init 可用 |
|---|---|---|
| Linux用户态 | ✅ | ✅ |
| RISC-V裸机 | ❌ | ❌ |
3.3 RISC-V S-mode下cgo调用触发PMP违规:.text段被标记为可执行但.data段未同步授权(PMP寄存器dump+OpenSBI日志关联分析)
当cgo从Go协程调用C函数时,若C代码访问全局变量(位于.data段),而PMP仅授权了.text区域(pmp0cfg = 0x1F → R/W/X),则触发illegal instruction异常——实际是PMP access fault被误译为非法指令。
PMP配置失配关键点
.text段(0x80000000)被pmp0覆盖,pmp0cfg=0x1F(TOR + R/W/X).data段(0x80200000)落入pmp1范围,但pmp1cfg=0x00(未启用)
OpenSBI日志片段
[ERROR] trap_handler: scause=0x000000000000000d (store access fault)
sepc=0x80001a2c ← cgo_call_trampoline
stval=0x80201234 ← .data地址
PMP寄存器快照(调试时读取)
| PMPADDR | PMPCFG | 描述 |
|---|---|---|
| 0x80001FFF | 0x1F | .text末地址,TOR模式 |
| 0x00000000 | 0x00 | .data未配置,拒绝所有访问 |
// cgo调用中触发违规的典型模式
extern int global_counter; // ← .data段,PMP未授权
void inc_counter() {
global_counter++; // store access → PMP violation
}
该store指令因.data无PMP读写权限被阻断;OpenSBI将mstatus.MPRV=1下访存失败统一归为access fault,需结合stval与PMPADDR比对定位越界段。
第四章:Go运行时与RISC-V硬件特性耦合缺陷
4.1 Go scheduler在RISC-V原子指令集(A扩展)缺失时误判CAS可用性,致cgo goroutine死锁(atomic.s源码比对+QEMU -d plugin跟踪)
数据同步机制
Go runtime 依赖 runtime/internal/atomic 中的汇编实现判断 CAS 是否可用。RISC-V 架构下,atomic_cmpxchg64 在 src/runtime/internal/atomic/atomic_riscv64.s 中被条件编译为:
// atomic_riscv64.s(截选)
TEXT runtime∕internal∕atomic·CmpXchg64(SB), NOSPLIT, $0-24
// 若未定义 GOARCH_RISCV64_A,跳转至 fallback
#ifdef GOARCH_RISCV64_A
amoswap.d a2, a1, (a0) // A扩展支持:原子交换
#else
JMP runtime∕internal∕atomic·CmpXchg64_fallback(SB)
#endif
该分支逻辑依赖构建时宏 GOARCH_RISCV64_A,但 scheduler 在运行时仅检查 cpu.ArchFeatures&cpu.ArchFeatureA != 0 —— 而 QEMU RISC-V 模拟器默认不暴露 misa.A 位,导致 runtime.osinit() 误设 cpu.ArchFeatureA = false,却仍加载了含 amoswap.d 的 atomic.s。
死锁触发路径
- cgo 调用进入
entersyscall→ 尝试atomic.Cas64(&m.lock, 0, 1) - 实际执行
amoswap.d→ trap 到非法指令异常(因为硬件无A扩展) - 异常 handler 未注册,goroutine 挂起且无法被调度器回收
关键差异对比
| 检查位置 | 依据来源 | 是否可动态修正 |
|---|---|---|
构建期 GOARCH_RISCV64_A |
make.bash 环境变量 |
❌ 编译即固化 |
运行期 cpu.ArchFeatureA |
misa CSR 读取 |
✅ 但 QEMU 未模拟 |
graph TD
A[cgo调用] --> B{atomic.Cas64}
B -->|A扩展缺失| C[amoswap.d trap]
C --> D[信号未捕获]
D --> E[goroutine stuck in _Gsyscall]
4.2 RISC-V CMO(Cache Management Operation)指令未在cgo回调前后显式刷新,引发数据缓存不一致(cache coherency模型推演+clflush模拟验证)
数据同步机制
RISC-V 的 cbo.clean/cbo.flush 指令需显式插入 cgo 调用边界,否则因 I/D cache 分离与弱序内存模型,Go runtime 线程可能读取 stale 缓存行。
关键验证代码
// asm.s: 在 cgo 函数入口插入 clean+flush
cbo.clean a0, zero // 清理数据缓存行(write-back)
cbo.flush a0, zero // 使该行失效(invalidate)
a0为待同步地址;zero表示无偏移;cbo.clean确保脏数据回写至下一级缓存/内存,cbo.flush防止后续读取旧副本——二者缺一不可。
模拟对比表
| 场景 | 是否插入 CMO | 观察到的 cache coherency 行为 |
|---|---|---|
| 无刷新 | ❌ | Go goroutine 读取 stale 值(100% 复现) |
| 仅 clean | ⚠️ | 写后读可能仍命中旧 clean 行(未 invalidate) |
| clean + flush | ✅ | 严格满足 RVWMO 模型要求 |
coherency 推演流程
graph TD
A[cgo 进入] --> B[CPU0 执行 cbo.clean]
B --> C[CPU0 执行 cbo.flush]
C --> D[共享内存更新可见性提升]
D --> E[CPU1 重新加载最新值]
4.3 Go runtime.sigtramp未适配RISC-V中断栈帧布局,导致cgo信号处理崩溃(trap frame结构体对比+gdb register dump分析)
RISC-V的mcause/mtval与sepc寄存器在异常进入时自动压入栈,但Go的runtime.sigtramp仍按x86-64约定解析栈顶为ucontext_t,造成sigaltstack切换后SP偏移错位。
trap frame结构差异
| 字段 | x86-64(Go期望) | RISC-V Linux(实际) |
|---|---|---|
| PC位置 | uc_mcontext.gregs[REG_RIP] |
uc_mcontext.__gregs[REG_PC] |
| 栈指针偏移 | +128 bytes | +0 bytes(sp即__gregs[REG_SP]) |
GDB现场关键片段
(gdb) info registers mepc mcause mtval sp
mepc 0x1234567890 0x1234567890
mcause 0x0000000000000005 # interrupt=5 (timer)
mtval 0x0000000000000000
sp 0xffff800012345000 # real RISC-V SP, not sigtramp's assumed offset
根本原因流程
graph TD
A[Signal arrives in cgo] --> B[RISC-V trap enters kernel]
B --> C[Kernel builds ucontext_t with __gregs layout]
C --> D[Go sigtramp reads SP+128 → garbage]
D --> E[corrupted *siginfo_t → segfault]
4.4 RISC-V vector扩展(V扩展)启用时,cgo调用破坏浮点/向量寄存器保存约定(calling convention RFC解读+rvv-asm插桩观测)
RISC-V V扩展引入 v0–v31 向量寄存器,但现行 cgo 调用链未遵循 RISC-V Vector Calling Convention RFC,默认不保存 v 寄存器。
寄存器保存责任错位
- Go runtime 仅按
F扩展约定保存f0–f31,忽略v* - C 函数修改
v4后返回,Go 汇编恢复时未重载该寄存器 → 数据污染
rvv-asm 插桩观测证据
# 在 cgo 入口插入(使用 riscv64-linux-gnu-gcc -march=rv64gcv)
csrr t0, vlenb # 获取 vlenb=64(512-bit)
vsetvli zero, a0, e32,m8 # 设置 vtype
vmv.v.i v4, 42 # clobber v4 —— Go 侧无对应 save/restore
此指令在 C 函数中执行后,Go 的后续向量化计算因
v4残留值产生越界访存。vlenb和vtypeCSR 状态亦未被 cgo ABI 保护。
| 寄存器类 | ABI 角色 | cgo 是否保存 | 后果 |
|---|---|---|---|
f0–f7 |
caller-saved | ✅ | 无影响 |
v0–v7 |
caller-saved (per RFC) | ❌ | 静默损坏 |
v8–v31 |
callee-saved | ❌ | 同上 |
graph TD
A[cgo Call] --> B{ABI Check}
B -->|Missing v-reg save| C[Vector Context Loss]
C --> D[Go RVV Loop Crash]
第五章:面向RISC-V的cgo健壮性工程化建议
内存对齐与ABI兼容性保障
RISC-V 64位平台(如rv64gc)默认遵循LP64D ABI,但部分嵌入式Linux发行版(如Buildroot生成的toolchain)可能启用-mabi=lp64f或-mabi=lp64d变体。cgo调用C函数时若结构体含float64字段而C侧未显式对齐,易触发SIGBUS。实测案例:某边缘AI推理库在QEMU+Debian RISC-V镜像中崩溃,根源是Go结构体type Input { Data *[1024]float64 }未加//go:align 16,而C侧struct input_t使用__attribute__((aligned(16)))。修复后通过unsafe.Offsetof()校验所有字段偏移量与_Alignof(float64)一致。
跨架构符号解析防御机制
RISC-V工具链(如riscv64-linux-gnu-gcc 12.2.0)默认不导出.symtab调试符号,导致cgo在-buildmode=c-shared下动态链接失败。工程化方案需在构建脚本中强制注入符号表:
riscv64-linux-gnu-gcc -Wl,--export-dynamic -Wl,--dynamic-list-data \
-shared -o libgo.so go.o c_wrapper.o
同时在Go代码中添加运行时校验:
func init() {
if runtime.GOARCH != "riscv64" {
panic("cgo bindings only support RISC-V 64-bit")
}
}
异常传播路径隔离设计
RISC-V的S-mode异常处理与x86-64差异显著——C函数内发生的SIGSEGV无法被Go的recover()捕获。某实时控制项目中,C侧FFT库因传入空指针触发SIGSEGV,导致整个goroutine崩溃。解决方案采用双进程沙箱:主进程通过os/exec启动C计算子进程,通过unix.SocketPair传递fd进行零拷贝数据交换,并设置syscall.Setrlimit(syscall.RLIMIT_CORE, &syscall.Rlimit{Cur: 0})禁用core dump。
构建时交叉验证矩阵
| 工具链版本 | Go版本 | CFLAGS | 是否通过cgo test |
关键问题 |
|---|---|---|---|---|
| riscv64-elf-gcc 11.2 | go1.21.6 | -O2 -march=rv64imafdc |
✅ | 无 |
| riscv64-linux-gnu-gcc 13.1 | go1.22.0 | -O0 -mabi=lp64d |
❌ | runtime·memclrNoHeapPointers符号未定义 |
该矩阵驱动CI流水线自动执行make check-riscv-cgo,覆盖QEMU模拟器与SiFive Unmatched开发板双环境。
运行时栈深度监控
RISC-V 64位ABI规定调用者负责分配16字节的red zone,但Go runtime在goroutine栈切换时未预留此空间。当C函数递归调用深度>128层时,触发栈溢出。部署级防护策略:在main.init()中注册runtime.SetMaxStack(8<<20),并用pprof.Lookup("goroutine").WriteTo()定期采样栈帧,结合perf record -e riscv_csrs/insn_retired/分析指令级栈消耗热点。
链接器脚本定制化实践
标准ld脚本在RISC-V上未正确处理.got.plt段重定位,导致cgo动态符号解析失败。某工业网关固件项目中,通过自定义riscv64.ld强制插入PROVIDE(__global_pointer$ = . + 0x800);,并在C头文件中声明extern char __global_pointer$;,确保全局指针寄存器gp初始化正确。该方案经OpenSBI 1.3+U-Boot 2023.04验证通过。
错误码语义映射表
RISC-V Linux内核返回的errno值与x86-64存在微小差异(如EDEADLOCK在riscv64中为45而非35)。在cgo封装层建立双向映射:
var riscvErrno = map[int]int{
35: 45, // x86 EDEADLOCK → riscv EDEADLOCK
112: 112, // ENOTRECOVERABLE 保持一致
}
该映射表由scripts/gen-errno.go从/usr/riscv64-linux-gnu/include/asm/errno.h自动生成,避免硬编码维护风险。
