Le code généré par le compilateur Tiny C émet des NOP et JMP supplémentaires (inutiles?)

Quelqu’un peut-il expliquer pourquoi ce code:

#include  int main() { return 0; } 

lorsqu’il est compilé avec tcc en utilisant tcc code.c produit cet asm:

 00401000 |. 55 PUSH EBP 00401001 |. 89E5 MOV EBP,ESP 00401003 |. 81EC 00000000 SUB ESP,0 00401009 |. 90 NOP 0040100A |. B8 00000000 MOV EAX,0 0040100F |. E9 00000000 JMP fmt_vuln1.00401014 00401014 |. C9 LEAVE 00401015 |. C3 RETN 

je suppose que

 00401009 |. 90 NOP 

est peut-être là pour un certain alignement de la mémoire, mais qu’en est-il

 0040100F |. E9 00000000 JMP fmt_vuln1.00401014 00401014 |. C9 LEAVE 

Je veux dire, pourquoi le compilateur insèrerait-il ce saut de près qui saute à l’instruction suivante , LEAVE s’exécutera quand même?

Je suis sur Windows 64 bits générant un exécutable 32 bits en utilisant TCC 0.9.26.

    JMP superflu avant l’épilogue des fonctions

    Le JMP au bas qui va à la déclaration suivante, cela a été corrigé dans un commit . La version 0.9.27 de TCC résout ce problème:

    Lorsque ‘return’ est la dernière déclaration du bloc de niveau supérieur (cas très courant et souvent recommandé), le saut n’est pas nécessaire.

    Quant à la raison pour laquelle il existait en premier lieu? L’idée est que chaque fonction a un sharepoint sortie commun possible. Si un bloc de code contenant un retour se trouve en bas, le JMP se dirige vers un sharepoint sortie commun où le nettoyage de la stack est effectué et où ret est exécuté. A l’origine, le générateur de code avait également émis l’instruction JMP à la fin de la fonction si elle était apparue juste avant la dernière } (accolade fermante). Le correctif vérifie s’il existe une instruction return suivie d’une accolade fermante au niveau supérieur de la fonction. Si c’est le cas, le JMP est omis

    Un exemple de code qui a un retour à une scope inférieure avant une accolade fermante:

     int main(int argc, char *argv[]) { if (argc == 3) { argc++; return argc; } argc += 3; return argc; } 

    Le code généré ressemble à:

      401000: 55 push ebp 401001: 89 e5 mov ebp,esp 401003: 81 ec 00 00 00 00 sub esp,0x0 401009: 90 nop 40100a: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 40100d: 83 f8 03 cmp eax,0x3 401010: 0f 85 11 00 00 00 jne 0x401027 401016: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 401019: 89 c1 mov ecx,eax 40101b: 40 inc eax 40101c: 89 45 08 mov DWORD PTR [ebp+0x8],eax 40101f: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] ; Jump to common function exit point. This is the `return argc` inside the if statement 401022: e9 11 00 00 00 jmp 0x401038 401027: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] 40102a: 83 c0 03 add eax,0x3 40102d: 89 45 08 mov DWORD PTR [ebp+0x8],eax 401030: 8b 45 08 mov eax,DWORD PTR [ebp+0x8] ; Jump to common function exit point. This is the `return argc` at end of the function 401033: e9 00 00 00 00 jmp 0x401038 ; Common function exit point 401038: c9 leave 401039: c3 ret 

    Dans les versions antérieures à 0.9.27, le return argc dans l’instruction if sautait à un sharepoint sortie commun (épilogue de fonction). De plus, l’ return argc au bas de la fonction permet également d’accéder au même sharepoint sortie commun de la fonction. Le problème est que le sharepoint sortie commun de la fonction se trouve juste après le return argc niveau supérieur return argc sorte que l’effet secondaire est un JMP supplémentaire qui se trouve être à l’instruction suivante.


    NOP après fonction prolog

    Le NOP n’est pas pour l’alignement. En raison de la manière dont Windows implémente les pages de garde pour la stack (programmes au format Portable Executable), TCC dispose de deux types de prologs. Si l’espace de stack local requirejs <4096 (inférieur à une page), vous voyez ce type de code généré:

     401000: 55 push ebp 401001: 89 e5 mov ebp,esp 401003: 81 ec 00 00 00 00 sub esp,0x0 

    Le sub esp,0 n’est pas optimisé. C’est la quantité d’espace de stack nécessaire pour les variables locales (dans ce cas 0). Si vous ajoutez des variables locales, vous verrez que 0x0 dans les modifications de l’instruction SUB coïncidera avec la quantité d’espace de stack nécessaire pour les variables locales. Ce prolog nécessite 9 octets. Il existe un autre prolog pour traiter le cas où l’espace de stack nécessaire est> = 4096 octets. Si vous ajoutez un tableau de 4096 octets avec quelque chose comme:

     char somearray[4096] 

    et regardez l’instruction résultante, vous verrez le prolog de la fonction passer à un prolog de 10 octets:

     401000: b8 00 10 00 00 mov eax,0x1000 401005: e8 d6 00 00 00 call 0x4010e0 

    Le générateur de code de TCC suppose que la fonction prolog est toujours de 10 octets lors du ciblage de WinPE. Ceci est principalement dû au fait que TCC est un compilateur en une seule passe. Le compilateur ne sait pas combien d’espace de stack une fonction utilisera jusqu’à ce que la fonction soit traitée. Pour éviter ce problème à l’avance, TCC préalloue 10 octets au prolog pour qu’il corresponde à la méthode la plus large. Tout ce qui est plus court est complété à 10 octets.

    Dans le cas où la stack nécessitait moins de 4096 octets, les instructions utilisées totalisaient 9 octets. Le NOP est utilisé pour cadrer le prolog à 10 octets. Dans le cas où> = 4096 octets sont nécessaires, le nombre d’octets est passé dans EAX et la fonction __chkstk est appelée pour allouer l’espace de stack requirejs à la place.

    TCC n’est pas un compilateur d’optimisation , du moins pas vraiment. Chaque instruction qu’elle a émise pour main est sous-optimale ou inutile, à l’exception du ret . IDK pourquoi vous pensiez que le JMP était la seule instruction qui n’ait aucun sens pour la performance.

    C’est ce que nous avons conçu: TCC signifie Tiny C Comstackr. Le compilateur lui-même est conçu pour être simple. Il n’inclut donc pas de code pour rechercher de nombreux types d’optimisations. Remarquez le sub esp, 0 : cette instruction inutile provient clairement du remplissage d’un gabarit de prolog de fonction, et TCC ne recherche même pas le cas particulier où l’offset est à 0 octet. Les autres fonctions ont besoin d’espace de stack pour les sections locales ou d’aligner la stack avant tout appel de fonction enfant, mais pas avec main (). Le CCT s’en fiche, et émet aveuglément des sub esp,0 pour réserver 0 octet. Notez (d’après la réponse de Michaels) qu’il utilise le codage imm32 , de sorte qu’il n’a même pas d’assembleur optimiseur utilisant le codage imm8 . Au lieu de cela, il code en dur le modèle fonction-prolog et ne remplit que ce champ 32 bits.

    L’optimiseur est l’essentiel du travail de création d’un compilateur d’optimisation efficace . Même l’parsing du C ++ moderne est un exploit comparé à l’émission fiable d’asm (ce que même gcc / clang / icc ne peut pas faire tout le temps, même sans tenir compte de l’autovectorisation). Générer juste un travail efficace mais inefficace est plus facile que d’optimiser; la plupart du code de base de gcc est l’optimisation, pas l’parsing. Voir la réponse de Basile sur Pourquoi y a-t-il si peu de compilateurs C?


    Le JMP (comme vous pouvez le voir dans la réponse de @ MichaelPetch) a une explication similaire: TCC (jusqu’à récemment) n’optimisait pas le cas où une fonction n’avait qu’un chemin de retour et n’avait pas besoin de JMP pour un épilogue commun.

    Il y a même un NOP au milieu de la fonction. C’est évidemment un gaspillage d’octets de code et de décodage / émission de bande passante frontale et de taille de fenêtre en panne. (Parfois, l’exécution d’un NOP en dehors d’une boucle ou de quelque chose vaut la peine d’aligner le haut d’une boucle qui est branchée à plusieurs resockets, mais un NOP au milieu d’un bloc de base ne vaut généralement jamais la peine, alors ce n’est pas pour cela que TCC l’a mis ici. Et si un NOP vous aidait, vous pourriez probablement faire encore mieux en réorganisant les instructions ou en choisissant des instructions plus grandes pour faire la même chose sans NOP. effet frontal.)

    @MichaelPetch fait remarquer que TCC veut toujours que son prolog de fonction soit de 10 octets, car il s’agit d’un compilateur en un seul passage (et il ne sait pas combien d’espace il lui faut pour les locaux jusqu’à la fin de la fonction, quand elle revient et se remplit. dans l’imm32). Mais les cibles Windows ont besoin de tests de stack pour modifier ESP / RSP de plus d’une page entière (4096 octets) et le prolog alternatif pour ce cas est de 10 octets, au lieu de 9 pour celui normal sans le NOP. C’est donc un autre compromis privilégiant la vitesse de compilation par rapport à bien.


    Un compilateur optimiseur utiliserait EAX xor-zéro (car il est plus petit et au moins aussi rapide que mov eax,0 ) et omettrait toutes les autres instructions. xor-zeroing est l’une des optimisations de judas x86 les plus connues / communes / de base et présente plusieurs avantages autres que la taille du code sur certaines microarchitectures x86 modernes .

     main: xor eax,eax ret 

    Certains compilateurs optimistes peuvent toujours créer un cadre de stack avec EBP, mais le pop ebp avec pop ebp serait ssortingctement préférable à leave sur tous les processeurs, dans ce cas particulier où ESP = EBP, de sorte que la partie mov esp,ebp de leave ne soit pas nécessaire. . pop ebp est toujours 1 octet, mais c’est aussi une instruction unique sur les processeurs modernes, contrairement à leave qui est au moins 2 ou 3. ( http://agner.org/optimize/ , et voyez aussi d’autres liens d’optimisation des performances dans Wiki tag x86 .) C’est ce que fait gcc. C’est une situation assez commune. si vous poussez d’autres registres après avoir pop ebx un cadre de stack, vous devez pointer ESP au bon endroit avant de faire pop ebx ou autre.


    Les références que TCC se préoccupe sont la vitesse de compilation , et non la qualité (vitesse ou taille) du code obtenu . Par exemple, le site Web de la STC a une référence en lignes / s et en Mo / s (de source C) par rapport à gcc3.2 -O0 , où il est environ 9 fois plus rapide sur un P4.

    Cependant, TCC n’est pas totalement inimaginable: il va apparemment faire de la doublure et, comme le souligne la réponse de Michael, un correctif récent laisse de côté le JMP (mais pas le inutile sub esp, 0 ).