风过空庭,字句正徐来。
关于关于本站关于我给我点钱
更多时间线友链文件服务wiki
联系写留言发邮件GitHub
© 2024-2026 yono. | RSS 订阅 | 站点地图 | | Stay hungry. Stay foolish.
Powered by Mix Space&
白い
.
| 粤 ICP 备2024284785号-1 |
正在被0人看爆
纸白微明,未成篇章。

为什么不用 MicroLib 和 printf —— Arm 的 Semihosting

(已编辑)
/
550
5
AI·GEN

关键洞察

这篇文章上次修改于,可能部分内容已经不适用,如有疑问可询问作者。

为什么不用 MicroLib 和 printf —— Arm 的 Semihosting

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • 结论

    结论写在前面

    1.

    首先不使用 microLib 是为了源码行为可控,不会由于厂商的胡作非为改源 lib ,同时也使得源码具备了跨平台的能力,不会因 C 标准库不同产生行为差异。

    1. 由此推演,如果不使用 microLib ,那么会引入原版 libc ,那么 C 源库里的诸多交互函数,会产生 Semihosting 代码,导致 debug 下会卡卡地运行,脱离 debug 根本无法运行。

    2. 总之,如果不使用 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() 函数

    CodeBlock Loading...

    clock() 函数所在,当真出现了 BKPT 软断点。

    其中 BKPT 是 Cortex-M 的 Break Point (软件断点)指令,而常数 0xAB 则是 Semihosting 专用暗号。如果使用的调试工具恰好支持 Semihosting ,那么甚至软件不会停下并正常运行。但断电再起就傻了,” BKPT 指令在非调试模式下执行,会直接让 Cortex-M 处理器进入 Hardfault “。

    CodeBlock Loading...

    开优化能否避免?

    事实上,现代的编译器已经非常智能了,开一点优化会修复许多你自己都没发现的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说的这么可怕,“还编译器默认植入”,我怎么还活的好好的?我怎么从来没碰到过?

    恕我直言,你可能符合以下特征:

    1. 大多数情况下使用的是Arm Compiler 5;
    2. 大多数情况下会默认使用 MicroLib;
    3. 在Arm Compiler 6下不选MicroLib的时候遇到“调试状态下一切正常,但下载程序直接跑就会死机”的现象——因此在小本本上默默记下了只能使用MicroLib的笔记;
    4. 从不使用 malloc 以外的 libc 函数,甚至包括 printf
    5. 用的程序模板是大佬做好的;
    6. 应用开发基于芯片厂商给的例子工程
    7. 使用类似RT-Thread这类“提供一站式服务”的软件平台。

    别看我列举了很多,其实只分两种情况:

    1. 瞎猫碰死耗子——运气好
    2. 有人替你负重前行
    ASM
       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
    
    ASM
    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