web-dev-qa-db-fra.com

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

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 s'agit-il uniquement d'un 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 des paramètres de registre sous Windows est rcx - rdx - r8 - r9 - rest on stack tandis que tout le monde utilise rdi - rsi - rdx - rcx - r8 - r9 - rest on stack.

P.S. Je sais en quoi 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 .

Modifier: pour le comment, voir par ex. le entrée wikipedia et les liens à partir de là.

93
JanKanis

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

Une des choses à garder à l'esprit à propos de x86 est que le nom du registre à l'encodage "numéro de reg" n'est pas évident; en termes d'encodage d'instructions (le 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.

Par conséquent, choisir A/C/D (regs 0..2) pour la valeur de retour et les deux premiers arguments (qui est la convention "classique" de 32 bits __fastcall) Est un choix logique. En ce qui concerne le passage à 64 bits, les paramètres "supérieurs" sont commandés, et Microsoft et UN * X/Linux ont choisi R8/R9 Comme premiers.

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'AMD64 UN * X ABI a choisi RDX avant RCX.

Choisir six les registres d'arguments sur x64 - UN * X spécifiques

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

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

Les regs "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 grande, donc vous ne voudriez pas en choisir une si vous avez des options. Parmi les registres classiques, en raison de la signification de implicite de RBP et RSP ceux-ci ne sont pas disponibles, et RBX a traditionnellement un spécial utiliser sur UN * X (table de décalage globale) avec laquelle les concepteurs AMD64 ABI ne voulaient apparemment 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 faire arg[0] Et arg[1] Présente certains avantages. Voir le commentaire de cHao.
?SI Et ?DI Sont des opérandes source/destination d'instructions 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 plus simple possible strcpy(), par exemple, ne comprend que les deux instructions CPU repz movsb; ret car les adresses source/cible 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 allocateurs de tas C++ remplissant zéro des objets sur la construction, ou les pages de tas remplissant zéro du noyau sur sbrk(), ou copier-sur-écrire pagefaults) une énorme quantité de bloc copier/remplir, donc il sera utile pour le code si fréquemment utilisé pour enregistrer les deux ou trois instructions CPU qui autrement chargeraient ces arguments d'adresse source/cible dans le " registres "corrects".

Donc, d'une certaine manière, UN * X et Win64 ne sont différents que dans la mesure où UN * X "ajoute" deux arguments supplémentaires, dans des registres RSI/RDI choisis délibérément, au choix naturel de quatre arguments dans RCX, RDX, R8 et R9.

Au-delà de ça ...

Il existe plus de différences entre les ABI UN * X et Windows x64 que le simple mappage d'arguments à des registres spécifiques. Pour l'aperçu 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 must alloue stackspace pour les arguments de fonction même si les arguments 0 ... 3 sont passés dans les registres. Sur UN * X d'autre part, une fonction feuille (c'est-à-dire une qui n'appelle pas d'autres fonctions) n'est même pas nécessaire d'allouer du stackspace du tout si elle n'a pas besoin de plus de 128 octets (oui, vous possédez et pouvez utiliser une certaine quantité de pile sans l'allouer ... enfin, sauf si vous êtes du code noyau, une source de bugs astucieux). Tous ces choix d'optimisation sont particuliers, la plupart de leur justification est expliquée dans les références ABI complètes vers lesquelles pointe la référence wikipedia de l'affiche originale.

75
FrankH.

IDK 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 SysV a été décidée, alors j'ai creusé dans l'archive de la liste de diffusion et j'ai trouvé des trucs sympas.

Il est intéressant de lire certains de ces anciens fils 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 parties difficiles: AMD a considéré renommer les 8 registres d'origine r0-r7, ou appeler les nouveaux trucs de registres comme UAX .

