gdb调试coredump(使用篇)

什么是coredumplinux

  Coredump叫作核心转储,它是进程运行时在忽然崩溃的那一刻的一个内存快照。操做系统在程序发生异常而异常在进程内部又没有被捕获的状况下,会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里。ios

   该文件也是二进制文件,可使用gdb、elfdump、objdump或者windows下的windebug、solaris下的mdb进行打开分析里面的具体内容。c++

 

   注:core是在半导体做为内存材料前的线圈,当时用线圈当作内存材料,线圈叫作core。用线圈作的内存叫作core memory。shell

 

 

ulimit segmentfault

虽然咱们知道进程在coredump的时候会产生core文件,可是有时候却发现进程虽然core了,可是咱们却找不到core文件。windows

 

LinuxSolaris下是须要进行设置的。sass

 

ulimit  -c 能够设置core文件的大小,若是这个值为0.则不会产生core文件,这个值过小,则core文件也不会产生,由于core文件通常都比较大。多线程

 

使用ulimit  -c unlimited来设置无限大,则任意状况下都会产生core文件。app

 

WindowsminiDumpFullDump的设置函数

 

Windows下须要在下面的注册表,

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\WindowsError Reporting]

下面加一项LocalDumps,并作以下项配置:

Value

描述

Type

默认值

DumpFolder

文件保存路径

REG_EXPAND_SZ

%LOCALAPPDATA%CrashDumps

DumpCount

dump文件的最大数目

REG_DWORD

10

DumpType

指定生成的dump类型:
0:Custom dump
1:Mini dump
2:Full dump

REG_DWORD

1

CustomDumpFlags

仅在DumpType0时使用
MINIDUMP_TYPE的组合

REG_DWORD

MiniDumpWithDataSegs|
MiniDumpWithUnloadedModules|
MiniDumpWithProcessThreadData
















上面的配置信息,能够经过打开注册表来手动添加。

首先,打开cmd或者运行程序(微软图标+R


 



如上截图,能够经过图形界面手动添加这些注册表信息,而后windows系统在有进程crash的时候就会保存fulldump的文件。

 

或者经过reg文件的方式来进行注册

Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps] "DumpFolder"="F:\\study_Test\\Dump" "DumpCount"=dword:a "DumpType"=dword:1

 

如上,经过新建一个fulldump.reg的文件,保存上面内容,双击后,这些信息就会注册到注册表中。

 

gdb 调试coredump的简单示例

 

#include "stdio.h" #include "stdlib.h" void dumpCrash() {     char *pStr = "test_content";     free(pStr); } int main() {     dumpCrash();     return 0; }

 

如上代码,pStr指针指向的是字符串常量,字符串常量是保存在常量区的,free释放常量区的内存确定会致使coredump

 

首先把上面的代码拷贝到linux机器上,保存为dumpTest.c文件,gcc编译


gcc -o dumpTestdumpTest.c

运行dumpTest产生core文件


 

生成core文件

如上,运行dumpTest的时候进程coredump了,可是没有产生core文件


如截图所示,系统设置的core文件大小为0,此时即便产生了coredump,也不会产生core文件。

 


如截图所示,ulimit -c unlimited设置core文件大小后,产生了名字为corecore文件。

此时生成的core文件名称都是统一的”core”命名。

 

自定义core文件的文件名

 上面的设置只是使能了core dump功能,缺省状况下,内核在coredump时所产生的core文件放在与该程序相同的目录中,而且文件名固定为core。很显然,若是有多个程序产生core文件,或者同一个程序屡次崩溃,就会重复覆盖同一个core文件。

 

咱们经过修改kernel的参数,能够指定内核所生成的coredump文件的文件名。例如,Easwy使用下面的命令使kernel生成名字为core_filename_time_pid格式的core dump文件:

echo /usr/core_log/core_%e_%t_%p > /proc/sys/kernel/core_pattern

echo后面内容最好不要带上引号,有的系统会把引号也带入,以下:



这样,系统是不识别该内容的,也就会致使程序coredump而不会生成core文件。



如上截图,经过设置core文件的名称以及路径,程序coredump的时候就会在指定路径按照指定的规则命名生成core文件。

能够在core_pattern模板中使用变量见下面的列表:

 

%%单个%字符

%pdump进程的进程ID

%udump进程的实际用户ID

%gdump进程的实际组ID

%s致使本次core dump的信号

