GDB之(5)打印线程变量和进程调试

Author:Once Day Date:2024年2月26日

漫漫长路,才刚刚开始…

全系列文章请查看专栏: Linux实践记录_Once-Day的博客-CSDN博客

推荐参考文档:

1. 概述
1.1 Linux上线程和进程的联系和区别

在操作系统的世界里,尤其是在Linux这一广受欢迎的开源系统中,进程(Process)和线程(Thread)是两种基础且核心的执行单位,它们共同构建起了系统进行任务管理和调度的基石。为了理解二者的联系和区别,可以将进程想象为一个工厂,而线程则是工厂里的工人。

进程可以被看作是一个程序的运行实例。它拥有独立的内存空间、数据栈以及其他用于跟踪执行的辅助数据。每个进程至少有一个线程,即主线程,但可以包含更多的线程。在Linux中,创建进程通常通过系统调用fork()实现,这一操作会生成一个与父进程几乎完全相同的子进程,包括代码和数据的复制。

线程,又称为轻量级进程,是一个基本的CPU执行单元,它比进程更为细小和轻量。线程共享其所属进程的内存空间和资源,却拥有各自的执行堆栈、程序计数器和一套寄存器。在Linux系统中,线程的创建通常通过pthread_create()等线程库提供的接口进行。

联系方面,线程存在于进程中,是进程中实际的运行单位。一个进程中的线程共享相同的地址空间和系统资源,比如文件描述符和网络连接等,这使得线程间的通信和数据共享变得高效。

区别方面,进程和线程的主要差异在于资源管理和开销。进程拥有独立的地址空间,进程间的通信(IPC)需要特定的机制如管道、信号量等。而线程共享进程的资源,线程间可以直接读写同一进程内的数据,但这也意味着必须小心处理同步和竞态条件。此外,创建新进程的系统开销通常远大于创建新线程,因为后者不需要复制资源,只是在已有的进程环境中分配新的执行路径。

再者,从系统调度的角度来看,线程作为调度的基本单位,其上下文切换比进程更快,因为线程切换不涉及地址空间的变换。不过,这也带来了安全性的问题,因为一个进程内的线程崩溃往往会影响到整个进程。

1.2 GDB对线程和进程调试的支持

GDB(GNU Debugger)是GNU项目的一部分,提供了对各种程序的深入调试能力。它是一款强大的调试工具,尤其是在Linux环境中,开发者们依赖它来调试包含复杂进程和线程的应用程序。

对于进程的支持,GDB允许开发者启动一个程序,逐行或逐指令地检查其执行过程。开发者可以在任何感兴趣的代码行设置断点,当程序执行到断点时暂停,这时可以检查当前的变量值、调用栈、寄存器状态等。此外,GDB支持对已运行程序的附加(attach)和从程序中分离(detach),这使得我们能够对生产环境中运行的进程进行实时调试,而不必重新启动。

在线程调试方面,GDB同样表现出色。它能够识别程序中的所有线程,允许开发者查看各个线程的状态、切换当前调试上下文到特定线程以及独立控制每个线程的执行(例如,单步执行或继续运行)。GDB利用线程库提供的信息(如pthread库)来管理这些操作。使用GDB时,可以通过info threads命令列出所有线程,thread <thread-id>命令切换到特定线程,以及通过set scheduler-locking命令控制在调试过程中其他线程的行为。

一个特别强大的特性是GDB的条件断点,它可以设置在特定线程上遇到特定条件时才触发。这在多线程程序中尤其有用,因为某些bug可能只在并发条件下的特定情况中出现,通过条件断点,开发者可以更精确地捕获和分析这些问题。

GDB还支持对多进程程序的调试。在fork或vfork后,子进程会继承父进程的地址空间,GDB能够识别出这种关系,并允许开发者选择是跟踪父进程还是子进程。当进程通过exec系列函数加载新的程序时,GDB也能够适应这种变化,继续提供调试支持。

GDB为程序员提供了强大的工具集,以便对多线程和多进程程序进行有效的调试。它通过提供对线程和进程级别操作的细粒度控制,帮助开发者发现并修复复杂并发程序中的错误。不过,GDB的使用可能有一定的学习曲线,但对于希望深入理解和解决复杂并发问题的开发者来说,这无疑是一个值得投入时间学习的工具。

2. 线程调试
2.1 查看线程信息

GDB的info threads命令能够查看当前调试会话中的所有线程信息,输入info threads命令后,GDB会列出当前程序中所有的线程,每个线程都会被分配一个唯一的标识符(通常是一个数字序号)。这个列表通常会包含以下信息:

  1. 线程ID:GDB内部为每个线程分配的一个唯一数字标识。
  2. 目标系统的线程ID:这通常是操作系统级别的线程标识,比如在Linux系统中对应的可能是轻量级进程ID(LWP ID)。
  3. 线程的状态:它可能是正在运行、暂停在断点上或其他状态。
  4. 当前执行位置:通常是函数名称和代码行号,表示该线程目前执行的代码位置。

这样的输出让开发者一目了然地看到每个线程的执行情况,从而可以决定接下来要关注哪个线程。比如,可能发现某个线程处于死锁状态,或者在某个特定的函数调用中暂停了。

这里有一个使用info threads命令的简化例子输出:

  Id   Target Id         Frame 
* 1    Thread 0x1234 (LWP 4321) "main" 0x00005555555548d9 in main(argc=1, argv=0x7fffffffe4f8) at app.c:22
  2    Thread 0x1235 (LWP 4322) "worker" 0x0000555555554910 in worker_thread(param=0x0) at worker.c:6

在这个例子中,星号(*)表示当前活动线程,即GDB当前正在关注的线程。线程1是主线程,在main函数的第22行暂停;线程2是一个工作线程,在worker_thread函数的第6行暂停。

2.2 指定线程上打断点

GDB中的条件断点是一种高级功能,它能够让开发者在满足特定条件时才触发断点。

这在调试复杂的多线程程序时尤其有用,因为它允许专注于特定线程的行为,而不会被其他线程的活动干扰。

使用GDB设置条件断点的基本命令格式是break <location> if <condition>,其中<location>指定了断点的位置,这可以是一个函数名、文件名加行号或者一个内存地址;<condition>则定义了触发断点的条件。而当你想要限定断点只在特定线程中触发时,可以使用break <location> thread <thread-id>命令,其中<thread-id>是GDB中的线程标识符。

这里有一个简单的例子来展示如何使用这个命令:

(gdb) break my_function thread 2

在这个例子中,my_function是函数的名称,而thread 2指定了这个断点只在线程2中触发。假设在多线程程序中,线程2负责特定的任务,而你怀疑在my_function中存在一个只有线程2会遇到的bug。使用线程条件断点,你就可以让GDB仅在线程2执行到该函数时暂停,不受其他线程的影响。

此外,你还可以在设置断点时结合使用条件和线程限定,例如:

(gdb) break my_function if my_variable == 1 thread 2

上述命令设置了一个更具体的条件断点,它不仅限定在线程2中的my_function函数触发,而且还要求变量my_variable的值必须等于1。

2.3 停止无关线程调度

GDB(GNU Debugger)允许开发者控制多线程程序中线程的调度行为,这就是通过set scheduler-locking命令实现的。在多线程程序的调试过程中,其他线程的运行可能会干扰对当前线程的分析,尤其是当你想要单步执行或者检查当前线程的状态时。

set scheduler-locking命令有三种模式来控制线程的调度:

  1. set scheduler-locking off:这是默认模式,GDB不会干预线程调度。也就是说,即使正在调试一个线程,其他线程也可以根据操作系统的调度策略自由运行。

  2. set scheduler-locking on:在这种模式下,只有当前被GDB暂停的线程可以运行,所有其他线程都将被冻结。这对于调试当前线程的行为非常有帮助,因为它确保了调试过程中没有其他线程的任何干扰。

  3. set scheduler-locking step:这个模式用于单步操作。当你在单步调试一个线程时,只有当前线程会执行下一步操作,其他线程保持暂停状态,直到当前线程停止在下一个断点或者完成单步操作。

这里有一个使用set scheduler-locking命令的例子:

(gdb) set scheduler-locking on

当执行以上命令后,在调试会话中,只有当前选中的线程会运行。这对于需要跟踪一个线程的执行路径,而不希望其他线程的运行造成影响时非常有用。

2.4 线程变量介绍

在多线程编程中,线程局部存储(Thread-Local Storage, TLS)是一种特殊的机制,用于声明某些变量是线程独有的,即每个线程都拥有这个变量的一个单独副本。这样,即使多个线程执行相同的代码,它们访问的也是各自独立的变量。在C和C++语言中,线程局部存储可以通过thread关键字或者其它编译器特定的修饰符来实现。

thread修饰符被用来声明一个线程局部变量。这意味着每个线程都有其自己的变量实例,不同线程间的实例互不影响。使用线程局部变量可以避免对于全局变量的共享访问,这在多线程环境中是非常有利的,因为它减少了锁的使用,避免了线程间的竞争条件,从而提高了程序的性能和可靠性。

在C++11及更高版本中,可以使用thread_local关键字来声明线程局部变量。例如:

thread_local int myVar = 0;

在该例中,myVar是一个线程局部变量,每个线程有自己的myVar副本。当一个线程修改它的myVar时,这个改变不会影响到其他线程中的myVar

在其他编程语言中,可能会有不同的机制来声明线程局部存储。例如,在Java中,可以使用ThreadLocal类来创建线程局部变量。

线程局部变量主要用于以下场景:

  1. 维护线程的状态信息,避免传递大量参数。
  2. 实现线程安全的单例模式。
  3. 在库函数中,用于存储线程特定的数据,避免影响其他线程。

值得注意的是,尽管线程局部存储提供了很多便利,但它也有一些缺点。例如,由于每个线程都有自己的存储副本,这可能会增加内存的使用量。

此外,线程局部变量的初始化和销毁可能会引入额外的性能开销,特别是在创建和销毁线程频繁的应用中。

2.5 打印errno值

errno是一个全局量,用于在C和C++程序中记录最近一次系统调用或库函数失败的错误代码。

在Unix-like系统中,errno是由C标准库提供的,它不是一个简单的全局变量,而是一个宏,它通常扩展为一个函数或者表达式,返回一个指向线程局部存储中实际错误代码的指针。由于errno是一个宏,直接在GDB中打印它可能不会像打印普通变量那样直接。

在GDB中打印errno的值,可以使用以下步骤:

  1. 启动GDB并加载你的程序。

  2. 如果你的程序还没有运行,使用run命令启动它。

  3. 当程序在某个断点停止或者出现错误时,可以使用以下命令来查看errno的值,或者尝试打印errno的地址,然后再打印这个地址所对应的值:

(gdb) print errno
$1 = 0
(gdb) print &errno
$2 = (int *) 0x7ffff7d866c0
(gdb) print *((int *)($2))
$8 = 0

在这里,$2是GDB打印出来的errno地址的临时变量。

如果直接打印errno不起作用(由于errno实际上可能被定义为宏,因此直接在GDB中打印它可能不会工作),那么可以使用一些间接的方法来获取其值。

第一种方法是使用 __errno_location() 函数。这个函数是GNU C库的一部分,它返回一个指向 errno 值的指针。在GDB中,可以使用以下命令来打印 errno 的值:

(gdb) p __errno_location()
$10 = (int *) 0x7ffff7d866c0
(gdb) p *__errno_location()
$11 = 0

这里,pprint 命令的缩写,它用于在GDB中打印表达式的值。__errno_location() 函数返回 errno 的地址,然后通过解引用(使用 * 符号)来获取该地址处的值。

第二种方法是使用了一个强制类型转换和函数调用的组合,来间接地获取错误码(针对二进制文件没有调试信息的场景,需要补全函数原型定义):

(gdb) p *((int* (*)())__errno_location)()
$12 = 0

在这条命令里:

  • int* (*)() 是一个函数指针类型,指向一个返回 int* 的函数。
  • __errno_location 是一个没有参数的函数,返回 errno 的地址。
  • 外层的一对括号 () 用来调用 __errno_location 函数。
  • *((int* (*)())__errno_location)__errno_location 函数的地址转换成了一个返回 int* 的函数指针。
  • 再次使用一对括号 () 来调用转换后的函数指针,这将返回指向 errno 的指针。
  • 最外层的 * 用来解引用该指针,获取 errno 的值。

还可以直接调用strerror函数输出字符串化,易于阅读的errno值:

(gdb) call (char*)strerror(errno)
$13 = 0x7ffff7f618a2 "Success"

__errno_location() 是GNU C库特有的,可能在其他C库中并不适用。而且,由于 errno 的值在每次系统调用或库函数调用之后可能会被改变,因此最好在相关调用之后立即检查 errno 的值,以确保获得准确的错误信息。

errno错误定义:

errno: 0 Success
errno: 1 Operation not permitted
errno: 2 No such file or directory
errno: 3 No such process
errno: 4 Interrupted system call
errno: 5 Input/output error
errno: 6 No such device or address
errno: 7 Argument list too long
errno: 8 Exec format error
errno: 9 Bad file descriptor
errno: 10 No child processes
errno: 11 Resource temporarily unavailable
errno: 12 Cannot allocate memory
errno: 13 Permission denied
errno: 14 Bad address
errno: 15 Block device required
errno: 16 Device or resource busy
errno: 17 File exists
errno: 18 Invalid cross-device link
errno: 19 No such device
errno: 20 Not a directory
errno: 21 Is a directory
errno: 22 Invalid argument
errno: 23 Too many open files in system
errno: 24 Too many open files
errno: 25 Inappropriate ioctl for device
errno: 26 Text file busy
errno: 27 File too large
errno: 28 No space left on device
errno: 29 Illegal seek
errno: 30 Read-only file system
errno: 31 Too many links
errno: 32 Broken pipe
errno: 33 Numerical argument out of domain
errno: 34 Numerical result out of range
errno: 35 Resource deadlock avoided
errno: 36 File name too long
errno: 37 No locks available
errno: 38 Function not implemented
errno: 39 Directory not empty
errno: 40 Too many levels of symbolic links
errno: 41 Unknown error 41
errno: 42 No message of desired type
errno: 43 Identifier removed
errno: 44 Channel number out of range
errno: 45 Level 2 not synchronized
errno: 46 Level 3 halted
errno: 47 Level 3 reset
errno: 48 Link number out of range
errno: 49 Protocol driver not attached
errno: 50 No CSI structure available
errno: 51 Level 2 halted
errno: 52 Invalid exchange
errno: 53 Invalid request descriptor
errno: 54 Exchange full
errno: 55 No anode
errno: 56 Invalid request code
errno: 57 Invalid slot
errno: 58 Unknown error 58
errno: 59 Bad font file format
errno: 60 Device not a stream
errno: 61 No data available
errno: 62 Timer expired
errno: 63 Out of streams resources
errno: 64 Machine is not on the network
errno: 65 Package not installed
errno: 66 Object is remote
errno: 67 Link has been severed
errno: 68 Advertise error
errno: 69 Srmount error
errno: 70 Communication error on send
errno: 71 Protocol error
errno: 72 Multihop attempted
errno: 73 RFS specific error
errno: 74 Bad message
errno: 75 Value too large for defined data type
errno: 76 Name not unique on network
errno: 77 File descriptor in bad state
errno: 78 Remote address changed
errno: 79 Can not access a needed shared library
errno: 80 Accessing a corrupted shared library
errno: 81 .lib section in a.out corrupted
errno: 82 Attempting to link in too many shared libraries
errno: 83 Cannot exec a shared library directly
errno: 84 Invalid or incomplete multibyte or wide character
errno: 85 Interrupted system call should be restarted
errno: 86 Streams pipe error
errno: 87 Too many users
errno: 88 Socket operation on non-socket
errno: 89 Destination address required
errno: 90 Message too long
errno: 91 Protocol wrong type for socket
errno: 92 Protocol not available
errno: 93 Protocol not supported
errno: 94 Socket type not supported
errno: 95 Operation not supported
errno: 96 Protocol family not supported
errno: 97 Address family not supported by protocol
errno: 98 Address already in use
errno: 99 Cannot assign requested address
errno: 100 Network is down
errno: 101 Network is unreachable
errno: 102 Network dropped connection on reset
errno: 103 Software caused connection abort
errno: 104 Connection reset by peer
errno: 105 No buffer space available
errno: 106 Transport endpoint is already connected
errno: 107 Transport endpoint is not connected
errno: 108 Cannot send after transport endpoint shutdown
errno: 109 Too many references: cannot splice
errno: 110 Connection timed out
errno: 111 Connection refused
errno: 112 Host is down
errno: 113 No route to host
errno: 114 Operation already in progress
errno: 115 Operation now in progress
errno: 116 Stale file handle
errno: 117 Structure needs cleaning
errno: 118 Not a XENIX named type file
errno: 119 No XENIX semaphores available
errno: 120 Is a named type file
errno: 121 Remote I/O error
errno: 122 Disk quota exceeded
errno: 123 No medium found
errno: 124 Wrong medium type
errno: 125 Operation canceled
errno: 126 Required key not available
errno: 127 Key has expired
errno: 128 Key has been revoked
errno: 129 Key was rejected by service
errno: 130 Owner died
errno: 131 State not recoverable
errno: 132 Operation not possible due to RF-kill
errno: 133 Memory page has hardware error
errno: 134~255 unknown error!
3. 进程调试
3.1 查看进程信息

在GNU Debugger (GDB) 中,info inferiors 命令被用来列出所有的“inferior processes”,也就是由GDB启动或附加的所有进程。在GDB中,“inferior”是一个术语,指的是被调试器控制的那个进程。这个命令尤其在调试多进程程序时非常有用,因为它可以让你看到当前所有的进程及其状态。

使用 info inferiors 命令,可以获得以下信息:

  • Inferior的ID:这是GDB内部分配给每个inferior的唯一标识符。
  • 进程ID(PID):这是操作系统分配给进程的标识符。
  • 进程的执行状态:显示进程是正在运行还是已经停止。
  • 执行命令:显示了启动每个inferior的命令。

想要查看当前所有的进程及其信息,可以在GDB的命令行界面中输入以下命令:

(gdb) info inferiors

命令执行后,假设有两个子进程,可能会看到类似下面的输出:

  Num  Description       Executable        
* 1    process 12345     /path/to/program  
  2    process 12346     /path/to/program  

在这个输出中:

  • “Num”列表示inferior的编号,这是GDB分配的。
  • “Description”列显示了进程的状态和PID。星号(*)表示当前GDB正在操作的进程。
  • “Executable”列显示了每个inferior的可执行文件路径。

info inferiors 提供的信息可以让快速了解多进程程序的状态,并允许选择要调试的具体进程。可以使用 inferior 命令加上编号来切换到不同的inferior。

(gdb) inferior 2

这条命令会切换GDB的焦点到编号为2的inferior。此时,如果再次执行 info inferiors,会看到星号已经移动到新的焦点inferior旁边。

3.2 追踪子进程

GDB提供了一些选项和命令,用于控制如何调试创建了子进程的程序。其中,follow-fork-modedetach-on-fork 是两个与此相关的重要命令。

首先,follow-fork-mode 命令允许设置GDB在遇到fork()系统调用时的行为。fork()是在Unix-like系统中创建进程的常用方法。当一个进程(父进程)调用fork()时,它会创建一个新的进程(子进程),子进程是父进程的一个副本。follow-fork-mode 命令有两个模式:

  1. parent:GDB将继续跟踪父进程,忽略子进程的行为。这是默认模式。
  2. child:GDB将转而跟踪子进程,忽略父进程的行为。

设置follow-fork-mode的命令如下:

(gdb) set follow-fork-mode child
(gdb) set follow-fork-mode parent

其次,detach-on-fork 命令控制GDB在分叉后是否保持对不在其跟踪下的进程的控制。当设置为on时,GDB将从不被跟踪的进程中分离,允许它独立运行;当设置为off时,GDB会保持对两个进程的控制(进程不会独立运行),即使不主动调试它们。

设置detach-on-fork的命令如下:

(gdb) set detach-on-fork on
(gdb) set detach-on-fork off

如果你想要对一个程序产生的子进程进行调试,你可以使用如下的命令序列:

(gdb) set follow-fork-mode child
(gdb) set detach-on-fork off

这样设置后,当父进程调用fork()并创建子进程时,GDB会自动跟踪子进程并忽略父进程。同时,由于detach-on-fork被设置为off,父进程仍然在GDB的控制之下,你可以随时切回父进程。

值得注意的是,如果在调试过程中想要切换回父进程或者切换到其他子进程,可以使用之前介绍的info inferiors命令来查看所有进程的信息,然后使用inferior命令加上相应的编号来切换焦点:

(gdb) info inferiors
(gdb) inferior NUM

其中NUMinfo inferiors命令显示的进程的编号。这样,就可以在父进程和子进程之间自由切换,对它们进行单独的调试。这些功能在调试多进程程序时非常有用。

此外,也可以使用catch fork命令来在forkclone调用发生时中断程序,然后可以手动选择继续跟踪哪个进程。

(gdb) catch fork
(gdb) catch vfork
(gdb) catch clone

fork, vforkclone事件发生时,gdb会暂停程序,这时可以查看父进程和子进程的信息,并决定要继续跟踪哪个进程。







Alt

Once Day

也信美人终作土,不堪幽梦太匆匆......

如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!

(。◕‿◕。)感谢您的阅读与支持~~~

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