本文转载自微信公众号《Linux内核那些事》,作者songsong001。转载本文请联系Linux内核那些事儿公众号。人都会犯错,所以写程序难免会出现bug。有些BUG是业务逻辑错误导致的,一般不会导致程序崩溃。例如,本应将两个数相加,但不小心将两个数相减,导致结果出现错误。这时候我们可以在程序中使用printf等输出函数进行打点调试。但是有些BUG是由某些致命操作引起的,一般会导致程序崩溃,比如访问未申请的内存地址。因为程序会异常退出,所以一般不能通过printf等输出函数来调试。另外,对于必须出现的BUG(即不管什么情况都会出现),一般可以通过GDB设置断点进行调试。但是对于偶发的bug,很难直接通过GDB调试,因为它们只在特定的条件下发生。那么,这时候就可以通过Linux提供的coredump文件进行调试了。1、coredump文件生成过程当程序出现某些错误,进程异常退出时,linux内核会根据当时进程的内存信息生成一个coredump文件。GDB可以通过这个coredump文件重现导致进程异常退出的场景,可以利用GDB找到导致进程异常退出的原因。当进程收到某些导致其异常退出的信号时,会生成coredump文件。那么,哪些信号会导致生成coredump文件呢?会导致产生coredump文件的信号如下表所示:一个示例来说明如何生成coredump文件。从上表可以看出,当一个进程收到SIGSEGV信号时,就会产生一个coredump文件。当进程访问错误的(未申请的)内存地址时会触发SIGSEGV信号,因此我们编写一个程序来访问错误的内存地址:intmain(intargc,char*argv[]){char*addr=(char*)0;//设置addr变量为内存地址"0"*addr='\0';//向内存地址"0"写入数据return0;}在上面的例子中,因为内存地址"0"没有通过由于调用了malloc函数应用,所以在向地址“0”写入数据时会引起段错误,进程会收到SIGSEGV信号。当进程收到SIGSEGV信号时,内核会根据当时进程的内存信息生成一个coredump文件,并kill该进程。我们编译运行上面的程序后,会发现程序异常退出,并生成了一个名为core.xxx的文件,也就是coredump文件。如下图所示:注意:编译时记得加-g参数保留调试信息,否则用GDB调试时会找不到函数名或变量名。如果没有生成coredump文件,一般是受资源限制。首先使用命令ulimit-cunlimited将资源设置为无限制。coredump文件中点后面的数字是进程的PID。现在我们只需要输入下面的命令就可以使用GDB调试带有coredump文件的程序:$gdb./coredump./core.6359运行后GDB会停在异常发生的代码处,并打印出代码发生异常的地方。如下图所示:从上面的输出我们可以看出,GDB除了将异常代码打印到终端外,还会打印出所在文件的函数、文件名和行号,这样我们可以快速定位到是哪一行代码导致了异常。2.coredump文件生成原理前面提到,当进程接收到某些信号导致异常退出时,会生成一个coredump文件。在进程从内核态返回到用户态之前,内核会检查进程的信号队列中是否还有未处理的信号,如果有,则调用do_signal内核函数对信号进行处理。我们可以用下图来说明内核是如何生成coredump文件的:进程从内核态返回到用户态的地方有很多,比如从系统调用返回,从硬中断处理程序返回,返回从进程调度程序。上图主要以进程调度器返回,内核如何生成coredump文件为例。下面我们来分析一下coredump文件生成过程的步骤:1.信号处理do_signal()当进程从内核态返回到用户态时,内核会检查该进程的信号队列中是否有还没有的信号已处理,如果是,则调用do_signal内核函数处理信号。我们看一下do_signal函数的实现:staticvoidfastcalldo_signal(structpt_regs*regs){siginfo_tinfo;intsignr;structk_sigactionka;sigset_t*oldset;...signr=get_signal_to_deliver(&info,&ka,regs,NULL);...}上面的代码去掉了很多与生成coredump文件无关的逻辑。最后我们可以看到do_signal函数主要是调用了get_signal_to_deliver核函数做进一步的处理。get_signal_to_deliver内核函数的主要工作是从进程的信号队列中获取一个信号,然后根据信号的类型进行不同的操作。我们主要关注生成coredump文件相关的逻辑,如下:intget_signal_to_deliver(siginfo_t*info,structk_sigaction*return_ka,structpt_regs*regs,void*cookie){sigset_t*mask=¤t->blocked;intsignr=0;...对于(;;){...//1。从进程信号队列中获取一个信号signr=dequeue_signal(current,mask,info);...//2.判断是否会产生coredump文件的signalif(sig_kernel_coredump(signr)){//3.调用do_coredump()函数生成coredump文件do_coredump((long)signr,signr,regs);}...}...}以上代码去掉了与生成coredump文件无关的逻辑,并最后我们可以看出get_signal_to_deliver函数主要完成了三个任务:调用dequeue_signal函数从进程的信号队列中获取一个信号。调用sig_kernel_coredump函数判断信号是否会生成coredump文件。如果信号会生成一个coredump文件,那么调用do_coredump函数生成一个coredump文件。2.生成coredump文件如果要处理的信号会触发coredump文件的生成,内核会调用do_coredump函数生成coredump文件。do_coredump函数的实现如下:intdo_coredump(longsignr,intexit_code,structpt_regs*regs){charcorename[CORENAME_MAX_SIZE+1];structmm_struct*mm=current->mm;structlinux_binfmt*binfmt;structinode*inode;structfile*file;intretval=0;intfsuid=current->fsuid;intflag=0;intispipe=0;binfmt=current->binfmt;//当前进程使用的可执行文件格式(如ELF格式)...//1.判断当前进程能否生成coredump文件大小是否受资源限制if(current->signal->rlim[RLIMIT_CORE].rlim_cur
