C语言:内存
内存基础概念
内存空间分布图
内存分布分为静态区和动态区,静态区在编译时已经决定好了内存分配大小,动态区是运行时分配。

#include <stdio.h>
#include <stdlib.h>
int global_inited = 10;
int global_uninited;
int main()
{
int local_var = 20;
char *str = "Hello";
static int static_var = 30;
static char static_str1 = 'b';
static char static_str2;
int *heap_var = (int *)malloc(sizeof(int));
printf("Address of main: %p\n", main);
printf("Address of str: %p\n", str);
printf("Address of global_inited: %p\n", &global_inited);
printf("Address of static_var: %p\n", &static_var);
printf("Address of static_str1: %p\n", &static_str1);
printf("Address of global_uninited: %p\n", &global_uninited);
printf("Address of static_str2: %p\n", &static_str2);
printf("Address of heap_var: %p\n", heap_var);
printf("Address of local_var: %p\n", &local_var);
free(heap_var);
return 0;
}输出:
Address of main: 0x6299c32ae1a9
Address of str: 0x6299c32af008
Address of global_inited: 0x6299c32b1010
Address of static_var: 0x6299c32b1014
Address of static_str1: 0x6299c32b1018
Address of global_uninited: 0x6299c32b1020
Address of static_str2: 0x6299c32b1024
Address of heap_var: 0x6299dfdac2a0
Address of local_var: 0x7ffe73a19c84函数名main实际上是指针常量,是函数的入口地址,存放在代码段;字符串常量str存放在只读数据段;初始化的全局变量global_inited和静态变量static_var存放在全局数据初始化段.data;未初始化的全局变量global_uninited和静态变量static_str2存放在全局数据未初始化段.bss;动态分配的内存heap_var存放在堆区;局部变量local_var存放在栈区。
.data和.bss没有固定大小的,按照程序中变量定义分配大小。.data和.bss在内存中紧密排列,.data在前,.bss在后。二者之间存在对齐填充,例如上面例子中,global_inited占4字节,static_var占4字节,static_str1占1字节,后面填充了3字节对齐,接着是global_uninited占4字节,static_str2占1字节,后面填充了3字节对齐。
操作权限
内存空间根据使用权限不同,可以分为以下几种:
- 代码段:可执行(读)权限,不可写权限
- 只读数据段:只读权限,不可写权限
- 全局数据初始化段
.data:可读写权限 - 全局数据未初始化段
.bss:可读写权限 - 堆区:可读写权限
- 栈区:可读写权限
代码段:存放程序的机器指令,CPU执行程序时从这里读取指令。为了防止恶意代码修改程序指令,代码段通常设置为只读不可写。
#include <stdio.h> void func(void) { printf("Function func called.\n"); } int add(int a, int b) { return a + b; } int main() { printf("Address of func: %p\n", func); void (* p)(void) = func; int (* pa)(int, int) = add; p(); int result = pa(3, 5); printf("Result of add(3, 5): %d\n", result); int *p1 = (int *)func; printf("Address stored in p1: %p\n", p1); printf("Address stored in p1: %d\n", *p1); // try to modify the function code (undefined behavior) *p1 = 0x90; // NOP instruction in x86, but this is unsafe and may cause a crash return 0; }输出:
Address of func: 0x5cd509983169 Function func called. Result of add(3, 5): 8 Address stored in p1: 0x5cd509983169 Address stored in p1: -98693133 段错误 (核心已转储)在上面的例子中,可以访问函数地址,并通过函数指针调用函数。但是尝试修改函数代码会导致段错误,因为代码段是只读的。
只读数据段:存放字符串常量等只读数据。尝试修改这些数据会导致段错误。
#include <stdio.h> int main() { printf("%s addr: %p \n", "Hello, World!", "Hello, World!"); printf("%s addr: %p \n", "Hello, World!", "Hello, World!"); char *s = "Hallo, World!"; s[1] = 'e'; // 修改字符串常量,未定义行为 return 0; }输出:
Hello, World! addr: 0x64ae11e8a004 Hello, World! addr: 0x64ae11e8a004 段错误 (核心已转储)
全局数据初始化段
.data和未初始化段.bss:存放全局变量和静态变量,可以读写。#include <stdio.h> int global_inited = 10; int global_uninited; int func() { static int static_var = 10; global_uninited++; return ++static_var; } int main() { static int a; printf("%p, %p, %p\n", &global_inited, &global_uninited, &a); global_inited = 20; global_uninited = 30; a = 40; printf("global_inited = %d, global_uninited = %d, a = %d\n", global_inited, global_uninited, a); printf("global_uninited = %d, static_var = %d, static_var = %d\n", global_uninited, func(), func()); return 0; }输出:
0x615a57d6d010, 0x615a57d6d01c, 0x615a57d6d020 global_inited = 20, global_uninited = 30, a = 40 global_uninited = 32, static_var = 12, static_var = 11堆空间:堆空间在运行时由程序员来分配(
malloc)和释放(free),可读可写,生命周期是程序员来决定。值得注意的是,如果在一个函数中分配堆内存,函数执行结束后malloc空间不会被释放,如果不手动进行释放,会造成内存泄漏。**堆空间是自下向上生长的。**在C语言中,malloc返回的是无类型指针(void *),可以自动转换为其他类型的指针。(在C++中需要强制转换)#include <stdio.h> #include <stdlib.h> void func(void) { int *heap_var = (int *)malloc(sizeof(int)); if (!heap_var) { return; } *heap_var = 10; printf("heap addr = %p, heap_var = %d\n", heap_var, *heap_var); free(heap_var); } int main() { func(); return 0; }输出结果:
heap addr = 0x64bf4270e2a0, heap_var = 10栈空间:栈空间指的是在函数运行时的上下文分配的空间,可读可写,生命周期在函数执行结束后结束。栈空间是自上向下生长的,在一个调用链上,不同函数的栈空间是从上到下分配的,同一个函数的栈帧内存分配不一定是自上向下的。
内存溢出问题
内存溢出指的是程序运行过程中,访问超过其分配空间范围的内存区域。
栈溢出
系统的整个栈空间被消耗殆尽,最后出现段错误。
#include <stdio.h> void try_overflow(void) { int a = 10; try_overflow(); } void main(void) { try_overflow(); }输出结果:
段错误 (核心已转储)局部变量过大导致栈溢出
通过
ulimit -a可以查看栈空间大小:stack size (kbytes, -s) 8192 # 8192k字节在程序中声明一个
8192 * 1024字节大小的数组,并赋值打印可以看到程序运行报错:段错误 (核心已转储)程序如下:
#include <stdio.h> #include <string.h> #define N 8192*1024 void main(void) { char arr[N]; memset(arr, 'c', sizeof(char)*N); for (int i=0;i<N;i++){ printf("%c", arr[i]); } puts(""); }
堆溢出
- 堆缓冲区溢出
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define N 5
int main()
{
char *str = (char *)malloc(sizeof(char)*N);
char *str1 = (char *)malloc(sizeof(char)*N);
strcpy(str, "hello world");
strcpy(str1, "hello world");
printf("str: %s\n", str);
printf("str1: %s\n", str1);
}以上程序给变量str和str1分别分配了5个char类型的内存大小,但是赋值大小是11个char的大小。
编译有waring提示:
1.c: In function ‘main’:
1.c:12:5: warning: ‘__builtin_memcpy’ writing 12 bytes into a region of size 5 overflows the destination [-Wstringop-overflow=]
12 | strcpy(str, "hello world");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
1.c:9:25: note: destination object of size 5 allocated by ‘malloc’
9 | char *str = (char *)malloc(sizeof(char)*N);
| ^~~~~~~~~~~~~~~~~~~~~~
1.c:13:5: warning: ‘__builtin_memcpy’ writing 12 bytes into a region of size 5 overflows the destination [-Wstringop-overflow=]
13 | strcpy(str1, "hello world");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~
1.c:10:26: note: destination object of size 5 allocated by ‘malloc’
10 | char *str1 = (char *)malloc(sizeof(char)*N);运行结果正常:
str: hello world
str1: hello worldgdb调试查看:
//设置断点
(gdb) b 12
// 运行
(gdb) run
// 内存地址相差32字节
(gdb) print str
$1 = 0x5555555592a0 ""
(gdb) print str1
$2 = 0x5555555592c0 ""
// 在str第6个位置设置字符
(gdb) set {char}0x5555555592a6 = 'v'
// 查看是否设置成功
(gdb) print {char}0x5555555592a6
$3 = 118 'v'
// 设置断点、继续运行
(gdb) b 15
(gdb) c
// 再次观察str第6个位置的字符,发现已经被覆盖
(gdb) print {char}0x5555555592a6
$4 = 119 'w'在上述调试过程发现,对str的操作可能越界访问内存,str起始的第6个位置的内存没有分配给它,如果该位置存储了别的变量的重要数据,则会发生篡改,可能导致严重后果。