Windows编程必学之从零手写C++调试器上篇(仿ollydbg)

C++技术汇编语言

目录

引言

一、调试器实现概览 

1.1 调试器是什么

1.2 调试器的两种类型 

1.3 调试器的核心功能

二、CPU对调试的硬件支持  

2.1 CPU的中断和异常  

2.2 CPU的中断表(IDT)  

2.3 陷阱标志和调试寄存器  

三、异常分发与调试子系统  

3.1 异常分发机制  

3.2 调试子系统 

3.3 调试事件  

四、调试器工作流程  

4.1 调试器与调试子系统的交互

4.2 调试器与用户的交互 

引言

        从零开始手写window平台C++调试器需融合多领域知识:需精通C++语法与x86汇编、内存管理与多线程编程,熟悉Windows API对进程/内存/事件的操控;掌握windows原理、异常分发机制及PE文件格式;理解CPU寄存器、中断/异常原理与调试寄存器功能;还要深入断点(软件/硬件/内存访问)、单步执行及符号解析等调试核心技术。这些知识共同构成从底层机制到上层功能实现的完整链条,是打造高效调试器的必要基础。基础较差的同学请先查看学习前面章节及本人其他专栏(csdn同名博主)。

14ddb17c-0a19-4eab-83dd-71455e060496.png

一、调试器实现概览 

1.1 调试器是什么

        当一个二进制可执行文件被加载器载入内存并开始运行后,如果没有错误,它会一直运行到正常结束。要是运行时出错,而且程序里没有处理异常的逻辑,操作系统就会捕捉到让程序崩溃的异常,还会弹出窗口结束这个进程。 

        想找出程序崩溃的原因,一般办法是查看代码来排查逻辑错误。可要是遇到隐藏的逻辑漏洞,这种办法效率很低。要是能在程序出现异常时,让它在出错的指令那里停下来,用可控的方式一步一步执行程序,同时实时查看内存和寄存器的状态变化,找错就会容易很多。这种通过工具控制程序执行并分析状态的操作叫做调试,实现这些功能的软件就是调试器。

1.2 调试器的两种类型 

(1)汇编级调试器
在展示被调试进程的指令时,只提供将二进制机器码翻译后的汇编语言代码。进行单步调试时,最小单位是一条汇编指令。  

(2)源码级调试器
一般在编译可执行文件时,会生成对应的调试符号文件(例如PDB文件),文件中存储着调试行号、函数名、变量名等信息。源码级调试器通过读取调试符号文件,把二进制代码和源代码关联起来,以源代码的形式显示调试信息,单步调试的最小单位是一条源代码语句(一条源代码通常对应多条汇编指令)。

1.3 调试器的核心功能

(1)异常捕获:当被调试进程出现异常时,调试器需及时接收异常信息。  
(2)执行控制:能对被调试进程的运行状态进行操控,如暂停、继续、单步执行等。  
(3)状态获取:可获取被调试进程的内存数据、寄存器状态等信息。  
(4)符号支持(源码级调试器):需解析调试符号文件,建立二进制代码与源代码的对应关系。  

        被调试进程与调试器是独立进程(各有4GB虚拟地址空间),且被调试程序无需预先添加调试支持代码,两者的交互依赖操作系统底层功能。例如,调试器捕获被调试进程异常的过程为:CPU检测到异常→通知操作系统→操作系统将异常转发给调试器。因此,实现调试器需深入理解程序异常产生机制及CPU与操作系统的异常处理流程。

二、CPU对调试的硬件支持  

2.1 CPU的中断和异常  

        在8086 CPU中,中断被分为两类:内部中断和外部中断。而在80386 CPU以及后续的x86架构中,外部中断被称为“中断”,而内部中断则被称为“异常”。这些CPU最多可以支持256种不同的中断或异常类型,并且通常会在两条指令之间响应这些中断或异常。

        外部中断是由外部硬件设备(例如鼠标、键盘)触发的。当发生外部中断时,硬件会通过中断信号向CPU发送一个8位的中断号,然后CPU根据这个中断号调用相应的处理程序来处理中断。

        内部中断(异常)则是由CPU自身产生的,它们可以进一步细分为以下三类:

        (1)故障(Faults):这类异常通常是可纠正的错误。比如,在访问未加载到内存中的页面时(P=0),异常处理程序可以分配内存并恢复状态,使得CPU能够回到故障指令之前的状态,从而重新执行该指令。

        (2)陷阱(Traps):主要用于调试目的,如单步中断INT 3或者溢出中断INTO等。当陷阱中断发生后,CPU会在栈中保存当前指令的下一条指令地址,以确保在处理完中断后程序能继续从正确的位置开始执行。

        (3)终止(Aborts):这类异常通常表示严重的错误情况,比如硬件故障或者是无效的系统表。在这种情况下,很难确定具体是哪条指令导致了错误,因此程序往往无法从中恢复,操作系统一般会选择终止相关的任务。

