2011-02-06 44 views
29

Mi piacerebbe scrivere un piccolo compilatore JIT proof-of-concept per un elaboratore di linguaggio giocattolo che ho scritto (puramente accademico), ma ho qualche problema nelle altezze medie del design. Concettualmente, ho familiarità con il funzionamento di JIT - si compila bytecode in codice (macchina o assieme?) Da eseguire. Tuttavia, a livello di dadi e bulloni, non sono abbastanza avvincente con il come lo si fa effettivamente il facendo.Come generare ed eseguire il codice nativo in modo dinamico?

mio (molto "newb") reazione istintiva, dal momento che ho non è la prima idea da dove cominciare, sarebbe quello di provare qualcosa di simile al seguente:

  1. mmap() un blocco di memoria , modificando l'accesso a PROT_EXEC
  2. scrivere il codice nativo nel blocco
  3. negozio registri correnti (stack pointer, et al.) in qualche posto accogliente
  4. modificare i registri correnti a punto nel blocco di codice nativo nella regione mappata
  5. il codice nativo sarebbe ora ottenere eseguito dalla macchina
  6. ripristinare i precedenti registri

è che anche vicino ad un/l'algoritmo corretto? Ho provato a esaminare diversi progetti che so essere in possesso di compilatori JIT (come ad esempio V8) ma questi codici sono difficili da consumare a causa delle loro dimensioni e non ho idea di dove iniziare a cercare.

+9

Probabilmente puoi semplificare ulteriormente le cose: spesso puoi semplicemente prendere l'indirizzo di partenza del tuo codice all'interno del blocco 'mmap''ed e lanciarlo su un puntatore a funzione. In tal caso, il codice dovrebbe salvare e ripristinare i propri registri e così via. Dovresti esaminare le convenzioni di chiamata nelle tue piattaforme ABI (Application Binary Interface) per esattamente ciò che devi salvare (e come ottenere argomenti dal codice C, chiamare le funzioni C, ecc.). –

+0

Non che io abbia molta esperienza con questo, ma potresti provare a interpretare l'interprete python di PiPi. Ho esaminato l'interprete CPython ed è abbastanza buono da leggere. – Falmarri

+3

Davvero una buona domanda Chris! – jweyrich

risposta

26

Non sono sicuro di Linux, ma funziona su x86/windows.
Aggiornamento: http://codepad.org/sQoF6kR8

#include <stdio.h> 
#include <windows.h> 

typedef unsigned char byte; 

int arg1; 
int arg2; 
int res1; 

typedef void (*pfunc)(void); 

union funcptr { 
    pfunc x; 
    byte* y; 
}; 

int main(void) { 

    byte* buf = (byte*)VirtualAllocEx(GetCurrentProcess(), 0, 1<<16, MEM_COMMIT, PAGE_EXECUTE_READWRITE); 

    if(buf==0) return 0; 

    byte* p = buf; 

    *p++ = 0x50; // push eax 
    *p++ = 0x52; // push edx 

    *p++ = 0xA1; // mov eax, [arg2] 
    (int*&)p[0] = &arg2; p+=sizeof(int*); 

    *p++ = 0x92; // xchg edx,eax 

    *p++ = 0xA1; // mov eax, [arg1] 
    (int*&)p[0] = &arg1; p+=sizeof(int*); 

    *p++ = 0xF7; *p++ = 0xEA; // imul edx 

    *p++ = 0xA3; // mov [res1],eax 
    (int*&)p[0] = &res1; p+=sizeof(int*); 

    *p++ = 0x5A; // pop edx 
    *p++ = 0x58; // pop eax 
    *p++ = 0xC3; // ret 

    funcptr func; 
    func.y = buf; 

    arg1 = 123; arg2 = 321; res1 = 0; 

    func.x(); // call generated code 

    printf("arg1=%i arg2=%i arg1*arg2=%i func(arg1,arg2)=%i\n", arg1,arg2,arg1*arg2,res1); 

} 
+3

