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 stack
alors 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à.
la source
Réponses:
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
__fastcall
convention 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é pourR8
/R9
comme premiers.En gardant cela à l' esprit, le choix de Microsoft
RAX
(valeur de retour) etRCX
,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
RDX
avantRCX
.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
,R8
etR9
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
RBP
etRSP
ceux-ci ne sont pas disponibles, et ontRBX
traditionnellement 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]
etarg[1]
présente certains avantages. Voir le commentaire de cHao.?SI
et?DI
sont 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, lastrcpy()
fonction la plus simple possible , par exemple, ne comprend que les deux instructions CPUrepz movsb; ret
car 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
/RDI
registres, le choix naturel de quatre argumentsRCX
,RDX
,R8
etR9
.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.
la source
__fastcall
sont 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.memcpy
qui pourrait être implémenté de cette façon, passtrcpy
.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 comme
UAX
.En outre, les commentaires des développeurs du noyau ont identifié des éléments qui ont fait la conception originale de
syscall
etswapgs
inutilisable . 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
,rdx
comme les trois premiers args a été motivée par:memset
ou une autre fonction de chaîne C sur leurs arguments (où gcc en ligne une opération de chaîne de répétition?)rbx
est 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).(background:
syscall
/sysret
détruit inévitablementrcx
(avecrip
) etr11
(avecRFLAGS
), de sorte que le noyau ne peut pas voir ce qui était à l'originercx
lors de l'syscall
exécution.)L'ABI de l'appel système du noyau a été choisi pour correspondre à l'appel de fonction ABI, sauf pour
r10
au lieu dercx
, donc un wrapper de libc fonctionne commemmap(2)
can justmov %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:eax
pour 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 juste
rdx: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
#ifdef
s 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.
la source
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":
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.
la source
__vectorcall
parce que passer__m128
sur 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.)alloca
ou 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-pointer
est 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.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.
la source
__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.