C 语言内存管理(一):内存段与布局(.text/.data/.bss/堆/栈)
- C语言进阶
- 6小时前
- 4热度
- 0评论
内存管理 — 内存段与布局
内存管理是嵌入式 C 语言开发的核心难点。本文聚焦于程序内存布局、内存段的逻辑与物理映射、以及嵌入式内存优化实践。
1 程序内存布局
1.1 内存段的基本概念
C 程序运行时,内存被划分为不同的区域,每个区域存储特定类型的数据:
- 代码段(.text):存放程序的机器指令,只读
- 只读数据段(.rodata):存放常量和字符串字面量,只读
- 已初始化数据段(.data):存放有初始值的全局变量和静态变量
- 未初始化数据段(.bss):存放未初始化的全局变量和静态变量,自动清零
- 堆(Heap):动态分配的内存区域,由
malloc/free管理 - 栈(Stack):存放局部变量、函数参数和返回地址,自动管理
编译器将程序分成不同的段(Section):
.text(代码)、.rodata(常量)、.data(已初始化变量)、.bss(未初始化变量)。链接器决定这些段最终存放在内存的哪个位置。
1.2 各段对比

| 区域 | 内容 | 生命周期 | 大小 |
|---|---|---|---|
.text |
机器指令 | 程序运行期间 | 编译时确定 |
.rodata |
const 常量、字符串字面量 |
程序运行期间 | 编译时确定 |
.data |
已初始化的全局/静态变量 | 程序运行期间 | 编译时确定 |
.bss |
未初始化的全局/静态变量 | 程序运行期间 | 编译时确定 |
| Heap | malloc 等动态分配的内存 |
手动 free 释放 |
运行时动态增长 |
| Stack | 局部变量、函数参数、返回地址 | 函数返回自动释放 | 通常 1~8 MB |
在嵌入式系统中,RAM 通常只有几十 KB,栈和堆空间极为有限。了解每个变量落在哪个段,是优化内存的第一步。
不同平台的内存布局:
| 平台 | .text / .rodata |
.data |
.bss / Heap / Stack |
|---|---|---|---|
| 桌面 Linux | RAM(从磁盘加载) | RAM | RAM |
| 嵌入式裸机 | Flash(XIP 原地执行) | Flash 存初值 → 启动时复制到 RAM | RAM |
.data 段最特殊.data 的变量有初始值(如 int g = 42;),又需要运行时修改,所以:
- 编译时:初始值
42存在 Flash(掉电不丢) - 启动时:启动代码把
42从 Flash 复制到 RAM - 运行时:程序在 RAM 里读写这个变量
这就是为什么 .data 会占用两份空间(Flash 存 + RAM 用)。链接脚本(.ld)负责定义这些段在 Flash 和 RAM 中的具体位置。
2 内存段验证实验
2.1 示例:查看变量所在的内存段
// 文件名: memory_layout.c
// 功能: 直观展示不同变量落在哪个内存段
#include <stdio.h>
#include <stdlib.h>
// ---- 全局变量 ----
int g_init = 42; // .data 段(已初始化)
int g_uninit; // .bss 段(未初始化,自动为 0)
const int g_const = 100; // .rodata 段
const char *g_string="helloworld";
static int s_init = 7; // .data 段(static 仍是全局存储)
static int s_uninit; // .bss 段
int main(void) {
// ---- 局部变量 ----
int local = 99; // 栈
static int local_static = 55; // .data 段!不在栈上
int *heap_ptr = (int *)malloc(sizeof(int)); // 堆
*heap_ptr = 888;
printf("====== C 程序内存布局实验 ======\n\n");
printf("--- 代码段 (.text) ---\n");
printf(" main 函数地址: %p\n", (void *)main);
printf("\n--- 只读数据段 (.rodata) ---\n");
printf(" g_const 地址: %p 值=%d\n", (void *)&g_const, g_const);
printf(" \"helloworld\" 字符串: %p (g_string 指向的内容)\n",
(void *)g_string);
printf("\n--- 已初始化数据段 (.data) ---\n");
printf(" g_init 地址: %p 值=%d\n", (void *)&g_init, g_init);
printf(" s_init 地址: %p 值=%d\n", (void *)&s_init, s_init);
printf(" g_string 指针本身: %p (指针变量在 .data)\n",
(void *)&g_string);
printf(" local_static 地址: %p 值=%d\n",
(void *)&local_static, local_static);
printf("\n--- 未初始化数据段 (.bss) ---\n");
printf(" g_uninit 地址: %p 值=%d (自动清零)\n",
(void *)&g_uninit, g_uninit);
printf(" s_uninit 地址: %p 值=%d (自动清零)\n",
(void *)&s_uninit, s_uninit);
printf("\n--- 堆 (Heap) ---\n");
printf(" heap_ptr 指向: %p 值=%d\n", (void *)heap_ptr, *heap_ptr);
printf("\n--- 栈 (Stack) ---\n");
printf(" local 地址: %p 值=%d\n", (void *)&local, local);
printf(" heap_ptr 变量本身地址: %p (指针变量在栈上)\n",
(void *)&heap_ptr);
printf("\n====== 地址规律总结 ======\n");
printf(" .text < .rodata < .data < .bss < Heap <<< Stack\n");
printf(" 低地址 ──────────────────────────────────→ 高地址\n");
free(heap_ptr);
heap_ptr = NULL;
return 0;
}
2.2 编译与运行
# 编译
gcc -g -Wall -o memory_layout memory_layout.c
# 运行
./memory_layout
输出示例:
====== C 程序内存布局实验 ======
--- 代码段 (.text) ---
main 函数地址: 0x62bcfcb7a1c9
--- 只读数据段 (.rodata) ---
g_const 地址: 0x62bcfcb7b008 值=100
"helloworld" 字符串: 0x62bcfcb7b00c (g_string 指向的内容)
--- 已初始化数据段 (.data) ---
g_init 地址: 0x62bcfcb7d010 值=42
s_init 地址: 0x62bcfcb7d014 值=7
g_string 指针本身: 0x62bcfcb7d020 (指针变量在 .data)
local_static 地址: 0x62bcfcb7d018 值=55
--- 未初始化数据段 (.bss) ---
g_uninit 地址: 0x62bcfcb7d02c 值=0 (自动清零)
s_uninit 地址: 0x62bcfcb7d030 值=0 (自动清零)
--- 堆 (Heap) ---
heap_ptr 指向: 0x62bd268c42a0 值=888
--- 栈 (Stack) ---
local 地址: 0x7fffa8d3662c 值=99
heap_ptr 变量本身地址: 0x7fffa8d36630 (指针变量在栈上)
====== 地址规律总结 ======
.text < .rodata < .data < .bss < Heap <<< Stack
低地址 ──────────────────────────────────→ 高地址
运行截图与分析:

