讲师博文
堆栈溢出的原因 来源 : 华清远见     2025-01-16

一、栈(Stack)

1、概念和作用

栈是一种数据结构,在 Linux C 语言中用于存储函数调用的相关信息。当一个函数被调用时,会在栈上创建一个栈帧(Stack Frame)。栈帧中包含了函数的参数、局部变量、返回地址等信息。栈的操作遵循后进先出(LIFO)原则,这意味着最后压入栈中的数据将最先被弹出。

2、存储内容

参数传递:在 C 语言中,函数参数通常是通过栈来传递的。

例如:对于函数int add(int a, int b);

当调用add(3, 5)时,a和b的值(3 和 5)可能会被压入栈中。

局部变量存储:函数内部定义的局部变量也存放在栈中。

例如:

返回地址保存:当一个函数调用另一个函数时,调用函数的下一条指令的地址(即返回地址)会被保存在栈中。这样,当被调用函数执行完毕后,可以根据这个返回地址回到调用函数继续执行。

3、栈的大小

在 Linux 终端中,可以使用ulimit -s命令来查看栈的大小限制。ulimit是一个用于控制 shell 资源的工具,-s参数专门用于查看栈大小(以字节为单位)。

这个输出结果8192表示当前用户的栈大小限制是 8192 字节。如果程序的栈使用超过了这个限制,就会导致栈溢出。

二、堆(Heap

1、概念和作用

堆是用于动态内存分配的区域。在 C 语言中,通过函数如malloc、calloc和realloc来从堆中分配内存,通过free函数来释放内存。堆用于存储那些在程序运行过程中需要动态分配和释放的内存块,这些内存块的生命周期通常由程序员控制,而不像栈中的数据在函数结束时自动释放。

2、内存分配和管理

2.1、malloc 函数:

void * ptr = malloc(size_t size),它会在堆中分配指定大小(size)的一块内存,并返回一个指向这块内存的指针(ptr)。如果内存分配成功,ptr指向的内存是未初始化的。

2.2、calloc 函数:

void * ptr = calloc(size_t num, size_t size),它也会在堆中分配内存。与malloc不同的是,calloc会将分配的内存块初始化为全 0。

2.3、realloc 函数:

void * new_ptr = realloc(void * old_ptr, size_t new_size),用于重新调整已经通过malloc或calloc分配的内存块的大小。

2.4、free 函数:

free(void * ptr)用于释放通过malloc、calloc或realloc分配的内存。如果不释放堆内存,可能会导致内存泄漏。

3、堆与栈的区别

3.1、内存分配方式:

栈的内存分配是由编译器自动完成的,在函数调用时自动分配,函数结束时自动释放;而堆的内存分配是由程序员通过函数调用手动进行的,并且需要手动释放,否则会导致内存问题。

3.2内存增长方向:

栈的内存增长方向通常是从高地址向低地址,而堆的内存增长方向通常是从低地址向高地址(这可能因操作系统和编译器而略有不同)。

3.3内存使用效率:

栈的内存分配和释放速度相对较快,因为它是自动完成的;堆的内存分配和释放相对复杂,速度较慢,并且可能会产生内存碎片。

三、堆栈溢出的原因

1、递归失控

在 Linux C 语言中,递归函数如果没有正确的终止条件,就会不断地进行自身调用,导致栈空间被无限制地占用。

例如:

这个func函数会无限递归,每次调用都会将函数的返回地址、局部变量等信息压入栈中。栈空间是有限的,最终就会导致栈溢出。

2、局部变量数组过大

如果在函数内部定义了过大的局部变量数组,而栈空间不足以容纳这些变量时,就会出现栈溢出(栈大小限制是 8192 字节)。

例如:

在这个例子中,func函数中定义的arr数组如果太大,超出了栈的容量,就会引发栈溢出。栈的大小在系统中是有限制的,一般由操作系统和编译时的设置决定。

3、函数嵌套过深

当有大量的函数嵌套调用时,每一次函数调用都会在栈上创建一个新的栈帧来存储函数的局部变量、参数和返回地址等信息。如果嵌套的层数过多,就会耗尽栈空间。

例如:

在这个代码中,从func1到func100层层嵌套调用,可能会因为栈帧的过度积累而导致栈溢出。

4、缓冲区溢出

当程

序向一个缓冲区写入数据时,如果写入的数据长度超过了缓冲区的大小,就可能会覆盖栈上的其他数据,从而导致栈溢出。例如,在处理字符串复制操作时:

在这个例子中,strcpy函数会将arr复制到buff中,但arr的长度超过了buff的容量,就会导致缓冲区溢出,可能会覆盖栈上相邻的内存区域,引发栈溢出。

四、防止堆栈溢出的方法

1、手动记录递归深度

当使用递归函数时,可以通过一个变量来记录递归的深度。例如,在计算阶乘的递归函数中:

在这里,depth变量用于记录递归深度,当depth超过预先定义的MAX时,就会进行相应的错误处理。

或者,定义一个全局变量,每次函数调用的时候就-1,当超出限制的时候,就错误处理结束调用。

2、估算局部变量空间需求,动态分配空间

在函数设计时,需要估算函数内部局部变量所占用的栈空间。尽量避免定义大量占用空间的局部变量。如果必须使用较大的局部变量数组,可以考虑将其定义为全局变量或者动态分配内存(在堆上)。

例如,对于一个可能导致栈溢出的函数:

可以将其修改为动态分配内存的方式:

这样,数组的内存是从堆上分配的,而不是栈,减少了栈溢出的风险。

3、优化函数参数传递方式

如果函数参数是大型结构体或者数组,可以考虑使用指针传递而不是值传递。值传递会复制整个参数对象到栈上,而指针传递只传递对象的地址,占用空间更小。

例如:

4、安全的字符串和缓冲区操作

1、使用安全的字符串处理函数

避免使用可能导致缓冲区溢出的函数,如strcpy和gets。取而代之,使用安全的函数,如strncpy和fgets。例如,对于strcpy可能导致的缓冲区溢出:

可以使用strncpy来安全地复制字符串:

这里strncpy会根据buff的大小来复制字符串,并且最后手动添加字符串结束符\0,以确保字符串的完整性。

2、检查缓冲区边界

在对缓冲区进行操作时,无论是写入还是读取,都要明确知道缓冲区的边界。例如,在循环向缓冲区写入数据时,要确保写入的数据量不超过缓冲区的大小。可以通过比较写入数据的索引和缓冲区大小来进行控制。例如:

这个示例在从标准输入读取字符并写入缓冲区buffer时,通过比较i和sizeof(buff)-1来确保不会写入超过缓冲区大小的数据。

扫码申领本地嵌入式教学实录全套视频及配套源码

上一篇:深入探究:如何编写一个标准宏 MIN

下一篇:C语言-变量存储方式

400-611-6270

Copyright © 2004-2024 华清远见教育科技集团 版权所有
京ICP备16055225号-5京公海网安备11010802025203号