137 votes

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

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

Quelqu'un connaît-il les raisons techniques, historiques ou politiques de cette différence, ou s'agit-il d'une simple question de syndrome NIH ?

Je comprends que des systèmes d'exploitation différents peuvent avoir des besoins différents pour les choses de haut niveau, mais cela n'explique pas pourquoi, par exemple, l'ordre de passage des paramètres de registre sous Windows est le suivant rcx - rdx - r8 - r9 - rest on stack alors que tous les autres utilisent rdi - rsi - rdx - rcx - r8 - r9 - rest on stack .

P.S. Je suis au courant de comment 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 le entrée de wikipédia et des liens à partir de là.

4 votes

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 soit différent n'était qu'une conséquence de cette décision initiale.

1 votes

@Chris : J'ai ajouté une référence au document complémentaire de l'ABI AMD64 (et quelques explications sur ce qu'il est réellement) ci-dessous.

1 votes

Je n'ai pas trouvé de justification de la part de MS mais j'ai trouvé quelques discussions aquí

103voto

FrankH. Points 8850

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

L'une des choses à garder à l'esprit à propos du x86 est que l'encodage du nom de registre en "numéro de registre" n'est pas évident ; en termes d'encodage d'instruction (l'instruction MOD R/M octet, 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 .

D'où le choix de A/C/D (regs 0..2) pour la valeur de retour et les deux premiers arguments (qui sont les 32 bits "classiques"). __fastcall convention) est un choix logique. En ce qui concerne le passage à 64 bits, les regs "supérieurs" sont commandés, et tant Microsoft que UN*X/Linux ont opté pour R8 / R9 que les premiers.

En gardant cela à l'esprit, le choix de Microsoft de 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'ABI de l'AMD64 UN*X a choisi RDX avant RCX .

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

UN*X, sur les architectures RISC, a traditionnellement effectué le passage d'arguments dans les registres - spécifiquement, pour le premier six (c'est le cas sur PPC, SPARC, MIPS au moins). C'est peut-être l'une des principales raisons pour lesquelles les concepteurs de l'ABI de l'AMD64 (UN*X) ont choisi d'utiliser six registres sur cette architecture également.

Donc si vous voulez six pour passer des arguments, et il est logique de choisir les registres suivants RCX , RDX , R8 y R9 pour quatre d'entre eux, quels sont les deux autres à choisir ?

Les registres "supérieurs" nécessitent un octet de préfixe d'instruction supplémentaire pour les sélectionner et ont donc une empreinte de taille d'instruction plus importante, vous ne voudrez donc pas choisir l'un d'entre eux si vous avez le choix. Parmi les registres classiques, en raison de la implicite signification de RBP y RSP ceux-ci ne sont pas disponibles, et RBX a traditionnellement une utilisation spéciale sur UN*X (global offset table) avec laquelle les concepteurs de l'ABI AMD64 ne voulaient apparemment pas devenir inutilement incompatible.
Ergo, le seul choix étaient RSI / RDI .

Donc si vous devez prendre RSI / RDI comme registres d'arguments, de quels arguments s'agit-il ?

Les rendre arg[0] y arg[1] présente certains avantages. Voir le commentaire de cHao.
?SI y ?DI sont des opérandes source/destination d'instructions de chaîne de caractères, et comme cHao l'a mentionné, leur utilisation en tant que registres d'arguments signifie qu'avec les conventions d'appel UN*X de l'AMD64, la plus simple des conventions d'appel est la suivante strcpy() par exemple, se compose uniquement des deux instructions du CPU repz movsb; ret car les adresses source/cible ont été placées dans les registres corrects par l'appelant. Il existe, en particulier dans le code de bas niveau et le code "glue" généré par le compilateur (pensez, par exemple, à certains allocateurs de tas C++ qui remplissent à zéro les objets lors de la construction, ou au noyau qui remplit à zéro les pages de tas lors de la construction), un code de bas niveau. sbrk() Il sera donc utile pour le code si fréquemment utilisé d'économiser les deux ou trois instructions du CPU qui, autrement, chargeraient de tels arguments d'adresse source/cible dans les registres "corrects".

Ainsi, d'une certaine manière, UN*X et Win64 ne sont différents que par le fait que UN*X "ajoute" deux arguments supplémentaires, dans des formats choisis à dessein. RSI / RDI aux registres, au choix naturel de quatre arguments en RCX , RDX , R8 y R9 .

Au-delà de ça...

Les différences entre les ABI UN*X et Windows x64 ne se limitent pas à l'affectation des arguments à des registres spécifiques. Pour une vue d'ensemble sur Win64, consultez :

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 de l'espace de stockage pour les arguments de la fonction même si les args 0...3 sont passés dans des registres. D'autre part, sur UN*X, une fonction feuille (c'est-à-dire une fonction qui n'appelle pas d'autres fonctions) n'est même pas obligée d'allouer de l'espace de pile si elle n'en a pas besoin de plus de 128 octets (oui, vous possédez et pouvez utiliser une certaine quantité de pile sans l'allouer... enfin, à moins que vous ne soyez du code noyau, source de bogues astucieux). Il s'agit là de 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 du posteur original.

2 votes

À propos des noms de registre : L'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 un ABI à partir de zéro, mais il n'y a aucune raison de les changer si un ABI parfait existe déjà, cela ne fait que créer plus de confusion.

3 votes

Sur RSI/RDI : Ces instructions seront généralement inlined, dans ce cas la convention d'appel n'a pas d'importance. Sinon, il n'y a qu'une seule copie (ou peut-être quelques-unes) de cette fonction dans tout le système, ce qui ne permet d'économiser qu'une poignée d'octets. au total . Cela n'en vaut pas la peine. Sur d'autres différences / pile d'appel : L'utilité de choix spécifiques est expliquée dans les références ABI, mais elles ne font pas de comparaison. Elles ne disent pas pourquoi d'autres optimisations n'ont pas été choisies - par exemple, pourquoi Windows n'a pas la zone rouge de 128 octets, et pourquoi l'ABI d'AMD n'a pas les emplacements de pile supplémentaires pour les arguments ?

0 votes

@Somejan : Une ABI parfaite existait bien avant x86-64. Doit-elle être changée maintenant au gré des caprices d'AMD ? Je ne pense pas.

62voto

Peter Cordes Points 1375

Je ne sais pas pourquoi Windows a fait ce qu'il a fait. Voir la fin de cette réponse pour une supposition. J'étais curieux de savoir comment la convention d'appel de SysV a été décidée, alors j'ai creusé dans l'archive des listes de diffusion et j'ai trouvé des trucs sympas.

Il est intéressant de lire certains de ces anciens fils de discussion sur la liste de diffusion AMD64, puisque les architectes AMD y étaient actifs. Par exemple, le choix des noms de registre était l'une des parties les plus difficiles : AMD a considéré en renommant les 8 registres originaux r0-r7, ou en appelant les nouveaux registres UAX etc.

De plus, le retour d'information des développeurs du noyau a permis d'identifier des éléments qui rendaient la conception originale de l'architecture de l syscall y swapgs inutilisable . C'est ainsi qu'AMD a mis à jour l'instruction pour régler ça avant de sortir les puces. Il est également intéressant de noter qu'à la fin de l'année 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 à préserver par rapport à la sauvegarde par l'appelant, a été réalisé initialement en nov. 2000, par Jan Hubicka (un développeur de gcc). Il compilé SPEC2000 et a examiné la taille du code et le nombre d'instructions. Ce fil de discussion fait rebondir certaines des mêmes idées que les réponses et les commentaires sur cette question de l'OS. Dans un 2e fil de discussion, 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 par les appels, qui doivent être poussés/jetés s'ils sont utilisés.

Le choix de rdi , rsi , rdx que les trois premiers args ont été motivés par :

  • un gain de taille de code mineur dans les fonctions qui appellent memset ou toute autre fonction de chaîne de caractères en C sur leurs arguments (où gcc met en ligne une opération de chaîne de caractères rep ?)
  • rbx est réservé aux appels car le fait d'avoir deux regs réservés aux appels accessibles sans préfixe REX ( rbx y rbp ) est une victoire. Vraisemblablement choisi parce que ce sont les seuls registres "hérités" qui ne sont pas implicitement utilisés par une instruction commune. (les sorties/entrées rep string, shift count, et mul/div touchent tout le reste).
  • Aucun des registres que les instructions courantes vous obligent à utiliser sont conservés par l'appel (voir le point précédent), donc une fonction qui veut utiliser un décalage ou une division à comptage variable peut avoir à déplacer les args de la fonction ailleurs, mais n'a pas à sauvegarder/restaurer la valeur de l'appelant. cmpxchg16b y cpuid ont besoin de RBX, mais sont rarement utilisés donc pas un grand facteur. ( cmpxchg16b ne faisait pas partie de l'AMD64 original, mais RBX aurait quand même été le choix évident. cmpxchg8b existe mais a été rendu obsolète par qword cmpxchg )
  • Nous essayons d'éviter le RCX au début de la séquence, car il s'agit du registre utilisé couramment à des fins spéciales, comme EAX, il a donc le même but d'être manquant dans la séquence. De plus, il ne peut pas être utilisé pour les syscalls et nous aimerions que la séquence des syscalls corresponde autant que possible à la séquence des appels de fonctions. corresponde autant que possible à la séquence d'appel de fonction.

