C 语言内存管理(一):内存段与布局(.text/.data/.bss/堆/栈)

内存管理 — 内存段与布局

内存管理是嵌入式 C 语言开发的核心难点。本文聚焦于程序内存布局、内存段的逻辑与物理映射、以及嵌入式内存优化实践。


1 程序内存布局

1.1 内存段的基本概念

C 程序运行时,内存被划分为不同的区域,每个区域存储特定类型的数据:

  • 代码段(.text):存放程序的机器指令,只读
  • 只读数据段(.rodata):存放常量和字符串字面量,只读
  • 已初始化数据段(.data):存放有初始值的全局变量和静态变量
  • 未初始化数据段(.bss):存放未初始化的全局变量和静态变量,自动清零
  • 堆(Heap):动态分配的内存区域,由 malloc/free 管理
  • 栈(Stack):存放局部变量、函数参数和返回地址,自动管理
💡 核心概念

编译器将程序分成不同的段(Section).text(代码)、.rodata(常量)、.data(已初始化变量)、.bss(未初始化变量)。链接器决定这些段最终存放在内存的哪个位置。

1.2 各段对比

image-20260314222744110

区域 内容 生命周期 大小
.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;),又需要运行时修改,所以:
  1. 编译时:初始值 42 存在 Flash(掉电不丢)
  2. 启动时:启动代码把 42 从 Flash 复制到 RAM
  3. 运行时:程序在 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
  低地址 ──────────────────────────────────→ 高地址

运行截图与分析:

image-20260314224304889

💡 关键观察: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 空间。