【问题标题】:Calling printf from aarch64 asm code on Apple M1 / MacOS在 Apple M1 / MacOS 上从 aarch64 asm 代码调用 printf
【发布时间】:2021-10-05 16:31:46
【问题描述】:

看来,在 Linux 上运行良好的从 aarch64 asm 代码调用 printf 的常用方法不适用于在 Apple M1 上运行的 MacOS。

是否有任何文档可以解释发生了什么变化?

我发现我在 x0..x2 中输入的参数在 printf 输出中出现乱码。

【问题讨论】:

  • 显示您的代码。我们怎么能不看你的代码就回答?
  • printf 不接受 x1 或 x2 中的参数...
  • 我发现 developer.apple.com/documentation/xcode/… 确实记录了与通常的 ARM64 约定的一些差异。当然,你也可以在 MacOS 上用你的 C 编译器编译一个printf 调用,看看它的样子。
  • 特别是,如果我没看错,它希望所有可变参数都在堆栈上传递?我不知道。如果是这样,那么@Siguza 是正确的:格式字符串指针将进入x0 以及堆栈中的所有其他内容。

标签: macos assembly variadic-functions arm64 calling-convention


【解决方案1】:

Darwin arm64 ABI 传递堆栈上的所有可变参数,每个参数填充到下一个 8 字节的倍数。

这是一个简单的例子:

.globl _main
.align 2
_main:
    stp x29, x30, [sp, -0x10]!
    sub sp, sp, 0x10

    mov x8, 66
    str x8, [sp]
    adr x0, Lstr
    bl _printf

    mov w0, 0
    add sp, sp, 0x10
    ldp x29, x30, [sp], 0x10
    ret

Lstr:
    .asciz "test: %x\n"

请注意,这与在堆栈上传递的非原型函数的非可变参数不同,后者最多只能填充 4 个字节 (sizeof(int))。以下代码:

#include <stdio.h>
#include <stdint.h>

extern void func();
__asm__
(
    "_func:\n"
    "    ret\n"
);

int main(void)
{
    uint8_t a = 1,
            b = 2,
            c = 3;
    printf("%hhx %hhx %hhx %hhx %hhx %hhx\n", a, b, c, a, b, c);
    func(a, b, c, a, b, c, a, b, c, a, b, c);
    return 0;
}

-O2编译成这样:

;-- _main:
0x100003ee8      ff0301d1       sub sp, sp, 0x40
0x100003eec      fd7b03a9       stp x29, x30, [sp, 0x30]
0x100003ef0      fdc30091       add x29, sp, 0x30
0x100003ef4      68008052       mov w8, 3
0x100003ef8      49008052       mov w9, 2
0x100003efc      e92302a9       stp x9, x8, [sp, 0x20]
0x100003f00      2a008052       mov w10, 1
0x100003f04      e82b01a9       stp x8, x10, [sp, 0x10]
0x100003f08      ea2700a9       stp x10, x9, [sp]
0x100003f0c      20040010       adr x0, str._hhx__hhx__hhx__hhx__hhx__hhx_n
0x100003f10      1f2003d5       nop
0x100003f14      13000094       bl sym.imp.printf
0x100003f18      480080d2       mov x8, 2
0x100003f1c      6800c0f2       movk x8, 3, lsl 32
0x100003f20      690080d2       mov x9, 3
0x100003f24      2900c0f2       movk x9, 1, lsl 32
0x100003f28      e92300a9       stp x9, x8, [sp]
0x100003f2c      20008052       mov w0, 1
0x100003f30      41008052       mov w1, 2
0x100003f34      62008052       mov w2, 3
0x100003f38      23008052       mov w3, 1
0x100003f3c      44008052       mov w4, 2
0x100003f40      65008052       mov w5, 3
0x100003f44      26008052       mov w6, 1
0x100003f48      47008052       mov w7, 2
0x100003f4c      e6ffff97       bl sym._func
0x100003f50      00008052       mov w0, 0
0x100003f54      fd7b43a9       ldp x29, x30, [sp, 0x30]
0x100003f58      ff030191       add sp, sp, 0x40
0x100003f5c      c0035fd6       ret

