Comment définir des arguments de fonction dans l’assembly lors de l’exécution dans une application 64 bits sous Windows?

J’essaie de définir des arguments à l’aide du code d’assemblage utilisé dans une fonction générique. Les arguments de cette fonction générique – résidant dans une dll – ne sont pas connus au moment de la compilation. Pendant l’exécution, le pointeur sur cette fonction est déterminé à l’aide de la fonction GetProcAddress. Cependant, ses arguments ne sont pas connus. Pendant l’exécution, je peux déterminer les arguments – valeur et type – à l’aide d’un fichier de données (et non d’un fichier d’en-tête ou de tout autre élément pouvant être inclus ou compilé). J’ai trouvé un bon exemple de solution à ce problème pour 32 bits ( arguments C Pass comme liste void-pointer-list à une fonction imscope de LoadLibrary () ), mais pour 64 bits, cet exemple ne fonctionne pas car vous ne pouvez pas remplir la stack mais vous devez remplir les registres. J’ai donc essayé d’utiliser le code d’assemblage pour remplir les registres, mais jusqu’à présent, aucun succès. J’utilise le code C pour appeler le code d’assemblage. J’utilise VS2015 et MASM (64 bits). Le code C ci-dessous fonctionne bien, mais pas le code d’assemblage. Alors, quel est le problème avec le code d’assemblage? Merci d’avance.

Code C:

... void fill_register_xmm0(double); // proto of assembly function ... // code determining the pointer to a func returned by the GetProcAddress() ... double dVal = 12.0; int v; fill_register_xmm0(dVal); v = func->func_i(); // integer function that will use the dVal ... 

