应用程序调试原理浅析

移动开发
本文主要探讨分析主流硬件平台和操作系统的软件程序Debug原理。

一、Bug和Debug

说起“Debug”,就不得不提及“Bug”这个程序猿和游戏玩家耳熟能详的词,它由美国格蕾丝·赫柏博士第一次提出,当时运行研究数据的Harvard Mark II计算机突然不能正常工作,经赫柏和团队的反复排查,发现是一只飞蛾飞入了电脑的内部继电器中造成短路而引起的故障。修复故障后,赫柏在日记中诙谐地记录下了这件事(图1), “Bug”一词(原意为“虫子”)也逐渐被广泛用于形容计算机程序中隐藏的错误,同时,受到从电脑中驱除飞蛾虫子的启发,计算机术语“Debug”(调试排错)开始使用。

图片

图1

Debug调试覆盖了整个计算机领域,包括不限于数字电路、模拟仿真、嵌入式软硬件以及应用软件,是技术研发人员必须熟练掌握的重要技能,对于产品研发过程的代码纠错和产品质量把控有重要影响,本文主要探讨分析主流硬件平台和操作系统的软件程序Debug原理。

二、调试原理-断点

对于如C、C++等编译运行的可执行程序,其Debug断点调试需要硬件和操作系统的支持,主要依赖以下两点:

(1) 硬件平台和操作系统提供设置断点的方法。

(2) 断点触发系统中断通知到调试器的功能。

对于第一点断点的实现,从计算机体系角度看分为软件断点和硬件断点。软件断点是指向指定的代码位置插入专用的断点指令实现(插桩)。而硬件断点则是通过直接利用CPU核心的调试寄存器实现,此场景主要针对不允许写入操作的ROM只读内存和软件断点无法处理的情况,如中断向量表被破坏等。

图片

图2

不同的硬件架构对应断点实现指令也不相同,如果我们的硬件处理器基于X86系列,其软件断点工作原理是调试器将代码对应位置的原指令的首个字节保存起来,然后写入一条INT3指令(图2)。因为INT3指令的二进制码为11001100b(0xCC),仅有一个字节,所以设置和取消断点时也只需要保存和恢复一个字节。当CPU执行到INT3指令时,将会触操作系统软中断并停止运行当前进程,转而执行内核定义好的中断处理函数。X86的硬件断点使用DR0-DR7调试地址寄存器,但是由于存储断点地址的寄存器数量有限(DR0-DR3),只能设置4个断点。基于ARM系列的断点实现与X86平台类似, 软件断点的工作原理是用HLT或BRK指令的操作码进行指令替换,硬件断点使用内置在core中的比较器,并在执行到达指定地址时停止执行并触发相应中断,和X86一样,由于只提供有限数量的硬件断点单元也存在断点设置数量限制。

对于第二点操作系统的中断通知,以X86平台为例,Windows平台由操作系统软中断触发的对应函数为KiTrap03(),Linux平台则是do_int3()函数,这些函数均为操作系统内核预先定义好的中断处理例程。KiTrap03()会将断点异常通过调试子系统以调试事件的形式分发给用户模式的调试器,并等待调试器的回复,只有调试器确认该异常为“自己”设置的断点后,才会允许挂起被调试进程进行交互性调试。do_int3()例程则是向被调试进程发送一个SIGTRAP信号,当进程接收到SIGTRAP信号后,当前进程让出CPU暂停运行。

三、调试原理-进程交互模型

调试器和被调试进程的如果都位于同一台物理机,即为跨进程调试,反之为远程调试,远程调试是在跨进程调试的基础上增加了一层网络协议交互。由于Windows和Linux的进程描述模型存在一定差异,我们分别介绍这两种平台的调试器进程交互原理。

3.1 Windows

WIN32内核提供了一组系统Api用于支持调试器与被调试进程交互,这里挑几个重要函数进行介绍。

图片

图3