(arrière-plan : syscall / sysret détruire inévitablement rcx (avec rip ) et r11 (avec RFLAGS ), donc le noyau ne peut pas voir ce qui était à l'origine dans le fichier rcx cuando syscall couru.)

L'ABI d'appel système du noyau a été choisie pour correspondre à l'ABI d'appel de fonction, à l'exception de r10 au lieu de rcx Ainsi, un wrapper libc fonctionne comme suit mmap(2) peut juste mov %rcx, %r10 / mov $0x9, %eax / syscall .


Notez que la convention d'appel SysV utilisée par Linux i386 est nulle par rapport à l'appel __vectorcall 32bit de Windows. Elle passe tout sur la pile, et ne retourne que dans les cas suivants edx:eax pour int64, pas pour les petits structs)%7B1,+2%7D%3B%0A++return+x%3B%0A%7D%0A%0Along+long+ret64(int+a,+int+b)+%7B+return+(long+long)a*b%3B+%7D%27)),filterAsm:(commentOnly:!t,directives:!t,labels:!t),version:3) . Il n'est pas surprenant que peu d'efforts aient été faits pour maintenir la compatibilité avec ce système. Quand il n'y a pas de raison de ne pas le faire, ils ont fait des choses comme garder rbx appel-préservé, puisqu'ils ont décidé qu'il était bon d'en avoir un autre dans les 8 originaux (qui n'ont pas besoin d'un préfixe REX).

