Чому Windows64 використовує інший режим виклику від усіх інших ОС на x86-64?


110

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 Я знаю, як ці конвенції про дзвінки взагалі різняться, і я знаю, де знайти деталі, якщо потрібно. Що я хочу знати, це чому .

Редагувати: про те, як див., Наприклад, запис Вікіпедії та посилання звідти


2
Ну, тільки для першого регістра: rcx: ecx був параметром "цей" для конвенції msvc __thiscall x86. Тому, ймовірно, просто для полегшення перенесення компілятора до x64 вони почали з rcx як першого. Що все інше тоді було б інакше, було лише наслідком того первісного рішення.
Кріс Бекк

@Chris: Я додав посилання на документ про доповнення AMD64 ABI (та деякі пояснення, що це насправді) нижче.
Френк.

1
Я не знайшов обґрунтування у МС, але знайшов тут
phuclv

Відповіді:


81

Вибір чотирьох регістрів аргументів на x64 - загальний для UN * X / Win64

Однією з речей, про які слід пам’ятати про x86, є те, що ім’я регістру для кодування «reg 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 (рег. 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). Що може бути однією з головних причин, чому дизайнери AMI64 (UN * X) ABI вирішили також використовувати шість регістрів для цієї архітектури.

Так що якщо ви хочете шість регістрів для передачі аргументів на, і це логічно вибрати RCX, RDX, R8і R9чотири з них, які інших двох ви повинні вибрати?

Для "вищих" регістрів потрібен додатковий байт префікса інструкції, щоб вибрати їх, і тому вони мають більший розмір розміру інструкції, тому ви не хочете вибирати жоден із них, якщо у вас є варіанти. З класичних регістрів, з-за неявного значення RBPта RSPїх немає, і RBXтрадиційно він має особливе застосування на UN * X (глобальна таблиця зміщення), з якою дизайнери AMD64 ABI, схоже, не хотіли б непотрібно ставати несумісними.
Ерго, єдиним вибором були 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, на які посилається оригінальна вікіпедія оригінального афіші.


1
Про імена регістрів: Цей байт префікса може бути фактором. Але тоді було б більш логічно для MS обрати rcx - rdx - rdi - rsi як регістри аргументів. Але числове значення першої восьми може навести вас, якщо ви розробляєте ABI з нуля, але немає причин їх змінювати, якщо ідеально точний ABI вже існує, що призводить лише до ще більше плутанини.
JanKanis

2
Про RSI / RDI: Ці інструкції, як правило, вказуються, і в такому випадку виклик конвенції не має значення. В іншому випадку, є тільки один екземпляр (або , може бути кілька) цієї функція загальносистемного, тому він зберігає тільки Handfull байт в цілому . Не варто. Інші відмінності / стек викликів: Корисність конкретних варіантів пояснюється в посиланнях ABI, але вони не роблять порівняння. Вони не розповідають, чому не було обрано інших оптимізацій - наприклад, чому Windows не має 128-байтну червону зону, і чому AMD ABI не має додаткових слотів для стеження аргументів?
JanKanis

1
@cHao: ні. Але вони все одно змінили. Win64 ABI відрізняється від Win32 (і не сумісний), а також відрізняється від AMD ABI.
JanKanis

7
@Somejan: Win64 та Win32 __fastcallє 100% однаковими для випадків, коли не більше двох аргументів не більше 32 біт і повернення значення не більше 32 біт. Це не малий клас функцій. Такої зворотної сумісності взагалі неможливо між ABI-кодами UN * X для 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 і переглянув розмір коду та кількість інструкцій. Ця тема обговорення підскакує про ті самі ідеї, що й відповіді та коментарі до цього питання. У 2-му потоці він запропонував поточну послідовність як оптимальну і сподіваюсь остаточну, генеруючи менший код, ніж деякі альтернативи .

Він використовує термін "глобальний", щоб означати регістри, що зберігаються від дзвінків, які повинні використовуватися при натисканні / випуску.

