调用visualstudio中的等效程序

winDBG .call equivalent in visual studio

本文关键字:程序 visualstudio 调用      更新时间:2023-10-16

对于作为VS插件构建的自制调试工具,我需要:

  • 在应用程序中的任意点中断
  • 调用另一个方法并在那里中断(在运行前不在该位置添加代码)
  • 在第二个断点从我的VS加载项运行其他命令

我对如何做到这一点的第一直觉在汉斯的精彩回答中碰壁了。

我的第二个想法是从断点设置对另一个方法的调用,并在允许应用程序继续时执行它(如果你能看到另一种方法来做我需要的事情,请随时指出!)。

这对于WinDBG来说是微不足道的:只需使用.call并执行即可。很遗憾,我需要在Visual Studio中执行此操作。

因此,我的问题是:有没有办法在VS中做到这一点?我找不到与.call等效的方法,也找不到操作寄存器、堆栈和模拟.call的方法。

经过一番调查,我相信这个问题的答案是:在VS.中没有等效的.call

唯一的解决方案是通过操纵堆栈指针、指令指针等来模拟.call的行为。这显然有局限性,例如,我的仅适用于Microsoft x64调用约定。向x86及其无数调用约定的转换留给读者练习;)

根据您的实际需要,我找到了两种方法。记住,这是为了在下次被调试对象运行时调用函数(这样您就可以打断它,因为不支持嵌套断点)。如果你只需要在不中断的情况下调用一个函数,那么最好使用即时窗口直接调用它!


简单的方法:

这会诱使VS认为当前帧在DLL和您选择的方法中。这很有用,因为表达式计算器不想在您停止的DLL中工作,需要在另一个DLL中。

警告:在不损坏堆栈的情况下,您实际上无法执行正在伪造的方法调用(除非您正在调用的方法非常简单,而且您非常幸运)。

通过即时窗口在调试器中直接使用以下操作:

@rsp=@rsp-8
*((__int64*)$rsp)=@rip
@rip={,,<DLL to jump in.dll>}<method to call>

现在VS将您指定的DLL和方法视为当前帧。完成后,使用以下操作返回到以前的状态:

@rip=*((__int64*)$rsp)
@rsp=@rsp+8

这也可以在VS外接程序中通过EnvDTE.Debugger.GetExpression()运行这些语句来实现自动化,如下面的其他方法所示。


艰难的道路:

这将适用于实际调用所需的DLL和函数,然后从中干净地返回。它更加复杂和危险;任何错误都会损坏您的堆栈。

调试和发布模式也很难正确,因为优化器可能对被调用者和调用者的代码做了一些你意想不到的复杂事情。

其想法是模仿Microsoft x64调用约定(此处记录)并中断调用的函数。我们需要做以下事情:

  • 按从右到左的顺序,将参数推送到堆栈的前4个参数之外
  • 在堆栈上创建阴影空间(1)
  • 推送返回地址,即RIP的当前值
  • 将RIP设置为要调用的函数的地址,就像上面一样
  • 保存被调用者可能更改的所有寄存器,而调用者可能不希望更改这些寄存器。这基本上意味着保存这里标记为"挥发物"的所有东西
  • 在被调用者中设置断点
  • 运行调试对象
  • 当被调试对象再次中断时,执行我们想要的任何操作
  • 退出
  • 恢复保存的寄存器
  • 将RSP返回到正确的位置(即拆除阴影空间)
  • 删除断点

(1) 32字节的暂存空间供被调用者溢出寄存器传递的前4个参数(通常;被调用者实际上可以随心所欲地使用它)。

这里是我的VS插件的一个简化块,用于一个非常基本的情况(非成员函数将一个参数设置为0,并且不接触太多寄存器)。除此之外的任何内容都将再次留给读者练习;)

EnvDTE90a.Debugger4 dbg = (EnvDTE90a.Debugger4)DTE.Debugger;
string method = "{,,dllname.dll}function";
string RAX = null, RCX = null, flags = null;
// get the address of the function to call and the address to break at (function address + a bit, to skip some prolog and help our breakpoint actually hit)
Expression expr = dbg.GetExpression3(method, dbg.CurrentThread.StackFrames.Item(1), false, false, false, 0);
string addr = expr.Value;
string addrToBreak = (UInt64.Parse(addr.Substring(2), NumberStyles.HexNumber) + 2).ToString();
if (!expr.IsValidValue)
    return;
// set a breakpoint in the function to jump into
EnvDTE.Breakpoints bpsAdded = dbg.Breakpoints.Add("", "", 0, 0, "", dbgBreakpointConditionType.dbgBreakpointConditionTypeWhenTrue, "c++", "", 0, addrToBreak, 0, dbgHitCountType.dbgHitCountTypeNone);
if (bpsAdded.Count != 1)
    return;
// set up the shadow space and parameter space
// NB: for 1 parameter : 4 words of shadow space, no further parameters... BUT, since the stack needs to be 16 BYTES aligned (i.e. 2 words) and the return address takes a single word, we need to offset by 5 !
dbg.GetExpression3("@rsp=@rsp-8*5", dbg.CurrentStackFrame, false, true, false, 0);
// set up the return address
dbg.GetExpression3("@rsp=@rsp-8*1", dbg.CurrentStackFrame, false, true, false, 0);
dbg.GetExpression3("*((__int64*)$rsp)=@rip", dbg.CurrentStackFrame, false, true, false, 0);
// save the registers
RAX = dbg.GetExpression3("@rax", dbg.CurrentStackFrame, false, true, false, 0).Value;
RCX = dbg.GetExpression3("@rcx", dbg.CurrentStackFrame, false, true, false, 0).Value;
// save the flags        
flags = dbg.GetExpression3("@efl", dbg.CurrentStackFrame, false, true, false, 0).Value;
// set up the parameter for the call
dbg.GetExpression3("@rcx=0x0", dbg.CurrentStackFrame, false, true, false, 0);
// set the instruction pointer to our target function
dbg.GetExpression3("@rip=" + addr, dbg.CurrentStackFrame, false, true, false, 0);
dbg.Go(true);
// DO SOMETHING USEFUL HERE ! ;)
dbg.StepOut(true);
// restore all registers
dbg.GetExpression3("@rax=" + RAX, dbg.CurrentStackFrame, false, true, false, 0);
dbg.GetExpression3("@rcx=" + RCX, dbg.CurrentStackFrame, false, true, false, 0);
// restore flags
dbg.GetExpression3("@efl=" + flags, dbg.CurrentStackFrame, false, true, false, 0);
// tear down the shadow space
dbg.GetExpression3("@rsp=@rsp+8*5", dbg.CurrentStackFrame, false, true, false, 0);
}