Вызов printf из кода asm aarch64 на Apple M1 / macOS

#macos #assembly #variadic-functions #arm64 #calling-convention

Вопрос:

Похоже, что обычный подход к вызову printf из кода asm aarch64, который отлично работает в Linux, не работает на macOS, работающих на Apple M1.

Есть ли какая-либо документация, объясняющая, что изменилось?

Я обнаружил, что параметры, которые я ввел в x0..x2, искажаются в выводе printf.

Комментарии:

1. Покажите свой код. Как мы можем ответить, не видя вашего кода?

2. printf не принимает аргументы в x1 или x2…

3. Я нашел developer.apple.com/documentation/xcode/… что действительно документирует некоторые отличия от обычных соглашений ARM64. Конечно, вы также можете скомпилировать printf вызов с помощью компилятора C на macOS и посмотреть, как он выглядит.

4. В частности, если я правильно читаю, он ожидает, что все переменные аргументы будут переданы в стек? Я этого не знал. Если это так, то @Siguza права: указатель строки формата войдет x0 и все остальное в стеке.

Ответ №1:

ABI Darwin arm64 передает все аргументы varags в стеке, каждый из которых дополняется следующим числом, кратным 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: %xn"
 

Обратите внимание, что это отличается от аргументов, отличных от varargs, для незащищенных функций, передаваемых в стеке, которые заполняются только до 4 байт ( sizeof(int) ). Следующий код:

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

extern void func();
__asm__
(
    "_func:n"
    "    retn"
);

int main(void)
{
    uint8_t a = 1,
            b = 2,
            c = 3;
    printf("%hhx %hhx %hhx %hhx %hhx %hhxn", 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
 

Комментарии:

1. Меня немного смутило замечание в примечаниях Apple к ABI : «Язык C требует продвижения аргументов, меньших, чем int, перед вызовом. Кроме того, на платформах Apple ABI не добавляет неиспользуемые байты в стек». Кроме того, кажется, что для непараметрических функций аргументы размером менее 8 байт могут быть упакованы, например, два int аргумента будут совместно использовать 8-байтовый слот стека вместо того, чтобы получать свои собственные. Я не уверен, означает ли это замечание, что то же самое относится к вариативным функциям или нет.

2. Итак , если мы сделаем printf("%d %dn", a, b) это с a,b бытием int , пойдет ли второе [sp 4] или [sp 8] ?

3. @PeterCordes: Текст определенно показывает, что это происходит для невариадных функций. «Следующий пример иллюстрирует, как платформы Apple задают аргументы на основе стека, которые не кратны 8 байтам. При входе в функцию s0 занимает один байт в текущем указателе стека (sp), а s1 занимает один байт в sp 1. Компилятор по-прежнему добавляет заполнение после s1, чтобы удовлетворить требованиям к выравниванию стека в 16 байтов». Это, по-видимому, одно из основных отличий от AAPCS.

4. @PeterCordes обновил мой ответ — аргументы стека могут быть уже, если дать прототип.

5. @PeterCordes да: если вы пишете функцию , которая принимает int8_t и сравнивает это < 0 , clang просто выдает cmp w0, 0 , поэтому предполагается, что значение было расширено по знаку. Если в стеке передается один и тот же аргумент, он выдает ldrsb . Но это не совсем ясно из документа ABI… Я надеялся, что в репо clang будет более четкая спецификация, но если это так, я ее не нашел. :/