0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

看一下GDB是通过什么机制来控制指令集的执行

3D视觉工坊 来源:IOT物联网小镇 作者:道哥 2022-11-14 09:54 次阅读

前言

这篇文章我们来聊聊大名鼎鼎的 GDB,它的豪门背景咱就不提了,和它的兄弟 GCC 一样是含着金钥匙出生的。相信每位嵌入式开发工程师都使用过 gdb 来调试程序,如果你说没有用过,那只能说明你的开发经历还不够坎坷,还需要继续被 BUG 吊打。

85c515ea-63a8-11ed-8abf-dac502259ad0.png

我们都知道,在使用 GCC 编译时,可以增加-g选项在可执行文件中嵌入更多的调试信息,那么具体嵌入了哪些调试信息呢?这些调试信息是如何与二进制的指令之间进行相互交互的呢?在调试的时候,调试信息中是如何获取函数调用栈中的上下文信息的呢? 针对上面这些疑惑,道哥用两篇文章把这些底层最深处的问题彻底描述清楚,让你一次看过瘾。

第一篇文章,就是当前这一篇,主要内容是介绍 GDB 的底层调试原理,我们来看一下 GDB 是通过什么机制来控制指令集的执行。 第二篇文章,我们选择一个体积小巧、五脏俱全的LUA语言来进行剖析,从源代码分析到函数调用栈,从指令集到调试库的修改,一网打尽。 内容比较多,看完本文需要的时间可能长一些,为了您的健康,不建议在处于蹲姿的时候阅读这篇文章。

GDB调试模型

GDB 调试包括 2 个程序:gdb 程序和被调试程序。根据这 2 个程序是否运行在同一台电脑中,可以把 GDB 的调试模型分为 2 种:本地调试和远程调试。 本地调试:调试程序和被调试程序运行在同一台电脑中。

85e54ed2-63a8-11ed-8abf-dac502259ad0.png

远程调试:调试程序运行在一台电脑中,被调试程序运行在另一台电脑中。

86170210-63a8-11ed-8abf-dac502259ad0.png

关于可视化调试程序并不是重点,它只是一个用来封装 GDB 的外壳而已。我们既可以使用黑乎乎的终端窗口来调试程序;也可以使用集成开发环境(IDE),这个IDE中已经嵌入了调试器,这样就可以单击各种 button 来代替手动输入调试命令了。

与本地调试相比,远程调试中多了GdbServer,它和目标程序都是运行在目标机中,可能是一台x86电脑或者是一个ARM板子。图中的红线表示 GDB 与 GdbServer 之间通过网络或者串口进行通讯。既然是通讯,那么肯定需要一套通讯协议:RSP协议,全称是:GDB Remote Serial Protocol(GDB远程通信协议)。

关于通讯协议的具体格式和内容,我们不需要关心,只需要知道:它们都是字符串,有固定的开始字符('$')和结束字符('#'),最后还有两个十六进制的 ASCII 字符作为校验和,了解这么多就足够了。

至于更多的细节,如果实在闲的XX可以瞄几眼,其实这些协议,就像社会中各种奇葩的规定一样,都是一帮砖家在厕所里想出来的。

8647c0da-63a8-11ed-8abf-dac502259ad0.png

在第二篇讲解 LUA 的文章中,我们会实现一个类似的远程调试原型。其中的通信协议也是字符串,直接把 HTTP 协议进行简化之后就拿过来使用了,十分清晰、方便。

GDB调试指令

为了完整性,这里把部分 GDB 调试指令贴一下,有感性认识即可。这里没有列举所有的指令,列出的指令都是常用的,比较容易理解。

在讲解 LUA 的时候,我们会选择其中的某些指令进行详细的对比,包括底层的实现机制。

86658fa2-63a8-11ed-8abf-dac502259ad0.png

每一条具体的调试指令,使用的参数还有很多,例如断点相关的就包括:设置断点、删除断点、条件断点、临时停用启用等等。

这篇文章的重点是理解 gdb 底层的调试机制,所以应用层的这些指令的使用方法就不再列出了,网络上的资源很多。

GDB与被调试程序之间的关系

为了方便描述,先写一个最最简单的 C 程序:

8686ab4c-63a8-11ed-8abf-dac502259ad0.png

