【问题标题】:Is this inline-asm approach for stack switching ok?这种用于堆栈切换的 inline-asm 方法可以吗?
【发布时间】:2012-01-11 11:54:44
【问题描述】:

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

#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),我也不确定这是否正确。您认为这种方法有什么问题或副作用吗?

【问题讨论】:

  • 请问您希望通过这样做实现什么目标?
  • @aix:我正在努力实现我所说的,即不更改原始堆栈。基本上我有两个相同的进程,我需要定期比较它们的内存(包括堆栈)。对于某些函数,这两个进程将采用不同的执行路径,因此我们将无法正确比较它们的内存,因为它们会变得不同。所以对于这样的功能,如果没有其他分歧发生,我需要切换堆栈,以便两个进程的原始堆栈相同。
  • 警告:GCC 4.7 将包含一个新的“收缩包装”优化,可能会打破你的假设;它将函数序言的一部分延迟到真正需要它们之前,因此如果函数提前退出,程序运行效率会更高。 Ubuntu GCC 已经有这个功能,虽然它默认是禁用的(由于稳定性问题)。
  • 您是否考虑过使用longjmpswapcontext
  • @MetallicPriest:请给你的问题一个更具体的标题。

标签: c gcc x86-64 inline-assembly stack-pointer


【解决方案1】:

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

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

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

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

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

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

编辑:

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

【讨论】:

    【解决方案2】:

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

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

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

    【讨论】:

      猜你喜欢
      • 2016-11-11
      • 2018-09-23
      • 1970-01-01
      • 1970-01-01
      • 2019-05-09
      • 2014-04-06
      • 1970-01-01
      • 1970-01-01
      • 2011-03-20
      相关资源
      最近更新 更多