koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

C言語でメモリ上のコードを実行する

(あまり低レイヤに詳しくない人間がこの記事を書いているので,信憑性については注意すること)

C言語といえば,自由度の高いプログラミングである. メモリ上に機械語を書いて,それを実行したいという欲求は多くあるだろう. 例えば,次のHello Worldプログラムなんかがそうだ.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>

static int stack[128 * 1024];
static unsigned char code[] = {
  0x53, 0x55, 0x41, 0x54, 0x48, 0x89, 0xfb, 0x48, 0x89, 0xf5, 0x49, 0x89, 0xd4, 0x41, 0x83, 0x04,
  0x24, 0x09, 0x41, 0x8b, 0x04, 0x24, 0x85, 0xc0, 0x0f, 0x84, 0x25, 0x00, 0x00, 0x00, 0x49, 0x83,
  0xc4, 0x04, 0x41, 0x83, 0x04, 0x24, 0x08, 0x49, 0x83, 0xc4, 0x04, 0x41, 0x83, 0x04, 0x24, 0x0b,
  0x49, 0x83, 0xc4, 0x04, 0x41, 0x83, 0x04, 0x24, 0x05, 0x49, 0x83, 0xec, 0x0c, 0x41, 0xff, 0x0c,
  0x24, 0xeb, 0xcf, 0x49, 0x83, 0xc4, 0x04, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x49, 0x83, 0xc4,
  0x04, 0x41, 0x83, 0x04, 0x24, 0x02, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x04, 0x24,
  0x07, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x04,
  0x24, 0x03, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x49, 0x83, 0xc4, 0x04, 0x41, 0xff, 0x0c, 0x24,
  0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x2c, 0x24, 0x0c, 0x49, 0x8b, 0x3c, 0x24, 0xff,
  0xd3, 0x49, 0x83, 0xec, 0x04, 0x41, 0x83, 0x04, 0x24, 0x08, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3,
  0x41, 0x83, 0x2c, 0x24, 0x08, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x04, 0x24, 0x03,
  0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x83, 0x2c, 0x24, 0x06, 0x49, 0x8b, 0x3c, 0x24, 0xff,
  0xd3, 0x41, 0x83, 0x2c, 0x24, 0x08, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x49, 0x83, 0xc4, 0x04,
  0x41, 0xff, 0x04, 0x24, 0x49, 0x8b, 0x3c, 0x24, 0xff, 0xd3, 0x41, 0x5c, 0x5d, 0x5b, 0xc3,
};

int
main(void)
{
  long page_size = sysconf(_SC_PAGESIZE) - 1;
  mprotect((void *) code, (sizeof(code) + page_size) & ~page_size, PROT_READ | PROT_EXEC);
  ((void (*)(int (*)(int), int (*)(), int *)) (unsigned char *) code)(putchar, getchar, stack);
  return EXIT_SUCCESS;
}

これは64bit上のLinuxならば,おそらく実行可能なコードであろう. コードの元ネタは,herumi/xbyakのサンプルコードのBrainfuck処理系:sample/bf.cppが,Hello Worldを出力するBrainfuckコード

+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.
------------.<++++++++.--------.+++.------.--------.>+.

を元に出力したC言語ソースコードである.

配列 code[]機械語であり,システムコールmprotect() を用いてこの領域に実行可能属性を付加している. 実行可能にしたメモリ領域の先頭を指すポインタを取得し,それを関数ポインタとしてコールすると,その領域の機械語を実行できるというカラクリのようだ.

mprotect()mmap() で確保した領域にのみにしか用いることができないが,staticなグローバル変数ならば問題はないらしい(このあたりについてのことは,僕の力量不足で調査できていない). また,僕の環境では,malloc() で確保した領域に mprotect() を用いたが,うまくいかなかった.

