読者です 読者をやめる 読者になる 読者になる

ゆとりーなの日記

日記的な事を書いて行くと思はれる

函数ポインタを渡すコールバックにメンバ函数を渡す【黒魔術編】

函数ポインタを渡すコールバック的なサムシングにメンバ函数を渡すというよくあるネタです。例によって適当な領域を確保してそこにメンバ函数を呼ぶマシン語を書き込んでしまおうというあれです。
今回の環境は64bitはLinuxGCCで試しているので他の環境では多分動きません。というか試した環境でも結構適当なところがあるので完全かどうかは分かりませんので悪しからず。
取り敢えずマシン語がどんな感じになるのかを知りたいので次のコードのアセンブリを吐いてマシン語を表示します。

class hoge {
 public:
  int f(int i) {
    return i * i;
  }
};

hoge *hp = new hoge;

int f(int i) {
  return hp->f(i);
}

int main() {
  hp = new hoge();
  f(313);
  delete hp;
  return 0;
}

コマンドを忘れやすいのでメモしておきます(割と自分用)。

g++ -S -masm=intel test.cc
as -a test.s

で、出てきたアセンブリマシン語がこれです(該当個所のみ)。

  35              	_Z1fi:
  36              	.LFB1:
  37              		.cfi_startproc
  38 0000 55       		push	rbp
  39              		.cfi_def_cfa_offset 16
  40              		.cfi_offset 6, -16
  41 0001 4889E5   		mov	rbp, rsp
  42              		.cfi_def_cfa_register 6
  43 0004 4883EC10 		sub	rsp, 16
  44 0008 897DFC   		mov	DWORD PTR [rbp-4], edi
  45 000b 488B0500 		mov	rax, QWORD PTR hp[rip]
  45      000000
  46 0012 8B55FC   		mov	edx, DWORD PTR [rbp-4]
  47 0015 89D6     		mov	esi, edx
  48 0017 4889C7   		mov	rdi, rax
  49 001a E8000000 		call	_ZN4hoge1fEi
  49      00
  50 001f C9       		leave
  51              		.cfi_def_cfa 7, 8
  52 0020 C3       		ret
  53              		.cfi_endproc
  54              	.LFE1:

丁度クラスのインスタンスが格納されているポインタ変数のアドレスとメンバ函数ポインタの個所が00000000で埋まっているのでここにそれらを適切に書き込むことにして、後はコピペすることにします。そうして出来たクラスがこれです。キャストがいっぱいです。例によって黒魔術なのでご利用は慎重にということで。

#include <cstdint>
#include <cstring>
#include <sys/types.h>
#include <sys/mman.h>

template <typename Class>
struct mf_to_pf {
  explicit mf_to_pf(Class *c, int (Class::*mf)(int))
      : class_(static_cast<Class **>(
                   mmap(NULL,
                        sizeof(Class *),
                        PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_32BIT | MAP_ANONYMOUS,
                        -1,
                        0))),
        f_(reinterpret_cast<int (*)(int)>(
               mmap(NULL,
                    size_,
                    PROT_EXEC | PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_32BIT | MAP_ANONYMOUS,
                    -1,
                    0))) {
    const unsigned char mem[size_] = {
      0x55,                                // push rbp
      0x48, 0x89, 0xe5,                    // mov rbp, rsp
      0x48, 0x83, 0xec, 0x10,              // sub rsp, 16
      0x89, 0x7d, 0xfc,                    // mov DWORD PTR [rbp - 4], edi
      0x48, 0x8b, 0x5, 0x0, 0x0, 0x0, 0x0, // mov rax, QWORD  PTR instance[rip]
      0x8b, 0x55, 0xfc,                    // mov edx, DWORD PTR [rbp - 4]
      0x89, 0xd6,                          // mov esi, edx
      0x48, 0x89, 0xc7,                    // mov rdi, rax
      0xe8, 0x0, 0x0, 0x0, 0x0,            // call member_function
      0xc9,                                // leave
      0xc3                                 // ret
    };
    *class_ = c; 
    std::memcpy(reinterpret_cast<void *>(f_), mem, size_);
    const auto class_rip
        = static_cast<std::int32_t>(
              reinterpret_cast<std::intptr_t>(class_) 
                  - (reinterpret_cast<std::intptr_t>(f_) + 18));
    std::memcpy(reinterpret_cast<char *>(f_) + 14,
                &class_rip,
                sizeof(class_rip));
    const auto mf_rip
        = static_cast<std::int32_t>(
               reinterpret_cast<std::intptr_t>(
                   *reinterpret_cast<void **>(&mf))
                       - (reinterpret_cast<std::intptr_t>(f_) + 31));
    std::memcpy(reinterpret_cast<char *>(f_) + 27, 
                &mf_rip,
                sizeof(mf_rip));
  }
  
  ~mf_to_pf() {
    munmap(reinterpret_cast<void *>(f_), size_);
    munmap(static_cast<void *>(class_), sizeof(Class *));
  }
  
  int (*ptr_fun() const)(int) {
    return f_;
  }
  
 private:
  Class **class_;
  int (*f_)(int);
  static constexpr std::size_t size_ = 33;
};

Linuxでは自前で確保する領域はmmapを使うっぽいです。これで実行可能な領域を確保出来ます。配列に参考にしたマシン語を書き込み確保した領域にmemcpy、その後00000000の部分に適切な値を書き込みます。インスタンスのアドレスを保持している変数のアドレスの指定とメンバ函数のアドレスの指定は呼び出しアドレスとの差分を指定する感じなので色々計算しているのはそこらへんのあたりの話です。
で、64bitだとアドレスは8バイトありますが参考にしたマシン語では4バイトしか指定する部分がないのでmmapで確保する領域で4バイトまでの領域しか確保しないように指定しています。インスタンス変数のアドレスを保持する変数もmmapで確保しているのはそういう理由だったりします。
取り敢えず次のコードでは動きました。

struct hoge {
  int f(int i) {
    std::cout << "value = " << i << std::endl;  
    return 8000;
  }
};

int caller(int (*f)(int)) {
  return (*f)(313);
}

int main() {
  hoge h;
  mf_to_pf<hoge> f(&h, &hoge::f);
  std::cout << caller(f.ptr_fun()) << std::endl;
  return 0;
}

実行結果

value = 313
8000

参考:サンクと継続の勉強会 完了報告 - 七誌の開発日記(旧)