Rendre l'ABI optimal, c'est beaucoup plus importante à long terme que toute autre considération. Je pense qu'ils ont fait un assez bon travail. Je ne suis pas tout à fait sûr de l'intérêt de renvoyer des structs empaquetés dans des registres, plutôt que des champs différents dans des registres différents. Je suppose que le code qui les fait circuler par valeur sans réellement opérer 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 d'entiers, plus que seulement rdx:rax Ainsi, le retour d'une structure avec 4 membres pourrait les retourner en rdi, rsi, rdx, rax ou autre.

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


Je soupçonne les concepteurs de l'ABI de Windows d'avoir 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 un couple de logiciels de gestion de l'environnement. #ifdef 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 séparée indiquant quel registre est utilisé pour quoi, et quelle est la convention d'appel. Il est peu probable qu'un léger chevauchement avec le 32 bits produise des économies significatives en termes de taille et de complexité du code de la chaîne d'outils.

21voto

Michael Burr Points 181287

Rappelez-vous que Microsoft était initialement "officiellement non engagé à l'égard de l'effort précoce de l'AMD64" (de "Une histoire de l'informatique moderne à 64 bits" par Matthew Kerner et Neil Padgett) car ils étaient de solides partenaires d'Intel sur l'architecture IA64. Je pense que cela signifie que même s'ils auraient été ouverts à travailler avec les ingénieurs de GCC sur une ABI à utiliser à la fois sur Unix et Windows, ils ne l'auraient pas fait car cela aurait signifié soutenir publiquement l'effort AMD64 alors qu'ils ne l'avaient pas encore fait officiellement (et aurait probablement contrarié Intel).