CPU内部中断向量表

45316061-4032-4848-91e7-26c4de75d808.png

ab86f371-d7e4-48c2-83af-882cb11de7ee.png

2.2 CPU的中断表(IDT)  

        80386 CPU处于保护模式时,借助**中断描述表(IDT)**来处理中断和异常。IDT是一个数组,最多能容纳256个函数地址,由操作系统在初始化阶段完成填充。当CPU产生内部中断时,会依据中断向量号从IDT中查找对应的处理程序地址。

        举例来说,若应用程序执行空指针赋值操作,引发内存访问异常,CPU就会调用IDT中对应的异常处理程序。此时,操作系统会先保存现场信息(包括栈帧、寄存器等),若存在调试器,就将异常信息转发给调试器;若没有调试器,则把异常交给程序自身的异常处理机制(如SHE、VEH)来处理。

2.3 陷阱标志和调试寄存器  

c1a4e1ca-5ea3-4f4a-8cc4-72611c5e2466.png

(1)陷阱标志(TF,也就是标志寄存器EFLAGS的第1位):要是TF的值为1,CPU每执行完一条指令就会主动触发异常(也就是单步中断),这个机制可用于实现调试器的“单步步入”功能。

(2)调试寄存器(DR0到DR7):  
  • DR0 - DR3:这几个寄存器用于保存断点地址,不过它们是否启用由DR7寄存器来控制。

  • DR7(调试控制寄存器):
    - 读写域(RW0 - RW3):用来指定断点触发的条件,比如执行指令时触发、写数据时触发,或者读写数据时都触发。
    - 长度域(LEN0 - LEN3):用于指定断点地址对应的区域长度,可以是1字节、2字节、4字节,也可能是未定义的长度。
    - 局部断点(L0 - L3):只会在当前的任务(也就是进程)中启用断点。
    - 全局断点(G0 - G3):理论上能在所有任务中启用断点,但调试器一般没办法设置出全局效果。

  • DR6(调试状态寄存器):会记录触发中断的断点来源,其中B0 - B3分别对应DR0 - DR3。

    (3)DR7 寄存器的不同位域控制着断点的不同属性,具体如下:

  • 对于 DR0 对应的断点:
    - 通过 DR7 的 L0 和 G0 来控制该断点地址是局部生效(仅当前任务)还是全局生效。
    - DR7 的 LEN0 用于设置此断点触发中断对应的地址区域长度,可以是 1 字节、2 字节或者 4 字节。
    - DR7 的 RW0 用来指定该断点的中断类型,包括读写操作触发或执行操作触发。

  • 对于 DR1 对应的断点:
    - DR7 的 L1 和 G1 决定该断点地址的生效范围是局部还是全局。
    - DR7 的 LEN1 能设置此断点触发中断的地址区域长度为 1 字节、2 字节或 4 字节。
    - DR7 的 RW1 可指定该断点是因读写操作还是执行操作而触发中断。

  • 对于 DR2 对应的断点:
    - DR7 的 L2 和 G2 控制该断点地址是局部有效还是全局有效。
    - DR7 的 LEN2 可将此断点触发中断的地址区域长度设置为 1 字节、2 字节或者 4 字节。
    - DR7 的 RW2 用于指定该断点是由读写操作还是执行操作触发中断。

  • 对于 DR3 对应的断点:
    - DR7 的 L3 和 G3 确定该断点地址的生效范围是局部还是全局。
    - DR7 的 LEN3 能设置此断点触发中断的地址区域长度为 1 字节、2 字节或 4 字节。
    - DR7 的 RW3 可指定该断点是因读写操作还是执行操作触发中断。

        借助上面说的这些硬件机制,CPU为调试器提供了设置断点、单步执行等核心功能的底层支持,让调试器能够精确控制被调试进程的执行流程,还能捕捉进程的状态变化。

三、异常分发与调试子系统  

3.1 异常分发机制  

        Windows系统把处理异常的函数地址存放在IDT(中断描述表)里,当CPU产生内部中断时,就会调用IDT中对应的函数,Windows从而接管异常处理。异常可能来自3环(用户态)的任何进程,此时操作系统需要定位目标进程:  
如果目标进程处于调试状态,操作系统会找到调试该目标进程的调试器进程,并通过WaitForDebugEvent函数将异常信息发送给调试器进程;要是调试器进程没调用这个函数,就获取不到这些异常信息。  
如果目标进程没被调试,操作系统会调用进程已经注册过的异常处理程序(比如SHE、VEH等)。  

        操作系统接管异常后,根据不同条件将异常分发给相应处理方的这个流程,叫做异常分发。要是没有Windows操作系统,这套异常分发机制就不存在了。

        这里有两个关键问题:Windows怎样判断进程是否处于调试状态?Windows如何确定调试器进程?

3.2 调试子系统 

