在编写程序的时候不可能是一帆风顺的,gcc编译器可以发现程序代码的语法错误,但不能发现程序的业务逻辑错误,调试程序是软件开发的内容之一。调试程序的方法有很多种,例如可以用printf语句跟踪程序的运行步骤和显示变量的值,现在来介绍一个功能强大的调试工具gdb。
一、gdb的安装
CentOS系统中,用root用户登录服务器,执行以下命令安装或升级。
yum -y install gdb
注意,如果您的服务器没有安装gdb,以上命令就会安装最新版本的gdb,如果已经安装了gdb,就会更新到最新版本的gdb,所以,以上命令不管执行多少次都没有问题。
安装gdb,前提条件是服务器必须接入互联网。
二、调试前的准备
用gcc编译源程序的时候,编译后的可执行文件不会包含源程序代码,如果您打算编译后的程序可以被调试,编译的时候要加-g的参数,例如:
gcc -g -o book113 book113.c
在命令提示符下输入gdb book113就可以调试book113程序了。
gdb book113
三、基本调试命令
命令 | 命令 缩写 | 命令说明 |
---|---|---|
set args | 设置主程序的参数。 例如:./book119 /oracle/c/book1.c /tmp/book1.c 设置参数的方法是: gdb book119 (gdb) set args /oracle/c/book1.c /tmp/book1.c | |
break | b | 设置断点,b 20 表示在第20行设置断点,可以设置多个断点。 |
run | r | 开始运行程序, 程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。 |
next | n | 执行当前行语句,如果该语句为函数调用,不会进入函数内部执行。 |
step | s | 执行当前行语句,如果该语句为函数调用,则进入函数执行其中的第一条语句。 注意了,如果函数是库函数或第三方提供的函数,用s也是进不去的,因为没有源代码,如果是您自定义的函数,只要有源码就可以进去。 |
p | 显示变量值,例如:p name表示显示变量name的值。 | |
continue | c | 继续程序的运行,直到遇到下一个断点。 |
set varname=v | 设置变量的值,假设程序有两个变量:int ii; char name[21]; set ii=10 把ii的值设置为10; set name=“西施” 把name的值设置为"西施",注意,不是strcpy。 | |
quit | q | 退出gdb环境。 |
clear | clear | clear + n :清除在(n)某一行上设置的所有断点,当某行的断点为多个时,执行一次被全部删除 |
delete | d + num | 删除第几个断点,num 是断点号。如果要删除5到7号的断点,就 d + 5-7 |
info + b | i b | 查看程序设置的断点 |
bt | 查看函数调用栈 |
注意,在gdb环境中,可以用上下光标键选择执行过的gdb命令。
四、调试core文件
有时候写程序会出现 coredump 的错误,也就是内存溢出,程序挂掉。要去查看是程序中的那个地方导致了内存溢出,可以在GDB调试的时候加上 core 文件,core 文件里面记录了程序挂掉的一些重要信息。
1.修改系统资源限制参数
系统缺省是不会生成 core 文件的,需要去修改系统参数。
ulimit -a : 查看所有系统资源限制参数
u——Unix 系统
limit——限制
a——all 所有
(1)用 ulimt -a 命令查看系统参数,core 文件的默认大小是 0 。
(2)将 core 文件的大小修改为无限制
ulimit -c unlimit
c——core 文件的代码
unlimit——无限制
修改了之后,再次查看系统参数就变为了无限制
2.core 文件存放的路径
(1)有时候系统已经生成了 core 文件,但是忘了是存放在哪个路径下了,可以去查看下面的文件。这个文件控制core文件保存位置和文件名格式。
/proc/sys/kernel/core_pattern
(2)我们也可以修改core文件的保存位置和文件格式名,可通过以下命令修改此文件:
echo "/corefile/core-%e-%p-%t" > core_pattern
或者到 /proc/sys/kernel/core_pattern 文件中直接修改。可以将core文件统一生成到/corefile目录下,产生的文件名为core-程序名-pid-时间戳
以下是core文件名格式参数列表:
%u | insert current uid into filename 添加当前uid |
---|---|
%g | insert current gid into filename 添加当前gid |
%s | 添加导致产生core的信号 |
%t | 文件生成时的unix时间 |
%h | 添加主机名 |
%e | insert coredumping executable name into filename 添加程序名 |
%p | insert pid into filename 添加pid |
3.gdb 调试加上 core 文件
(1)使用方法:
gdb + 可执行程序名 + core文件路径
(2)回车进入调试,就会显示程序导致发生coredump 的位置,也就是第几行代码开始。
(3)可以使用 bt 命令来查看函数的调用栈。
五、调试正在运行中的程序
1.运行示例程序
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int b(int bbb)
{
int ii = 0;
for(ii=0; ii<10000; ii++)
{
sleep(1);
printf("ii=%d\n",ii);
}
return 0;
}
int a(int aaa)
{
b(aaa);
return 0;
}
int main()
{
a(13);
return 0;
}
2.查看进程编号
3.gdb 调试加入进程编号
(1)gdb 调试的方法:
gdb + 可执行程序名 + -p + 进程编号
(2)回车进入调试,程序会停止
(3)使用bt 查看函数调用栈
六、调试多进程服务程序
1.示例程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
printf("begin\n");
if ( fork() != 0 )
{
printf("我是父进程:进程pid=%d,父进程ppid=%d\n",getpid(),getppid());
int ii;
for(ii=0; ii<10; ii++)
{
printf("ii=%d\n",ii);
sleep(1);
}
exit(0);
}
else
{
printf("我是子进程:进程pid=%d,父进程ppid=%d\n",getpid(),getppid());
int jj;
for(jj=0; jj<10; jj++)
{
printf("jj=%d\n",jj);
sleep(1);
}
exit(0);
}
}
2.先用 gdb 进去调试
首先我们要明白调试程序的目的是什么,原因之一就是让程序慢一点执行,或者说可以去控制程序的执行。
(1)进入 gdb 调试,把断点设置在第7行,然后一步一步执行程序,执行到第9行,就fork出一个子进程。
(2)查看程序执行步骤,我们会发现父进程停下来了,子进程先去执行。这说明了gdb 调试多进程程序,默认调试的是父进程,停下来就是让你去调试的。
3.gdb默认调试父进程
(1)gdb 默认调试的是父进程,如果想调试子进程,那么在用gdb调试的时候要增加(修改参数):
set follow-fork-mode child :设置追踪子进程。
(2)进入了gdb调试后,设置这个参数:
(3)查看效果:父进程先执行,子进程停下,执行完了之后再去调试子进程。
4.停止其他的进程
(1)在前面的程序中,当我们想调试子进程,但是父进程是运行的或者是继续运行的。有时候我们不想要其他的进程继续运行,那么就要设置调试的模式。
(2)设置调试模式:
set detach-on-fork [on|off]
缺省是on,表示调试当前进程的时候,其他的进程继续运行。off,表示调试当前进程,其他的进程被 gdb 挂起。
再次查看效果:默认调试的是父进程,其他的进程没有在跑了。
5.查看可以调试的进程: info inferiors
通过查看可以调试的进程,方便后面切换进程。
级别低的就是被压榨的,被调试的。
(1)先进入gdb调试,调试父进程,并且让其他的进程挂起(这里一般要将其他的进程挂起,比较方便查看可以调试的进程)。
(2)在没有fork子进程出来之前,查看可以调试的进程信息,带 * 表示当前调试的进程。
(3)fork 出了子进程之后,再次查看可以调试的进程信息,就把变成了两个
6.切换调试的进程:inferior + Num
(1)可以通过 info inferior 来查看可以调试的进程,当需要切换调试的进程时,就用:
inferior + Num :这个Num 不是进程编号,而是gdb给进程排的序号
(2)当前有两个可以调试的进程,序号是1和2,并且当前是在调试1号进程。
(3)切换调试的进程,切换到2号进程。
经过一顿猛如虎的操作(n)之后,再次查看调试的进程的信息 info inferior,就切换到了2号进程。
七、调试多线程
1.测试代码
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int x=0,y=0; // x用于线程一,y用于线程二。
pthread_t pthid1,pthid2;
// 第一个线程的主函数
void *pth1_main(void *arg);
// 第二个线程的主函数
void *pth2_main(void *arg);
int main()
{
// 创建线程一
if ( pthread_create(&pthid1,NULL,pth1_main,(void*)0) != 0 )
{
printf("pthread_create pthid1 failed.\n");
return -1;
}
// 创建线程二
if ( pthread_create(&pthid2,NULL,pth2_main,(void*)0) != 0 )
{
printf("pthread_create pthid2 failed.\n");
return -1;
}
printf("111\n");
pthread_join(pthid1,NULL);
printf("222\n");
pthread_join(pthid2,NULL);
printf("333\n");
return 0;
}
// 第一个线程的主函数
void *pth1_main(void *arg)
{
for(x=0; x<100; x++)
{
printf("x=%d\n",x);
sleep(1);
}
pthread_exit(NULL);
}
// 第二个线程的主函数
void *pth2_main(void *arg)
{
for(y=0; y<100; y++)
{
printf("y=%d\n",y);
sleep(1);
}
pthread_exit(NULL);
}
2.运行程序
(1)编译时记得链接线程库,运行程序:
查看进程,只看到了一个book3的进程
ps -aux | grep book3
3.查看线程
3.1 查看轻量级进程:ps -aL | grep xxx
轻量级的进程就是线程:p——process:进程; s——state:状态;a——all:全部;L——light:轻
查看轻量级进程状态:ps -aL | grep book3。
(1)执行了该命令,看到了3个线程,一个是主线程(23685),另外两个是子线程(23686,23687).
3.2查看主线程和新线程的关系:pstree -p 主线程id
执行程序,查看线程,再看主线程和新线程的关系:
主线程和子线程的关系用树形展开:
4.调试程序
4.1 设置断点
在程序主函数,线程的主函数的第一行设置断点,那么就是一共设置3个断点。
(1)程序的主函数,在第18行
(2)线程1的主函数,在46行
(3)线程2的主函数,58行
(4)查看断点的信息
4.2 程序运行,查看线程信息:info threads
(1)当程序运行到18行停下来,查看线程信息,现在只有一个主线程
(2)程序继续执行,就创建了一个线程,再次查看线程信息,这时候就多了一个线程。* 代表当前所在的线程
(3)程序继续执行,就又创建了一个线程,再次查看线程信息,这时候又多了一个线程,一共三个线程。* 代表当前所在的线程
4.3 切换线程: thread + 线程号
这里的线程号不是线程编号,而是gdb给线程排的序号。
(1)切换到2号线程:thread 2,再次查看线程信息,就切换到了2号线程(带*号):
4.4 只运行当前线程:set scheduler-locking on
当线程被创建了之后,就去执行线程的主函数。调试一个线程,而不希望其他的线程也继续执行时,可以执行 set scheduler-locking on,其他的线程就会挂起。
4.5 运行全部的线程:set scheduler-locking off
使用了该命令,所有的线程都会执行。
4.6 指定某线程执行某个gdb命令:thread apply 线程序号 命令
当我们只希望某个线程执行某个gdb的命令,比如只让某个线程继续执行,就可以使用下面的指令。注意这里也是线程排序号。
thread apply 线程序号 命令
4.7 全部线程执行某个gdb命令:thread apply all 命令
八、采用输出日志方法调试多进程多线程程序
设置断点或者单步跟踪可能会严重多进(线)程之间的竞争状态,导致我们看到的是一个假象。
一旦我们在某一个线程设置了断点,该线程在断点处停住了,只剩下另一个线程在跑。这时候,并发的场景已经被完全破坏了,通过调试器看到的只是一个和谐的场景(理想状态)。
调试者的调试行为干扰了程序的运行,导致看到的是一个干扰后的现象。既然断点和单步不一定好用,我们还是采用老办法去调试——输出 log 日志,它可以避免断点和单点所导致的副作用。