找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 4737|回复: 3

【单片机】将HAL优化成空气——STM32CubeIDE开启链接时间优化

[复制链接]

1112

主题

1652

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
245
威望
744 点
宅币
24251 个
贡献
46222 次
宅之契约
0 份
在线时间
2298 小时
注册时间
2014-1-26
发表于 2020-10-26 00:17:48 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
前言
GCC的链接时间优化(LTO优化)是一个非常有效的优化选项,开启后,可以实现函数、常量跨编译单元内联等,真正实现C语言的高效性。

有多高效呢?参照以下帖子的实验,可以得出结论:开启LTO优化,可以把单片机的运行性能优化到几乎每一条指令都是关键指令,直接怼外设进行工作的。
【C】认识GCC的链接时间优化
https://www.0xaa55.com/thread-16842-1-1.html
【C】记一次GCC -O3优化效果实测
https://www.0xaa55.com/thread-16820-1-1.html

在使用STM32Cube之前,我使用的是STM32F1的“STM32F10x_StdPeriph_Driver”作为外设驱动的代码进行编程。由于编译环境是自己用gcc-arm-toolchain配合若干批命令搭的,单片机的启动代码(以及开头的中断表)是自己写的,在当时我似乎毫无障碍就可以直接开启LTO优化。

到后来我发现STM32CubeMX自动化生成代码很好用,建立的STM32CubeIDE工程也是用gcc编译的,我就开始使用STM32CubeIDE进行STM32单片机的开发了。不过经过几次折腾,我发现STM32CubeIDE的工程编译配置里,默认没有LTO优化的选项,并且当我试图在Miscellaneous里面手动添加-flto选项后,我发现它存在编译不通过的问题。

经过一段时间的上网搜索和调试,我发现了GCC7以前的LTO优化的Bug,这个Bug导致了编译上出现的各种符号相关的问题,但这些问题是可以不用更换编译器就能解决,并且能成功应用LTO优化。


初次尝试-flto命令参数

LTO优化的效果非常显著,在我看来,这是一个必开的优化选项。它对于C语言是关键的,因为它能真正实现跨编译单元优化,不仅可以减少大量的不必要的函数调度成本,而且很多仅框架性的代码能被直接优化得无影无踪,架构的设计变得更加自由高效了。

所以一开始我在工程编译属性里面,先切换到Release,设置-O3优化选项,然后就是关键的地方——尝试给gcc的命令行选项直接加上-flto,看看是不是直接就有效果。
flto.png

结果编译失败了:说找不到函数_sbrk的符号。
sbrk.png

这个sbrk是个什么东西呢?我大致搜索了一下发现它好像是*nix系统在底层用于管理内存的东西,相当于malloc()之类的实现,要靠这个sbrk()来完成。

但是我发现工程里其实已经实现了sbrk()这个函数了。是gcc的系统库要调用sbrk(),而STM32CubeMX生成的代码里,sbrk()的实现在sysmem.c里面完成。
sysmem.png

搜了ST自己的论坛、github、stackoverflow后,发现这些网站上搜得到的东西给的方法都不管用。按照这些搜出来的内容,我尝试了很多的__attribute__()修饰,比如禁止函数被内联、把函数当成中断处理程序,或者弄个指针变量取函数指针等。然而都没用。
gg.png

搜遍了英文的资料后我打算看看别的语言的资料会不会有靠谱的。我在谷歌上搜到了一个德语的论坛,里面似乎给出了方法——虽说我其实根本看不懂德语,但不知怎么的总之就看懂了一句:“得使用__attribute__((__used__))修饰来加上used属性”
used.png

我照着加上了以后,成功通过编译!看样子没有什么问题了,我直接插上ST-LINK把程序烧写进了单片机。

useed.png


一直以来的疑惑,gcc开LTO的时候,如果要链接一个汇编程序.o进去会怎样?

