使用 GDB 和 Python 调试嵌入式 Linux 平台

作者:Tom Parkin

如果您为 Linux 系统编写代码,那么您很可能使用过久经考验的 GNU 调试器 (GDB)。GDB 作为许多 GUI 的后端以及嵌入式领域中各种 JTAG 调试工具的接口,是 Linux 最重要的调试器。从 7.0 版本开始,GDB 获得了一项引人注目的新功能:支持使用 Python 解释器编写调试操作脚本。在本文中,我将介绍如何使用 Python 驱动 GDB,并将这些知识应用于调试嵌入式 Linux 平台的棘手问题。

嵌入式 Linux 调试的挑战

在 x86 PC 平台上调试 Linux 程序,虽然不一定容易,但至少得到了各种工具的良好支持。大多数 Linux 发行版都打包了开发和调试工具,以帮助处理从分析运行时性能到跟踪内存泄漏和检测死锁的任何问题。

嵌入式平台很少有如此完善的服务。虽然许多项目试图为嵌入式开发提供在桌面上被认为是理所当然的那种完善和集成,但这些项目尚未在所有嵌入式 Linux 开发领域得到广泛采用。许多嵌入式设备是使用实际上是手工制作的 Linux 发行版开发的,该发行版与该设备的特定目标紧密相关。将各种方便的调试工具集成到该环境所需的时间,尤其是在快节奏的消费电子产品世界中,是很少有团队能够满足的额外开销。

许多嵌入式平台试图通过使用“低脂”系统库(例如 uClibc 代替 glibc)来节省资源开销,这可能会使某些调试工具的集成更加困难。实际上,在某些情况下,目标平台使用的 CPU 架构将完全阻止某些工具的使用。例如,优秀的 Valgrind instrumentation framework 对 ARM 架构的支持有限,而对 MIPS 或 SuperH 则完全不支持。

嵌入式设备的性质通常意味着 CPU 周期和内存都很稀缺。任何在两者上负担过重的调试工具都可能使其在嵌入式设备上不切实际,尤其是在尝试调试竞争条件等方面时。

嵌入式 Linux 世界中调试工具提供不一致的最终结果是,大多数开发人员不得不尽可能使用可用的工具进行凑合。幸运的是,GDB 在嵌入式设备上得到广泛应用,因为它易于交叉编译并且支持广泛的目标架构。随着最近 Python 脚本支持的集成,GDB 可以扩展到单步执行和变量检查的典型调试任务之外。

使用 Python 编写 GDB 脚本

长期以来,GDB 一直支持通过预先准备好的调试器命令序列进行扩展。这种能力使得自动化调试工作流程的某些部分甚至实现新的调试器功能成为可能。

将 Python 集成到 GDB 中为 GDB 脚本编写和扩展的可能性增加了一个额外的维度。除了 GDB 原生脚本语言的简单函数和流程控制之外,还可以使用 Python 语言的全部功能。

Python GDB API 以名为 gdb 的 Python 模块的形式呈现,该模块提供对 GDB 内部进程调试表示的访问。该模块包括进程信息、线程、堆栈帧、值和类型、函数、符号和断点的接口。此外,还提供了一种机制,可以将命令注入到 GDB 命令行界面中。

结果是,GDB 的内部结构现在可以作为丰富的库集使用,用于以编程方式驱动调试器。这为扩展和自动化创造了全新的机会。例如,假设您想在大型应用程序中调试对 malloc() 的调用,但您真正感兴趣的只是来自某个模块的调用。理想情况下,您希望能够在调用 malloc() 时,只有当模块的某个函数位于回溯中时才中断执行。Python API 为您提供了这种灵活性。

问题代码

为了探索在 GDB 中使用 Python,让我们调试一个小型 C 程序,其代码如清单 1 所示。它以相当复杂的方式执行打印短语“Hello World!”的简单任务,并且至少有一个明显的错误。除了对手头的任务来说过度设计之外,hello_world.c 还使用了两个互斥锁来序列化对不同数据结构的访问,并非所有这些互斥锁的用户都同意应该获取锁的顺序。这很快就会产生运行时死锁。

清单 1. hello_world 的 C 源代码

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>

