Linux 与 STM32 环境下 C 程序的编译链与内存管理实践
适合人群:Linux C/C++ 开发初学者、跨平台开发技术学习者、嵌入式初学者开发环境目标成果:理解GCC 编译工具链,ubuntu与stm32跨平台代码适配能力开发工具:电脑、STM32F103C8T6、面包板、CH340G模块、杜邦线、stlink电脑需安装好ubuntu、keil5与串口助手,本文不过多赘述,教程可参考其他博主的Ubuntu22.04保姆级安装教程keil5安装与配置江科大
Linux 与 STM32 环境下 C 程序的编译链与内存管理实践
Linux 与 STM32 环境下 C 程序的编译链与内存管理实践
前言
适合人群:Linux C/C++ 开发初学者、跨平台开发技术学习者、嵌入式初学者
开发环境:Ubuntu 22.04、Keil MDK-ARM(Keil5)
目标成果:理解GCC 编译工具链,ubuntu与stm32跨平台代码适配能力
开发工具:电脑、STM32F103C8T6、面包板、CH340G模块、杜邦线、stlink
电脑需安装好ubuntu、keil5与串口助手,本文不过多赘述,教程可参考其他博主的
Ubuntu22.04保姆级安装教程
keil5安装与配置
江科大自化协
一、Linux下静态库.a与动态库.so库文件的生成和使用
1.静态库和动态库
库是一段编译好的二进制代码,加上头文件就可以供别人使用。
库的使用情况分为两种:
(1)一种情况是某些代码需要给别人使用,但是我们不希望别人看到源码,就需要以库的形式进行封装,只暴露出头文件;
(2)另外一种情况是,对于某些不会进行大的改动的代码,我们想减少编译的时间,就可以把它打包成库,因为库是已经编译好的二进制了,编译的时候只需要Link(链接)一下,不会浪费编译时间。
上面提到库在使用的时候需要 Link,Link 的方式有两种,静态和动态,于是便产生了静态库和动态库。
静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。动态库在程序编译时并不会被连接到目标代码汇中,而是在程序运行时才被载入,因此在程序运行时还需要动态库存在。
下面我们举例说明在Linux中如何创建静态库和动态库并使用它们。
2.用GCC生成.a静态库
2.1编译生成例子程序test.h,test.c和main.c
-
创建项目目录
mkdir programming -
进入项目文件夹
cd programming -
用vim创建test.h文件
vim test.hi进入编辑模式,
然后粘贴以下代码:#ifndef TEST_H #define TEST_H void test(const char *name); #endif点击
esc后,输入:wq写入退出
同理:创建test.c文件vim test.c然后粘贴以下代码:
#include <stdio.h> void test(const char *name) { printf("Hello%s!\n",name); }创建main.c文件
vim main.c然后粘贴以下代码:
#include "test.h" int main() { test(" World"); return 0; }
效果如下:



ls能够看到programming目录下出现了test.h,test.c和main.c文件
2.2 生成并使用静态库
生成并使用静态库包含三个步骤:
- 💡第一步:生成.o目标文件
静态库和动态库都是由.o目标文件创建的,因此我们首先将源程序通过gcc编译成.o文件,在终端输入
gcc -c test.c,并利用ls命令查看已经生成了test.o文件

- 💡第二步:由.o文件创建静态库
静态库文件名的命名规范以lib为前缀,后接静态库名,扩展名为.a。下面程序中,创建的静态库名为mytest,则静态库名为libmytest.a。接下来我们使用ar命令创建静态库文件,在终端输入
ar -crv libmytest.a test.o,并使用ls命令查看已生成libmytest.a文件