Windows 调试子系统由以下三部分构成: 

  1. ntdll 中的支持函数
    这类函数用于 3 环(用户态)与 0 环(内核态)之间的通信,是向用户层暴露的 API。  

  2. 内核中的支持函数 
    主要功能是采集和传递调试事件,并对被调试进程进行控制。  

  3. 调试子系统服务器
    承担调试会话和事件的管理任务,是调试消息的核心集散中心。

调试会话建立流程:

  • 当调用创建进程函数并传入调试标志时,调试子系统会执行以下操作:  
    - 为被调试进程的内核对象 调试端口 赋值。  
    - 在调试进程的线程内核对象中添加调试对象句柄。  
    - 完成上述步骤后,创建被调试进程。  
  • 被调试进程创建过程中,内核会调用进程创建、线程创建、模块加载等函数。调试子系统在这些函数中预设检查逻辑:若判断进程处于被调试状态(通过调试端口是否为空判断),则向调试器进程发送调试事件(如进程创建、线程创建、模块加载等)。

参考资料:张银奎《软件调试》第 4 章和第 9 章。

3.3 调试事件  

        Windows把发生在被调试进程里的特定事件叫做调试事件,这些事件由调试子系统捕获后发送给调试器。常见的调试事件有以下这些:

55c7f4fa-7c1c-4006-ad7c-e318003425db.png

四、调试器工作流程  

        调试器一般有两个线程:一个用来等待调试事件,另一个用于和用户交互。不过有些控制台调试器通过单个线程也能实现。下面从两个方面来解析调试器的功能:

4.1 调试器与调试子系统的交互

        调试事件全部由调试子系统产生,调试器需要借助子系统提供的接口,来建立与被调试进程之间的联系。

(1)建立调试会话

        可以通过以下API将普通进程转变为调试器进程:  

  1. CreateProcess
    - 在创建进程时传入调试标志,使新创建的进程成为被调试进程,调用该函数的线程成为调试器线程。  
    - 注意:若调试器存在多个线程,只有调用 CreateProcess 的线程能等待调试事件,其他线程无法接收事件。  
  2. DebugActiveProcess 
    - 用于附加到一个已运行的进程,使其成为被调试进程,功能与 CreateProcess 类似,但适用于已启动的进程。

(2)接收调试事件

        调试器与被调试进程建立合法连接后,调试子系统会在调试事件产生时将信息发送到调试进程(类似窗口消息的传递机制)。  
调试器需要调用 WaitForDebugEvent 函数来等待调试事件,这个函数会使调用它的线程进入挂起状态,直到调试事件到达才会返回,并带回调试信息(这些信息以结构体的形式存储)。 

调试事件与结构体对应关系:

a083c35c-04c0-4e83-9761-4ca0c6fd96ee.png

        调试器的核心功能主要通过对不同类型事件的处理逻辑来体现。

(3)回复调试子系统

        调试子系统在发送事件后会暂停被调试进程的运行,直到收到调试器的回复:  
调试器处理完事件后,需要调用 ContinueDebugEvent 函数向调试子系统回复处理结果,支持以下两种回复类型:  

  1. DBG_CONTINUE:表示事件已处理,调试子系统会让被调试进程从异常发生的位置继续执行(不会调用进程自身的异常处理程序)。  

  2. DBG_EXCEPTION_NOT_HANDLED:表示事件未处理,调试子系统会将异常处理权交还给被调试进程的异常处理程序。

4.2 调试器与用户的交互 

        调试器在接收到调试事件后,需要向用户展示相关信息并接收控制命令,主要操作如下:  

(1)等待输入:接收用户指令,例如查看内存、寄存器状态、反汇编代码等操作请求。  

(2)根据命令操作被调试进程:  
- 通过 ReadProcessMemory/WriteProcessMemory 读写进程内存。  
- 使用 GetThreadContext/SetThreadContext 读写线程环境块(包含寄存器数据)。  
- 设置断点(软件断点或硬件断点)。  
- 执行单步调试(通过设置陷阱标志 TF 实现)。  

        通过上述流程,调试器形成了从事件捕获、用户交互到进程控制的完整调试闭环。

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
大模型解决方案白皮书——智能巡检场景全流程落地指南
当前,智能巡检行业面临着来自供给端同质化竞争的难题和需求端个性化需求、泛化场景管理的新兴诉求,智能巡检企业如何构建差异化壁垒?如何提升产品附加值?如何以更低的创新成本、更高的创新效率响应用户不断升级的需求? 大模型提供了唯一的解决方案——凭借其强大的自然语言处理、图像理解与生成能力,以及对海量数据的学习和推理优势,大模型能够高效率、低成本开发出即好用又好玩的新产品,并实现品牌差异化,这成为突破智能巡检行业发展瓶颈、重构竞争格局的核心驱动力。 本白皮书介绍了豆包大模型携手火山方舟,为智能巡检行业带来的全新解
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论