Perché Windows64 utilizza una convenzione di chiamata diversa da tutti gli altri sistemi operativi su x86-64?


110

AMD ha una specifica ABI che descrive la convenzione di chiamata da utilizzare su x86-64. Tutti i sistemi operativi lo seguono, ad eccezione di Windows che ha la propria convenzione di chiamata x86-64. Perché?

Qualcuno conosce le ragioni tecniche, storiche o politiche di questa differenza, o è solo una questione di sindrome del NIH?

Capisco che diversi sistemi operativi possano avere esigenze diverse per cose di livello superiore, ma questo non spiega perché, ad esempio, il parametro di registro che passa l'ordine su Windows è rcx - rdx - r8 - r9 - rest on stackmentre tutti gli altri usano rdi - rsi - rdx - rcx - r8 - r9 - rest on stack.

PS Sono consapevole di come queste convenzioni di chiamata differiscano in generale e so dove trovare i dettagli se necessario. Quello che voglio sapere è perché .

Modifica: per il come, vedere ad esempio la voce di Wikipedia e i collegamenti da lì.


2
Bene, solo per il primo registro: rcx: ecx era il parametro "this" per la convenzione x86 di msvc __thiscall. Quindi probabilmente solo per facilitare il porting del loro compilatore a x64, hanno iniziato con rcx come primo. Che poi anche tutto il resto sarebbe stato diverso era solo una conseguenza di quella decisione iniziale.
Chris Becke

@ Chris: ho aggiunto un riferimento al documento aggiuntivo ABI AMD64 (e alcune spiegazioni su cosa sia effettivamente) di seguito.
FrankH.

1
Non ho trovato una motivazione da MS ma ho trovato alcune discussioni qui
phuclv

Risposte:


81

Scelta di quattro registri di argomenti su x64 - comune a UN * X / Win64

Una delle cose da tenere a mente riguardo a x86 è che il nome del registro per la codifica "numero di registro" non è ovvio; in termini di codifica dell'istruzione (il byte MOD R / M , vedere http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm ), i numeri di registro 0 ... 7 sono - in quest'ordine - ?AX, ?CX, ?DX, ?BX, ?SP, ?BP, ?SI, ?DI.

Quindi scegliere A / C / D (regs 0..2) per il valore di ritorno e i primi due argomenti (che è la __fastcallconvenzione "classica" a 32 bit ) è una scelta logica. Per quanto riguarda i 64 bit, vengono ordinati i reg "superiori", e sia Microsoft che UN * X / Linux hanno scelto R8/ R9come primi.

Tenendo questo in mente, la scelta di Microsoft di RAX(valore di ritorno) e RCX, RDX, R8, R9(arg [0..3]) sono una scelta comprensibile se si sceglie quattro registri per argomenti.

Non so perché l'AMD64 UN * X ABI abbia scelto RDXprima RCX.

Scelta di sei registri di argomenti su x64 - UN * X specifico

UN * X, su architetture RISC, ha tradizionalmente eseguito il passaggio di argomenti nei registri, in particolare per i primi sei argomenti (almeno così su PPC, SPARC, MIPS). Questo potrebbe essere uno dei motivi principali per cui i progettisti ABI AMD64 (UN * X) hanno scelto di utilizzare anche sei registri su quell'architettura.

Quindi, se volete sei registri per passare argomenti, ed è logico scegliere RCX, RDX, R8e R9per quattro di loro, che altri due si dovrebbe scegliere?

I reg "più alti" richiedono un byte di prefisso dell'istruzione aggiuntivo per selezionarli e quindi hanno un'impronta di dimensione dell'istruzione maggiore, quindi non vorresti sceglierne nessuna se hai delle opzioni. Dei registri classici, per il significato implicito di RBPe RSPquesti non sono disponibili, e RBXtradizionalmente ha un uso speciale su UN * X (tabella di offset globale) con cui apparentemente i progettisti ABI di AMD64 non volevano diventare inutilmente incompatibili.
Ergo, l' unica scelta era RSI/ RDI.

