GDB 调试

在开发过程中,出现了bug 识别 并修复,也是重要的一环。 这里 有很多 不同的技术 来实现。包括 static and dynamic analysis, code review, tracing, proffling, and interactive debugging。 我想把重点放在通过调试器观察代码执行的传统方法上,在我们的例子中是GNU Project debugger (GDB)。GDB是一个强大而灵活的工具。您可以使用它来调试应用程序,检查程序崩溃后创建的事后检查漏洞(core漏洞),甚至可以逐步检查内核代码。

一 GDB 调试概述

GDB是编译语言的源代码级调试器,主要是C和c++,尽管它也支持各种其他语言,如Go和Objective-C。您应该阅读正在使用的GDB版本的说明,以了解对各种语言的当前支持状态。
这个项目的网站是https://www.gnu.org/software/gdb/,它包含了很多有用的信息,包括GDB用户手册,用GDB调试。

二 GDB 调试准备

1. -g 调试级别

需要用调试符号编译要调试的代码。GCC为此提供了两个选项:-g和-ggdb。后者添加特定于GDB的调试信息,而前者则为您正在使用的任何目标操作系统以适当的格式生成信息,使其成为更具可移植性的选项。在我们的特殊情况下,目标操作系统始终是Linux,使用-g还是-ggdb没有什么区别。更有趣的是,这两个选项都允许您指定调试信息的级别,从0到3:

  • 0:不产生任何调试信息,相当于省略-g或ggdb开关。
  • 1:这产生最少的信息,但包括函数名和外部变量,这足以产生回溯。
  • 2:这是默认值,包含有关局部变量和行号的信息,以便您可以执行源代码级调试和单步执行代码。
  • 3:这包括额外的信息,除其他事项外,意味着GDB可以正确处理宏扩展

在大多数情况下,-g就足够了:如果您在逐步执行代码时遇到问题,特别是在代码包含宏的情况下,可以保留-g3或-ggdb3。

2. -O代码优化级别

下一个要考虑的问题是代码优化的级别。编译器优化倾向于破坏源代码行和机器码行之间的关系,这使得遍历源代码变得不可预测。如果您遇到这样的问题,您很可能需要在不进行优化的情况下进行编译,省略-O编译开关,或者使用-Og,该开关启用不会干扰调试的优化

3. 栈帧回朔

一个相关的问题是堆栈帧指针,GDB需要它来生成一个回溯函数调用到当前调用的过程。在某些体系结构上,GCC不会生成具有更高优化级别(-O2及以上)的堆栈帧指针。如果您发现自己确实需要使用-O2进行编译,但仍然希望进行回溯,则可以使用 -fno-omit-frame-pointer覆盖默认行为。
还要注意那些通过添加 -fomit-frame-pointer(您可能想要临时删除)手工优化以省略帧指针的代码

三 调试应用程序

您可以通过以下两种方式之一使用GDB调试应用程序:如果您正在开发要在桌面和服务器上运行的代码,或者实际上在同一台机器上编译和运行代码的任何环境,那么自然地运行GDB。然而,大多数嵌入式开发都是使用交叉工具链完成的,因此您想要调试设备上运行的代码,但要从拥有源代码和工具的交叉开发环境中控制它。我将重点讨论后一种情况,因为这是嵌入式开发人员最可能出现的情况,但我也将向您展示如何设置

1. 使用gdbserver进行远程调试

远程调试的关键组件是调试代理gdbserver,它运行在目标上并控制被调试程序的执行。gdbserver通过网络连接或串行接口连接到主机上运行的GDB副本。
通过gdbserver进行调试与本机调试几乎相同,但不完全相同。差异主要集中在涉及两台计算机的事实上,它们必须处于正确的状态才能进行调试。以下是一些需要注意的事项:

  • 在调试会话开始时,您需要使用gdbserver在目标上加载要调试的程序,然后分别从主机上的交叉工具链加载GDB。
  • 在调试会话开始之前,GDB和gdbserver需要相互连接。
  • 运行在主机上的GDB需要被告知在哪里查找调试符号和源代码,特别是对于共享库。
  • GDB run命令没有正常运行。
  • gdbserver将在调试会话结束时终止,如果您想要另一个调试会话,则需要重新启动它。
  • 您需要调试符号和要在主机上调试的二进制文件的源代码,而不是在目标上。通常,目标上没有足够的存储空间来存储它们,并 且在部署到目标之前需要剥离它们。
  • GDB/gdbserver组合不支持本地运行GDB的所有特性:例如,gdbserver不能在fork后跟随子进程,而本地GDB可以。
  • 如果GDB和gdbserver来自不同的版本,可能会发生奇怪的事情GDB,或者是相同的版本,但配置不同。理想情况下,它们应该使用您最喜欢的构建工具从相同的源构建

调试符号会显著增加可执行文件的大小,有时会增加10倍。正如在“构建根文件系统”中提到的,在不重新编译所有内容的情况下删除调试符号是很有用的。该任务的工具是从交叉工具链中的binutils包中剥离出来的。您可以通过以下命令进行控制:

  • strip-all:删除所有符号(默认)。
  • strip-unneeded:删除重定位处理不需要的符号。
  • strip-debug:只删除调试符号。

注意: 对于应用程序和共享库,——strip-all(默认值)是可以的,但是对于内核模块,您会发现它会阻止模块加载。用“strip-unneeded”代替。

考虑到这一点,让我们来看看使用Yocto Project和Buildroot进行调试所涉及的细节。

2. 设置用于远程调试的Yocto项目

