Pourquoi Windows64 utilise-t-il une convention d'appel différente de celle de tous les autres systèmes d'exploitation sur x86-64?

110

AMD a une spécification ABI qui décrit la convention d'appel à utiliser sur x86-64. Tous les systèmes d'exploitation le suivent, à l'exception de Windows qui a sa propre convention d'appel x86-64. Pourquoi?

Quelqu'un connaît-il les raisons techniques, historiques ou politiques de cette différence, ou est-ce purement une question de syndrome des NIH?

Je comprends que différents systèmes d'exploitation peuvent avoir des besoins différents pour des choses de niveau supérieur, mais cela n'explique pas pourquoi, par exemple, l'ordre de passage du paramètre de registre sous Windows est rcx - rdx - r8 - r9 - rest on stackalors que tout le monde l'utilise rdi - rsi - rdx - rcx - r8 - r9 - rest on stack.

PS Je suis conscient de la façon dont ces conventions d'appel diffèrent généralement et je sais où trouver des détails si j'en ai besoin. Ce que je veux savoir, c'est pourquoi .

Edit: pour le comment, voir par exemple l' entrée wikipedia et les liens à partir de là.

JanKanis
la source
3
Eh bien, juste pour le premier registre: rcx: ecx était le paramètre "this" pour la convention msvc __thiscall x86. Donc, probablement juste pour faciliter le portage de leur compilateur vers x64, ils ont commencé avec rcx comme premier. Le fait que tout le reste serait alors différent aussi n'était qu'une conséquence de cette décision initiale.
Chris Becke
@Chris: J'ai ajouté une référence au document de supplément AMD64 ABI (et quelques explications sur ce que c'est réellement) ci-dessous.
FrankH.
1
Je n'ai pas trouvé de justification de MS mais j'ai trouvé une discussion ici
phuclv

Réponses:

81

Choix de quatre registres d'arguments sur x64 - commun à UN * X / Win64

