技术宅的结界

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

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 838|回复: 2
收起左侧

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

[复制链接]

1085

主题

2537

帖子

6万

积分

用户组: 管理员

一只技术宅

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

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

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

x
前言
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编译环境一样

由于这个启动代码汇编挺简短的,我很轻易就写出来了。
[C] 纯文本查看 复制代码
#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优化前:
DBG.png

开启LTO优化后:
IDA.png

评分

参与人数 1威望 +10 宅币 +30 贡献 +10 收起 理由
watermelon + 10 + 30 + 10 支持!

查看全部评分

本帖被以下淘专辑推荐:

回复

使用道具 举报

1

主题

36

帖子

299

积分

用户组: 中·技术宅

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

1085

主题

2537

帖子

6万

积分

用户组: 管理员

一只技术宅

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

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

本版积分规则

QQ|申请友链||Archiver|手机版|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图  

GMT+8, 2021-3-1 18:42 , Processed in 0.041568 second(s), 36 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

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