IE盒子

帖子
查看: 115|回复: 1

C语言的内存分配和段错误的处理

[复制链接]

5

主题

8

帖子

18

积分

新手上路

Rank: 1

积分
18
发表于 2023-1-15 21:09:51 | 显示全部楼层 |阅读模式
在嵌入式的程序设计中对于程序变量的内存的管理尤为重要,因为嵌入式产品的内存资源十分有限,合理分配内存是程序设计人员优先考虑的问题。当然,一些变量的内存分配不合理有时会导致程序的崩溃(即是段错误)等错误。本文针对这些问题进行了详细的解析并给予合理的解决方案。
一、内存分配
在C语言中,当程序加载到内存前被组织成三部分:
代码区;
静态存储区;
动态存储区。
1)代码段:将要执行程序的机器语言表示。即是我们通常所说的可执行的二进制代码。
2)静态存储区:变量的存储空间被系统静态分配,在程序的编译和链接时分配。
3)动态存储区:变量的存储空间被系统动态分配,在程序的调用和执行时分配。
如下程序为一个测试内存分配和段错误的代码:




程序执行结果:




静态存储区:主要存放的全局(global)的和静态(static)的数据。
如上程序中,在man()函数之前的一些变量的定义都属于静态存储区。而静态存储区又可以细分为已经初始化的变量和未初始化(BSS)的变量。
测试程序中,初始化的变量打印出的就是初始化的值,而没有初始化的全局变量,他们的值是唯一的:整型变量值为0,浮点型的变量其值为0.000000,字符型的变量其值为:’\0’,指针型的变量其值为NULL。大家看执行的结果可以发现,未初始化的全局变量都是这些固定的”零”值。
而static修饰的变量无论是局部的还是全局的未初始化时其值都为0(如上测试程序中的st、st1的值),对于static修饰的局部变量,会保存上一次执行结果的值,而一般的局部变量的值在程序调用时分配,调用结束时释放,再次进入子函数时重新分配值,如上程序代码中,a的值每一次都会重新赋值(加1后值为1),而b的值保存了函数执行结果的值,再次进入时其值在其基础上加1,于是出现了a的值一直为1,b的值累加。
动态存储区:分为堆区和栈区。
堆区(heap):malloc()函数调用时分配。如上测试代码中第42行的语句,通过malloc()系统在内存中开辟一块sizeof(int)大小的合适的空间,并返回一个指针给pt2,需要指出所谓合适的空间是指分配合法的内存空间,这样pt2就有了一个合法的地址指向了一块分配好的内存空间。
栈区(stack):存放的是局部变量和形参,函数的返回地址也存放在这里。如上测试代码中,局部变量未初始化时系统为局部变量赋的值为随机值,打印结果证实了这些打印的随机值。当然普通的变量赋随机值没有太大影响,但是当定义一个局部的指针变量时,有时候就会出现一些莫名其妙的错误,如段错误等,关于段错误后面会详细讲解。
补充:变量的生存期问题:
系统在给我们程序中的变量申请内存空间的时候,会根据变量的内存分配情况来决定此变量的生存期。
对于全局的变量,它的生存期是从变量分配内存空间开始,到整个程序的结束才释放空间。所以全局变量的生存期很长,会长时间占用内存资源,不是必要的时候一般不建议申请全局的变量。
对于局部变量来说,它的生存期就比全局变量短得多,在程序的调用时在堆空间申请,调用结束后就释放了所申请的这段空间,资源的利用率高,因此一般建议申请局部变量来节省内存的宝贵资源。
对于static修饰的变量,所修饰的是全局还是局部变量,它的意义是不一样的。当static修饰的是全局变量时,所关注的就是这个变量的作用域问题,就是说:被static限定的全局变量只是在本文件中有效,只能被本文件的函数调用,因此可以避免在其他的文件中引起错误,当然此变量的生存期还是从产生到程序结束释放。当static修饰的是局部变量的时候,所关注的就是它的生存期问题了,不再像普通的局部变量,函数调用结束空间释放,而是保存了上一次函数执行结果的值,在整个程序结束时才释放空间。
对于malloc()申请的变量,系统会在堆区开辟对应大小的空间,但是这个空间被申请好了就不会自动释放,必须手动申请和手动释放,利用free()函数释放空间。
二、段错误
在程序设计中经常遇到段错误的问题,在程序的编译链接过程中并不报错,但是在程序运行的时候就会出现段错误,让程序设计人员摸不到头脑,即便是经验丰富的程序员也难免出现段错误。而所谓的段错误就是指访问的内存超出了系统所给这个程序的内存空间,例如:访问了空指针,对一个没有给予确切地址的指针变量的引用和赋值等。
1)几种典型的段错误
1. 对常量赋值
int main(void){
char*s ="hello world";
*s ='H';
}
被装载时,系统把“hello world” 连同其它的字符串和const型数据放入到内存的只读区。执行时,一个变量s被设为指向该字符串的位置,当再试图向该位置写时,就会产生段错误。
2,操作空指针
int*ptr = NULL;
*ptr =1;
因为该代码只创建了一个空指针,并没有指向一个具体空间,当赋值时,产生段错误。
3,给一个没有明确地址的空间赋值
int *pt;(为局部变量)
*pt=5;
因为定义的是局部的变量,所以pt中装的是一个随机的指针,这个地址有可能是一个非法的地址(例如:是一个指向代码段的地址),就会造成内存操作错误,甚至会导致系统的奔溃,所以导致了段错误。
4,无限递归
int main(void){
main();
return0;
}
无限递归,这会导致栈溢出,也会产生段错误。
如下程序中是一个段错误的代码:




这是对空指针所在的内存区域进行了操作,而这个内存区域通常是不可访问的禁区,当然就会出错了。我们尝试编译运行它:




出现了段错误。
针对段错误,我们可以利用gdb逐步查找错误:
方法一:
具体步骤:
1.编译:gcc -g(生成调试级别) -dynamic XXX.c
2.gdb ./a.out //调试可执行的程序
3.输入r (运行代码)
4.可以看到出错的代码
这种方法也是被大众所熟知并广泛采用的方法,首先我们需要一个带有调试信息的可执行程序,所以我们加上“-g -rdynamic"的参数进行编译,然后用gdb调试运行这个新编译的程序,具体步骤如下:




从这里我们还发现进程是由于收到了SIGSEGV信号而结束的。通过进一步的查阅文档(man 7 signal),我们知道SIGSEGV默认handler的动作是打印“段错误”的出错信息,并产生Core文件,由此我们又产生了方法二。
方法二:分析core文件
具体步骤:
1.编译成调试级别的可执行文件
gcc -g XXX.c
2.生成core文件




3.按如下步骤:




4.锁定出错的代码
当然除了这些定位段错误的方法以外,还有其他的方法来对段错误进行定位查找,例如使用objdump -d a.out(elf)进行反汇编,但是要求程序设计人员能看得懂这些晦涩难懂的汇编代码等。个人感觉,通过GDB调试工具进行查找更加快捷方便,有大量编程经验的程序设计人员当然也能不通过工具直接查看代码找到引起段错误的代码段。
回复

举报

2

主题

9

帖子

16

积分

新手上路

Rank: 1

积分
16
发表于 2025-3-10 18:16:10 | 显示全部楼层
在撸一遍。。。
回复

举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表