Почему Windows64 использует другое соглашение о вызове от всех других ОС на x86-64?
AMD имеет спецификацию ABI, которая описывает соглашение о вызове для использования на x86-64. Все ОС следуют за ним, за исключением Windows, у которой есть собственное соглашение о вызове x86-64. Почему?
Кто-нибудь знает технические, исторические или политические причины этой разницы, или это просто вопрос NIHsyndrome?
Я понимаю, что разные ОС могут иметь разные потребности для вещей более высокого уровня, но это не объясняет, почему, например, порядок передачи параметров регистра в Windows равен rcx - rdx - r8 - r9 - rest on stack
, а все остальные используют rdi - rsi - rdx - rcx - r8 - r9 - rest on stack
.
P.S. Я знаю, как эти соглашения о вызовах отличаются в целом, и я знаю, где найти детали, если нужно. Я хочу знать, почему.
Изменить: для того, как, например, см. запись в википедии и ссылки оттуда.
Ответы
Ответ 1
Выбор четырех регистров аргументов на x64 - общий для UN * X/Win64
Одна из вещей, которые следует учитывать в отношении x86, заключается в том, что имя регистра для кодировки "reg number" не является очевидным; в терминах кодирования команд (байт MOD R/M, см. http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm), номера регистра 0... 7 - в этом порядке - ?AX
, ?CX
, ?DX
, ?BX
, ?SP
, ?BP
, ?SI
, ?DI
.
Следовательно, выбор A/C/D (regs 0..2) для возвращаемого значения и первые два аргумента (который является "классическим" 32битным соглашением __fastcall
) является логическим выбором. Что касается перехода на 64-битный, то "более высокие" регионы упорядочены, и как Microsoft, так и UN * X/Linux пошли на R8
/R9
в качестве первых.
Помня об этом, выбор Microsoft RAX
(возвращаемое значение) и RCX
, RDX
, R8
, R9
(arg [0..3]) является понятным выбором, если вы выберете четыре для аргументов.
Я не знаю, почему AMD64 UN * X ABI выбрал RDX
до RCX
.
Выбор шести аргументных регистров на x64 - UN * X специфический
UN * X, на архитектурах RISC, традиционно передавал аргументы в регистрах - в частности, для первых шести аргументов (что на PPC, SPARC, MIPS как минимум). Это может быть одной из основных причин, по которой дизайнеры AMD64 (UN * X) ABI решили использовать шесть регистров в этой архитектуре.
Итак, если вы хотите, чтобы шесть регистров передавали аргументы, и логично выбрать RCX
, RDX
, R8
и R9
для четырех из них, а остальные два должны вы выбираете?
Для "более высоких" regs требуется дополнительный байтовый префикс инструкции, чтобы выбрать их и, следовательно, иметь больший размер размера инструкции, поэтому вы не захотите выбирать какие-либо из них, если у вас есть параметры. Из классических регистров из-за неявного значения RBP
и RSP
они недоступны, а RBX
традиционно используется в UN * X (глобальная таблица смещения), которые, по-видимому, дизайнеры AMD64 ABI не использовали, я хочу, чтобы ненужно стало несовместимым с.
Эрго, единственным выбором было RSI
/RDI
.
Итак, если вам нужно взять RSI
/RDI
в качестве аргументов, какие аргументы они должны быть?
Создание их arg[0]
и arg[1]
имеет некоторые преимущества. См. Комментарий cHao.
?SI
и ?DI
являются операндами источника/назначения строки, и, как указано в cHao, их использование в качестве регистров аргументов означает, что с соглашениями о вызовах AMD64 UN * X, например, самая простая возможная функция strcpy()
состоит только из две команды CPU repz movsb; ret
, потому что адреса источника/цели были помещены в правильные регистры вызывающим. В частности, в низкоуровневом и сгенерированном компилятором "клеевом" коде (например, некоторые кубические распределители кучи С++ с нулевым заполнением при построении или страницы кучи нулевого заполнения ядра на sbrk()
или копирование -write pagefaults) огромное количество копий/заполнений блоков, поэтому это будет полезно для кода, который часто используется для сохранения двух или трех инструкций ЦП, которые в противном случае загружали бы такие аргументы источника/целевого адреса в "правильные" регистры.
Таким образом, UN * X и Win64 отличаются только тем, что UN * X "добавляет" два дополнительных аргумента в целенаправленно выбранных регистрах RSI
/RDI
к естественному выбору четырех аргументов в RCX
, RDX
, R8
и R9
.
Помимо этого...
Есть больше различий между ABI UN * X и Windows x64, чем просто сопоставление аргументов с конкретными регистрами. Для обзора Win64 проверьте:
http://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx
Win64 и AMD64 UN * X также поразительно отличаются тем, как используется stackspace; на Win64, например, вызывающий должен выделить stackspace для аргументов функции, даже если аргументы 0... 3 передаются в регистры. С другой стороны, в UN * X функция листа (то есть одна, которая не вызывает другие функции) даже не требуется выделять стековое пространство вообще, если ей требуется не более 128 байтов (да, вы владеете и можете использовать определенное количество стека, не выделяя его... ну, если вы не являетесь кодом ядра, источником отличных ошибок). Все это конкретные варианты оптимизации, большая часть их объяснения объясняется в полном объеме ссылок ABI, на которые ссылается исходная ссылка на wikipedia плаката.
Ответ 2
IDK, почему Windows сделала то, что они сделали. См. Конец этого ответа для догадки. Мне было любопытно, как было принято решение о вызове SysV, поэтому я выкопал в архив списка рассылки и нашел несколько аккуратных вещей.
Интересно прочитать некоторые из этих старых потоков в списке рассылки AMD64, так как на нем были активны архитекторы AMD. например Выбор имен регистров был одной из трудных частей: AMD рассмотрела переименование исходных 8 регистров r0-r7 или вызов новых элементов регистров, таких как UAX
.
Кроме того, обратная связь от разработчиков ядра определила те вещи, которые сделали оригинальную конструкцию syscall
и swapgs
непригодной для использования. Это как AMD обновил инструкцию, чтобы получить эту информацию, прежде чем выпускать какие-либо реальные фишки. Интересно также, что в конце 2000 года было предположено, что Intel, вероятно, не примет AMD64.
Соглашение о вызове SysV (Linux), а решение о том, сколько регистров должно быть сохранено в соответствии с запросом на вызов, было сделанное первоначально в ноябре 2000 года, Jan Hubicka (разработчик gcc). Он скомпилировал SPEC2000 и посмотрел размер кода и количество инструкций. Эта дискуссионная нить подпрыгивает вокруг некоторых из тех же идей, что и ответы и комментарии по этому вопросу SO. Во втором потоке он предложил текущую последовательность как оптимальную и, надеюсь, окончательную, генерируя меньший код, чем некоторые альтернативы.
Он использует термин "глобальный" для обозначения регистров, сохраняемых при вызове, которые должны быть нажаты/выведены, если они используются.
Выбор rdi
, rsi
, rdx
в качестве первых трех аргументов был мотивирован:
- сохранение меньшего размера кода в функциях, которые вызывают
memset
или другую функцию строки C в своих аргументах (где gcc встраивает операцию rep string?)
-
rbx
является сохранением вызова, поскольку наличие двух сохраняемых вызовов regs, доступных без префиксов REX (rbx и rbp), является победой. Предположительно выбран, потому что это единственный другой рег, который неявно используется какой-либо инструкцией. (строка rep, количество сдвигов и mul/div выходы/входы касаются всего остального).
- Ни один из регистров со специальными целями не сохраняется (см. предыдущую точку), поэтому функция, которая хочет использовать команды rep string или смену с переменным числом, может перемещать функции args где-то в другом месте, но не имеет для сохранения/восстановления значения вызывающего абонента.
-
Мы пытаемся избежать RCX в начале последовательности, так как это регистр обычно используется для специальных целей, таких как EAX, поэтому имеет такую же цель отсутствует в последовательности. Также он не может использоваться для системных вызовов, и мы хотели бы сделать последовательность syscall чтобы максимально соответствовать последовательности вызовов функций.
(background: syscall
/sysret
unavoidably destroy rcx
(с rip
) и r11
(с RFLAGS
), поэтому ядро не может видеть, что изначально было в rcx
, когда syscall
побежал.)
Системный вызов ABI ядра был выбран для соответствия вызову функции ABI, за исключением r10
вместо rcx
, поэтому обертка libc, такая как mmap(2)
, может просто mov %rcx, %r10
/mov $0x9, %eax
/syscall
.
Обратите внимание, что соглашение о вызове SysV, используемое i386 Linux, сравнивается с Window 32bit __vectorcall. Он передает все в стек и возвращается только в edx:eax
для int64, а не для небольших структур. Неудивительно, что для поддержания совместимости с этим было мало усилий. Когда нет причин для этого, они делали такие вещи, как сохранение rbx
вызовов, поскольку они решили, что наличие другого в исходном 8 (которое не требует префикса REX) было хорошим.
Оптимизация ABI гораздо важнее долгосрочного, чем любое другое соображение. Я думаю, они сделали очень хорошую работу. Я не совсем уверен в возврате структур, упакованных в регистры, вместо разных полей в разных регистрах. Я предполагаю, что код, который передает их по значению, фактически не работает на полях, побеждает таким образом, но дополнительная работа по распаковке кажется глупой. У них могло быть больше целочисленных регистров возврата, больше, чем просто rdx:rax
, поэтому возврат структуры с 4 членами может вернуть их в rdi, rsi, rdx, rax или что-то в этом роде.
Они рассматривали передачу целых чисел в векторных regs, потому что SSE2 может работать с целыми числами. К счастью, они этого не сделали. Целые числа очень часто используются как смещения указателей, а круговое перемещение в стеке довольно дешево. Кроме того, инструкции SSE2 занимают больше байтов кода, чем целые инструкции.
Я подозреваю, что дизайнеры Windows ABI, возможно, стремились свести к минимуму различия между 32 и 64 битами в интересах людей, которым приходится переносить asm из одного в другой, или которые могут использовать пару #ifdef
в некоторых ASM, поэтому одно и то же источник может более легко построить 32 или 64-битную версию функции.
Сведение к минимуму изменений в toolchain кажется маловероятным. Для компилятора x86-64 требуется отдельная таблица, для которой используется регистр, и что такое соглашение о вызове. Наличие небольшого перекрытия с 32 бит вряд ли приведет к значительной экономии в размере/сложности кода инструментальной цепочки.
Ответ 3
Win32 имеет собственные возможности использования ESI и EDI и требует, чтобы они не были изменены (или, по крайней мере, они были восстановлены до вызова в API). Я бы предположил, что 64-битный код делает то же самое с RSI и RDI, что объясняет, почему они не используются для передачи аргументов функции.
Я не мог сказать вам, почему RCX и RDX переключаются.
Ответ 4
Помните, что Microsoft изначально "официально уклонялась от ранних усилий AMD64" (от "История современных 64-битных вычислений" Мэтью Кернера и Нила Паджетта) потому что они были сильными партнерами Intel по архитектуре IA64. Я думаю, что это означало, что даже если бы они в противном случае были бы открыты для работы с инженерами GCC в ABI для использования как в Unix, так и Windows, они бы этого не сделали, поскольку это означало бы публичную поддержку усилий AMD64, t еще официально сделал это (и, вероятно, это расстроило бы Intel).
Кроме того, еще в те времена у Microsoft не было никаких претензий к тому, чтобы быть дружелюбными с проектами с открытым исходным кодом. Конечно, не Linux или GCC.
Итак, почему они сотрудничали в ABI? Я бы предположил, что ABI отличаются друг от друга, потому что они были разработаны более или менее одинаково и изолированы.
Еще одна цитата из "История современных 64-разрядных вычислений":
Параллельно с сотрудничеством Microsoft AMD также сообщество с открытым исходным кодом для подготовки к чипу. AMD заключила контракт с как Code Sorcery, так и SuSE для работы цепочки инструментов (Red Hat уже была подключенный Intel к порту цепи инструмента IA64). Рассел объяснил, что SuSE производил компиляторы C и FORTRAN, а Code Sorcery - Pascal. Вебер объяснил, что компания также занимается сообщество Linux готовит порт Linux. Это было очень важно: оно послужило стимулом для Microsoft продолжать инвестировать в усилия AMD64 Windows, а также обеспечить, чтобы Linux, в то время становилась важной ОС, будет доступна после чипы были выпущены.
Weber дошел до того, что Linux-работа была абсолютно решающей к успеху AMD64s, поскольку он позволил AMD выпускать сквозной системы без помощи каких-либо других компаний, если это необходимо. Эта возможность обеспечения того, чтобы AMD имела наихудшую стратегию выживания даже если другие партнеры отступили, что, в свою очередь, оставило других партнеров боясь остаться за собой.
Это указывает на то, что даже AMD не считала, что сотрудничество обязательно является самым важным из MS и Unix, но поддержка Unix/Linux очень важна. Может быть, даже попытка убедить одну или обе стороны пойти на компромисс или сотрудничество не стоила усилий или рисковала (?) Раздражать их обоих? Возможно, AMD подумала, что даже предлагая общий ABI может задержать или сорвать более важную цель просто получить поддержку программного обеспечения, когда чип был готов.
Спекуляция с моей стороны, но я думаю, что основная причина, по которой ABI отличаются, была политическая причина, по которой MS и Unix/Linux просто не работали вместе, и AMD не рассматривала это как проблему.
Ответ 5
Я бы предположил, что, когда какой-либо компилятор, который вы используете, хочет вызвать Windows API, он будет использовать соглашение о вызове Windows. Какое соглашение, используемое компилятором в приложении, обычно настраивается как опция компилятора. Я бы предположил, что использование передачи данных на основе регистров является функцией повышения производительности.
Я разрабатываю для x86-32 с помощью OpenWatcom с опцией передачи параметров регистра. С ним никогда не возникало никаких проблем (например, неправильное форматирование вызовов в Windows).