0xAA55 发表于 2020-10-26 00:17:48

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

前言
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,看看是不是直接就有效果。


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


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

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


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


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


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



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

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

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

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

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

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



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



如果是在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编译环境一样

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

extern char _sidata;
extern char _sdata;
extern char _edata;
extern char _sbss;
extern char _ebss;
extern char _estack;

#define BootRAM (void*)0xF108F85F

extern void SystemInit(void);
extern void __libc_init_array(void);
extern int main(void);

__attribute__((section(".text.Reset_Handler")))
static void CopyDataInit()
{
        char *src = &_sidata;
        char *dst = &_sdata;
        size_t count = (size_t)(&_edata) - (size_t)(&_sdata);
        memcpy(dst, src, count);
}

__attribute__((section(".text.Reset_Handler")))
static void FillZerobss()
{
        char *dst = &_sbss;
        size_t count = (size_t)(&_ebss) - (size_t)(&_sbss);
        memset(dst, 0, count);
}

__attribute__((weak, section(".text.Reset_Handler")))
void Reset_Handler(void)
{
        CopyDataInit();
        FillZerobss();
        SystemInit();
        __libc_init_array();
        main();
}

__attribute__((section(".text.Default_Handler")))
void Default_Handler()
{
        for(;;);
}

__attribute__ ((weak, alias ("Default_Handler"))) void NMI_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void HardFault_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void MemManage_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void BusFault_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void UsageFault_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void SVC_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void DebugMon_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void PendSV_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void SysTick_Handler();
__attribute__ ((weak, alias ("Default_Handler"))) void WWDG_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void PVD_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void TAMPER_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void RTC_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void FLASH_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void RCC_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void EXTI0_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void EXTI1_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void EXTI2_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void EXTI3_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void EXTI4_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel1_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel2_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel3_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel4_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel5_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel6_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void DMA1_Channel7_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void ADC1_2_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void USB_HP_CAN1_TX_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void USB_LP_CAN1_RX0_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void CAN1_RX1_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void CAN1_SCE_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void EXTI9_5_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void TIM1_BRK_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void TIM1_UP_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void TIM1_TRG_COM_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void TIM1_CC_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void TIM2_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void TIM3_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void TIM4_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void I2C1_EV_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void I2C1_ER_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void I2C2_EV_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void I2C2_ER_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void SPI1_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void SPI2_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void USART1_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void USART2_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void USART3_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void EXTI15_10_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void RTC_Alarm_IRQHandler();
__attribute__ ((weak, alias ("Default_Handler"))) void USBWakeUp_IRQHandler();

__attribute__((section(".isr_vector"), used))
void *const g_pfnVectors[] =
{
        &_estack,
        Reset_Handler,
        NMI_Handler,
        HardFault_Handler,
        MemManage_Handler,
        BusFault_Handler,
        UsageFault_Handler,
        0,
        0,
        0,
        0,
        SVC_Handler,
        DebugMon_Handler,
        0,
        PendSV_Handler,
        SysTick_Handler,
        WWDG_IRQHandler,
        PVD_IRQHandler,
        TAMPER_IRQHandler,
        RTC_IRQHandler,
        FLASH_IRQHandler,
        RCC_IRQHandler,
        EXTI0_IRQHandler,
        EXTI1_IRQHandler,
        EXTI2_IRQHandler,
        EXTI3_IRQHandler,
        EXTI4_IRQHandler,
        DMA1_Channel1_IRQHandler,
        DMA1_Channel2_IRQHandler,
        DMA1_Channel3_IRQHandler,
        DMA1_Channel4_IRQHandler,
        DMA1_Channel5_IRQHandler,
        DMA1_Channel6_IRQHandler,
        DMA1_Channel7_IRQHandler,
        ADC1_2_IRQHandler,
        USB_HP_CAN1_TX_IRQHandler,
        USB_LP_CAN1_RX0_IRQHandler,
        CAN1_RX1_IRQHandler,
        CAN1_SCE_IRQHandler,
        EXTI9_5_IRQHandler,
        TIM1_BRK_IRQHandler,
        TIM1_UP_IRQHandler,
        TIM1_TRG_COM_IRQHandler,
        TIM1_CC_IRQHandler,
        TIM2_IRQHandler,
        TIM3_IRQHandler,
        TIM4_IRQHandler,
        I2C1_EV_IRQHandler,
        I2C1_ER_IRQHandler,
        I2C2_EV_IRQHandler,
        I2C2_ER_IRQHandler,
        SPI1_IRQHandler,
        SPI2_IRQHandler,
        USART1_IRQHandler,
        USART2_IRQHandler,
        USART3_IRQHandler,
        EXTI15_10_IRQHandler,
        RTC_Alarm_IRQHandler,
        USBWakeUp_IRQHandler,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        BootRAM
};写完后,重新编译,一点问题也没有。烧录单片机,上电,单片机正常启动,功能全部上线。

这说明,汇编文件在工程里不仅不会参与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优化前:


开启LTO优化后:

大能猫 发表于 2020-10-26 22:52:27

学习了~
不过这样说起来LTO只能支持同样有LTO选项编译的.o文件么,感觉好坑 (

0xAA55 发表于 2020-10-26 23:49:44

大能猫 发表于 2020-10-26 22:52
学习了~
不过这样说起来LTO只能支持同样有LTO选项编译的.o文件么,感觉好坑 ( ...

只是gcc7的bug罢了,gcc8就OK了

Stat_headcrabed 发表于 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干掉。
页: [1]
查看完整版本: 【单片机】将HAL优化成空气——STM32CubeIDE开启链接时间优化