Lo hai provato? Perché Windows ha DEP per impedire esattamente questo, e produce una violazione di accesso per me. – Puppy

+2

'buf' deve essere contrassegnato con privilegi di lettura/scrittura/esecuzione per essere eseguito correttamente con DEP, tramite una sezione personalizzata, regole definite pragma o' VirtualAlloc (Ex) ' – Necrolis

+0

Aggiornato, ma era portatile prima e ora non lo è t. – Shelwien

3

Anche il compilatore Android Dalvik JIT potrebbe essere utile. Dovrebbe essere abbastanza piccolo e snello (non è sicuro se questo aiuta a capirlo o rende le cose più complicate). Si rivolge anche a Linux.

Se le cose stanno diventando più serie, guardare a LLVM potrebbe essere una buona scelta.

L'approccio del puntatore a funzione suggerito da Jeremiah sembra buono. Puoi comunque usare lo stack del chiamante e probabilmente ci saranno solo pochi registri rimasti (su x86) che devi conservare o non toccare. In questo caso, è probabilmente più semplice se il codice compilato (o lo stub della voce) li salva nello stack prima di procedere. Alla fine, tutto si riduce a scrivere una funzione assembler e ad interfacciarlo con C.

+0

Ottimo suggerimento - grazie! –

4

Youmay vuole avere uno sguardo a libjit che fornisce esattamente l'infrastruttura che stai cercando:

Gli strumenti della biblioteca libjit just-in-time compilation funzionalità. A differenza di altri JIT, questo è progettato per essere indipendente da qualsiasi formato macchina virtuale o linguaggio specifico .

http://freshmeat.net/projects/libjit

+0

Scoperta interessante. Questo potrebbe essere utile se deciderò di voler effettivamente implementare un JIT non banale. –

0

Oltre alle tecniche suggerite finora, potrebbe essere utile per esaminare le funzioni di creazione thread. Se crei un nuovo thread, con l'indirizzo iniziale impostato sul tuo codice generato, sai per certo che non ci sono vecchi registri che devono essere salvati o ripristinati e il sistema operativo gestisce l'impostazione dei registri pertinenti per te. Ie elimini i passaggi 3, 4 e 6 della tua lista.

+0

L'unica cosa che esiterei qui è che con i thread, devi quindi gestire la sincronizzazione, e così via. - altrimenti (se la sincronizzazione può essere ignorata e/o differita) è un'idea abbastanza intelligente. –

0

Potresti essere interessato al linguaggio di programmazione PotionPotion. È un linguaggio piccolo e incompleto con una compilazione just-in-time. Le piccole dimensioni della pozione rendono più facile la comprensione. Il repository include una descrizione dello language's internals (il contenuto JIT inizia all'intestazione "~ the jit ~").

L'implementazione è complicata dal fatto che viene eseguito nel contesto di Potion's VM. Non lasciare che questo ti spaventi, però. Non ci vuole molto per vedere cosa sta combinando. In sostanza, l'utilizzo di una piccola serie di codici operativi VM consente di modellare alcune azioni come optimized assembly.

2

La risposta dipende dal compilatore e da dove viene inserito il codice. Vedi http://encode.ru/threads/1273-Just-In-Time-Compilation-Improvement-For-ZPAQ?p=24902&posted=1#post24902

Test in Vista a 32 bit, Visual C++ fornisce un errore di prevenzione esecuzione dati (DEP) se il codice viene inserito nello stack, heap o memoria statica. g ++, Borland e Marte possono essere fatti funzionare a volte. I dati a cui si accede dal codice JIT devono essere dichiarati volatili.

+1

Ottimo punto, per dichiarare il supporto come volatile! +1 –

2

How to JIT - an introduction è un nuovo articolo (da oggi!) Che affronta alcuni di questi problemi e descrive anche l'immagine più grande.

Problemi correlati