JITコンパイル時の関数呼び出しの扱い方

x86_64での関数呼び出し

x86_64(以下x64)ではcall命令の呼び出し関数の指定を相対アドレスで行うため、JITコンパイルをする際はそのアドレスの取り扱いに苦労します。

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

int main(void) {
    const char code[] = {
        /*
         * int f(void) {
         *     return add(2, 3);
         * }
         */
        0xbe, 0x03, 0x00, 0x00, 0x00, // mov esi, 3
        0xbf, 0x02, 0x00, 0x00, 0x00, // mov edi, 2
        0xe8, 0x01, 0x00, 0x00, 0x00, // call +1 # (相対アドレスでaddを指す)
        0xc3,                         // ret
        
        /*
         * int add(int x, int y) {
         *     return x + y;
         * }
         */
        0x89, 0xf8, // mov eax, edi
        0x01, 0xf0, // add eax, esi
        0xc3,       // ret
    };
    
    const size_t size = sizeof(code);
    
    // 実行可能なメモリ領域を確保する
    void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
    if (ptr == MAP_FAILED) {
        return 1;
    }
    
    memcpy(ptr, code, size);
    
    // 機械語を呼び出す
    int (*f)(void) = ptr;
    printf("%d\n", f());  // 5
    
    // 確保した領域を解放する
    munmap(ptr, size);

    return 0;
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

上の例では、相対アドレス+0x00000001を指定して、その直後に定義される関数addを呼び出しています。

0xe8, 0x01, 0x00, 0x00, 0x00, // call +1 # (相対アドレスでaddを指す)

呼び出す関数がこのように同一のコードセグメントに含まれる場合はこのように指定すればいいのですが、 標準ライブラリの関数やコンパイラ側で定義した関数の呼び出しを行う場合は呼び出しアドレス指定の取り扱いに困ります。

// C言語側で定義された関数
int sub(int a, int b) {
    return a - b;
}

//------------------------

0xe8, 0x??, 0x??, 0x??, 0x??, // call sub (subの相対オフセットがわからない)

レジスタを経由して絶対アドレス指定で関数を呼び出す

このような場合は、レジスタ経由で呼び出すと絶対アドレス指定ができるため、取り扱いが簡単になります。

mov rax, <subの絶対アドレス>
call rax

これをコードにすると次のようになります。

int sub(int a, int b) {
    return a - b;
}

//-----------------------

char code[] = {
    /*
        * int f(void) {
        *     return sub(2, 3);
        * }
        */
    0xbe, 0x03, 0x00, 0x00, 0x00,       // mov esi, 3
    0xbf, 0x02, 0x00, 0x00, 0x00,       // mov edi, 2
    0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, // mov rax, sub
                0x00, 0x00, 0x00, 0x00, //
    0xff, 0xd0,                         // call rax
    0xc3,                               // ret
};

// subの絶対アドレスを指定する
code[0x0c] = (uintptr_t)sub >>  0;
code[0x0d] = (uintptr_t)sub >>  8;
code[0x0e] = (uintptr_t)sub >> 16;
code[0x0f] = (uintptr_t)sub >> 24;
code[0x10] = (uintptr_t)sub >> 32;
code[0x11] = (uintptr_t)sub >> 40;
code[0x12] = (uintptr_t)sub >> 48;
code[0x13] = (uintptr_t)sub >> 56;

全コード

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>

int sub(int a, int b) {
    return a - b;
}

int main(void) {
    char code[] = {
        /*
         * int f(void) {
         *     return sub(2, 3);
         * }
         */
        0xbe, 0x03, 0x00, 0x00, 0x00,       // mov esi, 3
        0xbf, 0x02, 0x00, 0x00, 0x00,       // mov edi, 2
        0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, // mov rax, sub
                    0x00, 0x00, 0x00, 0x00, //
        0xff, 0xd0,                         // call rax
        0xc3,                               // ret
    };

    code[0x0c] = (uintptr_t)sub >>  0;
    code[0x0d] = (uintptr_t)sub >>  8;
    code[0x0e] = (uintptr_t)sub >> 16;
    code[0x0f] = (uintptr_t)sub >> 24;
    code[0x10] = (uintptr_t)sub >> 32;
    code[0x11] = (uintptr_t)sub >> 40;
    code[0x12] = (uintptr_t)sub >> 48;
    code[0x13] = (uintptr_t)sub >> 56;

    const size_t size = sizeof(code);
    
    // 実行可能なメモリ領域を確保する
    void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
    if (ptr == MAP_FAILED) {
        return 1;
    }
    
    memcpy(ptr, code, size);
    
    // 機械語を呼び出す
    int (*f)(void) = ptr;
    printf("%d\n", f());  // -1
    
    // 確保した領域を解放する
    munmap(ptr, size);

    return 0;
}

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