代码调试跟踪与优化(一)--- 如何用GDB 调试代码?

本文介绍了代码调试的重要性和GDB的工作原理,包括GDB的调试模型、GDB与被调试程序的关系。详细讲解了GDB的本地调试和远程调试,强调了GDB调试程序需要的条件,如调试信息和控制权。此外,还提到了GDB的常用调试命令,如断点设置、变量堆栈查看和执行控制,并简单介绍了如何在VS Code中实现GDB的可视化调试。最后,预告了GDB远程调试的内容。
摘要由CSDN通过智能技术生成

前言

我们在开发软件时,免不了引入一些Bug,如何快速定位并解决这些bug 呢?工程师调试跟踪解决这些bug 的过程就像是医生给病人体检治病,医生需要借助各种医疗设备检测病人的各项指标,才能诊断病情分析病因并给出治疗方案。工程师解决这些bug,也需要借助各种调试跟踪技术,通过查看当前的执行指令、内存数据、运行日志等信息,分析出产生bug 的可能原因,再给出解决方案。

这些调试跟踪技术,可以帮我们更清晰的了解代码的执行状态,快速找到执行结果与期望结果不相符的起点或原因,比如控制逻辑或业务逻辑出现漏洞、中间结果被意外更改或损坏、低效的性能瓶颈等。

一、GDB 调试原理

我们初学程序设计时,C/C++/Java 这些静态类型语言都需要先经过编译、链接、执行过程,linux 系统常用的编译链接工具是GCC。程序设计免不了出现一些非预期的Bug,要解决这些bug 首先需要我们获得当前代码足够的执行信息,GNU 也提供了GDB 调试工具,供我们查看代码的执行信息,快速定位产生bug 的代码并解决它。

一般我们在程序开发过程中,代码编辑、编译链接、代码调试这几个工具配合使用,所以很多 IDE(Intergreated Development Environment) 常将这几部分封装在一起,比如windows 平台下的Visual Studio、嵌入式开发常用的Keil MDK 等。这些IDE 工具将常用操作以菜单按钮的形式提供,让我们专注于程序开发过程,对我们隐藏了编译链接和代码调试工具的原理,本文以GDB 为例,简单介绍下代码调试原理。

1.1 GDB 调试模型

目前软件开发主要在x86 平台上进行,但我们开发的目标程序有可能在其它平台比如ARM 上运行,为了方便我们在PC 上调试ARM 主机中运行的程序,GDB 根据调试程序和被调试程序是否运行在同一台电脑中,提供了如下两种调试模型:

  • 本地调试:调试程序和被调试程序运行在同一台电脑中。
    GDB本地调试模型图示

  • 远程调试:调试程序运行在一台电脑中,被调试程序运行在另一台电脑中。
    GDB远程调试模型图示

直接跟用户交互的可视化调试程序常有两种形式:一种是在终端窗口内手动输入调试命令,以字符形式显示调试信息;另一种是在IDE 内点击菜单按钮来代替手动输入调试命令,以图形加字符的形式显示调试信息。前一种形式更方便编写脚本实现自动化调试;后一种形式不需要记忆那么多调试命令,呈现的调试信息更直观、视觉辨识度更高,能提高点调试效率。

远程调试相比本地调试,多了一个GdbServer程序,该程序和目标程序(被调试程序)都运行在目标机(比如一个ARM 主板)中。上图中的红线表示GDB与GdbServer之间通过串口线或者网络进行通讯,用于传输GDB 调试消息的通讯协议可以称为GDB Remote Serial Protocol(GDB RSP)。