#define THREAD_COUNT        32

/* String output data */
static const char   *string = "Hello World!\n";
static int          cursor = 0;
pthread_mutex_t     print_lock = PTHREAD_MUTEX_INITIALIZER;

/* Runtime statistics */
static int          chars_printed = 0;
pthread_mutex_t     statistics_lock = PTHREAD_MUTEX_INITIALIZER;

/* Print one character of the string "Hello World!" to stdout */
/* Returns a pointer to the character printed */
static char *say_hello(void)
{
    char *printed_letter = NULL;

    printf("%c", string[cursor]);
    if (++cursor > strlen(string)) {
        cursor = 0;
        fflush(stdout);
    }

    printed_letter = (char *) malloc(1);
    if (printed_letter) {
        *printed_letter = string[cursor];
    }

    return printed_letter;
}

/* A "bug-free" printer function */
static void *good_printer(void *data)
{
    char *c = NULL;

    while(1) {
        c = NULL;
        pthread_mutex_lock(&print_lock);
        pthread_mutex_lock(&statistics_lock);
        c = say_hello();
        if (c) free(c);
        chars_printed++;
        pthread_mutex_unlock(&statistics_lock);
        pthread_mutex_unlock(&print_lock);
    }
    return NULL;
}

/* A buggy printer function */
static void *bad_printer(void *data)
{
    while(1) {
        pthread_mutex_lock(&statistics_lock);
        pthread_mutex_lock(&print_lock);
        say_hello();
        chars_printed++;
        pthread_mutex_unlock(&print_lock);
        pthread_mutex_unlock(&statistics_lock);
    }
    return NULL;
}

int main (int argc, char **argv)
{
    pthread_t threads[THREAD_COUNT];
    int i;

    /* Spawn many good children threads */
    for (i = 1; i < THREAD_COUNT; i++) {
        if (0 != pthread_create(&threads[i], NULL, good_printer, NULL)) {
            perror("pthread_create");
            exit(EXIT_FAILURE);
        }
    }

    /* Spawn one bad child thread */
    if (0 != pthread_create(&threads[0], NULL, bad_printer, NULL)) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    pthread_join(threads[0], NULL);

    return EXIT_SUCCESS;
}

虽然 hello_world.c 有些人为设计,但它确实演示了多线程应用程序在需要互斥锁来保护来自不同上下文的数据结构时可能遇到的运行时错误类型。

在继续阅读之前,值得考虑一下您可能如何调试这样的死锁。在 x86 平台上,您可以考虑使用 Valgrind 框架的 drd 工具。或者,您可以选择使用不同的选项重新编译代码以更改行为。但是,如果 Valgrind 在您的平台上不起作用,或者如果您要重建的代码是您只有二进制文件的第三方库,您会怎么做?

设置环境:嵌入式平台

本文的示例平台使用基于小端 MIPS 的片上系统 (SOC) 设备。MIPS 广泛用于家用路由器,例如流行的 Linksys WRT54G 系列,以及许多用于访问数字电视服务的机顶盒平台。我们的平台有一个相当强大的 400MHz CPU,以及 512MB 的 DDR RAM,使其成为一个功能相当强大的嵌入式设备。我们可以通过串行控制台和以太网端口与平台进行通信。

在软件方面,我们的平台运行 2.6 系列 Linux 内核,该内核已由 SOC 制造商扩展以支持我们正在使用的特定 CPU。它有一个相当典型的基于 uClibc 和 BusyBox 的用户空间,以及一系列 GNU 实用程序,例如 awk 和 sed。

设置环境:交叉编译 GDB

为了在我们的嵌入式平台上运行 GDB,我们将使用 gdbserver 工具进行远程调试。这允许我们在 Linux PC 上运行 GDB,并使用以太网连接到嵌入式目标。GDB 用于与 gdbserver 通信的协议在各个版本之间兼容,因此我们可以更新主机 PC 上的 GDB 安装,而无需在目标上安装新版本的 gdbserver。

