当程序进行函数调用时,这些调用信息(比如在哪里调用等)称为栈帧。每一个栈帧的内容还包括调用函数的参数、局部变量等。所有栈帧组成的信息称为调用栈(或者调用堆栈)。

当程序刚开始运行时,只有一个栈帧,即主函数 main。每调用一个函数,就产生一个新的栈帧;当函数调用结束时(即从函数返回后),该函数的调用随之结束,该栈帧也结束。如果该函数是一个递归函数,则调用该函数会产生多个栈帧。

1. 查看栈回溯信息

查看栈回溯信息的命令是 backtrace 。执行该栈回溯命令后,会显示程序执行到什么位置、包含哪些帧等信息。每一帧都有一个编号,从 0 开始。0 表示当前正在执行的函数,1 表示调用当前函数的函数,以此类推。栈回溯是倒序排列的。

下面来演示 backtrace 命令的用法。示例代码:

#include <iostream>
#include <string>

int call_fun_test_2(int level, const char *str)
{
    int number = 102;
    const char *name = "call_fun_test_2";
    printf("level is %d,str is %s,name is %s\n", level, str, name);
    return 2;
}

int call_fun_test_1(int level, const char *str)
{
    int number = 101;
    const char *name = "call_fun_test_1";
    printf("level is %d,str is %s,name is %s\n", level, str, name);
    call_fun_test_2(level + 1, "call_fun_test_2");
    return 1;
}

int main(int argc, char *argv[])
{
    call_fun_test_1(1, "call_fun_test_1");
}

其中 main 函数调用 call_fun_test_1 。启动 gdb 进入调试模式,为函数 call_fun_test_2 设置一个断点。当程序在 call_fun_test_2 中中断后,执行栈回溯 backtrace 命令,结果如图所示。

(gdb) b call_fun_test_2
Breakpoint 1 at 0x799: file demo.cpp, line 6.
(gdb) r
Starting program: /home/wohu/cppProject/book_debug/chapter_3.1/demo 
level is 1,str is call_fun_test_1,name is call_fun_test_1

Breakpoint 1, call_fun_test_2 (level=2, str=0x555555554939 "call_fun_test_2")
    at demo.cpp:6
6	    int number = 102;
(gdb) backtrace
#0  call_fun_test_2 (level=2, str=0x555555554939 "call_fun_test_2") at demo.cpp:6
#1  0x0000555555554823 in call_fun_test_1 (level=1, str=0x555555554972 "call_fun_test_1") at demo.cpp:17
#2  0x000055555555484a in main (argc=1, argv=0x7fffffffdea8) at demo.cpp:23
(gdb) 

命令 backtrace 可以简写为 bt 。从命令执行结果来看,一共有 3 个栈帧,编号分别为 0、1、2。每个栈帧中都包含函数名、调用函数的参数以及代码所在的行等。我们可以看到一个完整的函数调用链。

也可以执行命令来查看指定数量的栈帧:

bt 栈帧数量

这对于调用栈帧比较多的情况很有用处,可以忽略掉不太关心的那些栈帧。比如执行 bt 2 ,则只显示两个栈帧。

(gdb) bt 2
#0  call_fun_test_2 (level=2, str=0x555555554939 "call_fun_test_2") at demo.cpp:6
#1  0x0000555555554823 in call_fun_test_1 (level=1, str=0x555555554972 "call_fun_test_1") at demo.cpp:17
(More stack frames follow...)
(gdb) 

从上面可以看到,只显示了 0 和 1 两帧。如果想查看 1 和 2 这两个帧应该怎样做呢?
可以使用命令 bt -2 ,如下所示。

(gdb) bt -2
#1  0x0000555555554823 in call_fun_test_1 (level=1, str=0x555555554972 "call_fun_test_1") at demo.cpp:17
#2  0x000055555555484a in main (argc=1, argv=0x7fffffffdea8) at demo.cpp:23
(gdb) 

如果 bt 后面跟的是一个正数,则从 0 开始计数。如果是一个负数,则从最大的栈帧编号开始倒序计数,但是最后显示时还是按照从小到大的编号顺序显示,只是显示的栈帧不同。比如一共有 10 个帧,编号为 0-9,如果执行 bt 4 ,则显示的帧为 0-3;如果执行命令 bt -4,则显示的帧编号为 6~9。

2. 切换栈帧

