(23)ptype 查看类型
查看一个变量的数据类型
(24)display 跟踪变化
查看某个变量或表达式的值,和 p 命令类似,但是 display 会一直跟踪这个变量或表达式值得变化,每执行一条语句都会打印一次变量或表达式的值。
display 也可以按格式打印,语法和 print 一样,请参照上表(print)。
display 跟踪得变量或表达式也会放入一张表中,使用 info 命令可以查看信息
同样,Num表示编号,Enb表示是否激活,Expression表示被跟踪的表达式。
(25)undisplay 取消跟踪
后面加 Num 编号,删除取消跟踪。其实也可以使用 del 删除。
(26)bt (backtrace)查看栈信息
在一个程序的执行过程中,如果遇到函数调用,会产生一系列一些与函数上下文相关的信息:比如函数调用的位置、函数参数、函数内部的临时变量等。这些信息会被存放在一块称为栈帧的内存空间中,并且每一个函数调用都对应一个栈帧(main 函数也有自己的栈帧,称为初始帧)。这些所有的栈帧都存放在内存中的栈区。通过命令 info frame 可以查看当前使用的栈帧所存储的信息,这里面包含了栈帧编号、栈帧地址、调用者、源码编程语言等信息。通过命令 frame num 、up 、down 可以选的改变栈帧。
查看当前所有栈帧 bt
(27)x 查看内存
同样可以指定按什么格式查看。
(28)disas 反汇编
查看函数 print_array() 的反汇编代码,使用命令 q 退出。
(29)finish
跳出当前所在的函数。
(30)return
忽略后面的语句,立即返回,可以指定返回值 return -1 。
(31)call
调用某个函数,call func() 调用 func() 函数。
(32)edit
进入编辑模式
(33)search
search 搜索,reverse-search 反向搜索。
2. GDB跟踪可以正常编译运行的源文件
(1)调试非运行状态的可执行程序
这个很简单,我们前面介绍命令时,所举的例子,都是在这种情况下进行的。也就是对编译好的可执行文件进行调试。
进入gdb调试,然后用上面介绍的命令进行调试即可。
(2)调试一个正在运行的程序
有时候我们运行一个一直执行的程序时,希望能够调试这个程序。比如某个带有无限循环打印某些信息的程序。
我们可以这么做,首先编译生成可执行文件,然后在运行时加 & 让进程转为后台执行,或者通过 SecureCRT 克隆会话来新打开一个会话进行调试。
① 首先通过 ps 命令查看进程号,找到 loop 进程的进程信息
② 通过gdb的 -p 参数,指定进程进入调试
③ 正在运行的程序会暂停,可以正常调试了
3. GDB跟踪core(调试挂掉的程序)
(1)什么是 core dump 核心转储
core是指core memory,dump即堆放。core dump就是核心转储的意思。在Unix系统中,经常会将主内存 main memory 称为核心 core,而核心映像 core image 是指进程执行时的内存状态。当程序发生错误或者异常或者收到某些信号而终止执行的时候,操作系统会把核心映像写入一个文件(core 文件)来作为调试依据,这就是核心转储 core dump。
换句话说,当我们写的程序在运行时发生异常而退出的时候,由操作系统把程序当前的内存状况存储在一个core文件中,这就叫core dump。也就是说,所谓core dump核心转储,就是当我们写的程序当掉(异常退出)时,把程序当前的内存状况存储起来,以作为调试的参考的这么一种技术。
(2)产生 core dump 的原因
主要原因可以分为三大类:
① 访问越界
包括数组下标越界,C语言字符串无结束符引起的越界,使用非法指针(空指针NULL、野指针、未初始化的指针、越界指针)等。
② 多线程
多线程访问全局变量未加同步机制(锁机制等),或使用了线程不安全的函数。
③ 堆栈溢出
使用了太大的局部变量或无限嵌套、递归调用函数,可能会造成栈溢出。
(3)core 文件的相关配置与 shell 资源限制
我们先准备一个有问题的程序
编译并运行这个程序,程序发生 core dump,但是我们并没有找到 core 文件
这是因为,默认情况下 core 文件被 shell 限制大小为0了,所以我们看不到 core 文件,可以通过 ulimit 命令查看限制
实际上,ulimit 是 shell 的一个命令,通过这个命令可以查看 shell 对各种资源的限制,比如 -a 选项可以查看所有限制
第一条就是 core 文件的限制,大小被限制为0。我们可以去改变它的大小限制,最简单的方法就是改为无限制,无限制就相当于可以是任意大小。
ulimit -c unlimited
再次查看 shell 的限制就能看到,现在 core 的限制变为 unlimited 了
我们现在再一次运行刚才的 err 可执行文件,就可以看到生成了一个 core 文件
作为一个优秀的程序员,我们可能决定还不够好,这名字是啥呀 core.9546,怪怪的,我们希望他有一个符合我们心意的名字,这也可以实现,我们可以修改 core 的配置文件 /proc/sys/kernel/core_pattern ,那你改吧,你发现改完保存不了。
因为这个文件是不能写入的,我们可以借助重定向来修改这个文件
echo "core-%e-%t" > /proc/sys/kernel/core_pattern
关于里面的参数,列表如下
参数 | 含义 |
%p | 添加 pid |
%u | 添加 uid |
%g | 添加 gid |
%s | 添加导致 core dump 的信号 |
%t | 添加 core 生成的时间 |
%h | 添加主机名 |
%e | 添加命令名 |
注意,core 文件是执行可执行文件时,产生 core dump 后才会产生的一种文件,所以要先执行可执行文件,产生 core dump,这样才能得到 core 文件。
(4)通过core文件调试当掉的程序
使用 gdb 可执行文件名 core文件名 进入gdb调试
where 命令查看出错的位置
4. GDB调试多线程
(1)创建一个多线程测试文件
创建一个测试文件,代码如下,本人 Linux 专题系列有线程专题与进程专题,本文只做一个简单的线程创建。
#include <stdio.h> #include <unistd.h> #include <pthread.h> #include <stdlib.h> void* thread1() { printf("this is thread1...\n"); for(;;) { sleep(1); } } void* thread2() { printf("this is thread2...\n"); for(;;) { sleep(1); } } int main(int argc, char* argv[]) { pthread_t tid1; pthread_t tid2; printf("this is main..."); pthread_create(&tid1, NULL, thread1, NULL); /*创建线程1*/ pthread_create(&tid2, NULL, thread2, NULL); /*创建线程2*/ /*等待线程结束*/ pthread_join(tid1, NULL); pthread_join(tid2, NULL); return 0; }
(2)undefined reference to `pthread_create’ 错误
上面的文件创建好之后,如果直接编译,会报错undefined reference to `pthread_create’
这是因为,<pthread.h> 并非 Linux 系统的默认库,而是POSIX线程库。在Linux中将 <pthread.h> 作为一个库来使用的话,要加上 -l pthread 来显式链接该库。
这样编译就通过了。
(3)多线程调试
① 首先,运行 ttt 可执行文件,这里也会显示主进程 ID
② 然后用 SecureCRT 克隆会话或在 Linux 下直接打开一个新的终端,在另一个会话中查看进程 ID
查看主线程的线程树 pstree ,可以看到两个子线程的线程 ID
③ 查看线程栈信息,pstack
④ 进入 gdb 调试
查看线程
切换线程,根据 info 查看到的编号来切换,我们可以通过线程 ID 来判断是否切换
⑤ 打断点等等指令与之前讲的无异,这里讲一些用于线程的命令
(gdb)thread apply num n 让线程 num 继续执行,num 是线程的编号,用info查看
(gdb)set scheduler-locking on 只执行当前线程,输入 n 继续执行
(gdb)set scheduler-locking off 所有线程并发执行
总结
熟练掌握 gdb 调试是一个高水平程序员的基本技能,其实我们用习惯了 IDE 中的调试器之后,反而越来越忽视 gdb 这种命令行的调试。但是实际上,熟练掌握 gdb 会对调试程序本身产生更深刻的理解,可以大大提高程序调试水平。如果这篇文章大家觉得有帮助,可以关注我的 Linux 专栏,里面有更多 Linux 相关的优质文章。“纸上得来终觉浅,绝知此事要躬行”,学习 Linux 知识的同时,一定要动手练习,亲自去调试一些程序,只能理解这只指令是怎么执行的。