gdb调试相关

3.2.1 断点的工作原理 在本书的所有地方都使用了STATUS_BREAKPOINT异常,尤其是在本章中,但却没有很明确地解释这个异常的引发方式。现在,我们就来解释如何在进程中产生这个异常。 在x86指令集中包含了一个特殊的指令int 3,这个指令将在处理器上产生硬件中断STATUS_BREAKPOINT以用于调试。为了响应异常STATUS_BREAKPOINT,处理器将执行位于中断矢量3中的中断处理器。中断处理器将把这个硬件异常转换为在这条指令地址上引发的一个软件异常。这条指令在指令流中被表示为一个字节0xCC,也被称之为操作码(Operation Code)或者opcode。在没有调试器的情况下,软件异常将被视作为一个普通的异常;否则,Windows操作系统将告诉调试器在这条指令的地址上发生了中断。 调试器将通过0xCC来设置断点。在设置断点时,调试器将首先修改断点地址所在内存块的保护模式,这是为了接下来在这个地址上写入一个int 3指令。这个地址上原来的值,以及关于断点编号的信息,都将被保存在调试器的内存中。 断点地址必须是指令流中的一个有效指令的地址,这个地址通常是一条机器指令的第一个字节。如果在机器指令的其他地址上设置的断点,那么将改变指令的含义,从而导致这条指令不会触发硬件异常STATUS_BREAKPOINT。显然,运行一个包含错误机器指令的程序是非常危险的,并且将产生不可预测的行为。 这种修改内存的操作对于用户来说应该是不可见的,因为这些修改将影响对代码进行反汇编的结果。因此,当调试器停止时,会把所设置的断点又替换为原来的指令,当调试目标再次运行时,将再次把int 3的操作码插入到目标映象中。 为了说明这种机制,我们在调试器中启动调试目标notepad.exe。在触发初始断点时,我们在任意地址上设置一个断点,在本示例中是notepad!WinMain的起始地址,并且将另一个调试器以非侵入的方式附加到同一进程并查看在这个地址上的指令。这将为我们揭示调试目标真实的内存内容。 当用户态调试器等待用户输入命令时,在内存中将包含最初的指令流。当执行调试目标时,我们将在用户态命令窗口中键入命令g来改变内存,如清单3.30中第二部分所示。 在设置断点时,内核态调试器将遵循相同的模型,只不过操作系统的内存管理机制将带来一些差异。在Windows操作系统中,大多数包含可执行代码的内存页在多个进程之间是共享的,正是由于这个功能才使得同一个DLL可以被加载到不同的进程中。当用户态调试器激活一个新的断点时,它会把内存页的保护状态从“只读”变为“读写”。通过“写时复制(Copy On Write,COW)”技术生成的新内存页将作为被调试进程的私有页,并且在对其进行修改时不会影响共享这个页的其他进程。由于内核态调试器无法通过COW技术来生成一个私有页,它将直接在共享页中设置断点。 内核态断点将会影响共享这个内存页的所有进程。而且,根据系统中可用的内存量,在被调试进程执行完之后,内核态断点仍有可能驻留在系统内存中。在实际的调试情况中,这种情况带来的后果是很难预测的,这就像内存负载和整体系统的行为将对Windows内存管理产生极大的影响。然而,我们可以得出一些关于内核态断点的结论: ■如果在多个进程共享的内存页中设置断点,那么在所有这些进程中都会产生中断。由于内核调试器在处理断点时相对较慢,尤其是当通过串行电缆来调试时,因此我们绝不应该在一些被频繁调用的函数中设置断点,例如ntdll!RtlAllocateHeap。我们可以通过EPROCESS地址或者KTHREAD地址来缩小断点的范围,从而减少调试器的停止次数。不幸的是,调试器在每次遇到断点时仍然会收到通知,只不过对于所有不匹配的进程,调试器将自动处理断点。 ■当内核调试器中的被调试进程结束后,所有的用户态断点都必须被删除以避免与其他运行中的进程发生冲突(共享页将仍然在内存中停留一段时间,其中保留了之前设置的所有断点,即使进程被重新启动也是如此)。 ■当用户态调试器与内核态调试器一起使用时,通常必须从内核态调试器中设置这些断点。否则,断点异常将被分发给用户态调试器。由于无法知道int 3其实是一个断点而并非真实的int 3指令,因此执行流将被破坏。显然,在键入命令g后执行的指令流将是完全错误的,最终将在某个调试器中出现大量的访问违例异常或者单步异常。 GDB是一个极为强大的调试工具,绝对比那些IDE下辖的调试器强大得多。在GDB的学习过程中一个核心的问题就是Call Stack的理解,这是GDB安身立命之根本。进一步的,就要涉及到core dump的使用了。既然是只谈论GDB的基础,那么本文中就不涉及到core dump的问题,有兴趣的可以自己google一下。 准备工作 gdb需要的文件是可执行文件。要想使用gdb去调试某个可执行文件,必须在用gcc或g++生成该文件的时候使用-g选项加入调试信息。 载入可执行文件 产生了可调试的可执行文件后,必须将其载入到gdb中才可进行调试。载入文件有两种办法: 1> 使用命令 gdb execfilename 载入; 2> 在进入gdb后,使用 file execfilename 或是 target exec execfilename 断点管理 使用break(简写为b即可)命令设置程序断点。 break:可以在程序运行时利用frame命令选定某一个stack frame,然后执行命令break。这样使得一旦程序执行到该选中stack frame中时,就停下。 break location:其中的location可以指定为当前源文件的第几行,或是针对当前位置的偏移量,也可以指定为函数名,甚至可以指定内存中的位置。 break location if condition:这是一个条件断点设置语句。 除了break之外,还有几个断点设置的关键字: tbreak:在程序的一次运行中只停止一次。 rbreak regex:利用正则表达式设置一系列符合regex的断点。 可以使用info命令来查看断点信息,使用disable和enable来禁用启用断点,使用clear和delete来删除指定断点。 观察点管理 使用watch命令设置程序观察点。 watch expr:其中的expr可以是参数名,也可以是表达式。当expr的值被改写的时候程序就会暂停执行。 除了watch之外,还有几个观察点设置的关键字: rwatch:当参数或是表达式的值被程序读取的时候暂停。 awatch:当参数或是表达式的值被读取或改写的时候都会暂停。 可以使用info命令来查看断点信息,使用disable和enable来禁用启用观察点,使用delete来删除指定观察点。 设置程序运行参数 set args:可指定运行时参数。(如:set args 10 20 30 40 50); show args:命令可以查看设置好的运行参数。 变量管理 变量值显示:print var 变量值修改:set var=value 按指定地址输出内存值:x /NFU ADDR 其中,U指定一个单元的大小,可以是b(单字节)、h(双字节)、w(四字节)、g(八字节);N表示输出的单元个数;F表示输出的数据形式,可以是x(16进制整数格式 )、d(有符号十进制整数格式)、u(无符号十进制整数格式)、f(浮点数格式 )。 调试过程控制 step:单步执行,进入函数内部; next:单步执行,不进入函数内部; continue:恢复正常连续执行。 信息查询 info:这个应该是调试时用得最多的信息显示命令。它的主要作用是显示正在被调试的程序的一些指定信息,如breakpoints、watchpoints,frame中的args等等。 print:一般用来打印变量的当前值。 show:显示当前gdb本身的一些参数信息。 list:一般用于源文件的显示。 Call Stack stack frame:当某个函数被调用的时候,它的调用入口,参数,局部变量都将会被保存在栈中的一块内存中,这块内存就叫stack frame。 call stack:所有的stack frames所占据的空间就叫call stack。最近被调用的函数的stack frame总是位于call stack的最里面,依次向外扩散:最里面的stack frame编号为0. 有了以上两个概念之后,我们就可以讲述gdb调试实现的机制了。gdb工作的基本单元就是stack frame,简称frame。比如用print var来打印变量名时,gdb是会在当前选中的frame(默认是当前工作的frame,除非手动选择别的frame)中寻找并打印该变量。这就是理解gdb工作的关键! 以下是一些操作frame并显示frame信息的命令: frame framlocation:指定gdb工作的frame。其中,framlocation可以是frame的编号,也可以是frame的内存地址。 bt:显示当前程序Call stack中的函数调用顺序,或者说是frame的顺序。命令where和info stack和bt是一样的。 info frame:显示当前frame的详细信息。 info args:显示当前frame所对应的函数的参数信息。 info locals:显示当前frame所对应的函数的局部变量信息。 执行Shell命令 直接在gdb提示符后输入 shell commandname 。而如果需要在gdb中直接make源文件的话,直接在gdb提示符后输入 make make_args 即可;也可输入shell make make_args。 本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dexingchen/archive/2008/12/10/3486732.aspx gdb使用 无论是多么优秀的程序员,都难以保证自己在编写代码时不会出现任何错误,因此调试是软件开发过程中的一个必不可少的组成部分。当程序完成编译之后,它很可能无法正常运行,或者会彻底崩溃,或者不能实现预期的功能。此时如何通过调试找到问题的症结所在,就变成了摆在开发人员面前最严峻的问题。通常说来,软件项目的规模越大,调试起来就会越困难,越需要一个强大而高效的调试器作为后盾。对于Linux程序员来讲,目前可供使用的调试器非常多,GDB(GNU DeBugger)就是其中较为优秀的。      初识GDB      GDB是自由软件基金会(Free Software Foundation,FSF)的软件工具之一。它的作用是协助程序员找到代码中的错误。如果没有GDB的帮助,程序员要想跟踪代码的执行流程,唯一的办法就是添加大量的语句来产生特定的输出。但这一手段本身就可能会引入新的错误,从而也就无法对那些导致程序崩溃的错误代码进行分析。GDB的出现减轻了开发人员的负担,他们可以在程序运行的时候单步跟踪自己的代码,或者通过断点暂时中止程序的执行。此外,他们还能够随时察看变量和内存的当前状态,并监视关键的数据结构是如何影响代码运行的。      调试方法      如果想对程序进行调试,必须先在用GCC编译源代码时加上-g选项,以便产生GDB所需要的调试符号信息。例如,debugme.c是一个存在错误程序,可以使用如下的命令对其进行编译,同时产生调试符号:   # gcc -g debugme.c -o debugme      如果愿意的话,还可以在编译时使用“-ggdb”选项来生成更多的调试信息。由于这些调试信息中的相当一部分是GDB所特有的,所以生成的代码将无法在其它调试器中正常调试。对于大多数情况来说,普通的-g选项就足够了。需要注意的是,GCC虽然允许同时使用-g(调试)和-o(优化)选项,但优化会影响最终生成的代码,导致程序源代码和二进制代码之间的关系变得复杂起来。如果不想为调试制造障碍,建议不要将-g和-o选项一同使用,并且只在程序彻底调试完后才开始进行代码优化。这样调试过程将变得相对轻松和愉快。      基本应用      现在可以启动GDB来调试已经生成的可执行程序debugme,命令如下:      # gdb debugme   GNU gdb Red Hat Linux (5.3post-0.20021129.18rh)   ……   (gdb)      如果一切正常,GDB将被启动并在屏幕上输出版权信息,但如果使用了-q或--quiet选项则不会显示它们。启动GDB时另外一个有用的命令行选项是“-d dirname”,其中dirname是一个目录名。该目录名告诉GDB应该到哪里去寻找源代码。      一旦出现GDB的命令提示符(gdb),就表明GDB已经准备好接收来自用户的各种调试命令了。如果想在调试环境下运行这个程序,可以使用GDB提供的“run”命令,而程序在正常运行时所需的各种参数可以作为“run”命令的参数传入,或者使用单独的“set args”命令进行设置。如果在执行“run”命令时没有给出任何参数,GDB将使用上一次“run”或“set args”命令指定的参数。如果想取消上次设置的参数,可以执行不带任何参数的“set args”命令。下面尝试在调试器中运行这个程序:      (gdb) run   ……   Program received signal SIGSEGV, Segmentation fault.   0x4000c6ac in _dl_fini () from /lib/ld-linux.so.2      最后一行输出表明程序在调用动态链接库/lib/ld-linux.so.2中的_dl_fini()函数时出现了错误,地址是0x4000c6ac。这些对调试是非常重要的线索。另外还有一种信息对调试也很重要,就是错误发生时的函数调用层级关系,可以通过执行“backtrace”命令来获得。在使用GDB调试命令时,用户可以不必输入完整的命令名称,使用任何惟一的缩写都可以。例如“backtrace”命令就可以缩写成“back”甚至“bt”。GDB还支持很多常用的Shell命令编辑特征,比如可以像在bash或tcsh中那样按Tab键补齐命令。如果相关命令不惟一的话,则列出所有可能的匹配项。此外键盘上的方向键可用来翻动历史命令。      GDB是一个源代码级的调试器,使用“list”命令可以查看当前调试对象的源代码。该命令的通用格式为“list ”,表示显示从m行开始到n行结束的代码段,而不带任何参数的“list”命令将显示最近10行源代码。      设置断点   在调试有问题的代码时,在某一点停止运行往往很管用。这样程序运行到此外时会暂时挂起,等待用户的进一步输入。GDB允许在几种不同的代码结构上设置断点,包括行号和函数名等,并且还允许设置条件断点,让程序只有在满足一定的条件时才停止执行。要根据行号设置断点,可以使用“ break linenum”命令。要根据函数名设置断点,则应该使用“break funcname”命令。      在以上两种情况中,GDB将在执行指定的行号或进入指定的函数之前停止执行程序。此时可以使用“print”显示变量的值,或者使用“list”查看将要执行的代码。对于由多个源文件组成的项目,如果想在执行到非当前源文件的某行或某个函数时停止执行,可以使用如下形式的命令:      # break filename:linenum   # break filename:funcname      条件断点允许当一定条件满足时暂时停止程序的执行。它对于调试来讲非常有用。设置条件断点的正确语法如下:      break linenum if expr   break funcname if expr      其中expr是一个逻辑表达式。当该表达式的值为真时,程序将在该断点处暂时挂起。例如,下面的命令将在debugme程序的第38行设置一个条件断点。当程序运行到该行时,如果count的值等于3,就将暂时停止执行:   (gdb) break 38 if count==3      设置断点是调试程序时最常用到的一种手段。它可以中断程序的运行,给程序员一个单步跟踪的机会。使用命令“ break main”在main函数上设置断点可以在程序启动时就开始进行跟踪。      接下去使用“continue”命令继续执行程序,直到遇到下一个断点。如果在调试时设置了很多断点,可以随时使用“info breakpoints”命令来查看设置的断点。此外,开发人员还可以使用“delete”命令删除断点,或者使用“disable”命令来使设置的断点暂时无效。被设置为无效的断点在需要的时候可以用“enable”命令使其重新生效。      观察变量   GDB最有用的特性之一是能够显示被调试程序中几乎任何表达式、变量或数组的类型和值,并且能够用编写程序所用的语言打印出任何合法表达式的值。查看数据最简单的办法是使用“print”命令,只需在“print”命令后面加上变量表达式,就可以打印出此变量表达式的当前值,示例如下:      (gdb) print str   $1 = 0x40015360 "Happy new year!/n"      从输出信息中可以看出,输入字符串被正确地存储在了字符指针str所指向的内存缓冲区中。除了给出变量表达式的值外,“print”命令的输出信息中还包含变量标号($1)和对应的内存地址(0x40015360)。变量标号保存着被检查数值的历史记录,如果此后还想访问这些值,就可以直接使用别名而不用重新输入变量表达式。      如果想知道变量的类型,可以使用“whatis”命令,示例如下:      (gdb) whatis str   type = char *      对于第一次调试别人的代码,或者面对的是一个异常复杂的系统时,“whatis”命令的作用不容忽视。      单步执行   为了单步跟踪代码,可以使用单步跟踪命令“step”,它每次执行源代码中的一行。      在GDB中可以使用许多方法来简化操作,除了可以将“step”命令简化为“s”之外,还可以直接输入回车键来重复执行前面一条命令。      除了可以用“step”命令来单步运行程序之外,GDB还提供了另外一条单步调试命令“next”。两者功能非常相似,差别在于如果将要被执行的代码行中包含函数调用,使用step命令将跟踪进入函数体内,而使用next命令则不进入函数体内。      在进入下一部分之前,使用下面的命令退出GDB:   (gdb) quit      分析核心(core)文件      在程序发生崩溃时,有时可能无法直接运行GDB来进行调试。比如程序可能是在另外一台机器上运行的,或者因为程序对时间比较敏感,所以手动跟踪调试会产生无法接受的延迟等。遇到这些情况,就只能等到程序运行结束后才能判断崩溃的原因了。这时需要用到Linux提供的core dump机制。当程序中出现内存操作错误时,会发生崩溃并产生核心文件。使用GDB可以对产生的核心文件进行分析,找出程序是在什么时候崩溃的和在崩溃之前程序都做了些什么。当然,如果要用GDB来分析核心文件,也必须在编译时加上-g选项来产生调试符号表。      在分析核心文件之前必须确认系统是否允许生成核心文件,很多Linux发行版在默认时禁止生成核心文件。为了生成核心文件,首先必须执行下面的命令:   # ulimit -c unlimited      然后就可以生成核心文件了。这里仍以前面的debugme程序为例,再次执行下面命令将产生核心文件:      # ./debugme   Enter a string to count words:Happy new year!   The number of words is 3.   Segmentation fault (core dumped)      生成的核心文件名根据系统配置的不同会有所差异。要在GDB中分析核心文件,除了要给出核心文件的文件名外,还必须给出生成该核心文件的可执行程序的名称,示例如下:      #gdb debugme core.547   ……   Program terminated with signal 11, Segmentation fault.   Reading symbols from /lib/libc.so.6...done.   ……      从GDB的输出信息中可以看出,产生这个核心文件的原因是因为程序收到了序号为11的信号。如果想知道程序在崩溃之前运行到了哪里,可以使用“backtrace”或“info stack”命令查看一下堆栈的历史记录。示例如下:      (gdb) info stack   #0 0x4000c6ac in _dl_fini () from /lib/ld-linux.so.2   #1 0x40057940 in exit () from /lib/libc.so.6   #2 0x4004291f in _libc_start_main () from /lib/libc.so.6      由上可知,程序崩溃时正处于_dl_fini()函数之中。但很多时候程序员感兴趣的可能并不是这个,而是exit()或_libc_start_main()函数,因为它们才可能是问题真正的症结所在。GDB提供的“frame”命令可以用来在不同的调用上下文中切换。例如下面的命令可以查看exit()函数在执行时的状况:      (gdb) frame 1   #1 0x40057940 in exit () from /lib/libc.so.6      此外还可以用“up”或“down”命令在不同的函数调用上下文中切换。开发人员使用这三条命令可以很轻松地实现调用栈的遍历。在分析核心文件时,通过将遍历栈的命令和检查变量值的“print”命令结合起来,就能够复原程序运行时的全部景象。      调试其它进程      有时会遇到一种很特殊的调试需求,对当前正在运行的其它进程进行调试。这种情况有可能发生在那些无法直接在调试器中运行的进程身上,例如有的进程只能在系统启动时运行。另外如果需要对进程产生的子进程进行调试的话,也只能采用这种方式。GDB可以对正在执行的程序进行调度,它允许开发人员中断程序并查看其状态,之后还能让这个程序正常地继续执行。      GDB提供了两种方式来调试正在运行的进程:一种是在GDB命令行上指定进程的PID,另一种是在GDB中使用“attach”命令。例如,开发人员可以先启动debugme程序,让其开始等待用户的输入。示例如下:      #./debugme   Enter a string to count words:      接下去在另一个虚拟控制台中用下面的命令查出该进程对应的进程号:      # ps -ax | grep debugme   555 pts/1 S 0:00 ./debugme      得到进程的PID后,就可以使用GDB对其进行调试了:      # gdb debugme 555   GNU gd b Red Hat Linux (5.3post-0.20021129.18rh)   Attaching to program: /home/xiaowp/debugme, process 555   Reading symbols from /lib/libc.so.6...done.   ……      在上面的输出信息中,以Attaching to program开始的行表明GDB已经成功地附加在PID为555的进程上了。另外一种连接到其它进程的方法是先用file命令加载调试时所需的符号表,然后再通过“attaché”命令进行连接:      (gdb) file /home/xiaowp/debugme   Reading symbols from /home/xiaowp/debugme...done.   (gdb) attach 555   ……      如果想知道程序现在运行到了哪里,同样可以使用“backtrace”命令。当然也可以使用“step”命令对程序进行单步调试。      在完成调试之后,不要忘记用detach命令断开连接,让被调试的进程可以继续正常运行:      GDB是Linux下一个最基本的调试器,其功能非常丰富。完整地介绍GDB的功能可能需要几百页,本文只涵盖了GDB的一些最常见的用法。作为一个合格的Linux程序员,花在GDB上的功夫和时间越多,从调试中获得的益处就越多。 本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/bing_bing/archive/2010/11/16/6013697.aspx
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值