如何使用GDB调试内核

前言

  GDB是一个功能强大的开源调试工具,能够帮助您更好的诊断和修复程序中的错误。   它提供了一套丰富的功能,使您能够检查程序的执行状态、跟踪代码的执行流程、查看和修改变量的值、分析内存状态等。它可以与编译器配合使用,以便您在调试过程中访问程序的调试信息。

  此教程将告诉您如何在DragonOS中使用rust-gdb来调试内核,包括如何开始调试以及相应的调试命令。

备注

如果您已经熟悉了rust-gdb的各种命令,那您只需要阅读此教程的第一部分即可。


1.从何开始

1.1 准备工作

  在您开始调试内核之前,需要在/Kernel/Cargo.toml中开启调试模式,将Cargo.toml中的debug = false更改为debug = true

debug = false

  更改为

debug = true

1.2 运行DragonOS

  准备工作完成后,您就可以编译、运行DragonOS来开展后续的调试工作了。   在DragonOS根目录中开启终端,使用make run即可开始编译运行DragonOS,如需更多编译命令方面的帮助,详见

1.3 运行GDB

  当DragonOS开始运行后,您就可以启动GDB开始调试了。

  您只需要开启一个新的终端,运行make gdb即可运行GDB调试器。

 make gdb
rust-gdb -n -x tools/.gdbinit
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

--Type <RET> for more, q to quit, c to continue without paging--

备注

若出现以上信息,输入c再回车即可。


2.调试

2.1 开始

  当以上步骤完成后,就已经可以开始调试了。

For help, type "help".
Type "apropos word" to search for commands related to "word".
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0xffff8000001f8f63 in ?? ()
(gdb)

备注

GDB输出的信息中0xffff8000001f8f63 in ?? ()表明DragonOS还在引导加载的过程中。

  输入continue或者c,程序将继续执行。

For help, type "help".
Type "apropos word" to search for commands related to "word".
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0xffff8000001f8f63 in ?? ()
(gdb) continue
Continuing.

  在DragonOS运行时,您可以随时按下Ctrl+C来发送中断信息。来查看内核当前状态。

