调试

运行

Compiling for Debugging

使用-g选项来生成可调试的二进制文件;gcc支持-g和-O同时指定,允许调试优化过的代码,GDB有关于宏的信息,能够显示宏展开,在使用DWARF debugging format配合-g3选项时,gcc就能够编译出包含宏调试信息的程序了。

查看数据

查看数据使用print或者p命令,一般都用p,不带参数时会显示上次查看过的数据,p后面带斜线加字母是指定数据内容打印格式,在打印函数参数时,在参数名后面加@entry会打印函数调用时该变量的值,在打印数组时需要使用括号将变量转为数组或使用*addr@len的形式。
p后面支持的参数包括表达式、变量、数组、内存地址等,支持的格式包括:

  • x, d, u, o, t:分别表示16进制, 10进制,无符号10进制,八进制,二进制(这里二进制没用b表示,而是用t表示two)
  • a:表示address,作为内存地址表示,除了打印地址,还会打印出距离该地址最近的函数,和info symbol指令类似;
  • c:遇到char时将数字和ascii表示一同打印出来;
  • f:表示float,将内容视作浮点数打印;
  • s:将指向单字节数据的数组都视作字符串;
  • z:和x类似,当会显示前缀的0;
  • r:使用原始格式打印,绕过python printing的pretty-printer;

如果要检查某个内存位置,无法使用print指令,需要使用x指令:

  • x/nfu addr:检查addr处内存内容,addr为整数,会被解释为内存地址
    • n表示内存单位个数
    • f表示格式,格式部分和前面的p的格式一致,不过多出一个i表示instruction
    • u表示一个内存单位的长度,可取b ,h , w, g,分别表示byte, halfword, word, giant word,可以看到f部分和u部分表示完全不重叠,这样不论先写u部分还是f部分都可以;
    • 多次执行x时,可以省略参数来继续打印后续内存内容,也可以只指定nfu中的部分,其他的会和上次保持一致;
  • display expr:每次gdb中止时都打印expr;
  • display/fmt expr , display/fmt addr:display变种,可以使用fmt指定输出格式,fmt就是上面的print支持的输出格式,当指定了fmt时,还可以将expr部分写成addr,其效果为每次gdb中止时都打印指定内存处的内容;
  • undisplay dnums… , delete display dnums…:取消某些display自动打印项,dnums…为要取消的项的索引,可以用逗号列举出所有索引,也可以用连字符表示连续的索引;
  • disable/enable display dnums:暂时不自动打印某项,这一项并不会从display列表中删除,后续还能重新启用自动打印;
  • display:手动触发一次自动打印;
  • info display:查看当前display列表,从这里能看到每一项的索引,以及每一项是否disable的状态,当前上下文中是否能够打印(某些局部变量可能在当前上下文无效);

    查看栈

    当被调试的程序在断点上停止时,通常我们希望看到程序的调用栈。可以利用gdb提供的能力检查程序的调用栈信息。

    Stack Frames

    gdb将调用栈组织成连续的stack frame,可以称为“栈帧”,每一个栈帧的信息都包含被调用的函数参数、局部变量、栈帧内存地址。
    程序的主线程刚开始运行时只有一个栈帧,就是main函数栈帧,将这个栈帧称作initial frame或者outermost frame。和outtermost frame对应的是当前正在执行的代码对应的函数栈帧,被称作innermost frame。
    如果发生了递归调用,会有多个同名函数的栈帧,因此不能用函数名字来标记栈帧,栈帧的唯一标识是内存地址。通常由frame pointer register($fp)保存当前栈帧的地址。
    gdb使用level表示栈帧的调用层级,当前栈帧是第0级,调用当前函数的函数栈帧是第1级,依此类推。查看outermost frame的level就可以看出来当前栈帧有多少级。
    某些编译器允许设置特殊选项来让函数调用不通过栈来实现,gcc提供了-fomit-frame-pointer,此时gdb可能无法正确的将源码中的函数调用识别为栈帧。

    Backtraces

    使用backgrace指令可以打印出整个调用栈,每一行表示一个栈帧。bt为backtrace的别名。

  • 不带参数:打印整个栈;

  • n:打印innermost n frames,n为负数时表示打印outermost n frames
  • full:同时打印局部变量
  • no-filters和hide是和python扩展相关的,略过