基于WIN32的调试器交互就是通过上述所示的调试函数和一系列调试事件[1]相结合实现。调试器启动后首先通过CreateProcess函数创建待调试进程,或者通过调用DebugActiveProcess函数捆绑到正在运行的进程,在一系列准备操作后就会进入调试循环阶段,调试器会阻塞调用WaitForDebugEvent函数来等待调试事件通知,当有诸如异常事件或dll文件装卸载事件通知到来时,此函数立即返回,返回的事件信息被封装在DEBUG_EVENT结构中,这个结构包含事件的类型、相关进程描述信息和文件句柄等。此时调试器就进入了命令交互阶段,调试器将在自定义的事件处理函数ProcessEvent匹配事件并执行对应事件的回调代码,如果是断点触发这类型操作,被调试目标进程的所有线程都会被操作系统挂起,此时调试器可以调用相关函数如GetThreadContext来获取指定线程的上下文信息。调试器和目标进程地调试信息交互基于Windows进程间同步机制,相关信息可参阅微软相关开发文档[2]


图片

图4

3.2 Linux

相比Windows,Linux作为开源系统可以透过源码更深入地窥探调试器原理,这里以GDB调试为例。

当我们从shell终端对某个已编译C程序文件进行GDB命令调试时,系统首先会创建GDB进程(调试器进程),该进程会fork出一个子进程(调试目标进程),子进程初始化后首先调用关键系统函数ptrace(PTRACE_TRACEME…),使自身进入被追踪模式;同时调用execv函数执行待调试的C程序文件,此时会暂停当前进程的运行,并且发送一个SIGCHLD信号给父进程,父进程接收到SIGCHLD信号后就可以对被调试的进程进行调试。GDB也支持对已存在的进程进行调试,此时将由GDB进程调用ptrace(PTRACE_ATTACH, pid, ...)对被调试进程进入被追踪模式。

图片

图5

ptrace系统函数[3]是GDB交互调试的核心依赖函数,该函数的第一个参数request确定要执行的操作模式,这些操作模式定义了调试器控制读写被调试进程的行为,具体支持的操作模式如下:

图片

图6

借助ptrace函数的强大功能,GDB调试器进程可以对调试目标进程的指令空间、数据空间、堆栈和寄存器的值进行读写,如堆栈打印、变量展示修改等。GDB同时会截获内核通知到被调试进程的几乎所有信号,通过对这些信号的拦截和判定,调试器进程就可以对程序进行断点匹配和单步调试等操作[4]

4、调试器的未来发展

Windows平台的Windbg、Linux的GDB调试器都是功能全面、具有复杂逻辑实现的软件工具,这些debugger调试器因为根植于不同硬件平台和操作系统,存在着底层功能实现和交互模型的显著差异,很明显不适合跨平台发展,而随着Java、Js、python等解释型语言的兴起和云平台的发展,虚拟机调试体系(JDPA、v8 debug protocol)被提出和广泛应用,这种百花齐放的局面让IDE厂家面临着一个非常棘手的问题——调试器交互规范不统一带来的巨大开发难度,微软针对此问题率先提出了DAP(Debug Adapter Protocol)协议,让各厂家IDE(主要是还是服务自家的VsCode)通过相同的协议基于适配器模式与不同语言的debugger通信,力图屏蔽软硬件底层的差异性,降低IDE调试器的开发难度。DAP协议凭借着专业性和普适性得到了业界的一定认可,不过Eclipse和IDEA等JAVA编辑器仍然是直接适配JDPA调试体系的,毕竟软件行业统一规范的背后仍然是各家科技公司行业话语权的争夺。

责任编辑:庞桂玉 来源: 移动Labs
相关推荐

2018-10-25 15:13:23

APP脱壳工具

2012-02-15 13:26:56

IndexedDB

2011-07-20 16:59:07

2009-07-10 17:24:07

Swing应用程序

2009-09-24 17:21:17

加速Hibernate

2009-07-20 16:08:04

ASP.NET应用程序

2009-07-09 16:47:26

Servlet的Web

2010-01-26 13:29:46

VC++应用程序

2010-12-27 17:04:07

应用程序版本升级

2011-11-17 15:17:37

AdobeAIR调试性能

2010-03-02 14:24:00

Android应用程序

2010-02-04 10:17:38

Android应用程序

2009-07-17 13:42:36

Windows CE应WinCE可移植性

2009-09-22 18:39:02

Silverlight

2009-07-14 16:40:31

MyEclipse开发

2010-01-22 13:59:34

Visual C++应

2010-02-22 10:38:09

Python应用程序

2009-08-25 09:39:21

创建C# Window

2010-11-25 10:05:22

Visual StudSilverlightWCF

2011-05-05 18:08:43

云计算Windows AzuPaaS
点赞
收藏

51CTO技术栈公众号