编译命令:$ gcc -g test.c -o test 我们对可执行程序 test 进行调试,输入命令:$ gdb ./test,输出如下:

87307f46-63a8-11ed-8abf-dac502259ad0.png

在最后一行可以看到光标在闪烁,这是 gdb 程序在等着我们给他下达调试命令呢。当上面这个黑乎乎的终端窗口在执行gdb ./test的时候,在操作系统里发生了很多复杂的事情。

操作系统首先会启动gdb 进程,这个进程会调用系统函数 fork(),创建一个子进程,这个子进程做两件事情:

(1) 调用系统函数 ptrace(PTRACE_TRACEME,[其他参数]);

(2) 通过 execc 来加载、执行可执行程序test,那么 test 程序就在这个子进程中开始执行了。

88618ec8-63a8-11ed-8abf-dac502259ad0.png

补充一点:文中有时称之程序,有时称之进程。“程序”描述的是一个静态的概念,就是一堆数据躺着硬盘上,而“进程”描述的是动态的过程,是这个程序被读取、加载到内存上之后,在操作系统中有一个任务控制块(一个数据结构),专门用来管理这个进程的。

铺垫了半天,终于轮到主角登场了,那就是系统调用函数ptrace(其中的参数后面会解释),正是在它的帮助下,gdb 才拥有了强大的调试能力。函数原型是:

88be2598-63a8-11ed-8abf-dac502259ad0.png

我们先来看一下 man 中对这个函数的简介:

88fbc114-63a8-11ed-8abf-dac502259ad0.png

tracer就是调试程序,可以理解为 gdb 程序;tracee就是被调试程序,对应于图中的目标程序 test。老外一般喜欢用-er和-ee来表示主动和被动的关系,例如:employer 就是雇主(老板),employee 就是苦逼的被雇佣者(打工人)。 ptrace 系统函数是 Linux 内核提供的一个用于进程跟踪的系统调用,通过它,一个进程(gdb)可以读写另外一个进程(test)的指令空间、数据空间、堆栈和寄存器的值。

而且 gdb 进程接管了 test 进程的所有信号,也就是说系统向 test 进程发送的所有信号,都被 gdb 进程接收到,这样一来,test 进程的执行就被 gdb 控制了,从而达到调试的目的。

相当于这样一种情况:如果没有 gdb 调试,操作系统与目标进程之间是直接交互的;如果用 gdb 来调试程序,那么操作系统发送给目标进程的信号就会被 gdb 截获,gdb 根据信号的属性来决定:在继续运行目标程序时是否把当前截获的信号转交给 test,被调试程序 test 就在 gdb 发来的信号指挥下进行相应的动作。

89314fa0-63a8-11ed-8abf-dac502259ad0.png

GDB如何调试已经执行的服务进程

是否有小伙伴会提出这样一个疑问:上面被调试的程序 test 是从头开始执行的,是否可以用 gdb 来调试一个已经处于执行中的服务进程呢?答曰:可以。这就涉及到 ptrace 系统函数的第一个参数了,这个参数是一个枚举类型的值,其中重要的是2个:PTRACE_TRACEME,PTRACE_ATTACH。

在上面的讲解中,子进程在调用 ptrace系统函数时使用的参数是PTRACE_TRACEME,注意橙色文字:是子进程调用ptrace,相当于子进程对操作系统说:gdb 进程是我的爸爸,以后你有任何想发给我的信号,请直接发给 gdb 进程吧!

如果想对一个已经执行的进程B进行调试,那么就要在 gdb 这个父进程中调用 ptrace(PTRACE_ATTACH, [其他参数]),此时,gdb 进程会 attach(绑定) 到已经执行的进程B,gdb 把进程B收养成为自己的子进程,而子进程B的行为等同于它进行了一次 PTRACE_TRACEME 操作。

此时,gdb 进程会发送 SIGSTOP 信号给子进程B,子进程B接收到 SIGSTOP 信号后,就会暂停执行进入 TASK_STOPED 状态,表示自己准备好被调试了。

89600ec6-63a8-11ed-8abf-dac502259ad0.png

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

关于绑定,有几个限制需要了解一下:不予许自我绑定,不允许多次绑定到同一个进程,不允许绑定1号进程。

偷窥GDB如何实现断点指令

