gdb 不能显示变量_Linux C/C++ 开发人员要熟练掌握 GDB 调试代码块-续2

一、GDB 实用调试技巧( 上)

1、print 打印结果显示完整

当使用 print 命令打印一个字符串或者字符数组时,如果该字符串太长,print 命令默认显示不全的,我们可以通过在 GDB 中输入 set print element 0 命令设置一下,这样再次使用 print 命令就能完整地显示该变量的所有字符串了。

void ChatSession::OnGetFriendListResponse(const std::shared_ptr& conn)

{

std::string friendlist;

MakeUpFriendListInfo(friendlist, conn);

std::ostringstream os;

os << "{"code": 0, "msg": "ok", "userinfo":" << friendlist << "}";

Send(msg_type_getofriendlist, m_seq, os.str());

LOG_INFO << "Response to client: userid=" << m_userinfo.userid << ", cmd=msg_type_getofriendlist, data=" << os.str();

}

以上代码中,当第一次打印 friendlist 这个变量值时,只能显示部分字符串。使用 set print element 0 设置以后就能完整地显示出来了。

e76d03c2007852b99f9758e6a0061dc7.png
2、让被 GDB 调试的程序接收信号

请看下面的代码:

void prog_exit(int signo)

{

std::cout << "program recv signal [" << signo << "] to exit." << std::endl;

}

int main(int argc, char* argv[])

{

//设置信号处理

signal(SIGCHLD, SIG_DFL);

signal(SIGPIPE, SIG_IGN);

signal(SIGINT, prog_exit);

signal(SIGTERM, prog_exit);

int ch;

bool bdaemon = false;

while ((ch = getopt(argc, argv, "d")) != -1)

{

switch (ch)

{

case 'd':

bdaemon = true;

break;

}

}

if (bdaemon)

daemon_run();

//省略无关代码...

}

在这个程序中,我们接收到 Ctrl + C 信号(对应信号 SIGINT)时会简单打印一行信息,而当用 GDB 调试这个程序时,由于 Ctrl + C 默认会被 GDB 接收到(让调试器中断下来),导致无法模拟程序接收这一信号。解决这个问题有两种方式:

  • 在 GDB 中使用 signal 函数手动给程序发送信号,这里就是 signal SIGINT;
  • 改变 GDB 信号处理的设置,通过 handle SIGINT nostop print 告诉 GDB 在接收到 SIGINT 时不要停止,并把该信号传递给调试目标程序 。

(gdb) handle SIGINT nostop print pass

SIGINT is used by the debugger.

Are you sure you want to change it? (y or n) y

Signal Stop Print Pass to program Description

SIGINT No Yes Yes Interrupt

(gdb)

3、函数明明存在,添加断点时却无效

有时候一个函数明明存在,并且我们的程序也存在调试符号,使用 break functionName 添加断点时 GDB 却提示:

Make breakpoint pending on future shared library load? y/n

即使输入 y 命令,添加的断点可能也不会被正确地触发,此时需要改变添加断点的方式,使用该函数所在的代码文件和行号添加断点就能达到效果

二、GDB 实用调试技巧( 下)

1、多线程下禁止线程切换

假设现在有 5 个线程,除了主线程,工作线程都是下面这样的一个函数:

void thread_proc(void* arg)

{

//代码行1

//代码行2

//代码行3

//代码行4

//代码行5

//代码行6

//代码行7

//代码行8

//代码行9

//代码行10

//代码行11

//代码行12

//代码行13

//代码行14

//代码行15

}

为了能说清楚这个问题,我们把四个工作线程分别叫做 A、B、C、D。

假设 GDB 当前正在处于线程 A 的代码行 3 处,此时输入 next 命令,我们期望的是调试器跳到代码行 4 处;或者使用“u 代码行10”,那么我们期望输入 u 命令后调试器可以跳转到代码行 10 处。

但是在实际情况下,GDB 可能会跳转到代码行 1 或者代码行 2 处,甚至代码行 13、代码行 14 这样的地方也是有可能的,这不是调试器 bug,这是多线程程序的特点,当我们从代码行 4 处让程序 continue 时,线程 A 虽然会继续往下执行,但是如果此时系统的线程调度将 CPU 时间片切换到线程 B、C 或者 D 呢?那么程序最终停下来的时候,处于代码行 1 或者代码行 2 或者其他地方就不奇怪了,而此时打印相关的变量值,可能就不是我们需要的线程 A 的相关值。

