当前位置: 首页 > 科技观察

教你如何使用GNU调试器

时间:2023-03-16 13:28:27 科技观察

如果你是一名程序员,想为你的软件添加一些功能,你首先要考虑如何实现它:例如,写一个方法,定义一个类,或者创建一个新的数据类型。然后用编译器或解释器理解的编程语言实现此功能。但是如果你认为你所有的代码都是正确的,但编译器或解释器仍然不能理解你的指令怎么办?如果软件大部分时间运行良好,但在某些情况下会出现错误怎么办?在这种情况下,您需要知道如何正确使用调试器来找到问题的根源。GNU调试器GNU项目调试器(GDB)是一种用于查找项目缺陷的强大工具。它通过跟踪程序运行时发生的情况来帮助您找到程序错误或崩溃的原因。(LCTT注:GDB通篇为“GNUProjectDebugger”,即“GNUProjectDebugger”,但通常我们简称为“GNUDebugger”)本文是GDB的基本使用实战教程。按照示例,打开命令行并克隆此存储库:gitclonehttps://github.com/hANSIc99/core_dump_example.git快捷方式GDB的每个命令都可以缩短。例如:显示设置断点的infobreak命令可以缩写为ibreak。您可能在其他地方看到过这个缩写,但在本文中,我将写出整个命令,以便清楚地显示所使用的功能。命令行参数您可以将GDB附加到每个可执行文件。转到您克隆的存储库(core_dump_example),运行make进行编译。您现在应该看到一个名为coredump的可执行文件。(有关更多信息,请参阅我的文章《??创建和调试 Linux 的转储文件??》。)要将GDB附加到此可执行文件,请键入:gdbcoredump。您的输出应如下所示:gdbcoredump输出返回未找到调试符号。调试信息是目标文件目标文件(可执行文件)的组成部分,调试信息包括数据类型、函数签名、源代码和操作码之间的关系。此时,您有两个选择:继续调试汇编代码(请参阅下面的“未签名调试”)使用调试信息进行编译,请参阅下一节使用调试信息进行编译为了在您的二进制文件中包含调试信息,您必须重新编译。打开Makefile,删除第9行的注释标签(#),重新编译:CFLAGS=-Wall-Werror-std=c++11-g-gTellcompile调试器包含调试信息。运行makeclean,然后make,然后再次调用GDB。您可以在得到以下输出后调试代码:GDBoutputwithsymbols添加调试信息会增加可执行文件的大小。在这种情况下,执行文件增加了2.5倍(从26,088字节增加到65,480字节)。输入run-c1并使用-c1开关启动程序。当程序到达State_4时会崩溃:gdboutputcrashonc1switch您可以检索有关程序的附加信息,infosource命令提供有关当前文件的信息:gdbinfosourceoutput101行代码语言:C++编译器(版本、调优、体系结构、调试标志、语言标准)调试格式:DWARF2无预处理器宏(使用GCC编译时,宏仅使用-g3标志编译)。infoshared命令打印虚拟地址空间中动态库列表机器的地址,它们在启动时加载到那里,以便程序可以运行:gdbinfosharedoutput如果你想知道在Linux中如何处理库,请参阅我的文章HowtodealwithdynamicandstaticlibrariesinLinux。调试程序您可能已经注意到可以在GDB中使用run命令启动程序。run命令接受命令行参数,就像从控制台启动程序一样。-c1开关导致程序在第4阶段崩溃。要从头运行程序,不需要退出GDB,只需再次运行运行命令即可。如果没有-c1开关,程序会陷入死循环,必须使用Ctrl+C结束死循环。gdboutputstoppedbysigint您也可以逐步运行该程序。在C/C++中,入口点是main函数。使用listmain命令打开显示main函数的部分源码:gdboutputlistmainmain函数在第33行,所以可以在第33行输入break33添加断点:gdboutputbreakpointaddedinputrunrun该程序。正如预期的那样,程序在main函数处停止。输入layoutsrc并排查看源代码:gdboutputbreakatmain您现在处于GDB的文本用户界面(TUI)模式。您可以使用键盘向上和向下箭头键滚动浏览源代码。GDB突出显示当前正在执行的行。可以输入next(n)命令逐行执行命令。如果不指定新命令,GBD将执行之前的命令。要逐行运行代码,只需按回车键。偶尔,您会发现文本的输出显示有点不正确:gdboutputcorrupted如果发生这种情况,请按Ctrl+L重置屏幕。随时使用Ctrl+X+A进入和退出TUI模式。您可以在手册中找到其他键绑定。要退出GDB,只需键入quit。设置观察点示例程序的核心是一个在无限循环中运行的状态机。n_state变量枚举所有当前状态:while(true){switch(n_state){caseState_1:std::cout<<"State_1reached"<。要修改变量的值,请输入:setvariable。在下面的截图中,我将变量n_state_3_count的值设置为123。catch系统调用writeoutput/x表达式以十六进制打印该值;使用&运算符,您可以打印虚拟地址空间内的地址。如果您不确定符号的数据类型,可以使用whatis来查找。whatisoutput如果要列出main函数作用域内所有可用的变量,请输入infoscopemain:infoscopemainoutputDW_OP_fbreg值是指基于当前子程序位移的堆栈偏移量。或者,如果您已经在一个函数中并且想要列出当前堆栈帧上的所有变量,您可以使用infolocals:infolocalsoutput有关检查符号的更多信息,请参见手册。将调试附加到正在运行的进程gdbattach命令允许您通过指定进程ID(PID)附加到已运行的进程以进行调试。幸运的是,coredump程序会将其当前的PID打印到屏幕上,因此您不必使用ps或top手动查找PID。启动一个coredump应用程序的实例:./coredumpcoredump应用程序操作系统显示PID为2849。打开一个单独的控制台窗口,移动到coredump应用程序的根目录,并使用GDB附加到进程进行调试:gdbattach2849attachGDBtocoredump当你用GDB附加到一个进程时,GDB会立即停止进程运行。输入layoutsrcandbacktrace查看调用栈:layoutsrcandbacktraceoutput输出显示main.cpp的第92行调用了std::this_thread::sleep_for<。..>(...)函数执行时进程中断。只要退出GDB,进程就会继续运行。您可以在GDB手册中找到有关附加到正在运行的进程的更多信息。向上移动堆栈在命令窗口中,键入两次up以将堆栈向上移动到main.cpp:moveupthestacktomain.cpp通常,编译器会创建一个子例程。每个子例程都有自己的堆栈帧,因此向上移动堆栈帧意味着向上移动调用堆栈。您可以在手册中找到有关堆栈计算的更多信息。指定源文件当调试一个已经运行的进程时,GDB将在当前工作目录中查找源文件。您也可以使用directory命令手动指定源目录。评估转储文件请阅读为Linux创建和调试转储文件以获取有关此主题的信息。参考文章太长,简单说一下:假设你使用的是最新版本的Fedora使用-c1开关调用coredump:coredump-c1使用GDB加载最新的dump文件:coredumpctldebug开启TUI模式并输入layoutsrccoredumpoutputbacktrace输出显示崩溃发生在距离main.cpp五个堆栈帧处。回车直接跳转到main.cpp中的错误代码行:up5output查看源代码,发现程序试图释放一个内存管理函数不返回的指针。这会导致未定义的行为并引发SIGABRT。没有符号的调试没有源代码调试变得非常困难。当我试图解决逆向工程挑战时,我第一次遇到了这种情况。一些汇编语言知识会很有用。让我们通过一个例子看看它是如何工作的。找到根目录,打开Makefile,编辑第9行如下:CFLAGS=-Wall-Werror-std=c++11#-g重新编译程序,先运行makeclean,再运行make,最后启动GDB。该程序不再有任何调试符号来指导源代码的去向。nodebuggingsymbolsinfofile命令显示二进制文件的内存区域和入口点:信息文件output.text部分始终从入口点开始并包含实际操作码。要在入口点添加断点,请键入break*0x401110,然后键入run以启动程序:breakpointattheentrypoint要在地址设置断点,请使用取消引用运算符*指定地址。选择反汇编程序风格在深入研究汇编之前,您可以选择要使用的汇编风格。GDB默认使用AT&T,但我更喜欢Intel语法。改变风格如下:改变汇编风格现在输入layoutasm调出汇编代码窗口,输入layoutreg调出寄存器窗口。您现在应该看到这样的输出:setdisassembly-flavorintellayoutasmandlayoutregoutput保存配置文件虽然您输入了很多命令,但您还没有真正开始调试。如果您正在对应用程序进行大量调试或试图解决困难的逆向工程问题,那么将GDB特定设置保存在一个文件中会很有用。该项目的GitHub存储库中的gdbinit配置文件包含最近使用的命令:setdisassembly-flavorintelsetwriteonbreak*0x401110run-c2layoutasmlayoutregsetwriteon命令使您可以在程序执行期间修改二进制文件。退出GDB并使用配置文件重新启动GDB:gdb-xgdbinitcoredump。在读取指令上应用c2开关后,程序崩溃。程序停在入口函数,所以必须写继续继续:crash后继续执行idiv整数除法说明:RAX寄存器为被除数,指定参数为除数。商被加载到RAX寄存器中,余数被加载到RDX中。从寄存器的角度来看,您可以看到RAX包含5,因此您必须在存储堆栈上找到位置rbp-0x4的值。读取内存要读取原始内存内容,您必须指定比读取符号更多的参数。在汇编输出中向上滚动一点可以看到堆栈的划分:堆栈划分输出你应该对rbp-0x4的值最感兴趣,因为它是idiv的存储参数。您可以从屏幕截图中看到rbp-0x8处的下一个变量,因此rbp-0x4处的变量是4个字节宽。在GDB中,可以使用x命令查看任意内存内容:x/<可选参数n,f,u><内存地址addr>可选参数:n:单位大小重复次数(默认:1)f:格式说明符,例如printfu:单位大小b:字节h:半字(2字节)w:字(4字节)(默认)g:双字(8bytes)toToToprintvalueofrbp-0x4,pleaseenterx/u$rbp-4:printvalue如果你能记住这个模式,可以直接查内存。请参阅手册中的查看内存部分。操作汇编子程序zeroDivide()发生操作异常。当你用向上箭头键向上滚动一点时,你会发现以下信息:0x401211<_Z10zeroDividev>pushrbp0x401212<_Z10zeroDividev+1>movrbp,rsp这叫做函数序言:调用的基指针函数(rbp)将栈指针(rsp)中存储在栈中的值加载到基指针(rbp)中,从而完全跳过这个子程序。您可以使用回溯来查看调用堆栈。main函数之前只有一个栈帧,所以你可以通过up:Callstack程序集返回main在你的main函数中,你会发现以下信息:0x401431cmpBYTEPTR[rbp-0x12],0x00x401435je0x40145f0x401437调用0x401211<_Z10zeroDividev>子程序zeroDivide()只有跳转等于(je)为真时才进入。您可以轻松地将其替换为jump-not-equal(jne)指令,该指令的操作码为0x75(假设您使用的是x86/64;其他操作码不同)。键入运行以重新启动程序。当程序停在入口函数时,设置操作码:set*(unsignedchar*)0x401435=0x75最后进入Continue。该程序将跳过子例程zeroDivide()并且将不再崩溃。总结您会发现GDB在许多集成开发环境(IDE)的后台运行,包括QtCreator和VSCodium的NativeDebug扩展。VSCodium中的GDB有助于了解如何充分利用GDB的功能。通常,并非GDB的所有功能都在IDE中可用,因此您可以从从命令行使用GDB的经验中受益。