En plus de cela, à l'époque, Microsoft n'avait absolument aucun penchant pour les projets à code source ouvert. Certainement pas Linux ou GCC.

Alors pourquoi auraient-ils coopéré sur un ABI ? Je pense que les ABI sont différentes simplement parce qu'elles ont été conçues 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 avec Microsoft, AMD a également engagé la communauté open source pour 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à déjà engagé par Intel sur le portage de la chaîne d'outils IA64). Russell a expliqué que SuSE produit des compilateurs C et FORTRAN, et Code Sorcery produit un compilateur Pascal. compilateur Pascal. Weber a expliqué que la société s'est également engagée avec la communauté Linux pour préparer un portage Linux. Cet effort était très important : il a incité Microsoft à continuer d'investir dans l'initiative à investir dans l'effort de Windows AMD64, et a également permis de s'assurer que Linux, qui devenait un système d'exploitation important à l'époque, serait disponible une fois que les puces seraient disponibles.

Weber va jusqu'à dire que le travail sur Linux était absolument crucial. au succès de l'AMD64, parce qu'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 qu'AMD avait une stratégie de survie dans le pire des cas, même si d'autres partenaires se retiraient, ce qui à son tour a gardé les autres partenaires partenaires par crainte d'être eux-mêmes laissés pour compte.

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

C'est une spéculation de ma part, mais je pense que la raison principale pour laquelle les ABI sont différentes était la raison politique que MS et les parties Unix/Linux n'ont tout simplement pas travaillé ensemble sur ce sujet, et AMD n'a pas vu cela comme un problème.

14voto

cHao Points 42294

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

Je ne pourrais pas vous dire pourquoi le RCX et le RDX sont intervertis, cependant.

2 votes

Toutes les conventions d'appel ont certains registres désignés comme scratch et d'autres comme préservés comme ESI/EDI et RSI/RDI sur Win64. Mais ce sont des registres d'usage général, Microsoft aurait pu choisir sans problème de les utiliser différemment.

2 votes

@Somejan : Bien sûr, s'ils voulaient réécrire l'API entière et avoir deux OS différents. Je n'appellerais pas cela "sans problème", cependant. Depuis des dizaines d'années maintenant, MS a fait certaines promesses sur ce qu'elle fera et ne fera pas avec les registres x86, et elles ont été plus ou moins cohérentes et compatibles pendant tout ce temps. Ils ne vont pas jeter tout cela par la fenêtre juste à cause d'un décret d'AMD, surtout s'il est si arbitraire et en dehors du domaine de la "construction d'un processeur".

2 votes

BTW, ?SI et ?DI sont semi registres à usage général. Comme la plupart des registres qui existent depuis toujours, ils ont des utilisations spécifiques intégrées ; certaines instructions (les instructions de chaîne : MOVS ?, INS ?, OUTS ?, etc.) sont codées en dur pour utiliser ces registres afin de déplacer des données. À moins que x86-64 ne fournisse un autre moyen de le faire, il faudrait beaucoup plus que la simple utilisation de registres différents.

-3voto

Olof Forshell Points 1754

Je suppose que lorsque le compilateur que vous utilisez souhaite appeler une API Windows, il utilise la convention d'appel Windows. La convention utilisée par le compilateur dans l'application est généralement paramétrable en tant qu'option du compilateur. Je suppose que l'utilisation du passage de paramètres basé sur les registres est une fonction d'amélioration des performances.

Je développe pour x86-32 en utilisant OpenWatcom avec l'option de passage des paramètres de registre. Il n'y a jamais de problèmes avec cette option (comme des appels à Windows mal formatés).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X