文章目录
- 前言
- 一、GDB 使用
- 1. GDB 介绍
- 2. Debug版本与Release版本
- 3. 指令演示
- 3.1 显示行号
- 3.2 断点设置
- 3.3 查看断点信息
- 3.4 删除断点
- 3.5 开启 / 禁用断点
- 3.6 运行
- 3.7 打印 / 追踪变量
- 4. 最常用指令
- 二、Linux 应用程序调试
- 1. codedump 介绍
- 2. 在 Linux 系统中使用 coredump
- 2.1 开启 codedump
- 2.2 配置生成路径
- 2.3 调试示例
- 3. 在开发板上调试
- 3.1 开启 coredump
- 3.2 调试示例
- 三、Linux 驱动程序调试
- 1. ARM开发中特殊的三个寄存器
- 1.1 堆栈指针R13(SP)
- 1.2 连接寄存器R14(LR)
- 1.3 程序计数器R15(PC)
- 2. 栈回溯
- 3. 利用工具调试
前言
我们在使用 Linux 操作系统做项目的时候,当项目比较复杂,工程比较多的时候,编译运行程序很多时候会出现 bug。
这个 bug 可能在运行时立马出现,导致段错误,也可能运行时直接导致程序崩溃,也可能在运行一段时间后程序崩溃,有时候遇到这种莫名其妙的错误会导致我们无从下手。
那么我们就需要使用调试工具或者方法来快速定位问题,通过调试,开发者可以快速定位和修复问题,减少开发和测试的时间,提高开发效率。
一、GDB 使用
1. GDB 介绍
GDB 是由 GUN 软件系统社区提供的调试工具,同 GCC 配套组成了一套完整的开发环境,GDB 是 Linux 和许多类 Unix 系统的标准开发环境。
2. Debug版本与Release版本
我们在编写代码后运行一般是使用【DeBug】环境进行运行。因为在企业里写软件项目,将代码写完后程序员自己要做简单的测试,保证代码没有问题。
当程序员自己测试完没有问题之后,就会将这个可执行程序给到测试人员进行测试,而且会给出自己的单元测试报告。对于测试人员来说所处的模式是【Release】,也就是将来客户要使用的这款软件的发布版本。
当测试在测的过程中,一定会发现一些问题。此时测试人员就会把报告再打回研发部。研发部做修改重新生成Release版本的可行性程序给到测试人员继续测试。
最后只有当测试通过了,再将生成的【单元测试报告】与产品经理进行核对之后没有问题,那这个软件才可以真正地面向市场。
因此:Release 版本的内存会比 Debug 版本的内存小,因为添加调试信息意味着软件的体积就会变大,占的内存更多。
3. 指令演示
测试程序:test.c
#include <stdio.h>int AddToTop(int top)
{printf("Enter AddToTop\n");int count = 0;for(int i = 1;i <= top; ++i){count += i;}printf("Quit AddToTop\n"); return count;
}int main(void)
{int top = 10;int ret = AddToTop(top);printf("ret = %d\n", ret);return 0;
}
查看可执行文件内存大小:
ls -l
进行 gdb 调试:
gdb debug
3.1 显示行号
直接执行 l ,随机显示 10 行,执行 l 0 或 l 1,表示从第一行开始显示十行,继续显示按 enter 键即可:
l
l 0
l 1
3.2 断点设置
- b + 行号 —— 在那一行打断点
- b 源文件:函数名 —— 在该函数的第一行打上断点
- b 源文件:行号 —— 在该源文件中的这行加上一个断点
b 10
b test.c:AddToTop
b test.c:20
3.3 查看断点信息
执行 info 显示所有调试信息,执行 info b 显示断点信息:
info
info b
其中:
- Num —— 编号
- Type —— 类型
- Disp —— 状态
- Enb —— 是否可用
- Address —— 地址
- What —— 在此文件的哪个函数的第几行
- breakpoint already hit time,表示断点执行次数
3.4 删除断点
- d + 当前要删除断点的编号(Num)
- d + breakpoints
3.5 开启 / 禁用断点
- disable b(breakpoints) —— 使所有断点无效
- enable b(breakpoints) —— 使所有断点有效
- disable b(breakpoint) + 编号 —— 使一个断点无效
- enable b(breakpoint) + 编号 —— 使一个断点有效
3.6 运行
执行 r:
- 无断点直接运行到程序结束
- 再加上断点去运行的话就会在打的断点处停下来
执行 n 和 s:
- n(next) —— 逐过程【相当于F10,为了查找是哪个函数出错了】
- s(step) —— 逐语句【相当于F11,一次走一条代码,可进入函数,同样的库函数也会进入】
3.7 打印 / 追踪变量
- p(print) 变量名 —— 打印变量值
- display —— 跟踪查看一个变量,每次停下来都显示它的值【变量/结构体…】
- undisplay + 变量名编号,取消跟踪
- 我们也可以去追踪一下这两个变量的地址,不过可以看到对于地址来说是不会发生改变的
4. 最常用指令
- until + 行号
- finish —— 在一个函数内部,执行到当前函数返回,然后停下来等待命令
- c(continue) —— 从一个断点处,直接运行至下一个断点处
- bt —— 查看底层函数调用的过程【函数压栈】
二、Linux 应用程序调试
1. codedump 介绍
codedump 文件是指在程序崩溃或异常结束时,操作系统将程序的内存信息、寄存器状态、堆栈信息等保存到文件中以便进行调试和分析的文件。codedump 文件通常包含了程序崩溃时的全部状态信息,可以帮助程序员快速定位程序崩溃的原因并进行修复。
codedump 文件主要包含了用户空间的内存信息,包括用户空间栈、代码段、数据段和堆等。当一个进程因为某种原因(例如,非法内存访问、非法指令等)异常终止时,操作系统可以将进程的内存信息保存到一 codedump 文件中。这个文件可以用于后续调试,以便找出问题的根源。
codedump 文件通常不包含内核空间栈的信息,因为出于安全和隔离的原因,操作系统不会将内核空间的信息暴露给用户态程序。因此,codedump 文件主要用于分析用户空间的程序问题,而不是内核问题。
2. 在 Linux 系统中使用 coredump
当应用程序崩溃时,内核可以生产 coredump 文件:
我们可以使用 coredump 文件来调试应用程序。
2.1 开启 codedump
使用 ulimit -a
命令检查系统 codedump
配置(默认情况下,codedump是被关闭的)
ulimit -a
若输出结果中的"core file size"为"0",则表示Coredump被关闭。
执行以下命令开启:
ulimit -c unlimited
2.2 配置生成路径
开启生成 coredump 的 shell 脚本,配置保存路径:
shell.sh
#!/bin/bashDUMP_PATH=`pwd`# 检查当前用户是否具有sudo权限
if [ "$(id -u)" != "0" ]; thenecho "请使用sudo运行此脚本"exit 1
fi# 配置coredump
echo 2 > /proc/sys/fs/suid_dumpable
echo "$DUMP_PATH/coredump" > /proc/sys/kernel/core_pattern# 创建coredump保存目录
mkdir -p $DUMP_PATH
chmod 777 $DUMP_PATH# coredump功能已开启 配置信息
cat /proc/sys/fs/suid_dumpable
cat /proc/sys/kernel/core_pattern
在模板中,可以使用以下占位符:coredump
/ %e.%p.%t.coredump
- %e:可执行文件名
- %p:进程ID
- %u:当前用户ID
- %g:当前用户组ID
- %s:生成Coredump文件时的信号
- %t:生成Coredump文件时的时间戳
- %h:主机名
在目录下运行脚本:
vi shell.sh
chmod +x shell.sh
sudo ./shell.sh
2.3 调试示例
示例空指针 bug 代码:app_bug.c
#include <stdio.h>volatile int g_val = 0x12345678;int CreatBug(int b, int n)
{int ret;volatile int *p = NULL;printf("in CreatBug\n");*p = 1;ret = b / n;printf("leave CreateBug\n");return ret;
}void D(int n, int m)
{printf("in D\n");CreatBug(n, m);printf("leave D\n");
}void C(int n, int m)
{printf("in C\n");D(n, m);printf("leave C\n");
}
void B(int n, int m)
{printf("in B\n");C(n, m);printf("leave B\n");
}void A(int n, int m)
{printf("in A\n");B(g_val * n, m);printf("leave A\n");
}int main()
{printf("to Creat Bug ... \n");A(100, 0);printf("done\n");return 0;
}
编译运行程序,程序出现了段错误,并生成了 coredump
文件(路径为脚本配置的路径):
根据生成的 coredump
文件进行调试:
3. 在开发板上调试
开发板运行我们的可执行程序时,前提是内核不崩溃(驱动程序不崩溃),才会产生core文件。
3.1 开启 coredump
在开发板上执行:
ulimit -c unlimited
3.2 调试示例
在开发上运行测试程序:
崩溃后直接在板子上调试:
或者将 core 文件移 ubuntu ,在 ubuntu 上进行调试,原理跟上面一样,这样可能 core 文件会出现权限问题:
手动添加权限:
sudo chmod 644 core
解释:
- drwxrwxrwx
- -rw-------
- -rwxrwxr-x
- -:表示这是一个普通文件(非目录)。
- rwx:文件所有者(book)对该文件有读(r)、写(w)和执行(x)权限。
- rwx:文件所属组(book)的用户也有读、写和执行权限。
- r-x:其他用户(不属于 book 组的用户)对该文件有读和执行权限,但没有写权限。
然后就可以进行调试:
三、Linux 驱动程序调试
1. ARM开发中特殊的三个寄存器
在ARM体系中,一般分为四种寄存器:通用目的寄存器、堆栈指针(SP)、连接寄存器(LR) 以及程序计数器(PC), 其中需要着重理解后面三种寄存器。
1.1 堆栈指针R13(SP)
- 每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),都有各自独立的堆栈,用不同的堆栈指针来索引。
- 当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。
1.2 连接寄存器R14(LR)
- 保存子程序返回地址。使用BL或BLX时,跳转指令自动把返回地址放入r14中;
- 子程序通过把r14复制到PC来实现返回,通常用下列指令:MOV PC, LR;BX LR;
- 当异常发生时,异常模式的R14用来保存异常返回地址,将R14如栈可以处理嵌套中断。
1.3 程序计数器R15(PC)
- PC是有读写限制的;
- 没有超过读取限制的时候,读取的值是指令的地址加上8个字节,由于ARM指令总是以字对齐的,故bit[1:0]总是00;
- 在CM3内部使用了指令流水线,读PC时返回的值是当前指令的地址+4;
- 向PC中写数据,就会引起一次程序的分支(但是不更新LR寄存器),CM3中的指令至少是半字对齐的,所以PC的LSB总是读回0;
- 在分支时,无论是直接写 PC 的值还是使用分支指令,都必须保证加载到 PC 的数值是奇数(即 LSB=1),用以表明这是在Thumb 状态下执行;
- 倘若写了 0,则视为企图转入 ARM 模式,CM3 将产生一个 fault 异常。
2. 栈回溯
当驱动崩溃时,内核空间会打印寄存器信息、栈内容:
根据上述信息,我们只能进行纯手工的栈回溯。
先执行以下命令得到反汇编文件:
arm-buildroot-linux-gnueabihf-objdump -D hello_drv.ko > hello_drv.dis
通过内核崩溃打印信息和反汇编文件分析得到出错位置:
或者得到模块的代码段基地址:
cat /sys/module/hello_drv/sections/.text
通过崩溃时 PC 地址 - 代码段基地址 = 1A8,得到具体出错位置:
分析出错时候栈的地方:
在栈里面它保存有返回地址,我们只需要去对应函数的栈找到 LR 的值即可知道函数的调用关系:
通过上图可知LR对应值和调用关系:
- 1f8:C调用D
- 23c:B调用C
- 2c0:A调用B
- 37c:hello_write调用A
3. 利用工具调试
驱动崩溃时打印的串口信息,能否转换为core文件,然后使用gdb进行调试?
答案是可以的,在这里要借助百问网工具进行转换:
添加调试信息:在 Makefile中加以下选项:
KBUILD_CFLAGS += -g
参考内核源码目录 Makefile 文件:
修改 Makefile:
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
KBUILD_CFLAGS += -gall:make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMPILE)gcc -o hello_test hello_test.c clean:make -C $(KERN_DIR) M=`pwd` modules cleanrm -rf modules.orderrm -f hello_testobj-m += hello_drv.o
重新make:
传入支持文件:
执行 make 生成 mcu_coredump
第1步:把串口信息转换为core文件
./mcu_coredump 1.log 1.core
第2步:使用gdb调试内核
arm-buildroot-linux-gnueabihf-gdb ~/100ask_imx6ull-sdk/Linux-4.9.88/vmlinux 1.core
第3步:导入驱动文件
(gdb) add-symbol-file /home/book/nfs_rootfs/code/gdb/driver/01_hello_drv/hello_drv.ko 0x7f154000
0x7f154000为代码段
第4步:使用gdb命令查看驱动运行情况
可以清楚的看到各个函数的调用关系,快速定位代码出错地方。