GDB 调试

GDB 调试嵌入式 linux 应用程序和内核 gdb 调试 gdb 调试

大家好,欢迎来到IT知识分享网。

GDB 调试

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

一 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在目标上加载要调试的程序,然后分别从主机上的交叉工具链加载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  

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

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

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

4.2 设置 sysroot
(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 […] 
(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 
set sysroot /home/chris/buildroot/output/host/usr/aarch64- buildroot-linux-gnu/sysroot 

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

4.4 GDB命令概述

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


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

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

六 Core files

ulimit -c unlimited 

默认情况下,core文件名为core,并放置在进程的当前工作目录中,即/proc//cwd所指向的目录。这个计划有许多问题。首先,当查看具有多个名为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. 

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. […] 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 

七 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上找到一个在线版本。

  • 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. 调试会话示例

kgdboc=ttyO0, 

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

echo ttyO0 > /sys/module/kgdboc/parameters/kgdboc 
$ arm-poky-linux-gnueabi-gdb ~/linux/vmlinux 
(gdb) set serial baud  (gdb) target remote /dev/ttyUSB0 Remote debugging using /dev/ttyUSB0 Bogus trace status reply from target: qTStatus 
echo g > /proc/sysrq-trigger 

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

(gdb) set serial baud  (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, kgdbwait 

此时,您可以关闭控制台并以通常的方式从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

• 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.] SysRq : DEBUG Entering kdb (current=0xdf36c080, pid 83) due to Keyboard Entry kdb> 

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

  • Getting information:
  • 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: de 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 : de 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 = 0xde) 
Stack: (0xde597f18 to 0xde) 7f00: bf0000cc 00000002 7f20: 000fda00 de597f80 de597f4c de597f38 c024ff40 bf0000d8 de49ee40 00000002 7f40: de597f7c de597f50 c0250c40 c024ff24 c026eb04 c026ea70 de49ee40 de49ee40 7f60: 000fda00 00000002 c0 de de597fa4 de597f80 c025187c c0250b80 7f80: 00000000 00000000 00000002 000fda00 b6eecd60 00000004 00000000 de597fa8 7fa0: c0 c0 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 [<c0>] (ret_fast_ syscall+0x0/0x3c) Code: ec e3520b01 23a02b01 e1a05002 (e) ---[ end trace edcc51b432f0ce7d ]--- 
$ 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行代码生成的。

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

#define MBX_LEN 1024 struct mbx_data { char mbx[MBX_LEN]; int mbx_len; }; 
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的起始地址是0x BeagleBone,所以计算变成了c0f72428 – 0xc0000000 + 0x = 80 f72428。现在可以使用U-Boot md命令来显示日志

U-Boot# md 80f72428 80f72428: 00000000 00000000 00 c ........4.!..... 80f72438: 746f6f42 20676e69 756e694c 6e6f2078 Booting Linux on 80f72448:   c  physical CPU 0x 80f72458: 00000030 00000000 00000000 00 0.............s. 80f72468: a 756e694c  6f ....Linux versio 80f72478: 2e34206e 30312e31   n 4.1.10 (chris@ 80f72488: 6c    builder) (gcc ve 

九 总结

了解如何使用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

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/110536.html

(0)
上一篇 2026-01-30 21:20
下一篇 2026-01-30 21:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信