在使用Yocto时,要远程调试应用程序,需要做两件事:您需要将gdbserver添加到目标映像中,并且您需要创建一个包含GDB的SDK,并为您计划调试的可执行文件提供调试符号。

首先,要在目标镜像中包含gdbserver,你可以通过在conf/local.conf中添加这个包来显式地添加这个包:

IMAGE_INSTALL_append = " gdbserver"

在没有串行控制台的情况下,还需要添加一个SSH守护进程,以便有办法在目标上启动gdbserver:

EXTRA_IMAGE_FEATURES ?= "ssh-server-openssh"

或者,你可以在EXTRA_IMAGE_FEATURES中添加tools-debug,这将添加gdbserver,gdb和strace到目标映像中

EXTRA_IMAGE_FEATURES ?= "tools-debug ssh-server-openssh"

对于第二部分,您只需要构建一个SDK

bitbake -c populate_sdk <image>

3. 为远程调试设置builroot

builroot不区分构建环境和用于应用程序开发的环境:没有SDK。假设你正在使用Buildroot内部工具链,你需要启用这些选项来为主机构建跨GDB并为目标构建gdbserver:

  • BR2_PACKAGE_HOST_GDB, in Toolchain | Build cross gdb for the host
  • BR2_PACKAGE_GDB, in Target packages | Debugging, profiling and benchmark | gdb
  • BR2_PACKAGE_GDB_SERVER, in Target packages | Debugging, profiling and benchmark | gdbserver

您还需要构建带有调试符号的可执行文件,您需要启用调试符号BR2_ENABLE_DEBUG,在构建选项|构建包与调试符号。这将在output/host/usr//sysroot中创建带有调试符号的库。

4. 开始调试

既然已经在目标上安装了gdbserver,并且在主机上安装了跨GDB,那么就可以启动调试会话了。

4.1 连接GDB和gdbserver

GDB和gdbserver的连接方式可以是网络连接,也可以是串口连接。在网络连接的情况下,启动gdbserver时要使用要侦听的TCP端口号,以及可选的接受连接的IP地址。在大多数情况下,您并不关心要连接哪个IP地址,因此只需提供端口号即可。在这个例子中,gdbserver等待来自任意主机的端口10000的连接:

gdbserver :10000 ./hello-world
Process hello-world created; pid = 103
Listening on port 10000

接下来,从工具链中启动GDB的副本,将其指向程序的未剥离副本,以便GDB可以加载符号表:

aarch64-poky-linux-gdb hello-world

在GDB中,使用目标远程命令与gdbserver建立连接,为其提供目标的IP地址或主机名以及它正在等待的端口:

(gdb) target remote 192.168.1.101:10000

当gdbserver看到来自主机的连接时,它打印如下内容:

当gdbserver看到来自主机的连接时,它打印如下内容:

该过程与串行连接类似。在目标上,告诉gdbserver要使用哪个串口:

# gdbserver /dev/ttyO0 ./hello-world

您可能需要事先使用stty(1)或类似的程序配置端口波特率。一个简单的例子如下:

# stty -F /dev/ttyO0 115200

在主机上,您使用目标远程和位于电缆主机端的串行设备建立到gdbserver的连接。在大多数情况下,您需要先使用GDB命令set serial baud设置主机串口的波特率

(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0

尽管GDB和gdbserver现在已经连接,但我们还没有准备好设置断点并开始逐步执行源代码。

4.2 设置 sysroot

GDB需要知道在哪里可以找到正在调试的程序和共享库的调试信息和源代码。在本地调试时,路径是众所周知的,并且是内置于GDB中的,但是在使用跨工具链时,GDB无法猜测目标文件系统的根在哪里。你必须提供这些信息。
如果使用Yocto Project SDK构建应用程序,则系统根位于
所以你可以在GDB中这样设置它:

(gdb) set sysroot /opt/poky/3.1.5/sysroots/aarch64-poky-linux

如果您正在使用builroot,您会发现sysroot位于output/host/ usr//sysroot中,并且在output/ staging中有一个指向它的符号链接。所以,对于Buildroot,你可以这样设置sysroot:

(gdb) set sysroot /home/chris/buildroot/output/staging

GDB还需要找到正在调试的文件的源代码。GDB有一个源文件的搜索路径,你可以使用show directories命令查看:

gdb) show directories
Source directories searched: $cdir:$cwd

以下是默认值: c w d 是主机上运行的 G D B 实例的当前工作目录 ; cwd是主机上运行的GDB实例的当前工作目录; cwd是主机上运行的GDB实例的当前工作目录;cdir是编译源代码的目录。后者用标记DW_AT_comp_dir编码到目标文件中。你可以使用objdump——dwarf来查看这些标签,例如:

$ aarch64-poky-linux-objdump --dwarf ./helloworld | grep DW_AT_
comp_dir
[…]
<160> DW_AT_comp_dir : (indirect string, offset: 0x244): /home/
chris/helloworld
[…]

在大多数情况下,默认值 c d i r 和 cdir和 cdircwd就足够了,但是如果目录在编译和调试之间移动,就会出现问题。Yocto项目就是这样一个例子。深入查看使用Yocto Project SDK编译的程序的DW_AT_comp_dir标签,您可能会注意到以下内容:

$ aarch64-poky-linux-objdump --dwarf ./helloworld | grep DW_AT_
comp_dir
<2f> DW_AT_comp_dir : /usr/src/debug/glibc/2.31-r0/git/csu
<79> DW_AT_comp_dir : (indirect string, offset: 0x139): /usr/
src/debug/glibc/2.31-r0/git/csu
<116> DW_AT_comp_dir : /usr/src/debug/glibc/2.31-r0/git/csu
<160> DW_AT_comp_dir : (indirect string, offset: 0x244): /home/
chris/helloworld
[…]