- 💡第三步:在程序中使用静态库
制作完静态库之后使用它内部的函数只需要在公用函数的源程序中包含公用函数的原型声明,然后用gcc命令生成目标文件时指明静态库名,gcc将会从静态库中将公用函数连接到目标文件中。下面我们生成目标程序test,一共有三种方法:
方法一:
gcc -o test main.c -L. -lmytest
./test
效果如下:成功打印"Hello World!"
方法二:
gcc main.c libmytest.a -o test
./test
效果如下:成功打印"Hello World!"
方法三:
生成main.o之后再生成可执行文件
gcc -c main.c
gcc -o test main.o libmytest.a
./test
效果如下:成功打印"Hello World!"
生成动态库可分成以下三个步骤
- 💡第一步:生成.o目标文件
编译为 位置无关的目标文件(.o) 动态库需要被多个程序共享,且加载到内存的地址不固定,因此必须使用 -fPIC 选项编译目标文件(PIC = Position-Independent Code,位置无关代码)。
fPIC:核心参数,确保生成的目标文件可以被动态库使用(必须加,否则后续生成动态库可能失败)。
gcc -c -fPIC test.c -o test.o # 生成位置无关的 test.o
ls
./test

- 💡第二步:由.o文件创建动态库文件
动态库的命名规则和静态库命名规则类似,也是在动态库名前加前缀lib,但其文件扩展名为.so。
gcc -shared -o libmytest.so test.o
ls
./test
- shared:指定生成动态库(核心参数)。
- o libmytest.so:指定输出的动态库文件名。

- 💡第三步:在程序中使用动态库
使用动态库编译程序的命令与静态库类似,但运行时需要确保系统能找到动态库(否则会报“找不到共享库”的错误)。
编译 main.c 并链接动态库 libmytesh.so:
gcc main.c -o main -L. -lmytest
- 参数含义与静态库相同:
-L.指定库路径(当前目录),-lmytesh引用动态库(libmytesh.so简化为mytesh)。
直接运行 ./main 可能会报错:
原因是系统默认在 /lib、/usr/lib 等目录查找动态库,而我们的 libmytesh.so 在当前目录,需要手动指定路径。
临时解决方法:通过 LD_LIBRARY_PATH 环境变量指定动态库路径(仅当前终端有效):
export LD_LIBRARY_PATH=. # 将当前目录添加到动态库搜索路径
./main # 现在可以正常运行

永久解决方法(可选):
- 将动态库复制到系统默认库目录(如
/usr/lib或/usr/local/lib),需要 root 权限:sudo cp libmytest.so /usr/local/lib/ - 或者在
/etc/ld.so.conf.d/目录下新建一个.conf文件(如mytest.conf),写入动态库所在路径(如/home/tzj/programming),然后执行sudo ldconfig更新缓存。
3.动态库 vs 静态库的核心区别
| 特性 | 静态库(.a) |
动态库(.so) |
|---|---|---|
| 编译时行为 | 完整复制到可执行文件中 | 仅记录引用,不复制内容 |
| 可执行文件大小 | 较大(包含库代码) | 较小(仅包含自身代码) |
| 库更新后 | 必须重新编译程序 | 无需重新编译,直接替换库文件 |
| 运行时依赖 | 不依赖外部库(独立运行) | 依赖动态库存在(否则无法运行) |
总结:生成动态库的核心流程是 gcc -c -fPIC 编译位置无关目标文件 → gcc -shared 链接为 .so 文件,使用时需确保运行环境能找到动态库。
4.多个程序文件的编译
- 进入工作目录:
cd programming
- 创建
sub1.h头文件
#ifndef SUB1_H
#define SUB1_H
// 函数声明:接收两个int类型参数,返回float类型(平均数)
float x2x(int a, int b);
#endif
- 编写
sub1.c程序 —— 子程序文件,实现函数x2x(求平均数)
- 创建sub1.c文件
vim sub1.c
- 编写代码
#include <stdio.h>
float x2x(int a, int b){
return (a+b)*0.5;
}
- 创建
sub2.h头文件
#ifndef SUB2_H
#define SUB2_H
// 函数声明:接收两个int类型参数,返回float类型(求总数)
float x2y(int a, int b);
#endif
- 编写
sub2.c程序 —— 子程序文件,实现函数x2y(求总数)
- 创建sub2.c文件
vim sub2.c
- 编写代码
#include <stdio.h>
float x2y(int a, int b){
return (a+b)*1.0;
}
- 编写
main.c程序—— 主程序,调用x2x、x2y
- 创建main.c文件
vim main.c
- 编写代码
#include <stdio.h>
#include "sub1.h"
#include "sub2.h"
int main(void)
{
int num1=5, num2=4;
float result1=0.0,result2=0.0;
// 调用函数计算
result1 = x2x(num1, num2);
result2 = x2y(num1, num2);
// 输出结果
printf("result1=%.2f\n", result1); // 输出result1的值,保留2位小数
printf("result2=%.2f\n", result2); // 输出result2的值,保留2位小数
return 0;
}
效果如下:
- 编译生成3个目标文件(.o)
使用gcc -c命令将每个.c文件编译为目标文件(仅编译不链接):
# 分别编译main.c、sub1.c、sub2.c为目标文件
gcc -c main.c -o main1.o # 生成主程序目标文件main1.o
gcc -c sub1.c -o sub1.o # 生成x2x函数目标文件sub1.o
gcc -c sub2.c -o sub2.o # 生成x2y函数目标文件sub2.o
执行后,目录下会新增3个.o文件:main1.o、sub1.o、sub2.o。
8. 生成静态库(.a)并链接可执行程序
(1)用ar工具生成静态库
静态库命名规范为libxxx.a(此处命名为libsub.a,包含sub1.o和sub2.o):
ar crv libsub.a sub1.o sub2.o
- 选项说明:
c(创建库)、r(替换库中旧文件)、v(显示过程)。
执行后生成静态库libsub.a。

