book cover

CRÉER SA PROPRE VM
AVEC COMPILATION JIT

pour les nuls

🚨 SPOILER ALERT 🚨

CE TALK EST ASSEZ TECHNIQUE !

VOUS ALLEZ VOIR DU CODE C++ ET DE L'ASSEMBLEUR 😱

Mais ne vous inquiétez pas, tout va bien se passer 🤓

UNE JIT ?

for dummies

KESAKO ?

LA JIT ? KESAKO ?

Acronyme de « Just In Time » compiler, elle permet de :

  • de transformer à la volée un langage ou un bytecode en code machine compréhensible par le microprocesseur
  • d'augmenter la portabilité en s'affranchissant d'une phase de compilation anticipée vers un code binaire
  • d'opérer dans certains cas des optimisations de code machine en fonction du profil d'exécution

▪ ▪ ▪

QUELQUES CAS D'USAGES

  • Accélérer un interpréteur de langage (eg. JavaScript)
  • Augmenter les performances de machines virtuelles (eg. Java, C#, ...)
  • Faire de la translation dynamique de code (eg. Qemu, émulateurs)
  • Créer ex-nihilo un Domain Specific Language performant

▪ ▪ ▪

CODE SOURCE, CODE MACHINE
ET BYTECODE

for dummies

JUSTE UNE MISE AU POINT

LE CODE SOURCE

LE HAUT NIVEAU

  • C'est un langage de haut niveau conçu pour les humains
  • Il permet d'écrire des programmes de façon expressive
  • Il est incompréhensible par une architecture matérielle

▪ ▪ ▪

LE CODE MACHINE

LE BAS NIVEAU

  • C'est le langage natif de la machine
  • Il est spécifique à une architecture matérielle
  • C'est la seule chose qu'une machine peut comprendre

▪ ▪ ▪

LE BYTECODE

LE NIVEAU INTERMÉDIAIRE

  • C'est un « pseudo langage machine »
  • Il est indépendant de toute architecture matérielle
  • C'est un niveau entre le code source et le code machine

▪ ▪ ▪

LANGAGES ET COMPILATION

for dummies

DIFFÉRENTES FAÇONS DE COMPRENDRE UN LANGAGE

COMPRENDRE UN LANGAGE

QUATRE GRANDES TECHNIQUES

  • L'interpréteur
  • Le compilateur Ahead Of Time (AOT)
  • Le compilateur Just In Time (JIT)
  • Le compilateur AOT+JIT

▪ ▪ ▪

L'INTERPRÉTEUR

  • Le code source n'est absolument pas compilé
  • Le code source est analysé et exécuté au fil de l'eau

interpreter

AVANTAGES DE L'INTERPRÉTEUR

  • Pas de phase préliminaire de compilation des programmes
  • Relative facilité de développement de l'interpréteur
  • Portabilité assez élevée et plutôt aisée

👍

INCONVENIENTS DE L'INTERPRÉTEUR

  • L'exécution du programme peut être relativement lente
  • Difficulté plus élevée de maintenance et de déboggage
  • Des erreurs de syntaxe peuvent interrompre le programme

👎

LA COMPILATION AHEAD OF TIME

  • Le code source est compilé vers du code binaire avant l'exécution
  • Le code binaire produit est exécuté directement par le processeur

compiler-aot

AVANTAGES DE LA COMPILATION AHEAD OF TIME

  • Code analysé et optimisé avant l'exécution
  • Performance lors de l'exécution
  • Facilité de déboggage

👍

INCONVENIENTS DE LA COMPILATION AHEAD OF TIME

  • Nécessite une phase préliminaire de compilation
  • Perte de fléxibilité dans les phases de développement et de déboggage
  • Le code binaire produit est figé une fois pour toute

👎

LA COMPILATION JUST IN TIME

  • Le code source est compilé vers du bytecode lors de l'exécution
  • Le bytecode est compilé vers du code binaire lors de l'exécution

compiler-jit

AVANTAGES DE LA COMPILATION JUST IN TIME

  • L'exécution du code est de moyennement à assez performante
  • L'optimisation du code ne repose pas sur une compilation amont
  • Le code binaire produit peut être optimisé de façon adaptative

👍

INCONVENIENTS DE LA COMPILATION JUST IN TIME

  • Légère perte de performances due à la compilation en temps réel
  • Nécessite un runtime embarquant un exécuteur et un compilateur
  • La chaîne d'outils peut être plus complexe à développer

👎

LA COMPILATION AOT + JIT

  • Le code source est compilé vers du bytecode avant l'exécution
  • Le bytecode est compilé vers du code binaire lors de l'exécution

compiler-aot-jit

AVANTAGES DE LA COMPILATION AOT + JIT

  • Le meilleur de la compilation AOT et JIT
  • La phase de compilation en bytecode est faite en amont
  • Limite les erreurs et permet l'analyse du code en amont

👍

INCONVENIENTS DE LA COMPILATION AOT + JIT

  • Combinaison des inconvénients AOT et JIT
  • Nécessite une phase préliminaire de compilation intermédiaire
  • Nécessite une certaine standardisation du code intermédiaire

👎

CRÉONS UNE MACHINE VIRTUELLE

for dummies

AVEC SON BYTECODE ET SA JIT INTÉGRÉE

LE PRINCIPE

LA RECETTE DE TONTON Z'OLIV'

  • Concevoir un langage source
  • Concevoir un bytecode simple et complet
  • Compiler le langage vers le bytecode
  • Interpréter le bytecode lors de la première exécution
  • ↪ Exécuter chaque instruction du bytecode généré
  • ↪ Générer le code machine binaire correspondant
  • Exécuter le code binaire les fois suivantes

▪ ▪ ▪

UNE CALCULATRICE RPN

REVERSE POLISH NOTATION

hp48gx

La notation polonaise inverse (ou notation post-fixée) permet d'écrire des formules arithmétiques de façon simple et non ambiguë

EXEMPLES D'EXPRESSIONS RPN

POUR ILLUSTRER LE PRINCIPE


1 + 2             →   1 2 +
                        

3 * 4             →   3 4 *
                        

1 + 2 * 3         →   1 2 3 * +
                        

(3 + 4) * 2 + 1   →   3 4 + 2 * 1 +
                        

3 - (4 * 5)       →   3 4 5 * −
                        

▪ ▪ ▪

MOTS CLÉS DE LA CALCULATRICE

nop no operation; do absolutely nothing
nnn push the nnn value onto the stack
i1 top get the topmost value from the stack
i1 pop pop the topmost value from the stack
clr clear the stack
i1 dup duplicate i1
i1 i2 xch exchange i1 and i2
i1 i2 sto store i1 into i2 register
i1 rcl fetch i2 from i1 register
i1 abs compute the absolute value of i1
i1 neg compute the negation of i1
i1 i2 add compute the sum of i1 and i2
i1 i2 sub compute the difference of i1 and i2
i1 i2 mul compute the product of i1 and i2
i1 i2 div compute the division of i1 and i2
i1 i2 mod compute the remainder of i1 and i2
i1 cpl compute the one’s complement of i1
i1 i2 and compute the logical and between i1 and i2
i1 i2 ior compute the logical inclusive or between i1 and i2
i1 i2 xor compute the logical exclusive or between i1 and i2
i1 i2 shl shift i1 i2 bits to the left
i1 i2 shr shift i1 i2 bits to the right
i1 inc increment i1
i1 dec decrement i1
i1 hlt blocks the execution for at least i1 milliseconds

OPÉRATEURS DE LA CALCULATRICE

i1 i2 + same as <i1> <i2> add
i1 i2 - same as <i1> <i2> sub
i1 i2 * same as <i1> <i2> mul
i1 i2 / same as <i1> <i2> div
i1 i2 % same as <i1> <i2> mod
i1 i2 ~ same as <i1> <i2> cpl
i1 i2 & same as <i1> <i2> and
i1 i2 | same as <i1> <i2> ior
i1 i2 ^ same as <i1> <i2> xor
i1 i2 << same as <i1> <i2> shl
i1 i2 >> same as <i1> <i2> shr
i1 ++ same as <i1> inc
i1 -- same as <i1> dec

INSTRUCTIONS ÉTENDUES

i1 st0 same as <i1> 0 sto
i1 st1 same as <i1> 1 sto
i1 st2 same as <i1> 2 sto
i1 st3 same as <i1> 3 sto
i1 st4 same as <i1> 4 sto
i1 st5 same as <i1> 5 sto
i1 st6 same as <i1> 6 sto
i1 st7 same as <i1> 7 sto
i1 st8 same as <i1> 8 sto
i1 st9 same as <i1> 9 sto
rc0 same as 0 rcl
rc1 same as 1 rcl
rc2 same as 2 rcl
rc3 same as 3 rcl
rc4 same as 4 rcl
rc5 same as 5 rcl
rc6 same as 6 rcl
rc7 same as 7 rcl
rc8 same as 8 rcl
rc9 same as 9 rcl
rnd same as 29 rcl; get a pseudo-random number
now same as 30 rcl; get the number of milliseconds since epoch
i1 i2 fib same as dup 20 sto add 20 rcl xch; compute an iteration of the fibonacci suite
run run the compiled expression

ARCHITECTURE

VUE SIMPLIFIÉE

rpn calculator with jit

LA CALCULATRICE

C'EST NOTRE MACHINE VIRTUELLE


class Calculator
{
public: // public interface
    Calculator(Console&);
    virtual ~Calculator() = default;

    void execute(const std::string& expression);

    void compile(const std::string& expression);

    void run();

    void clear();

    int64_t result();

private: // private data
    Console& _console;
    Operands _operands;
    ByteCode _bytecode;
    HostCode _hostcode;
};
                        

L'ÉTAT DE LA CALCULATRICE RPN

CONTIENT LA STACK, LES REGISTRES ET LES OPÉRATEURS


struct Operands
{
    std::stack<int64_t>     stack;
    std::array<int64_t, 32> array;
};

struct Operators
{
    static int64_t op_nop(Operands& operands);
    static int64_t op_i64(Operands& operands, const int64_t operand);
    static int64_t op_top(Operands& operands);
    static int64_t op_pop(Operands& operands);
    static int64_t op_clr(Operands& operands);
    static int64_t op_dup(Operands& operands);
    static int64_t op_xch(Operands& operands);
    static int64_t op_sto(Operands& operands);
    static int64_t op_rcl(Operands& operands);
    static int64_t op_abs(Operands& operands);
    static int64_t op_neg(Operands& operands);
    static int64_t op_add(Operands& operands);
    static int64_t op_sub(Operands& operands);
    static int64_t op_mul(Operands& operands);
    static int64_t op_div(Operands& operands);
    static int64_t op_mod(Operands& operands);
    static int64_t op_cpl(Operands& operands);
    static int64_t op_and(Operands& operands);
    static int64_t op_ior(Operands& operands);
    static int64_t op_xor(Operands& operands);
    static int64_t op_shl(Operands& operands);
    static int64_t op_shr(Operands& operands);
    static int64_t op_inc(Operands& operands);
    static int64_t op_dec(Operands& operands);
    static int64_t op_hlt(Operands& operands);
};
                        

LE BYTECODE

C'EST LE PSEUDO CODE MACHINE DE NOTRE VM

  • Il est indépendant de toute architecture physique
  • Chaque pseudo instruction est encodée sur un octet
  • Chaque pseudo instruction peut éventuellement avoir des paramètres

▪ ▪ ▪

LE BYTECODE

REPRÉSENTATION EN MÉMOIRE

bytecode

LE BYTECODE

LE PSEUDO CODE MACHINE DE NOTRE VM


class ByteCode
{
public: // public interface
    ByteCode();
    virtual ~ByteCode();

    void reset();

    void emit_byte(const uint8_t value);
    void emit_word(const uint16_t value);
    void emit_long(const uint32_t value);
    void emit_quad(const uint64_t value);

    void emit_nop();
    void emit_i64(const int64_t operand);
    void emit_top();
    void emit_pop();
    void emit_clr();
    ...
    void emit_add();
    void emit_sub();
    void emit_mul();
    void emit_div();
    ...

private: // private data
    uint8_t* _buffer;
    uint8_t* _bufptr;
    size_t   _length;
};
                            

class ByteCode
{
public: // public interface
    ...
public: // opcodes
    static constexpr uint8_t OP_NOP = 0x00;
    static constexpr uint8_t OP_I64 = 0x01;
    static constexpr uint8_t OP_TOP = 0x02;
    static constexpr uint8_t OP_POP = 0x03;
    static constexpr uint8_t OP_CLR = 0x04;
    static constexpr uint8_t OP_DUP = 0x05;
    static constexpr uint8_t OP_XCH = 0x06;
    static constexpr uint8_t OP_STO = 0x07;
    static constexpr uint8_t OP_RCL = 0x08;
    static constexpr uint8_t OP_ABS = 0x09;
    static constexpr uint8_t OP_NEG = 0x0a;
    static constexpr uint8_t OP_ADD = 0x0b;
    static constexpr uint8_t OP_SUB = 0x0c;
    static constexpr uint8_t OP_MUL = 0x0d;
    static constexpr uint8_t OP_DIV = 0x0e;
    static constexpr uint8_t OP_MOD = 0x0f;
    static constexpr uint8_t OP_CPL = 0x10;
    static constexpr uint8_t OP_AND = 0x11;
    static constexpr uint8_t OP_IOR = 0x12;
    static constexpr uint8_t OP_XOR = 0x13;
    static constexpr uint8_t OP_SHL = 0x14;
    static constexpr uint8_t OP_SHR = 0x15;
    static constexpr uint8_t OP_INC = 0x16;
    static constexpr uint8_t OP_DEC = 0x17;
    static constexpr uint8_t OP_HLT = 0x18;
    ...
};
                            

COMPILER UNE EXPRESSION

TRANSFORMER UNE EXPRESSION EN BYTECODE

parsing

INTERPRÉTER LE BYTECODE

FETCH · DECODE · EXECUTE

interpreting

FETCH & DECODE

LIRE ET DÉCODER LE BYTECODE


void Calculator::run()
{
    int skip = 0;
    for(const uint8_t& opcode : _bytecode) {
        if(skip > 0) {
            --skip;
        }
        else switch(opcode) {
            case ByteCode::OP_NOP:
                skip = execute_nop(opcode);
                break;
            case ByteCode::OP_I64:
                skip = execute_i64(opcode);
                break;
...
            case ByteCode::OP_ADD:
                skip = execute_add(opcode);
                break;
            case ByteCode::OP_SUB:
                skip = execute_sub(opcode);
                break;
            case ByteCode::OP_MUL:
                skip = execute_mul(opcode);
                break;
            case ByteCode::OP_DIV:
                skip = execute_div(opcode);
                break;
...
            default:
                throw std::runtime_error("unexpected opcode");
        }
    }
}
                        

EXECUTE

EXÉCUTER LES MICRO-INSTRUCTIONS


int Calculator::execute_i64(const uint8_t& opcode)
{
    int64_t operand = 0;
    operand = ((operand << 8) | (&opcode)[8]);
    operand = ((operand << 8) | (&opcode)[7]);
    operand = ((operand << 8) | (&opcode)[6]);
    operand = ((operand << 8) | (&opcode)[5]);
    operand = ((operand << 8) | (&opcode)[4]);
    operand = ((operand << 8) | (&opcode)[3]);
    operand = ((operand << 8) | (&opcode)[2]);
    operand = ((operand << 8) | (&opcode)[1]);

    Operators::op_i64(_operands, operand)

    return 8;
}

...

int Calculator::execute_add(const uint8_t& opcode)
{
    Operators::op_add(_operands)

    return 0;
}

...
                        

GÉNÉRER DYNAMIQUEMENT DU CODE

for dummies

LA BASE DE LA COMPILATION JIT

LE PRINCIPE

UNE RECETTE ÉPROUVÉE

  • Allouer une mémoire tampon
  • Écrire du code binaire dans ce tampon
  • ↪ Prologue de fonction
  • ↪ Corps de fonction
  • ↪ Épilogue de fonction
  • Exécuter le code binaire

▪ ▪ ▪

PARLONS D'ISA

INSTRUCTION SET ARCHITECTURE

C'est le modèle d'architecture et la description complète
d'un microprocesseur ou d'une famille de microprocesseurs

▪ ▪ ▪

QUE DÉFINIT L'ISA ?

DES SPÉCIFICATIONS COMPLÈTES

  • Les registres internes
  • Les instructions disponibles
  • L'encodage des instructions
  • Les modes d'adressage
  • Le boutisme
  • Etc.

▪ ▪ ▪

PARLONS AUSSI D'ABI

APPLICATION BINARY INTERFACE

Elle décrit les conventions utilisées par le système d'exploitation
et les programmes dans le contexte d'exécution du code binaire

Elle est spécifique à un ou plusieurs systèmes d'exploitation
et peut être différente pour une même architecture processeur

▪ ▪ ▪

QUE DÉFINIT L'ABI ?

DES CONVENTIONS D'INTERFACES

  • Les types de données
  • Le boutisme
  • L'utilisation des registres
  • Les conventions d'appels aux fonctions
  • Comment les arguments sont passés aux fonctions
  • Comment les résultats sont renvoyés par les fonctions
  • Comment sont propagées les exceptions
  • Etc.

▪ ▪ ▪

L'ABI SYSTEM V X86-64

EXTRAITS DE LA SPÉCIFICATION

This is a 64-bit platform. The stack grows downwards. Parameters to functions are passed in the registers rdi, rsi, rdx, rcx, r8, r9, and further values are passed on the stack in reverse order. Parameters passed on the stack may be modified by the called function. Functions are called using the call instruction that pushes the address of the next instruction to the stack and jumps to the operand. Functions return to the caller using the ret instruction that pops a value from the stack and jump to it. The stack is 16-byte aligned just before the call instruction is called.

Functions preserve the registers rbx, rsp, rbp, r12, r13, r14, and r15; while rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 are scratch registers. The return value is stored in the rax register, or if it is a 128-bit value, then the higher 64-bits go in rdx. Optionally, functions push rbp such that the caller-return-rip is 8 bytes above it, and set rbp to the address of the saved rbp. This allows iterating through the existing stack frames. This can be eliminated by specifying the -fomit-frame-pointer GCC option.

Signal handlers are executed on the same stack, but 128 bytes known as the red zone is subtracted from the stack before anything is pushed to the stack. This allows small leaf functions to use 128 bytes of stack space without reserving stack space by subtracting from the stack pointer. The red zone is well-known to cause problems for x86-64 kernel developers, as the CPU itself doesn't respect the red zone when calling interrupt handlers. This leads to a subtle kernel breakage as the ABI contradicts the CPU behavior. The solution is to build all kernel code with -mno-red-zone or by handling interrupts in kernel mode on another stack than the current (and thus implementing the ABI).

▪ ▪ ▪

UNE FONCTION MINIMALE EN C++

UNE FONCTION EN APPELANT UNE AUTRE


void hello_world()
{
    std::cout << "Hello World!" << std::endl;
}

void function()
{
    hello_world();
}
                        

SON ANATOMIE EN ASSEMBLEUR X86-64

AVEC UN CODE SIMPLE ET NON OPTIMAL


; -------------------------------------------------------------------
; function prolog
; -------------------------------------------------------------------

push rbp              ; save the base pointer onto the stack
mov  rbp,rsp          ; load the base pointer with the stack pointer

; -------------------------------------------------------------------
; function body
; -------------------------------------------------------------------

mov  rax,hello_world  ; load rax with the hello_world address
call rax              ; call the address pointed by rax

; -------------------------------------------------------------------
; function epilog
; -------------------------------------------------------------------

mov  rsp,rbp          ; restore the stack pointer from the base pointer
pop  rbp              ; restore the saved base pointer from the stack
ret                   ; return to the caller
                        

SON CODE BINAIRE X86-64

APRÈS ASSEMBLAGE


00: 55                               push   rbp
01: 48 89 e5                         mov    rbp,rsp
04: 48 b8 00 11 22 33 44 55 66 77    mov    rax,0x7766554433221100
0e: ff d0                            call   rax
10: 48 89 ec                         mov    rsp,rbp
13: 5d                               pop    rbp
14: c3                               ret
                        

MAINTENANT GÉNÉRONS DU CODE BINAIRE EN C++

inception deeper

MODÉLISONS UN GÉNÉRATEUR DE CODE BINAIRE


class HostCode
{
public: // public interface
    HostCode();
    virtual ~HostCode();

    void emit_byte(const uint8_t value);
    void emit_word(const uint16_t value);
    void emit_long(const uint32_t value);
    void emit_quad(const uint64_t value);

    void nop();
    void push_rbp();
    void mov_rbp_rsp();
    void sub_rsp_imm08(const uint8_t imm08);
    void mov_rsp_rbp();
    void pop_rbp();
    void ret();
    void mov_rax_imm64(const uint64_t imm64);
    void mov_rdi_imm64(const uint64_t imm64);
    void mov_rsi_imm64(const uint64_t imm64);
    void call_rax();

    void execute();

private: // private interface
    void allocate();
    void deallocate();

private: // private data
    uint8_t* _buffer;
    uint8_t* _bufptr;
    size_t   _length;
};
                            

LE CONSTRUCTEUR


HostCode::HostCode()
    : _buffer(nullptr)
    , _bufptr(nullptr)
    , _length(0)
{
    allocate();
}

void HostCode::allocate()
{
    if(_length == 0) {
        const long length = ::sysconf(_SC_PAGESIZE);
        if(length > 0) {
            _length = length;
        }
        else {
            throw std::runtime_error("sysconf() has failed");
        }
    }
    if(_buffer == nullptr) {
        const int prot   = (PROT_READ | PROT_WRITE | PROT_EXEC);
        const int flags  = (MAP_PRIVATE | MAP_ANONYMOUS);
        void*     buffer = ::mmap(nullptr, _length, prot, flags, -1, 0);
        if(buffer != MAP_FAILED) {
            _buffer = _bufptr = reinterpret_cast<uint8_t*>(buffer);
        }
        else {
            throw std::runtime_error("mmap() has failed");
        }
    }
}
                            

LE DESTRUCTEUR


HostCode::~HostCode()
{
    deallocate();
}

void HostCode::deallocate()
{
    if(_buffer != nullptr) {
        const int rc = ::munmap(_buffer, _length);
        if(rc == 0) {
            _buffer = _bufptr = nullptr;
        }
        else {
            throw std::runtime_error("munmap() has failed");
        }
    }
    if(_length != 0) {
        _length = 0;
    }
}
                            

ÉCRIRE DES DONNÉES DANS LE TAMPON


void HostCode::emit_byte(const uint8_t value)
{
    if(_bufptr < (_buffer + _length)) {
        *_bufptr++ = value;
    }
    else {
        throw std::runtime_error("buffer is full");
    }
}

void HostCode::emit_word(const uint16_t value)
{
    emit_byte(static_cast<uint8_t>((value >>  0) & 0xff));
    emit_byte(static_cast<uint8_t>((value >>  8) & 0xff));
}

void HostCode::emit_long(const uint32_t value)
{
    emit_byte(static_cast<uint8_t>((value >>  0) & 0xff));
    emit_byte(static_cast<uint8_t>((value >>  8) & 0xff));
    emit_byte(static_cast<uint8_t>((value >> 16) & 0xff));
    emit_byte(static_cast<uint8_t>((value >> 24) & 0xff));
}

void HostCode::emit_quad(const uint64_t value)
{
    emit_byte(static_cast<uint8_t>((value >>  0) & 0xff));
    emit_byte(static_cast<uint8_t>((value >>  8) & 0xff));
    emit_byte(static_cast<uint8_t>((value >> 16) & 0xff));
    emit_byte(static_cast<uint8_t>((value >> 24) & 0xff));
    emit_byte(static_cast<uint8_t>((value >> 32) & 0xff));
    emit_byte(static_cast<uint8_t>((value >> 40) & 0xff));
    emit_byte(static_cast<uint8_t>((value >> 48) & 0xff));
    emit_byte(static_cast<uint8_t>((value >> 56) & 0xff));
}
                            

GÉNÉRER DES INSTRUCTIONS


void HostCode::push_rbp()
{
    emit_byte(0x55);
}

void HostCode::mov_rbp_rsp()
{
    emit_byte(0x48);
    emit_byte(0x89);
    emit_byte(0xe5);
}

void HostCode::mov_rsp_rbp()
{
    emit_byte(0x48);
    emit_byte(0x89);
    emit_byte(0xec);
}

void HostCode::pop_rbp()
{
    emit_byte(0x5d);
}

void HostCode::ret()
{
    emit_byte(0xc3);
}

void HostCode::mov_rax_imm64(const uint64_t imm64)
{
    emit_byte(0x48);
    emit_byte(0xb8);
    emit_quad(imm64);
}
                            

EXÉCUTER LE CODE DEPUIS LE TAMPON


using PointerToFunction = void(*)(void);

void HostCode::execute()
{
    PointerToFunction function = reinterpret_cast<PointerToFunction>(_buffer);

    if((_buffer != nullptr) && (_bufptr != nullptr) && (_buffer < _bufptr)) {
        (*function)();
    }
    else {
        throw std::runtime_error("cannot execute null buffer");
    }
}
                            

ÉCRIVONS UN PROGRAMME DE TEST


void hello_world()
{
    std::cout << "Hello World!" << std::endl;
}

int main(int argc, char* argv[])
{
    try {
        jit::HostCode hostcode;

        hostcode.push_rbp();
        hostcode.mov_rbp_rsp();
        hostcode.mov_rax_imm64(reinterpret_cast<uintptr_t>(&hello_world));
        hostcode.call_rax();
        hostcode.mov_rsp_rbp();
        hostcode.pop_rbp();
        hostcode.ret();

        hostcode.execute();
    }
    catch(const std::runtime_error& e) {
        std::cerr << e.what() << std::endl;
    }
    return EXIT_SUCCESS;
}
                        

TESTONS LE PROGRAMME


user@host:~$ make
g++ -c -std=c++14 -g -Og -Wall -I. example1.cc
g++ -L. -o example1.bin example1.o -lpthread -lm
=== build ok ===
user@host:~$ ./example1.bin 
Hello World!
                            

IT WORKS! 🎉

ANALYSONS AU DEBUGGER


user@host:~$ gdb ./example1.bin
Reading symbols from example1.bin...
(gdb) break HostCode::execute() 
Breakpoint 1 at 0x19c0: file example1.cc, line 203.
(gdb) run
Starting program: example1.bin 

Breakpoint 1, jit::HostCode::execute (this=this@entry=0x7fffffffdc50) at example1.cc:203
203	{
(gdb) set disassembly-flavor intel 
(gdb) disassemble _buffer,_bufptr
Dump of assembler code from 0x7ffff7ffa000 to 0x7ffff7ffa015:
   0x00007ffff7ffa000:	push   rbp
   0x00007ffff7ffa001:	mov    rbp,rsp
   0x00007ffff7ffa004:	movabs rax,0x555555555400
   0x00007ffff7ffa00e:	call   rax
   0x00007ffff7ffa010:	mov    rsp,rbp
   0x00007ffff7ffa013:	pop    rbp
   0x00007ffff7ffa014:	ret    
End of assembler dump.
(gdb) continue
Continuing.
Hello World!
[Inferior 1 (process 26251) exited normally]
(gdb) quit
                            

EASY PEASY 😎

LES BASIC BLOCKS

Un basic block est une séquence de code sans embranchements
ayant un seul point d'entrée et un seul point de sortie

Ils forment les sommets et les nœuds d'un graphe d'exécution

▪ ▪ ▪

BASIC BLOCKS · UN EXEMPLE


void function()
{
    hello_world();
}
                        

basic block 1 basic block 2

BASIC BLOCKS · UN AUTRE EXEMPLE


void function(long count)
{
    while(count > 0) {
        hello_world();
        --count;
    }
}
                        

basic block 3

MODÉLISONS UN BASIC BLOCK


class BasicBlock
{
public: // public interface
    BasicBlock(HostCode& hostcode);
    virtual ~BasicBlock() = default;

    void reset();
    void begin();
    void end();
    void execute();

    HostCode* operator->()
    {
        return &_hostcode;
    }

private: // private data
    HostCode&      _hostcode;
    const uint8_t* _begin;
    const uint8_t* _end;
};
                            

ÉCRIVONS UN PROGRAMME DE TEST


int main(int argc, char* argv[])
{
    try {
        jit::HostCode hostcode;
        jit::BasicBlock bb1(hostcode);
        jit::BasicBlock bb2(hostcode);

        /* initialize basic block 1 */ {
            bb1.begin();
            bb1->push_rbp();
            bb1->mov_rbp_rsp();
            bb1->mov_rax_imm64(reinterpret_cast<uintptr_t>(&hello_world1));
            bb1->call_rax();
            bb1->mov_rsp_rbp();
            bb1->pop_rbp();
            bb1->ret();
            bb1.end();
        }
        /* initialize basic block 2 */ {
            bb2.begin();
            bb2->push_rbp();
            bb2->mov_rbp_rsp();
            bb2->mov_rax_imm64(reinterpret_cast<uintptr_t>(&hello_world2));
            bb2->call_rax();
            bb2->mov_rsp_rbp();
            bb2->pop_rbp();
            bb2->ret();
            bb2.end();
        }
        /* execute all basic blocks */ {
            bb1.execute();
            bb2.execute();
        }
    }
    catch(const std::runtime_error& e) {
        std::cerr << e.what() << std::endl;
    }
    return EXIT_SUCCESS;
}
                            

EXÉCUTONS LE PROGRAMME DE TEST


user@host:~$ make
g++ -c -std=c++14 -g -Og -Wall -I. example2.cc
g++ -L. -o example2.bin example2.o -lpthread -lm
=== build ok ===
user@host:~$ ./example2.bin 
Hello World #1
Hello World #2
                            

IT WORKS AGAIN! 🥳

ANALYSONS AU DEBUGGER


user@host:~$ gdb ./example2.bin
Reading symbols from example2.bin...
(gdb) break HostCode::~HostCode()
Breakpoint 1 at 0x26ba: HostCode::~HostCode(). (2 locations)
(gdb) run
Starting program: example2.bin 
Hello World #1
Hello World #2

Breakpoint 1, jit::HostCode::~HostCode (this=0x7fffffffdc10, __in_chrg=<optimized out>) at example2.cc:52
52	HostCode::~HostCode()
(gdb) set disassembly-flavor intel
(gdb) disassemble _buffer,_bufptr
Dump of assembler code from 0x7ffff7ffa000 to 0x7ffff7ffa02a:
   0x00007ffff7ffa000:	push   rbp
   0x00007ffff7ffa001:	mov    rbp,rsp
   0x00007ffff7ffa004:	movabs rax,0x555555556400
   0x00007ffff7ffa00e:	call   rax
   0x00007ffff7ffa010:	mov    rsp,rbp
   0x00007ffff7ffa013:	pop    rbp
   0x00007ffff7ffa014:	ret    
   0x00007ffff7ffa015:	push   rbp
   0x00007ffff7ffa016:	mov    rbp,rsp
   0x00007ffff7ffa019:	movabs rax,0x55555555647a
   0x00007ffff7ffa023:	call   rax
   0x00007ffff7ffa025:	mov    rsp,rbp
   0x00007ffff7ffa028:	pop    rbp
   0x00007ffff7ffa029:	ret    
End of assembler dump.
(gdb) continue
Continuing.
[Inferior 1 (process 16969) exited normally]
(gdb) quit
                            

LIKE A BOSS 😎

FINALISONS LA MACHINE VIRTUELLE

for dummies

NOUS SAVONS GÉNÉRER ET EXÉCUTER DU CODE BINAIRE
AJOUTONS UNE JIT

EXÉCUTER OU RECOMPILER

EXÉCUTER LE BYTECODE OU LE CODE NATIF


void Calculator::run()
{
    if(_function.callable()) {
        log_trace("the bytecode has already been translated, executing the generated machine code...");
        _function.execute();
    }
    else {
        log_trace("the bytecode has never been translated, executing bytecode and translating to machine code...");
        translate();
    }
};
                        

FETCH & DECODE

LIRE ET DÉCODER LE BYTECODE


void Calculator::translate()
{
    prolog();
    int skip = 0;
    for(const uint8_t& opcode : _bytecode) {
        if(skip > 0) {
            --skip;
        }
        else switch(opcode) {
            case ByteCode::OP_NOP:
                skip = execute_and_compile_nop(opcode);
                break;
            case ByteCode::OP_I64:
                skip = execute_and_compile_i64(opcode);
                break;
...
            case ByteCode::OP_ADD:
                skip = execute_and_compile_add(opcode);
                break;
...
            default:
                throw std::runtime_error("unexpected opcode");
        }
    }
    epilog();
}
                        

FETCH & DECODE

PROLOG & EPILOG


void Calculator::prolog()
{
    BasicBlock bb;
    bb.begin(_hostcode.end());
    _hostcode.push_rbp();
    _hostcode.mov_rbp_rsp();
    bb.end(_hostcode.end());
    _function.add(bb);
};

void Calculator::epilog()
{
    BasicBlock bb;
    bb.begin(_hostcode.end());
    _hostcode.mov_rsp_rbp();
    _hostcode.pop_rbp();
    _hostcode.ret();
    bb.end(_hostcode.end());
    _function.add(bb);
};
                        

EXECUTE AND COMPILE

EXÉCUTER ET COMPILER LES MICRO-INSTRUCTIONS


int Calculator::execute_and_compile_i64(const uint8_t& opcode)
{
    int64_t operand = 0;
    operand = ((operand << 8) | (&opcode)[8]);
    operand = ((operand << 8) | (&opcode)[7]);
    operand = ((operand << 8) | (&opcode)[6]);
    operand = ((operand << 8) | (&opcode)[5]);
    operand = ((operand << 8) | (&opcode)[4]);
    operand = ((operand << 8) | (&opcode)[3]);
    operand = ((operand << 8) | (&opcode)[2]);
    operand = ((operand << 8) | (&opcode)[1]);
    exec_i64(operand);
    emit_i64(operand);
    return 8;
}
                            

void Calculator::exec_i64(const int64_t operand)
{
    Operators::op_i64(_operands, operand)
}

void Calculator::emit_i64(const int64_t operand)
{
    BasicBlock bb;
    bb.begin(_hostcode.end());
    _hostcode.mov_rsi_imm64(operand);
    _hostcode.mov_rdi_imm64(reinterpret_cast<uintptr_t>(&operands));
    _hostcode.mov_rax_imm64(reinterpret_cast<uintptr_t>(&Operators::op_i64));
    _hostcode.call_rax();
    bb.end(_hostcode.end());
    _function.add(bb);
}
                            

LA DÉMO

for dummies

SANS EFFET BONALDI

POUR CONCLURE

for dummies

TO INFINITY AND BEYOND!

POUR ALLER PLUS LOIN

O'RLY Book

LE CODE

for dummies

Retrouvez le code sur mes miroirs
 
github.com/ponceto/rpn-calculator-with-jit
gitlab.com/ponceto/rpn-calculator-with-jit
bitbucket.org/ponceto/rpn-calculator-with-jit

Olivier Poncet

for dummies

@ponceto91 emaxilde.net
@ponceto91 github.com/ponceto/
@ponceto91 gitlab.com/ponceto/
@ponceto91 bitbucket.org/ponceto/