**前言:**初学C语言的时候是在Linux环境下,那时候只知道需要通过GCC工具编译成可执行文件才可以在运行,后来进入到了嵌入式行业发现需要IDE将一个C项目工程编译成一个固件包,那时候经常会产生一个疑问:一个C项目工程在IDE中是怎么一步步编译成一个固件包的呢?下面就解答一下这个疑问。时光荏苒,也算是给当年刚入行的自己一个答案。
1️⃣,流程简述
将一个 C 项目工程 从 源代码 编译成 固件包(hex、bin、elf 等) 的过程,涉及 编译工具链 和 IDE 的工作原理。一般来说,固件编译流程分为以下关键步骤:
这个过程通常由 编译工具链 (如
GCC
、IAR
、Keil
、Clang
等)完成,而 IDE(如 Keil、STM32CubeIDE、IAR 等)负责调用这些工具链,并提供一个图形化的界面来简化开发流程
bash
源码 (C/ASM/头文件)
↓
预处理器 (Preprocessor)
↓
编译器 (Compiler)
↓
汇编器 (Assembler)
↓
链接器 (Linker)
↓
固件包 (HEX/BIN/ELF 文件)
对应的执行流程框图如下:
2️⃣,预处理(Preprocessing)
预处理器会在编译前对源代码进行 文本替换和展开,主要包括:
- 处理
#include
头文件的引用。- 替换宏定义(
#define
)。- 处理条件编译指令(
#ifdef
、#ifndef
、#endif
等)。- 移除注释。
举个例子:下面的这个是源代码
cpp
#include "my_header.h"
#define LED_PIN 13
void main() {
int pin = LED_PIN;
}
当预处理之后:
cpp
void main() {
int pin = 13;
}
对应的预处理工具与命令:(最经典的就是GCC,当然如果是其他的芯片环境平台可能就需要用到交叉编译工具了)
GCC:
gcc -E main.c -o main.i
3️⃣,编译(Compilation)
预处理完成的
.i
文件(纯文本 C 代码)会被编译器转换成 汇编代码。
- 编译器的任务 :
- 语法分析,检查代码的语法和语义。
- 生成对应的汇编代码(
.s
文件)
同样是上面的例子:编译后的汇编代码:
cpp
mov r0, #13
str r0, [sp, #4]
编译工具:
- GCC:
gcc -S main.i -o main.s
4️⃣,链接(Linking)
链接器的任务是将多个 目标文件(.o 文件) 、库文件(.a 或 .lib 文件) 、启动文件(startup 文件) 合并成一个 可执行的固件文件(.elf/.bin/.hex)。
- 链接的工作内容 :
- 解析和解决函数、变量的外部引用。
- 合并不同模块的目标文件。
- 分配内存地址(根据链接脚本
linker script
)。- 生成可执行文件(如
.elf
、.bin
、.hex
)。链接工具:
- GCC:
gcc main.o -o main.elf -T linker_script.ld
示例:
如果有多个文件:
cpp
main.o // 主程序
startup.o // 启动代码
libc.a // 标准库
链接后的 ELF 文件:
cpp
main.elf
📋 ELF 文件包含的信息:
- 可执行机器指令。
- 符号表、调试信息。
- 内存布局(.text、.data、.bss、堆栈等段)。
5️⃣,转换(Conversion)
编译完成后生成的
ELF
文件,可能还需要转换成 HEX 或 BIN 格式,便于烧录到芯片中。
.elf
文件是包含调试信息的可执行文件,通常用于开发和调试阶段。.hex
文件是Intel HEX 格式的固件包,通常用于烧录工具。.bin
文件是纯二进制格式的固件包。转换工具:
objcopy
工具可以将 ELF 文件转换成 HEX/BIN 文件。
bash
# 转换成 HEX 文件
arm-none-eabi-objcopy -O ihex main.elf main.hex
# 转换成 BIN 文件
arm-none-eabi-objcopy -O binary main.elf main.bin
🛠️ IDE 的编译流程
常用的 IDE(如 Keil、IAR、STM32CubeIDE)会自动调用编译工具链,按以下步骤完成编译:
解析项目文件 (
project.uvprojx
、.cproject
等)。调用编译器 ,对每个源文件进行预处理、编译、汇编,生成
.o
文件。调用链接器 ,根据链接脚本生成
.elf
文件。调用转换工具 ,生成
.hex
或.bin
文件。调用烧录工具,将固件烧录到芯片中(如果配置了烧录选项)。
📌 链接脚本(Linker Script)
链接脚本控制着固件的内存布局,包括:
- 代码段(.text) 放在 Flash 中。
- 初始化数据段(.data) 放在 RAM 中。
- 未初始化数据段(.bss) 放在 RAM 中。
- 堆栈和堆的分配。
典型的链接脚本片段:
cpp
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text : {
*(.text)
*(.rodata)
} > FLASH
.data : {
*(.data)
} > RAM AT > FLASH
.bss : {
*(.bss)
} > RAM
}