(2) 链接静态库生成可执行程序
将main1.o与静态库libsub.a链接,生成可执行程序main_static:
gcc main1.o -L. -lsub -o main_static
- 选项说明:
-L.(指定当前目录为库搜索路径)、-lsub(链接libsub.a,省略前缀lib和后缀.a)。
(3)记录静态链接可执行程序的大小
使用size命令查看各段大小(代码段、数据段等):
# 查看各段大小(text:代码段,data:已初始化数据,bss:未初始化数据)
size main_static
结果如下:
运行结果
9.生成动态库(.so)并链接可执行程序
(1)重新编译目标文件(带-fpic选项)
动态库需要“位置无关代码(PIC)”,需重新编译sub1.o和sub2.o(main.o无需PIC):
gcc -c main.c -o main1.o
# 编译sub1.c和sub2.c为位置无关目标文件
gcc -c -fpic sub1.c -o sub1.o
gcc -c -fpic sub2.c -o sub2.o
执行后,目录下会新增3个.o文件:main1.o、sub1.o、sub2.o。
(2)生成动态库
动态库命名规范为libxxx.so(此处命名为libsub.so):
gcc -shared -o libsub.so sub1.o sub2.o
- 选项说明:
-shared(生成动态库)。
执行后生成动态库libsub.so。

(3)链接动态库生成可执行程序
将main1.o与动态库libsub.so链接,生成可执行程序main_dynamic:
gcc main1.o -L. -lsub -o main_dynamic

- 注意:链接命令与静态库相同,但实际会优先链接动态库(若同名静态库和动态库同时存在)。
(4)记录动态链接可执行程序的大小
同样使用size命令:
size main_dynamic

