amd64ユーザーランド(とりあえず実行)
本投稿はnpt
に最小限のamd64コンパイラを実装できるか検討したときのメモです。
まあ結局npt
でamd64コンパイラは実装しないんですけど。
ユーザーランドとは、つまり一般ユーザーが作るプログラムのことです。
OSを作るわけじゃないので、カーネルモードは対象外になります。
条件は次の通り。
- CPUはamd64形式
- 64bitモードのみ
- ユーザーランドのみ
まず覚えなければいけないのは、機械語を実行して確認する方法です。
自分にとってはC言語から実行する方法が簡単だったので、その方法を示します。
方法はOS依存なので、下記の環境を対象に考えます。
機械語とは、結局は単なるバイナリデータの集合です。
C言語で言うならば、unsigned char
の配列でも作って、
それを強制的に関数ポインタとみなして実行すればいいことになります。
例えばこんな感じ。
unsigned char data[] = { /* exit(9)を呼び出す例文です */ 0xB8, 0x01, 0x00, 0x00, 0x00, /* mov eax <- 1 */ 0xBF, 0x09, 0x00, 0x00, 0x00, /* mov edi <- 9 */ 0xCD, 0x80 /* int 0x80 */ }; int main() { void (*call)(void); call = (void (*)(void))data; call(); /* ★たぶんうまく行かない */ return 0; }
注釈に記載した通り、たぶんうまく行きません。
FreeBSDで実行した結果が下記の通り。
$ cc main.c $ ./a.out Segmentation fault (core dumped)
大昔のOSだったらうまく行ったことでしょう。
しかしセキュリティが大切だともてはやされた結果、
メモリに対して実行可能な許可が無いとOSレベルでエラーになってしまいます。
実行許可があるメモリを用意する方法は大きく2つあり、
すでに確保されたメモリに実行許可を与える方法と新しく確保する方法です。
どちらの方法でも、mmap
関数か、VirtualAlloc
関数を使います。
今回は新規にメモリを確保することにします。
malloc
/free
のかわりに、mmap
/munmap
を使う方法を示します。
ではC言語にて下記の命令を実行することを考えます。
ptr = malloc(size);
これを実行可能なメモリ領域の確保に変えると次のようになります。
ptr = mmap(0, size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_SHARED, -1, 0);
ptr = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
ptr
が指しているメモリ空間は、関数と同様に実行可能な領域となります。
それでは、簡単な例として下記のコードを自力で機械語に翻訳してみます。
int testcode(int a, int b) { return a + b; }
関数testcode
は、単純に二つの引数を足すだけです。
面倒なことにOSによって機械語の翻訳結果が異なります。
unsigned char code[] = { 0x89, 0xF8, /* mov eax <- edi */ 0x01, 0xF0, /* add eax <- esi */ 0xC3 /* ret */ };
unsigned char code[] = { 0x89, 0xC8, /* mov eax <- ecx */ 0x01, 0xD0, /* add eax <- edx */ 0xC3 /* ret */ };
どうして内容が異なるのでしょうか?
それは「呼出規約」というものが異なるからです。
呼出規約とは、関数の呼び出しと返却をどのようにして行うかという、
関数間のインターフェイスのことです。
例えば、第一引数はスタックに積むのか、あるいはレジスタに格納するのか、
格納するならどういう順番で何を使うかなどを規定したものです。
呼出規約は、CPU、OS、言語の種類ごとに異なります。
例えばCPUのSPARCとIntelでは違っていますし、同じIntelでもi386とx86-64(amd64)で違います。
FreeBSDとLinuxは同じ場合も違う場合もあります。
FreeBSDとWindowsはどうも絶対に異なっているようです。
つまりコンパイラを作るためには、次の2つを勉強する必要があるという事です。
一気に説明することはできないので次の投稿になるとは思うのですが、 とりあえずは例で挙げた機械語のコードを実行するためのソースを示します。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/mman.h> unsigned char code[] = { 0x89, 0xF8, /* mov eax <- edi */ 0x01, 0xF0, /* add eax <- esi */ 0xC3 /* ret */ }; void execute(void *ptr) { int (*proc)(int, int); int result; memcpy(ptr, code, 5); proc = (int (*)(int, int))ptr; result = proc(30, 10); printf("%d\n", result); } int main() { void *ptr; size_t size; size = 1024; ptr = mmap(0, size, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_SHARED, -1, 0); if (ptr == MAP_FAILED) { perror("mmap"); return 1; } execute(ptr); if (munmap(ptr, size)) { perror("munmap"); return 1; } return 0; }
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <string.h> unsigned char code[] = { 0x89, 0xC8, /* mov eax <- ecx */ 0x01, 0xD0, /* add eax <- edx */ 0xC3 /* ret */ }; void execute(void *ptr) { int (*proc)(int, int); int result; memcpy(ptr, code, 5); proc = (int (*)(int, int))ptr; result = proc(30, 10); printf("%d\n", result); } int main() { void *ptr; size_t size; size = 1024; ptr = VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (ptr == NULL) { fprintf(stderr, "VirutalAlloc error\n"); return 1; } execute(ptr); if (! VirtualFree(ptr, 0, MEM_RELEASE)) { fprintf(stderr, "VirutalFree error\n"); return 1; } return 0; }
実行結果は、どちらも40
が出力されます。
これで思い通りのバイトコードを実行できるようになりました。