目录

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

文章目录

  • 前言
  • [一、GDB 使用](#一、GDB 使用)
    • [1. GDB 介绍](#1. GDB 介绍)
    • [2. Debug版本与Release版本](#2. Debug版本与Release版本)
    • [3. 指令演示](#3. 指令演示)
      • [3.1 显示行号](#3.1 显示行号)
      • [3.2 断点设置](#3.2 断点设置)
      • [3.3 查看断点信息](#3.3 查看断点信息)
      • [3.4 删除断点](#3.4 删除断点)
      • [3.5 开启 / 禁用断点](#3.5 开启 / 禁用断点)
      • [3.6 运行](#3.6 运行)
      • [3.7 打印 / 追踪变量](#3.7 打印 / 追踪变量)
    • [4. 最常用指令](#4. 最常用指令)
  • [二、Linux 应用程序调试](#二、Linux 应用程序调试)
    • [1. codedump 介绍](#1. codedump 介绍)
    • [2. 在 Linux 系统中使用 coredump](#2. 在 Linux 系统中使用 coredump)
      • [2.1 开启 codedump](#2.1 开启 codedump)
      • [2.2 配置生成路径](#2.2 配置生成路径)
      • [2.3 调试示例](#2.3 调试示例)
    • [3. 在开发板上调试](#3. 在开发板上调试)
      • [3.1 开启 coredump](#3.1 开启 coredump)
      • [3.2 调试示例](#3.2 调试示例)
  • [三、Linux 驱动程序调试](#三、Linux 驱动程序调试)
    • [1. ARM开发中特殊的三个寄存器](#1. ARM开发中特殊的三个寄存器)
      • [1.1 堆栈指针R13(SP)](#1.1 堆栈指针R13(SP))
      • [1.2 连接寄存器R14(LR)](#1.2 连接寄存器R14(LR))
      • [1.3 程序计数器R15(PC)](#1.3 程序计数器R15(PC))
    • [2. 栈回溯](#2. 栈回溯)
    • [3. 利用工具调试](#3. 利用工具调试)

前言

我们在使用 Linux 操作系统做项目的时候,当项目比较复杂,工程比较多的时候,编译运行程序很多时候会出现 bug。

这个 bug 可能在运行时立马出现,导致段错误,也可能运行时直接导致程序崩溃,也可能在运行一段时间后程序崩溃,有时候遇到这种莫名其妙的错误会导致我们无从下手。

那么我们就需要使用调试工具或者方法来快速定位问题,通过调试,开发者可以快速定位和修复问题,减少开发和测试的时间,提高开发效率。

一、GDB 使用

1. GDB 介绍

GDB 是由 GUN 软件系统社区提供的调试工具,同 GCC 配套组成了一套完整的开发环境,GDB 是 Linux 和许多类 Unix 系统的标准开发环境。

2. Debug版本与Release版本

我们在编写代码后运行一般是使用【DeBug】环境进行运行。因为在企业里写软件项目,将代码写完后程序员自己要做简单的测试,保证代码没有问题。

当程序员自己测试完没有问题之后,就会将这个可执行程序给到测试人员进行测试,而且会给出自己的单元测试报告。对于测试人员来说所处的模式是【Release】,也就是将来客户要使用的这款软件的发布版本。

当测试在测的过程中,一定会发现一些问题。此时测试人员就会把报告再打回研发部。研发部做修改重新生成Release版本的可行性程序给到测试人员继续测试。

最后只有当测试通过了,再将生成的【单元测试报告】与产品经理进行核对之后没有问题,那这个软件才可以真正地面向市场。

因此:Release 版本的内存会比 Debug 版本的内存小,因为添加调试信息意味着软件的体积就会变大,占的内存更多。

3. 指令演示

测试程序:test.c

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;
}

查看可执行文件内存大小:

bash 复制代码
ls -l

进行 gdb 调试:

bash 复制代码
gdb debug

3.1 显示行号

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

bash 复制代码
l 
l 0
l 1

3.2 断点设置

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

3.3 查看断点信息

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

bash 复制代码
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是被关闭的)

bash 复制代码
ulimit -a

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

执行以下命令开启:

bash 复制代码
ulimit -c unlimited

2.2 配置生成路径

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

shell.sh

shell 复制代码
#!/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:主机名

在目录下运行脚本:

bash 复制代码
vi shell.sh
chmod +x shell.sh
sudo ./shell.sh

2.3 调试示例

示例空指针 bug 代码:app_bug.c

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

在开发板上执行:

bash 复制代码
ulimit -c unlimited

3.2 调试示例

在开发上运行测试程序:

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

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

手动添加权限:

bash 复制代码
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. 栈回溯

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

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

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

bash 复制代码
arm-buildroot-linux-gnueabihf-objdump -D hello_drv.ko > hello_drv.dis	

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

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

bash 复制代码
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中加以下选项:

bash 复制代码
KBUILD_CFLAGS   += -g

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

修改 Makefile:

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:

传入支持文件:

执行 make 生成 mcu_coredump

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

bash 复制代码
./mcu_coredump 1.log 1.core

第2步:使用gdb调试内核

bash 复制代码
arm-buildroot-linux-gnueabihf-gdb ~/100ask_imx6ull-sdk/Linux-4.9.88/vmlinux 1.core

第3步:导入驱动文件

bash 复制代码
(gdb) add-symbol-file /home/book/nfs_rootfs/code/gdb/driver/01_hello_drv/hello_drv.ko 0x7f154000

0x7f154000为代码段

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

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

本文是转载文章,点击查看原文
如有侵权,请联系 xyy@jishuzhan.net 删除
相关推荐
生活百般滋味,人生需要笑对。 --佚名19 分钟前
linux按照nginx
linux·服务器·nginx
Tipriest_1 小时前
linux环境下快速输出电脑的系统/硬件/显卡/网络/已安装软件等信息
linux·网络·电脑·信息输出
多云的夏天1 小时前
ubuntu24.04-qt5-mysql8.0
linux·运维·服务器
一匹电信狗2 小时前
【Linux我做主】基础命令完全指南上篇
linux·服务器·开发语言·c++·开源·ssh·unix
世事如云有卷舒2 小时前
Linux驱动学习笔记(零)
linux·笔记·学习
年轮不改3 小时前
Ubuntu 配置 ffmpeg 开发环境
linux·ubuntu·ffmpeg
编程小小白白4 小时前
Jetson Orin NX jupyter lab的安装和使用
linux·ubuntu·jupyter
乙龙4 小时前
在麒麟系统(基于Ubuntu或Debuntu)的离线环境中创建本地APT仓库
linux·运维·ubuntu·kylin
舰长1154 小时前
Ubuntu docker安装milvusdb
linux·运维·服务器
niuTaylor4 小时前
Linux驱动开发实战之PCIE驱动(一)
linux·运维·驱动开发