const char *g_string = "helloworld";这一行代码涉及两个段:
- 字符串字面量
"helloworld"→.rodata段(只读) - 指针变量
g_string→.data段(可读写)
指针指向的内容和指针变量本身是两个不同的东西,可能在不同的段!
2.3 使用 GCC 验证内存段
# 方法1: 查看汇编代码(编译阶段就能看到段划分)
gcc -S memory_layout.c -o memory_layout.s
cat memory_layout.s | grep -E "\.text|\.rodata|\.data|\.bss" -A 2
# 方法2: 查看目标文件的段信息
gcc -c memory_layout.c -o memory_layout.o
size memory_layout.o # 查看各段大小
nm memory_layout.o | grep g_init # 查看符号所在的段
# 方法3: 查看可执行文件
gcc memory_layout.c -o memory_layout
size memory_layout # 查看各段大小
nm memory_layout | grep -E "g_init|g_uninit|g_const" # 查看符号表
汇编文件中的段标记:
.section .rodata # 只读数据段
.LC0:
.string "x=%d\n"
.text # 代码段
.globl main
main:
...
.data # 已初始化数据段
g_init:
.long 42
.bss # 未初始化数据段
g_uninit:
.zero 4
这就是编译器给数据贴的"逻辑标签"!
size 命令输出验证:
text data bss dec hex filename
2345 600 16 2961 b91 memory_layout
text= 代码段 + 只读数据段data= 已初始化的全局/静态变量bss= 未初始化的全局/静态变量(不占可执行文件空间)
nm 命令输出验证:
0000000000004010 D g_init ← D = .data 段
0000000000004018 D g_string ← D = .data 段(指针变量本身)
000000000000401c B g_uninit ← B = .bss 段
0000000000002004 R g_const ← R = .rodata 段(只读)
0000000000002010 r .LC0 ← r = .rodata 段("helloworld" 字符串)
0000000000004014 d s_init ← d = .data 段(小写 = 局部符号/static)
0000000000004020 b s_uninit ← b = .bss 段(小写 = 局部符号/static)
注意 g_string 和 .LC0(字符串字面量)在不同的段:
g_string在.data(地址 0x4018)—— 指针变量.LC0在.rodata(地址 0x2010)—— 字符串内容
3 自测题
Q1:下列变量分别存储在哪个内存段?
```c
int global_init = 100; // ?
int global_uninit; // ?
const int global_const = 50; // ?
static int static_var = 10; // ?
void func() {
int local = 5; // ?
static int local_s = 20; // ?
int *ptr = malloc(100); // ptr 变量在? ptr 指向的内存在?
}
```
**答案:**
- global_init → .data 段(已初始化全局变量)
- global_uninit → .bss 段(未初始化全局变量)
- global_const → .rodata 段(只读常量)
- static_var → .data 段(static 不影响存储位置)
- local → 栈(局部变量)
- local_s → .data 段(static 局部变量不在栈上!)
- ptr 变量本身 → 栈,ptr 指向的内存 → 堆
Q2:为什么 .data 段在嵌入式系统中会占用两份空间?
因为 .data 段的变量既有初始值,又需要运行时修改:
1. **Flash 存储**:初始值必须存在 Flash 中(掉电不丢失)
2. **RAM 使用**:程序运行时需要在 RAM 中读写
3. **启动复制**:启动代码在 main() 前将初始值从 Flash 复制到 RAM
所以 .data 段占用:Flash 空间(存初值)+ RAM 空间(运行时使用)
Q3:const char *str = "hello"; 涉及几个段?分别是什么?
涉及两个段:
1. **字符串字面量 "hello"** → .rodata 段(只读数据)
2. **指针变量 str** → .data 段(全局变量,可修改指向)
关键理解:指针变量和它指向的内容是两个独立的东西,可能在不同的段!
Q4:如何用 GCC 工具验证变量所在的段?
三种方法:
```bash
# 方法1:查看汇编代码
gcc -S file.c -o file.s
# 查看 .text、.data、.bss、.rodata 标记
# 方法2:查看符号表
gcc -c file.c -o file.o
nm file.o | grep variable_name
# D = .data, B = .bss, R = .rodata, T = .text
# 方法3:查看段大小
size file.o
# 输出 text、data、bss 各段大小
```
Q5:嵌入式系统为什么要避免使用 malloc?
三个主要原因:
1. **内存碎片**:频繁分配/释放导致外部碎片,可用内存变少
2. **不可预测**:动态分配可能失败,难以在编译时确定内存使用量
3. **RAM 有限**:嵌入式 RAM 通常只有几十 KB,堆空间极小
**替代方案**:使用内存池(Memory Pool)—— 预先分配固定大小的块,无碎片,可预测。
Q6:桌面 Linux 和嵌入式裸机的内存布局有什么区别?
| 对比项 | 桌面 Linux | 嵌入式裸机 |
|:---|:---|:---|
| **代码段** | RAM(从磁盘加载) | Flash(XIP 原地执行) |
| **.data 段** | RAM | Flash 存初值 → 启动时复制到 RAM |
| **RAM 大小** | GB 级别 | KB 级别 |
| **虚拟内存** | 有(MMU) | 无(直接物理地址) |
关键:嵌入式系统代码直接在 Flash 中执行,不需要加载到 RAM。
Q7:static int local_var = 10; 在函数内部,它存储在哪里?
**答案:.data 段**
虽然 local_var 是局部变量,但 static 关键字改变了它的存储位置:
- 普通局部变量 → 栈(函数返回后销毁)
- static 局部变量 → .data 段(生命周期是整个程序运行期间)
static 只影响作用域(仅函数内可见),不影响存储位置(仍在全局存储区)。
Q8:.bss 段和 .data 段的主要区别是什么?
| 对比项 | .data 段 | .bss 段 |
|:---|:---|:---|
| **内容** | 已初始化的全局/静态变量 | 未初始化的全局/静态变量 |
| **初始值** | 有明确初始值(如 int g = 42;) | 自动初始化为 0 |
| **可执行文件** | 占用空间(需存储初始值) | 不占空间(只记录大小) |
| **嵌入式 Flash** | 占用 Flash 空间 | 不占用 Flash 空间 |
**优化技巧**:嵌入式开发中,尽量使用未初始化变量(.bss)节省 Flash 空间。