这个疑惑我今天是揭晓了。被坑得目瞪口呆!先说结论:这个汇编程序会被孤立,它对C程序符号的导入会被链接器无视,因为gcc7链接器在进行LTO代码生成的时候,只考虑参与了LTO部分的函数的符号,很多函数在LTO编译期间就被直接去掉了,因为没有调用者。

首先,我把程序烧录进单片机后,拔掉烧录器,然后把单片机的USB线,插上电脑。

正常情况下(Debug的情况下),单片机会以USB CDC的方式报告,然后电脑就多了一个COM串口设备,我用VB程序可以直接读写串口控制我这个单片机。

但现在的情况是:单片机插电脑上,电脑和单片机都一点反应也没有。单片机本身GPIO C13管脚接了个LED,它启动后,这个LED会亮,但我发现它现在没亮。

这说明单片机可能根本就没有运行。我用STM32CubeIDE的按指令调试功能调试的话,发现好像指令也是照常跑的,没有出问题的样子:

INST.png

但是一旦我想设置个断点,然后等它停留到断点上的时候,这个单片机就跑到了Default_Handler的死循环位置上了。我一度怀疑是看门狗被启用了,然后就是被狗咬了,但好像不是。我是在HAL_Delay()的位置里会进入这个死循环。

gas.png

如果是在HAL_Delay()的地方,它突然跳到Default_Handler上,死循环了,那就说明应该是发生了一次中断,然后这个中断直接跳到Default_Handler上了。而HAL_Delay()里最容易发生的中断应该是SysTick的中断,但为何它没有进入SysTick_Handler()呢?我首先尝试在STM32CubeIDE里找到SysTick_Handler(),并且想要给它打断点。

然而!找不到!这个函数没了!

我顿时意识到,LTO会把不需要用到的函数去掉,这个SysTick_Handler()是不是被当作不需要的函数被去掉了呢?就像上述的sbrk()一样。但,我发现 __attribute__((__used__)) 对sbrk()的问题有效,对SysTick_Handler()无效,不管怎么加前缀,不管是extern还是used还是interrupt,都不能让这个函数出现在编译出来的bin里。

我打开了启动代码汇编文件 startup_stm32f103cbtx.s 想看个究竟。这个启动代码文件,确实是自己定义了一堆中断向量的弱符号,然后都转向Default_Handler()。按照推测,这个汇编的文件应该是无法参与LTO编译过程的,这些汇编文件的弱符号,在LTO优化期间也无法被C语言的强符号替用,因为LTO会把没有用到的函数去掉,而在被加入中断表之前,这些函数孤零零的,并没有其调用者,自然就会被去掉。


自己动手,编写C语言版本的启动代码,就像之前自己徒手搭建STM32编译环境一样

