Почему Windows64 использует другое соглашение о вызовах, чем все другие операционные системы на x86-64?

110

У AMD есть спецификация ABI, в которой описывается соглашение о вызовах для использования на x86-64. Все операционные системы следуют ему, за исключением Windows, которая имеет собственное соглашение о вызовах x86-64. Зачем?

Кто-нибудь знает технические, исторические или политические причины этой разницы, или это чисто синдром NIH?

Я понимаю, что разные ОС могут иметь разные потребности в вещах более высокого уровня, но это не объясняет, почему, например, порядок передачи параметров реестра в Windows такой, rcx - rdx - r8 - r9 - rest on stackкак все остальные rdi - rsi - rdx - rcx - r8 - r9 - rest on stack.

PS Я знаю , как эти соглашения о вызовах в целом отличаются , и я знаю , где найти информацию , если мне нужно. Я хочу знать почему .

Изменить: чтобы узнать как, см., Например, запись в Википедии и ссылки оттуда.

ЯнКанис
источник
3
Что ж, только для первого регистра: rcx: ecx был параметром «this» для соглашения msvc __thiscall x86. Так что, вероятно, чтобы облегчить перенос своего компилятора на x64, они начали с rcx в качестве первого. То, что все остальное тогда тоже было бы другим, было лишь следствием этого первоначального решения.
Крис Бек,
@Chris: Я добавил ссылку на дополнительный документ AMD64 ABI (и некоторые пояснения, что это на самом деле) ниже.
Фрэнк Х.
1
Я не нашел объяснения от MS, но нашел здесь
phuclv

Ответы:

81

Выбор четырех регистров аргументов на 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четыре из них, которые других двух вы должны выбрать?

«Старшие» регистры требуют дополнительного байта префикса инструкций для их выбора и, следовательно, имеют больший размер инструкции, поэтому вы не захотите выбирать какой-либо из них, если у вас есть опции. Из классических регистров, из - за неявный смысл RBPи RSPони не доступны, и RBXтрадиционно имеет особое применение на ООН * X (глобальная таблица смещений) , которые , казалось бы , дизайнеры AMD64 ABI не хотели понапрасну стать несовместимым с.
Ergo, единственным выбором были RSI/ RDI.

Итак, если вам нужно использовать RSI/ в RDIкачестве регистров аргументов, какими аргументами они должны быть?

Изготовление их arg[0]и arg[1]имеет ряд преимуществ. См. Комментарий cHao.
?SIи ?DIявляются операндами источника / назначения строковых инструкций, и, как упоминалось в cHao, их использование в качестве регистров аргументов означает, что с соглашениями о вызовах AMD64 UN * X простейшая возможная strcpy()функция, например, состоит только из двух инструкций ЦП, repz movsb; retпоскольку источник / цель адреса были помещены вызывающим абонентом в правильные регистры. Есть, в частности, в низкоуровневом и сгенерированном компилятором "связующем" коде (подумайте, например, о некоторых распределителях кучи C ++, заполняющих объекты нулями при создании, или страницы кучи ядра с нулевым заполнением наsbrk(), или ошибки страницы копирования при записи) огромное количество блоков копирования / заполнения, поэтому это будет полезно для кода, который так часто используется для сохранения двух или трех инструкций ЦП, которые в противном случае загружали бы такие аргументы исходного / целевого адреса в "правильные" регистры.

Таким образом , в некотором смысле, UN * X и Win64 отличаются только тем , что UN * X «помещает» два дополнительных аргумента, в целенаправленно выбранных RSI/ RDIрегистров, к естественному выбору четырех аргументов в RCX, RDX, R8и R9.

За гранью этого ...

Между UN * X и Windows x64 ABI есть больше различий, чем просто отображение аргументов в определенные регистры. Для обзора Win64 проверьте:

http://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx

Win64 и AMD64 UN * X также разительно отличаются по способу использования пространства стека; в Win64, например, вызывающий должен выделить пространство стека для аргументов функции, даже если в регистрах передаются аргументы 0 ... 3. В UN * X, с другой стороны, листовая функция (то есть та, которая не вызывает другие функции) даже не требуется для выделения пространства стека вообще, если ей требуется не более 128 байтов (да, вы владеете и можете использовать определенное количество стека без выделения его ... ну, если вы не код ядра, источник изящных ошибок). Все это конкретные варианты оптимизации, большая часть обоснования которых объясняется в полных ссылках на ABI, на которые указывает ссылка в Википедии исходного постера.

FrankH.
источник
1
Об именах регистров: этот префиксный байт может иметь значение. Но тогда для MS было бы логичнее выбрать в качестве регистров аргументов rcx - rdx - rdi - rsi. Но числовое значение первых восьми может помочь вам, если вы разрабатываете ABI с нуля, но нет причин менять их, если уже существует идеальный ABI, что только ведет к еще большей путанице.
JanKanis
2
На RSI / RDI: эти инструкции обычно встроены, и в этом случае соглашение о вызове не имеет значения. В противном случае, есть только один экземпляр (или , может быть несколько) этой функция общесистемного, поэтому он сохраняет только Handfull байт в общей сложности . Не стоит. О других различиях / стеке вызовов: полезность конкретных вариантов объясняется в справочниках ABI, но в них не проводится сравнение. Они не говорят, почему не были выбраны другие оптимизации - например, почему в Windows нет 128-байтовой красной зоны и почему у AMD ABI нет дополнительных слотов стека для аргументов?
JanKanis
1
@cHao: нет. Но все равно изменили. Win64 ABI отличается от Win32 (и несовместим), а также отличается от ABI AMD.
JanKanis
8
@Somejan: Win64 и Win32 __fastcallна 100% идентичны в случае наличия не более двух аргументов не более 32 бит и возврата значения не более 32 бит. Это немалый класс функций. Такая обратная совместимость между UN * X ABI для i386 / amd64 невозможна.
Фрэнк Х.
2
@szx: Я только что нашел соответствующую ветку списка рассылки от ноября 2000 года и отправил ответ, в котором резюмирует рассуждения. Обратите внимание, что это memcpyможет быть реализовано таким образом, а не strcpy.
Питер Кордес
42

IDK, почему Windows сделала то, что они сделали. Смотрите в конце этого ответа, чтобы предположить. Мне было любопытно, как было принято решение о вызовах SysV, поэтому я покопался в архиве списков рассылки и нашел кое-что интересное.

Интересно читать некоторые из этих старых веток в списке рассылки AMD64, поскольку архитекторы AMD активно участвовали в этом. например, выбор имен регистров был одной из сложных частей: AMD рассматривала переименование исходных 8 регистров r0-r7 или вызов новых регистров вродеUAX .

Кроме того , обратная связь с ядром УБС идентифицированные вещи , которые сделали оригинальный дизайн syscallи swapgsнепригодным для использования . Вот как AMD обновила инструкцию, чтобы разобраться с этим перед выпуском каких-либо реальных чипов. Также интересно, что в конце 2000 года предполагалось, что Intel, вероятно, не станет использовать AMD64.


Соглашение о вызовах SysV (Linux) и решение о том, сколько регистров следует сохранять для вызываемого абонента по сравнению с сохранением для вызывающего, было принято первоначально в ноябре 2000 года Яном Хубицкой (разработчиком gcc). Он скомпилировал SPEC2000 и посмотрел на размер кода и количество инструкций. В этой дискуссии обсуждаются некоторые из тех же идей, что и ответы и комментарии на этот вопрос SO. Во втором потоке он предложил текущую последовательность как оптимальную и, надеюсь, окончательную, генерируя меньший код, чем некоторые альтернативы .

Он использует термин «глобальный» для обозначения регистров с сохранением вызовов, которые должны быть выталкиваются / выталкиваются, если используются.

