一、变量查看与修改
在使用 GDB 调试程序时,除了控制程序的运行流程以外,最常见的操作就是查看变量的值、查看参数的值,以及在调试过程中临时修改某些变量的值。通过这些命令,可以观察程序运行时的数据变化,从而判断程序逻辑是否符合预期。
1.1 查看函数参数
使用info args(i args)命令可以查看当前函数传入参数的值,包括main函数的argv与argc参数。
i args
例如,当前程序停在某个函数内部:
cpp
void test(int a, int b)
{
int sum = a + b;
}
在断点处执行:
i args
就可以看见传入的a和b变量的值
除了使用 i args,也可以直接使用print(p)命令打印某个参数的值:
p a
p b
1.2 查看变量
1、查看普通变量
在 GDB 中,可以使用print命令查看变量的值,print可以简写为p。
p [变量名]
例如:
p num
p ch
默认情况下,p命令会按照变量本身的类型打印结果。如果想按照不同格式查看变量,可以在p后面加格式控制。
常见格式如下:
| 命令 | 含义 |
|---|---|
p/d var |
按十进制打印 |
p/x var |
按十六进制打印 |
p/t var |
按二进制打印 |
p/o var |
按八进制打印 |
p/c var |
按字符打印 |
p/s var |
按字符串打印 |
例如:
p/d num
p/x num
p/t num
p/c ch
如果有如下代码:
int num = 65;
char ch = 'A';
执行:
p/d num
p/x num
p/c num
可能得到:
$1 = 65
$2 = 0x41
$3 = 65 'A'
这样可以方便地从不同角度查看变量的值。
2、查看变量类型
如果想查看变量的类型,可以使用如下命令:
ptype [变量名]
ptype显示的信息详细,适合查看结构体、指针、数组等复杂类型。
例如,有如下结构体定义:
cpp
struct Student {
char name[32];
int age;
};
struct Student stu = {"Tom", 18};
struct Student *pstu = &stu;
在 GDB 中可以执行:
ptype stu
可能得到:
type = struct Student {
char name[32];
int age;
}
这说明变量 stu 的类型是 struct Student,并且 GDB 会把结构体中的成员也显示出来。
如果查看结构体指针变量:
ptype pstu
可能得到:
type = struct Student *
这说明 pstu 是一个指向 struct Student 类型的指针。
如果想查看指针指向的对象类型,也可以对指针解引用后再查看:
ptype *pstu
可能得到:
type = struct Student {
char name[32];
int age;
}
在调试结构体、链表、树、数组指针等复杂数据结构时,ptype 非常有用,可以帮助我们快速确认变量的数据类型以及内部成员组成。
1.3 查看数组、字符串、结构体和指针
1、查看数组
对于普通数组,可以直接打印数组名:
p arr
也可以查看数组中的某一个元素:
p arr[0]
p arr[1]
如果是指针指向一段连续内存,可以使用下面的方式查看多个元素:
p *ptr@10
这表示从 ptr 指向的位置开始,连续打印 10 个元素。
例如:
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
在 GDB 中执行:
p *ptr@5
可能输出:
$1 = {1, 2, 3, 4, 5}
2、查看字符串
直接通过p [字符串变量名称]命令就可以显示查看字符串。
查看字符串变量的时候,如果字符串为100个字符串,填充了10个字符之后,后面的字符全为0这时候显示字符串的时候就不是很好看,使用以下命令设置输出显示格式
set print null-stop
例子如下,在没有设置显示格式之前,可以看见,打印的test.name会因为\0的问题,出现显示问题。

设置字符串显示格式之后

可以看见,对应的字符串在应该结束的地方断开。
3、查看结构体变量
如果是结构体变量,可以直接打印:
p student
如果只想查看结构体中的某个成员,可以使用:
p student.name
p student.age
例如:
cpp
struct Student {
char name[32];
int age;
};
struct Student stu = {"Tom", 18};
在 GDB 中可以执行:
p stu
p stu.name
p stu.age
使用以下命令可以使得,在直接打印结构体变量的时候,可以让结构体的每一个成员独占一行
set print pretty

