C语言gdb调试


  在编写程序的时候不可能是一帆风顺的,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
breakb设置断点,b 20 表示在第20行设置断点,可以设置多个断点。
runr开始运行程序, 程序运行到断点的位置会停下来,如果没有遇到断点,程序一直运行下去。
nextn执行当前行语句,如果该语句为函数调用,不会进入函数内部执行。
steps执行当前行语句,如果该语句为函数调用,则进入函数执行其中的第一条语句。 注意了,如果函数是库函数或第三方提供的函数,用s也是进不去的,因为没有源代码,如果是您自定义的函数,只要有源码就可以进去。
printp显示变量值,例如:p name表示显示变量name的值。
continuec继续程序的运行,直到遇到下一个断点。
set varname=v设置变量的值,假设程序有两个变量:int ii; char name[21]; set ii=10 把ii的值设置为10; set name=“西施” 把name的值设置为"西施",注意,不是strcpy。
quitq退出gdb环境。
clearclearclear + n :清除在(n)某一行上设置的所有断点,当某行的断点为多个时,执行一次被全部删除
deleted + num删除第几个断点,num 是断点号。如果要删除5到7号的断点,就 d + 5-7
info + bi 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文件名格式参数列表:

%uinsert current uid into filename 添加当前uid
%ginsert current gid into filename 添加当前gid
%s添加导致产生core的信号
%t文件生成时的unix时间
%h添加主机名
%einsert coredumping executable name into filename 添加程序名
%pinsert 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 日志,它可以避免断点和单点所导致的副作用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值