为了解决调试多线程程序时出现的这种问题,GDB 提供了一个在调试时将程序执行流锁定在当前调试线程的命令:set scheduler-locking on。当然也可以关闭这一选项,使用 set scheduler-locking off。除了 on/off 这两个值选项,还有一个不太常用的值叫 step,这里就不介绍了。

2、条件断点

在实际调试中,我们一般会用到三种断点:普通断点、条件断点和硬件断点。

硬件断点又叫数据断点,这样的断点其实就是前面课程中介绍的用 watch 命令添加的部分断点(为什么是部分而不是全部,前面介绍原因了,watch 添加的断点有部分是通过软中断实现的,不属于硬件断点)。硬件断点的触发时机是监视的内存地址或者变量值发生变化。

普通断点就是除去条件断点和硬件断点以外的断点。

下面重点来介绍一下条件断点,所谓条件断点,就是满足某个条件才会触发的断点,这里先举一个直观的例子:

void do_something_func(int i)

{

i ++;

i = 100 * i;

}

int main()

{

for(int i = 0; i < 10000; ++i)

{

do_something_func(i);

}

return 0;

}

在上述代码中,假如我们希望当变量 i=5000 时,进入 do_something_func() 函数追踪一下这个函数的执行细节。此时可以修改代码增加一个 i=5000 的 if 条件,然后重新编译链接调试,这样显然比较麻烦,尤其是对于一些大型项目,每次重新编译链接都需要花一定的时间,而且调试完了还得把程序修改回来。

有了条件断点就不需要这么麻烦了,添加条件断点的命令是 break [lineNo] if [condition],其中 lineNo 是程序触发断点后需要停下的位置,condition 是断点触发的条件。这里可以写成 break 11 if i==5000,其中,11 就是调用 do_something_fun() 函数所在的行号。当然这里的行号必须是合理行号,如果行号非法或者行号位置不合理也不会触发这个断点。

(gdb) break 11 if i==5000

Breakpoint 2 at 0x400514: file test1.c, line 10.

(gdb) r

The program being debugged has been started already.

Start it from the beginning? (y or n) y

Starting program: /root/testgdb/test1

Breakpoint 1, main () at test1.c:9

9 for(int i = 0; i < 10000; ++i)

(gdb) c

Continuing.

Breakpoint 2, main () at test1.c:11

11 do_something_func(i);

(gdb) p i

$1 = 5000

把 i 打印出来,GDB 确实是在 i=5000 时停下来了。

添加条件断点还有一个方法就是先添加一个普通断点,然后使用“condition 断点编号断点触发条件”这样的方式来添加。添加一下上述断点:

(gdb) b 11

Breakpoint 1 at 0x400514: file test1.c, line 11.

(gdb) info b

Num Type Disp Enb Address What

1 breakpoint keep y 0x0000000000400514 in main at test1.c:11

(gdb) condition 1 i==5000

(gdb) r

Starting program: /root/testgdb/test1

y

Breakpoint 1, main () at test1.c:11

11 do_something_func(i);

Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64

(gdb) p i

$1 = 5000

(gdb)

同样的规则,如果断点编号不存在,也无法添加成功,GDB 会提示断点不存在:

(gdb) condition 2 i==5000

No breakpoint number 2.

3、使用 GDB 调试多进程程序

这里说的多进程程序指的是一个进程使用 Linux 系统调用 fork() 函数产生的子进程,没有相互关联的进程就是普通的 GDB 调试,不必刻意讨论。

在实际的应用中,如有这样一类程序,如 Nginx,对于客户端的连接是采用多进程模型,当 Nginx 接受客户端连接后,创建一个新的进程来处理这一路连接上的信息来往,新产生的进程与原进程互为父子关系,那么如何用 GDB 调试这样的父子进程呢?一般有两种方法:

  • 用 GDB 先调试父进程,等子进程 fork 出来后,使用 gdb attach 到进程上去,当然这需要重新开启一个 session 窗口用于调试,gdb attach 的用法在前面已经介绍过了;
  • GDB 调试器提供了一个选项叫 follow-fork,可以使用 show follow-fork mode 查看当前值,也可以通过 set follow-fork mode 来设置是当一个进程 fork 出新的子进程时,GDB 是继续调试父进程还是子进程(取值是 child),默认是父进程( 取值是 parent)。(gdb) show follow-fork modeDebugger response to a program call of fork or vfork is “parent”.(gdb) set follow-fork child(gdb) show follow-fork modeDebugger response to a program call of fork or vfork is “child”.(gdb)

