C语言内存分配
概述(Overview)
当我们编译一个C程序后,会创建一个二进制可执行文件(.exe),当我们执行程序时,这个二进制可执行文件会按照一定的组织方式加载到RAM中.
因为计算机不会直接从辅助存储器(secondary storage)访问程序指令,因为与RAM相比,辅助存储器的访问时间更长.RAM读取速度比辅助存储器快,但是存储容量有限,所以程序员有必要有效地利用这个有限的存储空间.了解关于C的内存布局对于程序员是很有帮助的,因为它可以决定程序执行时内存的使用量.
加载到RAM后,C程序中的内存布局由6个部分(section)组成,从低地址到高地址分别是:代码段(.text), 初始化的数据段(.data), 未初始化的数据段(.bss), 堆空间(heap), 栈空间(stack), 命令行参数以及环境变量区域(command-line arguments and environment variables).
这6个不同的区域存储着代码不同的部分,并拥有自己的读写权限.如果程序试图以不同于预期的方式访问存储在任何段中的值,则会导致段错误(segmentation fault error),这也是导致程序崩溃(program to crash)的主要原因.
C的内存布局简图(Diagram for memory structure of C)
下面提到的图表显示了 RAM 如何将C程序可执行文件加载到多个段中的可视化表示:
代码段(Text Segment)
- 编译程序后,会生成一个二进制文件,通过将其加载到RAM来执行我们的程序.该二进制文件就包含指令,这些指令存储在内存的代码段.
- 代码段具有只读权限(read-only permission),防止程序被意外修改.
- 代码段在内存中是共享(shareable)的,因此对于常见的应用程序(如文本编辑器, shell 等),内存中需要一份副本。
初始化数据段(Initialize Data Segment)
- 初始化数据段(也称为数据段)是C程序的计算机虚拟内存空间的一部分,它包含所有的外部(external), 全局(global), 静态(static), 常量(constant) 变量的值,这些变量的值在程序声明变量的时候初始化.
- 因为变量的值在程序执行过程中会发生变化,所以数据段具有读写权限(read-write permission).我们可以进一步将数据段分为读写区和只读区.const变量位于只读区域下,其余类型的变量位于读写区域.
例如:
#include <stdio.h>
int global_var = 50;//全局变量 global_var 存储于data段的读写区
char *hello = "Hello World";//全局指针变量 hello 存储于data段的读写区,字符串 "Hello World" 存储于data段的只读区
const char *hello_2 = "Hello World";//const指针变量 hello_2 存储于data段的读写区,字符串 "Hello World" 存储于data段的只读区
const int global_var2 = 30;//const变量 global_var2 存储于data段的只读区
static int num_1 = 10;//全局静态变量 num_1 存储于data段的读写区
int main()
{
static int a = 10;//静态变量 a 存储于data段的读写区
return 0;
}
未初始化数据段(Uninitialized Data Segment)
- 未初始化的数据段也被称为bss段(由符号开始的块 block started by symbol),被加载的程序在加载时为bss段分配内存.在C程序执行之前,bss段中的每个数据会被内核初始化为算数0(arithmetic 0)和指向空指针的指针(pointers to null pointer).
- bss段还包括所有被初始化为算数0的静态和全局变量以及指向空指针的指针
- bss段的数据是可以被改变的,所以这个数据段具有读写权限(read-write permission).
#include <stdio.h>
static int num_1 = 10;//全局静态变量 num_1 存储于data段的读写区
static int num_2 = 0;//全局静态变量 num_2 存储于bss段(被初始化为算数0)
static int num_3;//全局静态变量 num_3 存储于bss段
char *ptr_1 = "Hello";//全局指针变量 ptr_1 存储于data段的读写区
char *ptr_2 = NULL;//全局指针变量 ptr_2 存储于bss段(指向了空指针的指针)
char *ptr_3;//全局指针变量 ptr_3 存储于bss段
int main()
{
static int a = 10;//静态变量 a 存储于data段的读写区
static int b;//静态变量 b 存储于bss段
return 0;
}
堆(Heap)
- heap是用于存放进程运行时动态分配的内存(dynamically allocated memory),heap通常开始于bss段的末尾,增长方向和收缩方向和stack相反.
- 通常使用malloc, calloc, free, realloc等命令管理heap的内存分配, 内部使用sbrk 和 brk系统调用来更改heap的内存分配.
- heap在动态加载的模块和进程中的所有共享库之间共享.
#include <stdio.h>
int main()
{
char *var = (char*)malloc(1);//动态分配于heap中
return 0;
}
栈(Stack)
- stack拥有先进后出(First-In/Last-Out,FILO)的特性,向下增长到低地址(取决于计算架构).stack的增长方向与heap相反.
- stack存储局部变量, 传递给函数的参数, 函数的返回地址.
栈指针寄存器(stack pointer register)也称SP寄存器,用来维护和管理函数调用过程中栈帧变化,SP总是指向正在运行的函数的栈帧的栈顶.
函数被调用时将值传递到栈内,栈帧存储函数的临时变量和一些自动变量,例如返回地址和调用者环境(内存寄存器)的详细信息.
每次函数递归调用自身时,都会创建一个新的栈帧,它允许一个栈帧中的一组变量不会干扰函数不同实例的其他变量,这就是递归函数的工作原理.
具体可参考笔者的另一篇文章:C语言函数的栈帧与调用原理.
#include <stdio.h>
void foo()
{
int a, b; //当函数被调用时,局部变量 a, b 存储于stack
}
int main()
{
int local = 5; //局部变量 local 存储于stack
char name[26]; //局部变量 name 存储于stack
foo();
return 0;
}
命令行参数以及环境变量区域(Command-Line Arguments And Environment Variables)
- 当程序使用从控制台传递的参数(如 argc 和 argv 以及其他的环境变量)执行时,这些变量存储在这个内存段中.
在下面这个例子展示了命令行参数是如何在程序中传递和使用的.
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
// first value in argv stores file name
printf("File name = %s\n", argv[0]);
printf("Number of arguments passed = %d\n", argc - 1);
for (i = 1; i < argc; i++)
{
printf("Value of Argument_%d = %s\n", i, argv[i]);
}
return 0;
}
╰─❯ gcc memory_layout.c
╰─❯ ./a.out 10 11 12
File name = ./a.out
Number of arguments passed = 3
Value of Argument_1 = 10
Value of Argument_2 = 11
Value of Argument_3 = 12
这段内存中存储了argc(argument counter)和argv(argument value)的值,其中argc存储传递参数的数量,argv存储实际参数的值以及文件名.
总结(Conclusion)
- 当执行C语言程序时,二进制代码被加载到 RAM 中,并被分为五个不同的区域: text, data, bss, heap, stack, command-line arguments.
- 代码指令存储在text段中,这是可共享的内存.如果从控制台执行代码时传递参数,则参数的值将存储在内存中的command-line arguments段中.
- data段存储了程序中预先初始化的全局, 静态, 外部变量.bss段存储了所有未初始化的全局和静态变量.
- 堆栈存储函数的所有局部变量和参数.它们还存储指令的函数返回地址,该地址将在函数调用后执行.
- 堆栈和堆彼此相反地增长.
- 堆存储程序中所有动态分配的内存,并由 malloc, calloc, free 等命令管理.
#include <stdlib.h>
int a = 0; // a在bss段(因为被初始化为算数0)
char *p1; // p1在bss段
int main()
{
int b; // b在stack(局部变量)
char s[] = "abc"; // s在stack, "abc\0"在data-ro
char *p2; // p2在stack
char *p3 = "123456"; // p3在stack上, "123456\0"在data-ro
static int c = 0; // c在bss(因为被初始化为算数0)
p1 = (char *)malloc(10); // heap
p2 = (char *)malloc(20); // heap
return 0;
}