(5)运行动态链接程序(解决库路径问题)
动态链接程序运行时需找到libsub.so,临时指定库路径:
export LD_LIBRARY_PATH=./ # 当前目录添加到动态库搜索路径
./main_dynamic # 运行程序
运行结果:
(6)静态库与动态库链接程序的大小对比
| 可执行程序 | 代码段(text) | 核心差异原因 |
|---|---|---|
main_static |
1707字节 | 当被链接的库代码非常简单时,动态链接所需的 “额外信息” 可能超过静态库代码本身的大小。 |
main_dynamic |
1717字节 | 仅包含动态库(libsub.so)的引用 |
💡但是在工程稍显复杂时:静态库体积将大于动态库。动态链接的可执行文件体积更小,因为它不包含库的具体代码,仅在运行时加载动态库;静态链接包含库代码,体积更大但不依赖外部库。
二、GCC 编译工具集与 ELF 文件格式
1.GCC编辑器背后的故事
(1).GCC编译工具集中各部分的作用
GCC 原名为 GNU C 语言编译器(GNU C Compiler),因为它原本只能处理C语言。GCC 很快地扩展,变得可处理 C++。后来又扩展为能够支持更多编程语言,如Fortran、Pascal、Objective-C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件。
gcc命令下各选项的含义:
-E:仅作预处理,不进行编译、汇编和链接
-S:仅编译到汇编语言,不进行汇编和链接
-c:编译、汇编到目标代码(也就是计算机可识别的二进制)
-o:执行命令后文件的命名
-g:生成调试信息
-w:不生成任何警告
-Wall:生成所有的警告
(2).Binutils工具集
一组二进制程序处理工具,包括:addr2line、ar、 objcopy、objdump、as、ld、ldd、readelf、 size等。这一组工具是开发和调试不可缺少的工具,简介如下:
addr2line:用来将程序地址转换成其所对应的程序源文件及所对应的代码行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对应的源代码位置。
as:主要用于汇编
ld:主要用于链接
ar:主要用于创建静态库,静态库在之后进行了详细解释在此不再赘述
ldd:可以用于查看一个可执行程序依赖的共享库
objcopy:将一种对象文件翻译成另一种格式
objdump:主要作用是反汇编
readelf:显示有关ELF文件的信息
size:列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等
2.GCC的简单编译
✅ 2.1 GCC的编译过程分为 四个阶段。
| 阶段 | 工具 | 输入文件 | 输出文件 | 作用 |
|---|---|---|---|---|
| 1. 预处理(Preprocessing) | cpp |
.c |
.i |
展开头文件、宏替换、条件编译等 |
| 2. 编译(Compilation) | cc1 |
.i |
.s |
将 C 代码翻译为汇编语言 |
| 3. 汇编(Assembly) | as |
.s |
.o |
将汇编代码转为机器码(目标文件) |
| 4. 链接(Linking) | ld |
.o + 库 |
可执行文件 | 合并多个目标文件和库,生成最终程序 |
2.2 首先生成一个简单的hello.c代码
-
进入项目文件夹
cd programming -
用vim创建源文件
vim hello.ci进入编辑模式,
然后粘贴以下代码:#include <stdio.h> int main() { printf("Hello World!\r\n"); return 0; }点击
esc后,输入:wq写入退出
效果如下:

注:ls列出当前目录中所有非隐藏的文件和文件夹
2.3 通过hello.c体现GCC编译过程的 四个阶段
🔹(1)预处理(Preprocessing)
命令:
gcc -E hello.c -o hello.i
预处理命令源文件hello.c文件预处理成test.i,其中-E选项使GCC在进行完预处理后即停止

ls观测到programming目录下有了hello.c和hello.i两个文件
查看hello.i内容:
vim hello.i

:q退出
🔹(2)编译(Compilation)
命令:
gcc -S hello.i -o hello.s
编译过程是对预处理完的文件进行一系列的词法分析,语法分析,词义分析及优化后生成相应的汇编代码,代码将预处理生成的hello.i文件编译成汇编程序hello.s,-S选项使GCC在执行完编译后停止,生成汇编程序
ls观测到programming目录下有了hello.s文件
查看hello.s内容:
vim hello.s

:q退出
🔹 (3)汇编(Assembly)
命令:
gcc -c hello.s -o hello.o

汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀.o的目标文件中,格式为 ELF(Executable and Linkable Format)
- 目标文件包含机器指令、数据、符号表、重定位信息等,但尚未解析外部引用
📌
.o文件是 可重定位文件,还不能直接运行,需要链接。
🔹 (4)链接(Linking)
链接分为静态链接和动态链接,后文会详细讲解静态库和动态库的使用,这里仅做简述
- 静态链接:将库代码直接复制到可执行文件中(体积大,独立运行)
- 动态链接:只记录依赖库名,运行时加载(体积小,共享库)
链接动态库:
命令:
gcc hello.o -o hello
./hello
size hello

链接静态库:
命令:
gcc -static hello.o -o hello
./hello
size hello

