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であり,そのkcode_len <= k * page_sizeとなる最小のkである.つまり,要求サイズcode_lenpage_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)で実行権限を与えている.
  • あとは普通に関数ポインタとしてヒープ領域をキャストしてやれば呼び出すことができる.

という流れである.

これはPOSIXAPIを用いているので,LinuxBSDmacOSなどで動くはずで,実際,macOSLinuxでは動作を確認している. Windowsでもおそらく似たような感じで(メモリ確保のところと,mprotectで権限を与えるところをWindowsAPIに書き直せば良さそう)動くはずである.(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記事を書いたけど,この調子で,自作LispFFIを実装した話とか,自作VMについての話なども書いていきたい...