gdb调试

本文转自:http://www.javashuo.com/article/p-xvvtsuge-v.html

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 <[email protected]>
=> 0x080483ec <+24>:    leave
   0x080483ed <+25>:    ret
End of assembler dump.

 

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

 

如上截图,shell echo [email protected] |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函数里定义一个子类的实例化对象,并调用它的虚函数方法test,test里由于直接delete没有初始化的指针childPStr,肯定会造成coredump。本次我们就希望通过dump文件,找到子类dumpTest的this指针和虚函数指针。

和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文件中找出问题的所在。