Почему Windows64 использует соглашение о вызовах, отличное от всех других ОС на x86-64?
AMD имеет спецификацию ABI, которая описывает соглашение о вызовах для использования на x86-64. Все операционные системы следуют этому, кроме Windows, которая имеет свое собственное соглашение о вызовах x86-64. Зачем?
Кто-нибудь знает технические, исторические или политические причины этого различия или это просто вопрос Синдрома?
Я понимаю, что разные ОС могут иметь разные потребности в вещах более высокого уровня, но это не объясняет, почему, например, порядок передачи параметров реестра в Windows rcx - rdx - r8 - r9 - rest on stack
в то время как все остальные используют rdi - rsi - rdx - rcx - r8 - r9 - rest on stack
,
PS Я знаю о том, как эти соглашения о вызовах отличаются в целом, и я знаю, где найти детали, если мне нужно. То, что я хочу знать, это почему.
Редактировать: как, см., Например, запись в Википедии и ссылки оттуда.
4 ответа
Выбор четырех регистров аргументов на 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). Это может быть одной из основных причин, почему разработчики ABI AMD64 (UN*X) решили также использовать шесть регистров в этой архитектуре.
Итак, если вы хотите, чтобы шесть регистров передавали аргументы, и логично выбрать RCX
, RDX
, R8
а также R9
для четырех из них, какие еще два вы должны выбрать?
"Высокие" регистры требуют дополнительного байта префикса инструкции для их выбора и, следовательно, имеют больший размер инструкции, поэтому вам не захочется выбирать ни одну из них, если у вас есть варианты. Из классических регистров, из-за неявного значения RBP
а также RSP
они не доступны, и RBX
традиционно используется в UN*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
,
Сверх того...
Существует больше различий между ABI UN * X и Windows x64, чем просто сопоставление аргументов с конкретными регистрами. Для обзора на Win64, проверьте:
http://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx
Win64 и AMD64 UN * X также разительно отличаются тем, как используется стековое пространство; например, в Win64 вызывающая сторона должна выделять пространство стека для аргументов функции, даже если аргументы 0...3 передаются в регистрах. В UN * X, с другой стороны, конечная функция (то есть та, которая не вызывает другие функции) даже не требуется для выделения стекового пространства вообще, если ей требуется не более 128 байт (да, вы владеете и можете использовать определенное количество стека без его выделения... ну, если только вы не код ядра, источник изящных ошибок). Все это конкретные варианты оптимизации, большая часть которых объясняется в полных ссылках ABI, на которые указывает ссылка на википедию оригинального плаката.
ИДК, почему 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 rep?) rbx
является сохраняемым при вызове, потому что наличие двух сохраненных при вызове регистров, доступных без префиксов REX (rbx и rbp), является победой. Предположительно выбран, потому что это единственное другое правило, которое неявным образом не используется ни одной инструкцией. (строка rep, счетчик сдвигов и выходы / входы mul / div касаются всего остального).- Ни один из регистров специального назначения не является сохраняемым при вызове (см. Предыдущую точку), поэтому функция, которая хочет использовать строковые инструкции rep или сдвиг счетчика переменных, может нуждаться в перемещении аргументов функции куда-либо еще, но не должна сохранять / восстановить значение звонящего.
Мы стараемся избегать RCX на ранних этапах последовательности, поскольку он часто используется в специальных целях, таких как EAX, поэтому его цель отсутствует в последовательности. Также его нельзя использовать для системных вызовов, и мы хотели бы сделать последовательность системных вызовов максимально соответствующей последовательности вызова функции.
(фон:
syscall
/sysret
неизбежно уничтожитьrcx
(сrip
) а такжеr11
(сRFLAGS
), поэтому ядро не может видеть, что было вrcx
когдаsyscall
побежал.)
Системный вызов ядра ABI был выбран для соответствия вызову функции ABI, за исключением r10
вместо rcx
, так что обертка libc работает как mmap(2)
может просто mov %rcx, %r10
/ mov $0x9, %eax
/ syscall
,
Обратите внимание, что соглашение о вызовах SysV, используемое Linux i386, отстой по сравнению с 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
s в некоторых ASM, поэтому один и тот же источник может легче создать 32- или 64-битную версию функции.
Минимизация изменений в наборе инструментов кажется маловероятной. Компилятору x86-64 нужна отдельная таблица, для которой регистр используется и для чего используется соглашение о вызовах. Небольшое перекрытие с 32-битным вряд ли приведет к значительной экономии в размере / сложности кода инструментальной цепочки.
Помните, что изначально Microsoft "официально не проявляла приверженности ранним усилиям AMD64" (из "Истории современных 64-битных вычислений" Мэтью Кернера и Нила Паджетта), потому что они были сильными партнерами Intel по архитектуре IA64. Я думаю, это означало, что даже если бы они иначе были бы открыты для работы с инженерами GCC над ABI для использования как в Unix, так и в Windows, они бы этого не сделали, поскольку это означало бы публичную поддержку усилий AMD64, когда у них не было ' Официально так и не сделал (и, вероятно, расстроил бы Intel).
Кроме того, в те дни у Microsoft не было абсолютно никаких оснований дружить с проектами с открытым исходным кодом. Конечно, не Linux или GCC.
Так почему бы они сотрудничали на ABI? Я предполагаю, что ABI отличаются просто потому, что они были разработаны более или менее в то же время и в изоляции.
Еще одна цитата из "Истории современных 64-битных вычислений":
Параллельно с коллаборацией Microsoft AMD также привлекла сообщество open source к подготовке чипа. 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 не видела в этом проблемы.
Win32 имеет свои собственные применения для ESI и EDI и требует, чтобы они не были изменены (или, по крайней мере, чтобы они были восстановлены перед вызовом в API). Я представляю, что 64-битный код делает то же самое с RSI и RDI, что объясняет, почему они не используются для передачи аргументов функций.
Я не могу сказать вам, почему переключаются RCX и RDX.