5

Debugging with GDB: 查看数据

 2 years ago
source link: http://www.kongjunblog.xyz/2021/04/debugging-with-gdb.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Debugging with GDB: 查看数据

Debugging with GDB: 查看数据

在C/C++中,变量遵守作用域规则,有不同的生存期、链接类型。不同的函数中可能有相同的变量名,不同的文件中也可能有相同的文件名,必须无歧义地向GDB制定变量名。

具有外部链接的符号

非static的全局变量具有外部链接,全局只有一个,所以可以直接使用print命令打印。

比如打印全局变量global

xxxxxxxxxx
p global

具有内部链接的符号

非static局部变量

非static局部变量只在作用域内可见、生存期也只在作用域内,只有变量在作用域内时才能使用GDB查看。

同名局部变量可以出现在不同函数中,因此在查看局部变量时要指定栈帧,如果不指定就使用当前栈帧。

指定变量的方法为:

  • function::variable
  • 使用frame命令切换到变量查看栈帧,再使用print

比如以下程序varaible.c

xxxxxxxxxx
void func2(int x)
{
    x = 100;
}
void func1(int x) {
    x += 5;
    func2(x);
}
int
main(int argc, char *argv[])
{
    int x = 20;
    func1(x);
    return 0;
}
xxxxxxxxxx
gcc -O0 -ggdb -o variable variable.c

使用GDB调试,在进入func1main中的变量x

xxxxxxxxxx
Breakpoint 1, func1 (x=20) at /home/GDB/variable.c:7
7               x += 5;
(gdb) n
8               func2(x);
(gdb) p x
$2 = 25
(gdb) p main::x
$3 = 20
(gdb) bt
#0  func1 (x=25) at /home/kongjun/c_practice/variable.c:8
#1  0x0000555555555172 in main (argc=1, argv=0x7fffffffdfc8) at /home/GDB/variable.c:15
(gdb) f 1
#1  0x0000555555555172 in main (argc=1, argv=0x7fffffffdfc8) at /home/GDB/variable.c:15
15              func1(x);
(gdb) p x
$4 = 20

要避免在刚进入栈帧或即将退出栈帧时查看非static局部变量。在机器指令层面,建立栈帧和初始化变量一般由多条指令完成,如果在这时查看变量,很可能变量的值还没有被设置好,查看到错误的值,退出栈帧时同理。

static变量

static的全局变量和局部变量具有内部链接,一般存储在.data段或.bss段中。虽然static变量也只在作用域内可见,但是却存在于整个程序运行期间,鉴于这个特性,GDB允许在static局部变量所在作用域内查看变量。

不同文件中可能有同名的static变量,所以必须指明变量所在的文件或函数。指示变量位置的方法如下:

  • function::variable
  • file::variable'file'::variable

使用'file'::variable可以避免文件名和函数名冲突的情况。

在编译程序时,编译器很可能会对变量或它所在的函数进行优化,比如消除不必要的函数调用、将循环,等,这是可能无法查看到变量真正的值,甚至无法查看到变量。上面的variable.c就是一个例子,变量x并没有真的被使用,在使用O2级别的优化编译时,变量x和函数func1func2会被直接消除,main直接返回。

如果要查看变量的真实值,要么禁止优化,要么使用某些支持查看变量真实值的调试格式。

在使用::记号时要小心和C++中的作用域运算符冲突,如果真的发生了冲突,GDB优先使用C++作用域运算符的语义。

查看数组的值

对于数组类型,GDB分析它的符号信息后获取了它的起始地址、长度,可以直接使用print命令查看。某些指针指向某个元素,但它实际上代表一个数组,GDB不知道它代表一个数组,无法直接使用print命令查看,这时需要我们手动地将其转换为数组。方法如下:

  • 使用类型转换
  • element@lenelement代表数组第一个元素,@len代表数组元素个数。

arry.c

xxxxxxxxxx
int
main(int argc, char *argv[])
{
    int arry[] = {1, 2, 3, 4, 5, 6};
    int *p = arry;
    return 0;
}

