AMD verfügt über eine ABI-Spezifikation, die die auf x86-64 zu verwendende Aufrufkonvention beschreibt. Alle Betriebssysteme folgen ihm, mit Ausnahme von Windows, das über eine eigene x86-64-Aufrufkonvention verfügt. Warum?
Kennt jemand die technischen, historischen oder politischen Gründe für diesen Unterschied oder handelt es sich lediglich um ein NIH-Syndrom?
Ich verstehe, dass verschiedene Betriebssysteme unterschiedliche Anforderungen an übergeordnete Dinge haben können, aber das erklärt nicht, warum zum Beispiel die Reihenfolge der Registerparameterübergabe unter Windows so ist, wie sie rcx - rdx - r8 - r9 - rest on stack
alle anderen verwenden rdi - rsi - rdx - rcx - r8 - r9 - rest on stack
.
PS Ich bin mir bewusst, wie sich diese Aufrufkonventionen im Allgemeinen unterscheiden, und ich weiß, wo ich Details finden kann, wenn ich muss. Was ich wissen möchte ist warum .
Bearbeiten: Für das Wie siehe zB den Wikipedia-Eintrag und die Links von dort.
quelle
Antworten:
Auswahl von vier Argumentregistern auf x64 - gemeinsam für UN * X / Win64
Eines der Dinge, die Sie bei x86 beachten sollten, ist, dass der Registername für die Codierung "reg number" nicht offensichtlich ist. In Bezug auf die Befehlskodierung (das MOD R / M- Byte, siehe http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm ) sind die Registernummern 0 ... 7 - in dieser Reihenfolge -
?AX
,?CX
,?DX
,?BX
,?SP
,?BP
,?SI
,?DI
.Daher ist die Wahl von A / C / D (Regs 0..2) als Rückgabewert und der ersten beiden Argumente (die "klassische" 32-Bit-
__fastcall
Konvention) eine logische Wahl. In Bezug auf 64-Bit werden die "höheren" Regs bestellt, und sowohl Microsoft als auch UN * X / Linux haben sich fürR8
/R9
als erste entschieden.Mit diesem Hintergedanken, Microsofts Wahl
RAX
(Rückgabewert) undRCX
,RDX
,R8
,R9
(arg [0..3]) ist eine verständliche Auswahl , wenn Sie wählten vier Register für Argumente.Ich weiß nicht, warum sich der AMD64 UN * X ABI
RDX
zuvor entschieden hatRCX
.Auswahl von sechs Argumentregistern für x64 - UN * X-spezifisch
UN * X hat auf RISC-Architekturen traditionell Argumente in Registern übergeben - speziell für die ersten sechs Argumente (zumindest bei PPC, SPARC, MIPS). Dies könnte einer der Hauptgründe sein, warum die AMBI-Designer von AMD64 (UN * X) auch für diese Architektur sechs Register verwendet haben.
Also , wenn Sie wollen sechs Register übergeben Argumente, und es ist logisch , zu wählen
RCX
,RDX
,R8
undR9
für vier von ihnen, die beiden anderen sollten Sie wählen?Die "höheren" Register erfordern ein zusätzliches Befehlspräfix-Byte, um sie auszuwählen, und haben daher einen größeren Platzbedarf für Befehle. Sie möchten also keines davon auswählen, wenn Sie Optionen haben. Von den klassischen Registern sind diese aufgrund der impliziten Bedeutung von
RBP
undRSP
diese nicht verfügbar und werdenRBX
traditionell speziell für UN * X (Global Offset Table) verwendet, mit dem die AMD64 ABI-Designer anscheinend nicht unnötig inkompatibel werden wollten.Ergo, die einzige Wahl war
RSI
/RDI
.Also, wenn Sie
RSI
/RDI
als Argumentregister nehmen müssen, welche Argumente sollten sie sein?Sie zu machen
arg[0]
undarg[1]
hat einige Vorteile. Siehe den Kommentar von cHao.?SI
und?DI
sind Zeichenfolgenbefehls-Quell- / Zieloperanden, und wie cHao erwähnt, bedeutet ihre Verwendung als Argumentregister, dass bei den AMD64 UN * X-Aufrufkonventionen die einfachste möglichestrcpy()
Funktion beispielsweise nur aus den zwei CPU-Befehlen besteht,repz movsb; ret
weil die Quelle / das Ziel Adressen wurden vom Anrufer in die richtigen Register eingetragen. Dies gilt insbesondere für Low-Level- und Compiler-generierten "Kleber" -Code (denken Sie beispielsweise an einige C ++ - Heap-Allokatoren, die Objekte bei der Erstellung nicht füllen, oder an die Kernel-Zero-Filling-Heap-Seitensbrk()
(oder Seitenfehler beim Kopieren beim Schreiben) eine enorme Menge an Blockkopien / -füllungen, daher ist es nützlich für Code, der so häufig zum Speichern der zwei oder drei CPU-Anweisungen verwendet wird, die ansonsten solche Quell- / Zieladressenargumente in die laden würden "richtige" Register.So in einer Art und Weise, UN * X und Win64 sind nur insofern anders, als UN * X „ wird vorangestellt“ zwei weitere Argumente, in gezielt ausgewählten
RSI
/RDI
Register, auf die natürliche Wahl der vier ArgumenteRCX
,RDX
,R8
undR9
.Darüber hinaus ...
Es gibt mehr Unterschiede zwischen den UN * X- und Windows x64-ABIs als nur die Zuordnung von Argumenten zu bestimmten Registern. Die Übersicht über Win64 finden Sie unter:
http://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx
Win64 und AMD64 UN * X unterscheiden sich auch deutlich in der Art und Weise, wie Stackspace verwendet wird. Unter Win64 muss der Aufrufer beispielsweise Stackspace für Funktionsargumente zuweisen, obwohl die Argumente 0 ... 3 in Registern übergeben werden. Auf UN * X hingegen ist eine Blattfunktion (dh eine, die keine anderen Funktionen aufruft) nicht einmal erforderlich, um Stapelspeicher zuzuweisen, wenn sie nicht mehr als 128 Byte benötigt (ja, Sie besitzen und können verwenden eine bestimmte Menge an Stack, ohne ihn zuzuweisen ... nun, es sei denn, Sie sind Kernel-Code, eine Quelle für raffinierte Fehler). All dies sind spezielle Optimierungsoptionen. Die meisten Gründe hierfür werden in den vollständigen ABI-Referenzen erläutert, auf die die Wikipedia-Referenz des Originalplakats verweist.
quelle
__fastcall
sind zu 100% identisch, wenn nicht mehr als zwei Argumente nicht größer als 32 Bit sind und ein Wert zurückgegeben wird, der nicht größer als 32 Bit ist. Das ist keine kleine Klasse von Funktionen. Eine solche Abwärtskompatibilität zwischen den UN * X-ABIs für i386 / amd64 ist überhaupt nicht möglich.memcpy
dass dies auf diese Weise implementiert werden könnte, nichtstrcpy
.IDK, warum Windows das getan hat, was sie getan haben. Eine Vermutung finden Sie am Ende dieser Antwort. Ich war neugierig, wie die SysV-Anrufkonvention beschlossen wurde, also habe ich mich im Mailinglistenarchiv umgesehen und ein paar nette Sachen gefunden.
Es ist interessant, einige dieser alten Threads auf der AMD64-Mailingliste zu lesen, da AMD-Architekten darauf aktiv waren. zB war die Auswahl von Registernamen einer der schwierigen Aspekte
UAX
: AMD erwog, die ursprünglichen 8 Register r0-r7 umzubenennen oder die neuen Register aufzurufen .Das Feedback von Kernel-Entwicklern identifizierte auch Dinge, die das ursprüngliche Design von
syscall
undswapgs
unbrauchbar machten . Auf diese Weise hat AMD die Anweisung aktualisiert , um dies zu klären, bevor tatsächliche Chips freigegeben werden. Interessant ist auch, dass Ende 2000 davon ausgegangen wurde, dass Intel AMD64 wahrscheinlich nicht einführen würde.Die SysV (Linux) -Aufrufkonvention und die Entscheidung, wie viele Register im Vergleich zur Anrufer-Speicherung aufbewahrt werden sollen, wurde ursprünglich im November 2000 von Jan Hubicka (einem gcc-Entwickler) getroffen. Er kompilierte SPEC2000 und untersuchte die Codegröße und die Anzahl der Anweisungen. Dieser Diskussionsthread dreht sich um einige der gleichen Ideen wie Antworten und Kommentare zu dieser SO-Frage. In einem zweiten Thread schlug er die aktuelle Sequenz als optimal und hoffentlich endgültig vor und erzeugte kleineren Code als einige Alternativen .
Er verwendet den Begriff "global", um aufruferhaltene Register zu bezeichnen, die bei Verwendung gepusht / gepoppt werden müssen.
Die Wahl
rdi
,rsi
,rdx
als die ersten drei args war motiviert durch:memset
oder andere C-String-Funktionen in ihren Argumenten aufrufen (wo gcc eine Wiederholungs-String-Operation einfügt?)rbx
ist anruferhalten, da es ein Gewinn ist, wenn zwei anruferhaltene Regs ohne REX-Präfixe (rbx und rbp) zugänglich sind. Vermutlich gewählt, weil es die einzige andere Registrierung ist, die von keiner Anweisung implizit verwendet wird. (Wiederholungszeichenfolge, Verschiebungsanzahl und Mul / Div-Ausgänge / Eingänge berühren alles andere).(Hintergrund:
syscall
/sysret
Zerstöre unvermeidlichrcx
(mitrip
) undr11
(mitRFLAGS
), sodass der Kernel nicht sehen kann, was ursprünglichrcx
beimsyscall
Ausführen enthalten war.)Der Kernel-Systemaufruf ABI wurde ausgewählt, um mit dem Funktionsaufruf ABI übereinzustimmen, außer
r10
anstelle vonrcx
, sodass ein libc-Wrapper wiemmap(2)
can nurmov %rcx, %r10
/mov $0x9, %eax
/ funktioniertsyscall
.Beachten Sie, dass die von i386 Linux verwendete SysV-Aufrufkonvention im Vergleich zu Windows 32-Bit __vectorcall nicht funktioniert. Es übergibt alles auf dem Stapel und gibt nur
edx:eax
für int64 zurück, nicht für kleine Strukturen . Es ist keine Überraschung, dass wenig Anstrengungen unternommen wurden, um die Kompatibilität damit aufrechtzuerhalten. Wenn es keinen Grund gibt, dies nicht zu tun, haben sie Dinge wie die Beibehaltung desrbx
Anrufs getan , da sie entschieden haben, dass es gut ist, einen anderen in der ursprünglichen 8 zu haben (der kein REX-Präfix benötigt).Die Optimierung des ABI ist langfristig viel wichtiger als jede andere Überlegung. Ich denke, sie haben einen ziemlich guten Job gemacht. Ich bin mir nicht ganz sicher, ob ich in Register gepackte Strukturen anstelle verschiedener Felder in verschiedenen Regs zurückgeben soll. Ich denke, Code, der sie nach Wert weitergibt, ohne tatsächlich auf den Feldern zu arbeiten, gewinnt auf diese Weise, aber die zusätzliche Arbeit des Auspackens scheint albern. Sie hätten mehr als nur ganzzahlige Rückgaberegister haben können
rdx:rax
, so dass die Rückgabe einer Struktur mit 4 Mitgliedern sie in rdi, rsi, rdx, rax oder so zurückgeben könnte.Sie erwogen, Ganzzahlen in Vektorregs zu übergeben, da SSE2 mit Ganzzahlen arbeiten kann. Zum Glück haben sie das nicht getan. Ganzzahlen werden sehr oft als Zeiger-Offsets verwendet, und ein Roundtrip zum Stapelspeicher ist ziemlich billig . Außerdem benötigen SSE2-Anweisungen mehr Codebytes als Ganzzahlanweisungen.
Ich vermute, Windows ABI-Designer haben möglicherweise versucht, Unterschiede zwischen 32 und 64 Bit zu minimieren, um Menschen zu helfen, die asm von einem zum anderen portieren müssen oder
#ifdef
in einigen ASMs ein paar s verwenden können, damit dieselbe Quelle einfacher erstellt werden kann eine 32- oder 64-Bit-Version einer Funktion.Das Minimieren von Änderungen in der Toolchain scheint unwahrscheinlich. Ein x86-64-Compiler benötigt eine separate Tabelle, deren Register für was verwendet wird und wie die aufrufende Konvention lautet. Eine kleine Überlappung mit 32 Bit führt wahrscheinlich nicht zu erheblichen Einsparungen bei der Größe / Komplexität des Toolchain-Codes.
quelle
Denken Sie daran, dass Microsoft anfangs "offiziell unverbindlich gegenüber den frühen AMD64-Bemühungen" war (aus "Eine Geschichte des modernen 64-Bit-Computing" von Matthew Kerner und Neil Padgett), weil sie starke Partner von Intel in Bezug auf die IA64-Architektur waren. Ich denke, dies bedeutete, dass sie, selbst wenn sie sonst offen gewesen wären, mit GCC-Ingenieuren an einem ABI zu arbeiten, um sowohl unter Unix als auch unter Windows zu arbeiten, dies nicht getan hätten, da dies bedeuten würde, die AMD64-Bemühungen öffentlich zu unterstützen, wenn sie dies nicht getan hätten. Dies wurde noch nicht offiziell getan (und hätte Intel wahrscheinlich verärgert).
Darüber hinaus hatte Microsoft damals absolut keine Lust, mit Open-Source-Projekten befreundet zu sein. Mit Sicherheit nicht Linux oder GCC.
Warum hätten sie bei einem ABI zusammengearbeitet? Ich würde vermuten, dass die ABIs einfach deshalb unterschiedlich sind, weil sie mehr oder weniger zur gleichen Zeit und isoliert entworfen wurden.
Ein weiteres Zitat aus "Eine Geschichte des modernen 64-Bit-Computing":
Dies zeigt, dass selbst AMD nicht der Meinung war, dass die Zusammenarbeit zwischen MS und Unix unbedingt das Wichtigste ist, dass jedoch die Unterstützung von Unix / Linux sehr wichtig ist. Vielleicht war sogar der Versuch, eine oder beide Seiten zu Kompromissen oder Zusammenarbeit zu überreden, die Mühe oder das Risiko (?) Nicht wert, eine von beiden zu irritieren? Vielleicht dachte AMD, dass selbst das Vorschlagen eines gemeinsamen ABI das wichtigere Ziel, den Software-Support einfach bereit zu halten, wenn der Chip fertig ist, verzögern oder entgleisen könnte.
Spekulationen meinerseits, aber ich denke, der Hauptgrund, warum die ABIs unterschiedlich sind, war der politische Grund, warum MS und die Unix / Linux-Seiten einfach nicht zusammengearbeitet haben, und AMD sah das nicht als Problem an.
quelle
__vectorcall
weil das Weitergeben__m128
des Stapels scheiße war. Es ist auch seltsam, eine aufruferhaltene Semantik für die niedrigen 128b einiger Vektorregs zu haben (teilweise Intels Fehler, keinen erweiterbaren Speicher- / Wiederherstellungsmechanismus mit SSE ursprünglich und immer noch nicht mit AVX zu entwerfen.)alloca
oder ein paar andere Fälle). Dies ist normal, wenn Sie es gewohnt sind,gcc -fomit-frame-pointer
unter Linux die Standardeinstellung zu sein. Der ABI definiert Stack-Unwind-Metadaten, mit denen die Ausnahmebehandlung weiterhin funktioniert. (Ich nehme an, es funktioniert so etwas wie das CFI-Zeug von GNU / Linux x86-64 System V.eh_frame
).gcc -fomit-frame-pointer
ist seit jeher die Standardeinstellung (mit aktivierter Optimierung) auf x86-64, und andere Compiler (wie MSVC) tun dasselbe.Win32 hat seine eigenen Verwendungszwecke für ESI und EDI und erfordert, dass sie nicht geändert werden (oder zumindest wiederhergestellt werden, bevor die API aufgerufen wird). Ich würde mir vorstellen, dass 64-Bit-Code dasselbe mit RSI und RDI macht, was erklären würde, warum sie nicht zum Weitergeben von Funktionsargumenten verwendet werden.
Ich konnte Ihnen jedoch nicht sagen, warum RCX und RDX umgeschaltet werden.
quelle
__fastcall
Aufrufkonvention zu erweitern . Sie behaupten , Win32 / Win64 nicht kompatibel sind, aber dann genau hinsehen: Eine Funktion , die dauert zwei 32bit 32bit args und kehrt, Win64 und Win32__fastcall
tatsächlich ist 100% kompatibel (gleiche regs für das Bestehen zwei 32 - Bit - args, gleicher Rückgabewert). Sogar ein binärer (!) Code kann in beiden Betriebsarten funktionieren. Die UNIX-Seite hat völlig mit "alten Methoden" gebrochen. Aus guten Gründen, aber eine Pause ist eine Pause.