【问题标题】:Returning function arguments from assembly从程序集中返回函数参数
【发布时间】:2015-05-23 12:27:58
【问题描述】:

我在 Linux 上使用 AMD 64 位(我认为确切的架构并不重要),也是 64 位。用gcc编译到elf64。

我从 C ABI 中看到整数参数通过通用寄存器传递给函数,并且我可以在我的代码(被调用者)的汇编端找到值。当我需要将结果从被调用者检索到调用者时,就会出现问题。

据我所知,RAX 获取第一个整数返回值,我可以轻松找到并使用那个值。第二个整数 返回 值通过 RDX 传递。这就是让我困惑的地方。

我还可以从 C ABI 中看到,RDX 是用于将第三个整数函数参数从调用者传递给被调用者的寄存器,但我的函数不使用第三个参数。

如何从我的函数中取出 RDX?我是否必须在函数中伪造一个参数才能在调用方引用它?


定点乘法 16.16:

从 C 调用看起来像:

typedef long int Fixedpoint;
Fixedpoint _FixedMul(Fixedpoint v1, Fixedpoint v2);

这是函数本身:

_FixedMul:
   push bp
   mov bp, sp
; entering the function EDI contains v1, ESI contains v2. So:
   mov eax, edi   ; eax = v1
   imul dword esi ; eax = v1 * v2
                  ; at this point EDX contains the higher part of the
                  ; imul moltiplication, EAX the lower one.
   add eax, 8000h ; round by adding 2^(-17)
   adc edx, 0     ; whole part of result is in DX
   shr eax, 16    ; put the fractional part in AX
   pop bp
   ret

来自System V Application Binary Interface AMD64 Architecture Processor Supplement

返回值算法:

值的返回按照如下方式进行

  1. 使用分类算法对返回类型进行分类。
  2. 如果该类型具有类 MEMORY,则调用者为返回提供空间 值并在 %rdi 中传递此存储的地址,就好像它是第一个一样 函数的参数。实际上,这个地址变成了“隐藏”的第一个地址 牙龈。此存储不得与被调用方可见的任何数据重叠 此参数以外的其他名称。 返回时 %rax 将包含由 %rdi 中的调用者。
  3. 如果类是 INTEGER,则序列 %rax 的下一个可用寄存器, 使用了 %rdx。

我希望我的意思更清楚。

PS:对于我在 cmets 中造成的混乱,我深表歉意。感谢您的提示。

【问题讨论】:

  • 只能返回 1 个结果,根据类型在 raxxmm0 或内存中。
  • 你的英语绝对没问题。参数的传递方式取决于所采用的调用约定。实际返回值在(E/R)AX中返回;但是,如果函数接收到一些指向“返回”更多值的指针,这些指针将作为普通参数传递(通常在堆栈上,在最常见的 __stdcall__cdecl 中)。
  • 我想我需要从x86-64.org/documentation/abi.pdf过去一个sn-p:
  • 由于您正在尝试优化速度,请记住,在“普通”(Intel/AMD)x86 CPU 上,定点数学需要更多指令,足以使其比仅使用强大的硬件浮点。此外,在 amd64 上,您需要推送/弹出 rbp,而不仅仅是低 16 位!如果您没有将任何局部变量溢出到堆栈上的内存中,甚至不必通过保存 rsp 来创建堆栈帧。
  • 另外,如果要进行 64 位乘法运算,请使用 64 位寄存器。那么你就不需要 add-with-cary 了。也许将您的其他函数分成两部分携带定点数据会更好。

标签: c assembly x86-64 calling-convention


【解决方案1】:

AMD64 calling conventions (System V ABI) 指定寄存器 RDX 的双重用途:它可以用于传递第三个参数(如果有)并且它可以用作第二个返回寄存器。 (参见图 3.4,第 21 页)

根据函数签名,它用于这些角色中的一个或不用于这些角色。

那么为什么会有 2 个返回寄存器(即 RAX 和 RDX)?这允许函数在通用寄存器中返回最多 128 位的值 - 例如 __int128 或带有两个 8 字节字段的 struct

因此,要从 C 调用站点访问 RDX 寄存器,您只需调整函数的签名。这意味着而不是

typedef long int Fixedpoint;
Fixedpoint _FixedMul(Fixedpoint v1, Fixedpoint v2);

声明为:

typedef long int Fixedpoint;
struct Fixedpoint_Pair { // when used as return value:
    Fixedpoint fst;      //   - passed in RAX
    Fixedpoint snd;      //   - passed in RDX
};
typedef struct Fixedpoint_Pair Fixedpoint_Pair;

Fixedpoint_Pair _FixedMul(Fixedpoint v1, Fixedpoint v2);

然后你可以在你的 C 代码中像这样访问 RDX:

Fixedpoint_Pair r = _FixedMul(a, b);
printf("RDX: %ld\n", r.snd);

参考资料:

  • 第 18,19 页:尤其是“聚合(结构和数组)的分类”,第 3 点:

如果聚合的大小超过单个八字节,则每个都被分类 分别地。每八字节被初始化为 NO_CLASS 类。

  • 第 22 页:“返回值”,第 3 点:

如果类是 INTEGER,则序列 %rax 的下一个可用寄存器, 使用 %rdx。

【讨论】:

  • 也许值得指出的是,OP 的 asm 与它们的函数签名不匹配:它只使用每个 64 位 long 的低 32 位。并且由于某些疯狂的原因保存/恢复bp(但不是完整的 RBP)。或不;我确实已经在问题下对此发表了评论。
  • @PeterCordes,至少高 32 位是零扩展的,因此函数签名是兼容的。但可以肯定的是,函数体中的 OP 代码没有多大意义。可以说,具体的身体甚至与问题无关,真的。我看到了你的 cmets,我对他们投了赞成票。 :)
  • 是的,它不会导致 UB 或调用约定不匹配,并且适用于小输入。相反的问题可能更糟。假设函数 args 被零扩展为 64 位,而实际上它们可能包含调用者指望您忽略的高垃圾。 (与某些 RISC ABI 不同,x86-64 System V 允许高垃圾,因为每条指令都可用于(几乎)每个操作数大小。除了 clang 依赖于窄 args 零或符号扩展为 32 位,GCC 还实现调用者的非官方行为,但不依赖于被调用者。)
【解决方案2】:

你的英语很好。

如何传递参数取决于所采用的调用约定 - 两个最常见的,__stdcall__cdecl,使用堆栈来传递所有参数,但例如 __fastcall 约定将使用寄存器对于前两个参数,在 x64 中,它仍然不同。有关完整列表,请参阅 here

实际返回值在(E/R)AX中返回;但是,如果函数接收到一些指向“返回”更多值的指针,这些指针将作为普通参数传递 - 如前所述,通常在堆栈上。

【讨论】:

  • 我明白了。关键是我正在尝试优化速度,因此涉及的内存移动越少越好。我可以“SHL EDX,16”和“OR EAX,EDX”在 EAX 上获得完整的结果,或者简单地将 EDX 移动到 ESI 上以在 v2 上获得它,或者类似的东西。但是没有出路:rAX 是唯一一个“真正的”输出寄存器。我的错,我完全误解了 ABI。 :) 感谢您的回答。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-27
  • 2017-09-12
  • 2018-05-06
相关资源
最近更新 更多