GDB调试变量、内存与寄存器查看与修改

一、变量查看与修改

在使用 GDB 调试程序时,除了控制程序的运行流程以外,最常见的操作就是查看变量的值、查看参数的值,以及在调试过程中临时修改某些变量的值。通过这些命令,可以观察程序运行时的数据变化,从而判断程序逻辑是否符合预期。

1.1 查看函数参数

使用info args(i args)命令可以查看当前函数传入参数的值,包括main函数的argvargc参数。

复制代码
i args

例如,当前程序停在某个函数内部:

cpp 复制代码
void test(int a, int b)
{
    int sum = a + b;
}

在断点处执行:

复制代码
i args

就可以看见传入的ab变量的值

除了使用 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 = &num;

可以通过指针修改它指向的变量:

复制代码
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/4xbx/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架构中,通用寄存器通常为x0x30。常见寄存器含义如下:

寄存器 含义
x0 第 1 个参数,也常用于保存返回值
x1 第 2 个参数
x2 第 3 个参数
x3 第 4 个参数
x4 第 5 个参数
x5 第 6 个参数
x6 第 7 个参数
x7 第 8 个参数
sp 栈指针
x29 / fp 帧指针
x30 / lr 链接寄存器,保存函数返回地址
pc 程序计数器,保存当前执行位置

在 32 位 ARM 架构中,常见通用寄存器为 r0r15

常见寄存器含义如下:

寄存器 含义
r0 第 1 个参数,也常用于保存返回值
r1 第 2 个参数
r2 第 3 个参数
r3 第 4 个参数
r13 / sp 栈指针
r14 / lr 链接寄存器,保存函数返回地址
r15 / pc 程序计数器

在ARM 32位架构中,前4个参数通常通过r0r1r2r3传递。


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通常会通过栈传递。

可以结合rsprbp查看栈上的数据:

复制代码
x/8gx $rsp

该命令表示从当前栈顶地址开始,查看 8 个 8 字节数据,并以十六进制显示。


2、寄存器中有时候保存的不是普通整数,而是一个地址。例如函数参数是指针时,该指针的地址可能会存放在 rdirsi 等寄存器中。

例如:

复制代码
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

这在没有源码、没有调试符号,或者调试崩溃现场时非常有用。

相关推荐
Zevalin爱灰灰16 小时前
JTAG/SWD是什么
gdb·调试
ttkwzyttk14 天前
GDB TUI窗口管理
gdb
ttkwzyttk14 天前
GDB观察点与捕获点使用
gdb
ttkwzyttk20 天前
GDB函数调用栈管理
gdb
ttkwzyttk21 天前
GDB调试简介与调试配置
gdb
modelmd1 个月前
GDB 摘要
gdb
源分享1 个月前
GDB下载和安装保姆级教程
gdb
modelmd2 个月前
翻译 GDB 官方文档
gdb
kidwjb2 个月前
一次多进程信号量同步失效的排查实录
gdb·进程通信·信号量