嵌入式MCU编译工具链全面总结
本文介绍MCU在编译过程中的一些知识及技能,使用GLM5.0完成编写,作者只是提供了大纲,总结了从业以来所接触到的编译相关技能。相关附件存放到gitee。需要的自取------https://gitee.com/GWLZ/embedded-advanced.git
compile
目录
- 编译工具链可执行文件介绍
- 常用嵌入式编译选项
- 编译详细流程
- 链接流程与链接脚本
- 变量与函数的存储位置和生命周期
- ELF文件后处理与产物生成
- LST文件与MAP文件的应用
- Ninja与GN项目管理框架
- 资深工程师经验与技巧

1. 编译工具链介绍
在官方获取到编译工具包后,会发现在bin目录下有很多的可执行程序,我们编写的代码正式进过这些可执行文件的层层加工,最终变成了MCU能执行的机器码。下面就逐一介绍下常用的工具及其入参。

下载工具链:访问Arm Developer官网的下载页面,该工具链支持32位Arm Cortex-A、Cortex-M和Cortex-R系列处理器,包含GNU编译器(GCC),完全免费。
下载链接:https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
备用页面:如果上述链接失效,可访问 https://developer.arm.com/downloads/-/gnu-rm。请注意该页面已不再更新,但可用于查找历史版本。
具体文件:下载文件名类似 arm-gnu-toolchain-<版本号>-x86_64-arm-none-eabi.exe 的可执行文件。例如,当前可用的一个版本是 gcc-arm-none-eabi-10.3-2021.10-win32.exe。
安装并配置:运行下载的 .exe 文件,按照安装向导完成安装。安装完成后,需要将工具链的 bin 目录路径添加到系统的 PATH 环境变量中,才能在命令行中直接使用编译命令。
1.1 GCC (GNU Compiler Collection)
功能: 编译器前端,负责预处理、编译、汇编等阶段。
常用参数:
bash
# 基本编译选项
-E # 仅预处理,输出.i文件
-S # 编译到汇编,输出.s文件
-c # 编译、汇编到目标文件,输出.o文件
-o <file> # 指定输出文件名
# 预处理选项
-D<macro>[=value] # 定义宏
-U<macro> # 取消宏定义
-I<dir> # 添加头文件搜索路径
-include <file> # 强制包含头文件
# 优化选项
-O0 # 不优化(默认)
-O1 # 基本优化
-O2 # 推荐的优化级别
-O3 # 激进优化
-Os # 优化代码大小
-Og # 调试友好优化
# 警告选项
-Wall # 启用常见警告
-Wextra # 额外警告
-Werror # 警告视为错误
-Wpedantic # 严格遵循标准
-Wshadow # 变量遮蔽警告
-Wconversion # 隐式类型转换警告
# 调试选项
-g # 生成调试信息
-ggdb # 生成GDB专用调试信息
-g3 # 包含宏定义的调试信息
# 语言标准
-std=c99 # C99标准
-std=c11 # C11标准
-std=c++11 # C++11标准
-std=c++17 # C++17标准
# 架构相关
-mcpu=cortex-m4 # 指定CPU类型
-mthumb # 生成Thumb指令
-mfloat-abi=hard # 硬件浮点ABI
-mfpu=fpv4-sp-d16 # 指定FPU类型
# 代码生成选项
-fPIC # 生成位置无关代码
-ffreestanding # 独立环境编译
-fno-builtin # 禁用内置函数
-fno-common # 禁止common段
-fdata-sections # 每个数据项独立段
-ffunction-sections # 每个函数独立段
使用示例:
bash
# 预处理
arm-none-eabi-gcc -E main.c -o main.i -I./include
# 编译到汇编
arm-none-eabi-gcc -S main.c -o main.s -O2
# 编译到目标文件
arm-none-eabi-gcc -c main.c -o main.o -mcpu=cortex-m4 -mthumb -O2 -g
# 一步编译链接
arm-none-eabi-gcc main.c -o firmware.elf -T linker.ld -mcpu=cortex-m4 -mthumb
1.2 LD (Linker)
功能: 链接器,将多个目标文件和库文件链接成可执行文件。
常用参数:
bash
# 基本选项
-o <file> # 指定输出文件名
-T <script> # 指定链接脚本
-e <symbol> # 指定入口点
# 库和路径选项
-L<dir> # 添加库搜索路径
-l<library> # 链接库文件
--start-group # 库分组开始
--end-group # 库分组结束
# 符号选项
--defsym=<sym>=<value> # 定义符号
-Map=<file> # 生成map文件
# 段选项
--gc-sections # 垃圾回收未使用段
--print-gc-sections # 打印被回收的段
# 输出格式
--oformat=<format> # 输出格式(binary, ihex, srec等)
# 其他选项
-nostdlib # 不链接标准库
-static # 静态链接
-rdynamic # 导出符号给动态链接器
--whole-archive # 包含整个归档文件
--no-whole-archive # 结束whole-archive
使用示例:
bash
# 基本链接
arm-none-eabi-ld main.o -o firmware.elf -T stm32.ld -Map=firmware.map
# 带库链接
arm-none-eabi-ld main.o -L./lib -lmylib -o firmware.elf -T linker.ld
# 使用gc-sections
arm-none-eabi-ld main.o -o firmware.elf -T linker.ld --gc-sections
1.3 OBJDUMP
功能: 显示目标文件信息,包括反汇编、段信息、符号表等。
常用参数:
bash
# 反汇编选项
-d # 反汇编可执行段
-D # 反汇编所有段
-S # 混合源码和汇编
-C # C++符号名解码
# 信息显示
-h # 显示段头部信息
-x # 显示所有头部信息
-t # 显示符号表
-r # 显示重定位信息
-p # 显示程序头
# 输出控制
--start-address=<addr> # 起始地址
--stop-address=<addr> # 结束地址
-j <section> # 指定段
# 架构指定
-m <machine> # 指定架构
使用示例:
bash
# 反汇编并显示源码
arm-none-eabi-objdump -d -S firmware.elf > firmware.lst
# 显示段信息
arm-none-eabi-objdump -h firmware.elf
# 显示符号表
arm-none-eabi-objdump -t firmware.elf
# 反汇编指定段
arm-none-eabi-objdump -d -j .text firmware.elf
# 显示重定位信息
arm-none-eabi-objdump -r main.o
1.4 NM
功能: 列出目标文件中的符号。
常用参数:
bash
# 输出格式
-a # 显示所有符号
-g # 仅显示外部符号
-u # 仅显示未定义符号
-S # 显示符号大小
# 排序
-n # 按地址排序
-p # 不排序
-r # 逆序
--size-sort # 按大小排序
# 输出控制
-C # C++符号解码
--defined-only # 仅显示已定义符号
--undefined-only # 仅显示未定义符号
符号类型说明:
T/t - 代码段(.text)中的符号
D/d - 数据段(.data)中的符号
B/b - BSS段中的符号
R/r - 只读数据段(.rodata)中的符号
U - 未定义符号(外部引用)
W - 弱符号
C - common符号
S/s - 小数据段符号
使用示例:
bash
# 列出所有符号
arm-none-eabi-nm firmware.elf
# 按地址排序
arm-none-eabi-nm -n firmware.elf
# 显示符号大小
arm-none-eabi-nm -S firmware.elf
# 仅显示外部符号
arm-none-eabi-nm -g firmware.elf
# 查找特定符号
arm-none-eabi-nm firmware.elf | grep main
1.5 OBJCOPY
功能: 复制和转换目标文件格式。
常用参数:
bash
# 格式转换
-O <format> # 输出格式(elf32-littlearm, binary, ihex, srec)
-I <format> # 输入格式
-B <arch> # 二进制架构
# 段操作
-j <section> # 仅复制指定段
-R <section> # 删除指定段
--add-section <name>=<file> # 添加段
--set-section-flags <section>=<flags> # 设置段属性
# 符号操作
--strip-all # 删除所有符号
--strip-debug # 删除调试符号
--strip-unneeded # 删除不需要的符号
--keep-symbol <sym> # 保留指定符号
--localize-hidden # 隐藏符号本地化
使用示例:
bash
# 生成bin文件
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
# 生成hex文件
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
# 提取特定段
arm-none-eabi-objcopy -O binary -j .data firmware.elf data.bin
# 删除调试信息
arm-none-eabi-objcopy --strip-debug firmware.elf firmware_stripped.elf
# 删除指定段
arm-none-eabi-objcopy -R .comment -R .note firmware.elf firmware.elf
1.6 ADDR2LINE
功能: 将地址转换为文件名和行号。
常用参数:
bash
-e <file> # 指定可执行文件
-f # 显示函数名
-C # C++符号解码
-s # 隐藏文件路径
-p # 显示函数上下文
-i # 内联函数处理
-j <section> # 指定段
使用示例:
bash
# 地址转换
arm-none-eabi-addr2line -e firmware.elf 0x08001234
# 显示函数名
arm-none-eabi-addr2line -e firmware.elf -f 0x08001234
# 批量转换
arm-none-eabi-addr2line -e firmware.elf -f -C 0x08001234 0x08005678
# 内联函数处理
arm-none-eabi-addr2line -e firmware.elf -f -i 0x08001234
1.7 STRIP
功能: 从目标文件中删除符号信息。
常用参数:
bash
--strip-all # 删除所有符号
--strip-debug # 删除调试符号
--strip-unneeded # 删除不需要的符号
--keep-symbol <sym> # 保留指定符号
--remove-section <section> # 删除段
-o <file> # 输出文件
使用示例:
bash
# 删除所有符号
arm-none-eabi-strip --strip-all firmware.elf -o firmware_stripped.elf
# 仅删除调试符号
arm-none-eabi-strip --strip-debug firmware.elf
# 保留特定符号
arm-none-eabi-strip --keep-symbol=main --strip-all firmware.elf
1.8 SIZE
功能: 显示目标文件段大小。
常用参数:
bash
-A # System V格式
-B # Berkeley格式(默认)
--format=<format> # 输出格式
--common # 包含common符号
--totals # 显示总计
使用示例:
bash
# 显示段大小
arm-none-eabi-size firmware.elf
# System V格式
arm-none-eabi-size -A firmware.elf
# 显示详细信息
arm-none-eabi-size --format=sysv firmware.elf
输出示例:
text data bss dec hex filename
12345 1234 5678 19257 4b39 firmware.elf
1.9 READLF
功能: 显示ELF文件信息。
常用参数:
bash
-h # ELF头信息
-l # 程序头
-S # 段头
-s # 符号表
-r # 重定位信息
-d # 动态段
-V # 版本信息
-n # notes段
-W # 宽格式输出
使用示例:
bash
# 查看ELF头
arm-none-eabi-readelf -h firmware.elf
# 查看程序头
arm-none-eabi-readelf -l firmware.elf
# 查看段头
arm-none-eabi-readelf -S firmware.elf
# 查看符号表
arm-none-eabi-readelf -s firmware.elf
2. 常用嵌入式编译选项
2.1 资源优化选项
代码大小优化
bash
# 基本大小优化
-Os # 优化代码大小,启用-O2的基本优化但不增加代码大小
# 函数和数据段分离(配合链接器gc-sections)
-ffunction-sections # 每个函数独立段
-fdata-sections # 每个数据项独立段
# 链接时优化
-flto # 链接时优化,跨模块优化
# 内联控制
-fno-inline-functions # 禁止内联
-fno-inline-small-functions # 禁止小函数内联
--param max-inline-insns-single=50 # 内联指令数限制
# 循环优化
-funroll-loops # 展开循环(可能增大代码)
-fno-unroll-loops # 不展开循环
# 示例
arm-none-eabi-gcc -Os -ffunction-sections -fdata-sections -flto \
-mcpu=cortex-m4 -mthumb main.c -o firmware.elf
内存使用优化
bash
# 栈使用优化
-fstack-usage # 生成栈使用报告
-Wframe-larger-than=<bytes> # 警告大栈帧
-fno-stack-protector # 禁用栈保护(减小开销)
# 寄存器使用
-fcall-saved-<reg> # 指定寄存器跨调用保存
-fcall-used-<reg> # 指定寄存器跨调用可用
# 数据优化
-fmerge-all-constants # 合并常量
-fmerge-constants # 合并相同常量
-fno-common # 避免common段
# 示例
arm-none-eabi-gcc -Os -fstack-usage -Wframe-larger-than=256 main.c
优化效果对比
c
// 原始代码
int calculate(int a, int b) {
int result = 0;
for (int i = 0; i < 100; i++) {
result += a * b + i;
}
return result;
}
bash
# -O0: 60+ 条指令
# -O2: 20 条指令
# -Os: 15 条指令
# -O3: 可能展开循环,代码变大但速度更快
2.2 编程规范选项
编码标准检查
bash
# MISRA C检查(需要专门工具,但GCC可辅助)
-Wpedantic # 严格遵循ISO C标准
-std=c99 -std=c11 # 指定C标准
-Wtraditional # 传统C警告
# 类型和符号检查
-Wsign-conversion # 符号转换警告
-Wconversion # 隐式转换警告
-Wcast-qual # 类型转换丢失const
-Wcast-align # 类型转换对齐问题
-Wstrict-prototypes # 函数原型检查
-Wold-style-definition # 旧式函数定义警告
# 未使用代码检查
-Wunused # 未使用实体警告
-Wunused-function # 未使用函数
-Wunused-variable # 未使用变量
-Wunused-parameter # 未使用参数
-Wunused-but-set-variable # 仅设置未使用的变量
代码质量检查
bash
# 常见错误检测
-Wuninitialized # 未初始化变量
-Wmaybe-uninitialized # 可能未初始化
-Wnull-dereference # 空指针解引用
-Wdangling-else # 悬挂else
-Wdouble-promotion # float到double隐式转换
-Wshadow # 变量遮蔽
-Wformat=2 # printf/scanf格式检查
完整示例:
bash
arm-none-eabi-gcc -c main.c -o main.o \
-std=c11 \
-Wpedantic \
-Wall -Wextra \
-Wconversion -Wsign-conversion \
-Wshadow -Wformat=2 \
-Wstrict-prototypes \
-Wunused -Wuninitialized \
-Werror
2.3 防御编程选项
运行时检查
bash
# 栈保护
-fstack-protector # 启用栈保护
-fstack-protector-strong # 强栈保护(推荐)
-fstack-protector-all # 所有函数栈保护
-fstack-protector-explicit # 显式指定
# 边界检查
-fstack-check # 栈边界检查
-fsanitize=address # 地址消毒器
-fsanitize=undefined # 未定义行为消毒器
-fsanitize=thread # 线程消毒器
# 溢出检查
-ftrapv # 整数溢出陷阱
-fwrapv # 整数溢出环绕
错误预防
bash
# 严格别名规则
-fstrict-aliasing # 启用严格别名优化
-Wstrict-aliasing=3 # 严格别名警告级别
# 空指针检查
-fdelete-null-pointer-checks # 删除空指针检查(优化)
-fno-delete-null-pointer-checks # 保留空指针检查(安全)
# 浮点异常
-ftrapping-math # 浮点异常陷阱
-fsignaling-nans # 信令NaN处理
# 示例
arm-none-eabi-gcc -c main.c -o main.o \
-fstack-protector-strong \
-Wstrict-aliasing=2 \
-Wall -Wextra -Werror \
-fno-delete-null-pointer-checks
防御编程代码示例
c
// 启用防御编译选项后的代码保护效果
#include <stdint.h>
#include <string.h>
// 栈保护示例
void vulnerable_function(char *input) {
char buffer[64]; // -fstack-protector-strong 会添加canary
strcpy(buffer, input); // 编译器会警告不安全
}
// 更安全的版本
void safe_function(const char *input, size_t len) {
char buffer[64];
size_t copy_len = (len < sizeof(buffer) - 1) ? len : sizeof(buffer) - 1;
memcpy(buffer, input, copy_len);
buffer[copy_len] = '\0';
}
// 使用属性增强安全性
void critical_function(void) __attribute__((noinline));
void critical_function(void) {
// 关键函数,禁止内联以确保栈帧完整
}
// 使用静态断言
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
2.4 架构适配选项
ARM架构选项
bash
# 处理器指定
-mcpu=cortex-m0 # Cortex-M0
-mcpu=cortex-m0plus # Cortex-M0+
-mcpu=cortex-m3 # Cortex-M3
-mcpu=cortex-m4 # Cortex-M4
-mcpu=cortex-m7 # Cortex-M7
-mcpu=cortex-m23 # Cortex-M23
-mcpu=cortex-m33 # Cortex-M33
# 指令集选择
-mthumb # Thumb指令集
-marm # ARM指令集(部分MCU不支持)
# 浮点选项
-mfloat-abi=soft # 软件浮点
-mfloat-abi=softfp # 软件浮点ABI,使用硬件FPU
-mfloat-abi=hard # 硬件浮点ABI
# FPU类型
-mfpu=fpv4-sp-d16 # Cortex-M4 FPU
-mfpu=fpv5-sp-d16 # Cortex-M7 单精度FPU
-mfpu=fpv5-d16 # Cortex-M7 双精度FPU
-mfpu=fp-armv8 # ARMv8 FPU
# 字节序
-mlittle-endian # 小端(默认)
-mbig-endian # 大端
RISC-V架构选项
bash
# 架构指定
-march=rv32imc # RV32 I/M/C扩展
-march=rv32imac # RV32 I/M/A/C扩展
-march=rv32imafc # RV32 带FPU
# ABI
-mabi=ilp32 # 32位软浮点ABI
-mabi=ilp32f # 32位单精度浮点ABI
-mabi=lp64 # 64位软浮点ABI
# 代码模型
-mcmodel=medlow # 小代码模型
-mcmodel=medany # 中等代码模型
架构适配示例
bash
# Cortex-M4带FPU的配置
arm-none-eabi-gcc -c main.c -o main.o \
-mcpu=cortex-m4 \
-mthumb \
-mfloat-abi=hard \
-mfpu=fpv4-sp-d16 \
-O2 -g
# Cortex-M7高性能配置
arm-none-eabi-gcc -c main.c -o main.o \
-mcpu=cortex-m7 \
-mthumb \
-mfloat-abi=hard \
-mfpu=fpv5-d16 \
-O3 -g
# RISC-V配置
riscv32-unknown-elf-gcc -c main.c -o main.o \
-march=rv32imc \
-mabi=ilp32 \
-O2 -g
2.5 性能提升选项
速度优化
bash
# 基本速度优化
-O2 # 推荐的速度优化级别
-O3 # 激进优化
-Ofast # 极致优化(可能违反标准)
# 循环优化
-funroll-loops # 循环展开
-funroll-all-loops # 展开所有循环
-fpeel-loops # 循环剥离
-ftree-loop-vectorize # 循环向量化
-ftree-slp-vectorize # 超长指令字向量化
# 函数优化
-finline-functions # 函数内联
-finline-functions-called-once # 仅调用一次的函数内联
-finline-limit=<n> # 内联大小限制
-fipa-sra # 过程间标量替换
-fpartial-inlining # 部分内联
# 分支优化
-fbranch-probabilities # 分支概率优化
-fprofile-use # 使用profiling数据
-freorder-blocks # 重排基本块
-freorder-functions # 重排函数
# 数据优化
-fprefetch-loop-arrays # 预取循环数组
-ftree-loop-distribution # 循环分布
-ftree-loop-distribute-patterns # 循环模式分布
性能优化示例
c
// 原始代码
void matrix_mult(int n, float a[n][n], float b[n][n], float c[n][n]) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
c[i][j] = 0;
for (int k = 0; k < n; k++) {
c[i][j] += a[i][k] * b[k][j];
}
}
}
}
bash
# 性能优化编译
arm-none-eabi-gcc -c matrix.c -o matrix.o \
-O3 \
-mcpu=cortex-m7 \
-mthumb \
-mfloat-abi=hard \
-mfpu=fpv5-d16 \
-ftree-vectorize \
-ffast-math
Profile Guided Optimization (PGO)
bash
# 第一步:编译带profiling的版本
arm-none-eabi-gcc -fprofile-generate main.c -o firmware.elf
# 第二步:运行程序收集数据(需要目标板支持)
# 生成 *.gcda 文件
# 第三步:使用profiling数据重新编译
arm-none-eabi-gcc -fprofile-use main.c -o firmware_optimized.elf
3. 编译详细流程
3.1 编译流程概览
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 源文件 │ │ 预处理文件 │ │ 汇编文件 │ │ 目标文件 │
│ main.c │────▶│ main.i │────▶│ main.s │────▶│ main.o │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│ │ │ │
│ 预处理 │ 编译 │ 汇编 │
│ cpp/gcc -E │ cc1/gcc -S │ as/gcc -c │
▼ ▼ ▼ ▼
头文件展开 语法分析 汇编代码生成 机器码生成
宏替换 中间代码 目标文件格式
条件编译 优化