在这里,您可以看到对目录/usr/src/debug/ glibc/2.31-r0/git的多个引用,但是它在哪里?答案是,它在系统根目录中
所以完整路径是/opt/poky/3.1.5/sysroots/aarch64-poky-linux / usr/src/debug/glibc/2.31-r0/git。SDK包含目标映像中所有程序和库的源代码。GDB有一种简单的方法来处理像这样移动整个目录树:substitute-path。所以,在调试Yocto Project SDK时,你需要使用这些命令:

(gdb) set sysroot /opt/poky/3.1.5/sysroots/aarch64-poky-linux
(gdb) set substitute path /usr/src/debug/opt/poky/3.1.5/
sysroots/aarch64-poky-linux/usr/src/debug

您可能有存储在系统根之外的其他共享库。在这种情况下,可以使用set solib-search-path,它可以包含一个以冒号分隔的目录列表,用于搜索共享库。只有在sysroot中找不到二进制文件时,GDB才会搜索solib-search-path。告诉GDB在哪里查找库和程序的源代码的第三种方法是使用directory命令

(gdb) directory /home/chris/MELP/src/lib_mylib
Source directories searched: /home/chris/MELP/src/lib_
mylib:$cdir:$cwd

以这种方式添加的路径具有优先级,因为它们在来自sysroot或solib-search-path的路径之前被搜索。

4.3 GDB 命令文件

每次运行GDB时都需要做一些事情,例如设置sysroot。将这些命令放入命令文件中并在每次启动GDB时运行它们是很方便的。GDB从 H O M E / 中读取命令。 G d b i n i t ,然后 f r o m . g d b i n i t ,然后从命令行中使用 − x 参数指定的文件中删除。但是,最新版本的 G D B 会出于安全原因拒绝从当前目录加载 . g d b i n i t 。你可以通过在 HOME/中读取命令。Gdbinit,然后from .gdbinit,然后从命令行中使用-x参数指定的文件中删除。但是,最新版本的GDB会出于安全原因拒绝从当前目录加载.gdbinit。你可以通过在 HOME/中读取命令。Gdbinit,然后from.gdbinit,然后从命令行中使用x参数指定的文件中删除。但是,最新版本的GDB会出于安全原因拒绝从当前目录加载.gdbinit。你可以通过在HOME/.gdbinit中添加一行来覆盖该行为:

set auto-load safe-path /

或者,如果你不想启用全局自动加载,你可以像这样指定一个特定的目录:

add-auto-load-safe-path /home/chris/myprog

我的个人偏好是使用-x参数指向命令文件,这样就可以公开文件的位置,这样我就不会忘记它。
为了帮助您设置GDB, builroot会在output/staging/usr/share/ builroot /gdbinit中创建一个包含正确sysroot命令的GDB命令文件。它将包含类似于下面的一行:

set sysroot /home/chris/buildroot/output/host/usr/aarch64-
buildroot-linux-gnu/sysroot

现在GDB正在运行,并且可以找到所需的信息,让我们看看可以使用它执行的一些命令

4.4 GDB命令概述

这里列出了最常用的命令。在大多数情况下,该命令有一个简短的形式,如下表所示
Breakpoints 断点
下面是管理断点的命令:
在这里插入图片描述
Running and stepping 运行和单步
这些是控制程序执行的命令:

在这里插入图片描述
获取调试信息
这些命令用于获取有关调试器的信息:
在这里插入图片描述

在调试会话中开始逐步执行程序之前,我们首先需要设置一个初始断点。

4.5 运行到一个断点

gdbserver将程序加载到内存中,并在第一条指令处设置断点,然后等待来自GDB的连接。建立连接后,您将进入调试会话。然而,你会发现,如果你尝试立即单步执行,你会得到这样的消息:

Cannot find bounds of current function

这是因为程序已经停止在用汇编编写的代码中,这为C/ c++程序创建了运行时环境。C/ c++代码的第一行是main()函数。假设你想在main()处停止,你可以在那里设置一个断点,然后使用continue命令(缩写c)告诉gdbserver从程序开始处的断点继续,并在main()处停止

(gdb) break main
Breakpoint 1, main (argc=1, argv=0xbefffe24) at helloworld.c:8
printf("Hello, world!\n");
(gdb) c

此时,您可能会看到以下内容

Reading /lib/ld-linux.so.3 from remote target...
warning: File transfers from remote targets can be slow. Use
"set sysroot" to access files locally instead.

对于旧版本的GDB,您可能会看到这个

warning: Could not load shared library symbols for 2 libraries,
e.g. /lib/libc.so.6.

在这两种情况下,问题都是您忘记设置系统root!再看一下前面关于sysroot的部分。

这与本机启动程序非常不同,在本机启动程序时,您只需键入run。事实上,如果您尝试在远程调试会话中键入run,您将看到一条消息,说明远程目标不支持run命令,或者在旧版本的GDB中,它会挂起,没有任何解释

4.6 扩展 使用 gdb 调试python

5. 本地GDB 调试

在目标上运行GDB的本地副本并不像远程那样常见,但这是可能的。除了在目标映像中安装GDB之外,您还需要要调试的可执行文件的未剥离副本以及安装在目标映像中的相应源代码。Yocto Project和Buildroot都允许您这样做。

注:虽然本机调试不是嵌入式开发人员的常见活动,但在目标上运行概要文件和跟踪工具是非常常见的。如果目标上有未剥离的二进制文件和源代码,这些工具通常工作得最好,这是我在这里要讲的故事的一半

