3

对于某些功能,我需要切换堆栈以使原始堆栈保持不变。为此,我编写了两个宏,如下所示。

#define SAVE_STACK()    __asm__ __volatile__ ( "mov %%rsp, %0; mov %1, %%rsp" : \
"=m" (saved_sp) : "m" (temp_sp) );
#define RESTORE_STACK() __asm__ __volatile__ ( "mov %0, %%rsp" : \
"=m" (saved_sp) );

这里temp_spsaved_sp是线程局部变量。temp_sp指向我们使用的临时堆栈。对于我想要不修改其原始堆栈的函数,我将 SAVE_STACK 放在开头,将 RESTORE_STACK 放在底部。例如,像这样。

int some_func(int param1, int param2)
{
 int a, b, r;
 SAVE_STACK();
 // Function Body here
 .....................
 RESTORE_STACK();
 return r;
}

现在我的问题是这种方法是否可行。在 x86(64 位)上,通过rbp寄存器访问局部变量和参数,并在函数序言中相应地减去rsp,直到在函数尾声中添加它以使其恢复到原始值时才被触及。因此,我认为这里没有问题。

我不确定,如果这在上下文切换和信号存在的情况下是正确的。(在 Linux 上)。此外,如果函数是内联的,或者是否应用了尾调用优化(使用jmp而不是call ) ,我也不确定这是否正确。你觉得这种方法有什么问题或副作用吗?

4

2 回答 2

5

使用上面显示的代码,我可以想到以下破坏:

  1. 在 x86/x64 上,如果它认为合适,GCC 将使用序言/尾声“装饰”您的函数,并且您无法阻止它这样做(例如在 ARM 上,__attribute__((__naked__))强制创建没有序言/尾声的代码,也就是没有堆栈帧设置) . 在切换堆栈之前
    ,这可能最终会分配堆栈/创建对堆栈内存位置的引用。更糟糕的是,再次,由于编译器的选择,在您切换堆栈之前将这样的地址放入非易失性寄存器,它可能会别名为两个位置(您更改的相对于堆栈指针的位置和相对于其他寄存器的位置)那是一样的)。

  2. 同样,在 x86/x64 上,ABI 建议对叶函数(“红色区域”)进行优化,其中没有分配堆栈帧,但末端“下方”的 128 字节堆栈可供函数使用。除非您的内存缓冲区考虑到这一点,否则可能会发生您未预料到的溢出。

  3. 信号是在备用堆栈上处理的(请参阅sigaltstack()参考资料),并且进行自己的堆栈切换可能会使您的代码无法从信号处理程序中调用。它肯定会使其不可重入,并且取决于您检索“堆栈位置”的位置/方式,也肯定会使其成为非线程安全的。

一般来说,如果你想在不同的堆栈上运行一段特定的代码,为什么不呢:

  • 在不同的线程中运行它(每个线程都有不同的堆栈)?
  • 触发例如SIGUSR1并在信号处理程序中运行您的代码(您可以将其配置为使用不同的堆栈)?
  • 通过makecontext()/运行它swapcontext()(参见手册页中的示例)?

编辑:

既然你说“你想比较两个进程的内存”,那么再次有不同的方法,特别是外部进程跟踪 - 附加一个“调试器”(可以是你自己编写的一个进程,ptrace()用来控制你想要的监视,并让它代表您跟踪的人处理断点/检查点,以执行您需要的验证)。这也会更加灵活,因为它不需要更改您检查的代码。

于 2012-01-11T17:25:49.633 回答
1

-fomit-frame-pointer默认开启。除非您计划在禁用优化的情况下进行编译,否则函数不会触及 RSP 的假设(除了序言/尾声)是非常错误的。

即使您确实使用了-O3 -fno-omit-frame-pointer,编译器在某些情况下仍会移动 RSP,尽管他们不会使用它来访问 args 和 locals。例如alloc/C99 VLA,甚至调用具有超过 6 个 args 的函数(或更准确地说,一个 args 不适合寄存器的函数),都会移动 RSP。(调用函数可能只使用 mov 存储,具体取决于编译器选择的策略。)

此外,“收缩包装”优化,其中一个函数延迟保存调用保留的 reg 直到可能的提前退出可能意味着您的堆栈切换发生编译器准备保存/恢复之前。您的恢复可能发生得太晚或太早。( ams 在评论中提到了这一点。)

于 2022-01-05T09:30:17.483 回答