Une des choses à garder à l'esprit à propos de x86 est que le nom du registre pour l'encodage "reg number" n'est pas évident; en termes de codage d'instructions (l' octet MOD R / M , voir http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm ), les numéros de registre 0 ... 7 sont - dans cet ordre - ?AX, ?CX, ?DX, ?BX, ?SP, ?BP, ?SI, ?DI.

Par conséquent, choisir A / C / D (regs 0..2) pour la valeur de retour et les deux premiers arguments (qui est la __fastcallconvention 32 bits "classique" ) est un choix logique. En ce qui concerne le 64bit, les regs "supérieurs" sont commandés, et Microsoft et UN * X / Linux ont opté pour R8/ R9comme premiers.

En gardant cela à l' esprit, le choix de Microsoft RAX(valeur de retour) et RCX, RDX, R8, R9(arg [0..3]) sont une sélection compréhensible si vous choisissez quatre registres pour les arguments.

Je ne sais pas pourquoi l'AMI AMD64 UN * X a choisi RDXavant RCX.

Choix de six registres d'arguments sur x64 - spécifique à UN * X

UN * X, sur les architectures RISC, a traditionnellement fait passer des arguments dans les registres - en particulier, pour les six premiers arguments (c'est le cas sur PPC, SPARC, MIPS au moins). Ce qui pourrait être l'une des principales raisons pour lesquelles les concepteurs ABI AMD64 (UN * X) ont également choisi d'utiliser six registres sur cette architecture.

Donc , si vous voulez six registres pour passer des arguments dans, et il est logique de choisir RCX, RDX, R8etR9 pour quatre d'entre eux, qui deux autres si vous choisissez?

Les regs "supérieurs" nécessitent un octet de préfixe d'instruction supplémentaire pour les sélectionner et ont donc une plus grande empreinte de taille d'instruction, donc vous ne voudriez pas en choisir un si vous avez des options. Parmi les registres classiques, en raison de la signification implicite de RBPet RSPceux-ci ne sont pas disponibles, et ont RBXtraditionnellement une utilisation spéciale sur UN * X (table de décalage globale) avec laquelle apparemment les concepteurs AMD64 ABI ne voulaient pas devenir inutilement incompatibles.
Ergo, le seul choix était RSI/ RDI.

Donc, si vous devez prendre RSI/RDI comme registres d'arguments, quels arguments devraient-ils être?

Les fabriquer arg[0]et arg[1]présente certains avantages. Voir le commentaire de cHao.
?SIet ?DIsont des opérandes source / destination d'instruction de chaîne, et comme cHao l'a mentionné, leur utilisation comme registres d'arguments signifie qu'avec les conventions d'appel AMD64 UN * X, la strcpy()fonction la plus simple possible , par exemple, ne comprend que les deux instructions CPU repz movsb; retcar la source / cible les adresses ont été placées dans les registres corrects par l'appelant. Il y a, en particulier dans le code "glue" de bas niveau et généré par le compilateur (pensez, par exemple, à certains objets d'allocations de tas C ++ à remplissage nul lors de la construction, ou aux pages de tas à remplissage zéro du noyau sursbrk() , ou copie -write pagefaults) une énorme quantité de copie / remplissage de bloc, donc il sera utile pour le code si fréquemment utilisé pour sauvegarder les deux ou trois instructions CPU qui autrement chargeraient ces arguments d'adresse source / cible dans les registres "corrects".

D'une certaine manière, l' ONU * X et Win64 ne sont différentes que dans l' ONU * X « précèder » deux arguments supplémentaires, choisis à dessein RSI/ RDIregistres, le choix naturel de quatre arguments RCX, RDX, R8et R9.

Au-delà de ça ...

Il y a plus de différences entre les ABI UN * X et Windows x64 que le mappage d'arguments à des registres spécifiques. Pour un aperçu sur Win64, vérifiez:

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

Win64 et AMD64 UN * X diffèrent également de manière frappante dans la façon dont l'espace de pile est utilisé; sur Win64, par exemple, l'appelant doit allouer un espace de pile pour les arguments de fonction même si les arguments 0 ... 3 sont passés dans les registres. Sur UN * X en revanche, une fonction feuille (c'est-à-dire une fonction qui n'appelle pas d'autres fonctions) n'est même pas obligée d'allouer un espace de pile si elle n'en a pas besoin de plus de 128 octets (oui, vous possédez et pouvez une certaine quantité de pile sans l'allouer ... enfin, sauf si vous êtes un code noyau, une source de bugs astucieux). Ce sont tous des choix d'optimisation particuliers, dont la plupart des raisons sont expliquées dans les références ABI complètes auxquelles renvoie la référence wikipedia de l'affiche originale.

FrankH.
la source
1
À propos des noms de registre: cet octet de préfixe peut être un facteur. Mais alors, il serait plus logique pour MS de choisir rcx - rdx - rdi - rsi comme registres d 'arguments. Mais la valeur numérique des huit premiers pourrait vous guider si vous concevez une ABI à partir de zéro, mais il n'y a aucune raison de les changer si une ABI parfaitement fine existe déjà, cela ne fait que créer plus de confusion.
JanKanis
2
Sur RSI / RDI: Ces instructions seront généralement intégrées, auquel cas la convention d'appel n'a pas d'importance. Sinon, il n'y a qu'une seule copie (ou peut-être quelques-uns) de cette fonction dans tout le système, donc cela ne sauvegarde qu'une poignée d'octets au total . Ça ne vaut pas le coup. Sur les autres différences / pile d'appels: L'utilité des choix spécifiques est expliquée dans les références ABI, mais elles ne font pas de comparaison. Ils ne disent pas pourquoi d'autres optimisations n'ont pas été choisies - par exemple, pourquoi Windows n'a-t-il pas la zone rouge de 128 octets, et pourquoi l'ABI AMD n'a-t-il pas les emplacements de pile supplémentaires pour les arguments?
JanKanis
1
@cHao: non. Mais ils l'ont quand même changé. L'ABI Win64 est différent de celui de Win32 (et non compatible), et également différent de l'ABI d'AMD.
JanKanis
8
@Somejan: Win64 et Win32 __fastcallsont identiques à 100% dans le cas de ne pas avoir plus de deux arguments ne dépassant pas 32 bits et de renvoyer une valeur ne dépassant pas 32 bits. Ce n'est pas une petite classe de fonctions. Aucune rétrocompatibilité n'est possible du tout entre les ABI UN * X pour i386 / amd64.
FrankH.
2
@szx: Je viens de trouver le fil de discussion pertinent de la liste de diffusion de novembre 2000 et j'ai publié une réponse résumant le raisonnement. Notez que c'est memcpyqui pourrait être implémenté de cette façon, passtrcpy .
Peter Cordes
42

IDK pourquoi Windows a fait ce qu'il a fait. Voir la fin de cette réponse pour une estimation. J'étais curieux de savoir comment la convention d'appel SysV avait été décidée, alors j'ai fouillé dans l'archive de la liste de diffusion et j'ai trouvé des choses intéressantes.

Il est intéressant de lire certains de ces vieux threads sur la liste de diffusion AMD64, car les architectes AMD y étaient actifs. Par exemple, le choix des noms de registre était l'une des choses difficiles: AMD a envisagé de renommer les 8 registres d'origine r0-r7, ou d'appeler les nouveaux registres commeUAX .

En outre, les commentaires des développeurs du noyau ont identifié des éléments qui ont fait la conception originale de syscall et swapgsinutilisable . C'est ainsi qu'AMD a mis à jour les instructions pour résoudre ce problème avant de publier des puces réelles. Il est également intéressant de noter qu'à la fin de 2000, l'hypothèse était qu'Intel n'adopterait probablement pas AMD64.


La convention d'appel SysV (Linux), et la décision sur le nombre de registres à conserver par rapport à l'appelant-sauvegarder, était prises initialement en novembre 2000, par Jan Hubicka (un développeur gcc). Il a compilé SPEC2000 et a examiné la taille du code et le nombre d'instructions. Ce fil de discussion rebondit autour de certaines des mêmes idées que les réponses et les commentaires sur cette question SO. Dans un deuxième fil, il a proposé la séquence actuelle comme optimale et, espérons-le, finale, générant un code plus petit que certaines alternatives .

Il utilise le terme «global» pour désigner les registres préservés des appels, qui doivent être poussés / sautés s'ils sont utilisés.

Le choix de rdi , rsi, rdxcomme les trois premiers args a été motivée par:

  • sauvegarde mineure de la taille du code dans les fonctions qui appellent memsetou une autre fonction de chaîne C sur leurs arguments (où gcc en ligne une opération de chaîne de répétition?)
  • rbxest préservé des appels car avoir deux regs préservés des appels accessibles sans préfixes REX (rbx et rbp) est une victoire. Vraisemblablement choisi parce que c'est le seul autre reg qui n'est implicitement utilisé par aucune instruction. (La chaîne de répétition, le nombre de décalages et les sorties / entrées mul / div touchent tout le reste).
  • Aucun des registres à des fins spéciales n'est préservé des appels (voir le point précédent), donc une fonction qui souhaite utiliser des instructions de chaîne de répétition ou un décalage de nombre de variables peut avoir à déplacer les arguments de fonction ailleurs, mais n'a pas à enregistrer / restaurer la valeur de l'appelant.
  • Nous essayons d'éviter RCX au début de la séquence, car il s'agit d'un registre couramment utilisé à des fins spéciales, comme EAX, il a donc le même but de manquer dans la séquence. De plus, il ne peut pas être utilisé pour les appels système et nous aimerions que la séquence d'appel système corresponde le plus possible à la séquence d'appel de fonction.

    (background: syscall/ sysretdétruit inévitablement rcx(avec rip) et r11(avec RFLAGS), de sorte que le noyau ne peut pas voir ce qui était à l'origine rcxlors de l' syscallexécution.)

L'ABI de l'appel système du noyau a été choisi pour correspondre à l'appel de fonction ABI, sauf pour r10au lieu de rcx, donc un wrapper de libc fonctionne comme mmap(2)can just mov %rcx, %r10/ mov $0x9, %eax/ syscall.


Notez que la convention d'appel SysV utilisée par i386 Linux est nul par rapport à __vectorcall 32 bits de Windows. Il passe tout sur la pile et ne retourne que edx:eaxpour int64, pas pour les petites structures . Il n'est pas surprenant que peu d'efforts aient été faits pour maintenir la compatibilité avec lui. Quand il n'y a aucune raison de ne pas le faire, ils ont fait des choses comme garderrbx appels préservés, car ils ont décidé qu'il était bon d'en avoir un autre dans le 8 d'origine (qui n'a pas besoin d'un préfixe REX).

Rendre l'ABI optimal est beaucoup plus important à long terme que toute autre considération. Je pense qu'ils ont fait du très bon travail. Je ne suis pas totalement sûr de renvoyer des structures emballées dans des registres, au lieu de différents champs dans différents regs. Je suppose que le code qui les transmet par valeur sans réellement agir sur les champs gagne de cette façon, mais le travail supplémentaire de déballage semble idiot. Ils auraient pu avoir plus de registres de retour entiers, plus que justerdx:rax , donc retourner une structure avec 4 membres pourrait les renvoyer dans rdi, rsi, rdx, rax ou quelque chose comme ça.

Ils ont envisagé de passer des entiers dans les regs vectoriels, car SSE2 peut fonctionner sur des entiers. Heureusement, ils ne l'ont pas fait. Les entiers sont très souvent utilisés comme décalages de pointeur, et un aller-retour vers la mémoire de la pile est assez bon marché . Les instructions SSE2 prennent également plus d'octets de code que les instructions entières.


Je soupçonne que les concepteurs de Windows ABI ont peut-être cherché à minimiser les différences entre 32 et 64 bits au profit des personnes qui doivent porter asm de l'un à l'autre, ou qui peuvent utiliser quelques #ifdefs dans certains ASM afin que la même source puisse plus facilement construire une version 32 ou 64 bits d'une fonction.

Minimiser les changements dans la chaîne d'outils semble peu probable. Un compilateur x86-64 a besoin d'une table distincte indiquant quel registre est utilisé pour quoi et quelle est la convention d'appel. Avoir un petit chevauchement avec 32 bits est peu susceptible de générer des économies significatives en taille / complexité du code de la chaîne d'outils.

Peter Cordes
la source
1
Je pense avoir lu quelque part sur le blog de Raymond Chen sur la justification du choix de ces registres après l'analyse comparative du côté de la SP, mais je ne peux plus le trouver. Cependant, certaines raisons concernant le homezone ont été expliquées ici blogs.msdn.microsoft.com/oldnewthing/20160623-00/?p=93735 blogs.msdn.microsoft.com/freik/2006/03/06/…
phuclv
@phuclv: Voir aussi Est-il valide d'écrire sous ESP? . Les commentaires de Raymond sur ma réponse ont souligné certains détails SEH que je ne savais pas et qui expliquent pourquoi Windows x86 32/64 n'a actuellement pas de zone rouge de facto. Son article de blog a quelques cas plausibles pour la même possibilité de gestionnaire de page de code que j'ai mentionnée dans cette réponse :) Alors oui, Raymond a mieux expliqué que moi (sans surprise parce que j'ai commencé par en savoir très peu sur Windows), et le tableau des tailles de zone rouge pour les non-x86 est vraiment soigné.
Peter Cordes
13

Rappelez-vous que Microsoft était initialement "officiellement non engagé envers les premiers efforts AMD64" (de "A History of Modern 64-bit Computing" par Matthew Kerner et Neil Padgett) parce qu'ils étaient de solides partenaires avec Intel sur l'architecture IA64. Je pense que cela signifiait que même s'ils auraient autrement été ouverts à travailler avec des ingénieurs de GCC sur un ABI à utiliser à la fois sous Unix et Windows, ils ne l'auraient pas fait car cela signifierait soutenir publiquement l'effort AMD64 alors qu'ils ne l'avaient pas fait ' t encore officiellement fait (et aurait probablement contrarié Intel).

En plus de cela, à l'époque, Microsoft n'avait absolument aucune tendance à être amical avec les projets open source. Certainement pas Linux ou GCC.

Alors pourquoi auraient-ils coopéré sur un ABI? J'imagine que les ABI sont différents simplement parce qu'ils ont été conçus plus ou moins en même temps et de manière isolée.

Une autre citation de "A History of Modern 64-bit Computing":

Parallèlement à la collaboration Microsoft, AMD a également engagé la communauté open source pour se préparer à la puce. AMD a passé un contrat avec Code Sorcery et SuSE pour le travail sur la chaîne d'outils (Red Hat était déjà engagé par Intel sur le port de la chaîne d'outils IA64). Russell a expliqué que SuSE a produit des compilateurs C et FORTRAN et que Code Sorcery a produit un compilateur Pascal. Weber a expliqué que la société s'était également engagée avec la communauté Linux pour préparer un port Linux. Cet effort était très important: il incitait Microsoft à continuer à investir dans l'effort AMD64 pour Windows, et garantissait également que Linux, qui devenait un système d'exploitation important à l'époque, serait disponible une fois que les puces seraient sorties.

Weber va jusqu'à dire que le travail sous Linux était absolument crucial pour le succès d'AMD64, car il a permis à AMD de produire un système de bout en bout sans l'aide d'autres entreprises si nécessaire. Cette possibilité garantissait à AMD une stratégie de survie dans le pire des cas, même si les autres partenaires se retiraient, ce qui à son tour maintenait les autres partenaires engagés de peur d'être laissés pour compte.

Cela indique que même AMD n'a pas estimé que la coopération était nécessairement la chose la plus importante entre MS et Unix, mais que le support Unix / Linux était très important. Peut-être que même essayer de convaincre une ou les deux parties de faire des compromis ou de coopérer ne valait pas l'effort ou le risque (?) D'irriter l'un ou l'autre? Peut-être AMD pensait-il que même suggérer une ABI commune pourrait retarder ou faire dérailler l'objectif plus important consistant simplement à avoir un support logiciel prêt lorsque la puce est prête.

Spéculation de ma part, mais je pense que la raison principale pour laquelle les ABI sont différents était la raison politique pour laquelle MS et les côtés Unix / Linux ne travaillaient tout simplement pas ensemble, et AMD ne voyait pas cela comme un problème.

Michael Burr
la source
Belle perspective sur la politique. Je reconnais que ce n'est pas la faute ou la responsabilité d'AMD. Je blâme Microsoft d'avoir choisi une pire convention d'appel. Si leur convention d'appel s'était avérée meilleure, j'aurais de la sympathie, mais ils ont dû passer de leur ABI initial à __vectorcallparce que passer __m128sur la pile était nul. Avoir une sémantique préservée des appels pour le bas 128b de certains des regs vectoriels est également étrange (en partie la faute d'Intel pour ne pas avoir conçu un mécanisme de sauvegarde / restauration extensible avec SSE à l'origine, et toujours pas avec AVX.)
Peter Cordes
1
Je n'ai pas vraiment d'expertise ou de connaissance de la qualité des ABI. J'ai juste parfois besoin de savoir ce qu'ils sont afin de pouvoir comprendre / déboguer au niveau de l'assemblage.
Michael Burr le
1
Un bon ABI minimise la taille du code et le nombre d'instructions, et maintient les chaînes de dépendances à faible latence en évitant des allers-retours supplémentaires dans la mémoire. (pour les args, ou pour les locaux qui doivent être renversés / rechargés). Il y a des compromis. La zone rouge de SysV prend quelques instructions supplémentaires en un seul endroit (le répartiteur du gestionnaire de signaux du noyau), pour un avantage relativement important pour les fonctions feuilles de ne pas avoir à ajuster le pointeur de pile pour obtenir un espace de travail. C'est donc une victoire claire avec un inconvénient presque nul. Il a été adopté sans pratiquement aucune discussion après avoir été proposé pour SysV.
Peter Cordes
1
@dgnuff: Oui, c'est la réponse à Pourquoi le code du noyau ne peut-il pas utiliser une zone rouge . Les interruptions utilisent la pile du noyau, pas la pile de l'espace utilisateur, même si elles arrivent lorsque le processeur exécute du code d'espace utilisateur. Le noyau ne fait pas confiance aux piles de l'espace utilisateur car un autre thread du même processus d'espace utilisateur pourrait le modifier, prenant ainsi le contrôle du noyau!
Peter Cordes
1
@ DavidA.Gray: oui, l'ABI ne dit pas que vous devez utiliser RBP comme pointeur de cadre, donc le code optimisé ne le fait généralement pas (sauf dans les fonctions qui utilisent allocaou dans quelques autres cas). C'est normal si vous êtes habitué à gcc -fomit-frame-pointerêtre la valeur par défaut sous Linux. L'ABI définit les métadonnées de déroulement de la pile qui permettent à la gestion des exceptions de continuer à fonctionner. (Je suppose que cela fonctionne quelque chose comme le truc CFI de GNU / Linux x86-64 System V .eh_frame). gcc -fomit-frame-pointerest la valeur par défaut (avec l'optimisation activée) depuis toujours sur x86-64, et d'autres compilateurs (comme MSVC) font la même chose.
Peter Cordes le
12

Win32 a ses propres utilisations pour ESI et EDI, et exige qu'ils ne soient pas modifiés (ou du moins qu'ils soient restaurés avant d'appeler dans l'API). J'imagine que le code 64 bits fait de même avec RSI et RDI, ce qui expliquerait pourquoi ils ne sont pas utilisés pour transmettre des arguments de fonction.

Je ne pourrais pas vous dire pourquoi RCX et RDX sont commutés, cependant.

cHao
la source
1
Toutes les conventions d'appel ont certains registres désignés comme scratch et certains comme préservés comme ESI / EDI et RSI / RDI sur Win64. Mais ce sont des registres à usage général, Microsoft aurait pu choisir sans problème de les utiliser différemment.
JanKanis
1
@Somejan: Bien sûr, s'ils voulaient réécrire toute l'API et avoir deux systèmes d'exploitation différents. Je n'appellerais pas ça "sans problème", cependant. Depuis des dizaines d'années maintenant, MS a fait certaines promesses sur ce qu'il fera et ne fera pas avec les registres x86, et ils ont été plus ou moins cohérents et compatibles pendant tout ce temps. Ils ne vont pas jeter tout cela par la fenêtre juste à cause d'un édit d'AMD, en particulier un édit si arbitraire et en dehors du domaine de la "construction d'un processeur".
cHao du
5
@Somejan: L'AMD64 UN * X ABI a toujours été exactement cela - une pièce spécifique à UNIX . Le document, x86-64.org/documentation/abi.pdf , est intitulé System V Application Binary Interface, AMD64 Architecture Processor Supplement pour une raison. Les ABI UNIX (communs) (une collection multi-volumes, sco.com/developers/devspecs ) laissent une section pour le chapitre 3 spécifique au processeur - le Supplément - qui sont les conventions d'appel de fonction et les règles de disposition des données pour un processeur spécifique.
FrankH.
7
@Somejan: Microsoft Windows n'a jamais tenté d'être particulièrement proche de UN * X, et lorsqu'il s'agissait de porter Windows sur x64 / AMD64, ils ont simplement choisi d'étendre leur propre __fastcall convention d'appel. Vous prétendez que Win32 / Win64 ne sont pas compatibles, mais regardez de plus près: pour une fonction qui prend deux arguments 32 bits et retourne 32 bits, Win64 et Win32 sont en__fastcall fait 100% compatibles (mêmes regs pour passer deux arguments 32 bits, même valeur de retour). Même certains codes binaires (!) Peuvent fonctionner dans les deux modes de fonctionnement. Le côté UNIX a complètement rompu avec les «anciennes méthodes». Pour de bonnes raisons, mais une pause est une pause.
FrankH.
2
@Olof: C'est plus qu'un simple compilateur. J'ai eu des problèmes avec ESI et EDI quand j'ai fait des choses autonomes dans NASM. Windows se soucie définitivement de ces registres. Mais oui, vous pouvez les utiliser si vous les enregistrez avant de le faire et les restaurez avant que Windows n'en ait besoin.
cHao