建议读者自己写个程序,然后调用 fork() 函数去实践一下,若要想阅读和调试 Apache HTTP Server 或者 Nginx 这样的程序,这个技能是必须要掌握的。

三、自定义 GDB 调试命令

在某些场景下,我们需要根据自己的程序情况,制定一些可以在调试时输出程序特定信息的命令,这在 GDB 中很容易做到,只要在 Linux 当前用户家(home)目录下,如 root 用户是 “/root” 目录,非 root 用户则对应 “/home/ 用户名”目录。

在上述目录中自定义一个名叫 .gdbinit 文件,在 Linux 系统中以点号开头的文件名一般都是隐藏文件,因此 .gdbinit 也是一个隐藏文件,可以使用 ls -a 命令查看(a 的含义是 all 的意思,即显示所有文件,当然也就包括显示隐藏文件);如果不存在,使用 vim 或者 touch 命令创建一个就可以,然后在这个文件中写上你自定义命令的 shell 脚本即可。

以 Apache Web 服务器的源码为例(Apache Server 的源码下载地址请点击这里),在源码根目录下有个文件叫 .gdbinit,这个就是 Apache Server 自定义的 GDB 命令:

# gdb macros which may be useful for folks using gdb to debug

# apache. Delete it if it bothers you.

define dump_table

set $t = (apr_table_entry_t *)((apr_array_header_t *)$arg0)->elts

set $n = ((apr_array_header_t *)$arg0)->nelts

set $i = 0

while $i < $n

if $t[$i].val == (void *)0L

printf "[%u] '%s'=>NULL", $i, $t[$i].key

else

printf "[%u] '%s'='%s' [%p]", $i, $t[$i].key, $t[$i].val, $t[$i].val

end

set $i = $i + 1

end

end

# 省略部分代码

# Set sane defaults for common signals:

handle SIGPIPE noprint pass nostop

handle SIGUSR1 print pass nostop

当然在这个文件的最底部,Apache 设置了让 GDB 调试器不要处理 SIGPIPE 和 SIGUSR1 这两个信号,而是将这两个信号直接传递给被调试的程序本身(即 Apache Server)。

四、GDB TUI——在 GDB 中显示程序源码

很多 Linux 用户或者其他平台用户习惯了有强大的源码显示窗口的调试器,可能对 GDB 用 list 显示源码的方式非常不习惯,主要是因为 GDB 在调试的时候不能很好地展示源码。

GDB 中可以用 list 命令显示源码,但是 list 命令显示没有代码高亮,也不能一眼定位到正在执行的那行代码在整个代码中的位置。可以毫不夸张地说,这个问题是阻止很多人长期使用 GDB 的最大障碍,如此不便,以至于 GNU 都想办法解决了——使用 GDB 自带的 GDB TUI。

先来看一张效果图,是我在使用 GDB TUI 调试 redis-server 时的截图,这样看代码比使用 list 命令更方便。

f7e06a7c0be0e1c7fb0973da0e4d41a2.png
1、开启 GDB TUI 模式

开启 GDB TUI 模式有两个方法。

  • 方法一:使用 gdbtui 命令或者 gdb-tui 命令开启一个调试。gdbtui -q 需要调试的程序名
  • 方法二:直接使用 GDB 调试代码,在需要的时候使用切换键 Ctrl + X + A 调出 GDB TUI 。
2、GDB TUI 模式常用窗口
61737c307f74585158dba8b3487f8629.png

默认情况下,GDB TUI 模式会显示 command 窗口和 source 窗口,如上图所示,还有其他窗口,如下列举的四个常用的窗口:

  • (cmd)command 命令窗口,可以输入调试命令
  • (src)source 源代码窗口, 显示当前行、断点等信息
  • (asm)assembly 汇编代码窗口
  • (reg)register 寄存器窗口

可以通过“layout + 窗口类型”命令来选择自己需要的窗口,例如,在 cmd 窗口输入 layout asm 则可以切换到汇编代码窗口。

7eda4f4175c7e7c18a4b0db1051ff4c2.png