Quindi, se devi prendere RSI/ RDIcome registro degli argomenti, quali argomenti dovrebbero essere?

Farli arg[0]e arg[1]ha alcuni vantaggi. Vedi il commento di cHao.
?SIe ?DIsono operandi sorgente / destinazione di istruzioni di stringa e, come menzionato da cHao, il loro uso come registri di argomenti significa che con le convenzioni di chiamata UN * X di AMD64, la strcpy()funzione più semplice possibile , ad esempio, consiste solo delle due istruzioni della CPU repz movsb; retperché l'origine / destinazione gli indirizzi sono stati inseriti nei registri corretti dal chiamante. Esiste, in particolare nel codice "collante" generato dal compilatore e di basso livello (si pensi, ad esempio, ad alcuni allocatori di heap C ++ oggetti che riempiono zero durante la costruzione, o le pagine heap che riempiono lo zero del kernel susbrk(), o copy-on-write pagefaults) un'enorme quantità di block copy / fill, quindi sarà utile per il codice così frequentemente usato per salvare le due o tre istruzioni della CPU che altrimenti caricherebbero tali argomenti di indirizzo di origine / destinazione nel registri "corretti".

Quindi, in un certo senso, UN * X e Win64 sono diversi solo in quella UN * X "antepone" due argomenti aggiuntivi, in volutamente scelti RSI/ RDIregistri, per la scelta naturale di quattro argomenti a RCX, RDX, R8e R9.

Oltre a questo ...

Esistono più differenze tra le ABI UN * X e Windows x64 oltre alla semplice mappatura degli argomenti a registri specifici. Per la panoramica su Win64, controlla:

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

Win64 e AMD64 UN * X differiscono notevolmente anche nel modo in cui viene utilizzato lo stackspace; su Win64, ad esempio, il chiamante deve allocare lo spazio dello stack per gli argomenti della funzione anche se gli argomenti 0 ... 3 vengono passati nei registri. Su UN * X d'altra parte, una funzione foglia (cioè una che non chiama altre funzioni) non è nemmeno richiesta per allocare lo spazio dello stack se non ha bisogno di più di 128 byte di esso (sì, tu possiedi e puoi usare una certa quantità di stack senza allocare ... beh, a meno che tu non sia codice del kernel, una fonte di bug ingegnosi). Tutte queste sono scelte di ottimizzazione particolari, la maggior parte delle ragioni per queste è spiegata nei riferimenti ABI completi a cui fa riferimento il riferimento di wikipedia del poster originale.


1
Informazioni sui nomi di registro: il byte del prefisso può essere un fattore. Ma allora sarebbe più logico per MS scegliere rcx - rdx - rdi - rsi come registri degli argomenti. Ma il valore numerico dei primi otto potrebbe guidarti se stai progettando un ABI da zero, ma non c'è motivo di cambiarli se esiste già un ABI perfettamente corretto, questo porta solo a maggiore confusione.
JanKanis

2
Su RSI / RDI: queste istruzioni saranno solitamente inline, nel qual caso la convenzione di chiamata non ha importanza. Altrimenti, c'è solo una copia (o forse poche) di quella funzione in tutto il sistema, quindi salva solo una manciata di byte in totale . Non ne vale la pena. Su altre differenze / stack di chiamate: L'utilità di scelte specifiche è spiegata nei riferimenti ABI, ma non fanno un confronto. Non dicono perché non sono state scelte altre ottimizzazioni, ad esempio perché Windows non ha la zona rossa di 128 byte e perché l'ABI AMD non ha gli slot di stack aggiuntivi per gli argomenti?
JanKanis

1
@cHao: no. Ma l'hanno cambiato comunque. L'ABI Win64 è diverso da quello Win32 (e non compatibile), e anche diverso dall'ABI AMD.
JanKanis

7
@Somejan: Win64 e Win32 __fastcallsono identici al 100% nel caso in cui non siano presenti più di due argomenti non superiori a 32 bit e restituiscano un valore non superiore a 32 bit. Non è una piccola classe di funzioni. Non è possibile alcuna compatibilità con le versioni precedenti tra gli ABI UN * X per i386 / amd64.
FrankH.

