Skip to content

[Bug] [Smart][musl] 内核态调用 musl fopen 因 syscall 号位错配而失败,finsh 按键无反应 #11716

Description

@loongson-gz

RT-Thread Version

master

Affected area

RT-Smart

Hardware/BSP vendor

Loongson

Architecture

Not applicable / Other

Board and hardware details

LS2K0300

Develop Toolchain

GCC

Describe the bug

问题现象

LoongArch/LS2K300 启用 RT-Thread Smart 模式 + musl 工具链后,启动到 msh 提示符按键无反应stdio fd 卡在 -1,shell 线程首次 read(stdin) 时还会 page fault。

启动日志关键片段:

[DIAG] console_switch_to_tty: ttyS0=(nil)
[DIAG] ttyS0 not found, console stays uart0
[DIAG] main: before rt_posix_stdio_set_console
[DIAG] main: stdio fd=4

看起来控制台切换成功,但 rt_posix_stdio_set_console 内部调用 musl 的 fopen("/dev/ttyS0", "r+") 永远失败,返回的 fp 为 NULL,导致 stdio fd = -1,finsh 读不到任何输入。

机理分析

1. musl 的 fopen 永远发 Linux ABI syscall

musl 的 fopen 链路:

fopen → __fopenflags → open(path, flags, mode) → __syscall(__NR_openat, ...)

musl 给 open 的 syscall 号 = __NR_openat = 56(标准 Linux loongarch64 ABI)。这是 musl 作为"有宿主 libc"的固有设计——它预设自己运行在 Linux 内核之上,所有系统接口都走标准 Linux syscall ABI。

2. RT-Thread 的 sys_call_table 号位是自创的,不是 Linux ABI

components/lwp/lwp_syscall.c:11040-11046

SYSCALL_USPACE(SYSCALL_SIGN(sys_shmget)), /* 55 */
SYSCALL_USPACE(SYSCALL_SIGN(sys_shmrm)),  /* 56 ← RT-Thread 这里是 sys_shmrm */
SYSCALL_USPACE(SYSCALL_SIGN(sys_shmat)),  /* 57 */
SYSCALL_USPACE(SYSCALL_SIGN(sys_shmdt)),  /* 58 */

两边号位语义完全不同:

syscall 号 RT-Thread 表 Linux loongarch64 ABI
55 sys_shmget __NR_fcntl
56 sys_shmrm __NR_openatmusl 的 fopen 走的就是这个
57 sys_shmat __NR_close
58 sys_shmdt __NR_read

3. 错配的发生路径

musl 的 fopen() 用 Linux ABI 发 syscall 0a7=56,期待返回一个 fd。但 RT-Thread 内核态 syscall_handlera7 去查自己的 sys_call_table[56],结果命中 sys_shmrm——一个删除共享内存的函数。传入的设备路径指针被当成 shm id,要么直接返回错误,要么把不该删的东西删了。fopen 拿到非 0 返回值,判断为失败,于是 std_console = NULLstdio fd = -1

关键在于 LoongArch 的 syscall 0 不区分用户态/内核态——musl 在内核上下文里发的这条 syscall,仍然 trap 进 syscall_handler,按 Linux ABI 号位 56 查 RT-Thread 表,命中 sys_shmrm——错配。

4. 这不是 musl 的 bug,也不是 RT-Thread 的 bug

  • musl 没错:它走标准 Linux syscall ABI 天经地义,open = __NR_openat = 56 在 Linux loongarch64 上是稳定契约。
  • RT-Thread 的 syscall 表也没错:它的编号体系是自洽的,在 lwp 用户态这条主路径上是 work 的(用户态 musl 发的 syscall trap 进内核,按 RT-Thread 自己的表查,号位一致)。
  • 问题出在 musl 被静态链接进内核镜像、在内核上下文被调用这个非预期场景stdio.c 这段 rt_posix_stdio_set_console 是内核自身初始化控制台的代码,却在 RT_USING_MUSLLIBC 分支里调了 musl 的 fopen,等于让内核去走一个面向用户态的 libc 接口。

