wenmo8 发布的文章

从38 6 开始 , IA 一3 2 处理器 内部都含有 8 个 3 2 位的调试 寄存器 DR0一 DR7 (如 图 1 所 示 )。 除了 DR4 和 DR5 保留 外 , 其它 6 个寄存器 分别是 :

  • 四 个 3 2 位的调试地址 寄存器 (DR0~DR3)
  • 一个 3 2 位的调试控制寄存器 (DR7) 
  • 一个 3 2 位的调试状态寄存器 (DR6) 

通过 以上寄存器可 以最多设置 4 个断点, 其基本分工是DR0一DR3 用来指定断点的内存 (线性地址) 或l/ O 地址 。 DR7 用 来进一步定义断点的中断条件。 DR6的作用是当调试事件发生向调试器 《d e b g u g , ) 报告事件的类别和属性 . 以供调试器判断发生 的是何种事 件 (哪个断点 、 单步跟踪 、 断点属性等等 )。 下面分 别详细介绍 D R 7 和DR6 的用法 。

3 2 位的DR7寄存器 中 , 有2 4 位是被划分成 4 组分 别与 4 个调试地址寄存器相对应的.比如 L0 . G0 . R/ W0 和 LEN0这 六位都是 与DR0相对应的 .L1 . G1. R/ W1 和 LEN1 这六位都是与DR1相对应的 依此类推。表 1 列出了DR7 中各个位的具体含义。

调试状态寄存器 DR6 是 当 CPU 检测到匹配 断点 条件的断点时 . 用来向调试器的断点异常处理程序传递该断点异常的具体情况 . 以使调试器可 以很容易 的识别 出发 生的是什么断点 。 比如如果 BO被设置为1, 那么就说 明满足 DR0、 L E N 0、和 R/ W0 所定义条件 的断点发 生了 。 下面的表分 别列出了 DR6 中各个位的具体含义。

通过表 1 的定义可 以看出 . 调试控制寄存器的各个位域提 供了很灵活 的方式 .允许我们通过不同的组合定义出各种复杂的断点条件。 下面我们先进一步介绍一下读写域R/ Wn , 通过 对它的设里 , 我们可 以指定要定义的断点的访问类型 (数据 、代码还是 l/ 0 ) . 即断点的访问条件 :
1、读写内存中的数据时中断 , 这种断点又被称为数据访 问断点 (data breakpointer )。 利用数据访问 断点 可 以监 控对全局变量 , 或局部变量的读写操作。 例如 . 在进行某些复杂的系统级调试 . 或者调 试多线程程序时 , 我们不知道哪个线程在何时修改了某一变量,这时我们就可 以设置一个数据访问断 点。 现代调试器大多还 都支持复杂的条件断点, 比如 当某个变 量等于某个确定的值时中断 . 这其实也是利用数据访问断点实 现的 . 其基本思路是设置 一个数据访问断点来监视这个变量 , 每次这个变量改变时 CPU 都会调用调试器 的中断处理程序 ,调试器检查这个变量的值 . 如 果不 满足规定的条件就立刻返回让 CPU 继续执行 。 如果满足 , 就中断到调 试环境。
2、执行 内存中的代 码时 中断. 这 种断点 又被称为代码访问断点 (Code Br e a k Po in t 》或指令断点 (i n s tr u e t io n b r e a k po in t ) 。 代码访问断点从实现 的功 能上 看与软件断点类似 , 都是 当 C PU 执行到某个地址 的指令时 中断。但是通 过寄存器 实现的代码访问断 点具有一个软件断点无法 实现 的优点 , 就是不 需要 软 件断点那样向 目标代码处插入指令 。 这个优点在某些 情况 下非 常重要 。 例如 . 当我们调 试位于 RO M (只读存储器 ) 上的 代码 《比 如 BIO S 中的 PO S T 程序 ) 时 根本没 有办法向那里 插入软件断点 ( lNT 3 ) 指令 . 因为 目标内存是只 读的 。 另外软件断点的另 一个局限是 只有 当 目标代码被加载进内存后才可 以 向该区域设置软件断点。而调 试寄存器 断点没有这 些限 制. 因为只 要把需 要 中断的内存地址 放入调试地址 寄存器 (DR0一DR3 ) . 并设置好调试控制寄存器 ( DR7 ) 的相应位就可 以 了。

3、读写 I/O(输入输出 ) 端口时中断 .这种断点又被称为I/O访问断点 ( I  /O   b r e a kp oi n t ) 。I/ O 访问断点对于调试设计使用输入输出端口的设备驱动程序非常有用。 也可以利用 I/O 访问断点来监视对I/O空间的非法读写操作 . 提 高系统的安全性 。 因为某些恶意程序在实现破坏动作时 . 需要对某些 I/O 端 口进行读写操作 。