2
@szx: ho appena trovato il thread della mailing list pertinente del novembre 2000 e ho pubblicato una risposta che riassume il ragionamento. Nota che è memcpyche potrebbe essere implementato in questo modo, no strcpy.
Peter Cordes

42

IDK perché Windows ha fatto quello che hanno fatto. Vedere la fine di questa risposta per un'ipotesi. Ero curioso di sapere come fosse stata decisa la convenzione di chiamata SysV, quindi ho scavato nell'archivio della mailing list e ho trovato alcune cose interessanti.

È interessante leggere alcuni di quei vecchi thread sulla mailing list AMD64, poiché gli architetti AMD erano attivi su di essa. Ad esempio, la scelta dei nomi dei registri è stata una delle parti più difficili: AMD ha considerato di rinominare gli 8 registri originali r0-r7 o di chiamare i nuovi registri in modo simileUAX .

Inoltre, il feedback degli sviluppatori del kernel ha identificato cose che rendevano il design originale syscalle swapgsinutilizzabile . È così che AMD ha aggiornato le istruzioni per risolvere il problema prima di rilasciare qualsiasi chip effettivo. È anche interessante che alla fine del 2000, l'ipotesi era che Intel probabilmente non avrebbe adottato AMD64.


La convenzione di chiamata SysV (Linux) e la decisione su quanti registri devono essere conservati per chiamata e salvata per chiamata, è stata presa inizialmente nel novembre 2000 da Jan Hubicka (uno sviluppatore di gcc). Ha compilato SPEC2000 e ha esaminato la dimensione del codice e il numero di istruzioni. Quel thread di discussione rimbalza intorno ad alcune delle stesse idee delle risposte e dei commenti su questa domanda SO. In un secondo thread, ha proposto la sequenza corrente come ottimale e, si spera, finale, generando un codice più piccolo di alcune alternative .

Sta usando il termine "globale" per indicare i registri preservati dalle chiamate, che devono essere push / poppati se usati.