En outre, les commentaires des développeurs du noyau ont identifié des choses qui ont rendu la conception originale de syscall et swapgs inutilisable . C'est ainsi qu'AMD a mis à jour l'instruction pour obtenir ce résultat avant de libérer les 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 à préserver par rapport à la sauvegarde de l'appelant, était faite initialement en novembre 2000, par Jan Hubicka (développeur gcc). Il compilé SPEC20 et regarda 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 de plus petites code 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, rdx comme trois premiers arguments a été motivé par:

  • économie de taille de code mineure dans les fonctions qui appellent memset ou une autre fonction de chaîne C sur leurs arguments (où gcc insère une opération de chaîne de répétition?)
  • rbx est préservé par les appels car avoir deux regs préservés par les 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 ayant des finalités spéciales n'est préservé des appels (voir point précédent), donc une fonction qui souhaite utiliser des instructions de chaîne de répétition ou un décalage de comptage de variables peut devoir déplacer les arguments de fonction ailleurs, mais n'a pas à enregistrer/restaurer la valeur de l'appelant.
  • Nous essayons d'éviter le RCX au début de la séquence, car il est 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 appels système et nous aimerions faire en sorte que la séquence d'appels système corresponde autant que possible à la séquence d'appel de fonction.

    (fond: syscall/sysret détruit inévitablement rcx (avec rip) et r11 (avec RFLAGS), afin que le noyau ne puisse pas voir ce qui était à l'origine dans rcx lorsque syscall s'est exécuté.)

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


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

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 retourner 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 en 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 entiers, plus que simplement rdx:rax, Donc retourner une structure avec 4 membres pourrait les renvoyer dans rdi, rsi, rdx, rax ou quelque chose.

Ils ont envisagé de passer des entiers dans des régulations vectorielles, car SSE2 peut fonctionner sur des entiers. Heureusement, ils ne l'ont pas fait. Les nombres entiers sont très souvent utilisés comme décalages de pointeur, et un aller-retour pour empiler la mémoire 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 Windows ABI pourraient avoir pour objectif de 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 #ifdef S dans certains ASM, de sorte que le la même source peut plus facilement créer 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 dont le registre est utilisé pour quoi et quelle est la convention d'appel. Il est peu probable qu'un faible chevauchement avec 32 bits produise des économies significatives sur la taille/complexité du code de la chaîne d'outils.

34
Peter Cordes

Rappelez-vous que Microsoft était initialement "officiellement sans engagement 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'IA64 architecture. Je pense que cela signifiait que même s'ils auraient autrement été ouverts à travailler avec des ingénieurs de GCC sur un ABI pour utiliser à la fois sur Unix et Windows, ils ne l'auraient pas fait car cela signifierait soutenir publiquement l'effort AMD64 alors qu'ils n'avaient pas '' t encore officiellement fait (et aurait probablement bouleversé 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? Je suppose que les ABI sont différentes simplement parce qu'elles ont été conçues à peu près en même temps et isolément.

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 de 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 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 d'investir dans l'effort Windows AMD64 et garantissait également que Linux, qui devenait un système d'exploitation important à l'époque, serait disponible une fois les puces libérées.

Weber va jusqu'à dire que le travail Linux a été 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 sociétés si nécessaire. Cette possibilité a permis à AMD d'avoir une stratégie de survie dans le pire des cas, même si d'autres partenaires se retiraient, ce qui à son tour a maintenu les autres partenaires engagés de peur d'être 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 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'une ou l'autre? AMD a peut-être pensé que même suggérer un ABI commun pourrait retarder ou faire dérailler l'objectif plus important d'avoir simplement un support logiciel prêt lorsque la puce était prête.

Des spéculations de ma part, mais je pense que la principale raison pour laquelle les ABI sont différents est la raison politique pour laquelle MS et les côtés Unix/Linux n'ont tout simplement pas travaillé ensemble, et AMD n'a pas vu cela comme un problème.

12
Michael Burr

Win32 a ses propres utilisations pour ESI et EDI, et exige qu'ils ne soient pas modifiés (ou au moins qu'ils soient restaurés avant d'appeler dans l'API). J'imagine que le code 64 bits le fait la même chose 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.

12
cHao