ところで,システムコールmprotect() は当然ながらWindows環境下で用いることはできない. Windowsで,ある領域に実行可能属性を付加するにはどうすればいいのだろうか? 調査したところ,Windows APIVirtualProtectmprotect() に相当するらしい. 前述のC言語ソースコードWindows(64bit)用に書き直すと,以下のようになる. もちろん,機械語部分はWindows用に書き直してある.

#include <stdio.h>
#include <stdlib.h>
#ifndef WIN32_LEAN_AND_MEAN
#  define WIN32_LEAN_AND_MEAN
#  define WIN32_LEAN_AND_MEAN_IS_NOT_DEFINED
#endif
#include <windows.h>
#ifdef WIN32_LEAN_AND_MEAN_IS_NOT_DEFINED
#  undef WIN32_LEAN_AND_MEAN_IS_NOT_DEFINED
#  undef WIN32_LEAN_AND_MEAN
#endif

static int stack[128 * 1024];
static unsigned char code[] = {
  0x56, 0x57, 0x55, 0x48, 0x89, 0xce, 0x48, 0x89, 0xd7, 0x4c, 0x89, 0xc5, 0x83, 0x45, 0x00, 0x09,
  0x8b, 0x45, 0x00, 0x85, 0xc0, 0x0f, 0x84, 0x21, 0x00, 0x00, 0x00, 0x48, 0x83, 0xc5, 0x04, 0x83,
  0x45, 0x00, 0x08, 0x48, 0x83, 0xc5, 0x04, 0x83, 0x45, 0x00, 0x0b, 0x48, 0x83, 0xc5, 0x04, 0x83,
  0x45, 0x00, 0x05, 0x48, 0x83, 0xed, 0x0c, 0xff, 0x4d, 0x00, 0xeb, 0xd4, 0x48, 0x83, 0xc5, 0x04,
  0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x83,
  0xc5, 0x04, 0x83, 0x45, 0x00, 0x02, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6,
  0x48, 0x83, 0xc4, 0x20, 0x83, 0x45, 0x00, 0x07, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20,
  0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6,
  0x48, 0x83, 0xc4, 0x20, 0x83, 0x45, 0x00, 0x03, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20,
  0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x83, 0xc5, 0x04, 0xff, 0x4d, 0x00, 0x48, 0x8b, 0x4d,
  0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x83, 0x6d, 0x00, 0x0c, 0x48,
  0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x83, 0xed,
  0x04, 0x83, 0x45, 0x00, 0x08, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48,
  0x83, 0xc4, 0x20, 0x83, 0x6d, 0x00, 0x08, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff,
  0xd6, 0x48, 0x83, 0xc4, 0x20, 0x83, 0x45, 0x00, 0x03, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec,
  0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x83, 0x6d, 0x00, 0x06, 0x48, 0x8b, 0x4d, 0x00, 0x48,
  0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x83, 0x6d, 0x00, 0x08, 0x48, 0x8b, 0x4d,
  0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20, 0x48, 0x83, 0xc5, 0x04, 0xff,
  0x45, 0x00, 0x48, 0x8b, 0x4d, 0x00, 0x48, 0x83, 0xec, 0x20, 0xff, 0xd6, 0x48, 0x83, 0xc4, 0x20,
  0x5d, 0x5f, 0x5e, 0xc3,
};

int
main(void)
{
  DWORD old_protect;
  VirtualProtect((LPVOID) code, sizeof(code), PAGE_EXECUTE_READWRITE, &old_protect);
  ((void (*)(int (*)(int), int (*)(), int *)) (unsigned char *) code)(putchar, getchar, stack);
  return EXIT_SUCCESS;
}

ちなみに,Cygwinだと mprotect() を利用可能であるが,使用しても意味がなかった. Cygwinでも VirtualProtect() を用いなければならない.

まとめ

mprotect() または VirtualProtect() を用いることによって,メモリのパーミッションを変更できる. 実行可能属性を付加することにより,動的に生成した機械語を実行することも可能である. これがJITコンパイルの基礎となる仕組みなのだろう.