| 链接方式 | text 字节数 | data 字节数 | 总大小 (dec) |
|---|---|---|---|
| 动态库 | 1376 | 600 | 1984 |
| 静态库 | 781853 | 23240 | 828109 |
可以看出静态库相比动态库text的代码尺寸变得极大
text 段放的是可执行指令。
动态链接时,printf 等库函数只留一个“桩”(PLT/GOT),真正代码在运行时才从 libc.so 映射进来,所以 text 只有 1 千多字节。
静态链接时,gcc 把 libc.a 里所有被引用的目标文件(printf、write、缓冲区管理、locale、数学函数……)整体复制进可执行文件,结果 text 暴涨到 781 kB,是动态版本的 568 倍。
✅ 一句话:
“text 段从 1 kB 变 780 kB,差了两个数量级,这就是静态链接把整只 libc 搬进来的直接证据。”
3 分析ELF文件
ELF(Executable and Linkable Format)是 Linux 下标准的可执行文件、目标文件(.o)、共享库(.so)和核心转储(core dump)的格式。
3.1 📌 ELF 文件基本结构
+---------------------+
| ELF Header | ← 固定大小,描述整个文件的基本属性
+---------------------+
| Program Headers | ← 描述“运行时”视图,告诉内核如何加载程序(LOAD segments)
+---------------------+
| .text | → 代码段(机器指令)
+---------------------+
| .data | → 已初始化的全局/静态变量
+---------------------+
| .bss | → 未初始化的全局/静态变量(占位,不占磁盘空间)
+---------------------+
| .rodata | → 只读数据(字符串常量等)
+---------------------+
| ... |
+---------------------+
| Section Headers | ← 描述“链接时”视图,列出所有节(sections)
+---------------------+

🔍 注意:
- Program Headers 用于 运行时加载(由操作系统使用)
- Section Headers 用于 链接和调试(由
linker,objdump,readelf使用)
3.2 readelf —— 最专业的 ELF 分析工具
我们可以使用readelf -S查看各个section的信息如下:
readelf -S hello
输出:

3.3 反汇编ELF
反汇编 ELF 文件是深入理解程序底层执行逻辑的关键步骤。通过反汇编,你可以将机器码转换为人类可读的汇编语言,从而分析程序行为、调试问题、学习编译器优化,甚至进行逆向工程。
由于ELF文件无法被当做普通文本文件打开,如果希望直接查看一个ELF文件包含的指令和数据没需要使用反汇编的方法。
使用objdump -D对其进行反汇编
objdump 是 GNU Binutils 的一部分,是 Linux 下最常用的反汇编工具。
命令
objdump -D hello

