|
如何制作VB的P-Code调试器
|
| 更新时间:2008-8-12 1:15:40 |
责任编辑:阿loosen |
|
|
P-Code简介
术语P-Code既不是一个新名词也不是Microsoft的发明,P-Code只是简单地被解释执行的伪指令。因此,我们并不需要通过什么复杂的专业词汇来描述它。P-Code可以被认为是一种普通的机器级代码(我们的微处理器不能解释它)。在运行P-Code之前,需要一个解释器处理它,转换P-Code为CPU可以理解的机器语言。这个过程看起来有点象JAVA, 为了执行JAVA语言写成的应用程序,我们需要一个进行解释和翻译的处理程序- 虚拟机“Virtual Machine”。这个负有想象力的术语实际意味着它是一个翻译的机制,将JAVA编写的程序转换成我们的CPU可以理解的操作码(指令)。
P-Code的优势是明显的。假如我们定义了一组特殊的专属性指令集,并且不公布它的详细定义规范说明,那么一般人很难理解我们生成的程序代码;另一个优势是减小了执行文件的尺寸:通过定义独特的单字节操作码,我们能够使得某些伪指令执行一系列的操作(相当于大量的机器码完成的工作)。Microsoft的 Visual Basic 包含的P-Code的确是如此:一个VB虚拟机翻译P-Code到我们本地的机器码。 虚拟机(以dll形式出现),在P-Code程序执行前被调用,用来解释相关VB的伪指令。有如你猜测的那样,那些DLL的名称是 : MSVBVM50.DLL MSVBVM60.DLL 文件名称清晰的表明是Microsoft Visual Basic 虚拟机(Virtual Machine), 后跟不同的版本信息。两个版本的差异不大:版本6引入了一些新的指令,并采用了更直观的命名来标注某些版本5中的指令。换句话说,版本6只是改变了版本5中部分指令名称,而非其内在的功能。
虚拟机不仅解释Visual Basic 的P-Code文件,同时它也被用于执行编译过的机器码。这是因为VB 虚拟机(DLL文件)同时也包含所有VB程序要调用的API。 一个例子是rtcMsgBox, 这是个等价于标准Windows API MessageBox 的VB函数。P-Code代码被VB虚拟机解释执行,VB中所有的函数都是以这种间接的方式被提供的。
由于这个原因,当我们跟踪一个Windows API MessageBox被 P-Code程序时,产生了一个严重的问题:我们必须要跟踪P-Code伪指令。
SoftICE 无法跟踪P-Code伪指令, 它只能跟踪VB虚拟机的执行过程。更明确地说, SoftICE 只能理解CPU处理器的机器码,它不能理解任何伪指令。我们将尝试去跟踪P-Code(P-Code伪指令将被转换成可被我们的CPU理解和执行的机器码)。
起始表(Beginning of the Tale) 几乎所有的事情都是如此:好奇心引发了人们迎接一项新的挑战,我们的故事由此开始。
我记得曾在EFNet网站(论坛)与Green先生讨论有关VB P-Code的问题。他那时的工作正好涉及有关VB5编译的应用程序。他告诉我处理P-Code是非常的困难,所以我们有了制作一个VB P-Code的Debugger程序的想法。 实际上Black先生也认为这是个有意义的思路。考虑到这个项目,我说如果我们没有任何可用的有关资源,这可不容易实现。而后,我们查找了许多有关信息,但没有任何有意义的发现,空手而归。好奇心使得我更加努力去细心地发掘有用的信息资料。的确是不易呀…我曾和Snow先生探讨有关问题,他提供给我一个被Lazarus修改过的 MSVBVM50, 在其中,他描述了VB程序表现的所有可能的串比较。这促使我下决心制作一个VB Debugger。
我认为当MSVBVM50运行时注入代码是可能的。被注入的代码能够调用我的Debugger, 它在一个DLL中实现。 我决定告诉已加入这个项目的Snow先生, 由他负责制作代码注入器 (好像一个调用装入器Loader) ,我负责Debugger (DLL)编码,就是那个被装入的Debugger(DLL) 。 就在我们两个完成了一些工作后,我们进行了测试,令人振奋的是它真的可以工作!这个 Debugger项目已经迈出了它的第一步。
我们可以控制VB虚拟机(截获有关操作),并且在虚拟机与VB应用程序之间安置我们的Debugger。最大的问题已经得到解决,虽然在初始阶段,我们采用的解决方案(技术上如你所见)并不是最终我们采用的方法。在我们的大目标和指导思想始终如一的情况下,我们不断对它进行改进,一直到我们完全避免了对虚拟机本身的修改。
* 第一步
跟踪分析,控制虚拟机
为使我们的Debugger能够工作,有一个关键性必须解决的问题:发现P-Code代码的翻译转换是什么时候以及如何发生的。一旦我们认识到这一点,我们注入的代码将接管对被调试的VB应用程序的控制,并且发送有关数据到我们的Debugger。Debugger 依次处理操作码并返回到VB 虚拟机。 我本人以前在有关调试器Debugger编码方面的经验几乎为零。不过不久前我差不多完成了一个x86的反汇编器 , 因此我将那些知识用于我的VB Debugger 开发工作。我的构思是这样的:
对于反汇编/解释这部分代码包含以下基本原理:
- 一个指针(pointer)指向一个缓存区(buffer),它包含将被转换的数据。 - 一个控制程序,它从缓存区中读取操作指令(opcodes)并且重定向程序流,使其依据我们的意图,指向我们想要它执行的程序位置。
这个任务通常表现为两种形式: 1、一系列控制描述语句(对于每一个操作码);2、使用一个地址跳转表。 我放弃了第一种选择。因为P-Code中各不相同的操作码实在太多,这将需要一个巨大的条件控制结构(处理这样的工作将变成世界上最慢的事情)。 我猜VB虚拟机对P-Code的翻译转换过程采用的是地址跳转表方法去解释那些可能的操作代码。 这种做法同样出现在我设计的反汇编器中。 现在,我必须完成以下的工作:
- 定位缓存区中待解释的操作码,定位跳转地址表
我设计并且编译了一个小VB应用程序:
Private Sub Form_Load()
MsgBox "Hello this is P-Code!!!", VBInformation, "Example"
End Sub
我通过SoftICE的符号载入器(symbol loader)调入MSVBVM60.DLL (VB6虚拟机) ,设置BPX on _rtcMsgBox。 当SoftICE 中断时,我按 F12返回调用 _rtcMsgBox 的代码:
call eax // 调用 rtcMsgBox cmp edi,esp // 我们在此 jnz 66105595 // 检查堆栈指针 xor eax,eax // 准备寄存器 eax 去调用缓存区中的下一个操作码 mov al,[ESI] // 在al中装入待执行的操作码, 上面的演示中,它是36h inc ESI // 增加指针偏移量(在 ESI 寄存器中) jmp [eax*4+660FDA58] // 跳转到解释伪指令操作码 36h 的处理程序
我们看到,就如推测的一般,解释器从缓存区中读入操作码(P-Code伪指令)并且放入8位寄存器AL, 将此操作码作为一个地址偏移量,跳转到相应的处理子程序。我上面提供的小例子中的 36h 既是作为了一个地址偏移量 (这是一个聪明,灵活的办法去处理不同的指令避免了成千的分支判断检查。利用如此聪明的设计方法还真不太象Microsoft一贯的作风)。 如果我们继续跟踪下去,我们会看到利用寄存器ESI作为伪指令操作码缓存区指针来解释VB应用程序的步骤被不断地重复执行着。 其实,在VB 的虚拟机运行时, ESI寄存器始终指向包含被执行的VB应用程序的全部P-Code操作码的一个缓存区。 因此,在SoftICE中,我们总可以利用“ d *ESI”发现将要被执行的操作码。
的确有如我们已经发现的: ESI寄存器包含了一个指向伪指令操作码的缓存区的导航指针; AL包含下一个将要被执行的操作码(1字节)。 最有趣的指令语句是无条件跳转: JMP [4*EAX+ADDRESS]。 你可以使用从缓存区中读取的字节作为一个偏移量,进入地址跳转表中的相应程序入口(基于ADDRESS)。 这个跳转地址表的最大尺寸很容易被推算出来: 最大的偏移量AL (256) * 我们程序中的索引偏移量4(因为每个地址长度占4个字节) : 256 * 4 = 1024 bytes
以上事实和发现证实了我对VB中采用了地址跳转表技术的推测。 基于Microsoft的文档,它声明标准的P-Code 包含 了256 个操作码,其它作为扩展的操作码。 这提醒了我,我的可爱的老PC也有256个不同的操作码可以执行。不过,实际上包含的操作码要多的多。如果仅有256独特的可识别值-伪指令操作码,那更多的操作码是如何工作的呢? 它们是什么值? (我从事反汇编器的经验使我认识到) 这很容易做到: 256个被保留的识别值中有些是作为(指令)前缀使用的。
当解释器发现这样的前缀,它指示一个扩展的指令集合(给出一组新的可能有256种可能性的指令集)。因此,使用前缀可以允许无限制的指令操作码设置。 注意,每个前缀应该有它自己的新的跳转地址表。 稍后,我们会看到许多在当前的VB P-Code代码中使用的前缀,以及如何定位它们的地址跳转表。 为了确认我们发现的跳转地址的正确性,我反汇编了VB虚拟机,并且探察研究了跳转地址表中的所有的情况。 正如我们期望的,所有伪指令都有对应的处理程序。 而后,我分析了VB虚拟机的引擎(DLL)中这个表的内容验证它在虚拟机中的地址入口,以及包含的内容。 我查看了一些伪指令的处理程序,它们有着同样的结构:
它们从寄存器ESI指向的缓存区中读取数据并执行某些指令。我找到了我想要的-每一条伪指令的跳转地址。
下一步是用我们自己的内容替换跳转地址表(包含全部操作码的处理程序入口地址)。 这些新的地址应该指向我们自己的处理程序,它们在我们的Debugger的 DLL中。 在所有操作码真正被VB虚拟机执行前,先通过我们预先设置的程序的处理。 VB虚拟机中的处理程序被我们自己的处理程序替换(以C的调用方式),所有的寄存器和标志位被保存,并在执行下一条P-Code指令前恢复为原先的内容。
这里是我们自己的Debugger处理程序的开始和结束 :
__declspec( naked ) void DebuggerProc() {
_asm { mov VBDebugger.OldStack_ESP,esp // 我们保存VB 的堆栈 mov VBDebugger.OldStack_EBP,ebp // 基地址指针和堆栈指针 pushad // 保存所有寄存器状态 pushfd // 保存所有标志
push ebp // 现在我们放一个标准的调用框架 mov ebp, esp sub esp, __LOCAL_SIZE // 如果我们能有一些局部变量,这里应该预留出空间,从堆栈中减去相应的尺寸
1 2 下一页 | | |
|
|