Выбор rdi, rsi, rdxкак первые три арг был мотивирован:

  • незначительная экономия размера кода в функциях, которые вызывают memsetили другую строковую функцию C в своих аргументах (где gcc встраивает операцию строки rep?)
  • rbxсохраняется вызов, потому что наличие двух регистров с сохранением вызовов, доступных без префиксов REX (rbx и rbp), является выигрышем. Предположительно выбран, потому что это единственный другой регистр, который неявно не используется ни одной инструкцией. (строка повторения, количество сдвигов и выходы / входы multi / div касаются всего остального).
  • Ни один из регистров специального назначения не сохраняется при вызове (см. Предыдущий пункт), поэтому функции, которая хочет использовать инструкции строки реплики или сдвиг подсчета переменных, может потребоваться переместить аргументы функции в другое место, но не нужно сохранять / восстановить значение вызывающего абонента.
  • Мы стараемся избегать RCX на ранних этапах последовательности, поскольку это регистр, обычно используемый для специальных целей, таких как EAX, поэтому он имеет ту же цель, чтобы отсутствовать в последовательности. Кроме того, он не может использоваться для системных вызовов, и мы хотели бы сделать последовательность системных вызовов максимально совпадающей с последовательностью вызовов функций.

    (фон: syscall/ sysretнеизбежно уничтожает rcxrip) и r11RFLAGS), поэтому ядро ​​не может видеть, что было изначально rcxпри syscallзапуске.)

ABI системного вызова ядра был выбран для соответствия вызову функции ABI, за исключением r10вместо rcx, поэтому функции оболочки libc, такие как mmap(2)can, просто mov %rcx, %r10/ mov $0x9, %eax/ syscall.


Обратите внимание, что соглашение о вызовах SysV, используемое i386 Linux, отстойно по сравнению с 32-битным __vectorcall в Windows. Он передает все в стеке и возвращается только edx:eaxдля int64, а не для небольших структур . Неудивительно, что для обеспечения совместимости с ним было приложено мало усилий. Когда нет причин не делать этого, они делали такие вещи, как сохранение rbxвызовов с сохранением, так как они решили, что иметь другой в исходной 8 (для которого не нужен префикс REX) было бы хорошо.

Оптимизация ABI гораздо важнее в долгосрочной перспективе, чем любые другие соображения. Думаю, они проделали довольно хорошую работу. Я не совсем уверен в возвращении структур, упакованных в регистры, вместо разных полей в разных регистрах. Я думаю, что код, который передает их по значению, не работая с полями, таким образом побеждает, но дополнительная работа по распаковке кажется глупой. У них могло быть больше целочисленных регистров возврата, больше, чем просто rdx:rax, поэтому возвращение структуры с 4 членами могло вернуть их в rdi, rsi, rdx, rax или что-то в этом роде.

Они рассматривали возможность передачи целых чисел в векторных регистрах, потому что SSE2 может работать с целыми числами. К счастью, они этого не сделали. Целые числа очень часто используются в качестве смещения указателей, а обращение к стековой памяти довольно дешево . Также инструкции SSE2 занимают больше байтов кода, чем целочисленные инструкции.


Я подозреваю, что разработчики Windows ABI, возможно, стремились минимизировать различия между 32 и 64 битами в интересах людей, которым приходится переносить asm с одного на другой или которые могут использовать пару #ifdefв некоторых ASM, чтобы один и тот же источник мог легче создавать 32- или 64-битная версия функции.

Сведение к минимуму изменений в цепочке инструментов кажется маловероятным. Компилятору x86-64 нужна отдельная таблица, в которой указывается, какой регистр для чего используется и каково соглашение о вызовах. Небольшое перекрытие с 32-битной версией вряд ли приведет к значительной экономии в размере / сложности кода инструментальной цепочки.