code d’assembly dans un fichier .asm différent (syntaxe MASM):

 TITLE fill_register_xmm0 .code option prolog:none ; turn off default prolog creation option epilogue:none ; turn off default epilogue creation fill_register_xmm0 PROC variable: REAL8 ; REAL8=equivalent to double or float64 movsd xmm0, variable ; fill value of variable into xmm0 ret fill_register_xmm0 ENDP option prolog:PrologueDef ; turn on default prolog creation option epilogue:EpilogueDef ; turn on default epilogue creation END 

    Vous devez donc appeler une fonction (dans une DLL), mais vous ne pouvez déterminer le nombre et le type de parameters qu’au moment de l’exécution. Ensuite, vous devez supprimer les parameters, sur la stack ou dans des registres, en fonction de la convention d’appel / interface binary d’application.

    J’utiliserais l’approche suivante: certains composants de votre programme déterminent le nombre et le type de parameters. Supposons qu’il crée une liste de {type, value}, {type, value}, ...

    Vous transmettez ensuite cette liste à une fonction pour préparer l’appel ABI. Ce sera une fonction assembleur. Pour un ABI basé sur une stack (32 bits), il suffit de placer les parameters sur la stack. Pour une ABI basée sur les registres, il peut préparer les valeurs des registres et les enregistrer en tant que variables locales ( add sp,nnn ). Une fois tous les parameters préparés (éventuellement en utilisant les registres nécessaires à l’appel, donc en les sauvegardant d’abord), les registres ( une série d’instructions mov ) et exécute l’instruction d’ call .

    La convention d’appel Windows x86-64 est assez simple et permet d’écrire une fonction wrapper qui ne connaît pas les types de fonctions. Il suffit de charger les 32 premiers octets d’arguments dans des registres et de copier le rest dans la stack.


    Vous devez absolument appeler la fonction depuis asm ; Cela ne peut pas fonctionner de manière fiable de faire plusieurs appels de fonction comme fill_register_xmm0 et d’espérer que le compilateur n’écrase aucun de ces registres. Le compilateur C émet des instructions qui utilisent les registres dans le cadre de son travail normal, notamment en transmettant des arguments à des fonctions telles que fill_register_xmm0 .

    La seule alternative serait d’écrire une instruction C avec un appel de fonction avec tous les arguments ayant le type correct, pour que le compilateur émette du code afin de faire un appel de fonction normalement. S’il n’y a que quelques combinaisons d’arguments différentes possibles, les mettre dans des blocs if() pourrait être une bonne chose.

    Et BTW, movsd xmm0, variable s’assemble probablement à movsd xmm0, xmm0 , car la première fonction arg est passée dans XMM0 si c’est FP.


    En C, préparez un tampon avec les arguments (comme dans le cas des 32 bits).

    Chacun doit être complété à 8 octets s’il est plus étroit. Voir la documentation de MS pour x86-64 __fastcall . (Notez que x86-64 __vectorcall passe __m128 par valeur dans les registres, mais pour __vectorcall __m128 , il est ssortingctement vrai que les arguments forment un tableau de valeurs sur 8 octets, après le registre arguments. Et stocker ces valeurs dans l’espace fantôme crée un tableau complet de tous les arguments.)

    Tout argument qui ne correspond pas à 8 octets ou à 1, 2, 4 ou 8 octets doit être passé par référence. Il n’y a aucune tentative de propagation d’un seul argument sur plusieurs registres.

    Mais l’essentiel pour rendre les fonctions variadiques faciles dans la convention d’appel de Windows fonctionne également ici: le registre utilisé pour le 2nd argument ne dépend pas du type du premier . Autrement dit, si un argument FP est le premier argument, il utilise alors un intervalle de passage d’argument de registre entier. Vous ne pouvez donc avoir que 4 arguments de registre, et non 4 entiers et 4 FP.

    Si le 4ème argument est un entier, il va dans R9 , même s’il s’agit du premier argument entier . Contrairement à la convention d’appel System V x86-64, où le premier argument entier est rdi dans rdi , quel que soit le nombre d’ arguments FP précédents dans les registres et / ou sur la stack.

    Ainsi, le wrapper asm qui appelle la fonction peut charger les 8 premiers octets dans les registres entier et FP ! (Les fonctions variées en ont déjà besoin, de sorte qu’un appelé n’a pas besoin de savoir s’il doit stocker le nombre entier ou le registre FP pour former ce tableau argument. MS a optimisé la convention d’appel pour la simplicité des fonctions appelées variadiques au désortingment de l’efficacité des fonctions avec mélange d’arguments entiers et FP.)

    Le côté C qui met tous les arguments dans un tampon peut ressembler à ceci:

     #include  int asmwrapper(const char *argbuf, size_t argp-argbuf, void (*funcpointer)(...)); void somefunc() { alignas(16) uint64_t argbuf[256/8]; // or char argbuf[256]. But if you choose not to use alignas, then uint64_t will still give 8-byte alignment char *argp = (char*)argbuf; for( ; argp < &argbuf[256] ; argp += 8) { if (figure_out_an_arg()) { int foo = get_int_arg(); memcpy(argp, &foo, sizeof(foo)); } else if(bar) { double foo = get_double_arg(); memcpy(argp, &foo, sizeof(foo)); } else ... memcpy whatever size // or allocate space to pass by ref and memcpy a pointer } if (argp == &argbuf[256]) { // error, ran out of space for args } asmwrapper(argbuf, argp-argbuf, funcpointer); } 

    Malheureusement, je ne pense pas que nous puissions utiliser directement argbuf sur la stack en tant qu'argument + espace d'ombre pour un appel de fonction. Nous n'avons aucun moyen d'empêcher le compilateur de mettre quelque chose de précieux sous argbuf ce qui nous permettrait simplement de placer rsp en bas de celui-ci (et de sauvegarder l'adresse de retour quelque part, peut-être au sumt de argbuf en réservant un espace d'utilisation par l'ASM). .

    Quoi qu'il en soit, il suffit de copier l'intégralité de la mémoire tampon. Ou en fait, chargez les 32 premiers octets dans des registres (entier et FP) et copiez uniquement le rest. L'espace d'ombre n'a pas besoin d'être initialisé.

    argbuf pourrait être un VLA si vous saviez à l'avance quelle doit être sa taille, mais 256 octets sont assez petits. Ce n'est pas comme si lire au-delà de la fin pouvait être un problème, cela ne pouvait pas être à la fin d'une page avec une mémoire non mappée plus tard, car le cadre de stack de notre fonction parent prenait définitivement de la place.

     ;; NASM syntax. For MASM just rename the local labels and add whatever PROC / ENDPROC is needed. ;; UNTESTED ;; rcx: argbuf ;; rdx: length in bytes of the args. 0..256, zero-extended to 64 bits ;; r8 : function pointer ;; reserve rdx bytes of space for arg passing ;; load first 32 bytes of argbuf into integer and FP arg-passing registers ;; copy the rest as stack-args above the shadow space global asmwrapper asmwrapper: push rbp mov rbp, rsp ; so we can efficiently restore the stack later mov r10, r8 ; move function pointer to a volatile but non-arg-passing register ; load *both* xmm0-3 and rcx,rdx,r8,r9 from the first 32 bytes of argbuf ; regardless of types or whether there were that many arg bytes ; All bytes are loaded into registers early, some reg->reg transfers are done later ; when we're done with more registers. ; movsd xmm0, [rcx] ; movsd xmm1, [rcx+8] movaps xmm0, [rcx] ; 16-byte alignment required for argbuf. Use movups to allow misalignment if you want movhlps xmm1, xmm0 ; use some ALU instructions instead of just loads ; rcx,rdx can't be set yet, still in use for wrapper args movaps xmm2, [rcx+16] ; it's ok to leave garbage in the high 64-bits of an XMM passing a float or double. ;movhlps xmm3, xmm2 ; the copyloop uses xmm3: do this later movq r8, xmm2 mov r9, [rcx+24] mov eax, 32 cmp edx, eax jbe .small_args ; no copying needed, just shadow space sub rsp, rdx and rsp, -16 ; reserve extra space, realigning the stack by 16 ; rax=32 on entry, start copying just above shadow space (which doesn't need to be copied) .copyloop: ; do { movaps xmm3, [rcx+rax] movaps [rsp+rax], xmm3 ; indexed addressing modes aren't always optimal, but this loop only runs a couple times. add eax, 16 cmp eax, edx jb .copyloop ; } while(bytes_copied < arg_bytes); .done_arg_copying: ; xmm0,xmm1 have the first 2 qwords of args movq rcx, xmm0 ; RCX NO LONGER POINTS AT argbuf movq rdx, xmm1 ; xmm2 still has the 2nd 16 bytes of args ;movhlps xmm3, xmm2 ; don't use: false dependency on old value and we just used it. pshufd xmm3, xmm2, 0xee ; xmm3 = high 64 bits of xmm2. (0xee = _MM_SHUFFLE(3,2,3,2)) ; movq xmm3, r9 ; nah, can be multiple uops on AMD ; r8,r9 set earlier call r10 leave ; restore RSP to its value on entry ret ; could handle this branchlessly, but copy loop still needs to run zero times ; unless we bump up the min arg_bytes to 48 and sometimes copy an unnecessary 16 bytes ; As much work as possible is before the first branch, so it can happen while a mispredict recovers .small_args: sub rsp, rax ; reserve shadow space ;rsp still aligned by 16 after push rbp jmp .done_arg_copying ;byte count. This wrapper is 82 bytes; would be nice to fit it in 80 so we don't waste 14 bytes before the next function. ;eg maybe mov rcx, [rcx] instead of movq rcx, xmm0 ;mov eax, $-asmwrapper align 16 

    Cela s’assemble ( sur Godbolt avec NASM ), mais je ne l’ai pas testé.

    Cela devrait fonctionner plutôt bien, mais si vous obtenez des incompréhensions erronées autour de la coupure de <= 32 octets à> 32 octets, modifiez la twig pour qu'elle copie toujours 16 octets supplémentaires. (Décommentez le fichier cmp / cmovb dans la version de Godbolt, mais la boucle de copie doit toujours commencer à 32 octets dans chaque tampon.)

    Si vous passez souvent très peu d'arguments, les charges de 16 octets risquent de heurter un décrochage de transfert entre deux magasins étroits vers un rechargement large , ce qui provoque environ 8 cycles de latence supplémentaires. Ce n'est normalement pas un problème de débit, mais cela peut augmenter la latence avant que la fonction appelée puisse accéder à ses arguments. Si l'exécution dans le désordre ne peut pas cacher cela, il vaut mieux utiliser plus de charge uops pour charger chaque argument de 8 octets séparément. (Surtout dans les registres entiers, puis de là à XMM, si les arguments sont principalement entiers. Cela aura une latence inférieure à mem -> xmm -> entier.)

    Toutefois, si vous avez plus de deux arguments, espérons que les premiers se soient engagés dans L1d et qu’ils n’aient plus besoin de transfert de magasin au moment de l’exécution du wrapper asm. Ou bien il y a suffisamment de copies d'arguments ultérieurs pour que les 2 premiers arguments terminent leur chaîne load + ALU suffisamment tôt pour ne pas retarder le chemin critique à l'intérieur de la fonction appelée.

    Bien sûr, si les performances étaient un gros problème, vous écririez le code qui expliquait les arguments dans asm, vous n'avez donc pas besoin de cette copie, ou utilisez une interface de bibliothèque avec une signature de fonction fixe qu'un compilateur C peut appeler directement. . J'ai essayé de faire en sorte que cela soit le moins possible possible sur les processeurs grand public modernes Intel / AMD ( http://agner.org/optimize/ ), mais je ne l'ai ni évalué ni optimisé, de sorte qu'il pourrait probablement être amélioré avec certains le temps passé à le profiler, en particulier pour certains cas d'utilisation réels.

    Si vous savez que les arguments FP ne sont pas une possibilité pour les 4 premiers, vous pouvez simplifier en chargeant simplement les entiers.