4、查看指针
如果变量是结构体指针,需要使用 -> 访问成员:
p node->data
p node->next
如果想查看指针指向的整个结构体内容,可以使用:
p *node
这在调试链表、树、队列等数据结构时非常常用。
1.4 查看局部变量
使用info locals(i locals)命令可以查看当前函数中的局部变量。
i locals
例如:
cpp
void test(int a, int b)
{
int sum = a + b;
int count = 100;
}
当程序停在 test 函数内部时,执行:
i locals
会把当前函数中的所有局部变量的值打印出来:
sum = 30
count = 100
1.5 表达式与函数调用
在GDB中,p命令不仅可以打印普通变量,也可以计算表达式的值,甚至可以调用程序中的函数或库函数。
例如,可以使用sizeof查看类型或变量所占的字节数:
shell
p sizeof(int)
p sizeof(long)
p sizeof(char *)
p sizeof(变量名)
如果程序中有如下变量:
cpp
int num = 10;
char name[32] = "hello";
在 GDB 中执行:
shell
p sizeof(num)
p sizeof(name)
可能得到:
$1 = 4
$2 = 32
其中,sizeof(num) 表示变量 num 所占的字节数,sizeof(name) 表示整个数组 name 所占的字节数。
除了 sizeof,也可以在 GDB 中调用一些函数,例如 strlen:
p strlen(name)
如果 name 中保存的是字符串 "hello",执行结果可能是:
$3 = 5
这表示字符串的有效长度为 5,不包含字符串结尾的 '\0'。
1.6 修改变量
在 GDB 调试过程中,不仅可以查看变量的值,也可以临时修改变量的值。修改变量常用于验证程序逻辑,例如让程序进入某个特定分支、提前结束循环,或者人为设置某些异常条件,从而观察程序后续的执行情况。
例如,在调试 for 循环时,可以临时修改循环变量的值,让循环提前结束;在调试 if 判断时,也可以修改条件变量的值,让程序进入不同的分支。
在实际调试中,比较常用的方式是直接使用 p 命令执行赋值表达式:
p [arg]=[vaule]
例如:
p count = 100
p flag = 1
p i = 99
这种写法的含义是:让 GDB 执行一个赋值表达式,并把赋值后的结果打印出来。
例如:
(gdb) p count = 100
$1 = 100
这表示变量 count 已经被修改为 100。
1、修改普通变量
例如有如下代码:
int count = 10;
int flag = 0;
在 GDB 中可以执行:
p count = 100
p flag = 1
然后再查看变量:
p count
p flag
可以看到变量的值已经发生变化。
这种方式在调试条件判断时非常有用。例如:
cpp
if (flag == 1) {
printf("进入特殊分支\n");
}
如果程序当前没有进入这个分支,可以在 GDB 中临时修改 flag 的值:
p flag = 1
这样就可以观察程序进入该分支后的执行情况。
2、修改循环变量
在调试循环时,也可以通过修改循环变量来控制循环执行。
例如:
for (int i = 0; i < 100; i++) {
printf("%d\n", i);
}
如果程序停在循环内部,可以执行:
p i = 99
这样下一次循环判断时,循环就可能提前结束,避免一步一步执行完整个循环。
3、修改结构体成员
如果变量是结构体,可以直接修改结构体中的成员。
例如:
struct Student {
char name[32];
int age;
};
struct Student stu = {"Tom", 18};
修改结构体中的普通成员:
p stu.age = 20
然后查看结构体:
p stu
可能得到:
$1 = {
name = "Tom",
age = 20
}
4、修改结构体指针成员
如果变量是结构体指针,需要使用->访问并修改成员。
例如:
struct Student *pstu = &stu;
在 GDB 中可以执行:
p pstu->age = 25
查看修改后的结果:
p *pstu
5、修改结构体中的字符串
如果结构体中的成员是字符数组,例如:
struct Student {
char name[32];
int age;
};
struct Student stu = {"Tom", 18};
由于name是字符数组,不能直接使用下面这种方式修改整个字符串:
p stu.name = "soft"
这种写法通常是不合适的,因为数组名不能整体赋值。
可以使用strcpy函数修改字符数组中的内容:
p strcpy(stu.name, "soft")
然后查看结构体:
p stu
可能得到:
$1 = {
name = "soft",
age = 18
}
如果是结构体指针,也可以写成:
p strcpy(pstu->name, "soft")
需要注意,使用strcpy修改字符串时,要确保目标数组空间足够大,否则可能会造成内存越界。
6、修改指针指向的内容
如果有如下代码:
int num = 10;
int *pnum = #
可以通过指针修改它指向的变量:
p *pnum = 100
这表示修改pnum指向的内存内容,也就是把num的值修改为 100。
查看结果:
p num
可能得到:
$1 = 100
需要注意,下面两种写法含义不同:
p pnum = 0x12345678
这是修改指针变量pnum本身的值,也就是让它指向另一个地址。
p *pnum = 100
这是修改指针指向的内存内容。
在调试指针时要特别小心,如果把指针修改成非法地址,程序后续访问该指针时可能会崩溃。
除了 p 命令,也可以使用更标准的 set variable 命令修改变量:
set variable count = 100
二者都可以修改变量。实际调试时,p [变量名] = [值]更简单直接,并且会立即打印修改后的结果,因此使用非常普遍。
二、内存的查看与修改
在GDB中,除了可以直接查看变量的值,也可以查看变量在内存中的原始数据。通过查看内存,可以观察整型、字符串、结构体等数据在内存中的真实布局,对于理解指针、字节序、结构体内存对齐等问题非常有帮助。
2.1 查看内存地址与其内容
查看内存常用x命令,x是examine的缩写,表示检查内存内容。其基本格式如下:
x[/选项] [内存地址]
例如:
x/4xb &num
含义是:从变量 num 的地址开始,查看 4 个字节,并以十六进制显示。
常见显示格式
| 格式 | 含义 |
|---|---|
x |
十六进制显示 |
d |
十进制显示 |
u |
无符号十进制显示 |
o |
八进制显示 |
t |
二进制显示 |
c |
字符显示 |
s |
字符串显示 |
i |
反汇编指令显示 |
常见单位大小
| 单位 | 含义 |
|---|---|
b |
byte,1 字节 |
h |
half word,2 字节 |
w |
word,4 字节 |
g |
giant word,8 字节 |
例如:
x/4xb &num
表示查看 4 个字节,以十六进制显示。
x/4dw &num
表示查看 4 个 word,每个 word 为 4 字节,并以十进制显示。
需要注意,x/4d addr中的 4 表示查看 4 个单位,不一定表示 4 个字节。如果没有指定单位大小,GDB 会使用默认单位大小。为了避免歧义,建议明确写出单位,例如x/4xb、x/4dw。
1、查看整型变量的内存布局
例如有如下变量:
int itest = 0x12345678;
可以先查看变量的值:
p/x itest
然后查看它在内存中的字节分布:
x/4xb &itest
可能得到:
0x7fffffffdc4c: 0x78 0x56 0x34 0x12
这里可以看到,变量itest的值是0x12345678,但在内存中低地址处存放的是0x78,高地址处存放的是0x12。
这是因为大多数PC平台采用小端字节序,即低地址存放低字节,高地址存放高字节。
2、查看字符串的内存布局
例如有如下字符串:
char str[] = "hello";
可以使用 /s 按字符串方式查看:
x/s str
可能得到:
0x7fffffffdc40: "hello"
也可以按字节查看字符串在内存中的真实布局:
x/6xb str
可能得到:
0x7fffffffdc40: 0x68 0x65 0x6c 0x6c 0x6f 0x00
其中:
0x68 -> 'h'
0x65 -> 'e'
0x6c -> 'l'
0x6c -> 'l'
0x6f -> 'o'
0x00 -> '\0'
如果按字符方式查看,可以使用:
x/6cb str
可能得到:
0x7fffffffdc40: 104 'h' 101 'e' 108 'l' 108 'l' 111 'o' 0 '\000'
3、查看结构体的内存布局
例如有如下结构体:
cpp
struct Test {
char name[8];
int age;
char gender;
};
struct Test test = {"Tom", 18, 'M'};
可以先查看结构体变量本身:
p test
可能得到:
$1 = {
name = "Tom",
age = 18,
gender = 77 'M'
}
然后查看结构体整体的大小:
p sizeof(test)
再查看结构体在内存中的原始数据:
x/16xb &test
可能得到类似结果:
0x7fffffffdc30: 0x54 0x6f 0x6d 0x00 0x00 0x00 0x00 0x00
0x7fffffffdc38: 0x12 0x00 0x00 0x00 0x4d 0x00 0x00 0x00
其中:
0x54 0x6f 0x6d 0x00 ... 对应 name[8]
0x12 0x00 0x00 0x00 对应 age = 18
0x4d 对应 gender = 'M'
结构体中可能会出现一些额外的 0x00,这些通常是结构体对齐产生的填充字节。结构体成员在内存中不是简单地一个紧挨着一个存放,编译器可能会为了提高访问效率,在成员之间或结构体末尾插入填充字节。
2.2 修改内存
在 GDB 中,修改内存可以使用 set 命令。其基本格式如下:
set {类型}内存地址 = 新值
例如:
set {int}0x7fffffffdc4c = 100
表示把地址 0x7fffffffdc4c 处的内容当作int类型,并修改为100。
如果要修改某个变量的内存内容,可以使用变量地址:
set {int}&itest = 100
这表示把itest所在地址处的内容当作int修改为100。
不过在实际调试中,如果只是修改普通变量或结构体成员,更常用、更简单的方式是直接修改变量:
p itest = 100
或者:
set variable itest = 100
三、寄存器的查看与修改
在GDB调试中,除了可以查看变量和内存,也可以直接查看CPU寄存器的值。寄存器中保存着程序运行时非常关键的信息,例如函数参数、返回值、栈地址、当前正在执行的指令地址等。
在程序没有使用-g生成调试符号时,GDB可能无法直接通过变量名查看函数参数和局部变量。这种情况下,可以结合寄存器、栈内存和反汇编指令来分析程序运行状态。
3.1 查看寄存器命令
查看所有寄存器的值:
info registers
该命令可以简写为:
i r
查看某一个寄存器的值:
info registers [寄存器名]
例如:
i r rax
i r rdi
i r rip
在GDB中,如果要在表达式中使用寄存器,需要在寄存器名前加$:
p $rax
p/x $rax
p $rdi
p/x $rip
其中:
p $rax
表示打印rax寄存器的值。
p/x $rax
表示以十六进制格式打印rax寄存器的值。
在 x86-64 架构下,常见寄存器含义如下:
| 寄存器 | 作用 |
|---|---|
rax |
通常用于保存函数返回值 |
rdi |
第 1 个整型或指针参数 |
rsi |
第 2 个整型或指针参数 |
rdx |
第 3 个整型或指针参数 |
rcx |
第 4 个整型或指针参数 |
r8 |
第 5 个整型或指针参数 |
r9 |
第 6 个整型或指针参数 |
rsp |
栈顶指针,指向当前栈顶 |
rbp |
栈帧指针,常用于定位局部变量和函数参数 |
rip |
指令指针寄存器,保存下一条将要执行的指令地址 |
其中,rip比较特殊,它表示当前程序执行到哪里。程序每执行一条指令,rip通常会自动指向下一条指令。
在64位ARM架构中,通用寄存器通常为x0到x30。常见寄存器含义如下:
| 寄存器 | 含义 |
|---|---|
x0 |
第 1 个参数,也常用于保存返回值 |
x1 |
第 2 个参数 |
x2 |
第 3 个参数 |
x3 |
第 4 个参数 |
x4 |
第 5 个参数 |
x5 |
第 6 个参数 |
x6 |
第 7 个参数 |
x7 |
第 8 个参数 |
sp |
栈指针 |
x29 / fp |
帧指针 |
x30 / lr |
链接寄存器,保存函数返回地址 |
pc |
程序计数器,保存当前执行位置 |
在 32 位 ARM 架构中,常见通用寄存器为 r0 到 r15。
常见寄存器含义如下:
| 寄存器 | 含义 |
|---|---|
r0 |
第 1 个参数,也常用于保存返回值 |
r1 |
第 2 个参数 |
r2 |
第 3 个参数 |
r3 |
第 4 个参数 |
r13 / sp |
栈指针 |
r14 / lr |
链接寄存器,保存函数返回地址 |
r15 / pc |
程序计数器 |
在ARM 32位架构中,前4个参数通常通过r0、r1、r2、r3传递。
1、通过寄存器查看函数参数
在Linux x86-64平台下,函数调用时,前6个整型或指针类型参数通常通过寄存器传递。
例如有如下函数:
cpp
void test(int a, int b, int c)
{
printf("%d %d %d\n", a, b, c);
}
当程序刚进入 test 函数时,参数可能存放在如下寄存器中:
a -> rdi
b -> rsi
c -> rdx
在 GDB 中可以查看:
i r rdi
i r rsi
i r rdx
也可以使用 p 命令打印:
p $rdi
p $rsi
p $rdx
如果函数的整型或指针参数超过 6 个,多出来的参数通常会放到栈中。
例如:
cpp
void test(int a, int b, int c, int d, int e, int f, int g)
{
printf("%d\n", g);
}
前 6 个参数通常存放在寄存器中:
a -> rdi
b -> rsi
c -> rdx
d -> rcx
e -> r8
f -> r9
第7个参数g通常会通过栈传递。
可以结合rsp或rbp查看栈上的数据:
x/8gx $rsp
该命令表示从当前栈顶地址开始,查看 8 个 8 字节数据,并以十六进制显示。
2、寄存器中有时候保存的不是普通整数,而是一个地址。例如函数参数是指针时,该指针的地址可能会存放在 rdi、rsi 等寄存器中。
例如:
void print_str(char *str)
{
printf("%s\n", str);
}
当程序进入print_str函数时,第一个参数str通常存放在rdi寄存器中。
可以先查看rdi的值:
p/x $rdi
如果rdi中保存的是字符串地址,可以使用x/s查看该地址处的字符串:
x/s $rdi
如果rdi指向的是一个整型变量,可以使用:
x/d $rdi
如果想查看该地址处的原始字节,可以使用:
x/16xb $rdi
因此,在没有调试符号的情况下,可以通过寄存器先找到参数地址,再结合x命令查看内存内容。
3.2 修改寄存器的值
在GDB中,可以通过修改寄存器的值来改变程序运行状态。常见用途包括修改函数返回值、修改函数参数、跳转到指定指令位置等。
1、修改寄存器的基本格式如下:
set $寄存器名 = 新值
例如修改 rax 寄存器:
set $rax = 100
查看修改结果:
p $rax
如果想修改第一个参数寄存器 rdi:
set $rdi = 10
如果当前函数还没有使用或保存这个参数,那么后续程序可能会按照新的参数值继续执行。
2、修改pc/rip寄存器改变程序执行流程
pc/rip(program counter)寄存器,保存程序下一条需要执行的指令,通过修改pc寄存器来改变程序执行的流程
如果修改rip,就可以改变程序接下来要执行的位置。
查看当前rip:
p/x $rip
修改rip:
set $rip = 0x地址
或者使用通用的$pc:
set $pc = 0x地址
例如:
set $rip = 0x4011a0
或者使用
set var $rip=0x4011a0
p $rip=0x4011a0
这表示让程序下一步从地址0x4011a0开始执行。
需要注意,直接修改 $rip 或 $pc 是比较危险的操作。因为程序的栈、寄存器、局部变量等上下文可能并没有准备好,强行跳转到某个位置后,程序可能崩溃。
那么我们如何知道某一行代码对应的指令地址呢?
如果程序编译时带有调试信息,可以使用info line查看某一行源码对应的地址范围。
info line 行号
例如:
info line 20
也可以指定文件名:
info line main.c:20
可能得到类似结果:
Line 20 of "main.c" starts at address 0x401156 <main+32>
and ends at 0x401160 <main+42>.
这说明 main.c 第 20 行代码对应的汇编指令从地址 0x401156 开始,到 0x401160 之前结束。
如果想跳转到这一行对应的地址,可以使用:
set $rip = 0x401156
除了使用info line,也可以使用disassemble命令查看函数的汇编代码地址信息。
反汇编当前函数:
disassemble
反汇编指定函数:
disassemble main
简写形式:
disas main
如果只想查看当前执行位置附近的指令,也可以使用:
x/10i $rip
这在没有源码、没有调试符号,或者调试崩溃现场时非常有用。