由于这个启动代码汇编挺简短的,我很轻易就写出来了。
  1. #include<stdint.h>
  2. #include<stddef.h>
  3. #include<string.h>

  4. extern char _sidata;
  5. extern char _sdata;
  6. extern char _edata;
  7. extern char _sbss;
  8. extern char _ebss;
  9. extern char _estack;

  10. #define BootRAM (void*)0xF108F85F

  11. extern void SystemInit(void);
  12. extern void __libc_init_array(void);
  13. extern int main(void);

  14. __attribute__((section(".text.Reset_Handler")))
  15. static void CopyDataInit()
  16. {
  17.         char *src = &_sidata;
  18.         char *dst = &_sdata;
  19.         size_t count = (size_t)(&_edata) - (size_t)(&_sdata);
  20.         memcpy(dst, src, count);
  21. }

  22. __attribute__((section(".text.Reset_Handler")))
  23. static void FillZerobss()
  24. {
  25.         char *dst = &_sbss;
  26.         size_t count = (size_t)(&_ebss) - (size_t)(&_sbss);
  27.         memset(dst, 0, count);
  28. }

  29. __attribute__((weak, section(".text.Reset_Handler")))
  30. void Reset_Handler(void)
  31. {
  32.         CopyDataInit();
  33.         FillZerobss();
  34.         SystemInit();
  35.         __libc_init_array();
  36.         main();
  37. }

  38. __attribute__((section(".text.Default_Handler")))
  39. void Default_Handler()
  40. {
  41.         for(;;);
  42. }

  43. __attribute__ ((weak, alias ("Default_Handler"))) void NMI_Handler();
  44. __attribute__ ((weak, alias ("Default_Handler"))) void HardFault_Handler();
  45. __attribute__ ((weak, alias ("Default_Handler"))) void MemManage_Handler();
  46. __attribute__ ((weak, alias ("Default_Handler"))) void BusFault_Handler();
  47. __attribute__ ((weak, alias ("Default_Handler"))) void UsageFault_Handler();
  48. __attribute__ ((weak, alias ("Default_Handler"))) void SVC_Handler();
  49. __attribute__ ((weak, alias ("Default_Handler"))) void DebugMon_Handler();
  50. __attribute__ ((weak, alias ("Default_Handler"))) void PendSV_Handler();
  51. __attribute__ ((weak, alias ("Default_Handler"))) void SysTick_Handler();
  52. __attribute__ ((weak, alias ("Default_Handler"))) void WWDG_IRQHandler();
  53. __attribute__ ((weak, alias ("Default_Handler"))) void PVD_IRQHandler();
  54. __attribute__ ((weak, alias ("Default_Handler"))) void TAMPER_IRQHandler();
  55. __attribute__ ((weak, alias ("Default_Handler"))) void RTC_IRQHandler();
  56. __attribute__ ((weak, alias ("Default_Handler"))) void FLASH_IRQHandler();
  57. __attribute__ ((weak, alias ("Default_Handler"))) void RCC_IRQHandler();
  58. __attribute__ ((weak, alias ("Default_Handler"))) void EXTI0_IRQHandler();
  59. __attribute__ ((weak, alias ("Default_Handler"))) void EXTI1_IRQHandler();
  60. __attribute__ ((weak, alias ("Default_Handler"))) void EXTI2_IRQHandler();
  61. __attribute__ ((weak, alias ("Default_Handler"))) void EXTI3_IRQHandler();
  62. __attribute__ ((weak, alias ("Default_Handler"))) void EXTI4_IRQHandler();
  63. __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel1_IRQHandler();
  64. __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel2_IRQHandler();
  65. __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel3_IRQHandler();
  66. __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel4_IRQHandler();
  67. __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel5_IRQHandler();
  68. __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel6_IRQHandler();
  69. __attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel7_IRQHandler();
  70. __attribute__ ((weak, alias ("Default_Handler"))) void ADC1_2_IRQHandler();
  71. __attribute__ ((weak, alias ("Default_Handler"))) void USB_HP_CAN1_TX_IRQHandler();
  72. __attribute__ ((weak, alias ("Default_Handler"))) void USB_LP_CAN1_RX0_IRQHandler();
  73. __attribute__ ((weak, alias ("Default_Handler"))) void CAN1_RX1_IRQHandler();
  74. __attribute__ ((weak, alias ("Default_Handler"))) void CAN1_SCE_IRQHandler();
  75. __attribute__ ((weak, alias ("Default_Handler"))) void EXTI9_5_IRQHandler();
  76. __attribute__ ((weak, alias ("Default_Handler"))) void TIM1_BRK_IRQHandler();
  77. __attribute__ ((weak, alias ("Default_Handler"))) void TIM1_UP_IRQHandler();
  78. __attribute__ ((weak, alias ("Default_Handler"))) void TIM1_TRG_COM_IRQHandler();
  79. __attribute__ ((weak, alias ("Default_Handler"))) void TIM1_CC_IRQHandler();
  80. __attribute__ ((weak, alias ("Default_Handler"))) void TIM2_IRQHandler();
  81. __attribute__ ((weak, alias ("Default_Handler"))) void TIM3_IRQHandler();
  82. __attribute__ ((weak, alias ("Default_Handler"))) void TIM4_IRQHandler();
  83. __attribute__ ((weak, alias ("Default_Handler"))) void I2C1_EV_IRQHandler();
  84. __attribute__ ((weak, alias ("Default_Handler"))) void I2C1_ER_IRQHandler();
  85. __attribute__ ((weak, alias ("Default_Handler"))) void I2C2_EV_IRQHandler();
  86. __attribute__ ((weak, alias ("Default_Handler"))) void I2C2_ER_IRQHandler();
  87. __attribute__ ((weak, alias ("Default_Handler"))) void SPI1_IRQHandler();
  88. __attribute__ ((weak, alias ("Default_Handler"))) void SPI2_IRQHandler();
  89. __attribute__ ((weak, alias ("Default_Handler"))) void USART1_IRQHandler();
  90. __attribute__ ((weak, alias ("Default_Handler"))) void USART2_IRQHandler();
  91. __attribute__ ((weak, alias ("Default_Handler"))) void USART3_IRQHandler();
  92. __attribute__ ((weak, alias ("Default_Handler"))) void EXTI15_10_IRQHandler();
  93. __attribute__ ((weak, alias ("Default_Handler"))) void RTC_Alarm_IRQHandler();
  94. __attribute__ ((weak, alias ("Default_Handler"))) void USBWakeUp_IRQHandler();

  95. __attribute__((section(".isr_vector"), used))
  96. void *const g_pfnVectors[] =
  97. {
  98.         &_estack,
  99.         Reset_Handler,
  100.         NMI_Handler,
  101.         HardFault_Handler,
  102.         MemManage_Handler,
  103.         BusFault_Handler,
  104.         UsageFault_Handler,
  105.         0,
  106.         0,
  107.         0,
  108.         0,
  109.         SVC_Handler,
  110.         DebugMon_Handler,
  111.         0,
  112.         PendSV_Handler,
  113.         SysTick_Handler,
  114.         WWDG_IRQHandler,
  115.         PVD_IRQHandler,
  116.         TAMPER_IRQHandler,
  117.         RTC_IRQHandler,
  118.         FLASH_IRQHandler,
  119.         RCC_IRQHandler,
  120.         EXTI0_IRQHandler,
  121.         EXTI1_IRQHandler,
  122.         EXTI2_IRQHandler,
  123.         EXTI3_IRQHandler,
  124.         EXTI4_IRQHandler,
  125.         DMA1_Channel1_IRQHandler,
  126.         DMA1_Channel2_IRQHandler,
  127.         DMA1_Channel3_IRQHandler,
  128.         DMA1_Channel4_IRQHandler,
  129.         DMA1_Channel5_IRQHandler,
  130.         DMA1_Channel6_IRQHandler,
  131.         DMA1_Channel7_IRQHandler,
  132.         ADC1_2_IRQHandler,
  133.         USB_HP_CAN1_TX_IRQHandler,
  134.         USB_LP_CAN1_RX0_IRQHandler,
  135.         CAN1_RX1_IRQHandler,
  136.         CAN1_SCE_IRQHandler,
  137.         EXTI9_5_IRQHandler,
  138.         TIM1_BRK_IRQHandler,
  139.         TIM1_UP_IRQHandler,
  140.         TIM1_TRG_COM_IRQHandler,
  141.         TIM1_CC_IRQHandler,
  142.         TIM2_IRQHandler,
  143.         TIM3_IRQHandler,
  144.         TIM4_IRQHandler,
  145.         I2C1_EV_IRQHandler,
  146.         I2C1_ER_IRQHandler,
  147.         I2C2_EV_IRQHandler,
  148.         I2C2_ER_IRQHandler,
  149.         SPI1_IRQHandler,
  150.         SPI2_IRQHandler,
  151.         USART1_IRQHandler,
  152.         USART2_IRQHandler,
  153.         USART3_IRQHandler,
  154.         EXTI15_10_IRQHandler,
  155.         RTC_Alarm_IRQHandler,
  156.         USBWakeUp_IRQHandler,
  157.         0,
  158.         0,
  159.         0,
  160.         0,
  161.         0,
  162.         0,
  163.         0,
  164.         BootRAM
  165. };
