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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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