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-Link:
PC │ 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. 什么是 Semihosting?
Semihosting 是 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 工程截图来看,原来的配置是:
Library:Normal 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
这也是这次最终采用的方案。
IAR:
Project ↓ 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");就有可能在最需要故障诊断的时候,反过来成为新的故障源。