由于大多数发行版都没有打包支持 MIPS 架构的 GDB,我们需要从源代码编译 GDB。使用可以从 GDB 网站下载的源代码压缩包中的说明可以轻松完成此操作。如果您在交叉编译或 GDB/gdbserver 配置方面遇到困难,网上有很多好的参考资料可以帮助您;本文的“资源”部分列出了一些。

初始调试

现在我们已经交叉编译并安装了 GDB,让我们看一下在嵌入式目标上调试死锁。

首先,在目标上运行 gdbserver 并附加到死锁进程

gdbserver :5555 --attach <pid of process>

现在,在主机 PC 上启动 GDB

mipsel-linux-uclibc-gdb

GDB 运行后,将其指向目标的根文件系统和要调试的文件

(gdb) set solib-absolute-prefix /export/shared/rootfs
(gdb) file hello_world
(gdb)

最后,告诉 GDB 通过 gdbserver 附加到目标上运行的进程

(gdb) target remote 10.0.0.6:5555
(gdb)

如果一切顺利,现在您应该能够稍微探索正在运行的进程,以了解正在发生的事情。鉴于该进程已死锁,检查进程中线程的状态是首要任务

(gdb) info threads
Id  Target Id   Frame
33  Thread 737  0x2aac1068 in __lll_lock_wait from libpthread.so.0
32  Thread 738  0x2aac1068 in __lll_lock_wait from libpthread.so.0
31  Thread 739  0x2aac1068 in __lll_lock_wait from libpthread.so.0
....
3   Thread 767  0x2aac1068 in __lll_lock_wait from libpthread.so.0
2   Thread 768  0x2aac1068 in __lll_lock_wait from libpthread.so.0
1   Thread 736  0x2aab953c in pthread_join from libpthread.so.0
(gdb)

GDB 输出中省略的线程都在 libpthread.so 深处的 __lll_lock_wait() 中类似地阻塞。显然,其中一些线程必须正在等待另一个线程尚未放弃的互斥锁——但是哪些线程,以及哪个互斥锁?

对 uClibc 树中 libpthread 源代码的一些检查表明,__lll_lock_wait() 是 Linux futex 系统调用的底层包装器。此函数的原型是

void __lll_lock_wait (int *futex, int private);

在 MIPS 上,a0 寄存器通常用于函数的第一个参数。因此,如果我们检查每个在 __lll_lock_wait() 中阻塞的线程的 a0,我们应该很好地了解哪些线程正在等待哪些互斥锁。这是一个好的开始,但理想情况下,我们希望找出哪个线程当前拥有每个互斥锁。我们该如何管理?

回到 uClibc 源代码,我们可以看到 __lll_lock_wait() 是从 pthread_mutex_lock() 调用的。提供给 __lll_lock_wait() 的整数指针实际上是指向 pthread_mutex_t 结构的指针

typedef union
{
  struct __pthread_mutex_s
  {
    int __lock;
    unsigned int __count;
    int __owner;
    int __kind;
    unsigned int __nusers;
    __extension__ union
    {
      int __spins;
      __pthread_slist_t __list;
    };
  } __data;
  char __size[__SIZEOF_PTHREAD_MUTEX_T];
  long int __align;
} pthread_mutex_t;

__owner 字段看起来很有趣,并且经过进一步调查,似乎 __owner 设置为当前持有互斥锁的线程的线程 ID (TID)。

通过结合这两条信息(即提供给 __lll_lock_wait() 的互斥锁指针;以及该结构中两个整数后的 __owner 字段),我们应该能够找出哪些线程正在阻塞哪些互斥锁。

问题是,手动迭代执行此操作将非常乏味。需要选择每个在 __lll_lock_wait() 中阻塞的线程。然后必须为每个线程的适当堆栈帧查询寄存器 a0 的内容,并检查 a0 指向的位置的内存,以发现哪个线程拥有该线程正在等待的互斥锁。即使对于这个简单的程序,我们也有大约 32 个线程要查看,这需要大量的手工工作。

将 Python 应用于实践

与其手动驱动调试器,不如看看我们如何使用 GDB Python API 自动化上述任务。首先,我们需要能够迭代调试进程(GDB 术语中的“inferior”)中的每个线程。为此,我们可以使用 gdb.Inferior 类的 threads() 方法

for process in gdb.inferiors():
    for thread in process.threads():
        print thread

这很容易。现在我们需要查看每个线程的当前执行堆栈帧,并确定它是否正在等待互斥锁。我们可以使用 gdb 模块函数 selected_frame() 和 gdb.Frame 类的 name() 方法来做到这一点

for process in gdb.inferiors():
    for thread in process.threads():
        thread.switch()
        frame = gdb.selected_frame()
        if frame.name() == "__lll_lock_wait":
            print "Thread is blocking in __lll_lock_wait"

到目前为止,一切顺利。现在我们有了一种以编程方式查找每个正在等待互斥锁的线程的方法,我们需要检查每个线程的 a0 寄存器的内容。这应该提取指向线程正在等待的互斥锁结构的指针。幸运的是,GDB 提供了一个方便变量 $a0,我们可以使用它来访问 a0 寄存器。gdb 模块函数 parse_and_eval() 提供对方便变量的 API 访问,以及其他功能

for process in gdb.inferiors():
    for thread in process.threads():
        thread.switch()
        frame = gdb.selected_frame()
        if frame.name() == "__lll_lock_wait":
            print "Thread is blocking in __lll_lock_wait"
            a0 = gdb.parse_and_eval("$a0")

我们需要从 GDB 中提取的最后一条信息是 a0 寄存器中指针处的内存内容,以便我们可以确定每个正在使用的互斥锁的 __owner 字段。虽然这样做可能有点作弊,但我们可以回退到 gdb 模块函数 execute() 来传递x命令到 GDB 命令行界面。这将将内存内容打印到我们可以解析以查找所需信息的字符串

for process in gdb.inferiors():
    for thread in process.threads():
        thread.switch()
        frame = gdb.selected_frame()
        if frame.name() == "__lll_lock_wait":
            print "Thread is blocking in __lll_lock_wait"
            a0 = gdb.parse_and_eval("$a0")
            s = gdb.execute("x/4d $a0", to_string=True).split()
            s.reverse()
            owner = int(s[1])

看起来不是很漂亮,但它有效。此代码将从x命令返回的字符串拆分为以空格分隔的列表。由于 GDB 可能会根据它可以从应用程序二进制文件中提取的符号信息来更改输出开头使用的标签,因此我们反转列表并拉出倒数第二个值。这会产生结构中的第三个整数值,在本例中它是 pthread_mutex_t 的 __owner 字段。

现在剩下的就是将所有这些数据片段连接在一起,以提供一些有用的信息。清单 2 显示了执行此操作的完整 Python 代码。将它们放在一起

(gdb) source mutex_check.py 
Process threads : 
Id  Target Id   Frame 
33  Thread 737  0x2aac1068 in __lll_lock_wait from libpthread.so.0
32  Thread 738  0x2aac1068 in __lll_lock_wait from libpthread.so.0
....
3   Thread 767  0x2aac1068 in __lll_lock_wait from libpthread.so.0
2   Thread 768  0x2aac1068 in __lll_lock_wait from libpthread.so.0
1   Thread 736  0x2aab953c in pthread_join from libpthread.so.0
Analysing mutexes...
  Mutex 0x401cf0 :
     -> held by thread : 740
     -> blocks threads : 737 738 739 741 742 743 744 745 746 747
                         748 749 750 751 752 753 754 755 756 757
                         758 759 760 761 762 763 764 765 766 767
                         768
  Mutex 0x401d08 :
     -> held by thread : 768
     -> blocks threads : 740
(gdb) 

清单 2. 用于 GDB 互斥锁调试的 Python 代码

from collections import defaultdict

# A dictionary of mutex:owner
mutexOwners = {}

# A dictionary of blocking mutex:(threads..)
threadBlockers = defaultdict(list)

# Print the threads
print "Process threads : "
gdb.execute("info threads")

print "Analysing mutexes..."
# Step through processes running under gdb
for process in gdb.inferiors():

    # Step through each thread in the process
    for thread in process.threads():

        # Examine the thread -- is it blocking on a mutex?
        thread.switch()
        frame = gdb.selected_frame()
        if frame.name() == "__lll_lock_wait":

            # a0 is the first argument passed to the function
            a0 = gdb.parse_and_eval("$a0")
            mutex = int(a0)

            # Make a note of which thread blocks on which mutex
            threadBlockers[mutex].append(thread)

            # Make a note of which thread owns this mutex
            if not mutex in mutexOwners:
                s = gdb.execute("x/4d $a0", to_string=True).split()
                s.reverse()
                mutexOwners[mutex] = int(s[1])

    # Print the results of the analysis
    for mutex in mutexOwners:
        print "  Mutex 0x%x :" % mutex
        print "     -> held by thread : %d" % mutexOwners[mutex]
        s = ["%d" % t.ptid[2] for t in threadBlockers[mutex]]
        print "     -> blocks threads : %s" % ' '.join(s)

现在死锁变得非常清楚。线程 740 正在等待线程 768 当前拥有的互斥锁,而线程 768 又在等待线程 740 当前拥有的互斥锁。在另一个线程拥有的互斥锁可用之前,两个线程都无法运行。返回到 GDB 提示符,我们可以为两个线程生成回溯以获得更多见解

(gdb) t 30
[Switching to thread 30 (Thread 740)]
#0  0x2aac1068 in __lll_lock_wait ()
(gdb) bt
#0  0x2aac1068 in __lll_lock_wait () 
#1  0x2aaba568 in pthread_mutex_lock ()
#2  0x00400970 in good_printer (data=0x0) at hello_world.c:45
#3  0x2aab7f9c in start_thread ()
#4  0x2aac2200 in __thread_start ()
Backtrace stopped: frame did not save the PC
(gdb) t 2
[Switching to thread 2 (Thread 768)]
#0  0x2aac1068 in __lll_lock_wait () 
(gdb) bt
#0  0x2aac1068 in __lll_lock_wait ()
#1  0x2aaba568 in pthread_mutex_lock ()
#2  0x00400a04 in bad_printer (data=0x0) at hello_world.c:60
#3  0x2aab7f9c in start_thread ()
#4  0x2aac2200 in __thread_start ()
Backtrace stopped: frame did not save the PC
(gdb) 

正如回溯所示,这两个线程遵循两条不同的代码路径最终陷入死锁情况。根据此信息回顾 hello_world 的代码应该可以让我们找到错误:bad_printer() 以错误的顺序获取 print 和 statistics 锁。

结论

向 GDB 添加 Python API 在 Linux 调试武器库中提供了另一种强大的武器。对于其他调试工具可能不那么广泛可用的嵌入式系统,GDB 的强大编程接口可以使数小时的艰苦调试与数分钟的愉快脚本编写之间产生差异。

聪明的读者会注意到,我们在本文中发现的错误不是 hello_world.c 中唯一的错误。查找和修复剩余错误的任务留给读者使用他们新获得的 GDB Python 知识来解决。祝你玩得开心!

MIPS 寄存器

MIPS 架构有 32 个通用整数寄存器。其中,硬件架构指定寄存器 0 和 31 分别用于值零和函数返回地址。其余寄存器的使用完全由软件工具链定义。

然而,按照惯例,通用 MIPS 寄存器的使用非常牢固地设置,以允许软件互操作性。例如,寄存器 4 到 7 用于将前四个非浮点参数传递给函数,并被命名为 a0 到 a3。

资源

GDB: www.gnu.org/software/gdb

GDB/Python Wiki: sourceware.org/gdb/wiki/PythonGdb

Tom Tromey 关于 GDB 和 Python 的精彩博客文章: tromey.com/blog/?cat=17

OpenWrt 的 GDB 交叉编译 Makefile: https://dev.openwrt.org/browser/trunk/toolchain/gdb/Makefile

GDB/gdbserver 用法指南: www.linux.com/archive/feature/121735

uClibc 项目: uclibc.org

Linux Futex 信息: kernel.org/doc/man-pages/online/pages/man2/futex.2.html

Tom Parkin (tom.parkin@gmail.com) 从事 Linux 和嵌入式系统工作已有十年,并且仍然不断发现令人兴奋的新事物。当不面对电脑时,他喜欢 10 公里跑步和 Real Ale,尽管两者不会结合在一起。

加载 Disqus 评论