给函数一个实际的原型允许删除任何填充(除了用于对齐目的的填充),就像这样(注意最后一个参数是 8 个字节):

extern void func(uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t,
                 uint8_t, uint8_t, uint8_t, uint8_t, uint8_t, uint64_t);

然后代码编译成:

;-- _main:
0x100003ee4      ff4301d1       sub sp, sp, 0x50
0x100003ee8      f44f03a9       stp x20, x19, [sp, 0x30]
0x100003eec      fd7b04a9       stp x29, x30, [sp, 0x40]
0x100003ef0      fd030191       add x29, sp, 0x40
0x100003ef4      73008052       mov w19, 3
0x100003ef8      54008052       mov w20, 2
0x100003efc      f44f02a9       stp x20, x19, [sp, 0x20]
0x100003f00      28008052       mov w8, 1
0x100003f04      f32301a9       stp x19, x8, [sp, 0x10]
0x100003f08      e85300a9       stp x8, x20, [sp]
0x100003f0c      20040010       adr x0, str._hhx__hhx__hhx__hhx__hhx__hhx_n
0x100003f10      1f2003d5       nop
0x100003f14      13000094       bl sym.imp.printf
0x100003f18      68208052       mov w8, 0x103
0x100003f1c      f30700f9       str x19, [sp, 8]
0x100003f20      f40b0039       strb w20, [sp, 2]
0x100003f24      e8030079       strh w8, [sp]
0x100003f28      20008052       mov w0, 1
0x100003f2c      41008052       mov w1, 2
0x100003f30      62008052       mov w2, 3
0x100003f34      23008052       mov w3, 1
0x100003f38      44008052       mov w4, 2
0x100003f3c      65008052       mov w5, 3
0x100003f40      26008052       mov w6, 1
0x100003f44      47008052       mov w7, 2
0x100003f48      e6ffff97       bl sym._func
0x100003f4c      00008052       mov w0, 0
0x100003f50      fd7b44a9       ldp x29, x30, [sp, 0x40]
0x100003f54      f44f43a9       ldp x20, x19, [sp, 0x30]
0x100003f58      ff430191       add sp, sp, 0x50
0x100003f5c      c0035fd6       ret

【讨论】:

  • 我对@9​​87654322@ 中的评论感到有些困惑:“C 语言需要在调用之前提升小于 int 的参数。除此之外,Apple 平台 ABI 不会添加未使用的字节到堆栈。”除此之外,似乎对于非可变函数,可以打包小于 8 个字节的参数,例如两个 int 参数将共享一个 8 字节的堆栈槽,而不是获得它们自己的。我不确定这句话是否意味着这同样适用于可变参数函数。
  • 所以如果我们做printf("%d %d\n", a, b)a,bint,第二个是[sp+4] 还是[sp+8]
  • @PeterCordes:文本明确显示了非可变函数的情况。 “以下示例说明 Apple 平台如何指定不是 8 字节倍数的基于堆栈的参数。在进入函数时,s0 在当前堆栈指针 (sp) 处占用一个字节,而 s1 在 sp+1 处占用一个字节。编译器仍然在 s1 之后添加填充以满足堆栈的 16 字节对齐要求。”这似乎是 AAPCS 的主要差异之一。
  • @PeterCordes 更新了我的答案 - 如果给定原型,堆栈参数可以更窄。
  • @PeterCordes 是的:如果您编写一个接受int8_t 的函数并比较&lt; 0,clang 只会发出cmp w0, 0,因此它假定该值已被符号扩展。如果在堆栈上传递相同的参数,它会发出ldrsb。但这从 ABI 文档中并不清楚......我希望 clang 回购会有一个更明确的规范,但如果有,我还没有找到它。 ://
猜你喜欢
  • 1970-01-01
  • 2022-07-02
  • 2012-12-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-16
相关资源
最近更新 更多