程序调试

从Intel C/C++ Fortran编译器2015版开始,采用的是Intel改造的GDB调试器,命令为 gdb-ia 。PGI调试器很多调试命令类似GDB调试器,请自己查看相关资料。

GDB调试器简介

GDB调试器可以让使用者查看其它程序运行时内部发生了什么或查看其它程序崩溃时程序在做什么。主要包括以下四项功能以便帮助找出bug:

  • 启动程序,并指定任何可能影响行为的东西。
  • 使程序在特定条件下停止。
  • 当程序停止时,检查发生了什么。
  • 修改程序中的一些东西,以便能用正确的东西影响bug,并获得进一步信息。

GDB调试可用于调试采用C/C++、Fortran、D、Modula-2、OpenCL C、Pascal、Objective-C等编写的程序。

基本启动方式[gdbstart]

GDB调试器在Linux系统上可以采用命令行(command line)和图形界面(GUI,借助Eclipse* IDE或xxgdb)两种方式进行调试。

图形界面的GDB调试器相对简单,本手册主要介绍基于命令行的GDB调试器。基于命令行的启动方式主要有如下几种:

  • 最常用的方式是只跟程序名为参数,启动调试程序:
    gdb program
  • 启动应用程序及以前其出错时生成的core文件:
    gdb program core
  • 利用运行程序的进程号吸附到运行中程序进行调试:
    gdb program 1234
  • 如果需要调试的程序有参数,那么需要添加–args参数:
    gdb --args gcc -O2 -c foo.c
  • 采用静默方式启动,不打印启动后的版权信息等:
    gdb --silent
  • 仅显示帮助信息等:
    gdb --help

选择启动时文件

当GDB启动的时候,它读取除选项之外的任何参数用于指定可执行程序文件和core文件(或进程号),这与分别采用-se和-c(或-p)参数类似(gdb读取参数时,如第一个参数没有关联选项标记,那么等价于跟着-se选项之后的参数,如第二个参数没有关联选项标记,那么等价于跟着-c/-p选项之后的参数)。如果第二个参数以十进制数字开始,那么gdb尝试将其作为进程号并进行吸附,如果失败,则尝试作为core文件打开。如果以数字开始的core文件,那么可以在此之前添加./以防止被认为是进程号,例如./12345。很多选项同时具有长格式和短格式两种格式,如果采用了截断的长格式选项,且长度足够避免歧义,那么也可以被重新辨认为长格式。(如果你喜欢,可以采用–而不是-来标记选项参数)。

  • -symbols file、-s file:从文件file中读取符号表。
  • -exec file、-e file:适当时采用文件file作为可执行程序,并且与core dump文件关联时用于检查纯数据。
  • -se file:从文件file中读取符号表,并且将其作为可执行文件。
  • -core file、-c file:将文件file作为core dump文件进行检查。
  • -pid number、-p number:将附带的命令吸附到进程号number。
  • -command file、-x file:指定启动后执行的命令文件file,文件file中保存一系列命令,启动后会顺序执行。
  • -eval-command command、-ex command:执行单个gdb命令,此选项可以多次使用以多次调用命令。需要时,此选项也许与-command交替,如: gdb -ex 'target sim' -ex 'load' -x setbreakpoints -ex 'run' a.out
  • -init-command file、-ix file:从文件file中加载并执行命令,此过程在加载inferior [1] 之前(但在加载gdbinit文件之后)。
  • -init-eval-command command、-iex command:在加载inferior之前(但在加载gdbinit文件之后)执行命令command。
  • -directory directory、-d directory:添加目录directory到源文件和脚本文件的搜索目录

记录日志

可以采用以下方式记录日志等,启动GDB后执行:

  • 启用日志: set logging on
  • 关闭日志: set logging off

  • 记录日志到文件file(默认为gdb.txt): set logging file file

  • 设定日志是否覆盖原有文件(默认为追加): set logging overwrite [on|off]

  • 设定日志是否重定向(默认为显示在终端及文件中): set logging redirect [on|off]

  • 显示当前日志设置: show logging

退出GDB

退出调试器,在GDB内部命令执行完后的命令行,输入以下两者之一:

  • quit
  • <ctrl+d>

准备所需要调试的程序

准备调试代码源代码

调试程序时,一般无需修改程序源代码,但是在程序中建议做如下改变:

  • 如果程序运行后,利用调试器难于终止,请设置一个初始停止点;
  • 在源代码增加一些断言,以便帮助定位错误。

准备编译器和链接器环境

调试信息被编译器存储在.o文件。信息的级别和格式由编译器选项控制。

对于Intel C/C++ Fortran编译器,采用-g或-debug选项,例如:

  • icc -g hello.c
  • icpc -g hello.cpp
  • ifort -g hello.f90

对于GCC编译器,采用-g选项。对于一些较老版本的GCC,此选项也许会产生DWARF-1标准的调试信息,如果这样,请使用-gdwarf-2选项,例如:

  • gcc -gdwarf-2 hello.c
  • g++ -gdwarf-2 hello.cpp
  • gfortran -gdwarf-2 hello.f90