跨架构对比:这是共性问题,不是 LoongArch 独有

各 libc 分支的踩坑风险

components/libc/posix/io/stdio/stdio.c 有三条分支:

stdio.c 分支 libc 有内核态桩层? 踩坑?
RT_USING_NEWLIBC newlib _open_r 等全套 不踩
RT_USING_MUSLLIBC musl ✗ 只有 __errno_location
#else 无/bare-metal N/A(直接调内核 open 不踩

newlib 为什么不踩: newlib 是"无宿主 libc"——它把所有系统调用都做成可重定向的弱桩(_open_r_read_r_write_r…)。RT-Thread 在 components/libc/compilers/newlib/syscalls.c 里自己实现了这些桩,里面直接调 dfs_file_open(),根本不进 syscall 0 陷阱。

musl 为什么踩: RT-Thread 的 musl 适配层(components/libc/compilers/musl/syscalls.c只有 20 行的 __errno_location,没有像 newlib 那样提供 _open_r/_read_r/_write_r 桩层。musl 的 fopen 直通 Linux syscall,无人拦截,撞上 RT-Thread sys_call_table 号位错配。

master 分支上 10 个 Smart BSP 的情况

BSP 架构 工具链 prefix 内核态调 musl libc?
allwinner/d1 riscv64 riscv64-unknown-linux-musl- 是,潜在踩坑
allwinner/d1s riscv64 riscv64-unknown-linux-musl- 潜在踩坑
bouffalo_lab/bl808/d0 riscv64 riscv64-unknown-linux-musl- 潜在踩坑
cvitek/cv18xx_risc-v riscv64 riscv64-unknown-linux-musl- 潜在踩坑
k230 riscv64 riscv64-unknown-linux-musl- 潜在踩坑
xuantie/virt64/c906 riscv64 riscv64-unknown-elf- 不踩
nxp/imx/imx6ull-smart arm arm-none-eabi- 不踩
raspberry-pi/raspi-dm2.0 aarch64 aarch64-none-elf- 不踩
rockchip/rk3300 aarch64 aarch64-none-elf- 不踩
rockchip/rk3500 aarch64 aarch64-none-elf- 不踩

master 上有 5 个 Smart BSP 用 musl 工具链(d1、d1s、bl808、cv18xx、k230),它们的 stdio.cRT_USING_MUSLLIBC 分支,调 fopen——和 LoongArch/LS2K300 踩的坑完全一样。只是它们没在内核态调 fopen,所以没爆出来。

另外 5 个 Smart BSP 用 bare-metal 工具链*-none-elf-),它们的 stdio.c#else 分支,调内核强符号 open()——不踩坑。

master 分支的 musl 适配层(components/libc/compilers/musl/syscalls.c)仍然只有 20 行的 __errno_location,没有补 _open_r/_read_r/_write_r 内核态桩层——和 LoongArch 修复前的状态一致。

复现环境

  • BSP: bsp/loongarch/ls2k300_dev
  • 配置: CONFIG_RT_USING_SMART=y + musl 工具链 (loongarch64-unknown-linux-musl-)
  • 现象: 启动到 msh 提示符后按键无反应,stdio fd = -1

修复方案

方案 A(最小代价,已采用):内核态绝不调用 musl libc 接口

components/libc/posix/io/stdio/stdio.cRT_USING_MUSLLIBC 分支的 fopen/fileno/fclose 全部换成内核 dfs_posix 强符号(open/close)。

关键在于 RT-Thread 内核里有一份"强符号 open()"——它不是 musl 的弱符号 __open,而是 components/libc/posix/io/ 里直接调 dfs_file_open() 的内核函数。链接器在解析符号时,强符号优先于弱符号,所以内核镜像里这次 open 调用绑定的是 dfs_posix 的强符号实现,根本不发 syscall、不进 sys_call_table 查表,直接在内核态完成 /dev/ttyS0 的打开。号位错配的问题自然就被绕过了。

  • 优点:改动局部、立竿见影、不影响用户态 ABI。
  • 缺点:治标不治本。下次有人在内核态调 musl malloc/printf/pthread_* 还会踩坑。隐患面没有收敛。

