经过一番调查,我相信这个问题的答案是:VS中没有与.call等效的东西。
唯一的解决方案是自己通过操作堆栈指针、指令指针等来模拟 .call 的行为。这显然有局限性,例如我的仅适用于 Microsoft x64 调用约定。转换为 x86 及其无数的调用约定留给读者练习;)
根据您的实际需要,我找到了两种方法来做到这一点。请记住,这是为了在下次调试器运行时调用函数(以便您可以中断它,因为不支持嵌套断点)。如果您只需要调用一个函数而不会再次中断,那么您最好使用立即窗口直接调用它!
简单的方法:
这会诱使 VS 认为当前帧在您选择的 DLL 和方法中。这很有用,因为 Expression Evaluator 不想在您停止的 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)
- push 返回地址,即 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);
}