文章目录
- gdb的常用调试场景
- 调试strip过的程序的注意事项
- 对于多线程进程,只显示一个线程的问题
- gdb的基本原理
gdb的常用调试场景
gdb是一种强大的嵌入式环境下的程序调试工具,其常见的使用场景一般分为两种类型。
一种是直接以gdb启动可执行程序,类似如下流程,然后配置好参数,再直接允许程序,这种场景一般是在调试的过程中,比如调试特定场景下的崩溃或者死锁问题,可以使用这种方式。在出现崩溃时,通过 bt命令打印函数栈,就可以推断出潜在的崩溃风险点。
./gdb a.out
(gdb) set args -v -a=/abc
(gdb) r
Starting program: /home/linux_c_demo/gdb_test/a.out -v -a=/abc
另一种是以gdb 关联(attach)到一个运行时的程序,其使用形式如下。
$ ./a.out &
[1] 21078
$ gdb -p 21078
因为在模拟测试环境或者用户环境下,不太可能默认以gdb的方式去启动进程,而是需要在出现问题后,再对进程的实时运行状态进行查看或者排查。在attach后,可以像以gdb启动程序一样,对执行中的程序进行调试,attach上的第一时间,程序的运行时暂停的。这时可以采取如下3种动作。
1、bt命令查看当前的函数栈的情况
(gdb) bt
#0 0x76e9e338 in nanosleep () at ../sysdeps/unix/syscall-template.S:81
#1 0x76e9e098 in __sleep (seconds=0) at ../sysdeps/unix/sysv/linux/sleep.c:137
#2 0x00010518 in main ()
2、info thread 命令查看当前的线程状态
(gdb) info thread
Id Target Id Frame
* 1 process 21087 "a.out" 0x76de5338 in nanosleep () at ../sysdeps/unix/syscall-template.S:81
以及通过 thread $tid的命令来切换当前所在的线程,再来查看对应线程的栈。
在进程中包含多个线程时,建议使用prctl调用来给每个线程命名,一方面可以通过proc去查看找到线程的id号,另一方面gdb在执行info thread时也会直接显示出线程名,以如下一个极其简单的多线程示例程序为例,通过prctl设置了线程名称,为函数名和入参整数的拼接
#include <stdio.h>
#include <string.h>
#include <list>
#include <unistd.h>
#include <sys/prctl.h>
#include <pthread.h>
static void *
thread_start(void * arg){
int * index = (int *) arg;
char tname[64] = {0};
snprintf(tname,sizeof(tname),"%s_%d",__func__,*index);
prctl(PR_SET_NAME,tname);
while(1){
sleep(*index);
}
}
#define THREAD_LIMIT 3
int main()
{
int iCount = 0;
pthread_t tid;
while (iCount<THREAD_LIMIT){
sleep(1);
pthread_create(&tid,NULL,thread_start,(void*)&iCount);
//printf("for test %d\n",result++);
iCount++;
pthread_detach(tid);
}
pause();
}
上述进程用gdb启动后的栈打印如下
(gdb) info thread
Id Target Id Frame
* 4 Thread 0x76d33460 (LWP 21533) "thread_start_1" 0x76dd3360 in nanosleep () at ../sysdeps/unix/syscall-template.S:81
3 Thread 0x76533460 (LWP 21534) "thread_start_2" 0x76dd3360 in nanosleep () at ../sysdeps/unix/syscall-template.S:81
2 Thread 0x75d33460 (LWP 21535) "thread_start_3" 0x76dd3360 in nanosleep () at ../sysdeps/unix/syscall-template.S:81
1 Thread 0x76f1f000 (LWP 21532) "a.out" 0x76eb198c in pause () at ../sysdeps/unix/syscall-template.S:81
(gdb) thread 4
[Switching to thread 4 (Thread 0x76d33460 (LWP 21533))]
#0 0x76dd3360 in nanosleep () at ../sysdeps/unix/syscall-template.S:81
81 ../sysdeps/unix/syscall-template.S: No such file or directory.
可以发现,具体每个线程的名称在栈中可以被看得明明白白。然后通过线程命令切换即可。
或者可以通过下面的shell命令找到现场的对应的线程号,可以更快速地确认到出现问题的线程的栈以及其他运行信息的情况。以希望找到参数为3的线程名称为例。使用下述命令能精确定位到线程id
cat /proc/$pid/task/*/status | grep $Name -A 5 -B 5 |grep pid
e.g.
cat /proc/21532/task/*/status |grep thread_start_3 -A 5 -B 5
Cpus_allowed_list: 0-3
Mems_allowed: 1
Mems_allowed_list: 0
voluntary_ctxt_switches: 17
nonvoluntary_ctxt_switches: 1
Name: thread_start_3
State: S (sleeping)
Tgid: 21532
Ngid: 0
Pid: 21535
PPid: 20780
cat /proc/21532/task/*/status |grep thread_start_3 -A 5 -B 5 |grep Pid
Pid: 21535
在使用gdb调试运行时线程时,就可以直接attach到对应的线程。
gdb -p 21535
(gdb) info thread
Id Target Id Frame
* 1 process 21535 "thread_start_3" 0x76dd3360 in nanosleep () at ../sysdeps/unix/syscall-template.S:81
3、continue 命令(简写为 c 即可)让程序继续执行,直到出现想要的情况之后,再以ctrl+C的方式打断程序的执行,通过上述两种动作去查看线程的执行状态。
(gdb) c
Continuing.
或者时最终通过quit命令退出调试过程,此时进程仍然会继续执行,只是不再受到gdb的监控和管理。
(gdb) quit
A debugging session is active.
Inferior 1 [process 21078] will be detached.
Quit anyway? (y or n) y
Detaching from program: /home/linux_c_demo/gdb_test/a.out, process 21078
调试strip过的程序的注意事项
在嵌入式环境中因为受到flash空间大小的限制,经常会对程序进行strip操作以删除程序的函数符号表,但是这会使得程序本身不包含函数符号信息,在通过上述bt命令操作时只能得到 十六进制格式的地址,类似0x00010744 。比如上述可执行文件在经过strip后,在调用bt时呈现的打印如下
(gdb) bt
#0 0x76f1798c in pause () at ../sysdeps/unix/syscall-template.S:81
#1 0x00010744 in ?? ()
#2 0x76db2294 in __libc_start_main (Cannot access memory at address 0x0
main=0x7efa1354, argc=1995272192, argv=0x76db2294 <__libc_start_main+276>, init=<optimized out>, fini=0x107bc,
rtld_fini=0x76f6aa14 <_dl_fini>, stack_end=0x7efa1354) at libc-start.c:287
#3 0x0001056c in ?? ()
Cannot access memory at address 0x0
Backtrace stopped: previous frame identical to this frame (corrupt stack?)
可以看到相比之前增加了很多“??”因为对应的函数符号已经在可执行文件中不复存在了,但可以看到大小确实有所减少。
$ nm a.out
nm: a.out: no symbols
$ ls -al a.out*
-rwxr-xr-x 1 xxx xxx 3936 Jan 31 14:57 a.out
-rwxr-xr-x 1 xxx xxx 7100 Jan 31 14:57 a.out.nostrip
此时要注意的是在attach的时候必须要带上与基于原程序相同的SVN或者GIT版本号的,未经过strip的可执行文件。以如下的形式调用命令
./gdb -i $pid a.out.nostrip
e.g.
gdb -p 21705 a.out.nostrip
(gdb) bt
#0 0x76f1798c in pause () at ../sysdeps/unix/syscall-template.S:81
#1 0x00010744 in main ()
如是即可在bt命令时,可以显示出hex格式地址所对应的具体的函数符号。
对于多线程进程,只显示一个线程的问题
有时我们会遇到,通过gdb attach了进程之后,通过info threads只显示了一个线程的情况。这种情况一般gdb在初始化时会报 libthread_db 缺失的情况。因为gdb在初始化调试多线程时需要用到 libthread_db.so.1。而这个可能是因为嵌入式环境,被裁剪了。需要在调试时,将 对应平台的对应文件拷贝到lib目录下。再执行gdb方可成功,一般在交叉编译工具链中直接会带上该文件。
在我的测试树莓派中,该so库是在如下绝对路径地址的。
/lib/arm-linux-gnueabihf/libthread_db.so.1
gdb的基本原理
gdb的核心原理是对ptrace系统调用的调用,linux支持该调用以供某一进程的父进程或者任一其他进程成为该进程的追踪者。以便其他进程可以对该进程,通过信号来暂停进程,并通过ptrace的系统调用命令,在任一特定时刻对进程的函数栈,内存,寄存器等任意资源进行读取和控制。
long ptrace(enum __ptrace_request request, pid_t pid,
void *addr, void *data);