だいぶ前(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 , 0x48 , 0x89 , 0xe5 , 0x48 , 0x89 , 0xf8 , 0x48 , 0x0f , 0xaf , 0xc7 , 0x48 , 0x89 , 0xec , 0x5d , 0xc3 }; 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

" ); 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

" ); break ; case EINVAL : fprintf( stderr , "ERROR - EINVAL

" ); break ; case ENOMEM : fprintf( stderr , "ERROR - ENOMEM

" ); break ; default : fprintf( stderr , "ERROR - UNKNOWN ERROR

" ); } exit( EXIT_FAILURE ); } int (*sq)( int ) = ( int (*)( int ))code; printf( " %d

" , sq( 12 )); return 0 ; }

上のコードをもとに説明していく． とりあえず，今回やりたいこととしては「ヒープ領域として確保したメモリに，機械語をつめていってそれを実行する」ということである． で，手順としては

実行したい機械語の命令数を code_len とする．

とする． ページサイズを page_size とする．

とする． ここで，確保すべきヒープのサイズは k * page_size であり，その k は code_len <= k * page_size となる最小のkである．つまり，要求サイズ code_len を page_size でアライメントする．

であり，その は となる最小のkである．つまり，要求サイズ を でアライメントする． で，これを楽にする方法として， 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

" , square( 12 )); return 0 ; }

実行は

$ nasm -f macho64 square.asm $ clang -o exec_square exec_square.c square.o

とすればよい．

久しぶりにBlog記事を書いたけど，この調子で，自作LispにFFIを実装した話とか，自作VMについての話なども書いていきたい．．．