GDP RSP 既然是一个通讯协议,自然有标准的报文格式和内容要求,基本的报文格式如下图所示:
GDB RSP协议格式
GDP RSP 报文主要包括四个部分,固定的开始字符(’$’)和结束字符(’#’),中间的调试消息数据以及最后的校验和,我们使用GDB 调试工具并不需要了解的那么详细,这里也就不展开介绍协议报文了(若想了解更详细信息,可参阅文档:Howto: GDB Remote Serial Protocol)。

1.2 GDB 与被调试程序关系

不管是本地调试还是远程调试,GDB 调试程序都需要有两个条件:

  1. 目标程序代码包含必要的调试信息,比如文件名、函数名、变量名、行号等符号表信息,函数堆栈、寄存器等信息。这些调试信息可以在编译阶段设置编译选项添加,比如gcc 编译工具添加"-g" 选项就可以在可执行文件中添加调试信息。由于带调试信息的可执行文件较大,嵌入式开发中资源受限,软件项目常有Debug 和Release 两个版本,前者供调试跟踪,后者更精简;
  2. GDB 可以控制被调试程序的执行,可以访问被调试程序的任何指令和内存数据。比如GDB 可以启动或者接管被调试程序的运行,控制被调试程序在指定条件下停止运行,查看并修改被调试程序的变量值、参数值、执行结果、执行顺序等运行数据。

我们先使用man gdb 命令查看GDB 的简单介绍与用法:
man gdb

GDB 主要有三种启动方式:

  • gdb program:使用GDB 开始执行被调试程序program,可通过GDB 命令控制program 的行为;
  • gdb program core:使用GDB 同时执行被调试程序program 和core 文件(程序异常中止或退出时,保存的内存映像加调试信息文件,包含程序当前的内存、寄存器、堆栈等信息),便于定位分析程序异常中止或退出的原因;
  • gdb attach PID (gdb -p PID):使用GDB 接管(attach)一个正在运行的被调试程序,PID 为被调试程序的process-ID(可通过pidof program 查看),可通过GDB 命令控制program 的行为。

从GDB 与被调试程序间的关系看,GDB 的三种启动方式可以分为两类:一类是由GDB 程序调用执行一个尚未运行的被调试程序program;另一类是由GDB 程序attach 接管一个正在运行的被调试程序program。

前面也谈到,GDB 进程可以控制被调试程序的执行,可以访问被调试程序的任何指令和内存数据。GDB 进程相当于是被调试程序的父进程,GDB 进程对被调试进程program 有绝对的控制权。GDB 是如何调用或者接管一个正在运行的被调试程序呢?

Linux 内核提供了一个用于进程跟踪的系统调用函数ptrace,该函数提供了一个进程(the “tracer”)监察和控制另一个进程(the “tracee”)的方法,它不仅可以监控系统调用,而且还能够检查和改变“tracee” 进程的内存和寄存器里的数据,甚至还可以拦截系统调用。GDB 进程通过系统调用函数ptrace,就可以读写被调试进程program(GDB 进程作为tracer,被调试进程作为tracee)的指令空间、数据空间、堆栈和寄存器的值,接管被调试进程program 的所有信号。这样一来,被调试进程program 的执行就被GDB 进程完全控制了,从而达到调试的目的。

我们使用man ptrace 命令查看ptrace 的简单介绍如下:
GDB ptrace 函数原型与描述
我们知道了,GDB 进程借助系统调用函数ptrace 实现对被调试进程的监察和控制,也就好理解GDB 进程是如何调用或者接管被调试进程program 的了。对于尚未运行的被调试程序,启动GDB 调试进程后,该进程会创建一个子进程(linux 系统通过fork 创建子进程)并调用ptrace 函数,ptrace 函数再由第一个参数获知是启动一个尚未运行的进程(传入参数值 PTRACE_TRACEME)还是接管一个正在运行的进程(传入参数值 PTRACE_ATTACH 或 PTRACE_SEIZE)。GDB 进程启动或接管被调试进程test 的过程图示如下(接管正在运行的进程,GDB 向其发送信号SIGSTOP,正在运行的被调试进程就会暂停执行并进入TASK_STOPED状态,等待被调试):
GDB 进程通过ptrace 函数启动被调试进程图示

所以,不论是调试一个新程序,还是调试一个已经处于执行中状态的服务程序,通过ptrace系统调用,最终的结果都是:gdb程序是父进程,被调试程序是子进程,子进程的所有信号都被父进程gdb来接管,并且父进程gdb可查看、修改子进程的内部信息,包括:指令空间、数据空间、堆栈、寄存器等。

二、GDB 常用调试命令

如何使用GDB 调试程序呢?GDB 提供了一系列命令,我们可以在启动GDB 进程后通过help 命令查看,GDB 支持的调试命令类别如下:
GDB 命令类别
从help 命令的返回结果可以看出,GDB 主要支持12 类调试命令,其中比较常用的有断点设置breakpoints、数据查询data、文件指定与查看files、运行控制running、堆栈查询stack、状态查看status 等六大类。如果想查看某一类具体支持哪些调试命令,可以使用比如help breakpoints 形式的命令查看详情。GDB 支持的调试命令很多,本文只介绍几个最常用的调试命令。

前面已经谈到,要想使用gdb 调试程序,被调试程序需要包含调试信息,如果使用GCC 工具链编译链接程序,则需要添加-g 参数(也可以是-Og 参数)。使用前面介绍的GDB 启动命令,可以根据启动调试后的提示信息判断被调试程序是否包含调试信息:

$ gdb test
......
# 没有调试信息
Reading symbols from test...(no debugging symbols found)...done.
# 包含调试信息
Reading symbols from test...done.

2.1 断点设置命令

启动GDB 调试程序后,一般先设置普通断点、观察断点、捕捉断点等,以便在后续调试过程中,让程序及时暂停在我们关注的地方,查看断点处的数据和状态信息。GDB 常用的断点设置命令如下(可通过help breakpoints 查看支持的断点命令列表,可进一步通过help break 查看某个具体命令的用法):

常用断点设置命令 命令描述
break location 在源代码指定设置location 处设置断点,程序执行到location 处暂停执行。location 可以是行号linenum、函数名function,如果不止一个源文件,还可以在前面加上文件名,比如filename:linenum 或 filename:function。
break location if cond 在源代码指定位置location 处设置条件断点,程序执行到location 处判断条件condition 的真假,若条件condition 为true 则暂停执行,否则继续执行。condition 是一个布尔型表达式,location 含义跟前一条指令中的相同。
watch expression 在程序执行过程中,监控某个变量或表达式(也即expression)的值,当观察到该变量或表达式的值发生变化时,则程序暂停执行。
catch event 在程序执行过程中,监控某个事件的触发,比
  • 5
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流云IoT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值