arm-gcc编译代码体积大于armclang的原因及解决方法
问题背景
通过STM32CubeMX生成相同项目,分别创建Keil MDK项目(使用armclang v6.19)与Makefile项目(使用arm-none-eabi-gcc v12.2.1),均将main.c改为main.cpp,核心代码如下:
uint8_t* data; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART3_UART_Init(); MX_USB_OTG_FS_PCD_Init(); data = new uint8_t[16]; for(int i = 0; i < 16; i++)data[i] = i+1; while (1) { HAL_GPIO_TogglePin(LD3_GPIO_Port, GPIO_PIN_14); HAL_Delay(500); } }
不同优化级下的编译体积差异如下:
O0优化
- Keil:
Program Size: Code=11950 RO-data=510 RW-data=12 ZI-data=3012 - arm-none-eabi-gcc:
text data bss dec hex 17572 100 3268 20940 51cc
O3优化
- Keil:
Program Size: Code=8238 RO-data=510 RW-data=12 ZI-data=3012 - arm-none-eabi-gcc:
text data bss dec hex 12448 100 3268 15816 3dc8
Oz优化
- Keil:
Program Size: Code=6822 RO-data=510 RW-data=12 ZI-data=3012 - arm-none-eabi-gcc:
text data bss dec hex 11876 100 3268 15244 3b8c
体积差异的核心原因
优化标志的默认行为差异
armclang与gcc的-O系列优化标志默认启用的优化子集并不完全一致:- 比如
Oz(侧重代码大小的优化),armclang默认会更激进地进行函数内联、死代码消除、常量折叠,而gcc的Oz可能保留了更多默认的运行时检查逻辑; - 两者对C++特性的优化策略不同,比如
new操作的异常处理代码,clang默认可能更精简,而gcc默认保留了完整的异常抛出框架。
- 比如
标准库实现差异
- Keil配套的是ARM官方优化的C++标准库,针对STM32等嵌入式平台做了深度精简,尤其是内存分配(
malloc/new)、IO等模块; - gcc默认使用的是newlib标准库,未针对极致体积优化,若未指定
newlib-nano,代码体积会显著大于ARM的精简库。
- Keil配套的是ARM官方优化的C++标准库,针对STM32等嵌入式平台做了深度精简,尤其是内存分配(
编译/链接选项的隐含差异
- Keil MDK生成项目时,默认添加了针对STM32的专属优化选项,比如
-ffunction-sections -fdata-sections(拆分函数/数据段)和链接时的--gc-sections(垃圾代码消除),而手动编写的Makefile可能未配置这些选项,导致未使用的代码、数据未被移除; - Keil默认可能关闭了C++的RTTI(运行时类型信息)和异常支持,而gcc默认开启这些特性,带来额外的代码开销。
- Keil MDK生成项目时,默认添加了针对STM32的专属优化选项,比如
编译器版本与架构针对性差异
armclang v6.19是ARM专为自家架构打造的编译器,对Cortex-M系列的指令优化更精准;而gcc v12.2.1是通用编译器,虽然版本更新,但在嵌入式平台的极致体积优化上,针对性不如armclang。
解决办法
统一编译与链接选项
在gcc的Makefile中添加以下关键选项,对齐Keil的优化策略:# 编译选项 CXXFLAGS += -mthumb -mcpu=cortex-m4 # 根据你的STM32型号调整 CXXFLAGS += -ffunction-sections -fdata-sections CXXFLAGS += -fno-exceptions -fno-rtti # 关闭C++异常和RTTI CXXFLAGS += -Os # 或Oz,对应Keil的Oz优化 # 链接选项 LDFLAGS += --gc-sections LDFLAGS += --specs=nano.specs # 使用newlib-nano精简标准库替换动态内存分配为静态数组
代码中new uint8_t[16]属于动态内存分配,会引入malloc/free的实现代码,可直接替换为静态数组消除这部分开销:uint8_t data[16]; // 替换原有的uint8_t* data; int main(void) { // ... 初始化代码 ... for(int i = 0; i < 16; i++)data[i] = i+1; // ... 循环代码 ... }若必须使用动态分配,改用
nothrow版本配合-fno-exceptions:data = new(nothrow) uint8_t[16];对齐标准库配置
确保gcc项目使用newlib-nano(通过--specs=nano.specs链接选项),该库是newlib的精简版本,移除了很多嵌入式场景不需要的功能,能大幅减小代码体积。导出Keil编译参数对齐
在Keil中查看编译时的完整命令行(可通过Build Output窗口查看),将除编译器本身外的所有参数复制到Makefile中,完全对齐编译选项,消除因配置不同带来的体积差异。
内容的提问来源于stack exchange,提问作者lazba

