gdb调试、父子进程调试、Segmentation fault (core dumped)和段错误调试
方法:我认为与IDE相比,GDB没什么好处,如果能用IDE就尽量用IDE。
1.gdb常用命令
可参考:gdb调试
bt:查看函数的调用栈。比如main函数中调用func函数,那么调用栈为:main在栈底,func在main的上方
info b:查看断点的信息
info threads:查看线程信息
2.Segmentation fault (core dumped)和段错误调试
程序运行时试图访问无法访问的内存地址(段错误),程序可能挂掉,但是不返回发生错误的代码的位置。此时在gdb调试的时候引入core文件,就可以查看到发生core dump的位置。
如下代码会发生段错误(参考gdb调试——②调试core文件):
// test.cpp
#include <iostream>
using namespace std;
int* div2(int a, int b) {
int* tmp = nullptr;
return tmp;
}
int main() {
int a = 2;
int b = 1;
cout << *div2(a, b);
return 0;
}
运行:
$ g++ test.cpp
$ ./a.out
Segmentation fault (core dumped)
运行./a.out
以后会生成core文件,如果你没有生成,那么按照如下操作(参考Ubuntu下不产生core文件):
-
资源限制。ulimit -a 可查看core file size,如果为0,则不会生成core,需要改变,如ulimit -c unlimited;
-
/proc/sys/kernel/core_pattern文件定义了core文件的命名规则。若为“|”开头的管道格式字符串,则core会被管道传送处理,从而不会生成core文件。可改名,比如“core"。修改/proc/sys/kernel/core_pattern文件步骤如下:1.
su
进入root;2.执行echo core>/proc/sys/kernel/core_pattern
,从而将名字改为core。 -
/proc/sys/fs/suid_dumpable,该值若是2,则 /proc/sys/kernel/core_pattern必须是绝对路径或“|”开头的管道命令,可改变为其它值,比如
echo "1"> /proc/sys/fs/suid_dumpable
,改为1 -
2,3步骤在root下操作后,切换为普通用户,切换后ulimit -c又会变为默认值0,因此建议先操作2,3后,在做1。
接下来重新执行:
$ g++ test.cpp
$ ./a.out
Segmentation fault (core dumped)
$ ls
a.out core test.cpp
可以看到生成了core文件。但是要定位错误,还要使用调试模式生成可执行文件a.out,即执行下面命令:
g++ -g test.cpp
然后使用a.out和core文件定义段错误出现的地方:
$ gdb a.out core
Program received signal SIGSEGV, Segmentation fault.
main () at test.cpp:12
12 cout << *div2(a, b);
可以发现段错误出现在代码cout << *div2(a, b);
处。
其他方法:Linux平台Segmentation fault(段错误)调试方法,我尝试了,不行,以后时间充裕了再说。
3.调试正在运行的程序
4.父子进程调试
我以为调试子进程,直接在子进程中打断点就可以了,但其实还需要输入set follow-fork-mode child。
set follow-fork-mode parent(缺省)
set follow-fork-mode child
实例:
#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 = 0;
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 = 0;
for (jj = 0; jj < 10; jj++)
{
printf("jj=%d\n",jj);
sleep(1);
}
exit(0);
}
}
运行命令:
(gdb) b 24 # 在第24行(int jj = 0;)设置断点
Breakpoint 1 at 0x12b6: file test.cpp, line 24.
(gdb) set follow-fork-mode child # 调试子进程
(gdb) r
Starting program: /home/ubuntu1/projects/c++/restful_api/test/test
begin
[Attaching after process 42641 fork to child process 42645]
[New inferior 2 (process 42645)]
[Detaching after fork from parent process 42641]
我是父进程:pid=42641.ppid=42594
ii=0
[Inferior 1 (process 42641) detached]
我是子进程:pid=42645.ppid=42641
[Switching to process 42645]
Thread 2.1 "test" hit Breakpoint 1, main () at test.cpp:24
24 int jj = 0; # 子进程在第24行停止运行
(gdb) ii=1
ii=2 # 父进程执行完毕
(gdb) n # 输入n,一步一步调试子进程
25 for (jj = 0; jj < 3; jj++)
(gdb)
·
·
·
如果需要同时调试父进程和子进程,需要先介绍一下detach-on-fork。
set detach-on-fork [on | off]:默认为on,表示调试当前进程的时候,其它的进程继续运行。如果为off, 调试当前进程的时候,其它进程被GDB挂起。
为off时的运行机制:在fork()函数之后,判断follow-fork-mode类型。当follow-fork-mode为parent时,代表调试的是父进程,此时将子进程挂起。
detach-on-fork和follow-fork-mode组合起来的效果如下表:
follow-fork-mode | detach-on-fork | 效果 |
---|---|---|
parent | on | 只调试父进程 |
child | on | 只调试子进程 |
parent | off | 同时调试两个进程,子进程暂停 |
child | off | 同时调试两个进程,父进程暂停 |
info inferiors,查看当前所有进程
inferior <num>,切换当前GDB调试进程,其中num为上一条指令中列出的进程Num
若同时调试两个进程,并且切换进程的效果如下:
(gdb) set detach-on-fork off
(gdb) b 13 # 在第13行(int jj = 0;)设置断点
Breakpoint 1 at 0x1251: file test.cpp, line 13.
(gdb) b24 # 在第24行(int ii = 0;)设置断点
Undefined command: "b24". Try "help".
(gdb) r
Starting program: /home/ubuntu1/projects/c++/restful_api/test/test
begin
[New inferior 2 (process 44543)]
Reading symbols from /home/ubuntu1/projects/c++/restful_api/test/test...
Reading symbols from /usr/lib/debug/lib/x86_64-linux-gnu/libc-2.31.so...
我是父进程:pid=44539.ppid=44462
Thread 1.1 "test" hit Breakpoint 1, main () at test.cpp:13
warning: Source file is more recent than executable.
13 int ii = 0;
(gdb) n # 调试父进程
14 for (ii = 0; ii < 3; ii++)
(gdb) info inferiors
Num Description Executable
* 1 process 44539 /home/ubuntu1/projects/c++/restful_api/test/test
2 process 44543 /home/ubuntu1/projects/c++/restful_api/test/test
(gdb) inferior 2 # 切换到子进程
[Switching to inferior 2 [process 44543] (/home/ubuntu1/projects/c++/restful_api/test/test)]
[Switching to thread 2.1 (process 44543)]
#0 arch_fork (ctid=0x7ffff7fb7810) at ../sysdeps/unix/sysv/linux/arch-fork.h:49
49 ../sysdeps/unix/sysv/linux/arch-fork.h: No such file or directory.
(gdb) n
53 in ../sysdeps/unix/sysv/linux/arch-fork.h
(gdb) n
__libc_fork () at ../sysdeps/nptl/fork.c:78
78 ../sysdeps/nptl/fork.c: No such file or directory. # 这一堆信息应该是代表进入子进程前要经过的初始化函数
(gdb) n
83 in ../sysdeps/nptl/fork.c # 这一堆信息应该是代表进入子进程前要经过的初始化函数
(gdb) n
100 in ../sysdeps/nptl/fork.c # 这一堆信息应该是代表进入子进程前要经过的初始化函数
(gdb) n
102 in ../sysdeps/nptl/fork.c # 这一堆信息应该是代表进入子进程前要经过的初始化函数
(gdb) n
113 in ../sysdeps/nptl/fork.c # 这一堆信息应该是代表进入子进程前要经过的初始化函数
(gdb) n
126 in ../sysdeps/nptl/fork.c # 这一堆信息应该是代表进入子进程前要经过的初始化函数
(gdb) n
129 in ../sysdeps/nptl/fork.c # 这一堆信息应该是代表进入子进程前要经过的初始化函数,下次切换进程就没这些信息了
(gdb) n
main () at test.cpp:9
9 if ( pid != 0)
(gdb) n
22 printf("我是子进程:pid=%d.ppid=%d\n", getpid(), getppid());
(gdb)
参考:GDB调试之多进程/线程
5.多线程调试
调试多线的常用命令:
查看线程: info threads
切换线程: thread 线程id
指定某线程执行某gdb命令: thread apply 线程id cmd
全部的线程执行某adb命令: thread apply all cmd
使用 GDB 调试多线程程序时,默认的调试模式为:一个线程暂停运行,其它线程也随即暂停;一个线程启动运行,其它线程也随即启动。要知道,
这种调试机制确实能帮我们更好地监控各个线程的“一举一动”,但并非适用于所有场景。
一些场景中,我们可能只想让某一特定线程运行,其它线程仍维持暂停状态。要想达到这样的效果,就需要借助 set scheduler-locking 命令。
此命令可以帮我们将其它线程都“锁起来”,使后续执行的命令只对当前线程或者指定线程有效,而对其它线程无效。
set scheduler-locking mode,mode可以为:
off:不锁定线程,任何线程都可以随时执行;
on:锁定线程,只有当前线程或指定线程可以运行;
step:当单步执行某一线程时,其它线程不会执行,同时保证在调试过程中当前线程不会发生改变。但如果该模式下执行 continue、until、finish
命令,则其它线程也会执行,并且如果某一线程执行过程遇到断点,则 GDB 调试器会将该线程作为当前线程。
实例:
#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);
}
效果如下:
**(gdb) b 13 # 在第13行设置断点(if (pthread_create(&pthid1, NULL, pth1_main, (void *)0) != 0))
Breakpoint 1 at 0x11f1: file test.cpp, line 13.
(gdb) b 35 # 在第35行设置断点(for(x = 0; x < 100; x++))
Breakpoint 2 at 0x12c9: file test.cpp, line 35.
(gdb) b 46 # 在第46行设置断点(for (y = 0; y < 100; y++))
Breakpoint 3 at 0x132c: file test.cpp, line 46.
(gdb) r
Starting program: /home/ubuntu1/projects/c++/restful_api/test/test
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, main () at test.cpp:13
13 if (pthread_create(&pthid1, NULL, pth1_main, (void *)0) != 0)
(gdb) n
[New Thread 0x7ffff7d9d700 (LWP 48569)]
[Switching to Thread 0x7ffff7d9d700 (LWP 48569)]
Thread 2 "test" hit Breakpoint 2, pth1_main (arg=0x0) at test.cpp:35
35 for(x = 0; x < 100; x++) # 处于线程1
(gdb) n
[New Thread 0x7ffff759c700 (LWP 48581)]
111 # 主线程输出111
[Switching to Thread 0x7ffff759c700 (LWP 48581)]
Thread 3 "test" hit Breakpoint 3, pth2_main (arg=0x0) at test.cpp:46
46 for (y = 0; y < 100; y++) # 主线程开启了线程2,并在线程2的断点处停止
(gdb) n # 调试线程2
x=0 # 默认的调试模式为:一个线程暂停运行,其它线程也随即暂停,即这里每输入一个n,
# 线程2都前进一步,线程1就开始运行。线程2运行停止,线程1也跟着停止。
48 printf(" y=%d\n", y);
(gdb) set scheduler-locking on # 设定只有当前线程或指定线程可以运行
(gdb) n
y=0
49 sleep(1) ;**
6.运行日志
日志:平时经常使用print来进行代码的调试,日志就相当于将print的内容放入一个文件中,这个文件也叫做日志文件。
参考:C语言gdb调试之精髓