当进程中有多个线程时,bt仅打印当前线程调用栈,要展示其他线程调用栈,使用thread apply指令(https://sourceware.org/gdb/onlinedocs/gdb/Threads.html#Threads)。
如果编译器优化级别较高,那些在调用完成后没有被用到的函数参数会在发生调用发生前被优化调,因此gdb无法显示某些外层stack frame的函数参数,这些参数被显示为
虽然C程序的入口点一般是main函数,但程序的真正入口却是和操作系统相关的,gdb默认不会显示main函数更外层的stack frame。如果我们对main外层的stack frame感兴趣,可以使用set backtrace past-main on/off来修改这个行为。
使用set和show指令能够更改和查看backtrace的行为:

  • set backtrace past-main on/off, show backtrace past-main:是否显示main外层栈;
  • set backtrace past-entry on/off, show backtrace past-entry: 是否显示internal entry point外层栈;
  • set backtrace limit 0/n/unlimited, show backtrace limit:显示多少层栈,0和unlimited表示无限;
  • set filename-display relative/basename/absolute, show filename-display:如何显示文件名;

    Selecting a Frame

    gdb提供的很多指令都是针对当前stack frame的。可以指定stack frame,指定stack frame的指令生效后都会打印出被切换到的stack frame。
    切换frame时,会在第一行打印出frame的level、函数名、参数、源文件和源码行号,并在第二行打印调用的源码。

  • frame [frame-selection-spec]:用于切换stack frame,frame-selection-spec格式如下:

    • num / level num:根据stack frame level指定frame,当前函数frame为0;
    • address stack-address:根据frame的stack address指定frame,可以通过info frame指令查询到;
    • function function-name:根据函数名称指定frame,当有多个同名函数对应的frame时选择最内层的;
    • view stack-address [pc-addr]:查看不属于gdb的backtrace的frame,如果一个bug破坏了stack frame,可能需要用到这种方式;
  • up n:选中距离当前frame n级的外层frame,n默认1
  • down n:和up相反,选中更内层frame;
  • select-frame [frame-selection-spec] / _up-silently _n / _down-silently _n:这几个指令就是上面指令的静默版本,当gdb用于脚本时,可能不希望gdb太过于啰嗦。

    Information About a Frame

    有几个用于查看选中frame信息的指令:

  • frame:打印当前frame简略信息;

  • info frame:打印当前frame详细信息;
  • info frame [frame-selection-spec]:打印指定frame详细信息,frame-selection-spec和前面的一样;
  • info args [-q]:打印frame参数,q可以控制是否打印一些解释信息;
  • info args [-q] [-t type_regexp] [regexp]:打印指定的frame参数,可以指定frame参数类型和名称,只有匹配gerex的才会打印;
  • info locals [-q]:打印frame局部变量;
  • info locals [-q] [-t type_regex] [regexp]:打印frame局部变量,可以指定变量类型和名称regex;

    Applying a Command to Several Frames

  • frame apply [all | count | -count | level level… ] [flag]… command:同时对多个frame使用指令

    • all: 对所有
    • count:对最内层的count个frame
    • -count:对最外层的count个frame
    • level:一个level列表,连续的level中间可以用 - 省略
    • flag:用于控制apply行为和输出的标记,默认情况下,gdb在对frame执行指令前会打印frame信息,并且任何错误都会导致frame apply中止;
      • -c:错误会打印出来,但不会中止frame apply;
      • -s:错误不会打印出来;
      • -q:frame信息不会打印出来
  • faas command:frame apply all -s command的简写,代表frame apply all -s。

    Management of Frame Filters

    这是关于Python扩展的,略过。

    查看源码

    gdb可以显示源代码,是因为可执行文件中包含了源码文件信息,并且能够在本机上(对于gdbserver而言,本机指的是运行gdb的机器,而不是server)找到这些文件。

    显示源码行

    list指令用于显示文件源码,默认显示当前行附近10行源码:

  • list linenum/first,last:显示linenum行附近或者指定范围源码;

  • list function:显示函数开头附近源码;
  • list:打印更多行;
  • list -:打印之前行;
  • set listsize count / unlimited, show listsize:设置显示多少行

    指定源码位置

    有三种源码位置指定方式:Linespac, Explicit, Address。

  • Linespec Location

    • linenum:指定当前文件源码行数;
    • -offset/+offset:指定当前行前后的offset行,对于list指令当前行是正在显示的源码行,对于breakpoint指令则是断点行;
    • filename:linenum:指定文件中的某行,filename是相对路径时,gdb会在源码路径查找具有相同路径前缀的文件;
    • function:指定函数名,对于break指令,在C++中,函数名如果不明确指定命名空间,那么所有命名空间中的同名函数都会被指定,不过可以为break指令使用-qualified选项,说明函数名为全限定名;
    • function:label:指定函数中出现label的行;
    • filename: function:当多个文件中有同名函数时,需要指定filename;
    • label:指定当前stack frame中的label出现的行,在没有当前stack frame的上下文中无效;
    • -pstap | -probe-stap [objfile:[provider:]]name:和GNU/Linux的SystemTap工具有关,略过;
  • Explicit Location:前面的linespec方式指定源码行的方式在某些情况下会产生歧义,导致混乱,对于大型项目使用explicit location指定位置更精确,更不容易出错,相比linespec,这种方式明确指定了参数的含义,参数的解释方式和linespec类似,这里就略过了:
    • -source filename
    • -function function
    • -qualified
    • -label label
    • -line lineNumber
  • Address Location:对于list和edit这样针对某行的指令,address location指定的是包含address的某一行源码,对于break和其他断点指令,指定的是没有源码和调试信息的程序的某一部分。(这个解释不太明白,需要更多的背景知识,暂时略过。。。)

    编辑源文件,略过

    查询源文件,略过

    指定源文件路径

    可执行文件并不一定总是记录了源文件的编译目录,有时仅会记录源文件名称。虽然多数可执行文件还是记录了源文件目录,但源文件并不一定总是待在一个地方,你的目录结构在编译后可能已经发生过改变,gdb为了知道去哪里寻找源文件来展示源码,会去一系列目录下寻找,这些目录路径叫做source path。
    其实,gdb会尽力查找源码,如果没有找到还会尝试将源文件路径前缀去掉来查找,举个例子:
    源文件路径:/usr/src/foo-1.0/lib/foo.c,
    source path:/mnt/cross
    查找顺序:/usr/src/foo-1.0/lib/foo.c,/mnt/cross/usr/src/foo-1.0/lib/foo.c,/mnt/cross/foo.c,如果都查找不到,就报错。可执行文件的位置和源码查找路径没关系。
    当gdb运行时,默认的source path只有cdircwd。使用directory指令可以添加source path。(cdir是什么意思?)
    source path用于查找程序源码和GDB script file。
    gdb还提供了一系例管理source path路径替换的指令,这些指令会管理一系列substitution rule,一条substitution rule说明调试信息中记录的源文件路径在当前的调试机器上被映射到了何处,一个字符串用于指定什么路径需要替换,另一个字符串指定要被替换成什么路径。在使用另一台机器编译出的程序调试时,源文件路径替换是必须的。使用set substitution-path指令可以指定subtitution rule。
    在进行路径替换时,gdb会小心的仅替换起始路径部分,比如如果几个源文件:/root/usr/src/spec, /usr/srcprops, /usr/src, /usr/src/prop,并且gdb有替换规则/usr/src -> /mnt/cross,gdb只会替换最后一个/usr/src/prop,因为对于前两个文件而言/usr/src并不是目录,而对于/root/usr/src/spec替换规则制定的目录并不是它的目录起点。
    有时用directory指令指定目录能起到和subsitution rule一样的效果,但通常directory指令更麻烦些。此外,substitution rule只有在原位置找不到文件时,才去替换后的位置查找。

  • directory dirname:将dirname添加到source path,多个路径以:分隔,对于win以;分隔,可以用$cdir来引用编译路径,$cwd来引用gdb运行时的当前路径,注意$cwd和.的区别,前者是gdb命令运行时的路径,而后者可能在gdb session中被展开为当前session的路径;

  • directory:重置source path;
  • set directories path-list, _show directoties_:设置source path
  • set substitute-path from to:指定substitution rule,以from开头的路径会被替换为to进行查找,可以多次为同一个from指定替换规则,先指定的会先进行替换查找;
  • unset substitute-path [path]:指定path时取消指定规则,不指定则取消所有规则;
  • show substitute-path [path]:指定path时打印指定规则,不指定则打印所有规则;

    源码和机器码

  • info line [location]:打印location源码处的机器码地址,不指定location将打印当前行源码的机器码地址;

    • eg:info line m4_changequote:打印函数首行代码地址;
    • eg:info line *0x63ff:打印指定地址信息,可以用来查询机器码对应的源文件;
  • disassemble [/m /s /r] start, end/+length:将指定范围的地址翻译成汇编,/m和/s用于同时打印汇编和源码,其中/m选项已经废弃,因为它不支持inline函数,应该优先选择使用/s,/r用于在打印汇编的同时,将原始二进制也打印出来;
  • set/show disassembler-options:设置/查看汇编选项,可用参数需要查询objdump —help;
  • set/show disassembly-flavor instruction-set:选择intel或者att风格的汇编代码;
  • set/show disassemble-next-line:是否在gdb中止时自动显示中止处代码的汇编,打印汇编时也会把源码一同打印出来,可选项为ON, OFF, AUTO,如果是AUTO,那么gdb会在有源码时显示源码,没源码时显示汇编,默认为false,也就是只显示源码,无源码时则不显示。