layout 命令还可以用来修改窗口布局,在 cmd 窗口中输入 help layout,常见的有:

Usage: layout prev | next |

Layout names are:

src : Displays source and command windows.

asm : Displays disassembly and command windows.

split : Displays source, disassembly and command windows.

regs : Displays register window. If existing layout

is source/command or assembly/command, the

register window is displayed. If the

source/assembly/command (split) is displayed,

the register window is displayed with

the window that has current logical focus.

另外,可以通过 winheight 命令修改各个窗口的大小,如下所示:

(gdb) help winheight

Set the height of a specified window.

Usage: winheight [+ | -]

Window names are:

src : the source window

cmd : the command window

asm : the disassembly window

regs : the register display

##将代码窗口的高度扩大 5 行代码

winheight src + 5

##将代码窗口的高度减小 4 代码

winheight src - 4

当前 GDB TUI 窗口放大或者缩小以后,窗口中的内容不会自己刷新以适应新的窗口尺寸,我们可以通过 space 键强行刷新 GDB TUI 窗口。

3、窗口焦点切换

在默认设置下,方向键和 PageUp/PageDown 都是用来控制 GDB TUI 的 src 窗口的,因此,我们常用上下键显示前一条命令和后一条命令的功能就没有了,不过可以通过 Ctrl + N/Ctrl + P 来获取这个功能。

注意:通过方向键调整了GDB TUI 的 src 窗口以后,可以用 update 命令重新把焦点定位到当前执行的代码上。

我们可以通过 focus 命令来调整焦点位置,默认情况下焦点是在 src 窗口,通过 focus next 命令可以把焦点移到 cmd 窗口,这时候就可以像以前一样,通过方向键来切换上一条命令和下一条命令。同理,也可以使用 focus prev 切回到源码窗口,如果焦点不在 src 窗口,我们就不必使用方向键来浏览源码了。

(gdb) help focus

help focus

Set focus to named window or next/prev window.

Usage: focus { | next | prev}

Valid Window names are:

src : the source window

asm : the disassembly window

regs : the register display

cmd : the command window

五、GDB 高级扩展工具——CGDB

在使用 GDB 单步调试时,代码每执行一行才显示下一行,很多用惯了图形界面 IDE 调试的读者可能会觉得非常不方便,而 GDB TUI 可能看起来不错,但是存在经常花屏的问题,也让很多读者不胜其烦。那 Linux 下有没有既能在调试时动态显示当前调试处的文件代码,又能不花屏的工具呢?有的,这就是 CGDB。

CGDB 本质上是对 GDB 做了一层“包裹”,所有在 GDB 中可以使用的命令,在 CGDB 中也可以使用。

1、CGDB 的安装

(略)

2、CGDB 的使用

安装成功以后,就可以使用 CGDB 了,在命令行输入 cgdb 命令启动 CGDB ,启动后界面如下:

6548b9b7785479cad4a528a0ef5257c0.png

界面分为上下两部分:上部为代码窗口,显示调试过程中的代码;下部就是 GDB 原来的命令窗口。默认窗口焦点在下部的命令窗口,如果想将焦点切换到上部的代码窗口,按键盘上的 Esc 键,之后再次按字母 i 键将使焦点回到命令窗口。

注意:这个“焦点窗口”的概念很重要,它决定着你当前可以操作的是上部代码窗口还是命令窗口( 和GDB TUI 一样)。

我们用 Redis 自带的客户端程序 redis-cli 为例,输入以下命令启动调试:

cgdb redis-cli

启动后的界面如下:

da99c4a9949155ac4e93927ae990b96d.png

然后加两个断点,如下图所示:

7390e4eb037ad8c00f51fba356835a27.png

如上图所示,我们在程序的 main ( 上图中第 2824 行 )和第 2832 行分别加了一个断点,添加断点以后,代码窗口的行号将会以红色显示,另外有一个绿色箭头指向当前执行的行( 这里由于在 main 函数上加了个断点,绿色箭头指向第一个断点位置 )。单步调试并步入第 2827 行的 sdsnew() 函数调用中,可以看到代码视图中相应的代码也发生了变化,并且绿色箭头始终指向当前执行的行数:

064886afcd5e51396ecbfc86a527cca3.png

更多 CGDB 的用法可以查阅官网,也可以参考 CGDB 中文手册,点击这里可查看详情。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值