读写域定义 了要监视的访问类型 . 地址 寄存器(DR0一DR3 ) 定义 了要监视的起始地址 。 那么要监视的区域长度呢 , 这便是长度域 L E N n (n= 0, 1, 2,3 . 位于 D R 7 中 ) 的任务。L EN n 位段可 以指定 1,2,4 或 8 字节长的范围。 需要说明的是 :
1、对于代码访 问断点 . 长度域应该为00,代表一字节长 度 。 另外地址寄存器应该指向指令的起始字节 。 也就是 CPU 只会用指令的起始字节来检查代码 断点 匹配 。
2、对于数据和 I/O访问断点 . 有两点需要注意 :
第一 只 要 断点区域中的任一字节在被访问的范围 内 . 都会触发该断点。
第二 , 边界对齐要求 . 两字节区域必 须按字 (w o r d ) 边界对齐 : 四字节区域必须按双 宇 (d o u b l e w o r d ) 边界对齐 : 八字 节区城必 须按四字 (qu a d w o r d ) 边界对齐。 也就是说 . CP U 在 检查断点匹配时会 自动去除相应数量的低位 。 因此如果地址没 有按耍求对齐可能无法实现预期的结果。 例如 假设希望通过将 DR0 设为0 x A 003.L E N 0 设为 1 1 (代表 4 字节长)实现任何对0 x A00 3 ~0 x A c 0 6 内存区的写操作都会触发断点: 那么只有当0 x A 00 3 被访问时会触发断点 . 对0x A 00 4、0x A 00 s 和0x A006 处的内存访问都不会触发断点 。 因为长度域指定的是4 字节 所以CPU 在检查地址匹配时 . 会自动将起始地址0x A003 的低4 位屏蔽掉 , 也就是会被看作是0x A00 )。 表3 给出了更多的例子 用来说明断点的触发条件 。

