本文整理了在 YSYX(一生一芯)项目开发过程中,从 NEMU 模拟器异常处理、Abstract-Machine 适配到 RT-Thread 系统移植遇到的全部典型问题,包含问题现象、根因分析与解决方案,供同路径开发者参考。
一、NEMU 模拟器:RISC-V 异常处理与上下文切换
1. 上下文栈布局错位:汇编与 C 结构体不匹配导致访存崩溃
问题现象 运行多线程调度例程 yield-os 时,程序触发 address out of bound 断言直接崩溃;异常返回后 PC 跳转到 0 地址,通用寄存器值全部错乱。
根因分析 RISC-V 异常处理分为「汇编入口层」和「C 逻辑处理层」两部分,二者通过 Context 结构体传递完整的 CPU 上下文,这是异常处理的核心约定:
- 汇编文件
trap.S是硬件异常的第一入口,负责在栈上保存所有寄存器; - C 文件
cte.c负责异常分发、线程调度,直接读取结构体形式的上下文。
原代码两边的内存布局完全错位:汇编将 32 个通用寄存器放在栈低地址,mcause/mepc/mstatus 等特权寄存器放在高地址;而 C 语言 Context 结构体的成员顺序恰好相反(CSR 在前,gpr 在后)。 这导致 C 代码读取到的异常号、寄存器值全部是错误的内存数据,mepc += 4 也完全没有写到正确位置,最终异常返回时 PC 被恢复成 0,触发非法地址访问崩溃。
解决方案 重写 trap.S,严格对齐 C 结构体的内存布局:
- 栈低地址依次存放
mepc、mcause; - 中间区域存放 32 个通用寄存器;
- 高地址存放
mstatus与虚拟内存指针。 统一偏移量宏定义,确保汇编存取的字节偏移和 C 结构体成员偏移 100% 一致。
2. MMIO 设备未注册:串口访问触发物理内存越界
问题现象 重新编译 NEMU 后运行程序,访问串口地址 0xa00003f8 时报错 address is out of bound of pmem;启动日志中没有任何 Add mmio map 设备注册信息。
根因分析 增量编译导致 System 模式下的设备代码未正确链接,或是配置文件丢失,MMIO 设备映射没有被初始化。 NEMU 的内存访问逻辑是「先匹配 MMIO 设备,再校验物理内存范围」;设备未注册时,串口地址会被当成普通物理内存校验,直接触发越界断言。
解决方案 执行 make distclean 彻底清理所有编译产物与旧配置,用 make defconfig 恢复默认配置,确认 System mode 已开启后全量重新编译 NEMU。
3. etrace 非侵入式异常踪迹实现
问题背景 调试异常时如果直接在 CTE 代码里加 printf,属于侵入式修改,可能改变程序内存布局、执行时序,甚至让 bug 直接消失;需要实现模拟器层面的非侵入式异常追踪。
实现方案
- 在 NEMU 的
isa_raise_intr(异常触发)和isa_mret(异常返回)函数中增加日志埋点; - 用
CONFIG_ETRACE配置宏控制开关,使用 NEMU 标准Log宏输出,和其他 trace 工具保持统一风格; - 在 Kconfig 中将 ETRACE 配置项归入
Testing and Debugging菜单,依赖总开关TRACE,保持配置体系规范。
核心优势 完全不修改客户程序代码,即使异常处理函数跑飞、程序崩溃,也能正常记录异常事件,不会影响 bug 复现。
二、Abstract-Machine:编译与参数注入问题
mainargs 占位符丢失警告
问题现象 编译 AM 应用时出现 Error: placeholder not found! 警告,启动参数注入脚本执行失败。
根因分析 程序启动参数通过脚本修改 ELF 二进制中的占位符字符串实现注入,但编译器的 --gc-sections 垃圾回收优化,把未被直接引用的 mainargs 数组当成死代码回收了,导致脚本在二进制中找不到占位符标记。
解决方案 给 mainargs 数组加上 __attribute__((used)) 属性,强制编译器保留该符号,不被链接优化回收。
三、RT-Thread AM 移植:多应用集成链接错误
1. AM 库符号多重定义
问题现象 执行 make init 阶段,大量 AM 基础函数(trm_init/ioe_init/cte_init 等)报 multiple definition 重复定义错误。
根因分析 集成脚本的中间构建步骤同时做了两件事:直接编译 AM 平台源码,又链接 AM 静态库 am-native.a,同一符号被加载了两次。 这是脚本中间测试步骤的副作用,不影响最终 RT-Thread 镜像的生成,可以直接忽略。
2. hello 应用编译失败:MAINARGS 宏未定义
问题现象 集成 hello 应用时,编译报错 MAINARGS_MAX_LEN、MAINARGS_PLACEHOLDER 未声明。
根因分析 native 平台是宿主 Linux 环境,没有参数注入的二进制修改机制,缺少对应的编译宏定义;而 nemu 等裸机平台的 Makefile 中会预定义这两个宏。
解决方案
- 方案一:在 native 平台的编译选项中补充两个宏的定义,赋予默认空值即可;
- 方案二:暂时从集成列表中移除 hello 应用,优先保证系统主体编译通过。
3. fceux 模拟器移植不完整:海量符号未定义
问题现象 最终链接时报上百个 __am_fceux_am_ 前缀的未定义符号,涵盖 6502 CPU 核心、Mapper 映射、音效处理等核心模块。
根因分析 fceux-am 属于半成品移植项目,缺失 NES 模拟器的核心源码,且依赖自动生成的 roms.h 游戏列表文件,本地没有对应游戏资源无法生成,补全工作量极大。
解决方案 直接从应用集成列表中移除 fceux-am,日常学习调试无需保留。
4. 应用入口符号缺失导致最终链接失败
问题现象 最终链接 RT-Thread 镜像时,报错 undefined reference to __am_hello_main、__am_fceux_am_main。
根因分析 集成脚本会为每个应用生成 MSH 命令包装函数,调用加了前缀的应用入口 __am_xxx_main;但如果对应应用本身编译失败,就不会生成目标文件,入口符号自然不存在。
解决方案 修复问题应用的编译错误,或直接从集成列表中剔除失败应用,只保留可正常编译的 microbench、typing-game、snake。
四、踩坑总结与开发经验
-
底层开发,约定大于编码 汇编与 C 语言的交互,核心是内存布局约定。结构体成员顺序、字节偏移、对齐方式必须 100% 一致,差一个字节都会引发连锁崩溃。
-
增量编译是万恶之源 修改了配置、头文件、链接脚本后,一定要全量清理重编;很多诡异、无法解释的问题,本质都是旧编译产物残留导致的。
-
分层调试,逐层定位 遇到崩溃先分清层级:是模拟器硬件模拟层、AM 抽象层还是上层系统层的问题。从底向上排查,先确认异常处理、内存映射等底层机制正常,再排查上层业务逻辑。
-
非侵入式调试更可靠 调试底层问题优先使用模拟器自带的 trace 工具,少用
printf直接改代码,避免改变程序行为、让 bug 凭空消失,增加排查难度。


被折叠的 条评论
为什么被折叠?