Питер Кордес
источник
1
Я думаю, что где-то в блоге Раймонда Чена читал о причинах выбора этих регистров после тестирования со стороны MS, но я больше не могу его найти. Однако некоторые причины, касающиеся домашней зоны, были объяснены здесь blogs.msdn.microsoft.com/oldnewthing/20160623-00/?p=93735 blogs.msdn.microsoft.com/freik/2006/03/06/…
phuclv
@phuclv: См. также Допустимо ли писать под ESP? . Комментарии Раймонда к моему ответу указали на некоторые детали SEH, которые я не знал, которые объясняют, почему x86 32/64 Windows в настоящее время де-факто не имеет красной зоны. В его сообщении в блоге есть несколько правдоподобных случаев той же возможности обработчика кодовой страницы, о которой я упоминал в этом ответе :) Так что да, Раймонд объяснил это лучше, чем я (неудивительно, потому что я начал с очень небольшого знания о Windows), и таблица размеров красных зон для не-x86 действительно хороша.
Питер Кордес
13

Помните, что Microsoft изначально «официально не возлагала никаких обязательств на первые попытки AMD64» (из «Истории современных 64-битных вычислений» Мэтью Кернера и Нила Пэджетта), потому что они были сильными партнерами Intel по архитектуре IA64. Я думаю, это означало, что даже если бы они в противном случае были бы открыты для работы с инженерами GCC над ABI для использования как в Unix, так и в Windows, они бы этого не сделали, поскольку это означало бы публичную поддержку усилий AMD64, когда они этого не сделали. Тем не менее официально это сделано (и, вероятно, расстроило бы 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, которая в то время становилась важной ОС, будет доступна после выпуска чипов.

Вебер заходит так далеко, что говорит, что работа с Linux была абсолютно решающей для успеха AMD64, потому что она позволяла AMD производить сквозную систему без помощи других компаний, если это необходимо. Эта возможность гарантировала, что у AMD была стратегия выживания наихудшего случая, даже если другие партнеры отступили, что, в свою очередь, поддерживало участие других партнеров из-за страха остаться позади.

Это указывает на то, что даже AMD не считала, что сотрудничество между MS и Unix обязательно является самым важным, но поддержка Unix / Linux была очень важной. Может быть, даже попытка убедить одну или обе стороны пойти на компромисс или сотрудничать не стоит усилий или риска (?) Разозлить кого-либо из них? Возможно, AMD думала, что даже предложение общего ABI может задержать или подорвать более важную цель - просто подготовить поддержку программного обеспечения, когда чип будет готов.

Предположения с моей стороны, но я думаю, что основная причина, по которой ABI отличаются, была политическая причина, по которой MS и стороны Unix / Linux просто не работали вместе над этим, и AMD не считала это проблемой.

Майкл Берр
источник
Хороший взгляд на политику. Я согласен, что это не вина и не ответственность AMD. Я виню Microsoft в выборе худшего соглашения о вызовах. Если бы их соглашение о вызовах оказалось лучше, я бы посочувствовал, но им пришлось изменить свой первоначальный ABI на, __vectorcallпотому что передача __m128по стеку - отстой. Наличие семантики с сохранением вызовов для нижних 128b некоторых векторных регистров также является странным (отчасти вина Intel в том, что они не разработали расширяемый механизм сохранения / восстановления изначально с SSE, но все еще не с AVX.)
Питер Кордес,
1
На самом деле у меня нет опыта или знаний о том, насколько хороши ABI. Мне просто время от времени нужно знать, что это такое, чтобы я мог понимать / отлаживать на уровне сборки.
Майкл Берр,
1
Хороший ABI сводит к минимуму размер кода и количество инструкций и поддерживает низкую задержку цепочек зависимостей, избегая дополнительных циклов передачи данных через память. (для аргументов или для локальных, которые нужно пролить / перезагрузить). Есть компромиссы. Красная зона SysV принимает пару дополнительных инструкций в одном месте (диспетчер обработчика сигналов ядра), что дает относительно большое преимущество для листовых функций, поскольку им не нужно настраивать указатель стека, чтобы получить некоторое рабочее пространство. Так что это явная победа с почти нулевым недостатком. Он был принят почти без обсуждения после того, как был предложен для SysV.
Питер Кордес,
1
@dgnuff: Верно, это ответ на вопрос, почему код ядра не может использовать красную зону . Прерывания используют стек ядра, а не стек пользовательского пространства, даже если они поступают, когда ЦП выполняет код пользовательского пространства. Ядро не доверяет стекам пользовательского пространства, потому что другой поток в том же процессе пользовательского пространства может изменить его, тем самым получив контроль над ядром!
Питер Кордес
1
@ DavidA.Gray: да, ABI не говорит, что вы должны использовать RBP в качестве указателя фрейма, поэтому оптимизированный код обычно этого не делает (за исключением используемых функций allocaили некоторых других случаев). Это нормально, если вы привыкли использовать gcc -fomit-frame-pointerLinux по умолчанию. ABI определяет метаданные размотки стека, которые позволяют обрабатывать исключения по-прежнему. (Я предполагаю, что это работает что-то вроде CFI GNU / Linux x86-64 System V .eh_frame). gcc -fomit-frame-pointerпо умолчанию (с включенной оптимизацией) всегда на x86-64, и другие компиляторы (например, MSVC) делают то же самое.
Питер Кордес,
12

Win32 имеет собственное использование для ESI и EDI и требует, чтобы они не изменялись (или, по крайней мере, восстанавливались перед вызовом API). Я предполагаю, что 64-битный код делает то же самое с RSI и RDI, что объясняет, почему они не используются для передачи аргументов функций.

Однако я не могу сказать вам, почему переключаются RCX и RDX.

Чао
источник
1
Во всех соглашениях о вызовах некоторые регистры обозначены как царапины, а некоторые сохранены, например ESI / EDI и RSI / RDI в Win64. Но это регистры общего назначения, и Microsoft могла бы без проблем использовать их по-другому.
JanKanis
1
@Somejan: Конечно, если они хотели переписать весь API и иметь две разные ОС. Однако я бы не назвал это «без проблем». В течение десятков лет MS давала определенные обещания относительно того, что она будет и что не будет делать с регистрами x86, и все это время они были более или менее последовательными и совместимыми. Они не собираются выбросить все это из головы только из-за какого-то указа AMD, особенно такого произвольного и выходящего за рамки «создания процессора».
cHao,
5
@Somejan: AMD64 UN * X ABI всегда был именно таким - специфичным для UNIX . Документ, x86-64.org/documentation/abi.pdf , не зря называется « Двоичный интерфейс приложения System V, дополнение к процессору архитектуры AMD64» . (Общие) UNIX ABI (многотомная коллекция, sco.com/developers/devspecs ) оставляют раздел для главы 3 , относящейся к конкретному процессору, - Дополнение - который представляет собой соглашения о вызовах функций и правила компоновки данных для конкретного процессора.
Фрэнк Х.
7
@Somejan: Microsoft Windows никогда не пыталась быть особенно близкой к UN * X, и когда дело дошло до портирования Windows на x64 / AMD64, они просто решили расширить свое собственное __fastcall соглашение о вызовах. Вы утверждаете Win32 / Win64 не совместим, но потом, посмотрите внимательно: Для функции , которая принимает два 32bit арга и возвращают 32 - битных, Win64 и Win32 __fastcallфактически являются 100% совместимыми (теми же регистрами для прохождения два 32bit арга, то же возвращаемое значения). Даже некоторый двоичный (!) Код может работать в обоих режимах работы. Сторона UNIX полностью отказалась от «старых путей». На то есть веские причины, но перерыв есть перерыв.
Фрэнк Х.
2
@Olof: Это больше, чем просто компилятор. У меня были проблемы с ESI и EDI, когда я работал автономно в NASM. Windows определенно заботится об этих регистрах. Но да, вы можете использовать их, если вы сохраните их перед тем, как это сделаете, и восстановите их до того, как они понадобятся Windows.
cHao