C言語でヒープ領域にある機械語列に実行権限を与え,実行する方法.
だいぶ前(3ヶ月)に書こうと思っていた記事なのだが,ぼけーっとしていたら夏休みも終わり,そして夏休みが終わってからも1ヶ月がたっており,いい加減記事をかくか,とおもい書くことにした.
とりあえず,本文は続きから
とりあえず,まずコードを先に示す.
#include <stdio.h> #include <sys/mman.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <stdint.h> #include <string.h> int main(int argc, const char *argv[]) { long page_size = sysconf(_SC_PAGESIZE); char _code[] = { 0x55, // push rbp 0x48, 0x89, 0xe5, // mov rbp, rsp 0x48, 0x89, 0xf8, // mov rax, rdi 0x48, 0x0f, 0xaf, 0xc7, // imul rax, rdi 0x48, 0x89, 0xec, // mov rsp, rbp 0x5d, // pop rbp 0xc3 // ret }; size_t code_len = sizeof(_code) / sizeof(_code[0]); char *code; if ((posix_memalign((void **)&code, page_size, code_len))) { fprintf(stderr, "ERROR - posix_memalign\n"); exit(EXIT_FAILURE); } memcpy(code, _code, code_len); if ((mprotect((void *)code, code_len, PROT_WRITE | PROT_EXEC)) == -1) { switch (errno) { case EACCES: fprintf(stderr, "ERROR - EACCES\n"); break; case EINVAL: fprintf(stderr, "ERROR - EINVAL\n"); break; case ENOMEM: fprintf(stderr, "ERROR - ENOMEM\n"); break; default: fprintf(stderr, "ERROR - UNKNOWN ERROR\n"); } exit(EXIT_FAILURE); } int (*sq)(int) = (int (*)(int))code; printf("%d\n", sq(12)); return 0; }
上のコードをもとに説明していく. とりあえず,今回やりたいこととしては「ヒープ領域として確保したメモリに,機械語をつめていってそれを実行する」ということである. で,手順としては
- 実行したい機械語の命令数を
code_len
とする. - ページサイズを
page_size
とする. - ここで,確保すべきヒープのサイズは
k * page_size
であり,そのk
はcode_len <= k * page_size
となる最小のkである.つまり,要求サイズcode_len
をpage_size
でアライメントする. - で,これを楽にする方法として,
posix_memalign
という関数があり,これはint posix_memalign(void **memptr, size_t alignment, size_t size);
というシグネチャであるから,posix_mamalign(void **)&code, page_size, code_len)
を実行すれば良い. - ここまででヒープ領域の確保ができる.
- つづいて,上の例ではスタック領域に
_code
として機械語列が格納されているので,今確保した領域code
に_code
の内容をmemcpy
する. - これでヒープ領域に命令列を格納することが出来た.
- ここで,そのヒープ領域を実行したいが,確保したヒープ領域には実行可能権限がない.よってそれを
mprotect
を用いて実行可能権限を設定する. - 具体的には,
mprotect((void *)code, code_len, PROT_WRITE | PROT_EXEC)
で実行権限を与えている. - あとは普通に関数ポインタとしてヒープ領域をキャストしてやれば呼び出すことができる.
という流れである.
これはPOSIXのAPIを用いているので,Linux,BSD,macOSなどで動くはずで,実際,macOSとLinuxでは動作を確認している. Windowsでもおそらく似たような感じで(メモリ確保のところと,mprotectで権限を与えるところをWindowsのAPIに書き直せば良さそう)動くはずである.(Windowsが手元にないのでテストしていないが)
また,動的(実行時)にヒープの命令を実行することができるのでJITなどが可能になるが,先程Twitterで言及している人もいたが,投機的実行やパイプラインの観点から おそらくこの方法はパフォーマンス的に大きなペナルティも存在するらしい.このことについてはまだ不勉強なのでよくわかっていないので勉強していきたい.
最後に,今回の埋め込んだ機械語は以下のNASMで書いたアセンブリをアセンブル,それをobjdumpで抽出したものである.
GLOBAL _square _square: push rbp mov rbp, rsp mov rax, rdi imul rax, rdi mov rsp, rbp pop rbp ret
また,上のアセンブリは次のようにしてC言語側から呼び出すこともできる.
#include <stdio.h> extern int square(int); int main(int argc, const char *argv[]) { printf("square(12) : %d\n", square(12)); return 0; }
実行は
$ nasm -f macho64 square.asm $ clang -o exec_square exec_square.c square.o
とすればよい.
久しぶりにBlog記事を書いたけど,この調子で,自作LispにFFIを実装した話とか,自作VMについての話なども書いていきたい...