STM32开发GCC常用编译选项
-c
和 -o
是 GCC 编译器最常用的两个选项,下面是它们的具体含义和配合使用说明:
✅ -c
:只编译,不链接
- 告诉编译器只将
.c
或.cpp
文件编译为目标文件.o
- 不进行链接,不生成最终可执行文件
📌 作用:用于构建大型项目的中间目标文件,适合配合 Makefile
。
✅ -o <输出文件名>
:指定输出文件名
- 控制输出文件的名称
- 可以用于
.o
文件,也可以用于最终的可执行文件
🧩 它们配合使用的典型方式:
gcc -c main.c -o main.o
含义:
main.c
编译为main.o
- 不会进行链接(不会检查
main()
是否完整、不会报_start
缺失)
💡 示例场景
✅ 多文件项目编译步骤:
# 编译每个源文件为目标文件
gcc -c foo.c -o foo.o
gcc -c bar.c -o bar.o# 链接成最终可执行程序
gcc foo.o bar.o -o my_program
✅ 总结对比表
选项 | 作用 | 示例 |
---|---|---|
-c | 编译为 .o ,不进行链接 | gcc -c main.c |
-o <file> | 指定输出文件名(可用于 .o 或可执行) | gcc -c main.c -o main.o |
❗注意事项
情况 | 说明 |
---|---|
忘记加 -c 但用了 -o xxx.o | 编译器会尝试链接,报错找不到 main() 或 _start() |
没加 -o | 默认输出为 a.out (可执行)或 main.o (目标文件) |
🧠 示例完整构建流程(Cortex-M)
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -c main.c -o main.o
这里的意思是:
- 针对 ARM Cortex-M3 架构编译
- 编译
main.c
生成main.o
,不进行链接
如果你在用
Makefile
,或者想把多个.c
编译成.o
再链接为.elf
,需要 -c - o
-I
是 GCC 中用于指定 头文件(header file)搜索路径 的编译选项,是 C/C++ 项目构建中必不可少的一部分。
✅ 语法
-I<路径>
或者加空格也可以(不推荐):
-I <路径>
🎯 功能说明
告诉编译器:当遇到 #include
指令时,要去指定的路径找头文件。
适用于:
- 自己写的头文件(放在
include/
、inc/
等目录) - 第三方库的头文件(如 CMSIS、FreeRTOS、HAL 等)
🧠 用法示例
gcc -I./include -I../drivers -c main.c -o main.o
遇到这段代码时:
#include "my_driver.h"
编译器将按顺序查找:
- 当前文件所在目录
./include/my_driver.h
../drivers/my_driver.h
- 系统默认头文件目录(如
/usr/include
)
🧩 #include
两种写法的区别:
写法 | 搜索顺序 |
---|---|
#include "file.h" | 当前源文件目录 → -I 指定路径 → 系统默认路径 |
#include <file.h> | -I 指定路径 → 系统默认路径(不查当前目录) |
📌 所以,你用双引号 "
时更适合项目内文件,尖括号 < >
适合标准库或第三方库。
🛠 常见使用场景
✅ 1. 组织良好的项目结构
project/
├── include/
│ └── foo.h
├── src/
│ └── foo.c
编译命令:
gcc -I./include -c src/foo.c -o foo.o
✅ 2. 使用第三方库(如 FreeRTOS)
-I./FreeRTOS/Source/include
-I./FreeRTOS/Source/portable/GCC/ARM_CM4F
❗注意事项
问题 | 建议 |
---|---|
多个 -I 路径时有顺序依赖 | 编译器按顺序查找,建议主项目路径在最前面 |
不要滥用系统路径 /usr/include | 使用 -I 明确引用自定义库更安全 |
多层目录建议用绝对或相对路径 | 避免头文件名冲突 |
✅ 总结表
选项 | 作用 | 示例 |
---|---|---|
-I | 添加头文件搜索路径 | -I./include -I../lib |
配套使用 | 和 -c 一起用于编译 .c 文件 | gcc -c -I./include main.c -o main.o |
-mcpu=cortex-m3
和 -mthumb
是 GCC 针对 ARM 架构(特别是嵌入式开发) 的编译选项,用于控制生成的机器指令和目标架构。
下面是详细解释:
🔹 -mcpu=cortex-m3
✅ 作用:
指定目标 CPU 类型为 ARM Cortex-M3。
编译器将:
- 生成适用于 Cortex-M3 架构的代码
- 使用该架构支持的指令集(ARMv7-M)
- 启用/禁用该 CPU 支持的硬件特性(如是否有 FPU、DSP 指令等)
⚙️ 背景:
Cortex-M3 是一个典型的低功耗、低成本嵌入式处理器,广泛用于微控制器(如 STM32F1 系列)。
🔹 -mthumb
✅ 作用:
强制编译器生成 Thumb 指令集 而不是传统的 ARM 指令集。
🔍 什么是 Thumb?
-
Thumb 是 ARM 指令集的子集,使用 16 位压缩指令(后来也支持 32 位扩展指令)。
-
与标准 32 位 ARM 指令相比:
- ✅ 优点:体积小、执行快(特别在低带宽内存系统中)
- ⚠️ 缺点:功能不如完整 ARM 指令丰富
✅ Cortex-M 系列强制使用 Thumb:
- Cortex-M3 只支持 Thumb,不支持传统 ARM 模式。
- 所以在编译 Cortex-M3 时,必须加
-mthumb
,否则链接时会出错(因为生成的是 ARM 指令,而硬件不支持)。
🔧 一起使用示例:
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -O2 -Wall -c main.c -o main.o
说明:
- 使用适合 Cortex-M3 的 CPU 特性
- 编译成 Thumb 指令(Cortex-M3 强制)
- 开启优化和警告
⚠️ 注意事项:
问题 | 说明 |
---|---|
必须配合 -mthumb | Cortex-M3 不支持 ARM 指令,忘记加会出错或导致运行异常 |
链接时也要指定 CPU 和模式 | 链接器也需要同样的选项,比如 -mcpu=cortex-m3 -mthumb |
不同 Cortex-M 系列指令支持不同 | 例如:Cortex-M4 支持 DSP/FPU,可用 -mcpu=cortex-m4 -mfpu=... -mfloat-abi=... |
✅ 小结:
编译选项 | 含义 |
---|---|
-mcpu=cortex-m3 | 目标 CPU 为 ARM Cortex-M3,使用其特性 |
-mthumb | 生成 Thumb 指令(适用于 Cortex-M 系列) |
这是嵌入式 ARM Cortex-M3 开发的基本配置,必不可少。
如果用的是 Cortex-M4 或 M7,可能还要加
-mfpu=...
和-mfloat-abi=...
这两个参数是 针对 ARM Cortex-M 系列中的浮点单元(FPU)配置,特别适用于如 Cortex-M4F、Cortex-M7F 等带硬件浮点支持的内核。
🔹 -mfpu=...
:指定使用哪种 FPU(Floating Point Unit)
✅ 作用:
告诉编译器使用哪种类型的浮点单元指令集,以便能正确生成指令。
常见值:
选项 | 说明 | 适用芯片(举例) |
---|---|---|
-mfpu=fpv4-sp-d16 | 单精度浮点,16 个寄存器 | Cortex-M4F, Cortex-M7F |
-mfpu=fpv5-sp-d16 | 单精度浮点,支持更多指令 | Cortex-M7F(高性能型) |
-mfpu=none | 不使用浮点指令(默认) | Cortex-M0/M3/M4(无FPU) |
注意:M4 和 M7 有带
F
后缀和不带的版本,只有带F
的才有 FPU。
🔹 -mfloat-abi=...
:指定浮点参数传递方式(ABI)
✅ 作用:
控制浮点运算中,浮点数据是使用 FPU 寄存器处理,还是用整数寄存器(或内存)来传递参数/返回值。
可选值:
选项 | 说明 |
---|---|
soft | 所有浮点运算都用软件实现,不用 FPU 指令 |
softfp | 浮点运算用硬件 FPU,但 ABI 与 soft 兼容,浮点传参用整数寄存器 |
hard | 浮点运算用硬件 FPU,浮点参数和返回值通过 FPU 寄存器传递 |
⚠️ ABI 不兼容的风险:
-
hard
与soft
/softfp
不能混合链接。 -
所以你必须确保:
- 编译的所有代码(特别是中间库文件
.a
,.o
)用的是相同的-mfloat-abi
选项!
- 编译的所有代码(特别是中间库文件
🔧 实际组合推荐(例如 Cortex-M4F):
-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard
🔍 编译器如何选择浮点方式?
CPU 是否带 FPU | -mfpu | -mfloat-abi | 是否使用硬件浮点 | ABI 兼容性 |
---|---|---|---|---|
❌ Cortex-M3 | 不加或 none | soft | ❌ | 与纯整数 ABI 相同 |
✅ Cortex-M4F | fpv4-sp-d16 | softfp | ✅ 用硬件计算 | ✅ 与软浮点兼容 |
✅ Cortex-M4F | fpv4-sp-d16 | hard | ✅ 用硬件计算 | ⚠️ ABI 不兼容软浮点 |
✅ 编译组合推荐总结
目标芯片 | 推荐选项 |
---|---|
Cortex-M3 | -mcpu=cortex-m3 -mthumb |
Cortex-M4 (无FPU) | -mcpu=cortex-m4 -mthumb -mfpu=none -mfloat-abi=soft |
Cortex-M4F | -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard |
Cortex-M7F | -mcpu=cortex-m7 -mthumb -mfpu=fpv5-sp-d16 -mfloat-abi=hard |
-ffreestanding
是 GCC 的一个非常重要的编译选项,尤其在嵌入式开发、裸机编程、内核开发中常用。
✅ -ffreestanding
:开启“自由环境”编译模式(freestanding)
🔹 含义:
告诉编译器:你的程序不运行在标准 C 运行时环境中,而是在一个最小或自定义的运行环境中,比如裸机(bare-metal)或 bootloader。
🔧 和默认模式 -fhosted
的区别
模式 | 选项 | 编译器假设 | 适用场景 |
---|---|---|---|
hosted 模式 | -fhosted (默认) | 有完整操作系统和标准库(如 libc) | 桌面、Linux 应用开发 |
freestanding 模式 | -ffreestanding | 无标准库、无操作系统支持 | 裸机、嵌入式、内核、RTOS |
📦 编译器在 -ffreestanding
下的行为
-
不强制要求定义
main()
- 可以自己定义入口点(如
_start
,Reset_Handler
,boot_entry
等)
- 可以自己定义入口点(如
-
不假设标准头文件存在
- 如
<stdio.h>
,<stdlib.h>
等可能无法使用,除非你自己提供它们。
- 如
-
不会自动链接 libc
- 不链接标准的启动文件(
crt0.o
) - 不自动使用
exit()
结尾
- 不链接标准的启动文件(
✅ 示例:Cortex-M 裸机编译命令
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -ffreestanding -nostdlib -c main.c -o main.o
解释:
-ffreestanding
:告诉编译器我不需要标准库环境-nostdlib
:不链接系统默认标准库(你可能用的是newlib-nano
或自己的最小 libc)
⚠️ 常见配套选项
选项 | 含义 |
---|---|
-nostdlib | 不链接标准 C 库和启动代码 |
-nostartfiles | 不链接标准启动文件(如 crt0.o ) |
-nodefaultlibs | 不链接默认的 libgcc、libc 等 |
-Wl,-T<ldscript> | 指定自定义链接脚本(用于控制内存布局等) |
✅ 推荐使用场景
- 嵌入式裸机开发(如 STM32、ESP32 ROM 启动)
- 引导加载器(bootloader)
- 操作系统内核(如 U-Boot、Linux kernel)
- 编写最小 runtime 环境或 RTOS 内核
- 写自己的 startup.S + main.c,不依赖标准
crt0.o
🧠 小结
编译器选项 | 说明 |
---|---|
-ffreestanding | 禁用标准 C 运行时假设,适合裸机/操作系统等底层开发 |
必须手动配置 | 启动代码、入口函数、链接脚本、异常向量表等均要手动指定 |
-O0
、-O2
、-Os
是 GCC 的 编译优化等级选项,用来控制编译器如何生成代码,分别代表不同的优化目标。
🔹 1. -O0
(无优化)
-
功能:完全关闭优化(默认)。
-
特点:
- 保留源代码结构,变量和函数都如源码所示。
- 非常适合调试(变量名完整、调试步进准确)。
- 代码运行慢、体积大。
-
适用场景:调试阶段、开发初期
🔹 2. -O2
(速度优化)
-
功能:启用大多数优化选项,不影响编译时间太多。
-
优化目标:提高程序运行速度
-
常见优化:
- 消除死代码(Dead Code Elimination)
- 循环优化
- 内联函数
- 常量传播
-
适用场景:通用程序开发、对性能有要求的应用
🔹 3. -Os
(空间优化)
- 功能:开启与
-O2
类似的优化,但会禁用会让代码变大的优化(如循环展开)。 - 优化目标:减小程序体积
- 适用场景:嵌入式系统、Flash 或 ROM 空间有限、目标平台资源受限
📊 对比总结表
优化等级 | 优化目标 | 编译速度 | 程序运行速度 | 可执行文件大小 | 调试友好 | 常见用途 |
---|---|---|---|---|---|---|
-O0 | 无优化 | 快 | 慢 | 大 | ✅ 最友好 | 调试、开发初期 |
-O2 | 提高速度 | 中等 | ✅ 快 | 中 | 一般 | 性能敏感应用 |
-Os | 减小体积 | 中等 | 快(略慢于 -O2 ) | ✅ 小 | 一般 | 嵌入式、ROM 限制环境 |
🔧 实际开发中的组合建议
使用目标 | 推荐编译选项 |
---|---|
调试程序 | -O0 -g |
发布性能优先 | -O2 -march=native (可再加 -flto ) |
发布体积优先(嵌入式) | -Os -ffunction-sections -fdata-sections -Wl,--gc-sections |
❗注意事项
- 调试信息:高优化等级(
-O2
,-Os
)可能影响调试体验(如变量消失、断点跳转不准确)。加-g
可以保留调试符号。 - 行为改变:某些未定义或不稳定代码行为在优化后会出现不同表现,需注意编码规范。
–gc-sections
--gc-sections
是 GNU 链接器 (ld
) 的一个选项,用于 去除未使用的节(section),从而 减小最终可执行文件或固件的大小。它在嵌入式开发中非常常用,尤其是资源受限的系统。
一、什么是 --gc-sections
--gc-sections
(Garbage Collection of Sections)告诉链接器:
“将没有被引用的代码段和数据段清理掉(移除)”。
二、工作机制
这个机制依赖于:
1. 编译器的 -ffunction-sections
和 -fdata-sections
选项
-ffunction-sections
:将每个函数放入独立的.text.<funcname>
段中。-fdata-sections
:将每个全局变量放入独立的.data.<varname>
或.bss.<varname>
段中。
这样可以让链接器更精细地控制每个函数/变量的去留。
2. 链接器的 --gc-sections
- 链接器从入口点(如
_start
或main
)开始遍历引用关系。 - 所有 没有被直接或间接引用的节 都被视为“垃圾”,可以被移除。
三、用法示例
编译阶段:
gcc -ffunction-sections -fdata-sections -c foo.c
gcc -ffunction-sections -fdata-sections -c bar.c
链接阶段:
gcc foo.o bar.o -Wl,--gc-sections -o myapp
-Wl,
是告诉 GCC 把后面的选项传给链接器 ld
。
四、使用注意事项
-
✅ 必须配合
-ffunction-sections -fdata-sections
才有效
否则多个函数/数据仍在一个段里,链接器无法单独识别每个函数的可达性。 -
⚠️ 静态初始化或特殊节(如中断向量、构造函数)可能被误删
- 比如没有被显式引用的中断处理函数可能被清除。
- 使用
__attribute__((used))
或KEEP()
在链接脚本中保护这些符号。
示例:
KEEP(*(.isr_vector)) KEEP(*(.init_array))
-
⚠️ 链接脚本要支持节级保留或精细控制
- 自定义链接脚本中如果使用
*(.text)
而不是KEEP(*(.text.funcname))
,可能导致重要代码被清除。
- 自定义链接脚本中如果使用
-
❗ 某些语言特性或库依赖反射/动态行为,不能轻易剔除
- 如 C++ RTTI、虚函数表、某些库通过注册机制访问符号。
五、如何排查 --gc-sections
导致的错误?
-
链接失败或运行时出现异常行为,怀疑是符号被移除?
可临时禁用--gc-sections
或加__attribute__((used))
进行验证。 -
使用
objdump -t
、nm
等工具查看符号是否还在。 -
加
-Wl,--print-gc-sections
可打印出被移除的节:gcc -Wl,--gc-sections -Wl,--print-gc-sections ...
六、总结
项目 | 说明 |
---|---|
作用 | 去除未使用的代码和数据节,减小体积 |
搭配使用 | -ffunction-sections 、-fdata-sections |
风险点 | 重要符号误删、构造函数失效 |
保护方式 | __attribute__((used)) ,链接脚本中 KEEP() |
常用调试参数 | --print-gc-sections |
在做嵌入式或裸机开发,建议编写或调整链接脚本时特别注意使用 KEEP()
保住必要节。
这两个是 GCC 编译器的选项,和代码生成相关,作用如下:
-fno-inline
-
作用:禁止函数内联(inline)。
-
默认情况下,GCC 会根据优化等级和函数复杂度尝试内联一些函数(将函数调用展开成函数体代码,减少调用开销,提高速度)。
-
使用
-fno-inline
后,所有函数都会以正常的调用方式生成,不做内联优化。 -
适用于:
- 需要严格控制函数调用边界的场景(比如调试、性能分析)。
- 避免生成较大代码体积。
- 某些裸机或特定环境中,内联可能影响调试。
-fno-align-functions
-
作用:禁止对函数进行对齐。
-
默认情况下,GCC 会将函数起始地址对齐到一定字节边界(比如 4 或 16 字节),以提升执行效率(对齐访问更快)。
-
使用
-fno-align-functions
后,函数不会自动对齐,函数可能从任意地址开始。 -
适用于:
- 需要精确控制代码布局(比如某些安全或内存映射场景)。
- 追求更紧凑的代码空间(虽然可能牺牲性能)。
- 调试或实验用途。
总结
选项 | 作用 | 使用场景 |
---|---|---|
-fno-inline | 禁止函数内联 | 需要严格函数边界或调试 |
-fno-align-functions | 禁止函数地址自动对齐 | 代码布局控制,减少代码空间 |
-Wall
`` GCC 的 警告选项,用于控制编译器在编译时是否输出某些警告信息:
✅ -Wall
:开启“常用”警告
-
意思:
Wall
= “Warn All”,但其实它并不是开启所有警告,而是开启一组常见、重要的警告。 -
作用:
-
检查潜在的代码错误或不规范写法,例如:
- 未使用的变量或函数(
-Wunused-*
) - 隐式声明
- 条件中赋值操作(如
if (a = b)
) - 未初始化变量
- 类型不匹配等
- 未使用的变量或函数(
-
🧠 实际上 -Wall
包括了很多子警告,比如:
-Wunused-variable
-Wunused-function
-Wreturn-type
-Wparentheses
-Wswitch
-Wformat
...
- 推荐:几乎所有项目都应该加上
-Wall
作为基础检查。
🚫 -Wno-unused
:关闭未使用代码的警告
-
意思:关闭所有
-Wunused-*
类别的警告(如变量、参数、函数没用到)。 -
常见的相关警告包括:
-Wunused-variable
-Wunused-parameter
-Wunused-function
-Wunused-label
-
用途:当你不想看到未使用变量/参数等的警告时使用,比如:
- 写 API 函数时某些参数暂时未用
- 临时注释掉代码进行测试
🛠️ 示例:
int foo(int x, int y) {(void)y; // 更好的方式:明确告诉编译器 y 未使用return x * 2;
}
或用编译选项关闭它:
gcc -Wall -Wno-unused ...
🔍 总结对比
选项 | 作用 | 推荐使用 |
---|---|---|
-Wall | 开启一批重要警告,帮你发现常见错误 | ✅ 强烈推荐 |
-Wno-unused | 关闭未使用代码的警告 | ⚠️ 仅在确有需要时 |
✅ 建议搭配用法
开发中比较常见的组合如下:
gcc -Wall -Wextra -Werror -Wno-unused-parameter -g -O0
含义:
-Wall
:基本警告-Wextra
:更严格的额外警告-Werror
:将所有警告当作错误处理(适合 CI)-Wno-unused-parameter
:允许函数参数没用(但保留其它 unused 警告)
-Wl
是 GCC 编译器用来向链接器(ld)传递参数的前缀。它的全称是 “Windows linker” 还是啥的没关系,关键是告诉 GCC:
“后面的参数不是给 GCC 本身的,而是直接交给链接器处理。”
基本用法
gcc main.o -Wl,option1,option2,...
- 逗号分隔多个参数,会分别传递给链接器
- 参数之间不能有空格(用逗号分隔)
举例
gcc main.o -Wl,-Map=output.map,-T,linker.ld -o output.elf
相当于告诉链接器:
-Map=output.map
—— 生成映射文件-T linker.ld
—— 使用linker.ld
作为链接脚本
不能单独写 -Wl
单独写 -Wl
会导致编译器报错,因为它后面缺少参数。例如:
gcc main.o -Wl
会报错,提示参数不完整。
总结
选项 | 用途 | 示例 |
---|---|---|
-Wl,<args> | 传递 <args> 给链接器 ld | -Wl,-T,linker.ld,-Map=out.map |
-Wl,-L,../ldscripts,-T,linker.ld,-Map=out.map,--strip-debug
--strip-debug
是一个链接器(ld
)选项,用来 从生成的可执行文件或目标文件中删除调试信息。
详细说明
- 功能:去除 ELF 文件中的调试符号(
.debug_*
段),减小文件体积。 - 调试信息包含符号名、源代码行号、变量信息等,用于调试器(如
gdb
)定位代码。 - 去掉调试信息后,程序依然可以运行,但不能用调试器方便地调试。
典型用途
- 发布最终版本时,去除调试信息以节省存储空间。
- 有时调试符号单独存储在
.dSYM
或.debug
文件,主程序剥离调试符号。
使用示例
链接时传给 ld
:
ld --strip-debug -o program program.o
GCC 传递给链接器:
gcc main.o -Wl,--strip-debug -o program
注意事项
- 去除调试信息后,调试器无法显示变量名、断点和调用栈信息会受影响。
- 如果你想保留调试但又减小大小,可以用
strip
命令对最终文件处理,或者分离调试符号。
GCC 本身不能直接输出裸二进制(.bin
)文件,因为它默认输出的是 ELF 格式。但你可以把 arm-none-eabi-objcopy
加到 GCC 编译流程后面,以脚本或一条命令的形式完成整个过程。
✅ 推荐方式:用 &&
连写
arm-none-eabi-gcc main.c -T linker.ld -o firmware.elf && \
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
- 第一条:用
gcc
编译并链接生成firmware.elf
- 第二条:用
objcopy
转成裸二进制firmware.bin
✅ 一条命令方式(括号包裹)
(arm-none-eabi-gcc main.c -T linker.ld -o firmware.elf && \arm-none-eabi-objcopy -O binary firmware.elf firmware.bin)
这样写不会污染外部 shell 环境,适合写进构建脚本。
✅ Makefile 中示例
firmware.bin: firmware.elfarm-none-eabi-objcopy -O binary $< $@firmware.elf: main.carm-none-eabi-gcc main.c -T linker.ld -o $@
这样你只要执行 make firmware.bin
,就自动编译并转换了。
🚫 为什么不能直接让 gcc
输出 .bin
?
GCC 只能输出标准可执行格式(ELF、Mach-O、PE 等),不能直接生成裸 .bin
。生成裸二进制是 objcopy
的职责。