使用GDB调试:

xxxxxxxxxx
(gdb) n
6               int *p = arry;
(gdb) p arry
$2 = {1, 2, 3, 4, 5, 6}
(gdb) n
8               return 0;
(gdb) p p
$3 = (int *) 0x7fffffffdec0
(gdb) p *p
$4 = 1
(gdb) p *p@6
$5 = {1, 2, 3, 4, 5, 6}
(gdb) p *p@9
$6 = {1, 2, 3, 4, 5, 6, -8512, 32767, 1431654768}

指针p指向数组第一个元素,实际上代表数组arry,但是被指向对象的类型却是int,GDB无法知道它代表数组。

查看/查找内存

查看内存指令为examine(x)。格式为:x /nfu,其中n是要重复的次数,f是打印格式,u是单元大小。具体的值参考Examining data

GDB 除了查看内存,还提供了在内存中查找某个值的命令find。语法为:

xxxxxxxxxx
find [/sn] start_addr, +len, val1 [, val2, …]
find [/sn] start_addr, end_addr, val1 [, val2, …]

其中s是查找的变量的大小(b,h,w,g),n是匹配的最大个数。find命令可以根据查找的变量类型自动判断大小,因此不需要指定s,直接find即可,这样可以实现查找不同大小的变量。

GDB 提供了变量$_,可以通过它访问到find匹配到的最后一个地址。

程序如下:

xxxxxxxxxx
void
hello ()
{
  static char hello[] = "hello-hello";
  static struct { char c; short s; int i; }
    __attribute__ ((packed)) mixed
    = { 'c', 0x1234, 0x87654321 };
  printf ("%s\n", hello);
}

GDB 调试:

xxxxxxxxxx
(gdb) find &hello[0], +sizeof(hello), "hello"
0x804956d <hello.1620+6>
1 pattern found
(gdb) find &hello[0], +sizeof(hello), 'h', 'e', 'l', 'l', 'o'
0x8049567 <hello.1620>
0x804956d <hello.1620+6>
2 patterns found.
(gdb) find &hello[0], +sizeof(hello), {char[5]}"hello"
0x8049567 <hello.1620>
0x804956d <hello.1620+6>
2 patterns found.
(gdb) find /b1 &hello[0], +sizeof(hello), 'h', 0x65, 'l'
0x8049567 <hello.1620>
1 pattern found
(gdb) find &mixed, +sizeof(mixed), (char) 'c', (short) 0x1234, (int) 0x87654321
0x8049560 <mixed.1625>
1 pattern found
(gdb) print $numfound
$1 = 1
(gdb) print $_
$2 = (void *) 0x8049560

每次使用print打印变量的值,都会将该次打印的值记录在变量历史中,每次打印都有一个编号,这就是为什么print会输出类似$1 = 100的原因,其中$1中的1就是变量历史编号。

可以使用show variables查看近 10 次变量历史,show variables n查看以历史编号 n 为中心的变量历史,show variables +打印上次print之前的 10 此变量历史。

也可以使用$$$$N$N来打印变量历史中的值。$表示最近一次print$$表示上上次print$N表示编号为N的变量历史,$$N表示从上次print开始的变量历史号。如$$0等同于$(上次print),$$2前 3 次print

convenience variable

convenience variable 主要有两个用途:

  • 访问某些 GDB 记录的信息
  • 定义变量利用 GDB 命令实现自己需要的功能

先介绍 GDB 记录的信息:

  • $_exitsignal: 杀死进程的信号。函数$_isvoid()可以判断进程是否被信号杀死。
  • $_exitcode: 进程退出码
  • $_thread: 进程编号
  • $_gthread: 全局进程编号

还有其他一些变量,可以参考 GDB 使用手册。

还可以利用 convenience variable 自动化一些操作。比如有一个指针数组,其中的指针指向对象,现在需要查看数组中指针指向的对象,使用 convenience variable 可以轻易完成这个任务:

xxxxxxxxxx
set $i = 0
p *array[$i++]

然后按不断按回车即可遍历数组并查看指针指向的对象。

info registers查看通用寄存器,info all-registers查看包括浮点数寄存器在内的寄存器。

print $<register>可以查看寄存器register的值,如访问 RISCV 寄存器 a0:print $a0

将内存/变量拷贝到文件中

有时可能想要比较多次运行过程中某变量或某块内存的值,GDB 没有直接提供这种功能,这时可以将内存/变量拷贝到文件中再比较。

xxxxxxxxxx
dump [format] memory filename start_addr end_addr
dump [format] value filename expr
append [binary] memory filename start_addr end_addr
append [binary] value filename expr
restore filename [binary] bias start end

dump命令将内存/变量写入到文件filename中,append将内存/变量附加到文件中,restore命令将文件中的内容恢复到内存中。

文件格式必须是以下之一:

  • binary

    Raw binary form.

  • ihex

    Intel hex format.

  • srec

    Motorola S-record format.

  • tekhex

    Tektronix Hex format.

  • verilog

    Verilog Hex format.

如果不指定格式,dumpappend默认写入 binary 数据,实际上append暂时只支持 binary。restore可以自动判断格式,但由于 binary 无格式,必须手动指定。其他几种格式文件中以及记录了固定的地址,restore时不需要指定bias,binary 文件地址总是从 0 开始,因此restore filename binary bias start end实际上会将文件中的内容写入进程地址bias

生成 core file

generate-core-file [file]gcore [file]生成 core dump,如果不指定文件名,生成的文件名为core.pid

改善 GDB 的输出

GDB 默认print输出是最简的,部分内容不会被打印,并且没有格式化。可以通过设置一定的选项,改善输出,这里列出几个我个人觉得比较有用的选项:

  • set print object on: 当打印指向对象的指针时,显示其真实类型
  • set print array on: 用更好的格式打印数组,但是需要占用更多空间
  • set print pretty on: 打印结构体/类时使用缩进
  • set print vtbl on: 打印 C++ 虚函数表

C++

此博客中的热门博文

使用 Vim 搭建 C/C++ 开发环境

刚接触 Vim 的同学往往因为无法搭建开发环境而“从入门到放弃”,本文旨在帮助这些同学搭建开发环境,聚焦于最核心的开发需求,忽略换配色调字体之类的细枝末节。如果需要开箱即用的 vim 配置(发行版),可以使用 Spacevim 。 本文使用 neovim-nightly,但也适用于 Vim 8.2+,不需要读者有任何 VimL 基础,以 C/C++ 为例,但应该适用于任何语言。   插件管理 在 Vim 中,插件只是一些脚本,存放在特定的目录中,运行时将它们所在的目录加入到 runtimepath 中。Vim 8 内置了插件管理功能,但不支持高级的插件管理功能。Vimmers 实现了多个插件管理器,可以自动下载、更新、安装插件,还可以延迟加载、按需加载,提高启动速度。 上古时期流行手动或使用 Vundle 管理插件,以上两种方式已经落伍了,这里介绍目前比较流行的三个插件管理器: vim-plug :简单易用高效,是目前最流行的插件管理器 dein.vim :功能强大,但使用复杂 vim-pathogen :另一款流行的插件管理器,没有用过不做评价 以上三款插件管理器风格各不相同,都有大量用户,功能相当完善,根据自己的喜好选取即可。推荐新手选择 vim-plug,对启动时间特别敏感的同学可以考虑 dein.vim,我的配置在安装 70 余个插件的情况下,启动仅需 60 余秒。 使用 vim-plug 安装插件只需要在 .vimrc 中写入以下代码: call plug#begin('~/.vim/plugged') " 括号里面是插件目录                                 " 只能在 plug#begin() 和 plug#end() 之间写安装插件的命令 Plug 'junegunn/vim-easy-align'   " 用户名/插件名,默认从 github 下载安装 Plug 'https://github.com/junegunn/vim-github-dashboard.git' " 从特定 URL 下载安装 call plug#end() 使用 dein.vim 安装插件: ​ x set runtimepath+=

