一、变量查看与修改在使用 GDB 调试程序时除了控制程序的运行流程以外最常见的操作就是查看变量的值、查看参数的值以及在调试过程中临时修改某些变量的值。通过这些命令可以观察程序运行时的数据变化从而判断程序逻辑是否符合预期。1.1 查看函数参数使用info args(i args)命令可以查看当前函数传入参数的值包括main函数的argv与argc参数。i args例如当前程序停在某个函数内部void test(int a, int b) { int sum a b; }在断点处执行i args就可以看见传入的a和b变量的值除了使用i args也可以直接使用print(p)命令打印某个参数的值p a p b1.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显示的信息详细适合查看结构体、指针、数组等复杂类型。例如有如下结构体定义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非常有用可以帮助我们快速确认变量的数据类型以及内部成员组成。whatis命令可以用来查看结构体类派生类普通函数以及类成员函数ptype命令可以更加详细的查看这些内容如果显示一个类ptype会把类的成员变量和成员函数都显示出来i variables命令用来查看全局变量或者静态变量比如我们看见一个变量使用但是不知道该变量是在哪定义的就可以使用这个命令ptype /m //只显示成员变量不显示成员函数 ptype /t //不现实typedef的内容 ptype /o //显示结构体或者类的内存构造以及大小例如如果我们对结构体占用内存大小进行优化可以使用ptype命令对结构体进行查看查看他对内存空间的利用率(gdb) ptype /o node /* offset | size */ type struct NODE { /* 0 | 4 */ int ID; /* 4 | 4 */ int age; /* 8 | 1 */ char gender; /* XXX 7-byte hole */ /* 16 | 8 */ NODE *next; /* 24 | 4 */ int test; /* 28 | 1 */ char c; /* XXX 3-byte padding */ /* total size (bytes): 32 */ }可以看见对于上面的结构体所有成员直接加起来才22个字节但是使用不合理的排布之后会用32个字节才能存下调整结构体之后可以优化内存布局到24个字节(gdb) ptype /o node2 /* offset | size */ type struct NODE2 { /* 0 | 4 */ int ID; /* 4 | 4 */ int age; /* 8 | 8 */ NODE *next; /* 16 | 4 */ int test; /* 20 | 1 */ char gender; /* 21 | 1 */ char c; /* XXX 2-byte padding */ /* total size (bytes): 24 */ }i variables 变量的正则表达可以查看对应的变量是在哪个文件中定义的。1.3 查看数组、字符串、结构体和指针1、查看数组对于普通数组可以直接打印数组名p arr也可以查看数组中的某一个元素p arr[0] p arr[1]如果是指针指向一段连续内存可以使用下面的方式查看多个元素p *ptr10这表示从ptr指向的位置开始连续打印 10 个元素。例如int arr[5] {1, 2, 3, 4, 5}; int *ptr arr;在 GDB 中执行p *ptr5可能输出$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例如struct Student { char name[32]; int age; }; struct Student stu {Tom, 18};在 GDB 中可以执行p stu p stu.name p stu.age使用以下命令可以使得在直接打印结构体变量的时候可以让结构体的每一个成员独占一行set print pretty4、查看指针如果变量是结构体指针需要使用-访问成员p node-data p node-next如果想查看指针指向的整个结构体内容可以使用p *node这在调试链表、树、队列等数据结构时非常常用。1.4 查看局部变量使用info locals(i locals)命令可以查看当前函数中的局部变量。i locals例如void test(int a, int b) { int sum a b; int count 100; }当程序停在test函数内部时执行i locals会把当前函数中的所有局部变量的值打印出来sum 30 count 1001.5 表达式与函数调用在GDB中p命令不仅可以打印普通变量也可以计算表达式的值甚至可以调用程序中的函数或库函数。例如可以使用sizeof查看类型或变量所占的字节数p sizeof(int) p sizeof(long) p sizeof(char *) p sizeof(变量名)如果程序中有如下变量int num 10; char name[32] hello;在 GDB 中执行p sizeof(num) p sizeof(name)可能得到$1 4 $2 32其中sizeof(num)表示变量num所占的字节数sizeof(name)表示整个数组name所占的字节数。除了sizeof也可以在 GDB 中调用一些函数例如strlenp 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可以看到变量的值已经发生变化。这种方式在调试条件判断时非常有用。例如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 *pstu5、修改结构体中的字符串如果结构体中的成员是字符数组例如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反汇编指令显示常见单位大小单位含义bbyte1 字节hhalf word2 字节wword4 字节ggiant word8 字节例如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 \0003、查看结构体的内存布局例如有如下结构体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程序计数器