虫子就会出现。 识别和修复它们是开发过程的一部分。 有许多不同的技术可用于查找和表征程序缺陷,包括静态和动态分析、代码审查、跟踪、分析和交互调试。 我将在下一章介绍跟踪器和分析器,但这里我想集中介绍通过调试器监视代码执行的传统方法,在我们的例子中是GNU Project Debugger(gdb)。 Gdb 是一个强大而灵活的工具。 您可以使用它来调试应用、检查在程序崩溃后创建的事后文件(核心文件),甚至逐步执行内核代码。
在本章中,我们将介绍以下主题:
- GNU 调试器
- 正在准备调试
- 调试应用
- 实时调试
- 调试叉和线程
- 核心文件
- 地理数据库用户界面
- 调试内核代码
要按照示例操作,请确保您具备以下条件:
- 至少具有 60 GB 可用磁盘空间的基于 Linux 的主机系统
- Buildroot 2020.02.9 LTS 版本
- Yocto 3.1(邓费尔)LTS 版本
- 适用于 Linux 的蚀刻器
- MicroSD 卡读卡器和卡
- USB 转 TTL 3.3V 串行电缆
- 覆盆子派 4
- 5V 3A USB-C 电源
- 用于网络连接的以太网电缆和端口
- 比格尔博恩黑
- 5V 1A 直流电源
您应该已经为第 6 章,选择构建系统安装了 Buildroot 的 2020.02.9 LTS 版本。 如果没有,请参考Buildroot 用户手册(https://buildroot.org/downloads/manual/manual.html)的系统要求部分,然后再按照第 6 章中的说明在您的 LINUX 主机上安装 Buildroot。
您应该已经为第 6 章,选择构建系统安装了 Yocto 的 3.1(Dunfall)LTS 发行版。 如果没有,请参考Yocto Project Quick Build指南(https://www.yoctoproject.org/docs/current/brief-yoctoprojectqs/brief-yoctoprojectqs.html)的Compatible Linux Distribution和Build Host Packages部分,然后根据第 6 章中的说明在您的 LINUX 主机上安装 Yocto。
本章的所有代码都可以在本书的 GitHub 存储库的Chapter19
文件夹中找到:https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition。
GDB 是针对种编译语言(主要是 C 和 C++)的源代码级调试器,但也支持各种其他语言,如 GO 和 Objective-C。 您应该阅读您正在使用的 GDB 版本的说明,以了解当前对各种语言的支持状态。
项目网站是https://www.gnu.org/software/gdb/,它包含许多有用的信息,包括 gdb 用户手册、使用 gdb进行调试。
GDB 有一个开箱即用的命令行用户界面,有些人会觉得它令人不快,尽管在现实中,只要稍加练习就可以很容易地使用它。 如果命令行界面不是您喜欢的,GDB 有很多前端用户界面,我将在本章后面描述其中的三个。
您需要使用调试符号编译要调试的代码。 GCC 为此提供了两种选择:-g
和-ggdb
。 后者添加特定于 gdb 的调试信息,而前者为您使用的任何目标操作系统生成适当格式的信息,使其成为更可移植的选项。 在我们的特定情况下,目标操作系统始终是 Linux,无论您使用-g
还是-ggdb
,差别都不大。 更有趣的是,这两个选项都允许您指定调试信息的级别(从0
到3
):
0
:这根本不会产生调试信息,相当于省略了-g
或-ggdb
开关。1
:这会产生最少的信息,但包括函数名和外部变量,这足以生成回溯。2
:这是默认设置,包括有关局部变量和行号的信息,以便您可以执行源代码级别的调试和单步执行代码。3
:这包括额外的信息,这意味着 gdb 可以正确处理宏扩展。
在大多数情况下,-g
就足够了:如果您在单步执行代码时遇到问题,特别是当它包含宏时,请保留-g3
或-ggdb3
。
下一个要考虑的问题是代码优化级别。 编译器优化倾向于破坏源代码和机器代码行之间的关系,这使得单步执行源代码变得不可预测。 如果遇到这样的问题,很可能需要在不进行优化的情况下进行编译,省略-O
编译开关,或者使用-Og
,这样可以实现不干扰调试的优化。
一个相关的问题是堆栈帧指针,gdb 需要它来生成直到当前函数调用的回溯。 在某些架构上,GCC 不会生成优化级别较高(-O2
及以上)的栈帧指针。 如果您发现自己确实需要使用-O2
进行编译,但仍然需要回溯,则可以使用-fno-omit-frame-pointer
覆盖默认行为。 还要查找代码,这些代码经过手工优化,通过添加-fomit-frame-pointer
去掉了帧指针:您可能希望临时删除这些位。
您可以使用 gdb 以两种方式之一调试应用:如果您正在开发在桌面和服务器上运行的代码,或者实际上是在同一台机器上编译和运行代码的任何环境,那么在本地运行 gdb 是很自然的。 但是,大多数嵌入式开发都是使用跨工具链完成的,因此您希望调试设备上运行的代码,但要从拥有源代码和工具的交叉开发环境进行控制。 我将重点介绍后一种情况,因为这是嵌入式开发人员最有可能遇到的情况,但我还将向您展示如何设置用于本机调试的系统。 我不打算在这里描述使用 gdb 的基础知识,因为已经有很多关于该主题的参考资料,包括 gdb 用户手册和本章末尾建议的进一步阅读部分。
远程调试的关键组件是调试代理gdbserver
,它在目标系统上运行并控制所调试程序的执行。 gdbserver
通过网络连接或串行接口连接到主机上运行的 gdb 副本。
通过gdbserver
调试几乎(但不完全)与本机调试相同。 差异主要集中在这样一个事实上,即涉及两台计算机,它们必须处于正确的状态才能进行调试。 以下是一些需要注意的事项:
- 在调试会话开始时,您需要使用
gdbserver
在目标上加载要调试的程序,然后从主机上的交叉工具链单独加载 gdb。 - 在调试会话开始之前,gdb 和
gdbserver
需要相互连接。 - 在主机上运行的 gdb 需要被告知在哪里查找调试符号和源代码,尤其是查找共享库的调试符号和源代码。
- Gdb
run
命令不能按预期工作。 gdbserver
将在调试会话结束时终止,如果您需要另一个调试会话,则需要重新启动它。- 您需要在主机上调试二进制文件的调试符号和源代码,但不需要在目标上调试。 通常,目标上没有足够的存储空间来容纳它们,在部署到目标之前需要剥离它们。
- Gdb/
gdbserver
组合并不支持本机运行的 gdb 的所有功能:例如,gdbserver
不能在fork
之后跟随子进程,而本机 gdb 可以。 - 如果 gdb 和
gdbserver
来自不同的 gdb 版本,或者是相同的版本但配置不同,则可能会发生奇怪的事情。 理想情况下,它们应该使用您最喜欢的构建工具从同一来源构建。
调试符号会显著增加可执行文件的大小,有时会增加 10 倍。如第 5 章,构建根文件系统中所述,在不重新编译所有内容的情况下删除调试符号非常有用。 作业的工具是交叉工具链中的binutils
包中的strip
。 您可以使用以下开关控制条带级别:
-
--strip-all
:这将删除所有符号(默认)。 -
--strip-unneeded
:这将删除重新定位处理不需要的符号。 -
--strip-debug
: This removes only debug symbols.重要音符
对于应用和共享库,
--strip-all
(缺省值)是可以的,但是当涉及到内核模块时,您会发现它会阻止模块加载。 请改用--strip-unneeded
。 我仍在研究–-strip-debug
的一个用例。
考虑到这一点,让我们看看使用 Yocto 项目和 Buildroot 调试所涉及的细节。
在使用 Yocto 项目远程调试应用时,需要做两件事:您需要将gdbserver
添加到目标映像中;您需要创建一个 SDK,其中包含 gdb,并且包含您计划调试的可执行文件的调试符号。
首先,要在目标映像中包含gdbserver
,您可以通过将以下内容添加到conf/local.conf
来显式添加包:
IMAGE_INSTALL_append = " gdbserver"
在没有串行控制台的情况下,还需要添加 SSH 守护进程,以便您可以在目标系统上启动gdbserver
:
EXTRA_IMAGE_FEATURES ?= "ssh-server-openssh"
或者,您可以将tools-debug
添加到EXTRA_IMAGE_FEATURES
,这会将gdbserver
、Nativegdb
和strace
添加到目标映像中(我将在下一章讨论strace
):
EXTRA_IMAGE_FEATURES ?= "tools-debug ssh-server-openssh"
对于第二部分,您只需要构建 SDK,如我在第 6 章,选择构建系统中所述:
$ bitbake -c populate_sdk <image>
SDK 包含一份 gdb 副本。 它还包含目标的sysroot
,其中包含作为目标映像一部分的所有程序和库的调试符号。 最后,SDK 包含可执行文件的源代码。 以为例,查看为 Raspberry PI 4 构建并由 Yocto 项目的 3.1.5 版生成的 SDK,它默认安装在/opt/poky/3.1.5/
中。 目标的sysroot
是/opt/poky/3.1.5/sysroots/aarch64-poky-linux/
。 程序位于相对于sysroot
的/bin/
、/sbin/
、/usr/bin/
和/usr/sbin/
中,库位于/lib/
和/usr/lib/
中。 在每个目录中,您会发现一个名为.debug/
的子目录,其中包含每个程序和库的符号。 Gdb 知道在搜索符号信息时要查看.debug/
。 可执行文件的源代码相对于sysroot
存储在
/usr/src/debug/
中。
Buildroot 没有区分构建环境和用于应用开发的环境:没有 SDK。 假设您使用的是 Buildroot 内部工具链,则需要启用这些选项来构建主机的交叉 GDB 和构建目标的gdbserver
:
BR2_PACKAGE_HOST_GDB
,在工具链中|为主机构建交叉 gdbBR2_PACKAGE_GDB
,在目标包|调试、评测和 基准|GDB中BR2_PACKAGE_GDB_SERVER
,在目标包|调试、性能分析和基准测试|gdbserver中
您还需要在Build Options|Build Packages with Debug Symbol中构建带有调试符号的可执行文件,为此需要启用BR2_ENABLE_DEBUG
。
这将在output/host/usr/<arch>/sysroot
中创建带有调试符号的库。
既然您已经在目标上安装了gdbserver
,并且在主机上安装了一个交叉 GDB,您就可以启动调试会话了。
GDB 和gdbserver
之间的连接可以通过网络或串行接口。 在网络连接的情况下,您可以使用要侦听的 TCP 端口号启动gdbserver
,也可以选择使用要接受连接的 IP 地址来启动gdbserver
。 在大多数情况下,您并不关心要连接哪个 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 中,使用target remote
命令建立到gdbserver
的连接,为其提供目标的 IP 地址或主机名以及它正在等待的端口:
(gdb) target remote 192.168.1.101:10000
当gdbserver
看到来自主机的连接时,它会打印以下内容:
Remote debugging from host 192.168.1.1
该过程与串行连接类似。 在目标上,您告诉gdbserver
使用哪个串行端口:
# gdbserver /dev/ttyO0 ./hello-world
您可能需要使用stty(1)
或类似程序预先配置端口波特率。 下面是一个简单的示例:
# stty -F /dev/ttyO0 115200
stty
还有许多其他选项,请阅读手册页以了解更多详细信息。 值得注意的是,该端口不得用于任何其他用途。 例如,您不能使用正在用作系统控制台的端口。
在主机上,您可以使用target remote
加上电缆主机端的串行设备连接到gdbserver
。 在大多数情况下,您需要首先使用 gdb 命令set serial baud
设置主机串行端口的波特率:
(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
尽管 gdb 和gdbserver
现在已经连接,但我们还没有准备好设置断点并开始单步执行源代码。
Gdb 需要知道在哪里可以找到调试信息和您正在调试的程序和共享库的源代码。 在本地调试时,路径是众所周知的,并且内置于 gdb 中,但是当使用跨工具链时,gdb 无法猜测目标文件系统的根在哪里。 你必须提供这些信息。
如果您使用 Yocto Project SDK 构建应用,则sysroot
位于 SDK 中,因此您可以在 GDB 中进行如下设置:
(gdb) set sysroot /opt/poky/3.1.5/sysroots/aarch64-poky-linux
如果您使用的是 Buildroot,您会发现sysroot
在output/host/usr/<toolchain>/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
以下是缺省值:$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
[…]
在大多数情况下,缺省值$cdir
和$cwd
就足够了,但是如果在编译和调试之间移动了目录,就会出现问题。 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
的多个引用,但是它在哪里呢? 答案是它在 SDK 的sysroot
中,所以完整路径是/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
您可能有其他共享库存储在sysroot
之外。 在这种情况下,您可以使用set solib-search-path
,它可以包含冒号分隔的目录列表来搜索共享库。 Gdb 仅在无法找到sysroot
中的二进制文件时才搜索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
搜索之前搜索*。*
您每次运行 gdb 时都需要执行一些操作,例如,设置sysroot
。 将这样的命令放入命令文件中并在每次启动 gdb 时运行它们是很方便的。 Gdb 从$HOME/.gdbinit
读取命令,然后从当前目录中的.gdbinit
读取命令,然后从命令行中使用-x
参数指定的文件读取命令。 然而,出于安全原因,最近版本的 gdb 将拒绝从当前目录加载.gdbinit
。 您可以通过向$HOME/.gdbinit
添加如下行来覆盖该行为:
set auto-load safe-path /
或者,如果您不想全局启用自动加载,您可以指定一个特定的目录,如下所示:
add-auto-load-safe-path /home/chris/myprog
我个人的偏好是使用-x
参数指向命令文件,它公开了文件的位置,这样我就不会忘记它。
为了帮助您设置 gdb,Buildroot 在output/staging/usr/share/buildroot/gdbinit
中创建了一个包含正确的sysroot
命令的 gdb 命令文件。 它将包含类似于下面一行的行:
set sysroot /home/chris/buildroot/output/host/usr/aarch64-buildroot-linux-gnu/sysroot
既然 gdb 已经在运行,并且可以找到它需要的信息,那么让我们来看看我们可以使用它执行的一些命令。
Gdb 有更多的命令,这些命令在联机手册和进一步阅读部分提到的资源中进行了描述。 为了帮助您尽快开始使用,这里列出了最常用的命令。 在大多数情况下,命令有缩写形式,如下表所示。
以下是用于管理断点的命令:
以下是控制程序执行的命令:
以下是命令,用于获取有关调试器的信息:
在我们开始单步执行调试会话中的程序之前,我们首先需要设置一个初始断点。
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.
在这两种情况下,问题是您忘记设置sysroot
! 再看一下sysroot
前面的部分。
这与本机启动程序完全不同,在本机启动程序时只需键入run
即可。 事实上,如果您尝试在远程调试会话中键入run
,您将看到一条消息,提示远程目标不支持run
命令,或者在较早版本的 gdb 中,它将在没有任何解释的情况下挂起。
我们可以在 gdb 中嵌入一个完整的 Python 解释器来扩展它的功能。 这是通过在构建之前使用--with-python
选项配置 gdb 来实现的。 GDB 有一个 API,可以将其大部分内部状态公开为 Python 对象。 此 API 允许我们将自己的自定义 gdb 命令定义为用 Python 编写的脚本。 这些额外的命令可能包括一些有用的调试辅助工具,比如跟踪点和漂亮的打印机,这些都不是 gdb 内置的。
我们已经介绍了设置 Buildroot 以进行远程调试。 要在 GDB 中启用 Python 支持,还需要一些额外的步骤。 在撰写本文时,Buildroot 只支持在 GDB 中嵌入 Python2.7,这很不幸,但总比根本不支持 Python 要好。 我们不能使用 Buildroot 生成的工具链来构建具有 Python 支持的 GDB,因为它缺少一些必要的线程支持。
要为支持 Python 的主机构建跨 GDB,请执行以下步骤:
-
导航到安装 Buildroot 的目录:
$ cd buildroot
-
复制您要为其构建镜像的板的配置文件:
$ cd configs $ cp raspberrypi4_64_defconfig rpi4_64_gdb_defconfig $ cd ..
-
从
output
目录清除以前的生成项目:$ make clean
-
激活您的配置文件:
$ make rpi4_64_gdb_defconfig
-
开始自定义您的映像:
$ make menuconfig
-
导航到工具链|工具链类型|外部工具链并选择该选项,即可启用外部工具链。
-
退出外部工具链并打开工具链子菜单。 选择一个已知的工作工具链,例如Linaro AArch64 2018.05,作为您的外部工具链。
-
Select Build cross gdb for the host from the Toolchain page and enable both TUI support and Python support:
图 19.1-GDB 中的 Python 支持
-
从工具链页面深入到gdb 调试器版本子菜单,然后选择 Buildroot 中提供的最新版本的 gdb。
-
返回工具链页面的,并向下钻取到构建选项。 选择生成带有调试符号的包。
-
Back out of the Build options page and drill down into System Configuration and select Enable root login with password. Open Root password and enter a non-empty password in the text field:
![Figure 19.2 – Root password](img/B11566_19_02.jpg)
图 19.2-超级用户密码
- 退出System Configuration页面,深入到Target Packages|Debug,Profiling and Benchmark。 选择gdb包将
gdbserver
添加到目标镜像。 - 退出调试、分析和基准测试,深入到目标包|网络应用。 选择DropBear包以启用
scp
和ssh
访问目标。 请注意,dropbear
不允许在没有密码的情况下访问root``scp
和ssh
。 - 添加haveged熵守护进程,该守护进程可以在目标包|其他下找到,以便在引导时更快地使用 SSH。
- 将另一个包添加到您的映像中,这样您就有了需要调试的东西。 我选择了
bsdiff
二进制补丁/比较工具,它是用 C 语言编写的,可以在Target Packages|Development Tools下找到。 - 保存更改并退出 Buildroot 的
menuconfig
。 - 将更改保存到配置文件:
```sh
$ make savedefconfig
```
- 为目标构建映像:
```sh
$ make
```
如果您想跳过前面的menuconfig
步骤,可以在本章的代码档案中找到 Raspberry PI 4 的现成rpi4_64_gdb_defconfig
文件。 将该文件从MELP/Chapter19/buildroot/configs/
复制到您的buildroot/configs
目录,如果您愿意,可以在该目录上运行make
。
构建完成后,outpimg/
中应该有一个可引导的sdcard.img
文件,您可以使用 Etcher 将其写入 microSD 卡。 将 microSD 插入目标设备并引导它。 使用以太网电缆将目标设备连接到您的本地网络,并使用arp-scan
查找其 IP 地址。 以root
身份通过 SSH 登录设备,然后输入您在配置映像时设置的密码。 我指定temppwd
作为我的rpi4_64_gdb_defconfig
镜像的root
密码。
现在,让我们使用 gdb 远程调试bsdiff
:
-
首先,导航到目标上的
/usr/bin
目录:# cd /usr/bin
-
然后,以
gdbserver
开始bdiff
,就像我们之前对helloworld
所做的那样:# gdbserver :10000 ./bsdiff pcregrep pcretest out Process ./bsdiff created; pid = 169 Listening on port 10000
-
接下来,从工具链启动 gdb 副本,将其指向程序的未剥离副本,以便 gdb 可以加载符号表:
$ cd output/build/bsdiff-4.3 $ ~/buildroot/output/host/bin/aarch64-linux-gdb bsdiff
-
在 gdb 中,设置
sysroot
如下:(gdb) set sysroot ~/buildroot/output/staging
-
然后,使用命令 target remote 建立到
gdbserver
的连接,为其提供目标的 IP 地址或主机名以及它正在等待的端口:(gdb) target remote 192.168.1.101:10000
-
当
gdbserver
看到来自主机的连接时,它会打印以下内容:Remote debugging from host 192.168.1.1
-
We can now load Python command scripts such as
tp.py
into GDB from<data-directory>/python
and use these commands like so:(gdb) source tp.py (gdb) tp search
在本例中,
tp
是tracepoint命令的名称,search
是bsdiff
中递归函数的名称。 -
要显示 gdb 搜索 Python 命令脚本的目录,请执行以下命令:
(gdb) show data-directory
GDB 中的 Python 支持也可用于调试 Python 程序。 Gdb 可以查看 CPython 的内部结构,这是 Python 的标准pdb
调试器所不具备的。 它甚至可以将 Python 代码注入到正在运行的 Python 进程中。 这样就可以创建强大的调试工具,比如 Facebook 的 Python3 内存分析器(https://github.com/facebookincubator/memory-analyzer)。
在目标系统上运行 gdb 的本机副本并不像远程运行那样常见,但这是可能的。 除了在目标映像中安装 gdb 之外,您还需要要调试的可执行文件的未剥离副本以及目标映像中安装的相应源代码。 Yocto 项目和 Buildroot 都允许您这样做。
重要音符
虽然本机调试不是嵌入式开发人员的常见活动,但在目标系统上运行分析和跟踪工具非常常见。 如果目标上有未剥离的二进制文件和源代码,这些工具通常工作得最好,这就是我在这里讲述的故事的一半。 我将在下一章回到这个主题。
首先,通过将以下内容添加到conf/local.conf
,将gdb
添加到目标图像:
EXTRA_IMAGE_FEATURES ?= "tools-debug dbg-pkgs"
您需要要调试的包的调试信息。 Yocto 项目构建包的调试变体,其中包含未剥离的二进制文件和源代码。 通过将<package name>-dbg
添加到您的conf/local.conf
,您可以有选择地将这些调试包添加到您的目标映像中。 或者,您可以通过将dbg-pkgs
添加到EXTRA_IMAGE_FEATURES
来简单地安装所有调试包,如刚才所示。 请注意,这将显著增加目标图像的大小,可能会增加数百兆字节。
源代码安装在目标镜像的/usr/src/debug/<package name>
中。 这意味着 gdb 无需运行set substitute-path
即可获取它。 如果您不需要源代码,可以通过将以下内容添加到您的conf/local.conf
文件来阻止其安装:
PACKAGE_DEBUG_SPLIT_STYLE = "debug-without-src"
使用 Buildroot,您可以通过启用此选项告诉它在目标映像中安装 gdb 的本机副本:
BR2_PACKAGE_GDB_DEBUGGER
在目标包|调试、分析和基准测试|完全调试器
然后,要使用调试信息构建二进制文件,并在不剥离的情况下将其安装在目标映像中,请启用这两个选项中的第一个选项并禁用第二个选项:
BR2_ENABLE_DEBUG
在生成选项|生成带有调试符号的包BR2_STRIP_strip
中的生成选项|剥离目标二进制文件
关于本机调试,我要说的就是这些。 同样,这种做法在嵌入式设备上并不常见,因为额外的源代码和调试符号会使目标图像变得臃肿。 接下来,让我们看看另一种形式的远程调试。
有时,程序在运行一段时间后会开始不正常,您想知道它在做什么。 GdbAttach特性就是这样做的。 我称之为即时调试。 它既可用于本机调试会话,也可用于远程调试会话。
在远程调试的情况下,您需要找到要调试的进程的 PID,并使用--attach
选项将其传递给gdbserver
。 例如,如果 PID 为109
,则应键入以下内容:
# gdbserver --attach :10000 109
Attached; pid = 109
Listening on port 10000
这会强制进程停止,就像它在断点处一样,允许您以正常方式启动跨 gdb 并连接到gdbserver
。 完成后,您可以detach
,允许程序在没有调试器的情况下继续运行:
(gdb) detach
Detaching from program: /home/chris/MELP/helloworld/helloworld, process 109
Ending remote debugging.
通过 PID 附加到正在运行的进程当然很方便,但是多进程或多线程程序又如何呢? 也有使用 gdb 调试这些类型的程序的技术。
当您正在调试的程序派生时会发生什么? 调试会话是跟随父进程还是子进程? 此行为由follow-fork-mode
控制,可以是parent
或child
,其中parent
是默认值。 遗憾的是,当前版本(10.1)的gdbserver
不支持此选项,因此它只适用于本机调试。 如果您确实需要在使用gdbserver
时调试子进程,解决方法是修改代码,以便子进程在 fork 之后立即循环一个变量,这样您就有机会将一个新的gdbserver
会话附加到它,然后设置该变量,使其退出循环。
当多线程进程中的线程遇到断点时,默认行为是所有线程暂停。 在大多数情况下,这是最好的做法,因为它允许您查看静态变量,而不会被其他线程更改。 当您重新开始执行线程时,所有停止的线程都会启动,即使您是单步执行,尤其是最后一种情况会导致问题。 有一种方法可以通过名为scheduler-locking
的参数修改 gdb 处理停止线程的方式。 通常为off
,但如果将其设置为on
,则只恢复在
断点处停止的线程,而其他线程保持停止状态,这样您就有机会在不受干扰的情况下查看该线程单独执行了什么操作。 在关闭scheduler-locking
之前,这种情况一直存在。 gdbserver
支持此功能。
核心文件捕获失败程序在其终止点的状态。 当 bug 显现时,您甚至不需要和调试器在房间里。 因此,当您看到Segmentation fault (core dumped)
时,不要耸耸肩;研究核心文件并提取其中的信息宝库。
第一个观察结果是,默认情况下不会创建核心文件,但只有当进程的核心文件资源限制为非零时才会创建。 您可以使用ulimit -c
为当前 shell 更改它。 要取消对核心文件大小的所有限制,请键入以下命令:
$ ulimit -c unlimited
默认情况下,核心文件名为core
,放在进程的当前工作目录中,也就是/proc/<PID>/cwd
所指向的目录。 这项计划有很多问题。 首先,当查看包含多个名为core
的文件的设备时,并不清楚是哪个程序生成了每个文件。 其次,进程的当前工作目录很可能位于只读文件系统中,可能没有足够的空间来存储核心文件,或者进程可能没有写入当前工作目录的权限。
有两个文件控制核心文件的命名和放置。 第一个是
/proc/sys/kernel/core_uses_pid
。 向其写入1
会导致将死进程的 PID 号附加到文件名上,只要您可以将 PID 号与日志文件中的程序名相关联,这就有些用处。
更有用的是/proc/sys/kernel/core_pattern
,它使您可以更好地控制核心文件。 默认模式为 core,但您可以将其更改为由以下元字符组成的模式:
%p
:PID%u
:转储进程的真实 UID%g
:转储进程的真实 GID%s
:导致转储的信号编号%t
:转储时间,自纪元起秒数,1970-01-01 00:00:00+0000(UTC)%h
:主机名%e
:可执行文件名%E
:可执行文件的路径名,将斜杠(/
)替换为感叹号(!
)%c
:转储进程的核心文件大小软资源限制
您还可以使用以绝对目录名开头的模式,以便所有核心文件都集中在一个位置。 例如,下面的模式将所有核心文件放入/corefiles
目录,并使用程序名和崩溃时间对它们进行命名:
# echo /corefiles/core.%e.%t > /proc/sys/kernel/core_pattern
在core
转储之后,您会发现类似以下内容:
# ls /corefiles
core.sort-debug.1431425613
有关详细信息,请参阅手册页core(5)
。
下面的是查看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 sort-debug.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 sort-debug.c:41
#1 0x00008798 in main (argc=1, argv=0xbeac4e24) at sort-debug.c:89
这是一个明显的错误:addtree()
是用空指针调用的。
Gdb 最初是一个命令行调试器,许多人仍然以这种方式使用它。 尽管 LLVM 项目的 LLDB 调试器越来越受欢迎,但 GCC 和 gdb 仍然是 Linux 的重要编译器和调试器。 到目前为止,我们只关注 gdb 的命令行界面。 现在我们来看一下 GDB 的一些前台,这些前台的用户界面逐渐变得更加现代化。
Gdb 通过 gdb 机器接口 gdb/MI 控制在的较低级别,gdb/MI 可用于将 gdb 包装在用户界面中或作为更大程序的一部分,它极大地扩展了可供选择的范围。
在本节中,我将描述三个非常适合调试嵌入式目标的工具: 终端用户界面(TUI)、数据显示调试器(DDD)和 Visual Studio 代码。
终端用户界面(TUI)是标准 GDB 包的可选部分。 主要功能是一个代码窗口,其中显示即将执行的代码行以及任何断点。 这是对命令行模式 gdb 中 list 命令的明显改进。
TUI 的吸引力在于,它无需任何额外设置即可工作,而且由于它处于文本模式,因此可以在 SSH 终端会话上使用,例如,当在目标上本地运行gdb
时。 大多数交叉工具链使用 TUI 配置 gdb。 只需将-tui
添加到命令行,您将看到以下内容:
图 19.3-TUI
如果您仍然觉得 TUI 缺乏,并且更喜欢真正的图形化前端而不是 gdb,那么 GNU 项目也提供了其中之一(https://www.gnu.org/software/ddd)。
Data Display Debugger(DDD)是一个简单的独立程序,它为您提供了 GDB 的图形用户界面,而无需最少的麻烦,尽管 UI 控件看起来过时了,但它做了所有必要的事情。
--debugger
选项告诉 DDD 从您的工具链使用 gdb,您可以使用-x
参数给出 gdb 命令文件的路径:
$ ddd --debugger arm-poky-linux-gnueabi-gdb -x gdbinit sort-debug
下面的屏幕截图展示了最好的功能之一:数据窗口,它包含网格中的项目,您可以根据需要重新排列。 如果双击指针,它将展开为一个新的数据项,并且该链接用箭头显示:
图 19.4-DDD
如果这两个 GDB 前端都不能接受,因为您是一个习惯于使用您所在行业最新工具的全栈 Web 开发人员,那么我们仍然可以覆盖您。
Visual Studio Code是微软非常流行的开源代码编辑器。 因为它是用 TypeScript 编写的 Electron 应用,所以 VisualStudio 代码感觉比成熟的 IDE(如 Eclipse)更轻量级,响应速度更快。 有丰富的语言支持(代码完成、转到定义等)。 通过其庞大的用户社区贡献的扩展,用于许多语言。 远程跨 GDB 调试可以使用 CMake 和 C/C++扩展集成到 Visual Studio 代码中。
在 Ubuntu Linux 系统上安装 Visual Studio 代码的最简单方法是使用snap
:
$ sudo snap install --classic code
在创建可以部署到 Raspberry PI 4 并进行远程调试的 C/C++项目之前,我们首先需要一个工具链。
我们将使用 Yocto 为 Raspberry Pi 4 构建一个 SDK。该 SDK 将包括一个针对 Raspberry Pi 4 的 64 位 ARM 内核的工具链。 在第 7 章,使用 Yocto开发的构建现有 BSP一节中,我们已经使用 Yocto 为 Raspberry PI 4 构建了 64 位 ARM 图像。
让我们使用该章中相同poky/build-rpi
输出目录来构建新的core-image-minimal-dev
映像以及该映像的相应 SDK:
-
首先,在克隆 Yocto 的目录上导航一级。
-
接下来,获取
build-rpi
构建环境:$ source poky/oe-init-build-env build-rpi
-
Edit
conf/local.conf
so that it includes the following:MACHINE ?= "raspberrypi4-64" IMAGE_INSTALL_append = " gdbserver" EXTRA_IMAGE_FEATURES ?= "ssh-server-openssh debug-tweaks"
debug-tweaks
功能不需要root
密码,因此可以使用命令行工具(如scp
和ssh
)从主机部署和运行新构建的二进制文件。 -
然后,构建 Raspberry PI 4 的开发映像:
$ bitbake core-image-minimal-dev
-
使用 Etcher 将生成的
core-image-minimal-dev-raspberrypi4-64.wic.bz2
映像从tmp/deplimg/raspberrypi4-64/
写入 microSD 卡,并在 Raspberry PI 4 上引导它。 -
通过以太网将 Raspberry PI 4 插入您的本地网络,并使用
arp-scan
定位 Raspberry PI 4 的 IP 地址。稍后在配置 CMake进行远程调试时,我们将需要此 IP 地址。 -
Lastly, build the SDK:
$ bitbake -c populate_sdk core-image-minimal-dev
重要音符
切勿在生产图像中使用
debug-tweaks
。 OTA 软件更新的自动化 CI/CD 管道至关重要,但必须非常小心,以确保开发映像不会意外泄漏到生产中。
现在,我们在poky/build-rpi
下的tmp/deploy/sdk
目录中有一个名为poky-glibc-x86_64-core-image-minimal-dev-aarch64-raspberrypi4-64-toolchain-3.1.5.sh
的自解压安装程序,我们可以使用它在任何 Linux 开发机器上安装这个新构建的 SDK。 在tmp/deploy/sdk
中找到 SDK 安装程序并运行它:
$ ./poky-glibc-x86_64-core-image-minimal-dev-aarch64-raspberrypi4-64-toolchain-3.1.5.sh
Poky (Yocto Project Reference Distro) SDK installer version 3.1.5
=================================================================
Enter target directory for SDK (default: /opt/poky/3.1.5):
You are about to install the SDK to "/opt/poky/3.1.5". Proceed [Y/n]? Y
[sudo] password for frank:
Extracting SDK..........................................................done
Setting it up...done
SDK has been successfully set up and is ready to be used.
Each time you wish to use the SDK in a new shell session, you need to source the environment setup script e.g.
$ . /opt/poky/3.1.5/environment-setup-aarch64-poky-linux
请注意,SDK 已安装到/opt/poky/3.1.5
。 我们不会按照说明获取environment-setup-aarch64-poky-linux
,但该文件的内容将用于填充即将到来的 Visual Studio 代码的项目文件。
我们将使用CMake交叉编译我们将在 Raspberry PI 4 上部署和调试的 C 代码。要在 Ubuntu Linux 上安装 CMake,请执行以下命令:
$ sudo apt update
$ sudo apt install cmake
CMake 应该已经作为第 2 章,了解工具链的一部分安装在您的主机上。
使用 CMake 构建的项目具有规范的结构,其中包括一个CMakeLists.txt
文件和单独的src
和build
目录。
在您的主目录中创建名为hellogdb
的 Visual Studio 代码项目:
$ mkdir hellogdb
$ cd hellogdb
$ mkdir src build
$ code .
最后一个code .
命令将启动 Visual Studio 代码并打开hellogdb
目录。 当您从目录启动 Visual Studio 代码时,还会创建一个隐藏的.vscode
目录,其中包含项目的settings.json
和launch.json
。
我们需要安装以下 Visual Studio 代码扩展,以便使用 SDK 中的工具链交叉编译和调试代码:
- Microsoft 提供的 C/C++
- CMake by Txs
- 微软的 CMake Tools
单击 Visual Studio 代码窗口左侧的Extensions图标,在 Marketplace 中搜索这些扩展并安装它们。 安装后,您的扩展侧栏应该如下所示:
图 19.5-扩展
现在,我们将使用 CMake 集成我们构建的 SDK 附带的工具链,用于交叉编译和调试我们的hellogdb
项目。
我们需要填充CMakeLists.txt
和cross.cmake
,以使用我们的工具链交叉编译hellogdb
项目:
-
首先,将
MELP/Chapter19/hellogdb/CMakeLists.txt
复制到主目录中的hellogdb
项目文件夹。 -
在 Visual Studio 代码中,单击 Visual Studio 窗口左上角的Explorer图标,打开Explorer侧边栏。
-
单击Explorer侧栏中的
CMakeLists.txt
查看文件内容。 请注意,项目名称定义为HelloGDBProject
,目标板的 IP 地址硬编码为192.168.1.128
。 -
将其更改为与 Raspberry PI 4 的 IP 地址相匹配,并保存
CMakeLists.txt
文件。 -
展开src文件夹,然后单击Explorer侧边栏中的New File图标,在
hellogdb
项目的src
目录中创建名为main.c
的文件。 -
将以下代码粘贴到那个
main.c
源文件中并保存它:#include <stdio.h> int main() { printf("Hello CMake\n"); return 0; }
-
将
MELP/Chapter19/hellogdb/cross.cmake
复制到主目录中的hellogdb
项目文件夹。 -
最后,单击Explorer侧边栏中的
cross.cmake
查看文件内容。 请注意,cross.cmake
中定义的sysroot_target
和tools
路径指向我们安装 SDK 的/opt/poky/3.1.5
目录。 还要注意,CMAKE_C_COMPILE
、CMAKE_CXX_COMPILE
和CMAKE_CXX_FLAGS
变量的值是直接从 SDK 附带的环境设置脚本派生的。
有了这两个文件,我们就可以构建我们的hellogdb
项目了。
现在,让我们将hellogdb
项目的settings.json
文件配置为使用CMakeLists.txt
和cross.cmake
构建:
-
在 Visual Studio 代码中打开
hellogdb
项目后,点击Ctrl+Shift+P以调出Command Palette字段。 -
在命令调色板字段中输入
>settings.json
,然后从选项列表中选择首选项:打开工作空间设置(JSON)。 -
Edit the
.vscode/settings.json
forhellogdb
so that it looks something like this:{ "cmake.sourceDirectory": "${workspaceFolder}", "cmake.configureArgs": [ "-DCMAKE_TOOLCHAIN_FILE=${workspaceFolder}/cross.cmake" ], "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools" }
请注意
cmake.configureArgs
定义中对cross.cmake
的引用。 -
点击Ctrl+Shift+P再次调出命令调色板字段。
-
在Command Palette字段中输入
>CMake: Delete Cache and Configuration
并执行它。 -
单击 Visual Studio 窗口左边缘的CMake图标以打开CMake侧边栏。
-
单击CMake侧边栏中的
HelloGDBProject
二进制文件以构建它:
图 19.6-构建 HelloGDBProject
如果您正确配置了所有内容,Output窗格的内容应该类似于 :
[main] Building folder: hellogdb HelloGDBProject
[build] Starting build
[proc] Executing command: /usr/bin/cmake --build /home/frank/hellogdb/build --config Debug --target HelloGDBProject -- -j 14
[build] [100%] Built target HelloGDBProject
[build] Build finished with exit code 0
现在我们已经使用 Visual Studio 代码构建了一个针对 64 位 ARM 的可执行二进制文件,让我们将其部署到 Raspberry PI 4 中进行远程调试。
现在,让我们创建一个launch.json
文件,以便可以将HelloGDBProject
二进制文件部署到 RaspberryPI 4,并从 Visual Studio 代码中远程调试它:
-
单击 Visual Studio 代码窗口左边缘的Run图标,打开Run侧边栏。
-
在Run侧边栏上单击create a Launch.json file,然后为环境选择C++(gdb/ldb)。
-
提示输入 C/C++调试配置类型时,从选项列表中选择Default Configuration。
-
在
.vscode/launch.json
内的"(gdb) Launch"
配置中添加或编辑以下字段,如下所示:"program": "${workspaceFolder}/build/HelloGDBProject", "miDebuggerServerAddress": "192.168.1.128:10000", "targetArchitecture": "aarch64", "miDebuggerPath": "/opt/poky/3.1.5/sysroots/x86_64-pokysdk-linux/usr/bin/aarch64-poky-linux/aarch64-poky-linux-gdb",
-
将
miDebuggerServerAddress
中的192.168.1.128
地址替换为您的 Raspberry PI 4 的 IP 地址,然后保存该文件。 -
在
main()
函数体的第一行的main.c
中设置断点。 -
在Run侧边栏中单击新的Build_and_Debug-Utility,将
HelloGDBProject
二进制文件发送到 Raspberry PI 4 并用gdbserver
启动。
如果 Raspberry PI 4 和launch.json
文件设置正确,则Output窗格的内容应如下所示:
[main] Building folder: hellogdb build_and_debug
[build] Starting build
[proc] Executing command: /usr/bin/cmake --build /home/frank/hellogdb/build --config Debug --target build_and_debug -- -j 14
[build] [100%] Built target HelloGDBProject
[build] Process ./HelloGDBProject created; pid = 552
[build] Listening on port 10000
单击 Visual Studio 代码窗口左上角的**(Gdb)Launch按钮。 Gdb 应该命中我们在main.c
中设置的断点,并且在输出**窗格中应该出现如下所示的行:
[build] Remote debugging from host 192.168.1.69, port 44936
这是当 gdb 到达断点时 VisualStudio 代码应该是什么样子:
图 19.7-GDB 远程调试
点击上方悬停的蓝色Continue按钮,输出窗格中应显示以下行:
[build] Hello CMake
[build]
[build] Child exited with status 0
[build] [100%] Built target build_and_debug
[build] Build finished with exit code 0
祝贺你!。 您已经使用 CMake 成功地将使用 Yocto 构建的 SDK 集成到 Visual Studio 代码中,以便在目标设备上启用 GDB 远程调试。 这 不是一个小壮举,但是现在您已经了解了它是如何完成的,您可以为您自己的项目做同样的事情。
您可以使用kgdb
进行源代码级别的调试,其方式类似于使用gdbserver
进行远程调试。 还有一个自托管内核调试器kdb
,它对于轻量级任务非常方便,比如查看指令是否已执行,以及获取回溯以找出它是如何到达那里的。 最后,还有内核Oops消息和死机,它们告诉您很多关于内核异常原因的信息。
在使用源代码调试器查看内核代码时,必须记住内核是一个复杂的系统,具有实时行为。 不要期望调试像调试应用一样简单。 单步执行更改内存映射或切换上下文的代码可能会产生奇怪的结果。
kgdb是给内核 gdb 存根命名的,多年来,内核 gdb 存根一直是主流 Linux 的一部分。 内核 Docbook 中有一个用户手册,您可以在https://www.kernel.org/doc/htmldocs/kgdb/index.html上找到在线版本。
在大多数情况下,您将通过串行接口连接到kgdb
,该接口通常与串行控制台共享。 因此,此实现称为kgdboc,是 Console 上的kgdb 的缩写。 要工作,它需要支持 I/O 轮询而不是中断的平台tty
驱动程序,因为kgdb
在与 GDB 通信时必须禁用中断。 有几个平台支持 USB 上的kgdb
,也有一些版本可以在以太网上工作,但不幸的是,这些版本都没有进入主流 Linux。
关于优化和堆栈框架的相同警告也适用于内核,但限制是内核被编写为假定优化级别至少为-O1
。 您可以通过在运行make
之前设置KCFLAGS
来覆盖内核编译标志。
那么,以下是内核调试所需的内核配置选项:
CONFIG_DEBUG_INFO
在内核破解|编译时检查和编译器选项|使用调试信息编译内核菜单中。CONFIG_FRAME_POINTER
可能是适用于您的体系结构的选项,位于内核破解|编译时检查和编译器选项|使用帧指针编译内核菜单中。CONFIG_KGDB
在内核破解|kgdb:内核调试器菜单中。CONFIG_KGDB_SERIAL_CONSOLE
位于内核黑客|kgdb:内核调试器|kgdb:在串行控制台菜单上使用 kgdb。
除了zImage
或uImage
压缩内核映像之外,内核映像必须是 ELF 对象格式,以便 gdb 可以将符号加载到内存中。 这是在构建 Linux 的目录中生成的名为vmlinux
的文件。 在 Yocto 中,您可以请求在目标镜像和 SDK 中包含一份副本。 它被构建为一个名为kernel-vmlinux
的软件包,您可以像安装任何其他软件包一样安装它,例如,通过将其添加到IMAGE_INSTALL
列表中。
该文件被放入 sysrootboot
目录,名称如下:
/opt/poky/3.1.5/sysroots/cortexa8hf-neon-poky-linux-gnueabi/boot/vmlinux-5.4.72-yocto-standard
在 Buildroot 中,您将在构建内核的目录中找到vmlinux
,该目录位于output/build/linux-<version string>/vmlinux
中。
向您展示它的工作原理的最好方法是用一个简单的例子。
您需要通过内核命令行或在运行时通过sysfs
告知kgdb
使用哪个串行端口。 对于第一个选项,将kgdboc=<tty>,<baud rate>
添加到命令行,如下所示:
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,如果两者同时处于活动状态,则某些调试字符串可能会损坏。
现在,您可以返回 gdb 并尝试连接到kgdb
。 但是,您会发现此时从target remote
得到的响应无济于事:
(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 ()
最后,广发银行掌权了。 您可以设置断点、检查变量、查看回溯等。 例如,在sys_sync
上设置一个断点,如下所示:
(gdb) break sys_sync
Breakpoint 1 at 0xc0128a88: file fs/sync.c, line 103.
(gdb) c
Continuing.
现在目标又复活了(T2)。 在目标上键入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
的技术在内核引导完成后起作用。 但是,如果内核因为错误而无法完成引导,该怎么办呢?
前面的示例在系统完全引导时执行您感兴趣的代码的情况下有效。 如果您需要提早进入,您可以通过在命令行中的kgdboc
选项后面添加kgdbwait
来告诉内核在引导期间等待:
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 连接。
调试内核模块带来了额外的挑战,因为代码在运行时被重新定位,因此您需要找出它驻留在什么地址。 信息通过sysfs
呈现。 模块每个部分的重定位地址存储在/sys/module/<module name>/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
。
虽然kdb不具备kgdb
和 gdb 的特性,但它确实有其用途,而且是自托管的,不需要担心外部依赖。 kdb
有一个简单的命令行界面,您可以在串行控制台上使用。 您可以使用它检查内存、寄存器、进程列表和dmesg
,甚至可以将断点设置为在某个位置停止。
要配置内核以便可以通过串行控制台调用kdb
,请启用前面所示的kgdb
,然后启用此附加选项:
CONFIG_KGDB_KDB
,位于kgdb:内核破解|内核调试器|kgdb_kdb:包含 kgdb菜单的 kdb 前端
现在,当您强制内核进入陷阱时,您将在控制台上看到kdb
shell,而不是进入 gdb 会话:
# 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
:此选项显示活动进程。ps A
:显示所有进程。lsmod
:列出模块。dmesg
:这将显示内核日志缓冲区。 -
Breakpoints:
bp
:这将设置断点。bl
:这列出了断点。bc
:这将清除断点。bt
:这将打印回溯。go
:这将继续执行。 -
Inspect memory and registers:
md
:这显示内存。rd
:此选项显示寄存器。
下面是设置断点的快速示例:
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
命令显示回溯,这对于了解程序流和调用层次结构非常有用。
当内核执行无效内存访问或执行非法指令时,内核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)
Oops 中显示 PC 的行位于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 ]---
在本例中,我们没有了解更多信息,只知道mbx_write
是从虚拟文件系统函数_vfs_write
调用的。
找到与mbx_write+0x24
相关的代码行将是非常好的,我们可以使用带有/s
修饰符的 gdb 命令disassemble
,这样它就可以同时显示源代码和汇编器代码。 在本例中,代码位于mbx.ko
模块中,因此我们将其加载到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 的作者试图在这里迷惑我们。 偏移量是以十进制显示的,而不是hex: 36 = 0x24
,所以我最终还是得到了正确的偏移量!
您可以从行100
看到m
具有类型 structmbx_data *
。 下面是定义该结构的地方:
#define MBX_LEN 1024
struct mbx_data {
char mbx[MBX_LEN];
int mbx_len;
};
因此,看起来m
变量是一个空指针,这就是导致 Oops 的原因。 查看m
被初始化的代码,我们可以看到缺少一行。 通过修改驱动程序来初始化指针,如以下代码块中突出显示的那样,它工作正常,没有 Oop:
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 都这么容易定位,特别是如果它发生在内核日志缓冲区的内容可以显示之前。
解码 Oops 只有在您首先能够捕获它的情况下才有可能。 如果系统在控制台启用之前或挂起后启动时崩溃,您将看不到它。 有一些机制可以将内核 Oop 和消息记录到 MTD 分区或永久内存,但这里有一种简单的技术,它在许多情况下都有效,不需要事先考虑。
只要内存内容在重置期间没有损坏(通常不会损坏),您就可以重新引导到引导加载程序并使用它来显示内存。 您需要知道内核日志缓冲区的位置,记住它是一个简单的文本消息环形缓冲区。 符号为__log_buf
。 在System.map
中查找内核:
$ grep __log_buf System.map
c0f72428 b __log_buf
然后,通过减去PAGE_OFFSET
并添加 RAM 的物理起点,将该内核逻辑地址映射到 U-Boot 可以理解的物理地址。 PAGE_OFFSET
几乎总是0xc0000000
,而 RAM 的起始地址是 Beaglebone 上的0x80000000
,因此计算变成了c0f72428 - 0xc0000000 + 0x80000000 = 80f72428
。
现在可以使用 U-Bootmd
命令显示日志:
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
80f72498: 6f697372 2e34206e 20312e39 6f726328 rsion 4.9.1 (cro
80f724a8: 6f747373 4e2d6c6f 2e312047 302e3032 sstool-NG 1.20.0
80f724b8: 20292029 53203123 5720504d 4f206465 ) ) #1 SMP Wed O
80f724c8: 32207463 37312038 3a31353a 47203335 ct 28 17:51:53 G
重要音符
从 Linux3.5 开始,内核日志缓冲区中的每一行都有一个 16 字节的二进制头,用于编码时间戳、日志级别和其他内容。 在 Linux 周新闻朝向更可靠的日志记录,在https://lwn.net/Articles/492125/上有一个关于它的讨论。
在本节中,我们研究了如何使用kgdb
在源代码级别调试内核代码。 然后,我们研究了在kdb
shell 中设置断点和打印回溯。 最后,我们了解了如何使用dmesg
或 U-Boot 命令行从控制台读取内核 Oops 消息。
了解如何使用 gdb 进行交互式调试是嵌入式系统开发人员工具箱中的一个有用工具。 它是一个稳定的、有据可查的、知名的实体。 它能够通过在目标上放置一个代理来进行远程调试,无论是应用的gdbserver
还是内核代码的kgdb
,虽然默认的命令行用户界面需要一段时间才能习惯,但还有许多替代前端。 我提到的三个是 TUI、DDD 和 Visual Studio 代码。 Eclipse 是另一个流行的前端,它支持通过 CDT 插件使用 GDB 进行调试。 有关如何配置 CDT 以使用交叉工具链并连接到远程设备的信息,请参考进一步阅读部分中的参考资料。
第二种同样重要的调试方法是收集崩溃报告并离线分析它们。 在这个类别中,我们查看了应用核心转储和内核 Oops 消息。
然而,这只是识别程序缺陷的一种方式。 在下一章中,我将讨论分析和跟踪作为分析和优化程序的方法。
以下资源提供了有关本章中介绍的主题的更多信息:
- The Art of Debug with gdb,DDD,and Eclipse,Norman Matloff 和 Peter Jay Salzman
- GDB Pocket Reference,Arnold Robbins 著
- *GNU 调试器中的 Python 解释器,*by Crazygiar:https://www.pythonsheets.com/appendix/python-gdb.html
- *使用 Python 扩展 gdb,*作者:Lisa Roach:https://www.youtube.com/watch?v=xt9v5t4_zvE
- 用 CMake 和 VS 代码交叉编译,Enes?ZTÜRK:https://enes-ozturk.medium.com/cross-compiling-with-cmake-and-vscode-9ca4976fdd1
- 使用 gdb进行远程调试,由 enesÖZTÜRK:https://enes-ozturk.medium.com/remote-debugging-with-gdb-b4b0ca45b8c1
- 掌握 Eclipse:交叉编译:https://2net.co.uk/tutorial/eclipse-cross-compile
- 掌握 Eclipse:远程访问和调试:https://2net.co.uk/tutorial/eclipse-rse