(gdb) continue
Continuing.
^C
Thread 1 received signal SIGINT, Interrupt.
0xffff800000140c21 in io_in8 (port=113) at common/glib.h:136
136         __asm__ __volatile__("inb   %%dx,   %0      \n\t"
(gdb) 

2.2 设置断点和监视点

  设置断点和监视点是程序调试中最基础的一步。

  • 设置断点

  您可以使用break或者b命令来设置断点。

  关于break或者b命令的使用:

b <line_number> #在当前活动源文件的相应行号打断点

b <file>:<line_number> #在对应文件的相应行号打断点

b <function_name> #为一个命名函数打断点
  • 设置监视点

  您可以使用watch命令来设置监视点

watch <variable> # 设置对特定变量的监视点,将在特定变量发生变化的时候触发断点

watch <expression> # 设置对特定表达式的监视点,比如watch *(int*)0x12345678会在内存地址0x12345678处
                   # 的整数值发生更改时触发断点。
  • 管理断点与监视点

  当我们打上断点之后,我们该如何查看我们所有的断点信息呢?

  您可以通过info binfo break或者info breakpoints来查看所有的断点信息:

(gdb) b 309
Breakpoint 12 at 0xffff8000001f8f16: file /home/heyicong/.cargo/registry/src/mirrors.tuna.tsinghua.edu.cn-df7c3c540f42cdbd/thingbuf-0.1.4/src/lib.rs, line 315.
(gdb) watch slots
Watchpoint 13: slots
(gdb) info b
Num     Type           Disp Enb Address            What
12      breakpoint     keep y   0xffff8000001f8f16 in thingbuf::Core::pop_ref<u8> 
                                                   at /home/heyicong/.cargo/registry/src/mirrors.tuna.tsinghua.edu.cn-df7c3c540f42cdbd/thingbuf-0.1.4/src/lib.rs:315
13      watchpoint     keep y                      slots
(gdb) 

  以上信息中,编号为12的断点即是我们在活动源文件309行打的断点,若其Address<MULTIPLE>,则表示在多个地址上存在相同的断点位置。这在循环中是非常常见的情况。编号为13的便是我们对slots变量设置的监视点。

  我们可以通过以下命令对断点或者监视点进行操作:

delete <breakpoint#> # 或 d <breakpoint#> 删除对应编号的断点,在您不再需要使用这个断点的时候可以通过此命令删除断点
delete <watchpoint#> # 或 d <watchpoint##> 删除对应编号的监视点,在您不再需要使用这个监视点的时候可以通过此命令删除监视点

disable <breakpoint#> # 禁用对应编号的断点,这适合于您只是暂时不需要使用这个断点时使用,当您禁用一个断点,下
                      # 次程序运行到该断点处将不会停下来
disable <watchpoint#> # 禁用对应编号的监视点,这适合于您只是暂时不需要使用这个监视点时使用

enable <breakpoint#> # 启用对应编号的断点
enable <watchpoint#> # 启用对应编号的监视点

#clear命令
clear # 清除当前活动源文件的断点以及监视点
clear <point_number> # 清除对应编号的所有断点或监视点,这与delete行为是一致的
clear <file> # 清除指定文件的所有断点与监视点

2.3 变量和内存查看

  • print 和 display

  您可以通过print或者p来打印变量值。

  print命令用于打印变量或表达式的值。它允许您在调试过程中查看程序中的数据。

print <variable> # 打印对应变量名的值,例如:print my_variable 或者 p my_variable

print <expression> # 打印合法表达式的值,例如:print a+b 或者 p a+b

# 示例输出
(gdb) print order
$3 = core::sync::atomic::Ordering::SeqCst

备注

如果您不仅想打印值,还想显示更多详细信息(例如类型信息),可以使用ptype命令。

  您可以使用display命令来持续追踪变量或者表达式,display命令用于设置需要持续跟踪并在每次程序停止时显示的表达式。它类似于print命令,但与print不同的是,display命令在每次程序停止时自动打印指定表达式的值,而无需手动输入命令。

display <variable> # 打印对应变量名的值,例如:display my_variable

display <expression> # 打印合法表达式的值,例如:display a+b

# 示例输出
(gdb) display order
1: order = core::sync::atomic::Ordering::SeqCst #其中1表示display编号,
                                                #您可以通过info display命令来查看所有display编号

备注

一旦您设置了display命令,每当程序停止(例如,在断点处停止)时,GDB将自动打印指定表达式的值。

display命令非常有用,因为它允许您在调试过程中持续监视表达式的值,而无需每次都手动输入print命令。它特别适用于那些您希望持续跟踪的变量或表达式。

  要取消已设置的display命令并停止自动显示表达式的值,可以使用undisplay命令:

undisplay <display编号> # 如果不指定<display编号>,则将取消所有已设置的display命令,
                       # 您可以通过info display命令来查看所有display编号

备注

请注意,print和display命令只会在程序暂停执行时评估变量或表达式的值。如果程序正在运行,您需要通过设置断点或使用其他调试命令来暂停程序,然后才能使用print命令查看数据的值,display命令设置的值将会在程序暂停时自动输出。

  • 输出格式

  您可以设置输出格式来获取更多您需要的信息,例如:print /a var

参考至GDB Cheat Sheet

Format
a Pointer.
c Read as integer, print as character.
d Integer, signed decimal.
f Floating point number.
o Integer, print as octal.
s Try to treat as C string.
t Integer, print as binary (t = „two“).
u Integer, unsigned decimal.
x Integer, print as hexadecimal.

2.4 查看调用堆栈

  • 查看调用栈

  当程序在断点处暂停时,应该怎样追踪程序行为呢?

  您可以通过backtarce命令来查看调用栈。backtrace命令用于打印当前调用栈的回溯信息。它显示了程序在执行过程中所有活动的函数调用链,包括每个函数的名称、参数和源文件中的行号。

# 示例输出
(gdb) backtrace 
#0  function1 (arg1=10, arg2=20) at file1.c:15
#1  function2 () at file2.c:25
#2  xx () at xx.c:8

  每一行回溯信息都以#<frame_number>开头,指示帧的编号。然后是函数名和参数列表,最后是源文件名和行号。 通过查看回溯信息,您可以了解程序在哪些函数中执行,以及每个函数在调用栈中的位置。这对于调试程序和定位问题非常有用。

  • 切换堆栈

  您可以通过frame或者f命令来切换对应的栈帧获取更多信息以及操作。

frame <frame_number>
f <frame_number>

  除了简单地执行backtrace命令,还可以使用一些选项来自定义回溯信息的输出。例如:

backtrace full                          #显示完整的符号信息,包括函数参数和局部变量。
backtrace <frame_count>                 #限制回溯信息的帧数,只显示指定数量的帧。
backtrace <frame_start>-<frame_end>     #指定要显示的帧范围。
backtrace thread <thread_id>            #显示指定线程的回溯信息。

2.5 多核心

  在调试内核时,您可能需要查看各个核心的运行状态。

  您可以通过info threads命令来查看各个核心的运行状态

(gdb) info threads 
  Id   Target Id                    Frame 
  1    Thread 1.1 (CPU#0 [halted ]) 0xffff800000140a3e in Start_Kernel () at main.c:227
* 2    Thread 1.2 (CPU#1 [running]) thingbuf::Core::pop_ref<u8> ()
    at /home/heyicong/.cargo/registry/src/mirrors.tuna.tsinghua.edu.cn-df7c3c540f42cdbd/thingbuf-0.1.4/src/lib.rs:315
(gdb) 

  您可以使用thread <thread_id>命令切换到指定的核心上下文,以便查看和调试特定核心的状态。例如:

(gdb) thread 1
[Switching to thread 1 (Thread 1.1)]
#0  0xffff800000140a3e in Start_Kernel () at main.c:227
227                 hlt();

2.6 更多

  接下来,我将为您介绍更多您可能在调试中能够使用的命令:

step                #或者s,逐行执行程序,并进入到函数调用中。可以在step命令后加执行次数,例:step 3 表示要连续执行3个步骤
step <function>     #进入指定的函数,并停止在函数内的第一行。

next                #或者n,逐行执行程序,但跳过函数调用,直接执行函数调用后的下一行代码。
                    #它允许你在不进入函数内部的情况下执行代码,从而快速跳过函数调用的细节。
                    #同样,next也可以在命令后加执行次数

finish              #用于从当前函数中一直执行到函数返回为止,并停在调用该函数的地方。
                    #它允许你快速执行完当前函数的剩余部分,并返回到调用函数的上下文中。  

continue            #用于继续程序的执行,直到遇到下一个断点或
                    #程序正常结束或者程序暂停。 

quit                #退出调试                    

list                            #或者l,显示当前活动源文件源代码的片段,以及当前执行的位置。
list <filename>:<function>      #显示<filename>文件里面的<funtion>函数的源代码片段
list <filename>:<line_number>   #显示<filename>文件里面的<line_number>附近的源代码片段
list <first>,<last>             #显示当前活动源文件的<first>至<last>之间的源代码片段
set listsize <count>            #设置list命令显示的源代码行数。默认情况下,list命令显示当前行和其周围的几行代码。

info args                       #显示当前函数的参数及其值
info breakpoints                #显示断点以及监视点信息
info display                    #显示当前设置的display列表
info locals                     #显示当前函数/栈帧中的局部变量及其值
info sharedlibrary              #显示当前已加载的共享库(shared library)信息
info signals                    #显示当前程序所支持的信号信息。它可以列出程序可以接收和处理的不同信号的列表。
info threads                    #显示各个核心/线程信息,它可以列出当前正在运行的核心/线程以及它们的状态。

show directories                #显示当前源代码文件的搜索路径列表。这些搜索路径决定了GDB在查找源代码文件时的搜索范围。
show listsize                   #显示打印源代码时的上下文行数。它确定了在使用list命令(或其简写形式l)时显示的源代码行数。

whatis variable_name            #查看给定变量或表达式的类型信息。它可以帮助你了解变量的数据类型。
ptype                           #显示给定类型或变量的详细类型信息。它可以帮助你了解类型的结构和成员。
                                #相较于whatis命令,ptype命令更加详细。

set var <variable_name>=<value> #设置变量值

return <expression>             #强制使当前函数返回设定值                                                  

最后

  现在,您已经可以使用rust-gdb来调试DragonOS内核代码了。

您可以参阅GDB命令文档来获取更多帮助:GDB Cheat Sheet