如何使用反汇编等高级用法来完成coredump查询

0. 简介

coredump是一个使用c++编程工作者最常用的方法,但是如果在 GCC -O3 优化级别下,很多局部变量是会被优化掉的,此时只能通过人工分析反汇编代码来获取所需信息,而这么做的前提是保存下来的寄存器中的值是准确的。绝大部分情况下 coredump 是由于 segment fault 或 assert 触发的,segment fault 情况下 Kernel 保存下来的 registers 信息是准确的,GDB 中直接用 info registers 就可以看到。然而若是由 assert 触发,由于 assert 会进行多层函数调用后最终执行 raise(),错误现场的寄存器信息是不准确的,这时候就需要一些其他手段来解决此问题。为此我们这篇文章将着重介绍如何使用coredump当中的高级用法。这里主要参考了僷枫_华大佬的博客,并结合自己的理解做了一些整合和梳理

1 GDB使用整理

1.1 主动生成运行中进程的core dump

这种需求主要发生在进程卡死,想知道卡在何处时。

gcore -o <core dump文件名> <pid>

可以主动生成core dump文件,这个操作不会杀死进程,如果有需要可手动杀死。

然后参照[使用core dump](#使用core dump)的步骤去查看调用堆栈就可以了

1.2 GDB调试指令

  • q[uit]

退出gdb的q指令。

  • help

在gdb中,用help指令查看各种指令的联机帮助。

  • file

如果在启动gdb的时候,没有指定program,在gdb内,用file指令指定。

  • set args

用于在run程序之前,指定此程序的命令行参数,也可以用来去掉命令行的参数,如下:

(gdb) set args [arguments]
  • set print pretty on

漂亮的打印信息,总是需要的。

  • b[reak]

进入gdb后,一般我们都要先设置断点,然后再启动程序。

b指令用于设置断点,后面除了跟行号,还可以:

  1. 如果是多文件的场景设置断点:b <file_name>:<line_number>,此时file_name无需路径。
  2. 还可以直接用函数名设置断点:b function_name,比如:b main
  3. 或者: b <file_name>:<function_name>
  4. 还可以在设置断点的时候,增加一个condition:b location if condition
  5. 将断电设置在一个地址上,b *0x400540,有个*,调试汇编代码时很好用。

gdb还有很多其它花样的break:

  • tbreak

临时断点,作用一次后自动消失,设置方式与break相同。

  • rbreak regex

对符合regex正则表达式条件的函数名设置断点,效果与break一样,不会自动消失。

  • l[ist]

在gdb中查看源代码和对应的行号。

l指令输入后,默认从当前位置打印10行代码出来显示,按回车(自动执行上一条gdb命令)会自动继续显示10行代码。改变list指令默认behavior可以这样:

(gdb) l <line_number> # 从指定行号开始打印10号代码 (gdb) l <start_line_number>,<end_line_number> (gdb) l <function_name>

  • show listsize

查看l指令显示代码行数。

  • set listsize

设置l指令显示代码的行数,默认为10。

  • r[un] [args]

r指令(重新)启动程序,直到程序结束,或Crash,或者遇到breakpoint。

程序在断点停下来后,就要开始一点点推进程序代码的执行,在此过程中仔细检查各变量和相关内存区域的值。用r指令也可以在任何时候**重新运行程序!**r指令也可以带上给调试进程的命令行参数,但在重复run时,要去掉args,只能使用set args

  • start

启动调试程序,停在main处,使用start指令,gdb自动在main处设置了一个临时断点。同run指令一样,start也可以重复执行,每次都会停在main开始的位置。

  • kill

stop debugging.

  • n[ext]

执行下一条指令,如果下一条指令是函数调用,不进入函数。

直接回车,重复上一个命令!如果上一个命令是n,就很有用。

  • s[tep]

执行下一条指令,如果下一条指令是函数调用,进入函数。

  • c[ontinue]

继续,continue,直到下一个breakpoint,或者程序结束,或者程序Crash。所以,r指令一般只用一次,然后就不再用了,后面要让程序继续跑起来,用c指令。

  • finish

将当前的function执行完,返回function调用的地方。

  • where/bt

显示backtrace,查看当前程序调用栈信息,以及程序当前停在哪一行的。

  • advance

advance指令实现让代码自动运行到指定函数的入口,并进入函数。

可以简单地用advance bar,来代替tbreak bar; continue。如果没有到达指定位置,执行会在当前函数末尾停下来(Execution will also stop upon exit from the current stack frame)。

  • skip

Ignore a function while stepping.

  • i[nfo]

用于显示各种信息。

  • i[nfo] b

查看所有breakpoint信息,此处会显示出断点的编号。

  • i[nfo] shared

查看程序使用的动态链接库信息。

  • i[nfo] locals

查看所有local variables。

  • i[nfo] frame

查看当前的stack frame。

help info可以看到更多关于info的选项。

  • delete

删除N号断点。

  • delete [breakpoints]

清除所有断点。

  • disable/enable

关闭/开启N号断点。

  • p[rint]

print指令,后面跟一个表达式,显示表达式的值。

同时显示多个变量的值:print {var1, var2, var3},like this...

可以通过/fmt格式来增加格式信息,比如:p /x *b,以hex的方式打印b地址的内容。格式与x指令通用。p指令还可以用来查macro的“值”,但是需要gcc编译的时候,带上-g3 -gdwarf-2参数。

  • x examine memory,这里的memory,是指程序的虚拟地址空间。

显示当前rip指向地址的4条汇编指令:

(gdb) x /4i $pc
  • display

设置在程序停止运行时,自动显示的表达式。在单步调试的时候,这个指令很有用,每次next,自动显示你关心的变量值。可以设置多个display,用多行命令,或者display {exp1, exp2, exp3}

  • undisplay

取消display。

  • macro

调试代码中的macro。复杂代码中到底哪个macro在起作用?!

(gdb) help macro

macro exp最有用...

  • disass[emble]

将当前RIP位置周围的代码进行反汇编显示,或反汇编指定的函数,或某个地址范围进行反汇编显示。

disass          # show一下周围的汇编指令 disass $pc,+16  # 显示从rip地址开始,向后16bytes的汇编 disass funcname # disassemble function disass 0x400544 disass 0x400544, 0x40054d

  • show disassembly-flavor
(gdb) show disassembly-flavor The disassembly flavor is "att".
  • set disassembly-flavor
(gdb) set disassembly-flavor intel (gdb) disass

是时候学点汇编了!

  • i[nfo] r[egister]

显示所有整型寄存器的值。也可以指定显示某个寄存器:

(gdb) i r rax rax            0x2c63652           46544466
  • i[nfo] all-registers

显示所有寄存器。

  • i[nfo] line [N]

将源代码中的某行,与地址范围对应起来。例如:

(gdb) info line 5 Line 5 of "test.c" starts at address 0x401146 <main> and ends at 0x40114e <main+8>.

1.3 调试线程

  • i threads

显示所有线程,每个线程有个编号。

  • thread

切换到编号为N的线程。

  • thread apply [...]

让一个或多个线程执行gdb的command命令。

2. 反汇编直接分析

这种方法是最常见也是最方便的方法。下图是人为制造coredump,31 行对空指针赋值,产生 coredump

82cc5a8a3bc64eacb85f4fe2691b1536.png

gdb a.out core.11246

在地址 0x00000000004008ce 处发生 coredump

5494a25029f44183b0b24c9f4ffbedfc.png

 然后我们可以执行程序进行反汇编

objdump --source -d a.out > a.asm

搜索地址 “4008ce” 当赋值 0x23 时发生 coredump。这就可以和真实的代码对上了

0d2b37874b9c44f4b91a17cc925182cb.png

3. 终端调试分析---Segmentation fault

Segmentation fault(段错误)是一种由于程序访问了未分配的内存地址或者越界访问内存而导致的错误。当程序试图访问不属于它的内存段(比如访问空指针或者数组越界)时,操作系统会发送一个信号给程序,通知它发生了段错误。这通常是由于程序bug导致的,需要开发人员进行代码调试和修复。这类情况都是比较准的,所以我们可以使用disassemble反汇编的形式来看相关挂掉的地方。

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

5fb93bc3ac0349ac13655763b5f7bfb1.png

 如上,在带上调试信息的情况下,我们实际上是可以看到core的地方和代码行的匹配位置。

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

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

ede7c767fadb8573672f232136c920e9.png

 没有调试信息的情况下,打开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时该函数调用所在的位置

28fd563e76bb02e52f5ad218ea3cc776.png

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

不过上面的free使用去掉名词修饰效果和之前还是一样的。但是我们可以推测到这里是在调用free函数。

如此,我们就能知道我们coredump的位置,从而进一步能推断出coredump的原因。

当然,现实环境中,coredump的场景肯定远比这个复杂,都是逻辑都是一样的,我们需要先找到coredump的位置,再结合代码以及core文件推测coredump的原因。

4. 终端调试分析---Assert

Assert(断言)是一种在程序中加入的检查机制,用于检测程序的正确性。在代码中,开发人员可以使用assert宏来判断一个表达式是否为真,如果表达式为假,则程序会终止并输出错误信息。断言通常用于在开发过程中检查程序的逻辑是否正确,以帮助开发人员快速发现问题。这类断言一般的是在堆栈处理的时候出现的问题,这种情况下需要分析堆栈的问题,堆栈问题主要就是使用的是info frame以及info registers这类来分析是否是空等信息。

#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指针和虚函数指针。

./DumpCppTest  执行该程序,程序因为直接delete未初始化的指针,肯定会coredump。生成core文件如下

00d19c781fccc6aa6d7b433b137a9c3d.png

 使用gdb打开core文件,同时bt打开core的堆栈信息。从堆栈可以看到,最后两帧为我们程序自己的函数,其他的都是libc的代码。 f 6 调到第6帧上,之后info frame查看堆栈寄存器信息。

8a0477afeedc7c2c60d7504879af81a4.png

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

b8123ae8c6f85cf33d3f64916c9a8d7f.png

 如图所示,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 可以看到,存储的内容就是

d4675929ce62cae29d0920032e33367d.png

 dumpTest::test() 函数。

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

bd5ccf8e5500c5f6689c315db5b877fb.png

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

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

5. 线程堆栈

#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文件会报错,报错信息如下:

739585b2637c35ae6ea50719250ca8d2.png

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

ae2a611742a0d925bf842e54055978ad.png

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

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

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

50fc5fba4f15b1816d76d754a5870462.png

 thread apply all bt打开所有线程的堆栈信息

76bef0efdd6d0159b6d39151d508b06d.png

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

thread apply 5 bt

eedca9ba2b9d988c8aa4319f0f36270e.png

 进入指定线程栈空间

thread threadID如下:

2b5ff3ee1937ac436232f0b0d184768e.png

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

参考链接

详解gdb常用指令 | CS笔记 Coredump文件简易指南 | 孙勇峰的部落格 反汇编可执行程序--->分析coredump文件 - OSCHINA - 中文开源技术交流社区 https://hchen90.top/2019/11/12/linuxcoredumptofindbug/

gdb调试coredump(使用篇)_test-coredump-unwind.c:248: undefined reference to-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值