方案 B(中等代价):让 musl 走 RT-Thread syscall 号位

在 musl 移植层覆盖 musl 的 __syscall,把 Linux ABI 号位翻译成 RT-Thread sys_call_table 号位:

// 伪代码:musl_glue.c
long __syscall(long n, ...) {
    long rt_nr = linux_to_rt_syscall_nr(n);  // 56 (openat) → RT-Thread 的 sys_open 号位
    return rt_syscall(rt_nr, ...);
}

或者在 musl 的 arch/loongarch64/bits/syscall.h.in 里直接把 __NR_* 重定义成 RT-Thread 表里的号位。

  • 优点:musl 在内核镜像里也能正确工作,号位错配从根上消除。
  • 缺点:musl 用户态 lwp 进程也用同一套号位发 syscall,需要保证 lwp 用户态 trap 进内核后,syscall_handler 查的是同一张已对齐的表。这意味着要么 RT-Thread 改表对齐 Linux ABI,要么 musl 全量改号位——都是牵动多处的改动。

方案 C(根治,最大代价):RT-Thread syscall 表对齐 Linux ABI

components/lwp/lwp_syscall.csys_call_table[] 重排,让号位与 Linux loongarch64 ABI 完全一致(__NR_openat=56__NR_close=57__NR_read=63…)。同时 lwp 用户态 musl 不用任何 glue 就能直接工作。

  • 优点:根治。musl 在用户态和内核态都按标准 Linux ABI 发 syscall,号位天然对齐。未来移植 glibc、其他 Linux 二进制也直接可用。
  • 缺点:这是一次 ABI 级重构——现有 lwp 用户态二进制全部需要重编;sys_call_table 的维护从"RT-Thread 自定义"变成"跟踪 Linux UAPI",长期维护成本上升;所有已经依赖旧号位的 BSP、用户应用、测试程序都要同步迁移。

建议

  1. 短期:采用方案 A,在 stdio.c musl 分支顶部加一条 WARNING 注释,点名"内核态勿调 musl libc 会发 syscall 的接口(fopen/malloc/printf/pthread_*)",收敛隐患面。

  2. 中期:审计 git grep -n 'RT_USING_MUSLLIBC' 的所有命中点,确认哪些是类型定义(如 iovecitimerspec,无风险)、哪些是内核态调 musl libc 接口(有风险)。对后者,要么改用内核强符号,要么像 newlib 那样在 components/libc/compilers/musl/syscalls.c 里补一套内核态桩层(_open_r/_read_r/_write_r/...)。

  3. 长期:如果 RT-Thread Smart 要长期支持 musl/glibc 用户态、跑 Linux 二进制,走方案 C(RT-Thread sys_call_table 对齐 Linux ABI)才是根治。届时 musl 在用户态和内核态都按标准 Linux ABI 发 syscall,号位天然对齐,fopen/open/malloc/pthread 全部 work,不再需要"内核态避开 musl"这种约束。

参考

  • 修复 commit: stdio.c 弃用 musl fopen,改用内核 dfs_posix open()
  • 关键代码位置:
    • components/libc/posix/io/stdio/stdio.cRT_USING_MUSLLIBC 分支)
    • components/lwp/lwp_syscall.c:11040-11046(syscall 表 55~58 号位)
    • components/libc/compilers/musl/syscalls.c(RT-Thread 官方 musl 适配层,仅 20 行,缺桩层)
    • components/libc/compilers/newlib/syscalls.c(newlib 完整桩层 _open_r/_read_r/...,对照参考)
  • 详细分析文档: bsp/loongarch/ls2k300_dev/docs-bak/musl_fopen_syscall_错配分析.md

Other additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    BSPBSP: LoongsonBSP related with LoongsonRT-SmartRT-Thread Smart related PR or issuesbugThis PR/issue is a bug in the current code.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions