GDB之(5)线程和进程调试
在操作系统的世界里,尤其是在Linux这一广受欢迎的开源系统中,进程(Process)和线程(Thread)是两种基础且核心的执行单位,它们共同构建起了系统进行任务管理和调度的基石。为了理解二者的联系和区别,可以将进程想象为一个工厂,而线程则是工厂里的工人。进程可以被看作是一个程序的运行实例。它拥有独立的内存空间、数据栈以及其他用于跟踪执行的辅助数据。每个进程至少有一个线程,即主线程,但可以包含更
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会列出当前程序中所有的线程,每个线程都会被分配一个唯一的标识符(通常是一个数字序号)。这个列表通常会包含以下信息:
- 线程ID:GDB内部为每个线程分配的一个唯一数字标识。
- 目标系统的线程ID:这通常是操作系统级别的线程标识,比如在Linux系统中对应的可能是轻量级进程ID(LWP ID)。
- 线程的状态:它可能是正在运行、暂停在断点上或其他状态。
- 当前执行位置:通常是函数名称和代码行号,表示该线程目前执行的代码位置。
这样的输出让开发者一目了然地看到每个线程的执行情况,从而可以决定接下来要关注哪个线程。比如,可能发现某个线程处于死锁状态,或者在某个特定的函数调用中暂停了。
这里有一个使用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
命令有三种模式来控制线程的调度:
-
set scheduler-locking off
:这是默认模式,GDB不会干预线程调度。也就是说,即使正在调试一个线程,其他线程也可以根据操作系统的调度策略自由运行。 -
set scheduler-locking on
:在这种模式下,只有当前被GDB暂停的线程可以运行,所有其他线程都将被冻结。这对于调试当前线程的行为非常有帮助,因为它确保了调试过程中没有其他线程的任何干扰。 -
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
类来创建线程局部变量。
线程局部变量主要用于以下场景:
- 维护线程的状态信息,避免传递大量参数。
- 实现线程安全的单例模式。
- 在库函数中,用于存储线程特定的数据,避免影响其他线程。
值得注意的是,尽管线程局部存储提供了很多便利,但它也有一些缺点。例如,由于每个线程都有自己的存储副本,这可能会增加内存的使用量。
此外,线程局部变量的初始化和销毁可能会引入额外的性能开销,特别是在创建和销毁线程频繁的应用中。
2.5 打印errno值
errno
是一个全局量,用于在C和C++程序中记录最近一次系统调用或库函数失败的错误代码。
在Unix-like系统中,errno
是由C标准库提供的,它不是一个简单的全局变量,而是一个宏,它通常扩展为一个函数或者表达式,返回一个指向线程局部存储中实际错误代码的指针。由于errno
是一个宏,直接在GDB中打印它可能不会像打印普通变量那样直接。
在GDB中打印errno
的值,可以使用以下步骤:
-
启动GDB并加载你的程序。
-
如果你的程序还没有运行,使用
run
命令启动它。 -
当程序在某个断点停止或者出现错误时,可以使用以下命令来查看
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
这里,p
是 print
命令的缩写,它用于在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-mode
和 detach-on-fork
是两个与此相关的重要命令。
首先,follow-fork-mode
命令允许设置GDB在遇到fork()系统调用时的行为。fork()
是在Unix-like系统中创建进程的常用方法。当一个进程(父进程)调用fork()
时,它会创建一个新的进程(子进程),子进程是父进程的一个副本。follow-fork-mode
命令有两个模式:
parent
:GDB将继续跟踪父进程,忽略子进程的行为。这是默认模式。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
其中NUM
是info inferiors
命令显示的进程的编号。这样,就可以在父进程和子进程之间自由切换,对它们进行单独的调试。这些功能在调试多进程程序时非常有用。
此外,也可以使用catch fork
命令来在fork
或clone
调用发生时中断程序,然后可以手动选择继续跟踪哪个进程。
(gdb) catch fork
(gdb) catch vfork
(gdb) catch clone
当fork
, vfork
或clone
事件发生时,gdb
会暂停程序,这时可以查看父进程和子进程的信息,并决定要继续跟踪哪个进程。
Once Day
也信美人终作土,不堪幽梦太匆匆......
如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!
(。◕‿◕。)感谢您的阅读与支持~~~
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)