%t core dump的时间 (197011日计起的秒数)

%h主机名

%e程序文件名


设置永久保存

 上面截图能够看到,我后面再次执行生成coredump文件的时候实际上又再次设置了ulimit-c unlimited的,由于中间机器重启了。上面的设置都只是临时的,重启以后就须要从新设置,如何设置永久生效呢?

 

打开/etc/security/limits.conf  文件,在该文件的最后加上两行

#下面是个人配置

@root soft core unlimited

@root hard core unlimited



配置好后,放回原目录,重启reboot。

 

命名规则的修改在/proc/sys/kernel/core_pattern中也只是临时的,这个也是动态加载和生成的。永久修改在/etc/sysctl.conf文件中,在该文件的最后加上两行:

kernel.core_pattern = /var/core_log/core_%e_%t_%p

kernel.core_uses_pid = 0



可使用如下命令,使修改结果立刻生效。
#sysctl –p



如上截图,当前生成的core文件命名按照上面定义的规则加上了程序名称、coredump时间,进程ID等信息,并放到了指定目录/var/core_log

 

gdb调试coredump初步尝试

 gdb打开core文件的格式为

 gdb程序名(包含路径) core*core文件名和路径),以下截图



 

如上,gdb打开core文件时,有显示没有调试信息,由于以前编译的时候没有带上-g选项,没有调试信息是正常的,实际上它也不影响调试core文件。由于调试core文件时,符号信息都来自符号表,用不到调试信息。以下为加上调试信息的效果。



 

 

查看coredump时的堆栈

查看堆栈使用bt或者where命令



如上,在带上调试信息的状况下,咱们其实是能够看到core的地方和代码行的匹配位置。

但每每正常发布环境是不会带上调试信息的,由于调试信息一般会占用比较大的存储空间,通常都会在编译的时候把-g选项去掉。

 

没有调试信息的状况下找core的代码行



如上截图,没有调试信息的状况下,打开coredump堆栈,并不会直接显示core的代码行。

此时,frame addr(帧数)或者简写如上,f 1 跳转到core堆栈的第1帧。由于第0帧是libc的代码,已经不是咱们本身代码了。

disassemble打开该帧函数的反汇编代码。

 

#1  0x080483ec in dumpCrash () (gdb) disassemble Dump of assembler code for function dumpCrash:    0x080483d4 <+0>:     push   %ebp    0x080483d5 <+1>:     mov    %esp,%ebp    0x080483d7 <+3>:     sub    $0x28,%esp    0x080483da <+6>:     movl   $0x80484d0,-0xc(%ebp)    0x080483e1 <+13>:    mov    -0xc(%ebp),%eax    0x080483e4 <+16>:    mov    %eax,(%esp)    0x080483e7 <+19>:    call   0x80482f0 <free@plt> => 0x080483ec <+24>:    leave    0x080483ed <+25>:    ret End of assembler dump.

 

如上箭头位置表示coredump时该函数调用所在的位置



如上截图,shell echo free@plt |C++filt 去掉函数的名词修饰

不过上面的free使用去掉名词修饰效果和以前仍是同样的。可是咱们能够推测到这里是在调用free函数。

如此,咱们就能知道咱们coredump的位置,从而进一步能推断出coredump的缘由。

固然,现实环境中,coredump的场景确定远比这个复杂,都是逻辑都是同样的,咱们须要先找到coredump的位置,再结合代码以及core文件推测coredump的缘由。

 

 

寻找this指针和虚指针

#include "stdio.h" #include <iostream> #include "stdlib.h" using namespace std; class base { public:     base();     virtual void test(); private:     char *basePStr; }; class dumpTest : public base { public:     void test(); private:     char *childPStr; }; base::base() {     basePStr = "test_info"; } void base::test() {     cout<<basePStr<<endl; } void dumpTest::test() {     cout<<"dumpTest"<<endl;     delete childPStr; } void dumpCrash() {     char *pStr = "test_content";     free(pStr); } int main() {     dumpTest dump;     dump.test();     return 0; }

 

如上代码,实现了一个简单的基类和一个子类。在main函数里定义一个子类的实例化对象,并调用它的虚函数方法testtest里因为直接delete没有初始化的指针childPStr,确定会形成coredump。本次咱们就但愿经过dump文件,找到子类dumpTestthis指针和虚函数指针。

gcc同样,使用g++ -o DumpCppTest dumpTest.cpp编译cpp文件生成可执行程序。

