Stupid debugger tricks: Calling functions and methods - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20070427-00/?p=27083
Raymond Chen 2007年04月27日
一个比较笨的调试技巧:调用函数和方法
在过去,如果你想在调试器中调用一个函数,你必须手动进行:保存寄存器,将参数推送到栈上(如果函数使用fastcall
或thiscall
,则将参数放入寄存器),将ntdll!DbgBreakPoint
函数的地址推送到栈上,将指令指针移动到你想要调用的函数的开始位置,然后按“g”键恢复执行。函数运行后返回到ntdll!DbgBreakPoint
,调试器重新获得控制权,你可以查看结果。然后恢复寄存器(包括原始的指令指针)并继续调试。
那一段只是快速回顾;我假设你已经知道了。
现在,Windows符号调试引擎(ntsd
、cdb
和windbg
背后的调试引擎)可以自动化这个过程。
假设你想调用这个函数:
int DoSomething(int i, int j);
你可以让调试器做所有繁重的工作:
0:001> .call ABC!DoSomething(1,2)
线程已设置为调用,'g'将执行。
警告:这可能会产生严重的副作用,包括死锁和调试程序的损坏。
0:001> r
eax=7ffde000 ebx=00000001 ecx=00000001 edx=00000003 esi=00000004 edi=00000005
eip=10250132 esp=00a7ffbc ebp=00a7fff4 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000246
ABC!DoSomething:
10250132 55 push ebp
0:001> dd esp
00a7ffbc 00a7ffc8 00000001 00000002 ccfdebcc
注意,调试器巧妙地将参数推送到栈上,并为你设置了eip
寄存器。你只需要按“g”,DoSomething
函数就会运行。一旦它返回,调试器将恢复原始状态。
这种技术甚至适用于C++方法:
// 假设我们知道0x00131320是一个IStream指针
0:001> .dvalloc 1000
从00a80000开始分配了1000字节
0:001> .call ABC!CAlphaStream::Read(0x00131320, 0xa80000, 0x1000, 0)
线程已设置为调用,'g'将执行。
警告:这可能会产生严重的副作用,包括死锁和调试程序的损坏。
注意,当调用非静态C++方法时,你必须将“this”参数作为显式的第一个参数传递。调试器知道使用哪种调用约定,并将寄存器放置在正确的位置。在这种情况下,它知道CAlphaStream::Read
使用stdcall
调用约定,因此所有参数都已推送到栈上。
那么那个.dvalloc
命令是做什么的?这是另一个调试器辅助函数,它在被调试进程的地址空间中分配一些内存。在这里,我们用它来分配一个我们想要读取的缓冲区。
但是,如果你想调用一个接口上的方法,而你没有实现源代码怎么办?例如,你想从一个外部组件传递给你的流中读取。嗯,你可以玩一个小把戏。你可以假装调用一个你有源代码的函数,该函数具有相同的函数签名,然后将eip
寄存器移动到所需的入口点。
// 假设我们知道0x00131320是一个IStream指针
0:000> dp 131320 l1
00131320 77f6b5e8 // vtable
0:000> dps 77f6b5e8 l4
77f6b5e8 77fbff0e SHLWAPI!CFileStream::QueryInterface
77f6b5ec 77fb34ed SHLWAPI!CAssocW2k::AddRef
77f6b5f0 77f6b670 SHLWAPI!CFileStream::Release
77f6b5f4 77f77474 SHLWAPI!CFileStream::Read
0:000> .call SHLWAPI!CFileStream::Read(0x00131320, 0xa80000, 0x1000, 0)
^ 符号在'.call SHLWAPI!CFileStream::Read'中不是函数
那个错误消息是调试器有点令人困惑的方式,意思是“我没有足够的信息来执行那个函数调用。”但没关系,因为我们有一个“足够接近”的函数,即CAlphaStream::Read
:
0:001> .call ABC!CAlphaStream::Read(0x00131320, 0xa80000, 0x1000, 0)
线程已设置为调用,'g'将执行。
警告:这可能会产生严重的副作用,包括死锁和调试程序的损坏。
0:000> r eip=SHLWAPI!CFileStream::Read
0:000> r
eax=00131320 ebx=0007d628 ecx=00130000 edx=0013239e esi=00000000 edi=00000003
eip=77f77474 esp=0007d384 ebp=0007d3b0 iopl=0 nv up ei pl zr na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
SHLWAPI!CFileStream::Read:
77f77474 8bff mov edi,edi
哇哦!
我们让ABC!CAlphaStream::Read
为我们推送了所有参数,然后调用我们交换了那个函数,并将CFileStream::Read
插入到它的位置。现在你可以按“g”来执行CFileStream::Read
调用。
这只是.call
命令所能做的表面。将一些C++表达式求值等功能混合在一起,你就拥有了一个相当巧妙的“伪即时模式”表达式求值器。