大道理已经讲完了,这里我们通过设置断点(break)这个调试指令,来偷窥一下 gdb 内部的调试机制。 还是以上面的代码为例子,这里再重新贴一下代码:

8686ab4c-63a8-11ed-8abf-dac502259ad0.png

来看一下编译出来的反汇编代码是什么样的(编译指令:gcc -S test.c; cat test.S)

89969838-63a8-11ed-8abf-dac502259ad0.png

这里只贴了一部分反汇编代码,只要能说明底层的原理就达到我们的目的了。

上面说到,在执行 gdb ./test 之后,gdb 就会 fork 出一个子进程,这个子进程首先调用 ptrace,然后执行 test 程序,这样 gdb 就称为 test 的父进程了,从而可以接管 test 的所有信号。

我们把源码和汇编代码放在一起,方便理解:

89cd2b32-63a8-11ed-8abf-dac502259ad0.png

现在我们输入调试指令:在调试窗口输入设置断点指令 “break 5”,此时gdb 做 2 件事情:

(1)对第 5 行源码所对应的汇编代码存储到断点链表中。

(2)在汇编代码的第 10 行,插入中断指令 INT 3,也就是说:汇编代码中的第10行被替换为INT3。

89e83008-63a8-11ed-8abf-dac502259ad0.png

然后,在调试窗口继续输入执行指令“run”(一直执行,直到遇到断点就暂停),汇编代码中的 PC 指针(一个内部指针,指向即将执行的那行代码)执行到第10行时,发现是INT 3指令,于是操作系统就发送一个SIGTRAP信号给 test 进程。

(此刻,第 10 行汇编代码 INT3 就被执行过了,PC指针就指向第11行了。)

8a3a5d56-63a8-11ed-8abf-dac502259ad0.png

上面已经说过,操作系统发给 test 的任何信号,都被 gdb 接管了,也就是说 gdb 会首先接收到这个信号。

gdb 发现当前汇编代码执行的是第 10 行,于是到断点链表中查找,发现有第 10 行的代码,说明第 10 行被设置了断点,此刻gdb 又做了 3 个操作:

(1)把汇编代码中的第 10 行 INT3 替换为断点链表中原来的代码。

(2)把 PC 指针回退一步,也即是设置为指向第 10 行。

(3)继续等待用户的调试指令。

8a57fd98-63a8-11ed-8abf-dac502259ad0.png

此刻 test 程序就暂停下来了,PC 指针指向第 10 行,也就是源码中的第 5 行。

从我们调试者角度看,就是被调试程序在第 5 行断点处暂停了下来,我们可以继续输入其他调试指令来 debug,比如:查看变量值、查看堆栈信息、修改局部变量的值等等。

偷窥GDB如何实现单步指令next

还是以刚才的源代码和汇编代码为例,假设此时程序停止在源码的第 6 行,即汇编代码的第 11 行:

8a862c18-63a8-11ed-8abf-dac502259ad0.png

在调试窗口输入单步执行指令“next”,我们的目的是执行一行代码,也就是把源码中第 6 行代码执行完,然后停止在第7行。

gdb 在接收到 “next” 执行时,会计算出第 7 行源码,应该对应到汇编代码的第 14 行,于是 gdb 就控制汇编代码中的 PC 指针一直执行到第 13 行结束,也就是 PC 指向第 14 行时,就停止下来,然后继续等待用户输入调试指令。

8aca27ec-63a8-11ed-8abf-dac502259ad0.png

总结

通过 break 和 next 这2个调试指令,我们已经明白了 gdb 中是如何处理调试指令的了。当然,gdb 中的调试指令还有很多,包括更复杂的获取堆栈信息、修改变量的值等等,有兴趣的小伙伴可以继续深入跟踪。

后面我在写 LUA 语言中的调试库时,会更深入、详细的讨论这个问题,毕竟 LUA 语言更小巧、简单。我也会把 LUA 代码中如何设置 PC 指针的代码部分给小伙伴演示一下,这样我们对于一门编程语言的内部实现就会有更好的理解和掌握,也有可能录一个视频,这样就能更好的讲解 LUA 语言中的内部细节。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • ASCII
    +关注

    关注

    5

    文章

    172

    浏览量

    35089
  • RSP
    RSP
    +关注

    关注

    0

    文章

    3

    浏览量

    6006
  • Lua语言
    +关注

    关注

    0

    文章

    9

    浏览量

    1488
  • gdb调试器
    +关注

    关注

    0

    文章

    10

    浏览量

    1099