5.1 yocto 项目

首先,通过在conf/local.conf中添加以下命令,将gdb添加到目标镜像中:

EXTRA_IMAGE_FEATURES ?= "tools-debug dbg-pkgs"

您需要想要调试的包的调试信息。Yocto项目构建包的调试变体,其中包含未剥离的二进制文件和源代码。通过在conf/local.conf中添加<包名>-dbg,您可以有选择地将这些调试包添加到目标映像中。或者,您可以通过向EXTRA_IMAGE_FEATURES添加dbg-pkgs来安装所有调试包,如下所示。需要注意的是,这将极大地增加目标映像的大小,可能增加几百兆字节。源代码安装在目标文件的/usr/src/debug/<包名>

PACKAGE_DEBUG_SPLIT_STYLE = "debug-without-src"
5.2 Buildroot

使用builroot,你可以通过启用这个选项告诉它在目标映像中安装GDB的本地副本:

  • 目标包中的BR2_PACKAGE_GDB_DEBUGGER |调试,分析和基准|完全调试器
    然后,要构建带有调试信息的二进制文件,并在不剥离的情况下将它们安装在目标映像中,请启用第一个选项并禁用第二个选项:
  • 构建选项中的BR2_ENABLE_DEBUG |带有调试符号的构建包
  • BR2_STRIP_strip在构建选项|条带目标二进制文件
    这就是我要说的关于本机调试的全部内容。同样,这种做法在嵌入式设备上并不常见,因为额外的源代码和调试符号会使目标映像膨胀

四 即时调试

有时,一个程序在运行了一段时间后会开始出错,你想知道它在做什么。GDB附加特性就是这样做的。我称之为即时调试。它可用于本机和远程调试会话
在远程调试的情况下,您需要找到要调试的进程的PID,并使用——attach选项将其传递给gdbserver。例如,如果PID是109,你可以这样输入:

gdbserver --attach :10000 109
Attached; pid = 109
Listening on port 10000

这将迫使进程像在断点处一样停止,从而允许您以正常方式启动交叉GDB并连接到gdbserver。完成后,您可以分离,允许程序在没有调试器的情况下继续运行:

(gdb) detach
Detaching from program: /home/chris/MELP/helloworld/helloworld,
process 109
Ending remote debugging

通过PID连接到正在运行的进程当然很方便,但是多进程或多线程程序呢?还有一些技术可以用GDB调试这些类型的程序

五 调试 forks和 threads

当您正在调试的程序分叉时会发生什么?调试会话遵循父进程还是子进程?此行为由follow-forkmode控制,它可以是parent或child,其中parent是默认值。不幸的是,gdbserver的当前版本(10.1)不支持这个选项,所以它只适用于本机调试.
如果您确实需要在使用gdbserver时调试子进程,一种解决方法是修改代码,使子进程在fork之后立即在变量上循环,从而使您有机会将新的gdbserver会话附加到它,然后设置变量,以便它退出循环

当多线程进程中的线程遇到断点时,默认行为是所有线程停止。在大多数情况下,这是最好的做法,因为它允许您查看静态变量,而不会被其他线程更改。当您重新开始执行线程时,所有已停止的线程都会启动,即使您是单步执行,特别是最后一种情况可能会导致问题。有一种方法可以修改GDB处理已停止线程的方式,通过一个称为调度器锁定的参数。
通常它是关闭的,但是如果将它设置为打开,则只有在断点处停止的线程才会恢复,其他线程保持停止状态,从而使您有机会看到线程在没有干扰的情况下单独执行的操作。在关闭调度器锁定之前,情况一直如此。Gdbserver支持此特性。

六 Core files

核心文件捕获失败程序终止时的状态。当bug出现时,您甚至不需要在房间里有调试器。所以,当你看到分段错误(堆芯)时,不要耸耸肩;调查核心文件并从中提取信息的金矿。
第一个观察结果是,默认情况下不会创建核心文件,只有在进程的核心文件资源限制非零时才会创建。您可以使用ulimit -c为当前shell更改它。要删除对核心文件大小的所有限制,请键入以下命令:

ulimit -c unlimited

默认情况下,core文件名为core,并放置在进程的当前工作目录中,即/proc//cwd所指向的目录。这个计划有许多问题。首先,当查看具有多个名为core的文件的设备时,并不明显是哪个程序生成了每个文件。其次,进程的当前工作目录很可能位于只读文件系统中,可能没有足够的空间来存储核心文件,或者进程可能没有向当前工作目录写入的权限

有两个文件控制核心文件的命名和位置。首先是
/proc/sys/kernel/core_uses_pid。向它写入一个1将导致死亡进程的PID号被附加到文件名中,只要您可以将PID号与日志文件中的程序名关联起来,这在一定程度上是有用的。
更有用的是/proc/sys/kernel/core_pattern,它为您提供了对核心文件的更多控制。默认模式是core,但你可以将其更改为由以下元字符组成的模式:

  • %p: The PID
  • %u: The real UID of the dumped process
  • %g: The real GID of the dumped process
  • %s: The number of the signal causing the dump
  • %t: The time of dump, expressed as seconds since the Epoch, 1970-01-01 00:00:00
    +0000 (UTC)
  • %h: The hostname
  • %e: The executable filename
  • %E: The path name of the executable, with slashes (/) replaced by exclamation
    marks (!)
  • %c: The core file size soft resource limit of the dumped process