-D:反汇编所有节(包括.data,.rodata等),不只是可执行代码- 输出包含 地址、机器码、汇编指令
- 适用于分析常量、字符串等数据的编码方式
✅ 总结
| 命令 | 用途 |
|---|---|
readelf -S |
查看节结构,定位代码段 |
objdump -d |
反汇编所有可执行代码 |
objdump -D |
反汇编所有节(含数据) |
掌握这些工具,你就能深入理解程序是如何被构建、加载和运行的,对系统编程、安全分析和性能优化都大有帮助。
三、变量存储分析(Ubuntu vs STM32)
1.C 源程序设计(适用于两个平台)
main.c
#include <stdio.h>
#include <stdlib.h>
// 1. 全局变量
int global_var = 100;
// 2. 全局变量(未初始化)→ 放在 .bss 段
int global_uninit;
// 3. 全局常量(只读)
const int const_global = 200;
// 4. 静态全局变量(作用域限制)
static int static_global = 300;
// 5. 静态局部变量(函数内)
void func(void) {
static int static_local = 400;
printf("static_local: %p\n", &static_local);
}
// 6. 局部变量(栈上)
void print_addresses(void) {
int local_var = 500;
char local_char = 'A';
float local_float = 3.14f;
// 打印所有地址
printf("\n=== 变量地址信息 ===\n");
printf("global_var: %p\n", &global_var);
printf("global_uninit: %p\n", &global_uninit);
printf("const_global: %p\n", &const_global);
printf("static_global: %p\n", &static_global);
printf("local_var: %p\n", &local_var);
printf("local_char: %p\n", &local_char);
printf("local_float: %p\n", &local_float);
// 动态分配堆内存
int *heap_ptr = (int*)malloc(sizeof(int));
if (heap_ptr != NULL) {
*heap_ptr = 600;
printf("heap_ptr: %p\n", heap_ptr);
}
// 调用 static_local
func();
free(heap_ptr);
}
int main(void) {
print_addresses();
return 0;
}
💡 注意:在 STM32 上不能使用
printf直接输出到屏幕,需重定向至串口。我们将使用printf并配置为串口输出。
2.在 Ubuntu (x86) 上运行
步骤 1:编译
gcc -o test_addr main.c
步骤 2:运行
./test_addr
结果如下:
3.📌 在 STM32F103C8T6 上运行(Keil MDK)
3.1. 新建 Keil 工程
- 使用 STM32F103C8T6 或类似芯片
- 使用标准外设库
- 配置串口 1(USART1),波特率 115200
- TX 引脚:PA9
- RX 引脚:PA10
- 数据格式:8 数据位,1 停止位,无校验,无硬件流控
3.2. 添加 main.c 文件
替换默认 main.c,加入以下内容:
修改 main.c(STM32 版本)
#include "stm32f10x.h"
#include "stdio.h"
// 重定向 printf 到 USART1
int fputc(int ch, FILE *f) {
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
USART_SendData(USART1, (uint8_t)ch);
return ch;
}
// 全局变量
int global_var = 100;
int global_uninit;
const int const_global = 200;
static int static_global = 300;
// 函数定义
void func(void) {
static int static_local = 400;
printf("static_local: %p\n", &static_local);
}
void print_addresses(void) {
int local_var = 500;
char local_char = 'A';
float local_float = 3.14f;
printf("\n=== STM32 变量地址信息 ===\n");
printf("global_var: %p\n", &global_var);
printf("global_uninit: %p\n", &global_uninit);
printf("const_global: %p\n", &const_global);
printf("static_global: %p\n", &static_global);
printf("local_var: %p\n", &local_var);
printf("local_char: %p\n", &local_char);
printf("local_float: %p\n", &local_float);
int *heap_ptr = (int*)malloc(sizeof(int));
if (heap_ptr != NULL) {
*heap_ptr = 600;
printf("heap_ptr: %p\n", heap_ptr);
}
func();
free(heap_ptr);
}
int main(void) {
SystemInit(); // 初始化系统时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // TX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // RX
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
printf("=== STM32 地址测试程序 ===\n");
print_addresses();
while (1);
}
3.3. 配置 Keil 项目
- 设置
Target→STM32F10x_Cortex-M3 - 启用
Use MicroLIB(提高兼容性) - 在
Options for Target→Output中勾选Create HEX File - 在
Debug→Settings中选择ST-Link Debugger
3.4. 编译并下载
- 编译成功后,烧录到 STM32
- 打开串口助手(如 XCOM、SecureCRT),设置 115200 波特率
- 观察输出
输出结果如下:
4、地址对比分析(Ubuntu vs STM32)
| 变量类型 | Ubuntu (x86) 虚拟地址示例 | STM32 (Cortex-M3) 物理地址示例 | 存储位置 | 说明 |
|---|---|---|---|---|
| 全局变量(已初始化) | 0x5aa8d053f010 |
0x20000014 |
SRAM(.data) | Ubuntu 为虚拟数据段,STM32 为片内可读写 SRAM 数据段,程序运行时存储。 |
| 全局变量(未初始化) | 0x5aa8d053f020 |
0x20000018 |
SRAM(.bss) | Ubuntu 虚拟未初始化数据段,STM32 为 SRAM 未初始化段,启动时自动清零。 |
| 全局常量 | 0x5aa8d053d004 |
0x08000c6c |
Flash(.rodata) | Ubuntu 虚拟只读数据段,STM32 为片内 Flash 只读区,掉电数据不丢失。 |
| 静态全局变量 | 0x5aa8d053f014 |
0x2000001c |
SRAM(.data) | 作用域限制,Ubuntu 为虚拟数据段,STM32 为 SRAM 数据段。 |
| 局部变量(栈) | 0x7ffd6dfda1a8 |
0x20000608 |
SRAM(栈区) | Ubuntu 栈为虚拟高地址区向下生长,STM32 栈为 SRAM 高地址区向下生长。 |
堆变量(malloc) |
0x5aa9028ba6b0 |
0x20000038 |
SRAM(堆区) | Ubuntu 堆为虚拟中地址区向上生长,STM32 堆为 SRAM 低地址区向上生长。 |
| 静态局部变量 | 0x5aa8d053f018 |
0x20000020 |
SRAM(.data) | 生命周期为程序全程,Ubuntu 为虚拟数据段,STM32 为 SRAM 数据段。 |
5、STM32 存储器地址映射解析(结合 Cortex-M 架构示意图)
Cortex-M 系列(M3、M4)采用统一 4GB 物理地址空间,将所有硬件资源(Flash、SRAM、外设寄存器等)映射到连续地址,各区域功能明确:
| 区域分类 | 地址范围(示例) | 存储/硬件类型 | 典型用途 |
|---|---|---|---|
| Flash 区 | 0x08000000 起始 |
程序存储器 | 存储程序代码、全局常量(如 const_global),掉电不丢失,只读。 |
| SRAM 区 | 0x20000000 起始 |
片内内存 | 存储全局变量(.data/.bss)、局部变量(栈)、动态堆内存,可读写,掉电丢失。 |
| 外设寄存器区 | 0x40000000 起始 |
外设寄存器 | 映射 GPIO、USART、TIM 等外设的控制寄存器,通过地址直接操作外设(如串口发送)。 |
| 预留/扩展区 | 其他地址段 | 预留/外扩硬件 | 可用于外扩 SDRAM、SRAM 等(如 STM32F4 系列支持 FMC 外扩存储)。 |

