为什么不用 MicroLib 和 printf —— Arm 的 Semihosting
结论
结论写在前面
首先不使用 microLib 是为了源码行为可控,不会由于厂商的胡作非为改源 lib ,同时也使得源码具备了跨平台的能力,不会因 C 标准库不同产生行为差异。
由此推演,如果不使用 microLib ,那么会引入原版 libc ,那么 C 源库里的诸多交互函数,会产生 Semihosting 代码,导致 debug 下会卡卡地运行,脱离 debug 根本无法运行。
总之,如果不使用 microLib ,其中的 C 标准库函数的使用要非常谨慎,避免是系统交互函数产生 Semihosting 代码
祖宗之法不可变
自我初入行起,我的老大就坚决禁止使用 printf 系列函数,或者干脆说禁止所有的 libc 函数,包括 malloc 、 memset 、 strcpy 等,又或者说标准C 库只引入 stdint.h 、 stdlib.h 、 stdbool.h 。导致了海量自己实现的字符串操作、 buffer 操作、 trace 日志等。如果 malloc 还可以解释为避免线程暴栈,静态内存可以使 bug 规模可控,其他的实现究竟为何不可用呢?
总之,祖宗之法不可变,直到现在的工程依然是这样。
线索
看这个家伙的bootloader 漏洞介绍时(其实也是一个栈操作问题),看到下面这篇文章。或许可以解答这个祖宗之法的来源。
Semihosting 真的是嵌入式阑尾么?-腾讯云开发者社区-腾讯云 (tencent.com)
验证
其中一些代码如下。 clock(); 就是一个典型的产生系统交互的代码。在这个工程中取消了 microLib 。
#include <stdio.h>
#include <time.h>
int main(void)
{
// 省略一些初始化代码
while (1)
{
clock_t tTime = clock();
HAL_GPIO_TogglePin(GPIOH, GPIO_PIN_5);
HAL_Delay(500);
}
}
在 debug 调试下运行
首先是 clock() 函数相关的反汇编,将跳转到 0x0800037C 地址运行 clock() 函数
109: clock_t tTime = clock();
0x08002386 F7FDFFF9 BL.W 0x0800037C clock
0x0800238A 9001 STR r0,[sp,#0x04]
0x0800238C F6414000 MOVW r0,#0x1C00
0x08002390 F6C50002 MOVT r0,#0x5802
0x08002394 2120 MOVS r1,#0x20
其中 BKPT 是 Cortex-M 的 Break Point (软件断点)指令,而常数 0xAB 则是 Semihosting 专用暗号。如果使用的调试工具恰好支持 Semihosting ,那么甚至软件不会停下并正常运行。但断电再起就傻了,” BKPT 指令在非调试模式下执行,会直接让 Cortex-M 处理器进入 Hardfault “。
0x0800037C 2100 MOVS r1,#0x00
0x0800037E 2010 MOVS r0,#0x10
0x08000380 BEAB BKPT 0xAB ;将会停在这句,这就是Semihosting代码的软断点
0x08000382 4905 LDR r1,[pc,#20] ; @0x08000398
0x08000384 6809 LDR r1,[r1,#0x00]
0x08000386 1A40 SUBS r0,r0,r1
开优化能否避免?
事实上,现代的编译器已经非常智能了,开一点优化会修复许多你自己都没发现的bug。
在开到 O3 的情况下进行编译,仍然不可避免得出现 BKPT,这个 libc 应当是以二进制形式链接进烧录文件的。
避免哪些函数?
在参考文章里有写,摘到这里。
1. 标准输入/输出(Standard I/O)
- printf 系列函数:例如 printf、fprintf、sprintf 等,用于格式化输出到标准输出设备(通常是主机的控制台)。
- scanf 系列函数:例如 scanf、fscanf、sscanf 等,用于格式化输入从标准输入设备(通常是主机的键盘输入)。
2. 文件操作(File Operations)
- fopen:打开文件。
- fclose:关闭文件。
- fread:从文件读取数据。
- fwrite:向文件写入数据。
- fseek:移动文件指针到指定位置。
- ftell:获取文件指针当前位置。
- fflush:刷新文件输出缓冲区。
3. 时间和日期(Time and Date)
- time:获取当前时间。
- clock:获取处理器时间。
- difftime:计算两个时间点的时间差。
- strftime:格式化时间和日期为字符串。
4. 错误处理(Error Handling)
- perror:输出错误信息到标准错误设备。
- strerror:返回与错误码对应的错误信息字符串。
5. 系统调用(System Calls)
- exit:终止程序并返回状态码。
- system:执行系统命令(在嵌入式系统中很少使用,但在主机上调试时可能有用)。
6. 其他辅助功能(Other Auxiliary Functions)
- getenv:获取环境变量的值。
- putenv:设置环境变量(不常见)。
- remove:删除文件。
- rename:重命名文件。
原文章太有趣了,强烈建议观看
显然我们符合其归纳的特征 4,并且更加偏激。
【“嵌入式阑尾炎”的潜伏与诱因】
五星上将麦克阿瑟曾评论道:某度看病,癌症起步。你这伪专家,把Semihosting说的这么可怕,“还编译器默认植入”,我怎么还活的好好的?我怎么从来没碰到过?
恕我直言,你可能符合以下特征:
- 大多数情况下使用的是Arm Compiler 5;
- 大多数情况下会默认使用 MicroLib;
- 在Arm Compiler 6下不选MicroLib的时候遇到“调试状态下一切正常,但下载程序直接跑就会死机”的现象——因此在小本本上默默记下了只能使用MicroLib的笔记;
- 从不使用 malloc 以外的 libc 函数,甚至包括 printf
- 用的程序模板是大佬做好的;
- 应用开发基于芯片厂商给的例子工程
- 使用类似RT-Thread这类“提供一站式服务”的软件平台。
别看我列举了很多,其实只分两种情况:
- 瞎猫碰死耗子——运气好
- 有人替你负重前行