3.2 预处理阶段 (Preprocessing)
主要工作:
- 头文件包含展开
- 宏定义展开和替换
- 条件编译处理
- 注释删除
- 行号和文件名标记
示例:
c
// main.c
#include <stdio.h>
#include "config.h"
#define MAX_SIZE 100
#define SQUARE(x) ((x) * (x))
#ifdef DEBUG
#define LOG(msg) printf("[DEBUG] %s\n", msg)
#else
#define LOG(msg)
#endif
int main(void) {
int arr[MAX_SIZE];
int result = SQUARE(5);
LOG("Program started");
return result;
}
bash
# 执行预处理
arm-none-eabi-gcc -E main.c -o main.i -I./include -DDEBUG
# 查看预处理结果(部分)
c
// main.i (预处理输出,简化版)
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"
# 1 "stdio.h" 1 3 4
// ... stdio.h 内容 ...
# 1 "config.h" 1
// ... config.h 内容 ...
int main(void) {
int arr[100];
int result = ((5) * (5));
printf("[DEBUG] %s\n", "Program started");
return result;
}
3.3 编译阶段 (Compilation)
主要工作:
- 词法分析 - 将代码分解为token
- 语法分析 - 构建语法树(AST)
- 语义分析 - 类型检查、符号解析
- 中间代码生成 - 生成RTL/GIMPLE
- 优化 - 各种优化pass
- 汇编代码生成
示例:
bash
# 生成汇编代码
arm-none-eabi-gcc -S main.i -o main.s -O2 -mcpu=cortex-m4 -mthumb
asm
// main.s (汇编输出,简化版)
.arch armv7e-m
.eabi_attribute 28, 1
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 2
.eabi_attribute 34, 1
.eabi_attribute 18, 4
.file "main.c"
.text
.global main
.type main, %function
main:
@ args = 0, pretend = 0, frame = 8
@ frame_needed = 0, uses_anonymous_args = 0
push {r7, lr}
sub sp, sp, #400
add r7, sp, #0
movs r3, #25
str r3, [r7, #4]
ldr r0, .L3
bl printf
ldr r3, [r7, #4]
mov r0, r3
add sp, r7, #0
pop {r7, pc}
.L3:
.word .LC0
.size main, .-main
.section .rodata
.align 2
.LC0:
.ascii "[DEBUG] Program started\000"
3.4 汇编阶段 (Assembly)
主要工作:
- 解析汇编指令和伪操作
- 符号解析和符号表创建
- 指令编码为机器码
- 生成重定位信息
- 生成ELF目标文件
示例:
bash
# 生成目标文件
arm-none-eabi-gcc -c main.s -o main.o
# 或使用汇编器
arm-none-eabi-as main.s -o main.o
bash
# 查看目标文件内容
arm-none-eabi-objdump -h main.o
main.o: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000028 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 00000000 00000000 0000005c 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 0000005c 2**0
ALLOC
3 .rodata 00000018 00000000 00000000 0000005c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000058 00000000 00000000 00000074 2**0
CONTENTS, READONLY
3.5 链接阶段 (Linking)
主要工作:
- 地址空间分配
- 符号解析
- 重定位
- 段合并
- 生成可执行文件
示例:
bash
# 链接多个目标文件
arm-none-eabi-gcc main.o utils.o startup.o \
-o firmware.elf \
-T stm32f407.ld \
-L./lib -lc -lm \
-mcpu=cortex-m4 -mthumb \
-Wl,-Map=firmware.map
3.6 完整编译示例
项目结构:
project/
├── src/
│ ├── main.c
│ ├── uart.c
│ ├── timer.c
│ └── startup.s
├── include/
│ ├── uart.h
│ └── timer.h
├── lib/
│ └── libdriver.a
├── ld/
│ └── stm32f407.ld
└── Makefile
Makefile示例:
makefile
# 工具链
CC = arm-none-eabi-gcc
AS = arm-none-eabi-as
LD = arm-none-eabi-gcc
OBJCOPY = arm-none-eabi-objcopy
OBJDUMP = arm-none-eabi-objdump
SIZE = arm-none-eabi-size
# 编译选项
CPU = -mcpu=cortex-m4 -mthumb
CFLAGS = $(CPU) -Wall -Wextra -Werror -std=c11 -O2 -g
CFLAGS += -ffunction-sections -fdata-sections
CFLAGS += -I./include
# 链接选项
LDFLAGS = $(CPU) -T./ld/stm32f407.ld
LDFLAGS += -Wl,--gc-sections -Wl,-Map=build/firmware.map
LDFLAGS += -L./lib -ldriver -lc -lm -lnosys
# 源文件
C_SRCS = src/main.c src/uart.c src/timer.c
ASM_SRCS = src/startup.s
# 目标文件
OBJS = $(C_SRCS:.c=.o) $(ASM_SRCS:.s=.o)
# 默认目标
all: build/firmware.elf build/firmware.bin build/firmware.hex build/firmware.lst
# 编译C文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 编译汇编文件
%.o: %.s
$(AS) $(CPU) $< -o $@
# 链接
build/firmware.elf: $(OBJS)
$(LD) $(LDFLAGS) $(OBJS) -o $@
$(SIZE) $@
# 生成bin
build/firmware.bin: build/firmware.elf
$(OBJCOPY) -O binary $< $@
# 生成hex
build/firmware.hex: build/firmware.elf
$(OBJCOPY) -O ihex $< $@
# 生成反汇编
build/firmware.lst: build/firmware.elf
$(OBJDUMP) -d -S $< > $@
clean:
rm -f $(OBJS) build/*
.PHONY: all clean
编译流程输出:
bash
$ make all
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Wall -Wextra -Werror -std=c11 -O2 -g -ffunction-sections -fdata-sections -I./include -c src/main.c -o src/main.o
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Wall -Wextra -Werror -std=c11 -O2 -g -ffunction-sections -fdata-sections -I./include -c src/uart.c -o src/uart.o
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -Wall -Wextra -Werror -std=c11 -O2 -g -ffunction-sections -fdata-sections -I./include -c src/timer.c -o src/timer.o
arm-none-eabi-as -mcpu=cortex-m4 -mthumb src/startup.s -o src/startup.o
arm-none-eabi-gcc -mcpu=cortex-m4 -mthumb -T./ld/stm32f407.ld -Wl,--gc-sections -Wl,-Map=build/firmware.map -L./lib -ldriver -lc -lm -lnosys src/main.o src/uart.o src/timer.o src/startup.o -o build/firmware.elf
arm-none-eabi-size build/firmware.elf
text data bss dec hex filename
15234 1234 4568 21036 522c build/firmware.elf
arm-none-eabi-objcopy -O binary build/firmware.elf build/firmware.bin
arm-none-eabi-objcopy -O ihex build/firmware.elf build/firmware.hex
arm-none-eabi-objdump -d -S build/firmware.elf > build/firmware.lst
4. 链接流程与链接脚本
4.1 链接流程详解
┌────────────────────────────────────────────────────────────────────┐
│ 链接流程 │
├────────────────────────────────────────────────────────────────────┤
│ │
│ 输入文件 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ main.o │ │ utils.o │ │startup.o │ │ libmy.a │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
│ └────────────┴─────┬──────┴────────────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ 符号解析 │ │
│ │ 符号表合并 │ │
│ │ 未定义符号查找 │ │
│ └───────┬───────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ 段合并 │ │
│ │ .text 合并 │ │
│ │ .data 合并 │ │
│ │ .bss 合并 │ │
│ └───────┬───────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ 地址分配 │ │
│ │ 遵循链接脚本 │ │
│ └───────┬───────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ 重定位 │ │
│ │ 修正地址引用 │ │
│ └───────┬───────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ 生成ELF文件 │ │
│ └───────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
4.2 常用链接选项详解
bash
# 基本选项
-T <script> # 指定链接脚本
-o <file> # 输出文件名
-e <symbol> # 入口点符号
# 库路径和库文件
-L<dir> # 添加库搜索路径
-l<name> # 链接库libname.a或libname.so
--start-group <libs> --end-group # 库分组,解决循环依赖
# 段处理
--gc-sections # 回收未使用段
--print-gc-sections # 打印回收的段
--icf=safe # 相同代码折叠
--keep=<section> # 保留指定段
# 符号处理
--defsym=<symbol>=<value> # 定义符号
--undefined=<symbol> # 强制符号未定义
--require-defined=<symbol> # 要求符号定义
# 输出控制
-Map=<file> # 生成map文件
--cref # 交叉引用表
--orphan-handling=<mode> # 孤儿段处理
# 内存和地址
-Ttext=<addr> # 代码段起始地址
-Tdata=<addr> # 数据段起始地址
-Tbss=<addr> # BSS段起始地址
# 输出格式
--oformat=binary # 原始二进制输出
--oformat=ihex # Intel HEX格式
--oformat=srec # Motorola S-record
# 嵌入式常用
-nostartfiles # 不使用标准启动文件
-nostdlib # 不使用标准库
-static # 静态链接
-Wl,--wrap=<symbol> # 包装函数
# 调试和诊断
--verbose # 详细输出
--trace # 跟踪输入文件
--warn-common # common段警告
--warn-section-align # 段对齐警告
4.3 链接脚本语法详解
基本结构
ld
/* 链接脚本基本结构 */
/* 入口点 */
ENTRY(Reset_Handler)
/* 内存区域定义 */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
CCRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
/* 段定义 */
SECTIONS
{
/* 代码段 */
.text :
{
*(.vectors) /* 中断向量表 */
*(.text) /* 代码 */
*(.text*) /* 代码(通配符) */
*(.rodata) /* 只读数据 */
*(.rodata*) /* 只读数据(通配符) */
. = ALIGN(4);
} > FLASH
/* 数据段 */
.data :
{
_sdata = .; /* 数据段起始地址 */
*(.data)
*(.data*)
. = ALIGN(4);
_edata = .; /* 数据段结束地址 */
} > RAM AT > FLASH /* 运行在RAM,存储在FLASH */
/* BSS段 */
.bss :
{
_sbss = .;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
} > RAM
/* 堆栈 */
.stack (NOLOAD) :
{
. = ALIGN(8);
_sstack = .;
. = . + 0x2000; /* 8KB栈 */
_estack = .;
} > RAM
}
内存区域命令
ld
MEMORY
{
/* name (attributes) : ORIGIN = origin, LENGTH = length */
/* 属性:
R - 可读
W - 可写
X - 可执行
A - 可分配
I - 可初始化
L - 同I
! - 反选
*/
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
/* 使用表达式 */
EXTMEM (rw) : ORIGIN = 0x60000000, LENGTH = 1M
/* 别名 */
REGION_ALIAS("CODE_REGION", FLASH)
REGION_ALIAS("DATA_REGION", RAM)
}
段定义详解
ld
SECTIONS
{
/* 输出段语法:
output_section_name :
{
input_section_description
} > memory_region AT > load_memory_region
*/
/* 1. 基本段定义 */
.text :
{
/* 输入段描述 */
/* 匹配所有目标文件的.text段 */
*(.text)
/* 匹配所有目标文件的.text*段 */
*(.text*)
/* 匹配特定目标文件 */
main.o(.text)
/* 匹配特定库 */
libmylib.a:*(.text)
/* 排除特定文件 */
EXCLUDE_FILE(*debug.o) *(.text)
} > FLASH
/* 2. 带LMA的段 */
.data :
{
_data_start = .;
*(.data)
_data_end = .;
} > RAM AT > FLASH /* VMA在RAM, LMA在FLASH */
/* 显式指定LMA */
.fastcode :
{
*(.fastcode)
} > RAM AT (0x08010000)
/* 3. 带对齐的段 */
.aligned_section :
{
. = ALIGN(4096); /* 4KB对齐 */
_page_start = .;
*(.page_data)
} > RAM
/* 4. 填充 */
.fill_section :
{
*(.fill_data)
. = ALIGN(4);
LONG(0xFFFFFFFF) /* 填充特定值 */
FILL(0x00) /* 后续填充0x00 */
} > FLASH
/* 5. NOLOAD段 */
.noinit (NOLOAD) :
{
*(.noinit)
} > RAM
/* 6. 特殊段 */
.init_array :
{
_init_array_start = .;
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
_init_array_end = .;
} > FLASH
.fini_array :
{
_fini_array_start = .;
KEEP(*(SORT(.fini_array.*)))
KEEP(*(.fini_array))
_fini_array_end = .;
} > FLASH
/* 7. 丢弃段 */
/DISCARD/ :
{
*(.comment)
*(.note*)
*(.ARM.attributes*)
}
}
符号和表达式
ld
/* 符号定义 */
_symbol = 0x1000;
_array_size = (_array_end - _array_start) / 4;
SECTIONS
{
.text :
{
/* 位置计数器 */
_text_start = .; /* 当前位置 */
*(.text)
/* 表达式 */
_text_size = SIZEOF(.text);
_text_end = ADDR(.text) + SIZEOF(.text);
/* 对齐 */
. = ALIGN(4);
/* 条件判断 */
_has_debug = DEFINED(_DEBUG) ? 1 : 0;
} > FLASH
/* 使用LOADADDR, ADDR, SIZEOF等内置函数 */
.data :
{
_data_load = LOADADDR(.data); /* LMA */
_data_start = ADDR(.data); /* VMA */
_data_size = SIZEOF(.data); /* 大小 */
} > RAM AT > FLASH
}
内置函数
ld
/* 链接脚本内置函数 */
SECTIONS
{
/* ABSOLUTE(exp) - 返回绝对值 */
_abs_val = ABSOLUTE(0x1000);
/* ADDR(section) - 返回段的VMA */
_text_addr = ADDR(.text);
/* ALIGN(exp) - 对齐 */
. = ALIGN(4096);
/* DEFINED(symbol) - 判断符号是否定义 */
_use_ext = DEFINED(_EXTERNAL) ? 1 : 0;
/* LOADADDR(section) - 返回段的LMA */
_data_load = LOADADDR(.data);
/* LOG2CEIL(exp) - log2向上取整 */
_shift = LOG2CEIL(SIZEOF(.buffer));
/* MAX / MIN */
_max_size = MAX(SIZEOF(.text), SIZEOF(.data));
/* ORIGIN(memory) - 返回内存区域起始地址 */
_flash_start = ORIGIN(FLASH);
/* LENGTH(memory) - 返回内存区域长度 */
_ram_size = LENGTH(RAM);
/* SIZEOF(section) - 返回段大小 */
_text_size = SIZEOF(.text);
/* SIZEOF_HEADERS - 输出文件头大小 */
_headers_size = SIZEOF_HEADERS;
}
4.4 实际链接脚本示例
ld
/* STM32F407 完整链接脚本 */
/* 入口点 */
ENTRY(Reset_Handler)
/* 栈和堆大小 */
_stack_size = 0x4000; /* 16KB栈 */
_heap_size = 0x2000; /* 8KB堆 */
/* 内存布局 */
MEMORY
{
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
CCRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}
/* 段定义 */
SECTIONS
{
/* 中断向量表 - 必须在Flash起始位置 */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector))
. = ALIGN(4);
} > FLASH
/* 代码段 */
.text :
{
. = ALIGN(4);
_stext = .;
*(.text)
*(.text*)
*(.glue_7) /* ARM/Thumb转换代码 */
*(.glue_7t)
/* 只读数据 */
*(.rodata)
*(.rodata*)
/* C++构造/析构函数表 */
. = ALIGN(4);
__preinit_array_start = .;
KEEP(*(.preinit_array))
__preinit_array_end = .;
__init_array_start = .;
KEEP(*(SORT(.init_array.*)))
KEEP(*(.init_array))
__init_array_end = .;
__fini_array_start = .;
KEEP(*(SORT(.fini_array.*)))
KEEP(*(.fini_array))
__fini_array_end = .;
/* 异常处理 */
__eh_frame_start = .;
KEEP(*(.eh_frame))
__eh_frame_end = .;
. = ALIGN(4);
_etext = .;
} > FLASH
/* ARM异常处理段 */
.ARM.extab :
{
*(.ARM.extab* .gnu.linkonce.armextab.*)
} > FLASH
.ARM :
{
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} > FLASH
/* 用于辅助调试的信息 */
.ARM.attributes :
{
*(.ARM.attributes)
} > FLASH
/* 需要初始化的数据 - 存储在Flash,运行时复制到RAM */
_sidata = LOADADDR(.data);
.data :
{
. = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
*(.RamFunc) /* 在RAM中运行的函数 */
. = ALIGN(4);
_edata = .;
} > RAM AT > FLASH
/* 未初始化数据段 */
.bss :
{
. = ALIGN(4);
_sbss = .;
__bss_start__ = _sbss;
*(.bss)
*(.bss*)
*(COMMON)
. = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
} > RAM
/* 不需要初始化的数据 */
.noinit (NOLOAD) :
{
. = ALIGN(4);
*(.noinit)
. = ALIGN(4);
} > RAM
/* 用户堆 */
._user_heap_stack :
{
. = ALIGN(8);
PROVIDE(end = .);
PROVIDE(_end = .);
. = . + _heap_size;
. = . + _stack_size;
. = ALIGN(8);
} > RAM
/* 栈顶地址 */
_estack = ORIGIN(RAM) + LENGTH(RAM);
/* 内存使用统计 */
_Min_Heap_Size = _heap_size;
_Min_Stack_Size = _stack_size;
/* 内存区域使用情况 */
_flash_used = _etext - ORIGIN(FLASH) + (_edata - _sdata);
_ram_used = _ebss - _sdata + _stack_size + _heap_size;
/* 丢弃不需要的段 */
/DISCARD/ :
{
libc.a(*)
libm.a(*)
libgcc.a(*)
*(.note.GNU-stack)
*(.comment)
}
}
4.5 链接脚本使用技巧
1. 内存区域别名
ld
/* 使用REGION_ALIAS简化多配置链接脚本 */
REGION_ALIAS("TEXT_REGION", FLASH);
REGION_ALIAS("DATA_REGION", RAM);
SECTIONS
{
.text : { *(.text) } > TEXT_REGION
.data : { *(.data) } > DATA_REGION
}
2. 条件包含
ld
/* 根据符号定义条件包含段 */
SECTIONS
{
.text :
{
*(.text)
/* 仅在定义DEBUG符号时包含调试段 */
_debug_section_start = DEFINED(DEBUG) ? . : 0;
*(.debug_info)
_debug_section_end = DEFINED(DEBUG) ? . : 0;
} > FLASH
}
3. 段排序
ld
SECTIONS
{
.text :
{
/* 按名称排序 */
*(SORT(.text.*))
/* 按名称逆序 */
*(SORT_BY_NAME(.init_array.*))
/* 按对齐值排序 */
*(SORT_BY_ALIGNMENT(.data.*))
/* 按初始化优先级排序 */
KEEP(*(SORT(.init_array.*)))
} > FLASH
}
4. 垃圾回收保护
ld
SECTIONS
{
.text :
{
/* KEEP防止段被gc-sections删除 */
KEEP(*(.isr_vector))
KEEP(*(.init))
KEEP(*(.fini))
*(.text)
} > FLASH
}
5. 多内存区域分配
ld
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
DTCM (rw) : ORIGIN = 0x20000000, LENGTH = 64K
ITCM (rx) : ORIGIN = 0x00000000, LENGTH = 16K
}
SECTIONS
{
/* 关键代码放在ITCM */
.fastcode :
{
*(.fastcode)
*(.critical*)
} > ITCM AT > FLASH
/* 快速数据放在DTCM */
.fastdata :
{
*(.fastdata)
} > DTCM AT > FLASH
}
5. 变量与函数的存储位置和生命周期
5.1 存储区域分类
在嵌入式MCU中,主要的存储区域包括:
| 存储区域 | 类型 | 特点 | 用途 |
|---|---|---|---|
| Flash/XIP | 非易失 | 掉电保持,读取速度快,写入慢 | 代码、常量、初始化数据 |
| RAM | 易失 | 读写速度快,掉电丢失 | 变量、栈、堆 |
5.2 变量存储详解
全局变量
c
/* 全局变量 - 文件作用域,外部链接 */
/* 初始化的全局变量 */
int g_initialized_var = 100; // .data段 (RAM),初始值在Flash
int g_initialized_arr[10] = {1,2,3}; // .data段 (RAM)
/* 未初始化的全局变量 */
int g_uninitialized_var; // .bss段 (RAM),初始化为0
int g_uninitialized_arr[100]; // .bss段 (RAM)
/* 生命周期:程序启动到结束 */
/* 存储位置:RAM (.data 或 .bss) */
/* 初始化时机:程序启动时 */
静态全局变量
c
/* 静态全局变量 - 文件作用域,内部链接 */
static int s_initialized_var = 200; // .data段 (RAM)
static int s_uninitialized_var; // .bss段 (RAM)
/* 生命周期:程序启动到结束 */
/* 存储位置:RAM (.data 或 .bss) */
/* 作用域:仅本文件可见 */
局部变量
c
void function(void)
{
/* 自动局部变量 - 栈上分配 */
int local_var = 10; // 栈 (RAM)
int local_arr[50]; // 栈 (RAM)
/* 生命周期:函数调用期间 */
/* 存储位置:栈 (RAM) */
/* 初始化时机:每次函数调用 */
/* 寄存器变量 - 建议使用寄存器 */
register int reg_var = 20; // 寄存器或栈 (RAM)
}
静态局部变量
c
void function(void)
{
/* 静态局部变量 */
static int s_local_initialized = 30; // .data段 (RAM)
static int s_local_uninitialized; // .bss段 (RAM)
/* 生命周期:程序启动到结束 */
/* 存储位置:RAM (.data 或 .bss) */
/* 作用域:仅本函数可见 */
/* 初始化时机:首次执行时(实际是启动时) */
}
const修饰的变量
c
/* const全局变量 */
const int g_const_var = 100; // .rodata段 (Flash)
const char g_const_str[] = "Hello"; // .rodata段 (Flash)
/* const局部变量 */
void function(void)
{
const int local_const = 50; // 栈或寄存器 (RAM)
// 或被优化为立即数
}
/* 注意:const变量仍可能被修改(通过指针),但应该避免 */
volatile修饰的变量
c
/* volatile变量 - 每次都从内存读取 */
volatile int g_volatile_var; // RAM,禁止优化
volatile uint32_t *REG = (uint32_t *)0x40000000; // 外设寄存器
/* 常见的volatile使用场景:
1. 硬件寄存器
2. 中断服务程序中修改的变量
3. 多线程共享的变量
*/
5.3 函数存储详解
普通函数
c
/* 普通函数 - 默认外部链接 */
void normal_function(void)
{
// 函数体
}
/* 存储位置:.text段 (Flash) */
/* 生命周期:程序运行期间 */
/* 可见性:整个程序可见 */
静态函数
c
/* 静态函数 - 内部链接 */
static void static_function(void)
{
// 函数体
}
/* 存储位置:.text段 (Flash) */
/* 生命周期:程序运行期间 */
/* 可见性:仅本文件可见 */
内联函数
c
/* 内联函数 */
static inline void inline_function(void)
{
// 函数体
}
/* 存储位置:可能内联到调用处,不生成独立函数 */
/* 如果没有内联,仍存储在 .text 段 */
/* 强制内联 */
__attribute__((always_inline))
static inline void force_inline_function(void)
{
// 函数体
}
在RAM中运行的函数
c
/* 需要在RAM中运行的函数 */
/* 场景:Flash编程、低功耗模式等 */
__attribute__((section(".ramcode")))
void flash_write_function(void)
{
// Flash编程时不能从Flash执行代码
}
/* 需要链接脚本支持:
.ramcode :
{
*(.ramcode)
} > RAM AT > FLASH
*/
5.4 存储位置总结表
| 变量/函数类型 | 存储段 | 存储介质 | 生命周期 | 初始化时机 |
|---|---|---|---|---|
| 全局变量(初始化) | .data | RAM | 程序运行期 | 启动时复制 |
| 全局变量(未初始化) | .bss | RAM | 程序运行期 | 启动时清零 |
| static全局变量 | .data/.bss | RAM | 程序运行期 | 启动时 |
| 局部变量 | 栈 | RAM | 函数调用期 | 每次调用 |
| static局部变量 | .data/.bss | RAM | 程序运行期 | 启动时 |
| const全局变量 | .rodata | Flash | 程序运行期 | 编译时 |
| const局部变量 | 栈/立即数 | RAM | 函数调用期 | 每次调用 |
| 普通函数 | .text | Flash | 程序运行期 | - |
| static函数 | .text | Flash | 程序运行期 | - |
| RAM函数 | .ramcode | RAM | 程序运行期 | 启动时复制 |
5.5 内存布局示例
┌─────────────────────────────────────┐ 0x20020000
│ 栈区 (Stack) │ ← _estack (栈顶)
│ ↓ 向下增长 │
├─────────────────────────────────────┤
│ │
│ 堆区 (Heap) │
│ ↑ 向上增长 │
│ │
├─────────────────────────────────────┤ ← _ebss
│ .bss段 │
│ (未初始化的全局/静态变量) │
├─────────────────────────────────────┤ ← _sbss
│ .data段 │
│ (已初始化的全局/静态变量) │
└─────────────────────────────────────┘ ← _sdata, 0x20000000
┌─────────────────────────────────────┐ 0x08080000
│ │
│ (未使用区域) │
│ │
├─────────────────────────────────────┤ ← _etext
│ .rodata段 │
│ (const常量数据) │
├─────────────────────────────────────┤
│ .text段 │
│ (代码段) │
├─────────────────────────────────────┤
│ .isr_vector │
│ (中断向量表) │
└─────────────────────────────────────┘ ← 0x08000000
5.6 代码示例与验证
c
/* storage_demo.c - 存储位置演示 */
#include <stdint.h>
/* Flash存储 (.rodata) */
const char version_string[] = "Firmware v1.0.0";
const uint32_t crc_table[256] = { /* ... */ };
/* RAM存储 (.data) - 初始化 */
int system_status = 1;
static int error_count = 0;
/* RAM存储 (.bss) - 未初始化 */
uint8_t rx_buffer[1024];
static uint32_t tick_counter;
/* 栈存储 */
void process_data(int param) // param在栈上
{
int local_temp; // 栈上
static int call_count; // .bss
call_count++;
local_temp = param * 2;
}
/* Flash存储 (.text) */
void normal_function(void)
{
// 函数代码存储在Flash
}
/* RAM存储 (.ramcode) - 用于Flash编程 */
__attribute__((section(".ramcode")))
int flash_program(uint32_t addr, const void *data, uint32_t len)
{
volatile uint32_t *flash_reg = (uint32_t *)0x40022000;
// ... Flash编程操作
return 0;
}
/* 验证存储位置的函数 */
void print_memory_locations(void)
{
extern uint32_t _stext, _etext;
extern uint32_t _sdata, _edata;
extern uint32_t _sbss, _ebss;
printf("Code: 0x%08X - 0x%08X\n", &_stext, &_etext);
printf("Data: 0x%08X - 0x%08X\n", &_sdata, &_edata);
printf("BSS: 0x%08X - 0x%08X\n", &_sbss, &_ebss);
}
6. ELF文件后处理与产物生成
6.1 ELF文件格式简介
┌────────────────────────────────────┐
│ ELF Header │ 文件类型、架构、入口点
├────────────────────────────────────┤
│ Program Headers │ 段信息(用于加载)
├────────────────────────────────────┤
│ │
│ .text │ 代码段
│ .rodata │ 只读数据
│ .data │ 已初始化数据
│ .bss │ 未初始化数据
│ ... │
│ │
├────────────────────────────────────┤
│ Section Headers │ 段详细信息
├────────────────────────────────────┤
│ Symbol Table │ 符号表
├────────────────────────────────────┤
│ String Table │ 字符串表
├────────────────────────────────────┤
│ Debug Info │ 调试信息
└────────────────────────────────────┘
6.2 生成BIN文件
bash
# 基本转换
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
# 仅提取代码段
arm-none-eabi-objcopy -O binary -j .text firmware.elf code.bin
# 多段合并
arm-none-eabi-objcopy -O binary -j .text -j .rodata firmware.elf firmware.bin
# 带段间隙填充
arm-none-eabi-objcopy -O binary --gap-fill=0xFF firmware.elf firmware.bin
# 指定输出字节序
arm-none-eabi-objcopy -O binary --reverse-bytes=4 firmware.elf firmware.bin
生成过程:
ELF文件 BIN文件
┌─────────────┐ ┌─────────────┐
│ ELF Header │ │ │
├─────────────┤ │ │
│ .text │ ──────▶ │ .text内容 │
├─────────────┤ │ .rodata内容 │
│ .rodata │ ──────▶ │ .data内容 │
├─────────────┤ │ │
│ .data │ ──────▶ │ (无段信息) │
├─────────────┤ │ (无符号表) │
│ .bss │ │ │
├─────────────┤ └─────────────┘
│ 符号表等 │
└─────────────┘ (只保留实际的存储内容)
6.3 生成HEX文件
bash
# Intel HEX格式
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
# 带特定参数
arm-none-eabi-objcopy -O ihex \
--set-start=0x08000000 \
--change-addresses=0x08000000 \
firmware.elf firmware.hex
# Motorola S-record格式
arm-none-eabi-objcopy -O srec firmware.elf firmware.s19
Intel HEX格式说明:
:LLAAAATT[DD...]CC
LL - 数据字节数
AAAA - 16位地址
TT - 记录类型
00 - 数据记录
01 - 文件结束
02 - 扩展地址
03 - 起始段地址
04 - 扩展线性地址
05 - 起始线性地址
DD - 数据字节
CC - 校验和
示例:
:020000040800F2 ; 扩展地址 0x0800
:1000000020010020DD050008DD050008DD050008BF
:00000001FF ; 文件结束
6.4 生成反汇编文件(LST)
bash
# 基本反汇编
arm-none-eabi-objdump -d firmware.elf > firmware.lst
# 带源码混合
arm-none-eabi-objdump -d -S firmware.elf > firmware.lst
# 带C++符号解码
arm-none-eabi-objdump -d -S -C firmware.elf > firmware.lst
# 显示所有段
arm-none-eabi-objdump -D -S firmware.elf > firmware.lst
# 显示段头信息
arm-none-eabi-objdump -h -d firmware.elf > firmware.lst
# 仅反汇编特定函数
arm-none-eabi-objdump -d --disassemble=main firmware.elf
反汇编文件示例:
asm
firmware.elf: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00001a34 08000100 08000100 00000100 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000124 08001b38 08001b38 00001b38 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000020 20000000 08001c60 00001c60 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .bss 00000400 20000020 20000020 00001c80 2**2
ALLOC
Disassembly of section .text:
08000100 <main>:
int main(void)
{
8000100: b508 push {r3, lr}
uart_init();
8000102: f7ff ffe5 bl 80000d0 <uart_init>
while(1) {
uart_send_string("Hello\r\n");
8000106: 4805 ldr r0, [pc, #20] ; (800011c <main+0x1c>)
8000108: f7ff fffa bl 8000100 <uart_send_string>
for(int i = 0; i < 1000000; i++);
800010c: 2400 movs r4, #0
800010e: 4c04 ldr r4, [pc, #16] ; (8000120 <main+0x20>)
8000110: 3c01 subs r4, #1
8000112: d1fd bne.n 8000110 <main+0x10>
6.5 生成MAP文件
bash
# 编译时生成map文件
arm-none-eabi-gcc ... -Wl,-Map=firmware.map
# 链接时生成
arm-none-eabi-ld ... -Map=firmware.map
# 使用链接器选项
arm-none-eabi-gcc ... -Wl,--print-map -Wl,--cref
MAP文件内容示例:
Archive member included because of file (symbol)
./lib/libc.a(lib_a-memcpy.o)
main.o (memcpy)
Allocating common symbols
Common symbol size file
g_buffer 0x400 main.o
Memory Configuration
Name Origin Length Attributes
FLASH 0x08000000 0x00080000 xr
RAM 0x20000000 0x00020000 xrw
Linker script and memory map
LOAD main.o
LOAD utils.o
LOAD ./lib/libc.a
.isr_vector 0x08000000 0x1f8
*(.isr_vector)
.isr_vector 0x08000000 0x1f8 startup.o
.text 0x08000200 0x12a34
0x08000200 _stext = .
*(.text)
.text 0x08000200 0x64 main.o
0x08000200 main
0x08000264 uart_init
.text 0x08000264 0x3c utils.o
0x08000264 delay_ms
.data 0x20000000 0x120 load address 0x08012c34
0x20000000 _sdata = .
*(.data)
.data 0x20000000 0x120 main.o
0x20000000 g_initialized_var
.bss 0x20000120 0x400
0x20000120 _sbss = .
*(.bss)
.bss 0x20000120 0x400 main.o
0x20000120 g_buffer
6.6 生成其他格式文件
S-Record文件
bash
arm-none-eabi-objcopy -O srec firmware.elf firmware.s19
S00F00006669726D776172652E73726563F1
S3150800010020010020DD050008DD050008DD050008BF
S31508000110DD050008DD050008DD050008DD050008B0
S70508000000FA
符号表文件
bash
# 生成符号地址表
arm-none-eabi-nm -n firmware.elf > symbols.txt
# 生成带大小的符号表
arm-none-eabi-nm -S firmware.elf > symbols.txt
# 生成用于调试的符号文件
arm-none-eabi-objcopy --only-keep-debug firmware.elf firmware.sym
6.7 批量生成脚本示例
bash
#!/bin/bash
# build_outputs.sh - 批量生成输出文件
ELF_FILE="build/firmware.elf"
OUTPUT_DIR="build/outputs"
# 创建输出目录
mkdir -p $OUTPUT_DIR
# 设置工具链
OBJCOPY=arm-none-eabi-objcopy
OBJDUMP=arm-none-eabi-objdump
NM=arm-none-eabi-nm
SIZE=arm-none-eabi-size
READELF=arm-none-eabi-readelf
# 1. BIN文件
echo "Generating BIN file..."
$OBJCOPY -O binary $ELF_FILE $OUTPUT_DIR/firmware.bin
# 2. HEX文件
echo "Generating HEX file..."
$OBJCOPY -O ihex $ELF_FILE $OUTPUT_DIR/firmware.hex
# 3. S19文件
echo "Generating S19 file..."
$OBJCOPY -O srec $ELF_FILE $OUTPUT_DIR/firmware.s19
# 4. 反汇编文件
echo "Generating LST file..."
$OBJDUMP -d -S -C $ELF_FILE > $OUTPUT_DIR/firmware.lst
# 5. 段大小报告
echo "Generating size report..."
$SIZE -A $ELF_FILE > $OUTPUT_DIR/size_report.txt
# 6. 符号表
echo "Generating symbol table..."
$NM -n -S $ELF_FILE > $OUTPUT_DIR/symbols.txt
# 7. ELF信息
echo "Generating ELF info..."
$READELF -a $ELF_FILE > $OUTPUT_DIR/elf_info.txt
# 8. 分离调试信息
echo "Generating debug symbols..."
$OBJCOPY --only-keep-debug $ELF_FILE $OUTPUT_DIR/firmware.debug
$OBJCOPY --strip-debug $ELF_FILE $OUTPUT_DIR/firmware_stripped.elf
$OBJCOPY --add-gnu-debuglink=$OUTPUT_DIR/firmware.debug $OUTPUT_DIR/firmware_stripped.elf
# 9. 计算校验和
echo "Calculating checksums..."
crc32 $OUTPUT_DIR/firmware.bin > $OUTPUT_DIR/checksum.txt
md5sum $OUTPUT_DIR/firmware.bin >> $OUTPUT_DIR/checksum.txt
echo "All outputs generated in $OUTPUT_DIR"
7. LST文件与MAP文件的应用
7.1 LST文件分析
反汇编文件的作用
- 代码审查 - 验证编译器生成的代码是否正确
- 性能优化 - 分析热点代码的汇编实现
- 问题定位 - 定位死机、异常的根本原因
- 学习理解 - 学习编译器如何将C代码转换为机器码
代码大小分析
bash
# 分析各函数的代码大小
arm-none-eabi-nm -S --size-sort firmware.elf | grep -E " T | t "
# 输出示例
08001234 00000010 T small_function
08001400 00000050 T medium_function
08002000 00000200 T large_function
性能优化分析
c
// 原始C代码
int sum_array(int *arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
return sum;
}
asm
# -O0 优化级别的反汇编
08000100 <sum_array>:
8000100: push {r4, r5, lr}
8000102: mov r5, r0 ; arr
8000104: mov r4, r1 ; n
8000106: movs r0, #0 ; sum = 0
8000108: cmp r4, #0
800010a: beq.n 800011a <sum_array+0x1a>
800010c: movs r3, #0 ; i = 0
800010e: ldr r2, [r5, r3, lsl #2]
8000110: adds r0, r0, r2
8000112: adds r3, #1
8000114: cmp r3, r4
8000116: blt.n 800010e <sum_array+0xe>
8000118: pop {r4, r5, pc}
asm
# -O3 优化级别的反汇编
08000100 <sum_array>:
8000100: cmp r1, #0
8000102: beq.n 8000114 <sum_array+0x14>
8000104: push {r4, r5}
8000106: movs r2, #0
8000108: movs r3, #0
800010a: ldmia r0!, {r4, r5}
800010c: adds r3, r4
800010e: adds r3, r5
8000110: cmp r2, r1
8000112: blt.n 800010a <sum_array+0xa>
8000114: mov r0, r3
8000116: pop {r4, r5}
8000118: bx lr
7.2 MAP文件分析
段布局分析
# 从MAP文件查看内存布局
Memory Configuration
Name Origin Length Attributes
FLASH 0x08000000 0x00080000 xr
RAM 0x20000000 0x00020000 xrw
# 分析各段大小
.text 0x08000100 0x1234 (4660 bytes)
.rodata 0x08001334 0x0200 (512 bytes)
.data 0x20000000 0x0100 (256 bytes)
.bss 0x20000100 0x0400 (1024 bytes)
函数地址定位
# MAP文件中的函数地址信息
.text.main
0x08000100 0x64 main.o
0x08000100 main
.text.uart_init
0x08000164 0x3c uart.o
0x08000164 uart_init
.text.uart_send
0x080001a0 0x28 uart.o
0x080001a0 uart_send
全局变量定位
# MAP文件中的全局变量信息
.data.g_buffer
0x20000000 0x400 main.o
0x20000000 g_buffer
.bss.g_rx_buffer
0x20000500 0x200 main.o
0x20000500 g_rx_buffer
7.3 死机重启定位应用
步骤1:获取死机地址
c
// 在HardFault_Handler中保存死机信息
void HardFault_Handler(void)
{
uint32_t stack_ptr;
uint32_t pc_value;
__asm volatile (
"mov %0, sp\n"
"ldr %1, [sp, #24]\n" // 获取PC值
: "=r" (stack_ptr), "=r" (pc_value)
);
// 保存到备份寄存器或Flash
RTC->BKP0R = pc_value;
while(1);
}
步骤2:使用addr2line定位
bash
# 使用死机时的PC值定位代码位置
arm-none-eabi-addr2line -e firmware.elf -f -C 0x08001234
# 输出示例
process_data
src/data_processing.c:42
步骤3:使用LST文件详细分析
bash
# 在反汇编文件中查找地址
grep -n "8001234:" firmware.lst
# 或查看地址范围
arm-none-eabi-objdump -d --start-address=0x08001230 --stop-address=0x08001250 firmware.elf
asm
# 详细反汇编
8001230: 6813 ldr r3, [r2, #0]
8001232: 2b00 cmp r3, #0
8001234: d002 beq.n 800123c <process_data+0x28> ← 死机地址
8001236: 681b ldr r3, [r3, #0] ; 可能是空指针解引用
8001238: 601b str r3, [r3, #0]
800123a: e002 b.n 8001242
步骤4:使用MAP文件查找函数
bash
# 在MAP文件中查找地址对应的函数
grep -B5 "0x08001234" firmware.map
7.4 资源优化应用
段大小分析脚本
bash
#!/bin/bash
# analyze_size.sh - 分析各段和函数大小
echo "=== 段大小分析 ==="
arm-none-eabi-size -A firmware.elf
echo ""
echo "=== 最大函数TOP10 ==="
arm-none-eabi-nm -S --size-sort firmware.elf | grep -E " T " | tail -10
echo ""
echo "=== 最大全局变量TOP10 ==="
arm-none-eabi-nm -S --size-sort firmware.elf | grep -E " D | B " | tail -10
echo ""
echo "=== 段详细分析 ==="
arm-none-eabi-objdump -h firmware.elf
优化建议
bash
# 查找未使用的函数(如果使用了gc-sections)
grep "removing" firmware.map
# 输出示例:
Removing unused input section .text.unused_function from main.o
Removing unused input section .rodata.unused_string from main.o
# 查找被折叠的相同函数
grep "folded" firmware.map
7.5 实用分析工具
函数调用图生成
bash
# 使用nm和cscope生成调用关系
arm-none-eabi-nm -C firmware.elf > symbols.txt
# 使用objdump提取调用关系
arm-none-eabi-objdump -d firmware.elf | grep "bl\|b\." > calls.txt
栈使用分析
c
// 编译时使用 -fstack-usage 选项
// arm-none-eabi-gcc -fstack-usage -c main.c
// 生成的 main.su 文件内容示例:
// main.c:10:5:void process_data(void) 16 static
// main.c:25:5:void complex_calculation(void) 256 dynamic
内存映射可视化
bash
# 使用readelf提取段信息并生成报告
arm-none-eabi-readelf -S firmware.elf > sections.txt
# 解析MAP文件生成内存布局图
python3 generate_memory_map.py firmware.map
8. Ninja与GN项目管理框架
8.1 GN (Generate Ninja) 简介
GN是一个元构建系统,用于生成Ninja构建文件。GN的主要特点:
- 快速 - 比CMake快很多
- 简洁 - 语法简单直观
- 可扩展 - 支持自定义模板
- 跨平台 - 支持多种平台和工具链
8.2 GN基本语法
BUILD.gn基本结构
gn
# BUILD.gn - 基本示例
# 声明可执行目标
executable("firmware") {
sources = [
"src/main.c",
"src/uart.c",
"src/timer.c",
]
include_dirs = [
"include",
"//third_party/cmsis/include",
]
deps = [
":startup",
"//third_party/cmsis",
"//drivers/uart",
]
}
# 声明源集(一组源文件)
source_set("startup") {
sources = [
"src/startup.c",
"src/system_init.c",
]
}
# 声明静态库
static_library("mylib") {
sources = [
"lib/mylib.c",
]
public = [
"include/mylib.h",
]
}
配置声明
gn
# config.gn - 配置声明
config("compiler_config") {
# 编译器标志
cflags = [
"-Wall",
"-Wextra",
"-Werror",
"-O2",
"-g",
"-mcpu=cortex-m4",
"-mthumb",
"-mfloat-abi=hard",
"-mfpu=fpv4-sp-d16",
"-ffunction-sections",
"-fdata-sections",
]
# 预处理器定义
defines = [
"STM32F407xx",
"USE_HAL_DRIVER",
"DEBUG=1",
]
# 包含目录
include_dirs = [
"include",
"//third_party/cmsis/include",
]
}
config("linker_config") {
# 链接器标志
ldflags = [
"-T//ld/stm32f407.ld",
"-Wl,--gc-sections",
"-Wl,-Map=fake",
"-nostdlib",
"-static",
]
# 库
libs = [
"c",
"m",
"nosys",
]
}
目标类型
gn
# 可执行文件
executable("firmware") {
sources = [ "main.c" ]
configs += [ ":compiler_config" ]
}
# 静态库
static_library("mylib") {
sources = [ "lib.c" ]
}
# 共享库
shared_library("myso") {
sources = [ "lib.c" ]
}
# 源文件集
source_set("common") {
sources = [ "common.c" ]
}
# 动作(自定义命令)
action("generate_version") {
script = "scripts/generate_version.py"
outputs = [ "$target_gen_dir/version.h" ]
args = [ "--output", rebase_path(outputs[0], root_build_dir) ]
}
# 动作副本
copy("copy_headers") {
sources = [ "include/header.h" ]
outputs = [ "$root_out_dir/include/header.h" ]
}
# 组(聚合目标)
group("all") {
deps = [
":firmware",
":generate_docs",
]
}
8.3 完整嵌入式项目示例
项目结构
project/
├── BUILD.gn # 主构建文件
├── BUILD.gn # 配置文件
├── .gn # GN配置文件
├── args.gn # 构建参数
├── src/
│ ├── BUILD.gn
│ ├── main.c
│ ├── startup.c
│ └── system.c
├── drivers/
│ ├── BUILD.gn
│ ├── uart/
│ │ ├── BUILD.gn
│ │ ├── uart.c
│ │ └── uart.h
│ └── gpio/
│ ├── BUILD.gn
│ ├── gpio.c
│ └── gpio.h
├── hal/
│ ├── BUILD.gn
│ └── stm32f4xx_hal.c
├── third_party/
│ └── cmsis/
│ ├── BUILD.gn
│ └── include/
├── ld/
│ └── stm32f407.ld
└── toolchain/
└── arm_gcc.gni
顶层配置文件
gn
# .gn - GN配置文件
buildconfig = "//toolchain/BUILDCONFIG.gn"
# 默认工具链
set_default_toolchain("//toolchain:arm_gcc")
gn
# toolchain/BUILDCONFIG.gn
# 默认配置
_default_compiler_config = "//:compiler_config"
_default_linker_config = "//:linker_config"
set_default_toolchains() {
default_toolchain = "//toolchain:arm_gcc"
}
工具链定义
gn
# toolchain/arm_gcc.gni
# 工具链前缀
arm_toolchain_prefix = "arm-none-eabi-"
# 工具链路径
arm_toolchain_dir = "//toolchain/gcc-arm"
gn
# toolchain/BUILD.gn
import("//toolchain/arm_gcc.gni")
# 定义ARM GCC工具链
toolchain("arm_gcc") {
# 工具定义
tool("cc") {
depfile = "{{output}}.d"
command = "${arm_toolchain_prefix}gcc {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} -c {{source}} -o {{output}}"
depsformat = "gcc"
description = "CC {{output}}"
outputs = [
"{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o",
]
}
tool("cxx") {
depfile = "{{output}}.d"
command = "${arm_toolchain_prefix}g++ {{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}} -c {{source}} -o {{output}}"
depsformat = "gcc"
description = "CXX {{output}}"
outputs = [
"{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o",
]
}
tool("asm") {
command = "${arm_toolchain_prefix}gcc {{defines}} {{include_dirs}} {{asmflags}} -c {{source}} -o {{output}}"
description = "ASM {{output}}"
outputs = [
"{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o",
]
}
tool("alink") {
rspfile = "{{output}}.rsp"
rspfile_content = "{{inputs}}"
command = "${arm_toolchain_prefix}ar rcs {{output}} @{{rspfile}}"
description = "AR {{target_output_name}}{{output_extension}}"
outputs = [
"{{target_out_dir}}/{{target_output_name}}{{output_extension}}",
]
default_output_extension = ".a"
output_prefix = "lib"
}
tool("solink") {
command = "${arm_toolchain_prefix}ld -shared {{ldflags}} -o {{output}} {{inputs}} {{libs}}"
description = "SOLINK {{target_output_name}}"
}
tool("link") {
outfile = "{{target_output_name}}{{output_extension}}"
rspfile = "{{output}}.rsp"
rspfile_content = "{{inputs}}"
command = "${arm_toolchain_prefix}g++ {{ldflags}} -o {{output}} @{{rspfile}} {{libs}}"
description = "LINK {{outfile}}"
default_output_extension = ".elf"
outputs = [
"{{target_out_dir}}/$outfile",
]
}
tool("stamp") {
command = "touch {{output}}"
description = "STAMP {{output}}"
}
tool("copy") {
command = "cp -f {{source}} {{output}}"
description = "COPY {{source}} {{output}}"
}
}
主BUILD.gn
gn
# BUILD.gn - 主构建文件
import("//toolchain/arm_gcc.gni")
# 编译器配置
config("compiler_config") {
cflags = [
"-Wall",
"-Wextra",
"-Werror",
"-std=c11",
"-O2",
"-g",
"-mcpu=cortex-m4",
"-mthumb",
"-mfloat-abi=hard",
"-mfpu=fpv4-sp-d16",
"-ffunction-sections",
"-fdata-sections",
"-ffreestanding",
"-fno-builtin",
]
defines = [
"STM32F407xx",
"USE_HAL_DRIVER",
"HSE_VALUE=8000000",
]
include_dirs = [
"//include",
"//third_party/cmsis/include",
"//hal/include",
]
}
# 链接器配置
config("linker_config") {
ldflags = [
"-mcpu=cortex-m4",
"-mthumb",
"-mfloat-abi=hard",
"-mfpu=fpv4-sp-d16",
"-T//ld/stm32f407.ld",
"-Wl,--gc-sections",
"-Wl,-Map=fake",
"-nostartfiles",
"-nostdlib",
"-static",
"-Wl,--start-group",
"-lgcc",
"-lc",
"-lm",
"-lnosys",
"-Wl,--end-group",
]
}
# 主固件目标
executable("firmware") {
configs += [
":compiler_config",
":linker_config",
]
deps = [
"//src",
"//drivers",
"//hal",
"//third_party/cmsis",
]
}
# 后处理动作
action("generate_bin") {
deps = [ ":firmware" ]
script = "//scripts/generate_bin.py"
sources = [ "$root_out_dir/firmware.elf" ]
outputs = [ "$root_out_dir/firmware.bin" ]
args = [
"--input", rebase_path(sources[0], root_build_dir),
"--output", rebase_path(outputs[0], root_build_dir),
]
}
action("generate_hex") {
deps = [ ":firmware" ]
script = "//scripts/generate_hex.py"
sources = [ "$root_out_dir/firmware.elf" ]
outputs = [ "$root_out_dir/firmware.hex" ]
args = [
"--input", rebase_path(sources[0], root_build_dir),
"--output", rebase_path(outputs[0], root_build_dir),
]
}
# 构建所有产物
group("all") {
deps = [
":firmware",
":generate_bin",
":generate_hex",
]
}
# 默认目标
default("all")
子目录BUILD.gn
gn
# src/BUILD.gn
source_set("src") {
sources = [
"main.c",
"startup.c",
"system.c",
]
deps = [
"//drivers",
"//hal",
]
}
gn
# drivers/BUILD.gn
group("drivers") {
deps = [
"uart",
"gpio",
"spi",
"i2c",
]
}
gn
# drivers/uart/BUILD.gn
source_set("uart") {
sources = [
"uart.c",
]
public = [
"uart.h",
]
deps = [
"//hal",
]
}
8.4 Ninja构建系统
Ninja基本概念
Ninja是一个小型的构建系统,专注于构建速度。它使用简单的构建文件描述依赖关系。
Ninja文件语法
ninja
# 基本规则定义
rule cc
command = arm-none-eabi-gcc $cflags -c $in -o $out
description = CC $out
rule link
command = arm-none-eabi-gcc $ldflags $in -o $out
description = LINK $out
rule objcopy
command = arm-none-eabi-objcopy -O $format $in $out
description = OBJCOPY $out
# 变量定义
cflags = -Wall -O2 -mcpu=cortex-m4 -mthumb
ldflags = -T stm32f407.ld -Wl,--gc-sections
# 构建语句
build main.o: cc main.c
build uart.o: cc uart.c
build firmware.elf: link main.o uart.o
build firmware.bin: objcopy firmware.elf
format = binary
# 默认目标
default firmware.bin
GN生成的Ninja文件
当运行GN时,它会分析BUILD.gn文件并生成build.ninja文件:
bash
# 生成Ninja文件
gn gen out/debug
# 生成的目录结构
out/debug/
├── build.ninja # 主Ninja文件
├── build.ninja.d # 依赖信息
├── obj/ # 目标文件
│ ├── src/
│ ├── drivers/
│ └── ...
├── toolchain.ninja # 工具链规则
└── args.gn # 构建参数
8.5 构建流程
完整构建流程
1. 配置阶段
┌─────────────┐
│ BUILD.gn │
│ *.gn │
└──────┬──────┘
│ gn gen
▼
┌─────────────┐
│ build.ninja │
│ *.ninja │
└──────┬──────┘
2. 编译阶段
┌─────────────┐
│ *.c/*.cpp │
│ *.s/*.S │
└──────┬──────┘
│ ninja
▼
┌─────────────┐
│ *.o │
└──────┬──────┘
│
▼
┌─────────────┐
│ *.elf │
└──────┬──────┘
│
▼
┌─────────────┐
│ *.bin/*.hex │
└─────────────┘
构建命令
bash
# 生成Ninja文件
gn gen out/debug
# 带参数生成
gn gen out/debug --args='
target_cpu="arm"
is_debug=true
optimize_for_size=true
'
# 编译
ninja -C out/debug
# 编译特定目标
ninja -C out/debug firmware.bin
# 清理
ninja -C out/debug -t clean
# 查看依赖
ninja -C out/debug -t deps firmware.elf
# 查看构建图
ninja -C out/debug -t query firmware.elf
8.6 构建配置管理
多配置支持
gn
# args.gn - 构建参数
# 目标CPU
target_cpu = "arm"
# 调试/发布
is_debug = true
# 优化选项
optimize_for_size = true
# 工具链选择
use_gcc_toolchain = true
gn
# BUILD.gn - 根据配置选择不同选项
import("//build/args.gni")
config("compiler_config") {
cflags = []
if (optimize_for_size) {
cflags += [ "-Os" ]
} else if (is_debug) {
cflags += [ "-O0", "-g3" ]
} else {
cflags += [ "-O3" ]
}
if (target_cpu == "arm") {
cflags += [
"-mcpu=cortex-m4",
"-mthumb",
]
}
}
条件编译
gn
# BUILD.gn - 条件编译示例
source_set("hal") {
sources = [ "hal_common.c" ]
if (defined(target_soc) && target_soc == "stm32f407") {
sources += [
"stm32f4xx_hal.c",
"stm32f4xx_hal_uart.c",
]
defines = [ "STM32F407xx" ]
} else if (target_soc == "stm32f103") {
sources += [
"stm32f1xx_hal.c",
"stm32f1xx_hal_uart.c",
]
defines = [ "STM32F103xx" ]
}
}
9. 资深工程师经验与技巧
9.1 编译优化经验
选择正确的优化级别
┌────────────┬─────────────────────────────────────────────────────┐
│ 优化级别 │ 适用场景 │
├────────────┼─────────────────────────────────────────────────────┤
│ -O0 │ 调试阶段,完全无优化,便于单步调试 │
│ -Og │ 调试优化,保留调试信息同时做基本优化 │
│ -O1 │ 基本优化,编译时间短,适度优化 │
│ -O2 │ 推荐级别,平衡代码大小和性能 │
│ -Os │ 嵌入式首选,优化代码大小,可能牺牲部分性能 │
│ -O3 │ 性能优先,可能显著增加代码大小 │
│ -Ofast │ 极致性能,可能违反标准,慎用 │
└────────────┴─────────────────────────────────────────────────────┘
优化建议
bash
# 嵌入式项目推荐配置
arm-none-eabi-gcc \
-Os \ # 优化大小
-ffunction-sections \ # 函数独立段
-fdata-sections \ # 数据独立段
-fno-common \ # 避免common段
-finline-functions-called-once \ # 单次调用函数内联
-fmerge-all-constants \ # 合并常量
-flto \ # 链接时优化
-mcpu=cortex-m4 \
-mthumb
# 链接器配合
arm-none-eabi-ld \
--gc-sections \ # 回收未使用段
--icf=safe \ # 相同代码折叠
--print-gc-sections # 打印回收信息
9.2 调试信息管理
调试信息分离
bash
# 分离调试信息
arm-none-eabi-objcopy --only-keep-debug firmware.elf firmware.debug
arm-none-eabi-objcopy --strip-debug firmware.elf firmware_stripped.elf
arm-none-eabi-objcopy --add-gnu-debuglink=firmware.debug firmware_stripped.elf
# 使用分离的调试信息调试
gdb firmware_stripped.elf
(gdb) symbol-file firmware.debug
调试信息优化
bash
# 减少调试信息大小
-gsplit-dwarf # 分离DWARF信息
-gdwarf-4 # 使用DWARF 4格式
-g1 # 最小调试信息
-ggdb # GDB优化格式
9.3 链接脚本技巧
自动计算校验和
ld
/* 链接脚本中定义校验和计算区域 */
SECTIONS
{
.text :
{
*(.text)
} > FLASH
/* 定义校验和区域 */
.checksum :
{
LONG(CHECKSUM(.text)) /* 计算校验和 */
} > FLASH
/* 或者手动指定 */
_text_checksum = 0;
_text_start = ORIGIN(FLASH);
_text_end = .;
}
版本信息嵌入
ld
/* 在链接脚本中嵌入版本信息 */
_version_major = 1;
_version_minor = 0;
_version_patch = 0;
SECTIONS
{
.version :
{
BYTE(_version_major)
BYTE(_version_minor)
BYTE(_version_patch)
BYTE(0) /* Reserved */
} > FLASH
}
多镜像支持
ld
/* Bootloader和Application镜像 */
MEMORY
{
BOOTLOADER (rx) : ORIGIN = 0x08000000, LENGTH = 32K
APPLICATION (rx) : ORIGIN = 0x08008000, LENGTH = 480K
CONFIG (rx) : ORIGIN = 0x0807C000, LENGTH = 16K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.bootloader :
{
*(.bootloader)
*(.bootloader.*)
} > BOOTLOADER
.application :
{
*(.text)
*(.text*)
*(.rodata*)
} > APPLICATION
.config :
{
*(.config)
} > CONFIG
}
9.4 内存优化技巧
栈使用优化
c
// 避免大数组在栈上
void bad_function(void) {
uint8_t buffer[4096]; // 栈上分配4KB,危险!
// ...
}
// 使用静态或全局缓冲区
static uint8_t shared_buffer[4096];
void good_function(void) {
uint8_t *buffer = shared_buffer;
// ...
}
// 或使用动态分配(如果有堆)
void better_function(void) {
uint8_t *buffer = malloc(4096);
if (buffer) {
// ...
free(buffer);
}
}
内存池管理
c
// 定义内存池
#define POOL_SIZE 4096
static uint8_t memory_pool[POOL_SIZE];
static size_t pool_index = 0;
// 简单的内存分配
void *pool_alloc(size_t size) {
if (pool_index + size > POOL_SIZE) {
return NULL;
}
void *ptr = &memory_pool[pool_index];
pool_index += size;
return ptr;
}
// 重置内存池
void pool_reset(void) {
pool_index = 0;
}
9.5 常见问题解决
问题1:未定义符号
bash
# 错误信息
undefined reference to `xxx'
# 解决方法
1. 检查是否链接了相关库:-L/path/to/lib -lmylib
2. 检查库顺序:被依赖的库放在后面
3. 使用库分组:-Wl,--start-group -la -lb -Wl,--end-group
4. 检查符号是否声明但未定义
问题2:段溢出
bash
# 错误信息
region `RAM' overflowed by 1024 bytes
# 解决方法
1. 分析段大小:arm-none-eabi-size -A firmware.elf
2. 查找大符号:arm-none-eabi-nm -S --size-sort firmware.elf
3. 启用gc-sections
4. 使用-Os优化
5. 减少静态缓冲区大小
问题3:对齐错误
c
// 问题代码
uint32_t *ptr = (uint32_t *)0x20000001; // 未对齐地址
*ptr = 0x12345678; // 可能触发HardFault
// 解决方法
// 1. 确保数据对齐
uint32_t data __attribute__((aligned(4)));
// 2. 使用memcpy处理未对齐数据
uint32_t value;
memcpy(&value, ptr, sizeof(value));
// 3. 使用packed属性
struct __attribute__((packed)) packed_struct {
uint8_t a;
uint32_t b;
};
9.6 性能优化技巧
关键函数优化
c
// 使用RAM运行关键函数
__attribute__((section(".ramcode")))
void flash_program(uint32_t addr, uint32_t data)
{
// Flash编程时需要从RAM运行
}
// 强制内联热点函数
__attribute__((always_inline))
static inline int hot_function(int x)
{
return x * 2;
}
// 使用restrict提示优化
void copy_data(int *restrict dst, const int *restrict src, int n)
{
for (int i = 0; i < n; i++) {
dst[i] = src[i];
}
}
缓存优化
c
// Cortex-M7 D-Cache相关
// 对于DMA缓冲区,需要考虑缓存一致性
// 定义非缓存区域
#define NONCACHEABLE __attribute__((section(".noncacheable")))
uint32_t dma_buffer[256] NONCACHEABLE;
// 或使用SCB_InvalidateDCache和SCB_CleanDCache
void dma_transfer_prepare(void *buffer, size_t size)
{
SCB_CleanDCache_by_Addr((uint32_t *)buffer, size);
}
void dma_transfer_complete(void *buffer, size_t size)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)buffer, size);
}
9.7 工程实践建议
版本控制集成
bash
# 在构建时嵌入Git信息
#!/bin/bash
# generate_version.sh
GIT_HASH=$(git rev-parse --short HEAD)
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
BUILD_TIME=$(date +"%Y-%m-%d %H:%M:%S")
echo "#define GIT_HASH \"$GIT_HASH\"" > version.h
echo "#define GIT_BRANCH \"$GIT_BRANCH\"" >> version.h
echo "#define BUILD_TIME \"$BUILD_TIME\"" >> version.h
静态分析集成
bash
# 使用cppcheck进行静态分析
cppcheck --enable=all \
--std=c11 \
--target=arm-none-eabi \
--include=config.h \
src/
# 使用clang-tidy
clang-tidy src/*.c -- \
-std=c11 \
-target arm-none-eabi \
-I./include
构建日志管理
bash
# 保存详细构建日志
ninja -C out/debug 2>&1 | tee build.log
# 分析编译警告
grep -E "warning:" build.log > warnings.txt
附录
A. 常用工具链命令速查
| 功能 | 命令 |
|---|---|
| 预处理 | gcc -E main.c -o main.i |
| 编译到汇编 | gcc -S main.c -o main.s |
| 编译到目标文件 | gcc -c main.c -o main.o |
| 链接 | ld main.o -o a.out -T script.ld |
| 查看段信息 | objdump -h a.out |
| 反汇编 | objdump -d a.out |
| 查看符号 | nm a.out |
| 生成bin | objcopy -O binary a.out a.bin |
| 生成hex | objcopy -O ihex a.out a.hex |
| 地址转行号 | addr2line -e a.out 0x1234 |
| 查看ELF信息 | readelf -a a.out |
| 删除符号 | strip --strip-all a.out |
| 查看大小 | size a.out |
B. 编译选项速查
| 类别 | 选项 | 说明 |
|---|---|---|
| 优化 | -O0/-O1/-O2/-O3/-Os/-Og | 优化级别 |
| 调试 | -g/-g3/-ggdb | 调试信息 |
| 警告 | -Wall/-Wextra/-Werror | 警告控制 |
| 标准 | -std=c99/c11/c++11/c++17 | 语言标准 |
| 定义 | -Dmacro[=value] | 宏定义 |
| 包含 | -I | 头文件路径 |
| 架构 | -mcpu/-mthumb/-mfloat-abi | 目标架构 |
| 段 | -ffunction-sections/-fdata-sections | 独立段 |
| 链接 | -T/-L /-l | 链接控制 |
C. 常见错误速查
| 错误 | 原因 | 解决方法 |
|---|---|---|
| undefined reference | 符号未定义 | 添加库或定义符号 |
| multiple definition | 符号重复定义 | 使用static或weak |
| region overflowed | 内存不足 | 优化或减少内存使用 |
| relocation truncated | 地址超出范围 | 修改链接脚本 |
| cannot find -lxxx | 库文件未找到 | 检查-L路径 |
| alignment error | 地址未对齐 | 添加对齐属性 |
文档版本 : v1.0
最后更新 : 2026年4月
适用工具链 : ARM GNU Toolchain, RISC-V GCC
作者:GLM5.0
在AI大爆发的时代,作为从业者我们可能更多的是得有知识的广度,未来只会某一个专项的知识可能不太能符合市场需求,AI将很多东西平权带给所有人,我们可能需要借助AI高效的更多积累,做到厚积簿发。