Ibex 架构介绍

  Ibex 是什么? Ibex was initially developed as part of the PULP platform under the name "Zero-riscy" , and has been contributed to lowRISC who maintains it and develops it further. It is under active development. Ibex 是一个产品级的 32 位开源 RISC-V 处理器,使用 SystemVerilog 编写,麻雀虽小(11000 行左右),五章俱全。支持 RV32I、RV32C、RV32M、RV32B 等拓展,支持了 M-Mode 和 U-Mode,完整实现了 RISC-V 指令集规定的控制状态寄存器、中断异常、调试支持等,适用于嵌入式系统。 总体架构如下: 流水线 Ibex 默认使用两级流水线,但也支持三级流水线(实验性特性)。两级流水分别为: 取值(IF):通过预取缓冲区(prefetch buffer)从内存中取值,可以一个周期取一条指令,只要指令侧内存支持。 译码/执行(ID/EX):译码并立即执行,所有的操作,包括寄存器读写、内存访问都在该阶段进行。 Ibex 支持多周期指令,每条指令都至少需要两个周期才能通过流水线,周期数更大的指令将导致流水线停顿多个周期。指令类型及其停顿周期如下: Instruction Type Stall Cycles Description Integer Computational 0 Integer Computational Instructions are defined in the RISCV-V RV32I Base Integer Instruction Set. CSR Access 0 CSR Access Instruction are defined in ‘Zicsr’ of the RISC-V specification. Load/Store 1 - N Both loads and stores stall for at least one cycle to await a response. For loads this response is t

UNIX 进程关系

进程的起源 BSD 终端登录 网络登录 进程与进程组 会话与控制终端 作业控制 孤儿进程和孤儿进程组 总结 UNIX是分时系统,同时运行着多个进程,进程之间相互联系,形成了进程组、会话等进程关系,这些进程关系会影响某些函数/系统调用和信号的行为。 进程的起源 所有的进程都有一共同的起源,加电开机启动操作系统并登录(获取 login shell )就是用户进程的起始 1 。这里介绍传统的UNIX登录机制。 UNIX登录的过程一般分为两种: 终端登录( terminal login ) 网络登录( network login ) 终端登录就是在本地计算机中启动操作系统并取得 login shell,比如没有安装桌面环境(KDE、GNOME等)的 GNU/Linux 系统,启动后就是命令行,要求输入用户名和密码,验证通过后就会取得 login shell。 网络登录就是通过网络登录远程计算机取得 login shell,比如腾讯云、阿里云的 Linux 服务器,登录后就是命令行黑框框,就好像是在本地登录的一样。 BSD 终端登录 启动 init 进程,init 进程可能会读取终端相关的配置文件,如 /etc/ttys 等。 为每个终端 fork 出一个子进程,并使用 exec() 函数执行 getty() 例程。 getty() 例程打开终端设备(标准输出、标准输入、标准错误)、读取用户名、初始化环境变量,并使用 exec() 系列函数执行login 例程。 login 例程接收密码,如果验证成功就修改工作目录到家目录、修改终端所有权、设置用户信息(UID、GID等)、初始化环境变量(SHELL、PATH等)后执行 login shell。 如果验证失败,终止进程,init 进程再次执行步骤2。 通过以上步骤,用户就取得了 login shell,并且 login shell 是 init 进程的子进程(exec 系列函数只执行程序不改变进程ID)。login shell 也会读取它的配置文件来初始化自身。 BSD 的终端登录方式在 UNIX 世界有巨大的影响,被许多 UNIX 实现采用,比如 Linux 早期版本(Linux 0.12)就是用类似 BSD 的登录方式。现在终端登录复杂了很多,许多系统装有 X window,登录后是图形界面而不是字符界面。G

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK