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.c musl 分支顶部加一条 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 弃用 musl fopen,改用内核 dfs_posix open()
- 关键代码位置:
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
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。启动日志关键片段:
看起来控制台切换成功,但
rt_posix_stdio_set_console内部调用 musl 的fopen("/dev/ttyS0", "r+")永远失败,返回的fp为 NULL,导致stdio fd = -1,finsh 读不到任何输入。机理分析
1. musl 的
fopen永远发 Linux ABI syscallmusl 的
fopen链路:musl 给
open的 syscall 号 =__NR_openat= 56(标准 Linux loongarch64 ABI)。这是 musl 作为"有宿主 libc"的固有设计——它预设自己运行在 Linux 内核之上,所有系统接口都走标准 Linux syscall ABI。2. RT-Thread 的
sys_call_table号位是自创的,不是 Linux ABIcomponents/lwp/lwp_syscall.c:11040-11046:两边号位语义完全不同:
sys_shmget__NR_fcntlsys_shmrm__NR_openat← musl 的fopen走的就是这个sys_shmat__NR_closesys_shmdt__NR_read3. 错配的发生路径
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
open = __NR_openat = 56在 Linux loongarch64 上是稳定契约。stdio.c这段rt_posix_stdio_set_console是内核自身初始化控制台的代码,却在RT_USING_MUSLLIBC分支里调了 musl 的fopen,等于让内核去走一个面向用户态的 libc 接口。跨架构对比:这是共性问题,不是 LoongArch 独有
各 libc 分支的踩坑风险
components/libc/posix/io/stdio/stdio.c有三条分支:RT_USING_NEWLIBC_open_r等全套RT_USING_MUSLLIBC__errno_location#elseopen)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-Threadsys_call_table号位错配。master 分支上 10 个 Smart BSP 的情况
riscv64-unknown-linux-musl-riscv64-unknown-linux-musl-riscv64-unknown-linux-musl-riscv64-unknown-linux-musl-riscv64-unknown-linux-musl-riscv64-unknown-elf-arm-none-eabi-aarch64-none-elf-aarch64-none-elf-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/loongarch/ls2k300_devCONFIG_RT_USING_SMART=y+ musl 工具链 (loongarch64-unknown-linux-musl-)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的打开。号位错配的问题自然就被绕过了。malloc/printf/pthread_*还会踩坑。隐患面没有收敛。方案 B(中等代价):让 musl 走 RT-Thread syscall 号位
在 musl 移植层覆盖 musl 的
__syscall,把 Linux ABI 号位翻译成 RT-Threadsys_call_table号位:或者在 musl 的
arch/loongarch64/bits/syscall.h.in里直接把__NR_*重定义成 RT-Thread 表里的号位。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 就能直接工作。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"这种约束。参考
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_错配分析.mdOther additional context
No response