Clang:通过内联汇编获取函数的参数

Posted

技术标签:

【中文标题】Clang:通过内联汇编获取函数的参数【英文标题】:Clang : getting a function's arguments through inline assembly 【发布时间】:2021-01-03 19:48:53 【问题描述】:

我正在编写一段代码来获取函数的参数,而不使用stdarg。 参数将始终是整数。 平台是 Linux x86_64。 因此调用约定应该是:寄存器%rdi%rsi%rdx%rcx%r8%r8%r9 中的前6 个参数,然后是堆栈中的以下参数。考虑到这一点,我最终得到了以下代码,它使用内联汇编来获取前 6 个参数,然后使用指向堆栈的指针来解析剩余的参数。

#define CFI_DEF_CFA_OFFSET 16ull

void get_args (int arg1, ...)

    register int rdi __asm__ ("rdi"); // 1st arg
    register int rsi __asm__ ("rsi");
    register int rdx __asm__ ("rdx");
    register int rcx __asm__ ("rcx");
    register int r8  __asm__ ("r8" );
    register int r9  __asm__ ("r9" ); // 6th arg

    printf("%d %d %d %d %d %d\n", rdi, rsi, rdx, rcx, r8, r9);

    uint64_t frame_pointer = (uint64_t)__builtin_frame_address(0) + CFI_DEF_CFA_OFFSET;
    printf("%d\n", *((int*)frame_pointer)); // 1st stack argument
    frame_pointer += 8ull; // going to the next
    printf("%d\n", *((int*)frame_pointer)); // and so on ...


int main (void)

    get_args(666, 42, 64, 555, 1111, 8888, 7777, 4444);

这适用于 GCC,但内联汇编部分不适用于 Clang(它可以编译,但值似乎是随机垃圾)。 由于我对汇编的了解有限,并且可能对 cme​​t 在类似问题上的误解,我不明白是否可以用 Clang 以类似的方式读取这些特定的寄存器,如果可以,用什么语法。

感谢您的帮助!

【问题讨论】:

【参考方案1】:

至少有两个大问题,再加上其他问题:

函数可以内联,在这种情况下,没有理由期望 args 存在于任何特定的寄存器中,或者根本存在,因为在 C 级别它们是未使用的。在-O0 不会发生,但不能用于调试模式玩具实验以外的任何东西,除非您使用__attribute__((noinline)) 或将其放在与调用者不同的文件中并且小心使用链接时优化。这是 GCC 和 clang 的绝品。

更重要的是,GNU C register-asm local variables 唯一记录(并因此保证)的效果是确保"r" 约束选择注册扩展asm() 语句。 文档明确保证您所依赖的行为,因此 GCC 不正式支持。它可能会在任何未来的 GCC 版本中中断。 它曾经被记录在案,而 GCC 本身仍然碰巧超出了这个范围,因此通常读取未初始化的 C 变量会得到该寄存器中最初的任何内容,但 clang 不会。这就像读取任何其他未初始化的变量一样。查看编译器生成的 asm 以了解您的代码是如何编译的(例如在 https://godbolt.org/ 上)

还有一个问题:任何编译器生成的代码都可能在这些变量进入作用域之前使用寄存器。可能不太可能出现在函数的顶部。


要做你想做的事,将函数声明为采用 6 个整数/指针参数,然后是可变参数。因此,寄存器参数都有实际有效的 C 名称,并且您在任何地方都不需要 asm 关键字。或者在 asm 中手写get_args

如果您想传递更少的参数,请在调用编译器时撒谎,例如通过提供具有更少参数的原型。

也许使用__attribute__ ((weak, alias (get_args))) 声明一个可变参数函数的原型,您可以使用任意数量的参数调用该函数,但其​​ asm 符号名称与您声明的函数相同。 (这可能会阻止内联,如果它是正确有效的 C,则实际上没有必要。)

我没有尝试过这个,因为它基本上没有意义。如果你想做一些奇怪的事情,依赖调用约定而不是 C 抽象机器,用 asm.xml 编写它。 C 不是一种可移植的汇编语言,现代 C 与它相去甚远,即使使用内联汇编来试图击败它。

【讨论】:

感谢您的回答。我理解我的方法存在的问题。我意识到,在我的原始帖子中可能并不清楚我的主要兴趣是弱模拟stdarg,而不是使用内联汇编。这将假设我在运行时知道参数的数量(即通过第一个参数)和参数类型(总是类似整数)。我看到another of your answers 在那里您似乎设法使用(扩展?)asm 读取寄存器并在 Clang 中编译。该语法是否适合我的问题?非常感谢 @talentless:是的,在函数顶部有多个特定寄存器输出的 asm 语句可能会起作用。但我认为这并不比简单地拥有 6 个 unsigned long 类型的 C 参数和可能的弱别名破解更好。对于实际的实际生产用途,我不建议使用 asm 调用约定 hack。 我终于在某处找到了正确的语法。即使我几乎不明白,我认为这就是你所描述的想法。无论如何感谢您的帮助:)【参考方案2】:

似乎有几种方法可以做到这一点。

我可以将函数声明为采用 6 个参数,然后是可变参数。
void get_args (int argc, int a1, int a2, int a3, int a4, int a5, ...);

这里我可以通过名字获取前 6 个参数,然后使用指向堆栈的指针来获取剩余的参数 要调用少于 6 个参数的函数,我可以通过声明另一个原型来欺骗编译器。例如:

void get_args (int argc, ...);
我可以使用这种汇编语法将寄存器读入变量:
void get_args(int argc, ...)

    int rdi, rsi, rdx, rcx, r8, r9;
    // 32-bit alias (why ? ...)   ↓
    __asm__ __volatile__("movl %%r8d, %%eax" : "=a"(r8) :: "rdi", "rsi", "rdx", "rcx", "r8", "r9");
    // repeat this for the 5 other registers         ↑
 

同样通过抓取堆栈地址(例如__builtin_frame_address(0))来获取>6个参数。

【讨论】:

如果您要使用内联 asm,请使用 one asm 语句,该语句使用 "=D" (rdi), "=S"(rsi), "=d"(rdx), etc. 输出而没有 mov 指令。这可能比 6 个单独的 asm 语句更可靠。另外,当 args 可能是 64 位时,IDK 为什么你使用 32 位 int 好吧,我几乎不明白这个语法是如何工作的(例如,为什么它使用寄存器的 32 位名称)。我将尝试将 6 个电话合二为一。也是的,longs 更有意义,只是保证在我原来的问题中参数是ints。 您是编写mov 指令的人(并选择使用32 位操作数大小来匹配您为变量选择的int)。我对Reading a register value into a C variable 的回答没有这样做。

以上是关于Clang:通过内联汇编获取函数的参数的主要内容,如果未能解决你的问题,请参考以下文章

如何从 C 程序内部或使用内联汇编获取 C 函数的大小?

实用技能分享,充分利用内联函数,内联汇编,内部函数和嵌入式汇编提升代码执行效率和便捷性(2021-12-17)

C语言进阶——内联汇编

C语言进阶——内联汇编

实用技能分享,充分利用内联函数,内联汇编,内部函数和嵌入式汇编提升代码执行效率和便捷性(2021-12-17)

这个 GCC 内联汇编中的参数列表有啥问题?