复制代码
写完后,重新编译,一点问题也没有。烧录单片机,上电,单片机正常启动,功能全部上线。

这说明,汇编文件在工程里不仅不会参与LTO优化过程,而且很多时候是个累赘,还不如用C语言来写,并结合链接器脚本的行为把必须插入到bin里面的部分声明好。


结论

开启LTO优化后,由于gcc编译器不会把GAS汇编的语句内容也转换成LTO优化的特殊字节码,所以GAS汇编的部分是不会参与到LTO编译的过程的。

GAS汇编文件的导入符号,对于gcc7,是不起作用的。GAS汇编导入的C语言函数,因为这个C语言函数在LTO优化期间,要么被内联了,要么因为没有调用者,被移除了。gcc在链接时间生成的这块LTO相关的代码,就没有这个符号了。

对应之前的sbrk()函数找不到符号的问题,其实很有可能是gcc提供的最小系统库(提供诸如memcpy、sscanf、sprintf等函数)在编译的时候没有开启LTO选项,它没有生成对应的LTO段。而且事实上在IDA里也可以看出,像memcpy、memset这些函数,不能像别的C语言函数那样只要足够简短就能被内联——其实还有一点是gcc自带一个优化,比如,它会识别你自己用指针捏的内存拷贝,然后将其替换为对memcpy的调用——个人感觉这是一种负优化,因为memcpy在单片机上的实现其实就是按字节拷贝,根本不像在PC上那样高效。