可以通过 frame 栈帧号 的方式来切换栈帧。为什么要切换栈帧呢?因为每一个栈帧所对应的程序的运行上下文都不同,比如栈帧 1 的局部变量和栈帧 2 的局部变量都不相同,只有切换到某个具体的栈帧之后才能查看该栈帧对应的局部变量信息。比如上面的栈回溯中,共有 3 个栈帧,我们想查看栈帧号为2(也就是 main 函数中所对应)的信息,则执行命令即可切换到 2 号帧:

frame 2 
# 或者
f 2

这时我们可以查看该帧对应的一些变量信息,比如局部变量 numbername 的值

(gdb) f 2
#2  0x000055555555485c in main (argc=1, argv=0x7fffffffdea8) at demo.cpp:25
25	    call_fun_test_1(1, "call_fun_test_1");
(gdb) p name
$1 = 0x5555555549a2 "main"
(gdb) p number
$2 = 100
(gdb) 

再切换到 1号帧。因为 1 号帧中也包含两个临时变量 numbername ,执行 f 1 ,然后查看 numbername 的值,

(gdb) f 1
#1  0x0000555555554823 in call_fun_test_1 (level=1, str=0x555555554992 "call_fun_test_1") at demo.cpp:17
17	    call_fun_test_2(level + 1, "call_fun_test_2");
(gdb) p name
$3 = 0x555555554992 "call_fun_test_1"
(gdb) p number
$4 = 101
(gdb) 

除使用 print 查看局部变量外,还可以使用 info locals 来查看当前帧的所有局部变量的值,也可以使用info args 来查看当前帧所有的函数参数,

(gdb) info locals
number = 101
name = 0x555555554992 "call_fun_test_1"
(gdb) i locals
number = 101
name = 0x555555554992 "call_fun_test_1"
(gdb) info args
level = 1
str = 0x555555554992 "call_fun_test_1"
(gdb) i args
level = 1
str = 0x555555554992 "call_fun_test_1"
(gdb) 

还可以使用命令 updown 来切换帧。updown 都是基于当前帧来计数的。比如,当前帧号为1,up 1 则切换到 2 号帧,down 1 则切换到 0 号帧,

(gdb) bt
#0  call_fun_test_2 (level=2, str=0x555555554959 "call_fun_test_2") at demo.cpp:6
#1  0x0000555555554823 in call_fun_test_1 (level=1, str=0x555555554992 "call_fun_test_1") at demo.cpp:17
#2  0x000055555555485c in main (argc=1, argv=0x7fffffffdea8) at demo.cpp:25
(gdb) up 1
#2  0x000055555555485c in main (argc=1, argv=0x7fffffffdea8) at demo.cpp:25
25	    call_fun_test_1(1, "call_fun_test_1");
(gdb) up 1
#2  0x000055555555485c in main (argc=1, argv=0x7fffffffdea8) at demo.cpp:25
25	    call_fun_test_1(1, "call_fun_test_1");
(gdb) up 2
#2  0x000055555555485c in main (argc=1, argv=0x7fffffffdea8) at demo.cpp:25
25	    call_fun_test_1(1, "call_fun_test_1");
(gdb) down 1
#1  0x0000555555554823 in call_fun_test_1 (level=1, str=0x555555554992 "call_fun_test_1") at demo.cpp:17
17	    call_fun_test_2(level + 1, "call_fun_test_2");
(gdb) down 2
#0  call_fun_test_2 (level=2, str=0x555555554959 "call_fun_test_2") at demo.cpp:6
6	    int number = 102;
(gdb) 

还可以使用以下命令来切换帧:

f 帧地址

其中,帧地址是栈帧所对应的地址。如果程序崩溃,栈回溯信息可能会遭到破坏,这时就可以使用该命令来进行栈帧切换。假设有一个栈帧的地址为 0x7fffffffe3a0 ,则使用命令即可切换到该栈帧,

84

3. 查看帧信息

可以使用 info frame 命令(包括前面介绍的 info localsinfo args 命令)来查看帧的详细信息,还可以使用 info frame 命令来查看具体的某一帧的详细信息。

比如要查看编号为 1 的帧的详细信息,可以直接使用 info frame 1 (可以简写为 i f 1 )命令,而不用先进行帧的切换操作。如下所示为连续查看 1 号帧和 2 号帧的详细信息。

86
从图中可以看到,帧的详细信息包括帧地址、rip 地址、函数名、函数参数等信息。这里可以用 f 帧地址命令来切换帧地址。这个帧地址也可以用到 i f 命令中,比如使用 i f 0x7fffffffe400 可以查看 2 号帧的详细信息。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