Contents

C语言:内存

内存基础概念

内存空间分布图

内存分布分为静态区动态区,静态区在编译时已经决定好了内存分配大小,动态区是运行时分配

/images/C语言/内存/image.png

#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:可读写权限
  • 堆区:可读写权限
  • 栈区:可读写权限
  1. 代码段:存放程序的机器指令,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
    段错误 (核心已转储)

    在上面的例子中,可以访问函数地址,并通过函数指针调用函数。但是尝试修改函数代码会导致段错误,因为代码段是只读的。

  2. 只读数据段:存放字符串常量等只读数据。尝试修改这些数据会导致段错误。

    #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);
}

以上程序给变量strstr1分别分配了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 world

gdb调试查看:

//设置断点
(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个位置的内存没有分配给它,如果该位置存储了别的变量的重要数据,则会发生篡改,可能导致严重后果。