RT-Thread / RT-Thread/rt-thread
[Bug] [Smart][musl] 内核态调用 musl `fopen` 因 syscall 号位错配而失败,finsh 按键无反应
Nobody has claimed this yet.
- Dominant language
- C
- Stars
- 12.2k
- Forks
- 5.4k
- Avg merge
- 4d 12h
- Merged PRs (30d)
- 40
Description
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_openat ← musl 的 fopen 走的就是这个 |
| 57 | sys_shmat |
__NR_close |
| 58 | sys_shmdt |
__NR_read |
3. 错配的发生路径
musl 的 fopen() 用 Linux ABI 发 syscall 0、a7=56,期待返回一个 fd。但 RT-Thread 内核态 syscall_handler 用 a7 去查自己的 sys_call_table[56],结果命中 sys_shmrm——一个删除共享内存的函数。传入的设备路径指针被当成 shm id,要么直接返回错误,要么把不该删的东西删了。fopen 拿到非 0 返回值,判断为失败,于是 std_console = NULL,stdio 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.c 走 RT_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.c 里 RT_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.c 的 sys_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、用户应用、测试程序都要同步迁移。
建议
-
短期:采用方案 A,在
stdio.cmusl 分支顶部加一条 WARNING 注释,点名"内核态勿调 musl libc 会发 syscall 的接口(fopen/malloc/printf/pthread_*)",收敛隐患面。 -
中期:审计
git grep -n 'RT_USING_MUSLLIBC'的所有命中点,确认哪些是类型定义(如iovec、itimerspec,无风险)、哪些是内核态调 musl libc 接口(有风险)。对后者,要么改用内核强符号,要么像 newlib 那样在components/libc/compilers/musl/syscalls.c里补一套内核态桩层(_open_r/_read_r/_write_r/...)。 -
长期:如果 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弃用 muslfopen,改用内核 dfs_posixopen() - 关键代码位置:
components/libc/posix/io/stdio/stdio.c(RT_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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with components/libc/posix/io/stdio/stdio.c and compare its RT_USING_MUSLLIBC branch with components/libc/compilers/newlib/syscalls.c; then inspect components/lwp/lwp_syscall.c around entries 55–58. Reproduce the LS2K300 Smart configuration with the musl toolchain and verify that the console opens successfully and finsh accepts keyboard input without a page fault.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c
- Domain
- embedded-iot, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100