今天的主题是:访问了空指针一定会出现段错误(segmentation fault)吗?
看下面代码:
test.c
#include <stdio.h>
int main()
{
int *p = NULL;
*p = 1;
return 0;
}
在Linux里面写了这么多代码,大家应该很清楚,上面的代码会出现段错误,因为访问了空指针。
#gcc test.c -o test
#./test
Segmentation fault
#
现象和我们想的一样。在Linux中,如果访问了空指针或者野指针,都会出现段错误(segmentation fault),这也是操作系统出于对内存的保护。 发生段错误的时候,系统会发出11信号(SIGSEGV),收到这个信号,程序就挂了。我们来验证一下:
circle.c
#include <stdio.h>
int main()
{
while (1);
return 0;
}
就是一个简单的死循环程序,运行起来,再打开一个终端,先查询进程的pid,再用kill发送一个SIGSEGV信号:
再来看下运行程序的结果:
清楚了没,现象和程序出现段错误的现象一样。
众所周知,在Linux里面,进程对信号的处理有三种方法,捕获、忽略、缺省行为。一般情况下,你什么都不做,行为就是缺省的,比如CTRL+C就会让进程死。
但是下面的进程,CTRL+C之后不仅死不了,还能打印东西:
signal.c
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
printf("receive signal %d\n", sig);
}
int main()
{
signal(SIGINT, handler);
while (1);
return 0;
}
编译运行,然后不停的按CTRL+C:
原因很简单,进程的2号信号SIGINT被我们捕获了。这个从ps命令里面也可以看出(留意CAUGHT这列):
当然,我们也可以选择忽略它:
#include <stdio.h>
#include <signal.h>
int main()
{
signal(SIGINT, SIG_IGN);
while (1);
return 0;
}
不管怎么按CTRL+C,都没用:
我们还是可以ps一下(留意IGNORED这列):
在这么多信号里面,除了极少数信号类似SIGKILL、SIGSTOP这种不能捕获和忽略以外,其他的都是可以被我们拿来把玩的,当然也包括看起来牛逼轰轰的SIGSEGV段错误(编号11)的信号。
我必须反复强调一点,当你用CTRL+C等对应的信号去杀死一个进程A的时候,从来都不是你杀死了A,而是你给A发了个信号,而A在响应这个信号的时候,其对应行为是进程exit。所以,不是你杀死了进程A,而是你发个信号,“通知”目标进程A去死。所以你不能用人类世界的杀死来理解Linux的杀死。Linux的逻辑类似:你对进程A说:“你去死吧”(发个可以让它死的信号),A看到这个pending的信号后,啥废话都不说,立即闷声死翘翘!所以,你只要改变A的响应行为,就可以选择不死。
下面问题就简单了,我们利用setjmp和longjmp来实现段错误后继续执行:
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
jmp_buf env;
void handler(int sig)
{
longjmp(env, 1); //跳转到上一次保存的现场
}
int main()
{
int ret = setjmp(env); //保存执行现场,返回0
if (0 == ret)
{
signal(SIGSEGV, handler);
printf("制造段错误...\n");
int *p = NULL;
*p = 1;
}
else
{
printf("段错误后!\n");
}
return 0;
}
setjmp这个函数的原理是:
调用这个函数的时候,它会保存执行现场,并返回0;之后调用longjmp,可恢复到setjmp保存的现场,setjmp再次返回,不过这次返回的是longjmp()的第二个参数。看下面这个图:
如果我们对代码稍加修改,删除一行:
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
jmp_buf env;
void handler(int sig)
{
longjmp(env, 1); //跳转到上一次保存的现场
}
int main()
{
int ret = setjmp(env); //保存执行现场,返回0
if (0 == ret)
{
//signal(SIGSEGV, handler);
printf("制造段错误...\n");
int *p = NULL;
*p = 1;
}
else
{
printf("段错误后!\n");
}
return 0;
}
现象又是我们熟悉的:
看明白没,段错误其实只是操作系统为了保护内存而发出的信号,进程收到信号缺省处理为挂掉,所以要想避免段错误,忽略SIGSEGV就行。当然这绝对是下下策,用这个方法来解决段错误简直无语,更重要的还是写代码的时候注意内存的使用,不要访问不能访问的内存。
更多文章、视频、嵌入式学习资料,微信关注公众号 『学益得智能硬件』