La scelta di rdi, rsi, rdxcome i primi tre argomenti sono stati motivati da:

  • minore risparmio di dimensioni del codice in funzioni che chiamano memseto altre funzioni di stringa C sui loro argomenti (dove gcc inline un'operazione di stringa di ripetizione?)
  • rbxè chiamata preservata perché avere due registri conservati delle chiamate accessibili senza prefissi REX (rbx e rbp) è una vittoria. Presumibilmente scelto perché è l'unico altro registro che non è implicitamente utilizzato da nessuna istruzione. (la stringa di ripetizioni, il conteggio dei turni e gli output / input mul / div toccano tutto il resto)
  • Nessuno dei registri con scopi speciali viene preservato dalle chiamate (vedere il punto precedente), quindi una funzione che desidera utilizzare le istruzioni della stringa di ripetizione o uno spostamento del conteggio delle variabili potrebbe dover spostare gli argomenti della funzione da qualche altra parte, ma non deve salvare / ripristinare il valore del chiamante.
  • Stiamo cercando di evitare RCX all'inizio della sequenza, poiché è un registro usato comunemente per scopi speciali, come EAX, quindi ha lo stesso scopo di mancare nella sequenza. Inoltre non può essere utilizzato per le chiamate di sistema e vorremmo fare in modo che la sequenza delle chiamate di sistema corrisponda il più possibile alla sequenza delle chiamate di funzione.

    (background: syscall/ sysretinevitabilmente distrugge rcx(con rip) e r11(con RFLAGS), in modo che il kernel non possa vedere cosa si trovava originariamente rcxdurante l' syscallesecuzione.)

La chiamata di sistema del kernel ABI è stata scelta per corrispondere alla chiamata di funzione ABI, eccetto r10invece di rcx, quindi un wrapper libc funziona come mmap(2)può solo mov %rcx, %r10/ mov $0x9, %eax/ syscall.


Si noti che la convenzione di chiamata SysV utilizzata da i386 Linux fa schifo rispetto alla __vectorcall a 32 bit di Windows. Passa tutto nello stack e restituisce solo edx:eaxper int64, non per strutture piccole . Non sorprende che sia stato fatto un piccolo sforzo per mantenere la compatibilità con esso. Quando non c'è motivo per non farlo, hanno fatto cose come mantenere la rbxchiamata preservata, dal momento che hanno deciso che averne un altro nell'8 originale (che non ha bisogno di un prefisso REX) era buono.

Rendere ottimale l'ABI è molto più importante a lungo termine di qualsiasi altra considerazione. Penso che abbiano fatto un ottimo lavoro. Non sono del tutto sicuro di restituire strutture impacchettate in registri, invece di campi diversi in registri diversi. Immagino che il codice che li passa in giro per valore senza effettivamente operare sui campi vince in questo modo, ma il lavoro extra di decompressione sembra sciocco. Avrebbero potuto avere più registri di ritorno interi, più del semplice rdx:rax, quindi restituire una struttura con 4 membri potrebbe restituirli in rdi, rsi, rdx, rax o qualcosa del genere.

Hanno considerato il passaggio di interi nei registri vettoriali, perché SSE2 può operare su numeri interi. Fortunatamente non l'hanno fatto. I numeri interi sono usati molto spesso come offset del puntatore e un round trip per impilare la memoria è piuttosto economico . Anche le istruzioni SSE2 richiedono più byte di codice rispetto alle istruzioni intere.


Sospetto che i progettisti di Windows ABI avrebbero potuto mirare a ridurre al minimo le differenze tra 32 e 64 bit a vantaggio delle persone che devono eseguire il port asm dall'uno all'altro o che possono utilizzare un paio #ifdefdi ASM in modo che la stessa fonte possa costruire più facilmente una versione a 32 o 64 bit di una funzione.

Ridurre al minimo i cambiamenti nella toolchain sembra improbabile. Un compilatore x86-64 necessita di una tabella separata di quale registro viene utilizzato per cosa e quale sia la convenzione di chiamata. È improbabile che una piccola sovrapposizione con 32 bit produca risparmi significativi in ​​termini di dimensioni / complessità del codice della toolchain.


1
Penso di aver letto da qualche parte sul blog di Raymond Chen sulla logica alla base della scelta di quei registri dopo il benchmarking dal lato MS, ma non riesco più a trovarlo. Tuttavia alcuni motivi riguardanti la homezone sono stati spiegati qui blogs.msdn.microsoft.com/oldnewthing/20160623-00/?p=93735 blogs.msdn.microsoft.com/freik/2006/03/06/…
phuclv


@phuclv: Vedi anche È valido scrivere sotto ESP? . I commenti di Raymond sulla mia risposta hanno evidenziato alcuni dettagli SEH che non sapevo che spiegassero perché x86 32/64 Windows attualmente non ha una zona rossa di fatto. Il suo post sul blog ha alcuni casi plausibili per la stessa possibilità di gestione della code page-in che ho menzionato in quella risposta :) Quindi sì, Raymond ha fatto un lavoro migliore di spiegarlo di me (non sorprende perché ho iniziato sapendo molto poco di Windows), e la tabella delle dimensioni delle zone rosse per non x86 è davvero chiara.
Peter Cordes

13

Ricorda che Microsoft inizialmente era "ufficialmente non impegnata nei confronti del primo sforzo AMD64" (da "A History of Modern 64-bit Computing" di Matthew Kerner e Neil Padgett) perché erano forti partner di Intel sull'architettura IA64. Penso che questo significasse che anche se sarebbero stati altrimenti aperti a lavorare con gli ingegneri di GCC su un ABI da utilizzare sia su Unix che su Windows, non lo avrebbero fatto perché significherebbe supportare pubblicamente lo sforzo AMD64 quando non lo avevano fatto ' Non l'ho ancora fatto ufficialmente (e probabilmente avrebbe sconvolto Intel).

Inoltre, a quei tempi Microsoft non aveva alcuna tendenza ad essere amichevole con i progetti open source. Certamente non Linux o GCC.

Allora perché avrebbero collaborato a un ABI? Immagino che gli ABI siano diversi semplicemente perché sono stati progettati più o meno nello stesso momento e in isolamento.

Un'altra citazione da "A History of Modern 64-bit Computing":

Parallelamente alla collaborazione con Microsoft, AMD ha anche coinvolto la comunità open source per prepararsi al chip. AMD ha stipulato un contratto con Code Sorcery e SuSE per il lavoro sulla catena di strumenti (Red Hat era già impegnata da Intel sul porting della catena di strumenti IA64). Russell ha spiegato che SuSE ha prodotto compilatori C e FORTRAN e Code Sorcery ha prodotto un compilatore Pascal. Weber ha spiegato che la società si è anche impegnata con la comunità Linux per preparare un port di Linux. Questo sforzo è stato molto importante: ha agito come un incentivo per Microsoft a continuare a investire nello sforzo Windows AMD64 e ha anche assicurato che Linux, che all'epoca stava diventando un sistema operativo importante, sarebbe stato disponibile una volta rilasciati i chip.

Weber si spinge fino a dire che il lavoro su Linux è stato assolutamente cruciale per il successo di AMD64, perché ha permesso ad AMD di produrre un sistema end-to-end senza l'aiuto di altre società, se necessario. Questa possibilità ha assicurato che AMD avesse una strategia di sopravvivenza nel peggiore dei casi anche se gli altri partner si sono ritirati, il che a sua volta ha tenuto impegnati gli altri partner per paura di essere lasciati indietro.

Ciò indica che persino AMD non riteneva che la cooperazione fosse necessariamente la cosa più importante tra MS e Unix, ma che avere il supporto per Unix / Linux era molto importante. Forse anche solo cercare di convincere una o entrambe le parti a scendere a compromessi o cooperare non valeva lo sforzo o il rischio (?) Di irritare nessuno dei due? Forse AMD pensava che anche solo suggerire un ABI comune potesse ritardare o far deragliare l'obiettivo più importante di avere semplicemente il supporto software pronto quando il chip era pronto.

Speculazioni da parte mia, ma penso che il motivo principale per cui gli ABI sono diversi sia stato il motivo politico per cui MS e Unix / Linux non hanno lavorato insieme su di esso, e AMD non lo vedeva come un problema.


Bella prospettiva sulla politica. Sono d'accordo che non è colpa o responsabilità di AMD. Do la colpa a Microsoft per aver scelto una convenzione di chiamata peggiore. Se la loro convenzione di chiamata fosse risultata migliore, avrei avuto un po 'di simpatia, ma hanno dovuto cambiare dal loro ABI iniziale a __vectorcallperché passare __m128lo stack ha fatto schifo. Avere la semantica conservata dalle chiamate per i 128b bassi di alcuni registri vettoriali è anche strano (in parte colpa di Intel per non aver progettato originariamente un meccanismo di salvataggio / ripristino estensibile con SSE e ancora non con AVX.)
Peter Cordes

1
Non ho alcuna esperienza o conoscenza di quanto siano buoni gli ABI. Di tanto in tanto ho bisogno di sapere cosa sono in modo da poter capire / eseguire il debug a livello di assembly.
Michael Burr,

1
Un buon ABI riduce al minimo la dimensione del codice e il numero di istruzioni e mantiene una bassa latenza delle catene di dipendenze evitando round trip aggiuntivi attraverso la memoria. (per args, o per i locali che devono essere versati / ricaricati). Ci sono dei compromessi. La zona rossa di SysV accetta un paio di istruzioni extra in un unico posto (il dispatcher del gestore di segnali del kernel), per un vantaggio relativamente grande per le funzioni foglia di non dover regolare il puntatore dello stack per ottenere un po 'di spazio di lavoro. Quindi questa è una chiara vittoria con uno svantaggio vicino allo zero. È stato adottato praticamente senza discussioni dopo che è stato proposto per SysV.
Peter Cordes

1
@dgnuff: Esatto, questa è la risposta a Perché il codice del kernel non può usare una zona rossa . Gli interrupt utilizzano lo stack del kernel, non lo stack dello spazio utente, anche se arrivano quando la CPU esegue il codice dello spazio utente. Il kernel non si fida degli stack dello spazio utente perché un altro thread nello stesso processo dello spazio utente potrebbe modificarlo, assumendo così il controllo del kernel!
Peter Cordes

1
@ DavidA.Gray: sì, l'ABI non dire si dispone utilizzare RBP come puntatore telaio in modo codice ottimizzato solito no (se non in funzioni che uso allocao alcuni altri casi). Questo è normale se sei abituato a gcc -fomit-frame-pointeressere l'impostazione predefinita su Linux. L'ABI definisce i metadati di svolgimento dello stack che consentono alla gestione delle eccezioni di funzionare ancora. (Presumo che funzioni qualcosa come la roba CFI di GNU / Linux x86-64 System V in .eh_frame). gcc -fomit-frame-pointerè stata l'impostazione predefinita (con l'ottimizzazione abilitata) da sempre su x86-64 e altri compilatori (come MSVC) fanno la stessa cosa.
Peter Cordes

12

Win32 ha i suoi usi per ESI e EDI e richiede che non vengano modificati (o almeno che vengano ripristinati prima di chiamare l'API). Immagino che il codice a 64 bit faccia lo stesso con RSI e RDI, il che spiegherebbe perché non vengono utilizzati per passare gli argomenti delle funzioni.

Tuttavia, non saprei dirti perché RCX e RDX sono cambiati.


1
Tutte le convenzioni di chiamata hanno alcuni registri designati come zero e alcuni come conservati come ESI / EDI e RSI / RDI su Win64. Ma quelli sono registri di uso generale, Microsoft avrebbe potuto scegliere senza problemi di usarli in modo diverso.
JanKanis

1
@Somejan: Certo, se volessero riscrivere l'intera API e avere due diversi sistemi operativi. Non lo definirei "senza problemi", però. Per dozzine di anni ormai, MS ha fatto certe promesse su cosa farà e cosa non farà con i registri x86, e sono stati più o meno coerenti e compatibili per tutto quel tempo. Non getteranno tutto questo fuori dalla finestra solo a causa di qualche editto di AMD, specialmente uno così arbitrario e al di fuori del regno della "costruzione di un processore".
cHao

5
@Somejan: L'ABI AMD64 UN * X è sempre stato esattamente questo: un pezzo specifico di UNIX . Il documento, x86-64.org/documentation/abi.pdf , è intitolato System V Application Binary Interface, AMD64 Architecture Processor Supplement per una ragione. Gli ABI (comuni) UNIX (una raccolta multi-volume, sco.com/developers/devspecs ) lasciano una sezione per il capitolo 3 specifico del processore - il Supplemento - che sono le convenzioni di chiamata delle funzioni e le regole di layout dei dati per un processore specifico.
FrankH.

7
@Somejan: Microsoft Windows non ha mai tentato di essere particolarmente vicino a UN * X e quando si è trattato di portare Windows su x64 / AMD64 hanno semplicemente scelto di estendere la propria __fastcall convenzione di chiamata. Affermi che Win32 / Win64 non siano compatibili, ma poi guarda attentamente: per una funzione che accetta due argomenti a 32 bit e restituisce 32 bit, Win64 e Win32 sono__fastcall effettivamente compatibili al 100% (stessi reg per il passaggio di due argomenti a 32 bit, stesso valore restituito). Anche qualche codice binario (!) Può funzionare in entrambe le modalità operative. Il lato UNIX ha completamente rotto con i "vecchi modi". Per buone ragioni, ma una pausa è una pausa.
FrankH.

2
@ Olof: è più di una semplice cosa del compilatore. Ho avuto problemi con ESI e EDI quando ho fatto cose autonome in NASM. Windows ha decisamente a cuore quei registri. Ma sì, puoi usarli se li salvi prima di farlo e ripristinarli prima che Windows ne abbia bisogno.
cHao
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.