Écriture d'un compilateur JIT dans l'assemblage

J'ai écrit une machine virtuelle en C qui a des performances décentes pour une machine virtuelle non-JIT, mais je veux apprendre quelque chose de nouveau et améliorer les performances. Mon implémentation actuelle utilise simplement un commutateur pour traduire du bytecode VM en instructions, qui est compilé dans une table de saut. Comme je l'ai dit, des performances décentes pour ce que c'est, mais j'ai frappé une barrière qui ne peut être surmontée qu'avec un compilateur JIT.

J'ai déjà posé une question similaire il n'y a pas longtemps sur le code auto-modificateur, mais je suis venu pour réaliser que je ne posais pas la bonne question.

Donc, mon but est d'écrire un compilateur JIT pour cette machine virtuelle C, et je veux le faire dans l'assemblage x86. (J'utilise NASM comme mon assembleur) Je ne suis pas tout à fait sûr de savoir comment procéder. Je suis à l'aise avec l'assemblage, et j'ai regardé quelques exemples de code Auto-modifiables, mais je n'ai pas encore compris comment générer du code.

Mon bloc principal jusqu'à présent copie des instructions dans un morceau de mémoire exécutable, avec mes arguments. Je suis conscient que je peux étiqueter une certaine ligne dans NASM, et copier la ligne entière de cette adresse avec les arguments statiques, mais ce n'est pas très dynamique, et ne fonctionne pas pour un compilateur JIT. Je dois être capable d'interpréter l'instruction du bytecode, de la Copier dans la mémoire exécutable, d'interpréter le premier argument, de la Copier dans la mémoire, puis d'interpréter le deuxième argument et de la Copier dans la mémoire.

J'ai été informé de plusieurs bibliothèques qui feraient cette tâche plus facile, comme GNU lightning, et même LLVM. Cependant, je voudrais d'abord écrire ceci à la main, pour comprendre comment cela fonctionne, avant d'utiliser des ressources externes.

Y a-t-il des ressources ou des exemples que cette communauté pourrait fournir pour m'aider à commencer cette tâche? Un exemple simple montrant deux ou trois instructions comme " add " et " mov " utilisés pour générer du code exécutable, avec des arguments, dynamiquement, en mémoire, ferait des merveilles.

23
demandé sur jakogut 2011-02-18 02:23:48

2 réponses

Je ne recommanderais pas du tout d'écrire un JIT dans l'assemblage. Il existe de bons arguments pour écrire les bits les plus fréquemment exécutés de l'interpréteur dans assembly. Pour un exemple de la façon dont cela ressemble voir ce commentaire de Mike Pall , l'auteur de LuaJIT.

En ce qui concerne le JIT, il existe de nombreux niveaux différents avec une complexité variable:

  1. Compilez un bloc de base (une séquence d'instructions non branchantes) en copiant simplement le code de l'interpréteur. Par exemple, les implémentations de quelques instructions de bytecode (basées sur un registre) peuvent ressembler à ceci:

    ; ebp points to virtual register 0 on the stack
    instr_ADD:
        <decode instruction>
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        <dispatch next instruction>
    instr_SUB:
        ... ; similar
    

    Donc, étant donné la séquence d'instructionsADD R3, R1, R2, SUB R3, R3, R4 un simple JIT pourrait copier les parties pertinentes de l'implémentation des interprètes dans un nouveau bloc de code machine:

        mov ecx, 1
        mov edx, 2
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        add eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
        mov ecx, 3
        mov edx, 4
        mov ebx, 3
        mov eax, [ebp + ecx * 4]  ; load first operand from stack
        sub eax, [ebp + edx * 4]  ; add second operand from stack
        mov [ebp + ebx * 4], eax  ; write back result
    

    Cela copie simplement le code pertinent, nous devons donc initialiser les registres utilisés en conséquence. Une meilleure solution serait de traduire cela directement dans les instructions de la machine mov eax, [ebp + 4], mais maintenant vous vous devez déjà encoder manuellement les instructions demandées.

    Cette technique supprime les frais généraux d'interprétation, mais n'améliore pas beaucoup l'efficacité. Si le code n'est exécuté qu'une ou deux fois, il ne vaut peut-être pas la peine de le traduire d'abord en code machine (ce qui nécessite de vider au moins des parties du i-cache).

  2. Alors que certains JIT utilisent la technique ci-dessus au lieu d'un interprète, ils utilisent alors un mécanisme d'optimisation plus compliqué pour le code fréquemment exécuté. Cela implique de traduire le bytecode exécuté en une représentation intermédiaire (IR) sur laquelle des optimisations supplémentaires sont effectuées.

    Selon le langage source et le type de JIT, cela peut être très complexe (c'est pourquoi de nombreux JIT délèguent cette tâche à LLVM). Un JIT basé sur une méthode doit traiter la jonction des graphiques de flux de contrôle, de sorte qu'ils utilisent le formulaire SSA et exécutent diverses analyses sur cela (par exemple, Hotspot).

    Un JIT de traçage (comme LuaJIT 2) ne compile que du code en ligne droite qui rend beaucoup de choses plus faciles à mettre en œuvre, mais vous devez faire très attention à la façon dont vous choisissez des traces et comment vous liez plusieurs traces ensemble efficacement. Gal et Franz décrivent une méthode dans Cet article (PDF) . Pour une autre méthode, voir le code source LuaJIT. Les deux JITs sont écrits en C (ou peut-être C++).

18
répondu nominolo 2011-02-18 00:11:21

Je vous suggère de regarder le projet http://code.google.com/p/asmjit/. En utilisant le cadre qu'il offre, vous pouvez économiser beaucoup d'énergie. Si vous voulez écrire toutes les choses à la main, il suffit de lire la source et de la réécrire vous-même, je pense que ce n'est pas très difficile.

7
répondu Sili 2011-07-28 01:30:25