Linux应用 / 驱动程序崩溃调试

前言

我们在使用 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

image.png

进行 gdb 调试:

gdb debug

3.1 显示行号

直接执行 l ,随机显示 10 行,执行 l 0 或 l 1,表示从第一行开始显示十行,继续显示按 enter 键即可:

l 
l 0
l 1

image.png

3.2 断点设置

  • b + 行号 —— 在那一行打断点
  • b 源文件:函数名 —— 在该函数的第一行打上断点
  • b 源文件:行号 —— 在该源文件中的这行加上一个断点
b 10
b test.c:AddToTop
b test.c:20

image.png

3.3 查看断点信息

执行 info 显示所有调试信息,执行 info b 显示断点信息:

info
info b

image.png

image.png
其中:

  • Num —— 编号
  • Type —— 类型
  • Disp —— 状态
  • Enb —— 是否可用
  • Address —— 地址
  • What —— 在此文件的哪个函数的第几行
  • breakpoint already hit time,表示断点执行次数

3.4 删除断点

  • d + 当前要删除断点的编号(Num)
  • d + breakpoints

image.png

3.5 开启 / 禁用断点

  • disable b(breakpoints) —— 使所有断点无效
  • enable b(breakpoints) —— 使所有断点有效
  • disable b(breakpoint) + 编号 —— 使一个断点无效
  • enable b(breakpoint) + 编号 —— 使一个断点有效

image.png

image.png

3.6 运行

执行 r:

  • 无断点直接运行到程序结束
  • 再加上断点去运行的话就会在打的断点处停下来

image.png

image.png

执行 n 和 s:

  • n(next) —— 逐过程【相当于F10,为了查找是哪个函数出错了】
  • s(step) —— 逐语句【相当于F11,一次走一条代码,可进入函数,同样的库函数也会进入】

image.png

image.png

3.7 打印 / 追踪变量

  • p(print) 变量名 —— 打印变量值
  • display —— 跟踪查看一个变量,每次停下来都显示它的值【变量/结构体…】
  • undisplay + 变量名编号,取消跟踪
  • 我们也可以去追踪一下这两个变量的地址,不过可以看到对于地址来说是不会发生改变的

image.png

image.png

4. 最常用指令

  • until + 行号
  • finish —— 在一个函数内部,执行到当前函数返回,然后停下来等待命令
  • c(continue) —— 从一个断点处,直接运行至下一个断点处
  • bt —— 查看底层函数调用的过程【函数压栈】

二、Linux 应用程序调试

1. codedump 介绍

codedump 文件是指在程序崩溃或异常结束时,操作系统将程序的内存信息、寄存器状态、堆栈信息等保存到文件中以便进行调试和分析的文件。codedump 文件通常包含了程序崩溃时的全部状态信息,可以帮助程序员快速定位程序崩溃的原因并进行修复。

codedump 文件主要包含了用户空间的内存信息,包括用户空间栈、代码段、数据段和堆等。当一个进程因为某种原因(例如,非法内存访问、非法指令等)异常终止时,操作系统可以将进程的内存信息保存到一 codedump 文件中。这个文件可以用于后续调试,以便找出问题的根源。

codedump 文件通常不包含内核空间栈的信息,因为出于安全和隔离的原因,操作系统不会将内核空间的信息暴露给用户态程序。因此,codedump 文件主要用于分析用户空间的程序问题,而不是内核问题。

2. 在 Linux 系统中使用 coredump

当应用程序崩溃时,内核可以生产 coredump 文件:

image.png

我们可以使用 coredump 文件来调试应用程序。

2.1 开启 codedump

使用 ulimit -a 命令检查系统 codedump 配置(默认情况下,codedump是被关闭的)

ulimit -a

若输出结果中的"core file size"为"0",则表示Coredump被关闭。

执行以下命令开启:

ulimit -c unlimited

image.png

2.2 配置生成路径

开启生成 coredump 的 shell 脚本,配置保存路径:
shell.sh

#!/bin/bash

DUMP_PATH=`pwd`

# 检查当前用户是否具有sudo权限
if [ "$(id -u)" != "0" ]; then
  echo "请使用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

image.png

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 文件(路径为脚本配置的路径):

image.png

根据生成的 coredump 文件进行调试:

image.png

3. 在开发板上调试

开发板运行我们的可执行程序时,前提是内核不崩溃(驱动程序不崩溃),才会产生core文件。

3.1 开启 coredump

在开发板上执行:

ulimit -c unlimited

3.2 调试示例

在开发上运行测试程序:
image.png

崩溃后直接在板子上调试:

image.png

或者将 core 文件移 ubuntu ,在 ubuntu 上进行调试,原理跟上面一样,这样可能 core 文件会出现权限问题:

image.png

手动添加权限:

sudo chmod 644 core

解释:

  • drwxrwxrwx
  • -rw-------
  • -rwxrwxr-x
  • -:表示这是一个普通文件(非目录)。
  • rwx:文件所有者(book)对该文件有读(r)、写(w)和执行(x)权限。
  • rwx:文件所属组(book)的用户也有读、写和执行权限。
  • r-x:其他用户(不属于 book 组的用户)对该文件有读和执行权限,但没有写权限。

然后就可以进行调试:

image.png

三、Linux 驱动程序调试

1. ARM开发中特殊的三个寄存器

在ARM体系中,一般分为四种寄存器:通用目的寄存器、堆栈指针(SP)、连接寄存器(LR) 以及程序计数器(PC), 其中需要着重理解后面三种寄存器。

1.1 堆栈指针R13(SP)

R13被用作堆栈指针,用于指向当前堆栈的栈顶位置。

  • 每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),都有各自独立的堆栈,用不同的堆栈指针来索引。
  • 当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。

1.2 连接寄存器R14(LR)

R14是连接寄存器,用于存储函数调用的返回地址。

  • 使用BL或BLX时,跳转指令自动把返回地址放入r14中;
  • 子程序通过把r14复制到PC来实现返回,通常用下列指令:MOV PC, LR;BX LR;
  • 当异常发生时,异常模式的R14用来保存异常返回地址,将R14如栈可以处理嵌套中断。

1.3 程序计数器R15(PC)

R15是程序计数器,用来存放下一条指令的地址。

  • 每次执行一条指令后,PC会自动更新为下一条指令的地址,从而实现程序的顺序执行。
  • 通过修改PC的值,可以实现程序的跳转,例如在函数调用、分支指令执行或异常处理时。

2. 栈回溯

当驱动崩溃时,内核空间会打印寄存器信息、栈内容:

image.png

根据上述信息,我们只能进行纯手工的栈回溯。

先执行以下命令得到反汇编文件:

arm-buildroot-linux-gnueabihf-objdump -D hello_drv.ko > hello_drv.dis	

image.png

通过内核崩溃打印信息和反汇编文件分析得到出错位置:

image.png

或者得到模块的代码段基地址:

cat /sys/module/hello_drv/sections/.text

image.png

image.png

通过崩溃时 PC 地址 - 代码段基地址 = 1A8,得到具体出错位置:

image.png
分析出错时候栈的地方:

image.png
在栈里面它保存有返回地址,我们只需要去对应函数的栈找到 LR 的值即可知道函数的调用关系:
image.png
通过上图可知LR对应值和调用关系:

  • 1f8:C调用D
  • 23c:B调用C
  • 2c0:A调用B
  • 37c:hello_write调用A

image.png

image.png

image.png

image.png

3. 利用工具调试

驱动崩溃时打印的串口信息,能否转换为core文件,然后使用gdb进行调试?

答案是可以的,在这里要借助百问网工具进行转换:

image.png

添加调试信息:在 Makefile中加以下选项:

KBUILD_CFLAGS   += -g

参考内核源码目录 Makefile 文件:

image.png

修改 Makefile:

KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88
KBUILD_CFLAGS   += -g

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o hello_test hello_test.c 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
	rm -f hello_test

obj-m	+= hello_drv.o

重新make:

image.png

传入支持文件:

image.png

执行 make 生成 mcu_coredump

image.png

第1步:把串口信息转换为core文件

./mcu_coredump 1.log 1.core

image.png

第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为代码段

image.png

第4步:使用gdb命令查看驱动运行情况

image.png

image.png

可以清楚的看到各个函数的调用关系,快速定位代码出错地方。

猜你喜欢

转载自blog.csdn.net/m0_74712453/article/details/146326369
今日推荐