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指令用于设置断点,后面除了跟行号,还可以:
- 如果是多文件的场景设置断点:
b <file_name>:<line_number>
,此时file_name无需路径。 - 还可以直接用函数名设置断点:
b function_name
,比如:b main
。 - 或者:
b <file_name>:<function_name>
。 - 还可以在设置断点的时候,增加一个condition:
b location if condition
。 - 将断电设置在一个地址上,
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
gdb a.out core.11246
在地址 0x00000000004008ce 处发生 coredump
然后我们可以执行程序进行反汇编
objdump --source -d a.out > a.asm
搜索地址 “4008ce” 当赋值 0x23 时发生 coredump。这就可以和真实的代码对上了
3. 终端调试分析---Segmentation fault
Segmentation fault(段错误)是一种由于程序访问了未分配的内存地址或者越界访问内存而导致的错误。当程序试图访问不属于它的内存段(比如访问空指针或者数组越界)时,操作系统会发送一个信号给程序,通知它发生了段错误。这通常是由于程序bug导致的,需要开发人员进行代码调试和修复。这类情况都是比较准的,所以我们可以使用disassemble
反汇编的形式来看相关挂掉的地方。
查看堆栈使用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的原因。
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
函数里定义一个子类的实例化对象,并调用它的虚函数方法test
,test
里由于直接delete
没有初始化的指针childPStr
,肯定会造成coredump
。本次我们就希望通过dump
文件,找到子类dumpTest
的this
指针和虚函数指针。
./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指针、虚指针能验证我们的推测。之后再结合代码寻找问题所在。
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文件会报错,报错信息如下:
由于上面代码里在count等于5的时候,会delete一个未初始化的指针,肯定会coredump。
如上,gdb打开coredump文件,能看到5个线程LWP的信息。
如何,查看每个线程的堆栈信息呢?
首先,info threads
查看所有线程正在运行的指令信息
thread apply all bt
打开所有线程的堆栈信息
查看指定线程堆栈信息:threadapply threadID bt,如:
thread apply 5 bt
进入指定线程栈空间
thread threadID
如下:
如上截图所示,可以跳转到指定的线程中,并查看所在线程的正在运行的堆栈信息和寄存器信息
参考链接
详解gdb常用指令 | CS笔记 Coredump文件简易指南 | 孙勇峰的部落格 反汇编可执行程序--->分析coredump文件 - OSCHINA - 中文开源技术交流社区 https://hchen90.top/2019/11/12/linuxcoredumptofindbug/
gdb调试coredump(使用篇)_test-coredump-unwind.c:248: undefined reference to-优快云博客