反正,要开LTO优化,做这三件事:
  • 工程选项设置gcc命令参数要有-flto
  • sysmem.c的sbrk函数,加一个__attribute__((__used__))
  • startup重写C


LTO优化的效果还是非常明显的,用IDA可以直接看出,开了LTO优化,HAL层几乎就只剩下那几个大头函数了。

开启LTO优化前:
DBG.png

开启LTO优化后:
IDA.png

本帖被以下淘专辑推荐:

回复

使用道具 举报

1

主题

40

回帖

311

积分

用户组: 中·技术宅

UID
2054
精华
0
威望
21 点
宅币
200 个
贡献
28 次
宅之契约
0 份
在线时间
28 小时
注册时间
2016-11-10
发表于 2020-10-26 22:52:27 | 显示全部楼层
学习了~
不过这样说起来LTO只能支持同样有LTO选项编译的.o文件么,感觉好坑 (
回复 赞! 靠!

使用道具 举报

1112

主题

1652

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
245
威望
744 点
宅币
24251 个
贡献
46222 次
宅之契约
0 份
在线时间
2298 小时
注册时间
2014-1-26
 楼主| 发表于 2020-10-26 23:49:44 | 显示全部楼层
大能猫 发表于 2020-10-26 22:52
学习了~
不过这样说起来LTO只能支持同样有LTO选项编译的.o文件么,感觉好坑 ( ...

只是gcc7的bug罢了,gcc8就OK了
回复 赞! 靠!

使用道具 举报

0

主题

1

回帖

77

积分

用户组: 小·技术宅

UID
7715
精华
0
威望
12 点
宅币
42 个
贡献
10 次
宅之契约
0 份
在线时间
1 小时
注册时间
2022-3-7
发表于 2022-3-7 02:39:34 | 显示全部楼层
本帖最后由 Stat_headcrabed 于 2022-3-7 02:41 编辑

目前版本cubeide(1.8.0)能在软件设置里面切换到gcc10,可以解决掉编译器忽略掉weak修饰的问题,我实际测试,基于hal库的工程使用gcc10可以直接开-flto编译,不需要任何额外的操作(必须至少为gcc10,这个bug在arm embedded gcc9仍未修复);另外ltdc的framebuffer这类由dma访问的数组现在必须加入volatile修饰,否则会被lto干掉。
回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-4-26 13:45 , Processed in 0.048080 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表