GDB
GDB
一. GDB简介
1.1
支持的语言
二. 使用
1.0 例子代码
#include <stdio.h>
/* print a given string and a number if a pre-determined format. */
void
print_string(int num, char* string)
{
printf("String '%d' - '%s'\n", num, string);
}
int
main(int argc, char* argv[])
{
int i;
/* check for command line arguments */
if (argc < 2) { /* 2 - 1 for program name (argv[0]) and one for a param. */
printf("Usage: %s [<string> ...]\n", argv[0]);
exit(1);
}
/* loop over all strings, print them one by one */
for (argc--,argv++,i=1 ; argc > 0; argc--,argv++,i++) {
print_string(i, argv[0]); /* function call */
}
printf("Total number of strings: %d\n", i);
return 0;
}
1.1 调用gdb调试器
gcc -g debug_me.c -o debug_me
gdb debug_me
1.2 运行
run "hello, world" "goodbye, world"
1.3 设置断点
仅运行代码的问题在于它会一直运行直到程序退出,这通常为时已晚。为此,引入了断点。断点是调试器在执行特定源代码行之前停止程序执行的命令。我们可以使用两种方法设置断点:
1.3.1 指定要停止的特定代码行:
break debug_me.c:9
1.3.2 指定一个函数名,每次调用时都要中断:
break main
1.3.2 断点相关命令
break 6 //简写成b 6,既在第六行设置断点
info b //显示当前程序的断点设置情况
扩展:
b fn1 if a>b //条件断点设置
break func //在函数func()的入口处设置断点,如:break cb_button
delete 断点号n //删除第n个断点
disable 断点号n //暂停第n个断点
enable 断点号n //开启第n个断点
clear 行号n //清除第n行的断点
1.4 一次执行一个命令
break main
run "hello, world" "goodbye, world"
调试器给出如下内容:
Starting program: /usr/home/choo/work/c-on-unix/debug_me
warning: Unable to find dynamic linker breakpoint function.
warning: GDB will be unable to debug shared library initializers
warning: and track explicitly loaded dynamic code.
Breakpoint 1, main (argc=1, argv=0xbffffba4) at debug_me.c:9
9 if (argc < 2) { /* 2 - 1 for program name (argv[0]) and one for a param. */
(gdb)
1 "next": 使调试器执行当前命令,然后再次停止,显示要执行的代码中的下一个命令。
2 "step": 使调试器执行当前命令,如果它是函数调用 - 在该函数的开头中断。这对于调试嵌套代码很有用。
1.5 打印变量和表达式
print i
您也可以尝试打印更复杂的表达式,例如“i*2”、“argv[3]”或“argv[argc]”等。你也可以使用类型转换,调用程序中的函数。
1.6 检查函数调用堆栈
现在正在执行什么函数,哪个函数调用了它(目前在那个位置上)
where
#0 print_string (num=1, string=0xbffffc9a "hello") at debug_me.c:7
#1 0x80484e3 in main (argc=1, argv=0xbffffba4) at debug_me.c:23
该列表也称未:堆栈跟踪
frame 1
print i
“frame”命令告诉调试器切换到给定的堆栈帧('0' 是当前执行函数的帧)。在那个阶段,调用的任何打印命令都将使用该堆栈帧的上下文。
当然,如果我们发出“step”或“next”命令,程序将在顶部帧继续,而不是在我们要求查看的帧。毕竟,调试器无法“撤消”所有调用并从那里继续。
1.7 附加到已经运行的进程
可能想要调试一个无法从命令行启动的程序。这可能是因为程序是从某个系统守护进程启动的(比如网络上的 CGI 程序),
我们懒得让它直接从命令行运行。或者程序运行它的初始化代码可能需要很长时间,并且使用附加的调试器启动它会导致这个启动时间更长。
还有其他原因。为此,将以这种方式启动调试器:
gdb debug_me 9561
这里我们假设“debug_me”是所执行程序的名称,而 9561 是我们要调试的进程的进程 ID (PID)。
gdb 首先尝试寻找一个名为“9561”的“核心”文件(我们将在下一节中看到核心文件是什么),当它找不到时,它会假设提供的数字是进程 ID,并尝试附加到它。如果有进程执行与我们提供给 gdb 的路径完全相同的程序(不是文件的副本。它必须与进程运行的文件完全相同),它将附加到程序,暂停执行,并让我们继续调试它,就好像我们从调试器内部启动程序一样。当我们得到 gdb 的提示时,做一个“where”将向我们显示进程的堆栈跟踪,我们可以从那里继续。一旦我们退出调试器,它将从进程中分离出来,并且进程将从我们离开它的地方继续执行。
1.8 调试崩溃的程序
调试程序的问题之一,与墨菲定律有关: 程序会在最意想不到的时候崩溃。这句话只是意味着当你把程序作为生产代码取出后,它会崩溃。而且这些错误不一定很容易重现。幸运的是,在“核心文件”的形象中,我们有一些帮助。
核心文件包含进程的内存映像,以及(假设进程中的程序包含调试信息)其堆栈跟踪、变量内容等。程序通常设置为在由于 SEGV 或 BUS 等信号崩溃时生成包含其内存映像的核心文件。如果调用程序的 shell 没有设置限制这个核心文件的大小,我们会在进程的工作目录中找到这个文件(或者是它启动的目录,或者它最后切换到的目录,使用chdir系统调用)。
一旦我们得到这样一个核心文件,我们可以通过发出以下命令来查看它:
gdb /path/to/program/debug_me core
这假设程序是使用这个路径启动的,并且核心文件在当前目录中。如果不是,我们可以给出核心文件的路径。当我们得到调试器的提示时(假设核心文件被成功读取),我们可以发出诸如“print”、“where”或“frame X”之类的命令。我们不能发出暗示执行的命令(例如“下一步”,或函数调用的调用)。在某些情况下,我们将能够看到导致崩溃的原因。
需要注意的是,如果程序由于无效的内存地址访问而崩溃,这将意味着程序的内存已损坏,因此核心文件也已损坏,因此包含错误的内存内容,无效的堆栈帧等. 因此,我们应该将核心文件的内容视为一个可能的过去,在许多可能的过去中(这使得核心文件分析与量子理论非常相似。几乎)。
1.9 一些命令
list 1 // 将显示当前文件以“行号”为中心的前后10行代码,list 10
info frame // 查看堆栈列表
info registers //查看所有寄存器
info function //查询函数
x/4x $esp //查看4个字节的栈顶寄存器的内容
x/4x $ebp //查看4个字节的栈底寄存器的内容
扩展知识
(r是64位,e是32位)
//eax 累加寄存器,它是很多加法乘法指令的缺省寄存器
//ebx 基地址寄存器,在内存寻址时存放基地址
//esi 来源索引暂存器 edi 目的索引暂存器
//esp 栈顶寄存器 ebp 栈底寄存器
//eflags 状态位寄存器
//ss 堆栈段寄存器
//cs 代码段寄存器
//ds 数据段寄存器
三. 获取有关调试的更多信息
现在可能是时候使用您的程序和调试器了。建议在 gdb 中尝试“帮助”,以了解有关其命令的更多信息。尤其是“dir”命令,它可以调试源代码被拆分到多个目录的程序。
一旦你觉得 gdb 过于局限,你可以尝试各种图形调试器。尝试检查您是否安装了“xxgdb”——这是一个在 gdb 之上运行的图形界面。如果觉得太难看,可以试试“ddd”。与 xxgdb 相比,它的主要优势在于它允许您以图形方式查看指针、链表和其他复杂数据结构的内容。它可能没有安装在您的系统上,因此您需要从网络上下载它。
如果您在 SunOs 或 Solaris 环境中运行,则有一个名为“gcore”的程序,它允许获取正在运行的进程的核心,而无需停止它。如果进程在无限循环中运行,并且您想将核心文件放在一边,或者您想调试正在运行的进程而不中断它太长时间,这很有用。