当前位置: 首页 > 科技观察

应用程序调试原理解析

时间:2023-03-17 21:54:01 科技观察

1.Bug与Debug说到“Debug”,就不得不提到“Bug”,一个程序员和游戏玩家都耳熟能详的词。它是由美国的GraceHeber博士首先介绍的。有人提出,运行研究数据的HarvardMarkII计算机突然无法正常工作。经过赫伯和团队的反复调查,发现是一只飞蛾飞进了电脑内部的继电器造成了短路。故障修好后,赫伯在日记中幽默地记录了这件事(图1)。“Bug”(原意为“蠕虫”)一词逐渐被广泛用于描述计算机程序中隐藏的错误。与此同时,受让飞蛾远离计算机的启发,计算机术语“调试”开始使用。图1Debug调试涵盖了整个计算机领域,包括但不限于数字电路、模拟仿真、嵌入式软硬件、应用软件等。是技术研发人员必须熟练掌握的一项重要技能。对产品开发过程中的代码纠错和产品质量控制具有重要影响。本文主要讨论分析软件程序在主流硬件平台和操作系统上的调试原理。2、调试原理——断点对于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指令的操作码替换指令。硬件断点使用内核内置的比较器,当执行到指定地址时停止执行并触发相应的中断。和X86一样,因为只提供了有限数量的硬件断点单元,所以也有断点设置数量的限制。对于第二点,操作系统的中断通知,以X86平台为例,Windows平台操作系统软中断触发对应的函数是KiTrap03(),Linux平台是do_int3()函数.这些函数是操作系统内核预先定义好的中断处理例程。KiTrap03()会将断点异常通过调试子系统以调试事件的形式分发给用户态调试器,等待调试器的回复。只有在调试器确认异常是“自己”设置的断点后,才会允许挂起被调试进程进行交互式调试。do_int3()例程向正在调试的进程发送SIGTRAP信号。当进程收到SIGTRAP信号后,当前进程让出CPU进行挂起操作。3、调试原理——进程交互模型如果调试器和被调试进程位于同一台物理机上,则为跨进程调试,否则为远程调试。远程调试在跨进程调试交互的基础上增加了一层网络协议。由于Windows和Linux的进程描述模型存在差异,我们分别介绍这两个平台的调试器进程交互原理。3.1WindowsWIN32内核提供了一套系统API来支持调试器与被调试进程的交互。下面介绍几个重要的功能。图3中基于WIN32的调试器交互是通过上述调试功能和一系列调试事件的组合实现的[1]。调试器启动后,首先通过CreateProcess函数创建要调试的进程,或者通过调用DebugActiveProcess函数将其绑定到正在运行的进程上。经过一系列准备操作后,进入调试循环阶段,调试器会阻塞调用WaitForDebugEvent函数等待调试事件通知,当有异常事件或dll文件加载卸载事件通知时,该函数立即返回,返回的事件信息封装在DEBUG_EVENT结构中,包含事件类型、相关流程描述信息和文件句柄等。至此,调试器进入命令交互阶段。调试器会匹配自定义事件处理函数ProcessEvent中的事件,执行相应事件的回调代码。如果这种操作是由断点触发的,那么被调试目标进程的所有线程都会被阻塞。操作系统挂了,此时调试器可以调用GetThreadContext等相关函数获取指定线程的上下文信息。调试器与目标进程的调试信息交互基于Windows进程间同步机制。相关信息请参考微软的相关开发文档[2]。图43.2Linux与Windows相比,Linux作为一个开源系统,可以通过源代码更深入地窥探调试器原理。这里我们以GDB调试为例。当我们从shell终端对编译好的C程序文件进行GDB命令调试时,系统会先创建一个GDB进程(debugger进程),该进程会fork出一个子进程(debuggingtarget进程)。子进程初始化后,会先调用关键的系统函数ptrace(PTRACE_TRACEME...),使自己进入traced模式;同时调用execv函数执行待调试的C程序文件。此时会暂停当前进程的运行,并向父进程发送一个SIGCHLD信号,父进程收到SIGCHLD信号后,就可以对被调试进程进行调试了。GDB还支持现有进程的调试。此时GDB进程会调用ptrace(PTRACE_ATTACH,pid,...)进入被调试进程的traced模式。图5.ptrace系统函数[3]是GDB交互式调试的核心依赖函数。该函数的第一个参数请求决定了要执行的操作模式。这些运行模式定义了调试器控制被调试进程读写的行为,具体支持运行模式如下:图6借助ptrace函数的强大功能,GDB调试器进程可以读写调试目标进程的指令空间、数据空间、堆栈和寄存器值,如堆栈打印、变量显示修改等。同时,GDB截获内核通知被调试进程的几乎所有信号。通过截取和判断这些信号,调试器进程可以对程序进行断点匹配、单步调试等操作[4]。4、Windows平台调试器Windbg的未来发展和Linux上的GDB调试器是功能全面、逻辑实现复杂的软件工具。由于这些调试器植根于不同的硬件平台和操作系统,存在底层功能实现和交互模型的显着差异,显然不适合跨平台开发。随着Java、Js、python等解释型语言的兴起,以及云平台的发展,虚拟机调试系统(JDPA,v8调试协议)被提出并广泛应用,这种百花齐放的局面让IDE厂商面临着一个非常棘手的问题——调试器交互规范不一致带来的巨大开发难度。(主要服务于自身的VsCode)通过相同的协议基于适配器模式与不同语言的调试器进行通信,试图屏蔽软硬件底层差异,降低IDE调试器的开发难度。DAP协议已经以其专业性和通用性得到了业界的认可,但是Eclipse、IDEA等JAVA编辑器还是直接适配了JDPA调试系统。毕竟,软件行业统一规范的背后,依然是各家科技公司争权夺利的行业话语权。