ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

IAR Semihosting + printf 导致程序脱离 J-Link 后崩溃:一次调试环境依赖问题排查

IAR Semihosting + printf 导致程序脱离 J-Link 后崩溃:一次调试环境依赖问题排查 IAR Semihosting printf 导致程序脱离 J-Link 后崩溃一次调试环境依赖问题排查最近在调试 EtherCAT 从站程序时遇到了一个很有意思的问题程序连接 J-Link 调试时运行完全正常但拔掉 J-Link、让设备独立运行以后程序却会异常甚至直接跑飞。这种问题比较容易把排查方向带偏。因为连接调试器 → 正常 断开调试器 → 异常第一反应往往会怀疑供电问题 复位问题 启动时序 Watchdog 优化等级 Debug / Release 差异 JTAG 引脚 Cache但这次最终定位到的原因其实非常简单IAR 工程开启了 Semihosting同时程序中还残留了printf()。连接 J-Link 时调试器会帮助目标程序处理这些主机 I/O 请求因此程序表现正常。断开 J-Link 后目标程序仍然执行 Semihosting 调用但此时已经没有调试器提供服务最终导致程序执行异常。这篇文章把整个问题的背景、Semihosting 的工作机制、为什么“接着调试器正常、拔掉就死”以及正式产品中应该如何处理printf记录下来。1. 问题背景功能开发完成以后在测试过程中出现了一个奇怪现象。连接 J-LinkPC │ J-Link │ RZ/T2L程序可以长时间正常运行。但是拔掉 J-Link只让目标板独立运行RZ/T2L │ 独立运行程序却会异常。最开始怀疑新增加的EEPROM 写入 PHY 访问 ESC 寄存器读取存在问题。但后续逐项排查后发现真正的差异并不在业务代码而在IAR Runtime Library 配置上。2. 最终定位Semihosting 残留 printf检查 IAR 工程配置时发现Project ↓ Options ↓ General Options ↓ Library Configuration中Library low-level interface implementation选择的是Semihosted同时stdout / stderr也是Via semihosting也就是说工程明确告诉 IAR Runtime Library标准输入输出不由 MCU 自己处理而是交给调试器/主机处理。问题就在这里。代码中还残留了一些printf(EtherCAT error...\n);当执行到这些代码时程序并不是简单地CPU → UART而是进入printf ↓ C Runtime Library ↓ low-level I/O ↓ Semihosting ↓ Debugger ↓ PC只要 J-Link C-SPY 还在线这条链路是成立的。一旦调试器被拔掉printf ↓ Semihosting Request ↓ ???问题就出现了。3. 什么是 SemihostingSemihosting 是 Arm 平台常见的一种调试机制。它允许运行在目标 CPU 上的软件借助调试器访问 PC 端资源。Arm 官方将 Semihosting 定义为一种让目标端程序使用主机 I/O 能力的机制。例如目标代码printf(Hello World\n);表面上看只是普通 C 标准库调用。但对于一个没有 UART、没有文件系统的裸机系统来说stdout 到底在哪里如果启用了 Semihosting可以变成Target CPU │ │ Semihosting Request ▼ Debugger │ ▼ Host PC │ ▼ Terminal Window因此目标程序甚至可以在没有 UART 驱动的情况下直接printf(debug %d\n,value);然后在 IAR Terminal I/O 中看到输出。除了标准输出以外Semihosting 还可以支持文件打开 文件读写 字符输入 标准输出 程序退出等功能。4. IAR 中的 Semihosting 实际做了什么IAR 的 DLIB Runtime Library 对底层 I/O 做了一层抽象。例如printf()最终会进入标准库底层stdout ↓ DLIB Low-Level I/O至于真正把字符送到哪里由Library low-level interface implementation决定。从你当前 IAR 工程截图来看原来的配置是LibraryNormal Library low-level interface implementation ● Semihosted stdout/stderr ● Via semihostingIAR 官方文档也说明选择Semihosted后会启用 C-SPY emulated I/O使目标程序的低层 I/O 调用通过调试器完成。因此printf(test\n);实际上产生的是printf() ↓ DLIB ↓ __write / Low-Level I/O ↓ Semihosting ↓ C-SPY / J-Link而不是printf() ↓ UART这一点非常关键。5. 为什么连接 J-Link 时程序完全正常因为这时存在完整的 Semihosting 服务链路RZ/T2L │ │ JTAG/SWD ▼ J-Link │ ▼ IAR C-SPY │ ▼ Host PC当目标程序执行 Semihosting 调用以后调试器可以识别对应的调试事件然后帮助目标程序完成stdout file I/O input等操作再恢复目标 CPU 执行。IAR 对其 C-SPY emulated I/O 的描述也很直观目标调用 DLIB 底层 I/O 后会进入调试器能够识别的服务点由 debugger 执行相应操作完成后继续运行目标程序。所以Debugger Connected ↓ Semihosting Request ↓ Debugger Handles It ↓ Continue Running整个过程没有问题。6. 为什么拔掉 J-Link 就可能崩溃这是整篇文章最关键的地方。Semihosting 的设计前提之一就是有调试环境参与。Arm 的 Semihosting 机制会通过特殊的调试陷阱进入 debugger service具体使用哪一种指令和异常机制与目标架构及实现有关例如 Arm 文档中的某些 Semihosting 实现会使用BKPT。于是程序运行时可能形成printf() ↓ Semihosting Trap ↓ 等待 Debugger连接调试器时Trap ↓ Debugger 捕获 ↓ 执行 Host I/O ↓ 返回目标程序没有调试器时Trap ↓ 没有 Debugger 响应 ↓ 异常 / 停滞 / Fault至于具体表现是HardFault Undefined Instruction 异常处理 程序卡死 跑飞取决于CPU 架构 Semihosting 实现 异常向量配置 Runtime Library Debugger 机制因此不能简单认为printf 只是打印慢一点在启用了 Semihosting 的裸机系统中它实际上可能改变CPU 控制流7. 这次问题真正的因果链把整个问题串起来IAR 工程开启 Semihosting ↓ 代码中残留 printf() ↓ printf 进入 DLIB Low-Level I/O ↓ 触发 Semihosting ↓ 连接 J-Link 时 ↓ C-SPY 正常处理 ↓ 程序正常但拔掉调试器以后代码执行 printf() ↓ 进入 Semihosting ↓ 此时没有 Debugger ↓ Semihosting 请求无法正常完成 ↓ 程序异常所以最终出现接 J-Link正常 拔 J-Link崩溃8. 第一个解决方案正式版本关闭 Semihosting这也是这次最终采用的方案。IARProject ↓ Options ↓ General Options ↓ Library Configuration把Library low-level interface implementation从Semihosted修改成None修改以后Library low-level interface implementation ● None ○ Semihosted ○ IAR breakpoint意味着正式程序不再依赖调试器提供底层 I/O 服务。这一项非常适合发布版本。9. 第二个解决方案清理残留的 printf仅仅修改工程配置还不够。正式版本最好同时检查printf fprintf puts putchar sprintf等调试代码。尤其关注ISR 实时线程 EtherCAT Callback FOC 异常路径 通信错误处理这些地方。这次就是因为程序中还有没有完全注释掉的printf()。才使问题暴露出来。可以直接全工程搜索printf(以及puts( putchar( fprintf(逐个确认。10. 总结这次问题让我重新意识到一点能够在调试器下稳定运行并不代表程序已经具备独立运行能力。对于嵌入式实时系统Debugger、Semihosting、Breakpoint 等都应该被视为Development Infrastructure而不是产品运行环境的一部分。真正发布到产品上的程序最终必须验证拔掉 J-Link 拔掉 PC 没有 IDE 没有 Debugger以后依然能够独立、稳定地长期运行。否则一次看似无害的printf(error\n);就有可能在最需要故障诊断的时候反过来成为新的故障源。
返回列表