引言
一个调试器(精确地称为symbolic debugger),是一个运行你的程序的应用。它可以单步调试源码、一行一行地执行你想要执行的每一行代码。你甚至可以单步调试你的机器指令。在任何时候,你可以在运行时检查甚至修改任何变量的值。如果你的程序崩溃,一个符号调试器会告诉你程序在哪以及为什么崩溃。你可以执行程序并看到哪些源码行以哪种顺序被执行。
调试器也可以解决无限循环的问题。使用它可以单步调试这个循环并看到你的条件为什么不像你所期望的那样工作。它还可以解决一个变量访问时导致的程序崩溃。它可以告诉你关于你尝试访问的变量之间被赋的值(或可能从未被赋过值)。如果你代码里有一行没有执行,使用调试器可以看到什么被执行了,以何种顺序,以及为什么一个特定行不能到达。除了编译器,调试器是一个程序员可以使用的最有用的工具。
不要使用printf来调试代码。当调试的点很多时,放入printf再取出让事情变得复杂。而且调试器可以做到很多printf不能做到的事:在运行时改变变量、临时中断程序、列出源码、打印你不认识的变量或结构体的数据类型、跳到代码里的任意一行、等等等等。调试器还能附加到一个正在运行的进程上,且不用杀死它个进程。它还可以在一个已经崩溃并死掉的程序上使用,而不必重新运行这个程序,并看到程序死的时候程序的状态和所有变量的值。
同样,GDB的知识会增加你对程序、进程、内存和你选择的语言的了解。虽然偶尔printf可能更有用,但绝大多数情况下调试器会更快更容易。使用调试器总是更优雅,如果你不关心优雅,你应该退出Linux编程并开始使用VC。
调试器有许多种,GDB是其中一个。GDB是GNU操作系统的一部分。它的原作者是Richard M. Stallman。它可以用来调试C、C++、Objectiv-C、Fortran、Java、和汇编程序。对Modula-2和Pascal也有部分支持。它可以运行在任何支持Unix的架构上,所以在你的PC上学习GDB会给你在任何Unix可以运行的地方调试代码的能力!
过去,dbx是Unix系统上的权威调试器。随着GNU的出现成为衡量所有Unix系统的标准,GDB变成了调试世界的权威调试器。结果,甚至商业调试器都倾向于与GDB命令行兼容(甚至是概念兼容),所以学习GDB可以使你能够使用大量的其它调试器。简而言之,如果你学习GDB,你几乎可以在Unix世界是使用任何调试器来调试任何东西。
GDB的主页是www.gnu.org/software/gdb/gdb.html。当前最新的版本(2012年1月24日发行的)是7.4。GDB有很多前端,比如DDD(Data Display Debugger)等。
内存布局和栈
在有效学习如何使用GDB前,你必须理解框架(frame)。因为它们是组成栈(stack)的框架,所以也被称为调用栈框(call stack frames)。要学习栈,我们需要知道一个执行的程序的内存布局。
每当一个进程被创建时,内核提供一块可以放置在任何地方的内存。然而,通过虚拟内存 (virtual memory,VM)的魔力,进程相信它拥有计算机上的所有内存。你可以已经听说过当RAM用完时使用硬盘空间作为内存的情况下的“虚拟内存”。那也被称为虚拟内存,但是和我们这里说的完全没有关系。
VM由以下原则组成:
1、每个进程被给定被称为进程虚拟内存空间的物理内存。
2、进程不知道它的物理内存的细节(也说是它物理上的地址)。进程所知道的只是内存块有多大,以及它的内存块从地址0开始。
3、每个进程都不知道任何属于进程的VM块。
4、即使进程知道了VM的其它块,它在物理上被阻止访问那块内存。
每次一个进程想要读要者写内存时,它的请求必须从一个VM地址翻译到一个物理内存地址。相反地,当内核需要访问一个进程的VM,它必须把一个物理内存地址翻译成一个虚拟地址。这样做有两个主要问题:
1、计算机持续地访问内存,所以翻译非常普遍。它们必须是及其迅速的。
2、OS如何保证一个进程没有践踏另一个进程的VM?
这两个问题的答案在于OS没有自己管理VM;它从CPU得到了帮助。许多CPU包含一个被称为MMU的设备:内存管理单元(memory management unit)。MMU和OS共同负责管理VM,在虚拟和物理内存之间进行翻译;决定哪些进程被允许访问哪些内存地址,以及控制一个VM空间上区域的读写权限,即使是对于拥有这个空间的进程。
过去Linux通常只能被移植到有一个MMU的架构上,所以说Linux不能在x286上运行。然而,在1998年,Linux被移植到没有MMU的68000上。这也造就了嵌入式Linux和如Palm Pilot(PDA)之类的设备上的Linux。
MMU有时也称为paged memory management unit(PMMU),一个计算机硬件组件。它处理CPU所请求的对内存的访问。它的功能包括虚拟内存到物理内存的翻译,内存保护,缓存控制,总线仲裁和简易计算机架构(特别是8位系统)的bank switching。
MMU把虚拟地址空间(处理器可寻址的范围)分成页,地址的底部n个位(一个页里的偏移量)被保持不变。更多的地址位是虚拟页号。MMU通常通过一个相关的页表缓存(Translation Lookaside Buffer, TLB)来把物理页号翻译成物理页号。当TLB没有缓存翻译时,一个涉及硬件数据结构或软件协助的更慢的机制被使用。在这样的数据结构里找到的数据通常被称为页表项(page table entries, PTE),而数据结构本身被称为页表。物理页号和页偏移量结合,来给出完整的物理地址。
PTE或TLB项也可能包含页是否被写(dirty bit)的信息,它上次何时被使用(accessed bit,用于最近使用内存替换算法),何种类型的进程(用户模式、超级用户模式)可以读写它,以及它是否应该被缓存。
有时,一个TLB项或PTE禁止对一个虚拟页的访问,可能因为还没有为那个虚拟页分配RAM。在这种情况下MMU发送一个页面失效的信号给CPU。OS然后处理这种情况,可能在RAM找到一个空间的框架并建立一个新的PTE来把它映射到所请求的虚拟地址上。如果没有RAM可用,那么它可能必须选择一个已有的页(称为受害者),使用某种替换算法,把它存储到磁盘(称为换页)。对于一些MMU,PTE或TLB项可能有限,在这种情况下OS必须释放一个来映射新的。
在一些情况下,一个页面失效可能指明一个软件bug。MMU的一个关键好处是内存保护:OS可以使用它来防护出格的程序,通过禁一个特定程序访问其没有访问权限的内存。通常,一个OS为每个程序分配它自己的虚拟地址空间。
MMU也减少了内存碎片的问题。在各内存块被分配和释放后,释放的内存可能变为碎片(不连续的)以致最大的连续空闲内存比总内存量小得多。使用虚拟内存,一个连续的虚拟内存地址范围可以被映射成多个不连续的物理内存块。
内存布局
每个进程的VM空间以一个相似的可预测的方式排列:
High Address | Args and env vars | <-- Command line arguments and environment variables |
Stack | V | ||
Unused memory | ||
^ | Heap | ||
Uninitialized Data Segment (bss) | <-- Initialized to zero by exec . | |
Initialized Data Segment | <-- Read from the program file by exec . | |
Low Address | Text Segment | <-- Read from the program file by exec . |
写一个最简单的程序:int main(void) { },再用gcc -c和-o命令分别编译成object文件和可执行文件。用size可以查看它们的(在硬盘上的)每个区域的尺寸:
$ size foo.o foo
text data bss dec hex filename
61 0 0 61 3d foo.o
1056 252 8 1316 524 foo
我们也可以通过objdump -h或objdump -x命令可以得到object文件各区域的尺寸:
$ objdump -h foo.o
foo.o: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000005 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 0000003c 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 0000003c 2**2
ALLOC
3 .comment 0000002b 00000000 00000000 0000003c 2**0
CONTENTS, READONLY
4 .note.GNU-stack 00000000 00000000 00000000 00000067 2**0
CONTENTS, READONLY
5 .eh_frame 00000038 00000000 00000000 00000068 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
size命令没有列出栈和堆,因为它们没有存储在文件里。此外,foo程序没有全局数据,但foo.o的数据段和bss段的尺寸为0,而可执行程序的却不为0,这是由于linker的参与造成的。还有,size命令和objdump产生的text段的结果不同,这是为什么呢?首先objdump是以十六进制显示的,当然这不会造成这里的差别。如果用size -A -x命令查看的话,就一目了然了:
size -A -x foo.o
foo.o :
section size addr
.text 0x5 0x0
.data 0x0 0x0
.bss 0x0 0x0
.comment 0x2b 0x0
.note.GNU-stack 0x0 0x0
.eh_frame 0x38 0x0
Total 0x68
其实差别的部分在于.eh_frame的段。size里的text段包含了该段。该段也有时也被称为.rodata,用来存储常量数据。
栈框架和栈
内存布局里的一个区域被称为栈,它是栈框架的集合。每个栈框架表示一个函数调用。随着函数被调用,栈框架的数量会增加,栈也是增长。相反,当函数从它们的调用者那里返回,栈框架的数量减少,栈也会缩减。
一个程序由一个或多个通过调用对方来交互的函数组成。每当一个函数被调用时,内存的一块区域为这个新的函数调用分配好,被称为栈框架。这块区域拥有一些重要信息,比如;
1、新的被调用函数的所有自动变量的存储空间。
2、被调用函数返回值要返回到的调用者的行号。
3、被调用函数的参数。
每个函数都有它自己的栈。总的来看,所有栈框架组成了调用栈(call stack)。
调试之前,应该准备好可以调试的可执行程序。简单来说,我们需要在程序里加入信息。
一个符号(symbol)是一个变量或一个函数。符号表(Symbol Tables)就是在一个可执行程序里的变量和函数的一张表。通常,符号表只包含符号的内存地址,因为计算计不使用(或不关心)我们给变量和函数的命名。
但是为了让GDB对我们有用,它需要能够引用变量名和函数名,而不是它们的地址。人类使用main()或i这样的名字。计算机使用0x804b64d或0xbffff7784这样的地址。为了这个目的,我们可以用“调试信息”编译代码,来告诉GDB两件事:
1、如何把符号的地址和源码的名字关联起来;
2、如何把一个机器代码的地址和一行源码关联起来。
拥有这些额外调试信息的符号表被称为一个参数化的或增强的符号表。因为GCC和GDB在如此多的不同平台上运行,调试信息有许多不同的格式:
stabs:多数BSD系统上的DBX使用的格式。
coff:在System V Release 4之前的多数System V系统上的SDB使用的格式。
xcoff:IBM RS/6000系统上的DBX使用的格式。
dwarf:多种SVR4上的SDB使用的格式。
dwarf2:IRIX6上的DBX使用的格式。
vms:VMS系统上的DEBUG使用的格式。
除了调用格式外,GDB了解允许它使用GNU扩展的这些格式的增强变体。使用不是GDB的其它东西来调试一个带有GNU增强调试格式的可执行程序,会导致调试器崩溃。
不要被所有这些格式吓到,GDB会自动为你挑选最好的格式。极少情况下你才需要一个不同的格式,可能千分之一的概率。
使用gcc的-g选项可以为一个可执行程序产生一个增强的符号表。
正如之前讨论的,有许多调试格式。-g的确切含义是为你的系统以本地格式产生调试信息。
作为-g的替代,你还可以使用gcc的-ggdb选项。它以最昂贵的格式来产生调试信息,包括前面讨论的GNU增强变体。这很可能是你在多数情况下想要使用的选项。
你也可以给-g、-ggdb和所有其它调试格式选项一个数值参数。1表示最少量的信息而3表示最多量。没有一个数值参数时,调试等级默认为2。通过使用-g3你甚至可以访问预处理的宏,这非常棒。我们最好总是使用-ggdb3来产生增强符号表。
被编译进可执行程序的调试信息不会被读入内存,除非GDB载入这个可执行程序。这表示含有调试信息的可执行程序不会比不带调试信息的可执行程序运行得更慢(一个普遍误解)。尽管可调试的可执行程序占据更多的磁盘空间,可执行程序不会有更大的“内存使用量”,除非从GDB载入。相似的,除非你有GDB来运行调试可执行程序,否则它的载入时间也是近乎相同。
最后一点,我们可以在带有一个参数化的符号表的可执行程序上执行编译器优化。换句话说:gcc -g -O9 try1.c。事实上,GDB是少有的通常能很好调试被优化的可执行程序的符号调试器。然而,你通常应该在调试一个可执行程序前关闭优化,因为会有使GDB困惑的情况。变量可能因为优化而不存在,函数可能被内联,更多的可能或可能不困惑gdb的事情都会发生。为了安全起见,在调试一个程序时关闭优化。
给出下面的C代码try1.c:
- #include <stdio.h>
- static void display(int i, int *ptr);
- int main(void) {
- int x = 5;
- int *xptr = &x;
- printf("In main():\n");
- printf(" x is %d and is stored at %p.\n", x, &x);
- printf(" xptr points to %p which holds %d.\n", xptr, *xptr);
- display(x, xptr);
- return 0;
- }
- void display(int z, int *zptr) {
- printf("In display():\n");
- printf(" z is %d and is stored at %p.\n", z, &z);
- printf(" zptr points to %p which holds %d.\n", zptr, *zptr);
- }
用命令gcc -ggdb3 -O0 -o try1 try1.c来编译。先查看try1的size:
$ ls -l try1
-rwxrwxr-x 1 tommy tommy 24752 2012-03-28 10:55 try1
size -A try1
try1 :
section size addr
.interp 19 134512980
.note.ABI-tag 32 134513000
.note.gnu.build-id 36 134513032
.gnu.hash 32 134513068
.dynsym 96 134513100
.dynstr 81 134513196
.gnu.version 12 134513278
.gnu.version_r 32 134513292
.rel.dyn 8 134513324
.rel.plt 32 134513332
.init 46 134513364
.plt 80 134513424
.text 556 134513504
.fini 26 134514060
.rodata 182 134514088
.eh_frame_hdr 60 134514272
.eh_frame 228 134514332
.ctors 8 134520596
.dtors 8 134520604
.jcr 4 134520612
.dynamic 200 134520616
.got 4 134520816
.got.plt 28 134520820
.data 8 134520848
.bss 8 134520856
.comment 42 0
.debug_aranges 32 0
.debug_info 228 0
.debug_abbrev 157 0
.debug_line 429 0
.debug_str 138 0
.debug_loc 112 0
.debug_macinfo 15956 0
Total 18920
使用命令strip --only-keep-debug try1来移除除debug符号之外的符号。再看下它的size:
$ ls -l try1
-rwxrwxr-x 1 tommy tommy 21060 2012-03-28 11:25 try1
$ size -A try1
try1 :
section size addr
.interp 19 134512980
.note.ABI-tag 32 134513000
.note.gnu.build-id 36 134513032
.gnu.hash 32 134513068
.dynsym 96 134513100
.dynstr 81 134513196
.gnu.version 12 134513278
.gnu.version_r 32 134513292
.rel.dyn 8 134513324
.rel.plt 32 134513332
.init 46 134513364
.plt 80 134513424
.text 556 134513504
.fini 26 134514060
.rodata 182 134514088
.eh_frame_hdr 60 134514272
.eh_frame 228 134514332
.ctors 8 134520596
.dtors 8 134520604
.jcr 4 134520612
.dynamic 200 134520616
.got 4 134520816
.got.plt 28 134520820
.data 8 134520848
.bss 8 134520856
.comment 42 0
.debug_aranges 32 0
.debug_info 228 0
.debug_abbrev 157 0
.debug_line 429 0
.debug_str 138 0
.debug_loc 112 0
.debug_macinfo 15956 0
Total 18920
如果用命令strip --strip-debug try1来移除调试符号,它的size为:
$ ls -l try1
-rwxrwxr-x 1 tommy tommy 7154 2012-03-28 11:25 try1
看下此时的段信息:
$ size -A try1
try1 :
section size addr
.interp 19 134512980
.note.ABI-tag 32 134513000
.note.gnu.build-id 36 134513032
.gnu.hash 32 134513068
.dynsym 96 134513100
.dynstr 81 134513196
.gnu.version 12 134513278
.gnu.version_r 32 134513292
.rel.dyn 8 134513324
.rel.plt 32 134513332
.init 46 134513364
.plt 80 134513424
.text 556 134513504
.fini 26 134514060
.rodata 182 134514088
.eh_frame_hdr 60 134514272
.eh_frame 228 134514332
.ctors 8 134520596
.dtors 8 134520604
.jcr 4 134520612
.dynamic 200 134520616
.got 4 134520816
.got.plt 28 134520820
.data 8 134520848
.bss 8 134520856
.comment 42 0
Total 1868
如果用命令strip --strip-all try1来移除所有符号,它的size为:
$ ll try1
-rwxrwxr-x 1 tommy tommy 5520 2012-03-28 11:38 try1*
它的段信息:
try1 :
section size addr
.interp 19 134512980
.note.ABI-tag 32 134513000
.note.gnu.build-id 36 134513032
.gnu.hash 32 134513068
.dynsym 96 134513100
.dynstr 81 134513196
.gnu.version 12 134513278
.gnu.version_r 32 134513292
.rel.dyn 8 134513324
.rel.plt 32 134513332
.init 46 134513364
.plt 80 134513424
.text 556 134513504
.fini 26 134514060
.rodata 182 134514088
.eh_frame_hdr 60 134514272
.eh_frame 228 134514332
.ctors 8 134520596
.dtors 8 134520604
.jcr 4 134520612
.dynamic 200 134520616
.got 4 134520816
.got.plt 28 134520820
.data 8 134520848
.bss 8 134520856
.comment 42 0
Total 1868
在移除了上面的部分后,程序仍可以正常运行。但是执行strip --remove-section=.text try1的话,程序无法运行,报出错误“段错误”。看看它此时的信息:
$ ll try1
-rwxrwxr-x 1 tommy tommy 5472 2012-03-28 11:42 try1*
$ size -A try1
try1 :
section size addr
.interp 19 134512980
.note.ABI-tag 32 134513000
.note.gnu.build-id 36 134513032
.gnu.hash 32 134513068
.dynsym 96 134513100
.dynstr 81 134513196
.gnu.version 12 134513278
.gnu.version_r 32 134513292
.rel.dyn 8 134513324
.rel.plt 32 134513332
.init 46 134513364
.plt 80 134513424
.fini 26 134514060
.rodata 182 134514088
.eh_frame_hdr 60 134514272
.eh_frame 228 134514332
.ctors 8 134520596
.dtors 8 134520604
.jcr 4 134520612
.dynamic 200 134520616
.got 4 134520816
.got.plt 28 134520820
.data 8 134520848
.bss 8 134520856
.comment 42 0
Total 1312
可以看到移除.text段会连同debug信息一同移除。
COFF(Common Object File Format)是在Unix上使用的可执行文件、object文件和共享库文件的格式规范。它规定了:
符号调试信息。它由程序里的函数和变量的符号(字符串)名以及用来设置断点和跟踪执行的行号信息组成。
符号名被存储在COFF符号表里。每个符号表项包含一个名字、存储类别、类型、值和段号。不超过8字符的短名字被直接存储在符号表里,更长的名字作为COFF对象末的字符串表里的一个偏移量被存储。
存储类别(storage class)描述符号表示的类型实体,并可能包含外部变量(C_EXT)、自动(栈)变量(C_AUTO)、注册器变量(C_REG)、函数(C_FCN)和许多其它的。符号类型(symbol type)描述了符号实体值的解释,并包含了所以C数据类型的值。
当用恰当的选项编译时,一个COFF object文件将为object文件里的代码段里的每个可能的断点包含行号信息。行号信息有两种形式:
第一种,对于代码里第个可能的断点,行号表项记录了地址和它匹配的行号。
第二种,表项标识一个函数开始的符号表项,使一个断点可以使用函数名来设置。
相对虚拟地址。当一个COFF文件被产生时,它通常不知道它会被载入到内存的哪个地方。文件第一个字节将会载入到的虚拟地址(virtual address)被称为映射基地址(image base address)。文件剩余部分不必被载入到相邻的块里,而是在不同的区域里。
不要把相对虚拟地址(Relative Virtual Address)和标准虚拟地址混淆。一个相对虚拟地址是这个文件被载入到内存的虚拟地址,减去文件映射的基地址。如果文件被逐字地从磁盘映射到内存里,那么RVA会和文件里的偏移量相同,但这很不常见。
注意RVA术语只用在映射文件里的object上。一旦载入到内存,映射基地址被增加,且原始的VA被使用。
用GDB检查栈
载入一个程序并设置一个断点
把之前的try1.c编译成可执行文件,并用gdb来载入:
$ gdb try1
GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08
Copyright (C) 2011 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>...
Reading symbols from /home/tommy/tmp/try1...done.
(gdb)
(gdb)是GDB的输入提示。用run命令运行程序:
(gdb) run
Starting program: /home/tommy/tmp/try1
In main():
x is 5 and is stored at 0xbffff2a8.
xptr points to 0xbffff2a8 which holds 5.
In display():
z is 5 and is stored at 0xbffff290.
zptr points to 0xbffff2a8 which holds 5.
[Inferior 1 (process 18365) exited normally]
我们可以用“break 行号”的方式来设置断点。比如break 5表示在第5行暂停,这时第4行已经执行,而第5行还没有。
(gdb) break 10
Breakpoint 1 at 0x8048475: file try1.c, line 10.
(gdb) run
Starting program: /home/tommy/tmp/try1
In main():
x is 5 and is stored at 0xbffff2a8.
xptr points to 0xbffff2a8 which holds 5.
Breakpoint 1, main () at try1.c:10
10 display(x, xptr);
backtrace命令简单地列出当前在栈上的框架。
(gdb) backtrace
#0 main () at try1.c:10
接着可以用step命令执行下一行代码:
(gdb) step
display (z=5, zptr=0xbffff2a8) at try1.c:15
15 printf("In display():\n");
再用backtrace看下栈上的框架:
(gdb) backtrace
#0 display (z=5, zptr=0xbffff2a8) at try1.c:15
#1 0x08048489 in main () at try1.c:10
一些要注意的地方:
1、我们现在有两个框架。框架1属于main()而框架0属于display()。
2、每个框架列项给出函数的参数。我们看到main没有参数,而display有,而且显示出了参数的值。
3、每个框架列项给出了框架里正被执行的行号。
4、框架的编号系统可能有点令人疑惑。main是1而display是0。这是为了个栈往下增长的概念一致。
不带参数的frame命令可以让GDB告诉我们现在在哪个框架里:
(gdb) frame
#0 display (z=5, zptr=0xbffff2a8) at try1.c:17
17 printf(" zptr points to %p which holds %d.\n", zptr, *zptr);
当前的frame是0,我们可以访问在frame 0里的所有局部变量。相反,我们不能访问其它框架的自动变量。
我们可以用print打印display函数里的z和zptr变量:
(gdb) print z
$3 = 5
(gdb) print zptr
$4 = (int *) 0xbffff2a8
但是我们不访问main里的变量:
(gdb) print x
No symbol "x" in current context.
(gdb) print xptr
No symbol "xptr" in current context.
我们可以使用frame命令来改变当前栈:
(gdb) frame 1
#1 0x08048489 in main () at try1.c:10
10 display(x, xptr);
(gdb) print x
$5 = 5
(gdb) print xptr
$6 = (int *) 0xbffff2a8
(gdb) print z
No symbol "z" in current context.
(gdb) print zptr
No symbol "zptr" in current context.
顺便一提,程序的输出会和GDB的输出混在一起,容易造成混淆。需要花些时间来适应。
quit命令退出GDB。
查看源码
list命令可以查看源码。启动GDB后第一个list命令会定位到main函数,以main函数为中心显示上下共10行代码。
下一个list命令查看下面的10行。可以重复执行这个命令,直到到达文件尾。
list -查看前10行代码。
list n命令显示以第n行为中心的上下共10行代码。
list n,命令显示以第n行开头的10行代码。
list ,n显示以第n行结尾的10行代码。
list m,n显示以第m行开头,第n行结尾的代码。
list f以函数f为中心的上下10行代码。f可以是其它文件里定义的函数。
list file:line显示文件file里的第line行代码。
list file:f显示文件file里的函数f的代码。
函数名、文件名+行号/函数名也都适用于list f1,f2的格式。
通过内存地址查看源码。
用print命令打印一个函数的内存地址:
(gdb) print *main
$1 = {int (int, char **)} 0x80486ff <main>
注意:这里的地址不是调用栈的地址。
通过这个地址可以找到源码:list *0x80486ff可以列出main函数的上下10行代码。
list *0x8048711(main函数地址之后的一些偏移)可以列出main函数里的某行代码。
set listsize n可以设置每次显示的行数。
.gdbinit文件
GDB启动时会载入.gdbinit文件。这个文件里包含比如“set listsize”之类的命令,GDB启动时会执行它们。GDB首先会在主目录下找,找不到则会在启动GDB的当前目录里找。GDB每次启动时会打印copyright信息。使用gdb -q启动GDB可以禁这个信息。使用shell的别名机制,alias gdbq="gdb -q",每次启动gdbq就可以启动不带copyright信息的GDB。
在GDB里使用set prompt命令可以设置GDB的提示,比如“set prompt gdb>”,这样提示就不再是默认的(gdb)了。可以把这个设置放入.gdbinit文件里。同样可以使用逃脱字符\033
(在终端里是"/e["和"m")来设置颜色,比如“set prompt \033[01;34mgdb> ”
启动被调试程序:
run命令启动程序。
run arg1 arg2 ...可以给程序传入参数。
set args设置下一次run命令执行时传入程序的参数。比如
set args arg1 arg2...
run
等同于run arg1 arg2 ...
程序已经运行时(并可能中断在某个断点上),可以使用kill来杀死程序,这样可以重新运行。
也可以直接执行run,GDB会提示你是否重新启动程序。
断点
断点有三种类型:
- breakpoint在程序到达某个特定的点时中断。
- watchpoint在一个变量或表达式的值发生改变时中断。
- catchpoint当一个特定的事件发生时中断。
对于breakpoint,有两种原因导致GDB没有停在设置中断点的位置。一、由于编译器的优化,代码在程序里没有对应的机器指令;二、不是所有的代码都会编译成机器指令,比如变量声明。通常这种情况下,程序会中断在离中断点最近的下方的有对应机器指令的源码行。
你设置的每个breakpoint、watchpoint和catchpoint都被赋予一个从1开始的编号。你使用这个编号来标识断点。
info breakpoints可以列出当前所有设置的断点。 也可以简写为i b。GDB里的命令都可以用首字母或前几个字母来简写。
info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804865d in main at test_insert_sort.c:16
breakpoint already hit 1 time
2 breakpoint keep y 0x00131464 <insert_sort_array+6>
3 hw watchpoint keep y n
可以用disable n命令来禁掉编号为n的断点,用enable n命令重新启用断点。
gdb> disable 2
gdb> i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804865d in main at test_insert_sort.c:16
breakpoint already hit 1 time
2 breakpoint keep n 0x00131464 <insert_sort_array+6>
3 hw watchpoint keep y n
可以看到第2个断点已经被禁用了。
break n在当前文件的第n行设置断点,比如break 9。
break function在函数上设置断点。
break +n在当前行(中断时所在的行)的后n行加上断点。
break -n在当前行的前n行上加上断点。
break file:function在文件里的函数上加断点。
break *address在地址上加断点。
不带参数的break在当前行加上断点。
break可以选择性地中断,使用break n if ...的形式。比如 b 13 if i > 5,
5 breakpoint keep y 0x080486d2 in main at test_insert_sort.c:27
stop only if i>5
continue恢复中断程序的执行。
tbreak可以设置一个临时的断点。它一旦被击中就会被删除,下次就不会再断在这个点上。
4 breakpoint del y 0x08048662 in main at test_insert_sort
ignore n count可以忽略第n个断点后的count个击中。
2 breakpoint keep n 0x00131464 <insert_sort_array+6>
ignore next 1 hits
clear可以删除断点,是break的逆过程。
clear *0x80483f4删除地址上的断点。
clear 4删除第4行的断点。
clear main删除main函数上的断点。
不带参数的clear会删除当前行的断点(中断的时候所在的行)。
delete也可以删除断点,但是它的参数是断点的编号,比如delete 5删除第5个断点。
不带编号的delete删除所有的断点。
finish可以运行当前函数,直到它返回。
观察和改变变量
ptype命令可以查看变量的类型,简写为pt。
gdb> ptype n
type = int
也可以查看结构体的类型:
gdb> pt mt
type = struct mytype {
int a;
char *b;
short int c;
}
print命令查看变量的值,简写成p。
gdb> p mt.a
$2 = 30
它以最‘舒适”的方式打印变量,即根据变量的类型来打印变量的值。
gdb> p mt.b
$3 = 0x80483d0 "UWVS\350i"
也可以打印结构体。
gdb> p mt
$4 = {a = 30, b = 0x80483d0 "UWVS\350i", c = 0}
set print pretty会让输出好看些:
gdb> p mt
$5 = {
a = 30,
b = 0x80483d0 "UWVS\350i",
c = 0
}
也可以打印数组。
gdb> p ar
$2 ={19, 235, 32, 53, 11, 89, 21}
gdb> pt ar
type = int [7]
可以设置打印的格式,比如以十六进制格式打印上面的数组:
gdb> p /x ar
$6 = {0x13, 0xeb, 0x20, 0x35, 0xb, 0x59, 0x15}
可以设置的格式有:
o | octal | x | hex | d | decimal | u | unsigned decimal | |||
t | binary | f | float | a | address | c | char |
也可以打印变量的地址或指针所指的变量值。
gdb> p &mt.a
$7 = (int *) 0xbffff1dc
gdb> p *(&mt.a)
$8 = 2797556
set命令可以改变变量值。
gdb> set mt.a=16
gdb> p mt.a
$9 = 16
或者直接用print命令,设置并打印变量值:
gdb> p mt.a = 81
$10 = 81
在源码之间移动
step命令一次执行一行。如果当前行是一个函数,step会进入这个函数。
next命令也是一次执行一行。但如果当前行是一个函数,它会略过这个函数。
where命令可以知道自己当前在源码的哪一行。
gdb> where
#0 main () at ptype_struct.c:14
调试正在运行的进程
我编写了一个循环:
long i;
for (i = 0; i < 999999; i++) {
mt.a += 1;
sleep(1);
}
把它编译成a.out,并在后台执行它:./a.out &
[1] 2570
然后用命令gdb ./a.out 2570可以附加到这个进程上。被时进程会中断。
或者在GDB里输入attach 2570同样可以附加到进程。
输入bt来查看栈
(gdb) bt
#0 0x008e9416 in __kernel_vsyscall ()
#1 0x003bb900 in nanosleep () from /lib/i386-linux-gnu/libc.so.6
#2 0x003bb71f in sleep () from /lib/i386-linux-gnu/libc.so.6
#3 0x0804845d in main () at ptype_struct.c:22
可以看到程序正中断在系统调用vsyscall上。用frame 3进入main函数的栈框架并打印i的值,
(gdb) frame 3
#3 0x0804845d in main () at ptype_struct.c:22
22 sleep(1);
(gdb) p i
$1 = 153
next可以执行下一行。
(gdb) next
Single stepping until exit from function __kernel_vsyscall,
which has no line number information.
0x003bb900 in nanosleep () from /lib/i386-linux-gnu/libc.so.6
设置i的值。
(gdb) p i = 999999
$3 = 999999
detach可以分离进程。
(gdb) detach
Detaching from program: /home/tommy/tmp/a.out, process 2570
(gdb) q
[1]+ 完成 ./a.out
如果被调试的进程没有调试信息:
$ strip ./a.out
$ ./a.out &
[1] 2603
tommy:~/tmp$ gdb
gdb> attach 2603
(gdb) bt
#0 0x00b0f416 in __kernel_vsyscall ()
#1 0x00721900 in nanosleep () from /lib/i386-linux-gnu/libc.so.6
#2 0x0072171f in sleep () from /lib/i386-linux-gnu/libc.so.6
#3 0x0804845d in ?? ()
#4 0x0069e113 in __libc_start_main () from /lib/i386-linux-gnu/libc.so.6
#5 0x08048351 in ?? ()
函数名没有被打印。
注意GDB其实可以看作是ptrace系统调用的前端。ptrace专门用来观察和控制另一个进程的执行。观察别的进程可能需要恰当的权限,比如超级用户。不要去调试init进程,不然可能会很伤。我不小心把它杀掉,电脑直接黑屏。
tty命令可以把被调试程序的输出定位到另一个终端,这样可以和GDB的输出分离。
tommy:~$ tty
/dev/pts/0
tommy:~/tmp$ gdb a.out
Reading symbols from /home/tommy/tmp/a.out...(no debugging symbols found)...done.
gdb> tty /dev/pts/1
gdb> run
然而在ubuntu上不能这样做……在目标终端(/dev/pts/1)上会出现错误:warning: GDB: Failed to set controlling terminal: 不允许的操作,即使是使用超级用户权限。这可能是一个ubuntu的bug。http://ubuntuforums.org/showthread.php?t=1416313
参考: http://www3.sympatico.ca/rsquared/gdb/
gdb文档: http://sourceware.org/gdb/current/onlinedocs/gdb