您还可以使用以绝对目录名开头的模式,以便将所有核心文件聚集在一个位置。例如,下面的模式将所有核心文件放入/corefiles目录,并用程序名和崩溃时间命名它们:

 echo /corefiles/core.%e.%t > /proc/sys/kernel/core_pattern

在核心转储之后,您会发现如下内容:

 ls /corefiles
core.sort-debug.1431425613

For more information, refer to the manual page, core(5)

1. 使用GDB 查看 core 文件

下面是一个查看核心文件的示例GDB会话:

$ arm-poky-linux-gnueabi-gdb sort-debug /home/chris/rootfs/
corefiles/core.sort-debug.1431425613
[…]
Core was generated by `./sort-debug'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000085c8 in addtree (p=0x0, w=0xbeac4c60 "the") at sortdebug.c:41
41 p->word = strdup (w);

这表明程序在第41行停止。list命令显示附近的代码:

gdb) list
37 static struct tnode *addtree (struct tnode *p, char *w)
38 {
39 int cond;
40
41 p->word = strdup (w);
42 p->count = 1;
43 p->left = NULL;
44 p->right = NULL;
45

backtrace命令(缩写为bt)显示了我们是如何做到这一点的:

(gdb) bt
#0 0x000085c8 in addtree (p=0x0, w=0xbeac4c60 "the") at sortdebug.c:41
#1 0x00008798 in main (argc=1, argv=0xbeac4e24) at sortdebug.c:89

这是一个明显的错误:addtree()是用空指针调用的。
GDB最初是一个命令行调试器,许多人仍然这样使用它。尽管LLVM项目的LLDB调试器越来越受欢迎,但GCC和GDB仍然是Linux的主要编译器和调试器。到目前为止,我们只关注GDB的命令行接口。现在,我们将研究一些具有越来越现代的用户界面的GDB前端。

七 GDB用户界面

Visual Studio Code

	具体 环境搭建 内容 后续补充

八 使用 GDB调试 内核代码

您可以使用kgdb进行源代码级调试,其方式类似于使用gdbserver进行远程调试。还有一个自托管的内核调试器kdb,它可以方便地执行较轻的任务,例如查看一条指令是否被执行,并进行回溯以找出它是如何执行的。最后,还有内核Oops消息和panic,它们告诉您很多关于内核异常的原因

1. 使用 kgdb 调试 内核代码

在使用源代码调试器查看内核代码时,必须记住内核是一个复杂的系统,具有实时行为。不要期望调试像应用程序调试一样简单。逐步执行更改内存映射或切换上下文的代码可能会产生奇怪的结果。

kgdb是内核GDB存根的名称,这些存根多年来一直是主流Linux的一部分。在内核DocBook中有一个用户手册,您可以在https://www.kernel.org/doc/htmldocs/kgdb/index.html上找到一个在线版本。

在大多数情况下,您将通过串行接口连接到kgdb,该接口通常与串行控制台共享。因此,这个实现被称为kgdboc,它是kgdb over console的缩写。为了工作,它需要一个支持I/O轮询而不是中断的平台tty驱动程序,因为kgdb在与kgdb通信时必须禁用中断
GDB。一些平台支持USB上的kgdb,也有一些可以在以太网上工作的版本,但不幸的是,这些版本都没有进入主流Linux。

关于优化和堆栈帧的相同警告也适用于内核,其限制是编写内核时假定优化级别至少为- O1。可以通过在运行make之前设置KCFLAGS来覆盖内核编译标志。
那么,这些就是内核调试所需的内核配置选项:

  • CONFIG_DEBUG_INFO is in the Kernel hacking | Compile-time checks and compiler options | Compile the kernel with debug info menu.
  • CONFIG_FRAME_POINTER may be an option for your architecture and is in the Kernel hacking | Compile-time checks and compiler options | Compile the kernel with frame pointers menu.
  • CONFIG_KGDB is in the Kernel hacking | KGDB: kernel debugger menu.
  • CONFIG_KGDB_SERIAL_CONSOLE is in the Kernel hacking | KGDB: kernel debugger | KGDB: use kgdb over the serial console menu.

除了zImage或uImage压缩内核映像之外,内核映像必须是ELF对象格式,以便GDB可以将符号加载到内存中。这是在构建Linux的目录中生成的名为vmlinux的文件。在Yocto中,您可以请求在目标映像和SDK中包含一个副本。它是作为一个名为kernel-vmlinux的包构建的,您可以像安装其他包一样安装它,例如,将它添加到 IMAGE_INSTALL列表。

文件被放入sysroot引导目录,文件名如下所示:

/opt/poky/3.1.5/sysroots/cortexa8hf-neon-poky-linux-gnueabi/
boot/vmlinux-5.4.72-yocto-standard

在builroot中,您将在构建内核的目录中找到vmlinux,即output/build/linux-/vmlinux。

2. 调试会话示例

用一个简单的例子来展示它是如何工作的最好方法。
您需要通过内核命令行或在运行时通过sysfs告诉kgdb要使用哪个串行端口。对于第一个选项,在命令行中添加kgdboc=,,如下所示:

kgdboc=ttyO0,115200

对于第二个选项,启动设备并将终端名称写入 /sys/module/kgdboc/parameters/kgdboc文件,如下所示

echo ttyO0 > /sys/module/kgdboc/parameters/kgdboc

请注意,不能通过这种方式设置波特率。如果它与控制台的tty相同,那么它就已经设置好了。如果没有,请使用stty或类似的程序。
现在您可以在主机上启动GDB,选择与正在运行的内核匹配的vmlinux文件:

$ arm-poky-linux-gnueabi-gdb ~/linux/vmlinux

GDB从vmlinux加载符号表并等待进一步的输入。
接下来,关闭连接到控制台的任何终端模拟器:您将使用它来做什么.如果两者同时处于活动状态,则一些调试字符串可能会损坏。
现在,您可以返回到GDB并尝试连接到kgdb。但是,您会发现此时从目标远程得到的响应是没有帮助的:

(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
Remote debugging using /dev/ttyUSB0
Bogus trace status reply from target: qTStatus

问题是kgdb此时没有监听连接。在进入与内核的交互式GDB会话之前,需要中断内核。
不幸的是,只是在GDB中键入Ctrl + C,就像在应用程序中一样,不起作用。您必须通过在目标上启动另一个shell来强制将一个陷阱放入内核,以SSH为例,将g写入目标板的/proc/sysrq-trigger。

echo g > /proc/sysrq-trigger

目标在这一刻停止了。现在,您可以通过电缆主机端的串行设备连接到kgdb:

(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
Remote debugging using /dev/ttyUSB0
0xc009a59c in arch_kgdb_breakpoint ()

最终,GDB银行掌握了主动权。您可以设置断点、检查变量、查看回溯,等等。例如,在sys_sync上设置break,如下所示:

(gdb) break sys_sync
Breakpoint 1 at 0xc0128a88: file fs/sync.c, line 103.
(gdb) c
Continuing.

现在目标又活过来了。在目标上输入sync调用sys_sync并到达断点:

[New Thread 87]
[Switching to Thread 87]
Breakpoint 1, sys_sync () at fs/sync.c:103

如果你已经完成了调试会话,想要禁用kgdboc,只需将kgdboc终端设置为null:

echo "" > /sys/module/kgdboc/parameters/kgdboc

与使用GDB连接到正在运行的进程一样,一旦内核完成引导,这种捕获内核并通过串行控制台连接到kgdb的技术就可以工作。但是,如果内核由于错误而无法完成引导呢?

3. Debugging early code

前面的示例适用于在系统完全启动时执行您感兴趣的代码的情况。如果您需要尽早进入,您可以通过在命令行中添加kgdbwait,在kgdboc选项之后告诉内核在引导过程中等待:

kgdboc=ttyO0,115200 kgdbwait

现在,当您启动时,您将在控制台上看到以下内容:
[ 1.103415] console [ttyO0] enabled
[ 1.108216] kgdb: Registered I/O driver kgdboc.
[ 1.113071] kgdb: Waiting for connection from remote gdb…

此时,您可以关闭控制台并以通常的方式从GDB连接

4. Debugging modules

调试内核模块带来了额外的挑战,因为代码是在运行时重新定位的,因此您需要找出它所在的地址。信息通过sysfs呈现。模块的每个节的重定位地址存储在/sys/module/<模块名>/sections中。注意,由于ELF部分以点(.)开头,因此它们显示为隐藏文件,如果您想列出它们,则必须使用ls -a。重要的是。text,。data和。bss。

以一个名为mbx的模块为例

 cat /sys/module/mbx/sections/.text
0xbf000000
 cat /sys/module/mbx/sections/.data
0xbf0003e8
 cat /sys/module/mbx/sections/.bss
0xbf0005c0

现在你可以在GDB中使用这些数字来加载这些地址上模块的符号表:

(gdb) add-symbol-file /home/chris/mbx-driver/mbx.ko 0xbf000000
\
-s .data 0xbf0003e8 -s .bss 0xbf0005c0
add symbol table from file "/home/chris/mbx-driver/mbx.ko" at
.text_addr = 0xbf000000
.data_addr = 0xbf0003e8
.bss_addr = 0xbf0005c0

现在一切都应该正常工作:你可以设置断点,检查模块中的全局和局部变量,就像在vmlinux中一样:

(gdb) break mbx_write
Breakpoint 1 at 0xbf00009c: file /home/chris/mbx-driver/mbx.c,
line 93.
(gdb) c
Continuing.

然后,强制设备驱动程序调用mbx_write,它将达到断点:

Breakpoint 1, mbx_write (file=0xde7a71c0, buffer=0xadf40
"hello\n\n",
length=6, offset=0xde73df80)
at /home/chris/mbx-driver/mbx.c:93

如果您已经在用户空间中使用GDB调试代码,那么您应该能够熟练地使用kgdb调试内核代码和模块。接下来让我们看看kdb。

5. Debugging kernel code with kdb

虽然kdb没有kgdb和GDB的特性,但它有自己的用途,而且由于是自托管的,所以不需要担心外部依赖关系。KDB有一个简单的命令行接口,您可以在串行控制台上使用它。您可以使用它来检查内存、寄存器、进程列表和dmesg,甚至可以设置断点以在特定位置停止。
要配置内核,使您可以通过串行控制台调用kdb,请像前面所示启用kgdb,然后启用这个附加选项:

• CONFIG_KGDB_KDB, which is in the KGDB: Kernel hacking | kernel debugger | KGDB_KDB: Include kdb frontend for kgdb menu

现在,当您强制内核进入陷阱,而不是进入GDB会话时,您将在控制台上看到kdb shell:

 echo g > /proc/sysrq-trigger
[ 42.971126] SysRq : DEBUG
Entering kdb (current=0xdf36c080, pid 83) due to Keyboard Entry
kdb>

在kdb shell中可以做很多事情。help命令将打印所有选项。以下是概述:

  • Getting information:

    ps: This displays active processes.
    ps A: This displays all processes.
    lsmod: This lists modules.
    dmesg: This displays the kernel log buffer

  • Breakpoints:

      bp: This sets a breakpoint.
      bl: This lists breakpoints.
      bc: This clears a breakpoint.
      bt: This prints a backtrace.
      go: This continues execution.
    
  • Inspect memory and registers:

     md: This displays memory.
     rd: This displays registers.
    

下面是一个设置断点的快速示例:

kdb> bp sys_sync
Instruction(i) BP #0 at 0xc01304ec (sys_sync)
is enabled addr at 00000000c01304ec, hardtype=0 installed=0
kdb> go

内核恢复正常,控制台显示正常的shell提示符。如果你输入sync,它会到达断点并再次输入kdb:

Entering kdb (current=0xdf388a80, pid 88) due to Breakpoint @0xc01304ec

KDB不是源代码级别的调试器,因此您无法看到源代码或单步调试。但是,您可以使用bt命令显示回溯,这对于了解程序流和调用层次结构非常有用。

6. Looking at an Oops

当内核执行无效的内存访问或执行非法指令时,内核Oops消息将被写入内核日志。其中最有用的部分是回溯,我想向您展示如何使用那里的信息来定位导致错误的代码行。我还将解决在Oops消息导致系统崩溃时如何保留它们的问题。

此Oops消息是通过写入MELP/中的驱动程序生成的 Chapter19 / mbx-driver-oops

Unable to handle kernel NULL pointer dereference at virtual
address 00000004
pgd = dd064000
[00000004] *pgd=9e58a831, *pte=00000000, *ppte=00000000
Internal error: Oops: 817 [#1] PREEMPT ARM
Modules linked in: mbx(O)
CPU: 0 PID: 408 Comm: sh Tainted: G O 4.8.12-yocto-standard #1
Hardware name: Generic AM33XX (Flattened Device Tree)
task: dd2a6a00 task.stack: de596000
PC is at mbx_write+0x24/0xbc [mbx]
LR is at __vfs_write+0x28/0x48
pc : [<bf0000f0>] lr : [<c024ff40>] psr: 800e0013
sp : de597f18 ip : de597f38 fp : de597f34
r10: 00000000 r9 : de596000 r8 : 00000000
r7 : de597f80 r6 : 000fda00 r5 : 00000002 r4 : 00000000
r3 : de597f80 r2 : 00000002 r1 : 000fda00 r0 : de49ee40
Flags: Nzcv IRQs on FIQs on Mode SVC_32 ISA ARM Segment none
Control: 10c5387d Table: 9d064019 DAC: 00000051
Process sh (pid: 408, stack limit = 0xde596210)

读取PC的Oops行位于mbx_write+0x24/0xbc [mbx],并告诉您想知道的大部分内容:最后一条指令位于名为mbx的内核模块的mbx_write函数中。此外,它在函数开始处的偏移量为0x24字节,其长度为0xbc字节
接下来,看一下回溯:

Stack: (0xde597f18 to 0xde598000)
7f00: bf0000cc 00000002
7f20: 000fda00 de597f80 de597f4c de597f38 c024ff40 bf0000d8
de49ee40 00000002
7f40: de597f7c de597f50 c0250c40 c024ff24 c026eb04 c026ea70
de49ee40 de49ee40
7f60: 000fda00 00000002 c0107908 de596000 de597fa4 de597f80
c025187c c0250b80
7f80: 00000000 00000000 00000002 000fda00 b6eecd60 00000004
00000000 de597fa8
7fa0: c0107700 c0251838 00000002 000fda00 00000001 000fda00
00000002 00000000
7fc0: 00000002 000fda00 b6eecd60 00000004 00000002 00000002
000ce80c 00000000
7fe0: 00000000 bef77944 b6e1afbc b6e73d00 600e0010 00000001
d3bbdad3 d54367bf
[<bf0000f0>] (mbx_write [mbx]) from [<c024ff40>] (__vfs_
write+0x28/0x48)
[<c024ff40>] (__vfs_write) from [<c0250c40>] (vfs_
write+0xcc/0x158)
[<c0250c40>] (vfs_write) from [<c025187c>] (SyS_
write+0x50/0x88)
[<c025187c>] (SyS_write) from [<c0107700>] (ret_fast_
syscall+0x0/0x3c)
Code: e590407c e3520b01 23a02b01 e1a05002 (e5842004)
---[ end trace edcc51b432f0ce7d ]---

在本例中,我们没有了解更多信息,只是知道从虚拟文件系统函数_vfs_write调用了mbx_write。
如果能找到与mbx_write+0x24相关的代码行,那就太好了,我们可以使用带有/s修饰符的GDB命令反汇编,这样它就能同时显示源代码和汇编程序代码。在本例中,代码位于mbx中。我们将它加载到gdb中:

$ arm-poky-linux-gnueabi-gdb mbx.ko
[…]
(gdb) disassemble /s mbx_write
Dump of assembler code for function mbx_write:
99 {
0x000000f0 <+0>: mov r12, sp
0x000000f4 <+4>: push {r4, r5, r6, r7, r11, r12, lr, pc}
0x000000f8 <+8>: sub r11, r12, #4
0x000000fc <+12>: push {lr} ; (str lr, [sp, #-4]!)
0x00000100 <+16>: bl 0x100 <mbx_write+16>
100 struct mbx_data *m = (struct mbx_data *)file->private_data;
0x00000104 <+20>: ldr r4, [r0, #124] ; 0x7c
0x00000108 <+24>: cmp r2, #1024 ; 0x400
0x0000010c <+28>: movcs r2, #1024 ; 0x400
101 if (length > MBX_LEN)
102 length = MBX_LEN;
103 m->mbx_len = length;
0x00000110 <+32>: mov r5, r2
0x00000114 <+36>: str r2, [r4, #4]

Oops告诉我们错误发生在mbx_write+0x24。从反汇编中,我们可以看到mbx_write位于地址0xf0。加上0x24得到0x114,它是由第103行代码生成的。

注意:
您可能会认为我得到了错误的指示,因为清单如下所示0x00000114 <+36>: STR r2, [r4, #4]。当然,我们要找的是+24,而不是+36?啊,但是GDB的作者试图在这里混淆我们。偏移量以十进制显示,而不是十六进制:36 = 0x24,所以我最终得到了正确的偏移量!

从第100行可以看到m具有类型结构体mbx_data *。这里是定义该结构的地方:

#define MBX_LEN 1024
struct mbx_data {
char mbx[MBX_LEN];
int mbx_len;
};

所以,看起来m变量是一个空指针,这就是导致错误的原因。
查看初始化m的代码,我们可以看到缺少一行。通过修改驱动程序来初始化指针,如下面的代码块中高亮显示的那样,它可以正常工作,没有Oops:

static int mbx_open(struct inode *inode, struct file *file)
{
	if (MINOR(inode->i_rdev) >= NUM_MAILBOXES) {
	printk("Invalid mbx minor number\n");
	return -ENODEV;
		}
file->private_data = &mailboxes[MINOR(inode->i_rdev)];
return 0;
}

并不是每个Oops都这么容易查明,特别是如果它发生在内核日志缓冲区的内容可以显示之前

7. Preserving the Oops

解码Oops只有在你能首先捕获它的情况下才有可能。如果系统在启动控制台之前崩溃,或者在挂起之后崩溃,则不会看到它。有一些机制可以将内核Oops和消息记录到MTD分区或持久内存中,但是这里有一种简单的技术,它在许多情况下都可以工作,并且不需要事先考虑

只要内存的内容在 reset 期间没有被破坏(通常不会),您就可以重新引导进入引导加载程序并使用它来显示内存。您需要知道内核日志缓冲区的位置,记住它是一个简单的文本消息环形缓冲区。符号是__log_buf。在系统中查找。映射内核:

$ grep __log_buf System.map
c0f72428 b __log_buf

然后,通过减去PAGE_OFFSET并添加RAM的物理起始位置,将该内核逻辑地址映射到U-Boot可以理解的物理地址。PAGE_OFFSET几乎总是0xc0000000, RAM的起始地址是0x80000000 BeagleBone,所以计算变成了c0f72428 - 0xc0000000 + 0x80000000 = 80 f72428。现在可以使用U-Boot md命令来显示日志

U-Boot#
md 80f72428
80f72428: 00000000 00000000 00210034 c6000000 ........4.!.....
80f72438: 746f6f42 20676e69 756e694c 6e6f2078 Booting Linux on
80f72448: 79687020 61636973 5043206c 78302055 physical CPU 0x
80f72458: 00000030 00000000 00000000 00730084 0.............s.
80f72468: a6000000 756e694c 65762078 6f697372 ....Linux versio
80f72478: 2e34206e 30312e31 68632820 40736972 n 4.1.10 (chris@
80f72488: 6c697562 29726564 63672820 65762063 builder) (gcc ve

重要提示
从Linux 3.5开始,内核日志缓冲区中的每行都有一个16字节的二进制头,用于编码时间戳、日志级别和其他内容。在Linux每周新闻中有一个关于它的讨论,标题是迈向更可靠的日志记录,网址是https://lwn.net/Articles/492125/。

在本节中,我们研究了如何使用kgdb在源代码级别调试内核代码。然后我们研究了在kdb shell中设置断点和打印回溯。
最后,我们学习了如何使用dmesg或U-Boot命令行从控制台中读取内核Oops消息。

九 总结

了解如何使用GDB进行交互式调试是嵌入式系统开发人员工具箱中的一个有用工具。它是一个稳定的、文档完备的、知名的实体。它能够通过在目标上放置一个代理来远程调试,无论是用于应用程序的gdbserver还是用于内核代码的kgdb,尽管默认的命令行用户界面需要一段时间才能习惯,但是有许多可选择的前端。我提到的三个是TUI、DDD和Visual Studio Code。Eclipse是另一个流行的前端,它支持通过CDT插件调试GDB。我将把你介绍给

第二种同样重要的调试方法是收集崩溃报告并脱机分析它们。在这个类别中,我们研究了应用程序核心转储和内核
糟糕的消息。

然而,这只是识别程序缺陷的一种方法。在下一章中,我将讨论分析和跟踪作为分析和优化程序的方法。

更多的信息

  • The Art of Debugging with GDB, DDD, and Eclipse, by Norman Matloff and Peter Jay Salzman
  • GDB Pocket Reference, by Arnold Robbins
  • Python Interpreter in GNU Debugger, by crazyguitar: https://www.pythonsheets.com/appendix/python-gdb.html
  • Extending GDB with Python, by Lisa Roach: https://www.youtube.com/watch?v=xt9v5t4_zvE
  • Cross-compiling with CMake and VS Code, by Enes ÖZTÜRK: https://enesozturk.medium.com/cross-compiling-with-cmake and-vscode-9ca4976fdd1
  • Remote Debugging with GDB, by Enes ÖZTÜRK: https://enes-ozturk.medium.com/remote-debugging-with-gdb-b4b0ca45b8c1
  • Getting to grips with Eclipse: cross compiling: https://2net.co.uk/tutorial/eclipse-cross-compile
  • Getting to grips with Eclipse: remote access and debugging: https://2net.co.uk/tutorial/eclipse-rse
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