./DumpCppTest  执行该程序,程序由于直接delete未初始化的指针,确定会coredump。生成core文件以下



如上,使用gdb打开core文件,同时bt打开core的堆栈信息。

从堆栈能够看到,最后两帧为咱们程序本身的函数,其余的都是libc的代码。

f 6 调到第6帧上,以后info frame查看堆栈寄存器信息。



如上截图所示,前一帧的栈寄存器地址是0xbf8cdb50,它的前一帧也就是main函数的位置,main函数里调用dump.test()的位置,那咱们在这个地址上应该能够找到dump的this指针和它的虚指针,以及虚指针指向的虚函数表



如图所示,0xbf8cdb50地址指向的是前一帧保存dump信息的位置,0xbf8cdc14bf8cdb64就表示dump的this指针,而this指针指向的第一个8字节0x0804893008048958就表示虚指针,如上,经过x 0x0804893008048958看到_ZTV8dumpTest+8的内容。

 shell echo_ZTV8dumpTest|c++filt 能够看到“vtable for dumpTest”的内容。这个就表示dumpTest的虚函数表。

从上面也能够看到,这个地址指向的是虚函数表+8的偏移位置,而这个位置0x000000000804876a 经过x 0x000000000804876a 能够看到,存储的内容就是



dumpTest::test() 函数。

这里也印证了,在继承关系里,基类的虚函数是在子类虚函数的前面。


如上,x 0x000000000804876a-4 就能够看到dumpTest的基类base的虚函数test的位置。

 

如上,在实际问题中,C++程序的不少coredump问题都是和指针相关的,不少segmentfault都是因为指针被误删或者访问空指针、或者越界等形成的,而这些都通常意味着正在访问的对象的this指针可能已经被破坏了,此时,咱们经过去寻找函数对应的对象的this指针、虚指针能验证咱们的推测。以后再结合代码寻找问题所在。

 

gdb 查看core进程的全部线程堆栈

#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; #define NUM_THREADS 5 //线程数 int count = 0; void* say_hello( void *args ) {     while(1)     {         sleep(1);         cout<<"hello..."<<endl;         if(NUM_THREADS ==  count)         {             char *pStr = "";             delete pStr;         }     } } //函数返回的是函数指针,便于后面做为参数 int main() {     pthread_t tids[NUM_THREADS]; //线程id     for( int i = 0; i < NUM_THREADS; ++i )     {         count = i+1;         int ret = pthread_create( &tids[i], NULL, say_hello,NULL); //参数:建立的线程id,线程参数,线程运行函数的起始地址,运行函数的参数
        if( ret != 0 ) //建立线程成功返回0         {             cout << "pthread_create error:error_code=" << ret << endl;         }     }     pthread_exit( NULL ); //等待各个线程退出后,进程才结束,不然进程强制结束,线程处于未终止的状态 }

如上代码,简单示意C++多线程。

在linux下使用g++直接编译该cpp文件会报错,报错信息以下:



会报 undefined reference to `pthread_create' 的错误信息,解决办法以下:

使用 g++ -o MultiThreadDump MultiThread.cpp -lpthread 编译,编译参数上带上-lpthread便可。



运行./MultiThreadDump



因为上面代码里在count等于5的时候,会delete一个未初始化的指针,确定会coredump。

 


如上,gdb打开coredump文件,能看到5个线程LWP的信息。

如何,查看每一个线程的堆栈信息呢?

首先,info threads查看全部线程正在运行的指令信息



thread apply all bt打开全部线程的堆栈信息



查看指定线程堆栈信息:threadapply threadID bt,如:

thread apply 5 bt



进入指定线程栈空间

thread threadID以下:


 

如上截图所示,能够跳转到指定的线程中,并查看所在线程的正在运行的堆栈信息和寄存器信息。

 

 

总结:

如上,简单介绍了3种不一样状况下的gdb调试coredump文件的状况,基本涵盖了调试coredump问题时的大部分会用到的gdb命令。

gdb调试coredump,大部分时候仍是只能从core文件找出core的直观缘由,可是更根本的缘由通常仍是须要结合代码一块儿分析当时进程的运行上下文场景,才能推测出程序代码问题所在。

所以gdb调试coredump也是须要经验的积累,只有有必定的功底和对于基础知识的掌握才能在一堆二进制符号的core文件中找出问题的所在。