Вибір rdi, rsi, rdxяк перші три арг був мотивований:

  • незначне збереження розміру коду у функціях, що викликають memsetабо іншу функцію C-рядка в їхніх арках (де gcc вказує на операцію з повторним рядком?)
  • rbxзбережено викликом, оскільки наявність двох регістрів, що зберігаються при виклику, доступних без префіксів REX (rbx та rbp) - це виграш. Імовірно, вибраний, оскільки це єдиний інший регістр, який неявно не використовується жодною інструкцією. (репр-рядок, кількість змін і виходи / входи mul / div стосуються всього іншого).
  • Жоден з реєстрів зі спеціальними цілями не зберігається за викликом (див. Попередню точку), тому функція, яка хоче використовувати вказівки для рядка повтору або зміщення підрахунку змінних, можливо, повинна буде переміщувати аргументи функцій кудись ще, але не потрібно зберігати / відновити значення абонента.
  • Ми намагаємось уникати RCX на початку послідовності, оскільки це регістр використовується зазвичай для спеціальних цілей, як EAX, тому він має ту саму мету, щоб його не було в послідовності. Крім того, він не може бути використаний для системних дзвінків, і ми хотіли б зробити послідовність syscall, щоб максимально відповідати послідовності викликів функцій.

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

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


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

Зробити оптимальним ABI набагато важливіше довгострокове, ніж будь-яке інше. Я думаю, що вони зробили досить гарну роботу. Я не зовсім впевнений у поверненні структур, упакованих у регістри, замість різних полів у різних регістрах. Я здогадуюсь, що код, який передає їх за значенням, фактично не оперуючи полями, виграє таким чином, але додаткова робота з розпакування видається дурною. Вони могли мати більше цілих регістрів повернення, більше, ніж просто rdx:rax, тому повернення структури з 4 членами може повернути їх у rdi, rsi, rdx, rax чи щось інше.

Вони розглядали проходження цілих чисел у векторних регістрах, оскільки SSE2 може працювати на цілі числа. На щастя, вони цього не зробили. Цілі лічильники дуже часто використовуються як зміщення покажчика, а зворотній шлях для складання пам'яті досить дешевий . Також інструкції SSE2 приймають більше байтів коду, ніж цілі.


Я підозрюю, що дизайнери Windows ABI, можливо, прагнули мінімізувати різниці між 32 і 64 бітами на користь людям, які повинні переносити ASM від одного до іншого, або вони можуть використовувати пару #ifdefs в деяких 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, коли вони не мали ' 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, що на той час стає важливою ОС, буде доступний після випуску мікросхем.

Вебер іде так далеко, що говорить, що робота 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
Я насправді не маю ніяких знань і знань, наскільки хороші ІПС. Мені просто час від часу потрібно знати, що вони є, щоб я міг зрозуміти / налагодити на рівні складання.
Майкл Берр

1
Хороший ABI мінімізує розмір коду та кількість інструкцій, а також підтримує ланцюги залежностей низькою затримкою, уникаючи зайвих зворотних поїздок через пам'ять. (для арг, або для місцевих жителів, які потрібно розлити / перезавантажити). Є компроміси. Червона зона SysV має кілька додаткових інструкцій в одному місці (диспетчер обробки сигналів ядра), для порівняно великої переваги для функцій листя не потрібно коригувати покажчик стека, щоб отримати трохи місця подряпин. Тож це явна перемога при зниженні майже нуля. Він був прийнятий майже без обговорення після того, як він був запропонований для SysV.
Пітер Кордес

1
@dgnuff: Правильно, це відповідь на те, чому код ядра не може використовувати червону зону . Переривання використовують стек ядра, а не стек простору користувача, навіть якщо вони надходять, коли CPU працює з кодом простору користувача. Ядро не вірить стекам простору користувача, оскільки інший потік у цьому ж просторі користувача може змінити його, переймаючи таким чином контроль над ядром!
Пітер Кордес

1
@ DavidA.Gray: так, ABI не говорить про те, що вам потрібно використовувати RBP як покажчик кадру, тому оптимізованого коду зазвичай немає (за винятком функцій, які використовують allocaабо в кількох інших випадках). Це нормально, якщо ви звикли використовувати gcc -fomit-frame-pointerLinux за замовчуванням. ABI визначає метадані, розмотані стеком, які дозволяють обробляти винятки як і раніше. (Я припускаю, що це працює на кшталт CFI GNU / Linux x86-64 системи 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. Але це регістри загального призначення, корпорація Майкрософт могла без проблем обрати їх по-різному.
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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.