原文标题:原来gdb的底层调试原理这么简单

文章出处:【微信号:3D视觉工坊,微信公众号:3D视觉工坊】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    有没有RISC-V的指令集文档分享一下

    有没有RISC-V的指令集文档分享一下?了解一下他的汇编指令
    发表于 03-30 11:48

    CISC(复杂指令集)与RISC(精简指令集)的区别  

    RISC的指令集中指令数反超了CISC,因此,引用指令的复杂度而非数量区 分两种指令集。 当然,CISC也是要通过操作内存、寄存器、运算器
    发表于 07-30 17:21

    常用ARM指令集及汇编

    常用ARM指令集及汇编ARM处理器是基于精简指令集计算机(RISC)原理设计的,指令集和相关译码机制较为简单,ARM7IDMI(-S)具有32位ARM
    发表于 11-23 09:38

    简单介绍ARM的指令集

    ,可能的指令集取决于执行状态:1. AArch64:AArch64 state只支持A64指令集。这是个固定长度的指令集,使用32位
    发表于 08-18 10:58

    ARM的指令集文章集合

    三、指令集如果你想要集中学习一下关于ARM指令集方面的知识(比如下面几个知识点),可以看下下面的文章1、机器码2、运算指令3、控制指令4、汇
    发表于 09-07 22:06

    ARM指令集的特点有哪些呢

    1、ARM指令集ARM指令集格式ARM指令集(ARM920T的核(core)的指令集)的格式如下图所示,可以看出,无论哪
    发表于 12-14 07:24

    为什么个芯片需要thumb和arm两套指令集

    的区别Thumb指令集由16位指令组成,它们作为标准ARM的32位指令子集的简写,每条Thumb指令都可以通过等效的32位ARM
    发表于 05-05 10:38

    ARM指令集和THUMB指令集分别在什么情况使用

    ARM指令集和THUMB指令集分别在什么情况使用,它们有什么区别?通俗的解释一下
    发表于 07-13 11:59

    ARM指令集和THUMB指令集分别在什么情况使用

    懂ARM7的:ARM指令集和THUMB指令集分别在什么情况使用,它们有什么区别?通俗的解释一下
    发表于 11-02 14:20

    ARM指令集详解

    ARM指令集详解 内容提要 ARM指令集 ARM指令集分类与指令格式 ARM指令的寻址方式 ARM
    发表于 03-09 09:39 263次下载
    ARM<b class='flag-5'>指令集</b>详解

    thumb指令集是什么_thumb指令集与arm指令集的区别

    。thumb不是个完整的体系结构,不能指望处理器只执行thumb指令集而不支持arm指令集。 thumb指令集分为:分支
    发表于 11-03 17:34 1.8w次阅读
    thumb<b class='flag-5'>指令集</b>是什么_thumb<b class='flag-5'>指令集</b>与arm<b class='flag-5'>指令集</b>的区别

    Cortex-M系列处理器指令集_指令集特性比较总结

    本文主要介绍Cortex-M系列处理器指令集指令集特性比较总结,具体的跟随小编起来了解一下
    发表于 04-18 16:59 7946次阅读
    Cortex-M系列处理器<b class='flag-5'>指令集</b>_<b class='flag-5'>指令集</b>特性比较总结

    精简指令集和复杂指令集区别

    ),RISC机中采用的微处理器统称RISC处理器。这样一来,它能够以更快的速度执行操作(每秒执行更多百万条指令,即MIPS)。因为计算机执行
    的头像 发表于 08-10 11:17 3.3w次阅读

    ARM处理器的寻址方式和指令集介绍

    Thumb指令集具有较高的代码密度,却仍然保持ARM的大多数性能上的优势,它是ARM指令集的子集。所有的ARM指令都是可以有条件执行的,而Thumb
    发表于 11-24 17:24 33次下载
    ARM处理器的寻址方式和<b class='flag-5'>指令集</b>介绍

    文详解Arm Cortex-M处理器指令集

    的代码密度,却仍然保持Arm的大多数性能上的优势,它是Arm指令集的子集。所有Arm指令都是可以有条件执行的,而Thumb指令仅有
    的头像 发表于 05-17 09:31 6316次阅读