#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 будет более четкая спецификация, но если это так, я ее не нашел. :/