- 9.4 如何处理段错误
- ifdef__USE_POSIX199309
- define sa_handler__sigaction_handler.sa_handler
- define sa_sigaction__sigaction_handler.sa_sigaction
- else
- endif
- define sa_handler__sigaction_handler.sa_handler
- define sa_sigaction__sigaction_handler.sa_sigaction
- include<memory.h>
- include<stdlib.h>
- include<stdio.h>
- include<signal.h>
- define NGREG 19
9.4 如何处理段错误
所谓段错误,就是指访问了那些不可用或受保护的内存所导致的错误。那么该如何查找发生段错误的原因呢?其实,前面讲解函数间的调用关系时已经讲到了一种查看的方法,接下来介绍如何通过寄存器来查看段错误的原因。
通过寄存器来查看段错误的思路是在代码中捕捉SIGSEGV信号。该信号是当一个进程执行了一个无效的内存引用或发生段错误时发送给程序的信号。SIGSEGV的符号常量在头文件signal.h中定义。
由于在此不是对捕捉信号进行简单地处理,而是要从中得到一些寄存器方面的信息,因此不采用signal函数来捕捉,而是采用另外一个函数sigaction来捕捉。下面看一下捕捉函数sigaction的定义形式。
int sigaction(int signum,const struct sigactionact,struct sigactionoldact);
在讲解sigaction函数的参数的含义之前,先来看在Linux环境下参数struct sigaction的实现。在sigaction.h的头文件中实现,所在路径为:usr/include/bits。
struct sigaction
{
ifdef__USE_POSIX199309
union
{
__sighandler_t sa_handler;
void(sa_sigaction)(int,siginfo_t,void*);
}
__sigaction_handler;
define sa_handler__sigaction_handler.sa_handler
define sa_sigaction__sigaction_handler.sa_sigaction
else
__sighandler_t sa_handler;
endif
__sigset_t sa_mask;
int sa_flags;
void(*sa_restorer)(void);
};
在上面的实现代码中,使用了一条条件编译语句来进行选择性编译。如果系统定义了__USE_POSIX199309,那么就编译#ifdef部分的代码,否则编译#else部分的代码。要注意下面两个宏的实现:
define sa_handler__sigaction_handler.sa_handler
define sa_sigaction__sigaction_handler.sa_sigaction
上面的两个宏简化了对结构体中嵌入的共用体内成员变量的引用,其中的共用体数据结构中的两个元素_sa_handler和*_sa_sigaction用于指定信号关联函数,即用户指定的信号处理函数。信号处理函数除了可以是用户自定义的处理函数外,还可以为SIG_DFL(默认的处理方式),也可以为SIG_IGN(忽略信号)。由_sa_handler指定的处理函数只有一个参数,即信号值,因此不能传递除信号值之外的任何信息。由_sa_sigaction指定的信号处理函数带有3个参数,这个函数是为实时信号而设的(当然同样支持非实时信号)。第一个参数为信号值;第三个参数传递的是一些发生异常时的寄存器信息,在此也是根据这些寄存器所携带的信息进行异常的定位;第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构如下。
typedef struct siginfo
{
int si_signo;
int si_errno;
int si_code;
union
{
int_pad[__SI_PAD_SIZE];
struct
{
pid_t si_pid;uid_t si_uid;
}_kill;
struct
{
int si_tid;int si_overrun;
sigval_t si_sigval;
}_timer;
struct
{
pid_t si_pid;uid_t si_uid;sigval_t si_sigval;
}_rt;
struct
{
pid_t si_pid;uid_t si_uid;
int si_status;__clock_t si_utime;
__clock_t si_stime;
}_sigchld;
struct
{
void*si_addr;
}_sigfault;
struct
{
long int si_band;
int si_fd;
}_sigpoll;
}_sifields;
}siginfo_t;
分析上面的实现,重点看前三个结构体成员变量,si_signo表示信号值,si_errno是error值,si_code表示信号产生的原因,三个值都对所有信号有意义。其他的结构体成员变量在此并不涉及,所以就不一一讲解了。
接下来分析sigaction结构体成员的含义。
sa_mask,指定在信号处理程序执行过程中哪些信号应当被阻塞。除非指定为SA_NODEFER或者SA_NOMASK标志位,否则默认当前信号本身被阻塞,以防止信号的嵌套发送。
sa_restorer,此参数没有使用。
sa_flags,控制内核对该信号的处理标记,在此的取值为SA_SIGINFO。sa_flags包含了许多标志位,有SA_INTERRUPT和SA_NOCLDWAIT等。当设置了SA_SIGINFO标志位,表示信号附带的参数可以被传递到信号处理函数中,其附加信息为一个指向siginfo结构体的指针和指向进程上下文标识符的指针。因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,那么信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误。
接下来介绍sigaction函数中其他参数的含义。
signum:可以取除了SIGKILL和SIGSTOP之外的其他任何信号的编码。
act:如果值非NULL,将采用signum关联信号的新处理方式。
oldact:如果值非NULL,将存储以前对signum关联信号的处理方式。
下面通过代码来了解如何通过捕捉函数实现信号来捕捉和查找段错误。
include<memory.h>
include<stdlib.h>
include<stdio.h>
include<signal.h>
static void sigsegv_handler(int signum,siginfo_tinfo,voidptr)
{
ucontext_tucontext=(ucontext_t)ptr;
printf("info.si_signo=%d\n",signum);
printf("info.si_errno=%d\n",info->si_errno);printf("info.si_code=%d\n",info->si_code);
printf("发送异常的地址为:0x%3x\n",ucontext->uc_mcontext.gregs[14]);
exit(-1);
}
void catch_sig()
{
struct sigaction action;
memset(&action,0,sizeof(action));
action.sa_sigaction=sigsegv_handler;
action.sa_flags=SA_SIGINFO;
if(sigaction(SIGSEGV,&action,NULL)!=0)
{
perror("sigaction");
}
return;
}
void cause_segv()
{
int*a;
*a=123;
return;
}
int main()
{
catch_sig();
cause_segv();
return 0;
}
在分析代码之前,先来看看上面通过捕捉函数实现的处理函数中的一句代码:
ucontext_tucontext=(ucontext_t)ptr;
这句代码把捕捉函数的第三个参数强制转换为ucontext_t类型的指针。接下来看ucontext_t的实现代码。
typedef struct ucontext
{
unsigned long int uc_flags;
struct ucontext*uc_link;
stack_t uc_stack;
mcontext_t uc_mcontext;
__sigset_t uc_sigmask;
struct_libc_fpstate__fpregs_mem;
}ucontext_t;
在此重点介绍其结构体中的mcontext_t类型的变量uc_mcontext。mcontext_t的实现代码如下:
typedef struct
{
gregset_t gregs;
fpregset_t fpregs;
unsigned long int oldmask;
unsigned long int cr2;
}
在代码中又有一个fpregset_t类型的fpregs变量,其实现代码如下:
typedef int greg_t;
define NGREG 19
typedef greg_t gregset_t[NGREG];
由此可以看出,fpregs是结构体中的一个数字,其保存的是寄存器中的值,可以通过该数组中的内存进行段错误的查找。知道了其实现方法,接下来看看以上代码的运行结果。
root@ubuntu:/home#gcc-g seg.c-o seg
root@ubuntu:/home#./seg
info.si_signo=11
info.si_errno=0
info.si_code=2
发送异常的地址为:0x8048566
分析上面的运行结果,在ucontext_t结构体中有很多的寄存器值,在此选用保存发生异常地址数组元素所保存的值,其中保存的是代码运行过程中的出错位置,这样还不能直观地发现出错的具体代码,我们采用前面的addr2line工具将其转换为代码中的相对位置。
root@ubuntu:/home#addr2line-f-e seg 0x8048566
cause_segv
/home/seg.c:36
对于addr2line工具的使用,在此就不再讲解了,读者可以查看前面对函数间调用关系的讲解。通过上面定位的行号,我们再来看行号对应的代码。
*a=123;
分析第36行所对应的代码可以发现,定义的指针没有指向一个具体可用的地址,所以在此导致段错误。分析上面对段错误的查找,再根据代码的运行结果,很快就定位了出错点。因此编程的时候,我们可以采用上面的方法来查找段错误。