GCC/x86内联asm:你怎么告诉gcc内联汇编部分会修改%esp?

Per*_*erg 11 c x86 assembly gcc inline-assembly

在试图让一些旧代码再次运行时(https://github.com/chaos4ever/chaos/blob/master/libraries/system/system_calls.h#L387,FWIW)我发现一些语义gcc似乎已经改变了在最近10到15年间,以一种非常微妙但仍然危险的方式......:P

该代码曾用于旧版本gcc,如2.95.无论如何,这是代码:

static inline return_type system_call_service_get(const char *protocol_name, service_parameter_type *service_parameter,
    tag_type *identification)
{
    return_type return_value;

    asm volatile("pushl %2\n"
                 "pushl %3\n"
                 "pushl %4\n"
                 "lcall %5, $0"
                 : "=a" (return_value),
                   "=g" (*service_parameter)
                 : "g" (identification),
                   "g" (service_parameter),
                   "g" (protocol_name),
                   "n" (SYSTEM_CALL_SERVICE_GET << 3));

    return return_value;
}
Run Code Online (Sandbox Code Playgroud)

上面的代码的问题是gcc(在我的情况下为4.7)将编译为以下asm代码(AT&T语法):

# 392 "../system/system_calls.h" 1
pushl 68(%esp)  # This pointer (%esp + 0x68) is valid when the inline asm is entered.
pushl %eax
pushl 48(%esp)  # ...but this one is not (%esp + 0x48), since two dwords have now been pushed onto the stack, so %esp is not what the compiler expects it to be
lcall $456, $0

# Restoration of %esp at this point is done in the called method (i.e. lret $12)
Run Code Online (Sandbox Code Playgroud)

问题:变量(identificationprotocol_name)在调用上下文的堆栈中.所以gcc(优化结果,不确定是否重要)将从那里获取值并将其交给内联asm部分.但是因为我在堆栈上推送东西,所以gcc在第三次调用(pushl 48(%esp))中计算的偏移量将减少8 .:)

这花了我很长时间才弄清楚,起初并不是很明显.

最简单的方法当然是使用r输入约束,以确保该值在寄存器中.但还有另一种更好的方法吗?一个显而易见的方法当然是重写整个系统调用接口,而不是首先在堆栈上推送东西(而是使用寄存器,比如Linux),但这不是我今晚想做的重构......

有没有办法告诉gcc内联asm"堆栈是不稳定的"?你们过去一直在处理这样的事情?


同一天晚上更新:我确实找到了一个相关的gccML线程(https://gcc.gnu.org/ml/gcc-help/2011-06/msg00206.html),但它似乎没有帮助.似乎%esp在clobber列表中指定应该使它做偏移%ebp,但它不起作用,我怀疑它在-O2 -fomit-frame-pointer这里有影响.我启用了这两个标志.

Per*_*erg 3

什么有效,什么无效:

  1. 我尝试省略-fomit-frame-pointer. 没有任何效果。我把%espesp列入了破坏者名单sp中。

  2. 我尝试省略-fomit-frame-pointer-O3。这实际上生成了有效的代码,因为它依赖于%ebp而不是%esp.

    pushl 16(%ebp)
    pushl 12(%ebp)
    pushl 8(%ebp)
    lcall $456, $0
    
    Run Code Online (Sandbox Code Playgroud)
  3. 我尝试只在命令行中指定-O3并且未指定。-fomit-frame-pointer创建错误的、损坏的代码(依赖于%esp在整个汇编块内保持不变,即没有堆栈框架)。

  4. 我尝试跳过-fomit-frame-pointer并仅使用-O2. 代码损坏,没有堆栈框架。

  5. 我尝试只使用-O1. 代码损坏,没有堆栈框架。

  6. 我尝试添加cc为破坏者。不可以,没有任何区别。

  7. 我尝试将输入约束更改为ri,给出下面的输入和输出代码。这当然可行,但比我希望的稍微不那么优雅。话又说回来,完美是美好的敌人,所以也许我现在不得不忍受这一点。

输入C代码:

static inline return_type system_call_service_get(const char *protocol_name, service_parameter_type *service_parameter,
    tag_type *identification)
{
    return_type return_value;

    asm volatile("pushl %2\n"
                 "pushl %3\n"
                 "pushl %4\n"
                 "lcall %5, $0"
                 : "=a" (return_value),
                   "=g" (*service_parameter)
                 : "ri" (identification),
                   "ri" (service_parameter),
                   "ri" (protocol_name),
                   "n" (SYSTEM_CALL_SERVICE_GET << 3));

    return return_value;
}
Run Code Online (Sandbox Code Playgroud)

输出asm代码。可以看出,使用寄存器来代替应该总是安全的(但性能可能会稍差,因为编译器必须移动东西):

#APP
# 392 "../system/system_calls.h" 1
pushl %esi
pushl %eax
pushl %ebx
lcall $456, $0
Run Code Online (Sandbox Code Playgroud)

  • gcc 已经将 `-fomit-frame-pointer` 作为 `-O1` 的一部分启用了好几年了,即使对于 32 位代码也是如此,因此您需要显式指定否定形式 `-fno-omit-frame-pointer` 来在启用优化的情况下将其关闭。(但是,是的,要求寄存器或立即数中的所有输入可能是一个更好的主意。)还可能有一个函数属性,可以让您在每个函数的基础上强制使用帧指针。 (2认同)