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; }
上の例では、相対アドレス+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; }