【问题标题】:Is it safe to use `rax` to `jmp` to a function?使用 `rax` 到 `jmp` 到函数是否安全?
【发布时间】:2021-09-10 08:55:34
【问题描述】:

您好,我遇到了一种情况,需要将参数传递给预定义的函数,如下所示:

void dojmp(args...)
{
    args[0]->func(args...);
}

与其写多个跳转函数来传递不同的参数,我想用裸跳转,这样跳转后,相关的栈和寄存器数据都不会改变。像下面这样。

void __attribute__((naked)) dojmp(void)
{
    __asm__ __volatile__    (   
        "jmp %%rax  \n\t"
        :
        :"a"(args[0]->func)
        :"memory"                   
    );
}

而gcc编译的结果是:

<dojmp>:
  mov    %rdi,%rax
  mov    (%rax),%rax
  jmpq   *%rax

现在我的问题是,使用rax 作为带有void 返回函数的跳转目标(对于gcc)是否安全??

因为通常rax 会存储返回值。但是我调用的所有函数都是无效的......


在这里记住它是为了将相对地址传递给汇编代码,以防我把笔记弄乱了。

#include <stdio.h>

#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)


struct have_func{
    int a;
    void* func;
};

int main()
{
    printf("hello world\n");
}

void __attribute__((naked)) dojmp(struct have_func * args)
{
    asm volatile(
        "jmpq *%P0(%%rdi)\n\t" 
        :
        : "i"(offsetof(struct have_func, func))
    );
}

【问题讨论】:

  • 顺便说一句,$rax 是一个直接操作数,其地址是extern char rax[] 或其他地址,而不是寄存器%rax
  • 您要修复标题和其他文本中的$rax 吗?尤其是在第一个代码块中,除非您确实是指$rax 而不是%%rax。您在未修复的情况下进行了编辑,这很奇怪。此外,GCC 手册明确警告您的 "i"(offsetof(...)) 不受官方支持。在那个简单的naked 函数中,它在实践中可能是安全的,或者至少会以明显的方式中断,但我在更新的答案中引用了它。这就是为什么我还没有在我的回答中使用它,并建议使用 static_assert。

标签: c gcc x86-64 inline-assembly calling-convention


【解决方案1】:

你不需要内联汇编。 调用此包装器函数应该完全等同于将函数指针转换为与用于包装器相同的原型。(就向目标函数传递 arg 而言。)


请记住,RAX 返回值寄存器不是由函数写入的,直到之后你已经跳转到它,所以当然可以使用 RAX 作为临时之前 尾调用,甚至是带有返回值的函数。这不是问题。你弄乱了 RAX,然后函数运行,然后它返回给你的调用者。

但是,x86-64 System V 确实将 AL 用于可变参数函数以传递 XMM 寄存器 arg 的计数。如果您的函数实际上是可变参数,那么在实际 C 源代码中使用 void foo(arg a, ...) 和文字 ... 时,踩上去会有问题。然后需要传递 AL(为了现代实现的效率,以及在一般情况下的正确性,包括使用 AL 计算跳转到 movdqa 存储序列的旧 GCC 代码生成。)

naked 函数中使用Extended Asm 在技术上是不安全的,也不支持(因为处理操作数涉及编译器生成的代码)。因此,出于多种原因,您最好的选择是使用内存间接跳转,而不是要求编译器为您加载:

 asm( "jmpq *(%rdi)" );

offsetof(args, func) 硬编码为 0,因此您可以对其进行 static_assert(或放置在结构中的任何位置)。

使用像"i"(offsetof(..))"m"(args[0]-&gt;func) 这样的扩展Asm 可能有效,但在a naked function 中技术上不支持。 (因为它是扩展 asm)。

此属性允许编译器构造必要的函数声明,同时允许函数体为汇编代码。指定的函数不会有编译器生成的序言/尾声序列。只有基本的 asm 语句可以安全地包含在裸函数中(请参阅基本 Asm)。 虽然使用扩展 asm 或混合使用基本 asm 和 C 代码似乎可以正常工作,但它们不能可靠地工作并且不受支持。


在编译器生成的 asm 中引入这种额外的间接(并消除内联)只是为了让编译器对函数类型感到满意,这有点糟糕。 https://gcc.gnu.org/wiki/DontUseInlineAsm

如果您可以完全避免内联汇编,那就更好了,尽管这可能会以使用可变函数指针类型为代价,但实际上将其指向非可变函数。 x86-64 System V 可以很好地处理这个问题,至少在您不需要它们时没有任何 args 最终被默认提升的情况下。

args[0]->func(args, ...)

例如

  void (*arg0func)(struct Arg, ...) = ( void (*)(struct Arg, ...) )args[0]->func;
  arg0func(args, whatever you were going to pass);

您可以使用 CPP 宏对其进行包装。

甚至可能有一种方法可以编写一个可变参数函数,该函数可以内联并将其所有参数转发到另一个函数。

【讨论】:

  • 非常感谢!!!我真的很愚蠢,没有注意到使用rdi 作为跳转寄存器。而且,偏移地址是在struct定义后设置的,请问是否可以将其传递到rdi版本的汇编代码中~~
  • 嗨,我好像找到了办法,lol~~~真的很感谢你的回答~~
  • @Shore:你最好的选择可能是确保func 成员是第一个,所以它总是在偏移0。如果不使用 Extended Asm,就无法将其传入。不过,即使官方不支持,也可以使用带有 "i" 约束的 Extended Asm。否则,您只能将 C 预处理器常量放入 Basic Asm 语句中,而 offsetof 不是。
  • 顺便说一句,您可能会想从一个可以内联的 asm 语句中使用 call(而不是在 naked 函数中)。这是一个巨大的蠕虫罐头,因为你必须在每个寄存器上声明 clobbers,包括 XMM0..15、ZMM0..31,并且你必须避开红色区域......或者做一个@987654341 @ 在可以内联的函数中的 asm 语句中:但是你需要在 asm 之后 __builtin_unreachable() 告诉 GCC asm 语句不会返回。当然,只有naked 函数让编译器将 args 放入 arg 传递寄存器中。 (或通过 func 指针调用。)
  • 嗨,调用者和函数不在我的控制范围内......这样做的背景是做一个足够灵活的移位器来重定向不同的函数调用。老实说,如果我可以修改调用者的来源,我会简单地使用一个函数指针......
猜你喜欢
  • 2012-10-19
  • 1970-01-01
  • 2016-09-19
  • 2011-10-12
  • 2023-04-10
  • 1970-01-01
  • 2019-09-12
  • 1970-01-01
  • 2015-11-15
相关资源
最近更新 更多