调试信息将通过 ld 命令导入到a.out(可执行程序)或.so(共享库)文件中。

如果是在调试优化编译的代码,采用-g选项将自动增加-O0选项。

请参看调试优化编译的代码部分中关于-g和相关扩展调试选项及它们的与优化之间的关系。

调试优化编译的代码

GDB调试器可以通过使用-g参数帮助调试优化编译的程序。但是关于此程序的信息也许并不准确,尤其是变量的地址和值经常没有被正确报告,这是因为通用调试信息模式无法全部表示-O1、-O2、-O3及其它优化选项的复杂性。

为了避免此限制,采用Intel编译器编译程序时在所需的-O1、-O2或-O3优化选项同时指明-g和-debug扩展选项。这会产生具有更多高级但更少通用支持的调试信息,主要激活以下:

  • 给出变量的正确地址和值,不管其是在寄存器或不同时间在不同地址时。注意:
    • 在程序中,一些变量可能被优化掉或转换成不同类型的数据,或其地址没有在所有点都被记录。在这些情形下,打印变量时将显示无值。
    • 否则,这些值和地址将正确,但这些寄存器没有地址,调试器中 print &i 命令将打印一条警告。
    • 尽管 break main 命令通常将在程序开始处理后停止,但程序大多数变量和参数在程序的开始处理和结束处理时是未定义的。
  • 在堆栈追踪中显示内联函数,这通过使用inline关键词识别。注意:
    • 只有在堆栈顶端和通常(非内联)调用的函数显示指令指针,其原因在于其它函数与其调用的内联函数共享硬件定义的堆栈帧。
    • 返回指令将只返回对那些采用调用指令时是非内联调用函数的控制,其原因在于内联调用没有定义返回地址。
    • updowncall 命令以通常方式工作。
  • 允许在内联函数中设置断点。

准备所需要调试的并行程序

编译时必须用-g等调试参数编译源代码才可以使用GDB调试器特性,比如分析共享数据或在重入函数调用中停止。

为了使用并行调试特性,需要:

  • 如果存在makefile编译配置文件,请对它进行编辑。
  • 在命令行添加编译器选项-debug parallel(Intel编译器针对OpenMP多线程)。
  • 重编译程序。

编译所要调试的程序

下面以常做为例子的hello程序为例介绍。

  • hello.c例子:
    #include <stdio.h>
    int main() {
        printf("Hello World!");
        return 0;
    }
    

    编译:

    icc -g helloworld.c -o helloworld

  • hello.f90例子:
    program main
    print *,"Hello World!"
    end program main
    

    编译:

    ifort -debug -O0 helloworld.f90 -o helloworld

开始调试程序

启动调试: gdb helloworld 。更多启动方式参见 [gdbstart]

显示源代码

在调试器启动后的命令行中输入 list 命令可以显示源代码,如输入 list main ,将显示main函数的代码。

运行程序

在命令行中输入 run ,将开始运行程序。

设置和删除断点

  • 设置断点:
    • 输入以下命令: break main
      此时在程序main处设置了一个断点。
    • 输入 run 再次运行程序
      应用将停止在设置的断点处。
  • 删除断点:
    • 列出所有设置的断点ID号: info breakpoints
      调试器将显示所有存在的断点。
    • 指明所要删除的断点ID号。如果从开始调试后没有设置其它断点,那么只有1个断点,其ID号为1。
    • 删除此断点: delete breakpoint 1
      那么将删除设置断点1。
    • 重新运行程序。
      那么程序将运行并显示“Hello World!”,并退出程序。

控制进程环境

用户可以:1、对进程的环境变量进行设置或者取消设置以便在将来使用;2、设置与当前调试器环境和启动调试器的shell不同的环境。设置的变量将影响后续调试的新进程。环境命令不影响当前运行进程。设置的环境变量不改变或显示调试器的环境变量,它们只影响新产生的进程。

  • 显示当前集的所有环境变量: show environment
  • 增加或改变环境变量: set environment
  • 取消一个环境变量: unset environment

注意:GDB调试器没有命令可以简单回到调试器启动时的环境变量的初始状态,用户必须正确设置和取消环境变量。

执行一行代码

如果源代码当前行是函数调用,那么可以步入(step into)或者跨越(step over)此函数。

  1. step 命令:应用程序执行一行代码,如果此行是函数调用,那么应用程序步入到函数中,即不执行完此函数调用。
  2. next 命令:应用程序执行一行代码,如果此行是函数调用,那么应用程序跨越此函数,即执行完此函数调用。

执行代码直到

运行代码直到某行或某个表达式,可用 until 命令。

执行一行汇编指令

如果应用的当前指令为函数调用,那么可以步入或者跨越此函数。

  1. stepi 命令:应用程序执行一行汇编指令,如果此行指令是函数调用,那么应用程序步入到函数中。
  2. nexti 命令:应用程序执行一行汇编指令,如果此行指令当前行是跳出或调用,那么应用程序跨越过它。

显示变量或表达式值