6、关键结论与总结
6.1. 内存布局核心差异
| 特性 | Ubuntu (x86) | STM32 (Cortex-M3) |
|---|---|---|
| 地址类型 | 虚拟地址(操作系统管理,隔离进程) | 物理地址(直接映射硬件,无地址转换) |
| 存储介质依赖 | 计算机内存(动态分配,容量大) | 片内 Flash + SRAM(容量小,固定) |
| 全局/静态变量 | 虚拟数据段(.data/.bss) | 片内 SRAM 数据段 |
| 全局常量 | 虚拟只读段(.rodata) | 片内 Flash 只读区 |
| 栈/堆管理 | 操作系统动态管理大小 | 编译器/启动文件/链接脚本静态配置大小 |
| 外设访问 | 系统调用/驱动间接访问 | 直接通过物理地址操作外设寄存器 |
- Ubuntu 侧:虚拟地址让多任务、大内存应用更灵活,但需依赖操作系统内存管理单元(MMU)。
- STM32 侧:物理地址直接对应硬件,适合嵌入式实时控制,但内存容量有限,需关注栈/堆溢出风险(可通过 Keil 启动文件或链接脚本调整栈/堆大小)。
通过本次对比,可清晰看到通用计算机与嵌入式处理器在内存管理和硬件映射上的本质差异,为后续嵌入式开发(如驱动编写、内存优化)奠定基础。
💡五、总结
- 库文件实践:静态库包含代码,可执行文件体积大但不依赖外部库;动态库仅存引用,体积小但运行时需加载,需解决路径问题。
- GCC工具集:核心是GCC编译器+Binutils工具集,各组件分工明确(汇编、链接、反汇编、静态库管理等),ELF格式是Linux下目标文件的统一标准。
- 变量存储与ARM映射:Ubuntu(x86_64)用虚拟地址,栈/堆地址范围灵活;STM32(Cortex-M3)用物理地址,存储器映射固定,栈/堆大小受限,需结合硬件资源配置。
📌 如本文对你有帮助,欢迎点赞 ✅、收藏 ⭐、关注 💡,你的支持是我持续创作的最大动力!
💬 有任何问题或建议,欢迎在评论区留言,我会一一回复!
参考文献
GCC编译器背后的故事.pdf
Linux GCC常用命令.pdf
用gcc生成静态库和动态库.pdf
静态库.a与.so库文件的生成与使用.pdf
深入理解GCC:编译器、工具链与库
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)