因为以上介绍的断点不需要像软件断点那样向代码中插入 软件指令 , 依靠处理器本身的功能便可 以实现 . 所以人们习惯 上把这些使用调试寄存器 (DR0~DR7 ) 设置的断点叫硬件断点 ,以与软件断点区别开来 。
l A 一 32 处理器专门分配了两个中断向量来处理调试异常 向量1 和向量3。 向量3 用来处理 INT 3 指令产生的断点异常。 向量1 用来处理调试异常(de b u g e x c e pt io n . 简称 # DB ) 。 硬件断点产生的是调试异常 .所以硬件断点发生时C U P 会执行 1 号向量对应的处理例程 。

硬件断点其 有很多优点 , 但是也有不足 , 最明显的就是数量限制因为只用 4 个调试地址寄存器 . 所以 lA 一3 2 Cpu 允许 最多设置 4 个硬件断点。 这基本可以满足大多数情况下的调试 需要 。

另一点祷要说明的是 . 只有在实模式或保护模式的内核优先级( ring0 ) 下 才能访问调试寄存器 . 否则便会导致保护性 异常。 这是 出于安全性的考虑. 那么像 v i s u a . s t u d i o 这样的用 户态调试器是如何设一硬件断点 (VC 6 支持数据访问断点。 没 必 要也不支持 l/ O 访问断点 , 因为从 Win d o w s 9 8 开始用户态 下不允许进行直接 l/ O 读写 ) 的呢 ? 答案是通过访问线程 的C ON E T X T 数据 (每个线程被挂起时 . 寄存器等上下文信息都被 保存起来 . 当该线程恢复执行时 . 寄存器会被恢复回来) 来间 接访问调试寄存器 。

下面给出一个C + + 例子演示一下如何手工设置 数据访问断点.

 

 

两期我们分别介绍了通过 INT3指令设置的软件断点 和通过调试寄存器设置 的硬件断点。 除了断点 . 还有一类常用的方法使C P U 中断到调试器 . 这便是调试陷阱标志 (debug trap  f l a g ) 。 也就 是 当这些陷阱标志被设置时, 一旦有符合陷阱条件的事件发生,就会陷入到调试器 。 IA 一3 2 处理器所支持的调试陷阱标志可以概括如下

  • 80866就支持的单步执行标志 (标志寄存器E F a g I s 的一位)
  • 3 8 6 引入的任务状态陷阱标志(任务状态段TSS 的T标志)
  • 奔腾Pro引入 的分支到分支单步执行标志 (D e b u gC t !寄存 器 中的BT F 标志)

下面分别详细介绍 。

单步执行标志 (T F )

从 8 0 8 6 C pU 开始 , x 86 系列处理器的标志(EF Ia g s ) 寄存器 中便有一个陷阱标志位 ( b i t 8 )名为Trap Flag 简称T F。当 T F 位为 1 时 CPU 每执行完一条指令便会产生一个调 试异常 《# DB ) . 中断到调试器的调试异常处理 程序。 这就是 通 常所说 的单步执行 。 调试异常的向量号是 1 因此 T F 标志引发的是 1 号异常服例程 。在 80 86 和 2 8 6 时代 , 这个服务 例程是专门用来处理 单步事件的。 从 3 8 6 开始 . 硬 件断点发.生时也会产生调试异常 . 调 用 1 号服务例程 . 但可利用调试 状态寄存器(DR66 ) 来识别发生的是何种事件 。 为了表达方便我们把因 T F标志 触发的软件异常称 为单步异 常。

单步异常属于陷阱类异常。 也 就是 C P U 总是 在执 行完导致此类异常的指令后 才产生该异常。 这意味 着 当因单步异常中断到调 试器 中时, 导致 该异 常的指令已经执行 完毕 了 。 软件 断点 异常(#BP) 和硬 件断点中的数据及I/O 断点 也是 陷阱类异常. 但是硬 件断点 中的指令访 问异 常是错误类异常 . 也就是当由于此异常而 中断到调试器 时 . 相应调试地址寄存器 DRn中所指地址 处的 指令还 没“执行 。 这是 因为CPU是 在取指操作时进行此类断点 匹配的 。

CPU是何 时检查 TF标志的呢?’IA-32手册的原 文是 "while an instruction is begin executed" 也就 是在执行一个指令的过程 中。 尽管没有说过程 中的哪个阶段 (开 始 、 中间还 是末尾 ) . 可以推测应 该是 一条指令即将执行完毕的时 候 。 也就是说 当CPU在 将执行完一条指令的时候检测TF位 ,如果该位为 1那么 CPU会先清除此位 , 然后准备产生异常。 但 是 这里 有个例外 , 对于 那 些可 以设置 TF 位的指令 (例如 POPF) CPU 不会在执行这些指令期间做以上 检查。 也就是说这些 ,旨令不会立刻产生单步异常 . 而是其后的下一 条指令将 产生单步异常。
因 为 C P U 在进入 异 常 处理 例 程前会 自动 清除 T F标 ,因此 当 CPU中断到调 试 器 中后 再观 察 TF标 志 . 它总是0。

既然调试异常的向量号是 1 ,可 不可 以像 INT 3 那样通过在代码 中插入INT 1 这样的指令来实现手工断点呢 , 对于应用 程序答案是否定的。 INT 3尽 管具有INTn的形式 . 但是它具有独特的单字节机器码 . 而且 其作用就是产生一 个断点异常 一(#BP ) 。 因此 系统对其有特别的对待 .允 许其在用户模式下执行。 而 INT 1则不然, 它属于普通的 INTn 指令 机器码为 CD01。 保护模式下如果执行 INTn指令时,当前的 CPL大于引用的门描述符的DPL。那 么便会导致通 用保 护异常 (# GP) 。 在 W id n o w s 20 0 0 和 x p 这样的操作系统下,INT1对用的中断门描述符的DPL 为 0 . 这要求只有内核模式的代码 才能执 行 IN T 1指令,访 问该中断门。 也就是说 , 用户模 式下 的应用程序没有权利使用INT 1指令 。 一 旦使用就会导致一个一般保护性异常 ( # GP) 。W id n o w s会将其封装为一个访 `问违例错误 (如图2一 2 1 所示 )。 在内核模式 下 可 以在代码 (驱 动程序)中写入INT 1指令。CP U 执行到该指令时会转去执行 1号向量对应的处理例程 , 如果在使用 W in D g b 进行内核级调试. 那 么会中断到W i n o b g 中 . W i n o b g 会以为是发生了一个单 步异常 如 图 1 所示 。

下面谈谈调试高级语言时的单步机制 。 由于高级语言的一条语句通常都对应多条汇编指令 例如 , 表 1 中C+ 十的一条语句= i a 十b* “ + d / e + f / g 十 h对应于 1 5 条汇编语句 。 因此容易想到单 步执行这条C+ + 语句有几种可能方法 。 一种是也用一标志一步 步的走过每条汇编指令 , 这种方 法意味着会产生 1 5 次调试异常 . CPU 中断到调试器 1 5 次.不过中间的 1 4 次都是简单的重 新设 皿起 下F 标志 便让 CPU返 回执行 。 第二种方法是在 C + + 语 句对应 的最后一 条汇编指令处动态插入一 条lN 下 3指令 . 让C P U 一下子 跑到那里 然后 再单步执行一次将被替换的那条 指令执行完 . 这种方 法需要 CPU中断到调试器 两次 。 第三种方法是 在这条 C++ 语句的下一条语 句的第一 条汇编指令处( 即行 18 ) 替换入一 个 N I 下 3 . 这样 C PU 中断到调试器一次就可 以了 。
表 1 : 高级语 言的单步执行

 

 

 

 

 

后两种方法较第一种方法速度会快很多 . 但是不幸的是并不总能正确的预测 出最后高级语言对应的最后一条指令和下一行语句开始指令 (要替换为INT 3 的那一条指令)。比如 2 8 行的 e l s e if (b ) 语句就很难判断出它对应 的最后一条汇编语句 和 下一 条高级 语言语 句的起始指令 。 因此 今天 的大 多数调试器在进行高级语言调试时都是使 用第一种方法来实现 单步跟踪的。关于 TF标志还有一点值得注意: . INTn 和 INTO 指令会清除TF标志 , 因此调试器在单步跟踪这些指令时 . 必须做特别处理。

任务状态陷阱标志

除了标志寄存器中的陷阱标志 (TF ) 位.38 6 引入 了一种新 的调试陷阱标志 任务状态段 (TSS ) 中的T标志。任务状态段 (Ta s k一S t a te S e g m e n t ) 用来记录 一个任务 (CPU 可以独立调度和执行 的程序段 ) 的状态 . 包括通用寄存器的值 . 段寄存器 的值和其它重要信息 。 在任务切换时 , 当前任务的状态会被保存到这个内存段里。当要恢复执行这个任务时 . 会先根据这个 记录来把寄存器 的值恢复回来。 T S S 中 , 在宇节偏移为 10 0 的 1 6 位字 (w o r d ) 的最低位是 调试陷阱标志位 . 简称 T标志 。 T 标志被设置为 1, 那么 当CPU切 换到这个任务时 , 便会产生调试异常。 准确的说CPU 是在程序控制权已经转移到新的任务 , 但还没有开始执行新任务的第一 条指令前产 生异常的。 调试 中断处理程序可 以通过调试状态 寄存器 (DR6 ) 的 BT来识别出发生的是否是任务切换异 常。 值得注意的是 . 如果调试器接管了调试异常处理 . 而且该处理例程 属于一个独立 的任务 , 那 么一定不要设置该任务的TSS段中的 T位。否则便会导致死循环。

分支到分支单步执行标志 ( B T F )

在 lA 一 3 2 处理器 家族中 , 所有的 P e n i t u m Pr o  Pe n t l u m II和 Pe n t . u m III处理器 (包括相应的 C e .e r o n 《赛扬 ) 和 X e o n (至 强 ) 版本) 因为都是基于相 同的 6 P 内核 《C o r e ) . 因此经常被 统称为P6处理器 。 P6处理器引入 了一项新 的对调试非 常有用 的功能 : 记录 分支 、 中断和异 常事件 . 和 针对分 支单步执行(s in g le 一s z e p o n b r a n e h ) 的功能。 奔腾 4 处理器对这一功能又 做了很大增强 。 下面具体介绍一下按分支单步执行的功能和使 用方法 。
首先解释一下分支到分支单步执行的含义 。 前面我们介绍过 , 当 ElF a g s 寄存器的T F 位为 1 时 C PU 每执行完一条指 令便会中断到调试器 . 也就是以指令为单位单步执行 。 顾名思义, 针对分支单步执行就是以分支为单位单步执行 。换句 话说 , 每单步 ( s t ep ) 一次 C P U 会一直执行到有分支 、 中 断或异常发生 。 下面 结合表 2 中的代码进行说明。
那么如何启用按分支单步执行呢? 简单来说 . 就是要 同时设置TF和BTF 标志 。TF 标志位干 E F Ia g s寄存器, 大家已经很 熟悉。 BTF 标志位于名为DebugCtlMSR 寄存器中 。

 

 

 

在V C6 的ID E 环境下(系统的 C PU 应该是 P6 或更高 ),先在第 2 2 行设置一个断点 . 然后按F5 运 行到这个断点位置 。 行 1 9是用来启用按分支单步执行功能的 . 也就是设置BTF 标志. 细节我们等一下再讲。 接下来 . 我们按 F10单步执行 , 会发现一下子会执行到行 3 1 . 从第 2 2 行单步一次执行到第 3 1 行 这便 是按分支单步执行的效果。 那么为什么会执行到第 3 1 行呢? 按照分支到分支单步执行的定义 , CP U 会在执行到下一次分支、 中 断或异常发生时停止。 对于我们的例子 . CP U 在执行第 2 0 行对 应的第一条汇编指令时 , CP U 会检测到 TF 标志 (因为我们是按F10单步执行 . 所以 VC 6 会帮助我们设置TF 标志)。

此外 ,P6及以后的 lA 一 3 2 CP U 还会检变 BTF 标志 . 当发现BTF标志也被设置起时 , C P U会认为当前是在按分支单步执行 . 所 以会判断当前指令是否是分支指令 , 如果不是 ,CP U 便会继续执 行。 因为第 2 2 行的第一条汇编指令不是分支指令 . 所以 CP U 会 继续执行。 依次类推 . CP U 会连续执行到第 2 4 行的 i f 语句对应的最后一条汇编指令jne (参见表 3 )。 因为这条语句是分支语句 ,所以 CPU会认为已经满足停止执行的条件 , 在清除 , TF 和BTF标志后,产生单步异常中断到调试器 。 中断到调试器时 . 分支语句已经执行完毕 , 但是跳转到的那条语句 (即表 2 中的行 3 1 } 还 没有执行 。

 

对以上过程还有有几 点需要说明:

第一 如果在从第 2 2 行执行到 2 4 行过程中,有中断或异 常发生 , 那么 C P U 也会认为停止单步执行的条件已经满足. 因 此 按分支单步执行的全称是按分支 、 异常和中断单步执行。

第二 , CPU 认为有分支发生的条件是执行以下分支指令 :JM P (无条件跳转), JCC (包括条件跳转指令 , 如 J A 、 J A E、 JNE等等)、L OO P (循环 ) 和 Call( 函数 / 过程调用 ) 。

第三 , 由于只有内核代码 才能访问MS R 寄存器 (通过RDMSR 和四WRMSR 指令) 所以上面的例子 中 , 在 WriteMSR()函数中 我们使用一个未公开 的 A p l  ZwSystemDebugControl()来设置 BTF 标志。

有很多因素决定了调试在编程乃至整个计算机世界中的无比重要性。从根本上来说,是计算机系统的设计理念决定了调试的重要地位。现代计算机系统的一个重要设计原则是让硬件在软件的指挥下工作,把灵活和智能留在软件中实现,这同时也把计算机系统的控制权交给了软件。让软件控制强大的计算机硬件是聪明的,运行不同的软件就可以让同一台机器做完全不同的事情也是冒险的,一条错误的指令就可能让系统崩溃甚至导致灾难。 降低风 险的方法是提高软件的质量和我们对软件的控 制力。 但对人 类而言 , 无形 的软件比有形 的硬件更加难以驾驭 ! 软件瑕疵 总是存在但却难以寻找 ; 病毒和恶意软件不请 自来 , 而且挥之不去 ; C P U 不堪重负 , 用 户下达的命令却还得不 到执行。 相对于淳朴的硬件 , 软件更 加复杂 、 多变、 柒鹜、 事故和狡黯 ! 何以应对? 为了控制软件 , 计算机先 驱在一开始设计计算机系统时就设计了各种调试设施 , 包括单步执行和 中 断执行等。 今天我们将这些 功能纳入 到调试器工具中。 一旦进 了调 试器 , 再狂野的软件都会变得服服帖帖 , 所有的宏观结构和微观 细节任由我们审 阅 , 大到整个地址空 间和每个文件 , 小到每一个内存单元和每一条指令 。 如果静态的分析还不够 , 那么我们可 以让 它根据我们的要求来执行 , 执行 一 条指令 , 执行到下一个分支 , 执行到上一级 函数 , 等等。 总之 , 以调试 器 为核心的调 试技术是征服软件和计算机世界的最强大工具 , 其用途很广。

具 体地讲 , 首先 , 调试是 定位软件瑕疵的最直接和最 有效的方法 。 没 有哪个程序员能一下子 写出没有错误 的代码。 而使用 以调试器 为主的调试 工具进行调试是定位瑕疵的最直接方法 , 可以从 问题 的症状入手 , 正 向跟 踪或者反 向追 溯。 对于大多数瑕疵 , 使用合适的调试方法可 以大大提高定 位到问题根源 的效率 。 今天的软件环境在不断向着大型化 、 并行化、 复杂 化 方向发展 , 定位瑕疵的难度也在随之不 断提高。 完全靠读源代码来寻找 b u g 的方法已经很难适应今天的软件发展形势 。 另外 , 枚举和排除法通常 也会因系统中的软硬件模块数量太 多而难 以实施 , 有时候 , 花了几天时间 来做替换仍然找不 到怀疑对 象。

第二 , 调 试可 以帮助程序员提高编写代码 的能力。 因 为调试可 以让程 序员彻底了解程 序的实际执行过程 , 检查与自己设计时的预想是 否一致 , 如果 不一致 , 那 么很可能预示 着有问题存在 , 应该引起重视 。 另外 , 调试 过程可 以让 程序员更好的认识到 提高代码可调试性和代码质量 的重要性 。 从此 , 自觉的改进编码方式 , 合理 添加用来支持调 试的代码 。 编码 和调 试 是程序员 日常工作中的两个最 主要任务 , 这两个任务是相辅相成的 , 编写 具有可 调试性的高质量代码 , 可 以明显提高调试效率 , 节约调试时间。 另 一方面 , 调试可 以让程 序员真切感受程序的实际执行过程 , 反思编码和 设 计中的问题 , 加深对软件和 系统的理解 , 提高对代码的感知力和控制力。

第三 , 调试工具是学习计算机系统和其它软硬件知识 的好帮手 。 通过 软件调试技术的强大观察能力和 断点、 栈回溯 、 跟踪等功能可 以快速的了 解一个软件和系统的模块 、 架构 、 和工 作流程 , 因此是学习 其它软硬件技 术的一 个快速而有效的方 法 。

今天在调试分析一个dump文件时,当我执行了".loadby sos clrjit"指令后,准备分析托管代码的问题,于是我又输入了"!clrstack"扩展指令想看下托管栈,结果Windbg给我如下输出:

0:000> .loadby sos clrjit
0:000> !clrstack
c0000005 Exception in C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\sos.clrstack debugger extension.
      PC: 0c53d0f3  VA: 00000000  R/W: 0  Parameter: 00000000
这还是第一次遇到,当我在执行"!pe"指令时,又有正确输出了

0:000> !pe
Exception object: 1eda1120
Exception type:   System.ExecutionEngineException
Message:          <none>
InnerException:   <none>
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80131506
然后在执行"!clrstack",结果也是能正确输出:

0:000> !clrstack
OS Thread Id: 0x6f90 (0)
Child SP       IP Call Site
00efc708 50e51a49 [PrestubMethodFrame: 00efc708] XXXXXX..cctor()
00efc8ec 50e51a49 [GCFrame: 00efc8ec]
00efce54 50e51a49 [PrestubMethodFrame: 00efce54] XXXXXX..ctor()
00efcec4 1d8908f4 XXXXXX(Int64)
00efd208 50e1f066 [DebuggerU2MCatchHandlerFrame: 00efd208]
00efcfc8 50e1f066 [HelperMethodFrame_PROTECTOBJ: 00efcfc8] System.RuntimeMethodHandle.InvokeMethod(System.Object, System.Object[], System.Signature, Boolean)
00efd2a4 78dfc799 System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(System.Object, System.Object[], System.Object[])
00efd2c8 78dfc2aa System.Reflection.RuntimeMethodInfo.Invoke(System.Object, System.Reflection.BindingFlags, System.Reflection.Binder, System.Object[], System.Globalization.CultureInfo)
00efd2fc 1d7b2cf0 *** WARNING: Unable to verify checksum for Adapter.dll
.?A0xc8a1231f.CallFunctionByReflectionInternal(DiyHomePlugin.Plugin, System.String, System.Object ByRef, System.Object[])
00efd364 1d8907eb .XXXX.AppStart(CAdapter*, Int64)
00efd380 1d890731 DomainBoundILStubClass.IL_STUB_ReversePInvoke(Int32, Int64)
后来又多试了几次,发现当加载了sos.dll扩展后,第一个扩展指令(任何指令)都会报错,然后第二条指令不会。看来要研究研究一下,开启Windbg调试自己:

(9ec.4b0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
sos!GetCMDOption+0x63:
00007ff9`a35ac7e3 488b01          mov     rax,qword ptr [rcx] ds:00000000`00000000=????????????????
0:004> ub
sos!GetCMDOption+0x3f:
00007ff9`a35ac7bf 4889542420      mov     qword ptr [rsp+20h],rdx
00007ff9`a35ac7c4 488bf1          mov     rsi,rcx
00007ff9`a35ac7c7 4d85c0          test    r8,r8
00007ff9`a35ac7ca 743f            je      sos!GetCMDOption+0x8b (00007ff9`a35ac80b)
00007ff9`a35ac7cc 488d5a18        lea     rbx,[rdx+18h]
00007ff9`a35ac7d0 392dd2e20500    cmp     dword ptr [sos!ControlC (00007ff9`a360aaa8)],ebp
00007ff9`a35ac7d6 0f85fe000000    jne     sos!GetCMDOption+0x15a (00007ff9`a35ac8da)
00007ff9`a35ac7dc 488b0dfd6d0500  mov     rcx,qword ptr [sos!g_ExtControl (00007ff9`a36035e0)]
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000000a`05aad2c0 00007ff9`a35973ac sos!GetCMDOption+0x63
01 0000000a`05aad3b0 00007ff9`a86f353b sos!ClrStack+0x21c
02 0000000a`05aad570 00007ff9`a86f3718 dbgeng!ExtensionInfo::CallA+0x233
03 0000000a`05aad630 00007ff9`a86f37f8 dbgeng!ExtensionInfo::Call+0x16c
04 0000000a`05aad830 00007ff9`a86f2689 dbgeng!ExtensionInfo::CallAny+0x78
05 0000000a`05aad870 00007ff9`a872a89b dbgeng!ParseBangCmd+0x4a9
06 0000000a`05aadd30 00007ff9`a872b6ab dbgeng!ProcessCommands+0xa8f
07 0000000a`05aade00 00007ff9`a8685fe8 dbgeng!ProcessCommandsAndCatch+0x8f
08 0000000a`05aade70 00007ff9`a868628f dbgeng!Execute+0x24c
09 0000000a`05aae340 00007ff7`0d0c5c72 dbgeng!DebugClient::ExecuteWide+0x83
0a 0000000a`05aae3a0 00007ff7`0d0c60d5 windbg!ProcessCommand+0x2b2
0b 0000000a`05aae7c0 00007ff7`0d0c7c17 windbg!ProcessEngineCommands+0x185
0c 0000000a`05aaf800 00007ff9`d6a316ad windbg!EngineLoop+0x3e3
0d 0000000a`05aaf840 00007ff9`d7244629 KERNEL32!BaseThreadInitThunk+0xd
0e 0000000a`05aaf870 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

让我们为sos!g_ExtControl变量上的写访问设置一个断点,以查找其归零位置。

0:001> ba w8 sos!g_ExtControl
0:001> g
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000008d`40d2d3f0 00007ff9`a7b1dc6e dbgeng!DebugClient::QueryInterface+0xb
01 0000008d`40d2d420 00007ff9`a7b471d0 sos!ExtQuery+0x2e
02 0000008d`40d2d450 00007ff9`a86f353b sos!ClrStack+0x40
...
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000008d`40d2c080 00007ff9`a7b1dc6e dbgeng!DebugClient::QueryInterface+0xb
01 0000008d`40d2c0b0 00007ff9`a7b1d973 sos!ExtQuery+0x2e
02 0000008d`40d2c0e0 00007ff9`a86f2d25 sos!DebugExtensionInitialize+0x83
03 0000008d`40d2c120 00007ff9`a86f4182 dbgeng!ExtensionInfo::Load+0x48d
04 0000008d`40d2c3f0 00007ff9`a87312f6 dbgeng!ExtensionInfo::CheckAdd+0x6e
05 0000008d`40d2c430 00007ff9`a8731ca8 dbgeng!LoadSOSAndCheckVer+0x36
06 0000008d`40d2c690 00007ff9`a86f00e1 dbgeng!ProcessInfo::LoadClrDebugDllForExt+0x828
07 0000008d`40d2cec0 00007ff9`a7b61004 dbgeng!ExtIoctl+0xc6d
08 0000008d`40d2d410 00007ff9`a7b4722c sos!LoadClrDebugDll+0x24
09 0000008d`40d2d450 00007ff9`a86f353b sos!ClrStack+0x9c
...
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000008d`40d2c0b0 00007ff9`a7b1d98d sos!ExtRelease+0x28
01 0000008d`40d2c0e0 00007ff9`a86f2d25 sos!DebugExtensionInitialize+0x9d
02 0000008d`40d2c120 00007ff9`a86f4182 dbgeng!ExtensionInfo::Load+0x48d
03 0000008d`40d2c3f0 00007ff9`a87312f6 dbgeng!ExtensionInfo::CheckAdd+0x6e
04 0000008d`40d2c430 00007ff9`a8731ca8 dbgeng!LoadSOSAndCheckVer+0x36
05 0000008d`40d2c690 00007ff9`a86f00e1 dbgeng!ProcessInfo::LoadClrDebugDllForExt+0x828
06 0000008d`40d2cec0 00007ff9`a7b61004 dbgeng!ExtIoctl+0xc6d
07 0000008d`40d2d410 00007ff9`a7b4722c sos!LoadClrDebugDll+0x24
08 0000008d`40d2d450 00007ff9`a86f353b sos!ClrStack+0x9c
...
0:004> g
(fe4.f70): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
sos!GetCMDOption+0x63:
00007ff9`a7b5c7e3 488b01          mov     rax,qword ptr [rcx] ds:00000000`00000000=????????????????
0:004> k
 # Child-SP          RetAddr           Call Site
00 0000008d`40d2d360 00007ff9`a7b473ac sos!GetCMDOption+0x63
01 0000008d`40d2d450 00007ff9`a86f353b sos!ClrStack+0x21c
...
sos!ExtQuery:
00007ff9`6a16dc40 48895c2408      mov     qword ptr [rsp+8],rbx
00007ff9`6a16dc45 4889742410      mov     qword ptr [rsp+10h],rsi
00007ff9`6a16dc4a 57              push    rdi
00007ff9`6a16dc4b 4883ec20        sub     rsp,20h
00007ff9`6a16dc4f 33f6            xor     esi,esi
00007ff9`6a16dc51 4c8d0588590900  lea     r8,[sos!g_ExtControl (00007ff9`6a2035e0)]
00007ff9`6a16dc58 488d1571760600  lea     rdx,[sos!GUID_d4366723_44df_4bed_8c7e_4c05424f4588 (00007ff9`6a1d52d0)]
00007ff9`6a16dc5f 4889357a590900  mov     qword ptr [sos!g_ExtControl (00007ff9`6a2035e0)],rsi
00007ff9`6a16dc66 488b01          mov     rax,qword ptr [rcx]
00007ff9`6a16dc69 488bf9          mov     rdi,rcx
00007ff9`6a16dc6c ff10            call    qword ptr [rax] ds:00007ff9`60e018d0={dbgeng!DebugClient::QueryInterface (00007ff9`60fd0240)}
sos!ExtRelease:
00007ff9`6a16ddc0 4053            push    rbx
00007ff9`6a16ddc2 4883ec20        sub     rsp,20h
00007ff9`6a16ddc6 488b0d13580900  mov     rcx,qword ptr [sos!g_ExtControl (00007ff9`6a2035e0)]
00007ff9`6a16ddcd 33db            xor     ebx,ebx
00007ff9`6a16ddcf 48891d02580900  mov     qword ptr [sos!g_ExtClient (00007ff9`6a2035d8)],rbx
00007ff9`6a16ddd6 4885c9          test    rcx,rcx
00007ff9`6a16ddd9 740d            je      sos!ExtRelease+0x28 (00007ff9`6a16dde8)
00007ff9`6a16dddb 488b01          mov     rax,qword ptr [rcx]
00007ff9`6a16ddde ff5010          call    qword ptr [rax+10h] ds:00007ff9`60e01358={dbgeng!DebugClient::Release (00007ff9`60fd0200)}

我们可以看到,IDebugControl2接口获得了两次调用,然后仍在使用被释放的sos!ClrStack函数。另外,我们可以看到sos.dll是第二次加载的。为了找出原因,让我们调试dbgeng!LoadSOSAndCheckVerr函数。在dbgeng!ExtensionInfo::Add方法我们可以看到它正在调用dbgeng!ExtensionInfo::FindByName方法中按名称查找扩展名dbgeng!ExtensionInfo::s_Chain扩展信息链表

0:003> k
 # Child-SP          RetAddr           Call Site
00 0000003a`a963c970 00007ff9`61044168 dbgeng!ExtensionInfo::Add
01 0000003a`a963c9d0 00007ff9`610812f6 dbgeng!ExtensionInfo::CheckAdd+0x54
02 0000003a`a963ca10 00007ff9`61081ca8 dbgeng!LoadSOSAndCheckVer+0x36
03 0000003a`a963cc70 00007ff9`610400e1 dbgeng!ProcessInfo::LoadClrDebugDllForExt+0x828
04 0000003a`a963d4a0 00007ff9`6a601004 dbgeng!ExtIoctl+0xc6d
05 0000003a`a963d9f0 00007ff9`6a5e722c sos!LoadClrDebugDll+0x24
06 0000003a`a963da30 00007ff9`6104353b sos!ClrStack+0x9c
...
0:003> r
rax=0000000000000036 rbx=0000003aa9505490 rcx=0000003aa963ca40
rdx=0000000000000000 rsi=0000000000000037 rdi=0000003aa963ca40
rip=00007ff961043dc5 rsp=0000003aa963c970 rbp=0000000000000000
 r8=0000000000000000  r9=0000003aa963c9f0 r10=0000000000000000
r11=0000003aa963c9e0 r12=0000000000000000 r13=0000000000000000
r14=0000003aa963c9f0 r15=0000003aa963cd01
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
dbgeng!ExtensionInfo::Add+0x95:
00007ff9`61043dc5 e876feffff      call    dbgeng!ExtensionInfo::FindByName (00007ff9`61043c40)
0:003> du @rcx
0000003a`a963ca40  "C:\Windows\Microsoft.NET\Framewo"
0000003a`a963ca80  "rk64\v4.0.30319\SOS.dll"
0:003> u dbgeng!ExtensionInfo::FindByName
dbgeng!ExtensionInfo::FindByName:
00007ff9`61043c40 488bc4          mov     rax,rsp
00007ff9`61043c43 48895808        mov     qword ptr [rax+8],rbx
00007ff9`61043c47 48896810        mov     qword ptr [rax+10h],rbp
00007ff9`61043c4b 48897018        mov     qword ptr [rax+18h],rsi
00007ff9`61043c4f 48897820        mov     qword ptr [rax+20h],rdi
00007ff9`61043c53 4156            push    r14
00007ff9`61043c55 4883ec20        sub     rsp,20h
00007ff9`61043c59 4883cfff        or      rdi,0FFFFFFFFFFFFFFFFh
00007ff9`61043c5d 488bf2          mov     rsi,rdx
00007ff9`61043c60 488be9          mov     rbp,rcx
00007ff9`61043c63 4533f6          xor     r14d,r14d
00007ff9`61043c66 48ffc7          inc     rdi
00007ff9`61043c69 6644393479      cmp     word ptr [rcx+rdi*2],r14w
00007ff9`61043c6e 75f6            jne     dbgeng!ExtensionInfo::FindByName+0x26 (00007ff9`61043c66)
00007ff9`61043c70 488b1d91041c00  mov     rbx,qword ptr [C:\Windows\Microsoft.NET\Framewo"
0000003a`a94fb360  "rk64\v4.0.30319\sos"
0000003a`a94f8830  "dbghelp"
0000003a`a94f8560  "ext"
0000003a`a94f8290  "exts"
0000003a`a94f7fc0  "uext"
0000003a`a94f7cf0  "ntsdexts"

如果没有,它会叫dbgeng!ExtensionInfo::Link方法将新的扩展dll添加到链接列表中。

0:003> r
rax=0000000000000000 rbx=0000003aa5f1e6b0 rcx=0000003aa5f1e6b0
rdx=0000003aa5f1e6d0 rsi=0000000000000037 rdi=0000003aa963ca40
rip=00007ff961043ed0 rsp=0000003aa963c970 rbp=0000000000000000
 r8=0000000000000000  r9=0000000000000000 r10=0000003aa95abd30
r11=0000003aa5f1e6d0 r12=0000000000000000 r13=0000000000000000
r14=0000003aa963c9f0 r15=0000003aa963cd01
iopl=0         nv up ei pl nz na pe nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
dbgeng!ExtensionInfo::Add+0x1a0:
00007ff9`61043ed0 e8e7110000      call    dbgeng!ExtensionInfo::Link (00007ff9`610450bc)
0:003> du @rdx
0000003a`a5f1e6d0  "C:\Windows\Microsoft.NET\Framewo"
0000003a`a5f1e710  "rk64\v4.0.30319\SOS.dll"
0:003> !list -x "du poi(@$extret+8)" poi(dbgeng!ExtensionInfo::s_Chain)
0000003a`a5f1e6d0  "C:\Windows\Microsoft.NET\Framewo"
0000003a`a5f1e710  "rk64\v4.0.30319\SOS.dll"
0000003a`a94fb320  "C:\Windows\Microsoft.NET\Framewo"
0000003a`a94fb360  "rk64\v4.0.30319\sos"
0000003a`a94f8830  "dbghelp"
0000003a`a94f8560  "ext"
0000003a`a94f8290  "exts"
0000003a`a94f7fc0  "uext"
0000003a`a94f7cf0  "ntsdexts"

最后是dbgeng!ExtensionInfo::CheckAdd方法调用dbgeng!ExtensionInfo::Load加载sos.dll:

0:003> u
dbgeng!ExtensionInfo::CheckAdd+0x69:
00007ff9`6104417d e816e7ffff      call    dbgeng!ExtensionInfo::Load (00007ff9`61042898)

这是DbgEng.dll中的一个错误。为coreclr添加了一个解决方案。不确定它是否已经进入了完整的框架。可以通过下面的命令之一来解决:

.loadby sos.dll clr

.loadby sos.dll clrjit

.cordll -ve -u -l

首先 , 学习时应该多动手实验 , 拳不离手 , 曲不离口 。 以我个人的经历 为例 , 干编程以来, 我几 乎每天工作时都使用 调试器 。 除了使用它调 试程 序 、 寻 找代码 中 的问题 , 我还使用调试器认识其它软件 、 探 索操作系统 、 观察硬件等等 。

另外 , 学习调试时要 多思 考, 多问为什 么。 这样就可 以慢慢打通未知领域 , 使自己 的理 解不 断深入 , 直到有一 天 , 不 同方向的 探索纷纷会合 , 融汇贯通 , 那么功夫便学成了 。 因为调试技术的广泛 关联性 , 所 以一 旦把调 试技术都搞通 了, 那么对整个计算机系统的 理解也会有一个质的飞跃 。

关于调 试工 具 , 在 Win d o w s 平 台上 , 我 主要使用的是 W i n D B G 。 在Li n u x 平 台中, 使 用 GDB 。 二者都是 以命令方式为主的。 对于 习惯图形界面的很多初学者来说 , 可 能觉得 命令方式不 好学 , 事实上 , 先学会一些 常用 的命令并不难 , 然后 可 以慢慢学习更 多的命 令。 在入 门后 , 应该学一些 调试原理 , 这样 才能深入 了解不 同 调试功能 的长处和 短处 ,更好的应用他们。

调试是一门实践性强涉及面广的综合技能。所以实际动手是重中之重,且我认为,一定要在实际工作中去学习,因为只有在实际工作中你才会遇到各种问题和真实的问题,只有身经百战,才能学到真正的技能。只有自己亲自动手解决问题,才能够记忆深刻,形成自己的知识系统。