利用 print 命令可以显示变量值或表达式的值。如:

  • 显示变量val2的当前值: print val2
  • 显示表达式val2*2的值: print val2*2

传递命令给调试器

命令、文件名和变量补全

GDB调试器支持命令、文件名和变量的补全。在GDB调试器命令行中开始键入一个命令、文件名或变量名,然后按Tab键。如果有不只一个备选,调试器会发出铃声。再一次按Tab键,将列出备选。

利用单引号和双引号影响可能备选集。利用单引号填充C++名字,包含特殊字符“:”、“\(<\)”、“\(>\)”、“(”、“)”等。利用双引号告诉调试器在文件名中查看备选。

自定义命令

GDB调试器支持用户自定义命令。

用户定义的命令支持在定义体内包含if、while、loop_break和loop_continue命令。用户定义的命令最多可有10个参数,以空白分割。参数名依次为$arg0、$arg1、$arg2、 \(\dots\) 、$arg9。参数总数存储在$argc中。

其步骤为:

  • 输入define commandname
  • 每行输入一个命令
  • 输入end

调试并行程序

调试OpenMP等多线程程序

一个单独的程序可以有不止一个线程执行,但一般来说,一个程序的线程除了它们共享一个地址空间外,还类似于多个进程。另一方面,每个县城具有自己的寄存器和执行堆栈,也许还占有私有内存。

线程是进程内部单个、串行控制流。每个线程包含单个执行点。线程在单个地址空间中(共享)执行;因此,进程的线程可以读写相同的内存地址。

多个进程执行时,当用户需要关注某个进程时,它却恼人地或不切实际地枚举所有进程。

当为了设置代码断点而定义停止线程和线程过滤器时,用户需要定义线程集。

用户可以以紧凑方式指定进程或线程集,集可包含一个或多个范围。用户可以对每个进程集执行普通操作,调试器变量既可以存储集也可以存储范围以便操作、引用和查看。

  • info threas :查看线程集
  • thread :在线程间进行切换,如thread 2
  • thread apply :对线程应用特定命令,如thread apply 2 break 164
  • thread apply all :对所有线程应用特定命令
  • thread find :发现满足某些特定条件的线程
  • thread name :给当前线程设定名字

注意:线程与当前执行到多线程程序中的位置有关系,在单线程执行的地方只显示一个线程,在多线程执行的地方会显示多线程。

对各线程就可采用普通GDB命令对单个进程分别进行调试。

调试MPI并行应用

采用Intel MPI时,可以采用类似下面命令调用GDB调试器:

mpirun -gdb -n 4 ./tmissem-dbg

之后可以像单进程程序一样调试程序。

也可以吸附到一个运行中的程序:

mpirun -n 4 -gdba <pid>

其中<pid>为MPI进程的进程号。

如: mpirun -gdb -n 4 ./tmissem-dbg 显示:

mpigdb: np = 4
mpigdb: attaching to 13526 ./tmissem-dbg tc4600v4
mpigdb: attaching to 13527 ./tmissem-dbg tc4600v4
mpigdb: attaching to 13528 ./tmissem-dbg tc4600v4
mpigdb: attaching to 13529 ./tmissem-dbg tc4600v4

上面np=4显示使用了4个进程启动MPI程序,13526之类的为系统MPI程序进程号(不是MPI rank号),./tmissem-dbg为应用程序,tc4600v4为对应节点。

查看源码,执行 list

[2,3]   200 implicit none
[0,1]   200 implicit none
[2,3]   201 include 'mpif.h'
[0,1]   201 include 'mpif.h'
[2,3]   202 integer nmstep,ik,NStep,jk,i,PNum
[0,1]   202 integer nmstep,ik,NStep,jk,i,PNum
[2,3]   203 !real pathxyz(3,100000),t_p(3) !path
[0,1]   203 !real pathxyz(3,100000),t_p(3) !path
[2,3]   204 real    Time_S
[0,1]   204 real    Time_S
[2,3]   205 real(8)  T3
[0,1]   205 real(8)  T3
[2,3]   206 character*2 resf
[0,1]   206 character*2 resf
[2,3]   207
[0,1]   207
[2,3]   208 call MPI_Init(ierr)
[0,1]   208 call MPI_Init(ierr)

上面[0-3]、[0,1]之类的为MPI进程编号,表示改行后面显示的内容为这些进程的。

z 命令可设置对某MPI进程进行操作,如 z 0,1,3 命令设置当前进程集包含进程0、1、3:

mpigdb: set active processes to 0 1 3

z all 切换到全部进程。

之后对各进程就可采用普通GDB命令对单个进程分别进行调试。

[1]GDB采用对象表示每个程序执行状态,这个对象被称为inferior。典型的,一个inferior对应一个进程,但是更通常的是对应一个没有进程的目标。inferior有可能在进程执行之前生成,并且可以在进程停止后驻留。inferior具有独有的与进程号不同的标志符。尽管一些嵌入的目标也许具有多个运行在单个地址空间内不同部分的多个inferior,但通常每个inferior具有自己隔离的地址空间。反过来,每个inferior又有多个线程运行它。在GDB中可以用info inferiors查看。