「 データ解析 」一覧

Mach-OのFatバイナリ(Universal Binary) について


Universal Binaryというものがあります。これは1つのバイナリの中に2つ以上の実行体が格納されたものです。32bit版と64bit版のバイナリを1つのファイルで配布することも出来るようになるので便利なシロモノです。Windowsもこういったものを採用してほしかったと思います。

この形式の場合次のようなヘッダがファイル先頭に付きます。

構造としては fat_headerが出現し、nfat_archの個数分だけfat_arch構造体が後続します。fat_arch構造体にあるoffsetの位置からmach_headerやmach_header_64が出現します。

手元で試してみたところ、ファイルマジックは FAT_CIGAM のほうで出現し、この構造体の他のメンバについてはエンディアン反転する必要がありました。後続のmachヘッダ部分でのMH_MAGIC/MH_MAGIC_64についてはそのままだったのでひっくり返す必要はありませんでした。
この差がちょっと不思議な感じです。

今までのコードはマジック値をそのままチェックしていましたが、今回の件でわかるようにエンディアンが逆の場合でも読めるように直さないといけないなと感じました。


Mach-O セグメントとセクション補足


dysymtab_commandの中身を調べていたのですが、よくわからない感じだったので後回しにすることにしました。今回はセグメントやセクションの中身の方を調べていきたいとおもいます。

現在すでにわかっているセグメント&セクションの情報はこんな感じでした。

  [ 0] : LC_SEGMENT_64
    __PAGEZERO
    vmaddr=0x0000000000000000, vmsize=0x0000000100000000
    fileoff=0x0000000000000000, filesize=0x0000000000000000
    maxprot=0x00000000, initprot=0x00000000
    nSect(s)=0
  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
      section[0] : __TEXT [__text]
      section[1] : __TEXT [__stubs]
      section[2] : __TEXT [__stub_helper]
      section[3] : __TEXT [__cstring]
      section[4] : __TEXT [__unwind_info]
      section[5] : __TEXT [__eh_frame]
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
      section[0] : __DATA [__nl_symbol_ptr]
      section[1] : __DATA [__la_symbol_ptr]
  [ 3] : LC_SEGMENT_64
    __LINKEDIT
    vmaddr=0x0000000100002000, vmsize=0x0000000000001000
    fileoff=0x0000000000002000, filesize=0x0000000000000130
    maxprot=0x00000007, initprot=0x00000001
    nSect(s)=0

セグメントについて

今回の例ではセグメントとして以下のものが含まれていました。

  • __PAGEZERO
  • __TEXT
  • __DATA
  • __LINKEDIT

__TEXTセグメントは実行コードと読み込み専用のデータが配置される領域です。__DATAセグメントは読み書き可能なデータが配置される領域です。
一方初めて見るような他のセグメントを調べてみると次のようになっていました。__PAGEZEROセグメントはメモリ1ページを使用し、仮想メモリのゼロ番地となるように配置されるセグメントです。__LINKEDITセグメントはシンボル、文字列、リロケーションテーブルなどがダイナミックリンカが使用する情報が配置されるセグメントです。

セクションについて

今回の例では以下のセクションが含まれていました。

  • __text
  • __stubs
  • __stub_helper
  • __cstring
  • __unwind_info
  • __eh_frame
  • __nl_symbol_ptr
  • __la_symbol_ptr

__text は実行可能なプログラムコードが格納されているセクションです。
__cstringは文字定数が格納されているセクションです。

__nl_symbol_ptrはNon-lazy symbol pointersの意らしく、__la_symbol_ptrはLazy symbol pointers の意らしいです。従来は __IMPORTセグメント__jump_tableというものがあったようですが、最近は__la_symbol_ptrに変化しているとかいう話です。

今回のサンプルのバイナリデータでは含まれておらず表示されませんでしたが、初期化済みデータが格納される__dataセクションや、未初期化のスタティック変数領域としての__bssセクションといったものもMach-Oバイナリの中に存在します。

セクションの情報の中にはそのセクションの種別&属性を設定しているフラグが存在します。これを表示するようにしてみたものが以下となります。

  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
      section[0] : __TEXT [__text]
                 : addr=0x100000f10, offset=3856, size=45
                 : align=16, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=80000400 > S_REGULAR S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS
      section[1] : __TEXT [__stubs]
                 : addr=0x100000f3e, offset=3902, size=6
                 : align=2, reloff=0, nreloc=0
                 : resv0=0, resv1=6, resv2=0
                 : flags=80000408 > S_SYMBOL_STUBS S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS
      section[2] : __TEXT [__stub_helper]
                 : addr=0x100000f44, offset=3908, size=26
                 : align=4, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=80000400 > S_REGULAR S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS
      section[3] : __TEXT [__cstring]
                 : addr=0x100000f5e, offset=3934, size=13
                 : align=1, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=00000002 > S_CSTRING_LITERALS
      section[4] : __TEXT [__unwind_info]
                 : addr=0x100000f6b, offset=3947, size=72
                 : align=1, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=00000000 > S_REGULAR
      section[5] : __TEXT [__eh_frame]
                 : addr=0x100000fb8, offset=4024, size=64
                 : align=8, reloff=0, nreloc=0
                 : resv0=0, resv1=0, resv2=0
                 : flags=00000000 > S_REGULAR
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
      section[0] : __DATA [__nl_symbol_ptr]
                 : addr=0x100001000, offset=4096, size=16
                 : align=8, reloff=0, nreloc=0
                 : resv0=1, resv1=0, resv2=0
                 : flags=00000006 > S_NON_LAZY_SYMBOL_POINTERS
      section[1] : __DATA [__la_symbol_ptr]
                 : addr=0x100001010, offset=4112, size=8
                 : align=8, reloff=0, nreloc=0
                 : resv0=3, resv1=0, resv2=0
                 : flags=00000007 > S_LAZY_SYMBOL_POINTERS

Mach-O 実行体のロードコマンドの情報を表示してみる


前回は概要とセクション情報表示程度だったので、今回はもうすこしロードコマンドの表示をすすめて見ようと思います。

ロードコマンドの種別によって、そのコマンドのサイズと型が決まります。
あとはこれに応じてメンバの表示を行ってみたものが下記の結果です。
ロードコマンドの型の準備と表示が面倒なだけなので、プログラムコードは割愛します。

Mach-O 64bit
  CPU TYPE      : x86_x64
  CPU SUB TYPE  : i386 ALL(Lib64)
  FileType      : MH_EXECUTE
  Cmd(s)        : 16
  CmdSize       : 1296
  Flag(s)       : MH_NOUNDEFS MH_TWOLEVEL MH_PIE (200085)
Load Command(s)
  [ 0] : LC_SEGMENT_64
    __PAGEZERO
    vmaddr=0x0000000000000000, vmsize=0x0000000100000000
    fileoff=0x0000000000000000, filesize=0x0000000000000000
    maxprot=0x00000000, initprot=0x00000000
    nSect(s)=0
  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
      section[0] : __TEXT [__text]
      section[1] : __TEXT [__stubs]
      section[2] : __TEXT [__stub_helper]
      section[3] : __TEXT [__cstring]
      section[4] : __TEXT [__unwind_info]
      section[5] : __TEXT [__eh_frame]
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
      section[0] : __DATA [__nl_symbol_ptr]
      section[1] : __DATA [__la_symbol_ptr]
  [ 3] : LC_SEGMENT_64
    __LINKEDIT
    vmaddr=0x0000000100002000, vmsize=0x0000000000001000
    fileoff=0x0000000000002000, filesize=0x0000000000000130
    maxprot=0x00000007, initprot=0x00000001
    nSect(s)=0
  [ 4] : LC_DYLD_INFO_ONLY
    rebaseOff=8192
    rebaseSize=8
    bindOff=8200
    bindSize=24
    weakBindOff=0
    weakBindSize=0
    lazyBindOff=8224
    lazyBindSize=16
    exportOff=8240
    exportSize=48
  [ 5] : LC_SYMTAB
    symOff=8360
    nSyms=4
    strOff=8440
    strSize=56
  [ 6] : LC_DYSYMTAB
    ilocalSym=0
    nLocalSym=0
    iExtdefSym=0
    nExtdefSym=2
    iUndefSym=2
    nUndefSym=2
    tocOff=0
    nToc=0
    modtabOff=0
    nModtab=0
    extrefsymOff=0
    nExtrefSyms=0
    indirectSymOff=8424
    nIndirectSyms=4
    extrelOff=0
    nExtrel=0
    locrelOff=0
    nlocrel=0
  [ 7] : LC_LOAD_DYLINKER
    name: /usr/lib/dyld
  [ 8] : LC_UUID
    uuid: 69016DA8-F21836CE-98122CF8-DDD270E2
  [ 9] : LC_VERSION_MIN_MACOSX
    version = 10.8.0
  [10] : LC_SOURCE_VERSION
    version=0.0.0.0
  [11] : LC_MAIN
    entryOff=3856
    stackSize=0
  [12] : LC_LOAD_DYLIB
    /usr/lib/libSystem.B.dylib
  [13] : LC_FUNCTION_STARTS
    dataOff=8288
    dataSize=8
  [14] : LC_DATA_IN_CODE
    dataOff=8296
    dataSize=0
  [15] : LC_DYLIB_CODE_SIGN_DRS
    dataOff=8296
    dataSize=64

セグメントやセクション以外にも色々と興味深いものがあります。
”otool -L”コマンドで表示される依存関係は LC_LOAD_DYLIB の情報を表示していることがわかります(予想できます)。上記の物は若干otoolの結果と違うので見直してみます。

LC_LOAD_DYLIBのロードコマンド構造体の中身は以下のようになっています。上記では手抜き?してnameだけ表示していました。
これをcurrent_versionやcompatibility_versionについても表示してみます。

これによりバージョンの情報が出るようになって、より結果が期待するものに近づきました。

  [12] : LC_LOAD_DYLIB
    /usr/lib/libSystem.B.dylib (compatibility ver 1.0.0, current ver 169.3.0)

シンボルテーブルの中身表示

まずはLC_SYMTABの中身を表示してみます。このロードコマンドの構造体は以下のようになっています。

symOffやstrOffは先頭からのオフセットが入っています。そして nSyms がシンボル数を示しています。
symOffの位置からシンボルの情報が配列として格納されています。この配列要素の構造が nlist.h, stab.h にあるらしいです。Windowsなのでこの型を自前で定義すると以下のようになりました。

そしてファイルの先頭からのオフセットを足してシンボルテーブルやストリングテーブルの位置を確定させ、シンボル名を表示する部分は以下のようになってます。

これによりサンプルのプログラムでは以下のように表示されました。

---- Symbol table(s) ----
    __mh_execute_header
    _main
    _printf
    dyld_stub_binder

ここまでの関係を図示するとこんな感じになります。

macho-format-symtab

シンボル(関数とか)の位置を表示してみる

ここまででシンボルの名前情報は取得できるようになったので、次にそのシンボルがどこにあるのかを調べてみます。

シンボルに関しては nlist構造体で情報が定義されています。

nSectメンバはそのシンボルがあるセクションのインデックスです。nDescについてはシンボルの参照に関する追加情報といったところです。
大事なのは nTypeメンバで、この値によって、nValueの値が意味する内容が変化します。そしてこのnTypeが取り得る値は以下の項目のOR結合となっているようです。

これらの情報を解釈して、もうすこしシンボルに関して情報を出すようにしてみました。シンボルが示す関数が自身のものの場合にはその仮想アドレスを表示するようにしています。

64bit版も同様に解釈部分を実装して表示させると以下のようにシンボルの情報が表示されます。

Symbol table(s)
        __mh_execute_header vaddr=0x100000000   (defined at sect1)  [Public]
        _main vaddr=0x100000f10   (defined at sect1)  [Public]
        _printf  (undefined.)
        dyld_stub_binder  (undefined.)

まとめ

ロードコマンドの中身を表示することができるようになりました。そしてその際にシンボルテーブルが格納されていることがわかったので、シンボルテーブルの中身を解釈して、シンボル名やアドレス情報を確認することができるようになりました。


Mach-O形式の調査~導入編~


PEフォーマットは以前調査したので、今度はApple製品で採用されているMach-O(マークオー)形式の調査をしてみたいと思います。まずは最小サンプルの実行体を用意して既存ツールでちょっと確認してみます。

このようなサンプルを準備して、依存する他のライブラリの情報がどうなっているかを確認します。

$ otool -L sample01.out
sample01.out:
  /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)

$ otool -L sample02.out 
sample02.out:
  /usr/lib/libstdc++.6.dylib (compatibility version 7.0.0, current version 56.0.0)
  /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 169.3.0)

Mach-O形式は Appleのサイトに仕様がきちんと公開されているのでこれを見ればよいです(Mac OS X ABI Mach-O File Format Reference)。
この情報を元に自分のプログラムコードで各情報を取得&表示ということをしばらくはやってみたいと思います。

Mach-O概要

Mach-O(マーク・オー, Mach object)はMacで採用されているアプリバイナリインターフェース(ABI)です。ファイルの構造としては先頭にヘッダ構造を持ち、ロードコマンド、データ(各セグメントやセクション情報)、と記録されています。

ヘッダ部分では、マジックナンバーと対象とするアーキテクチャ情報、後続のデータに関する個数などの情報を記録しています。
ロードコマンド部分では、後続するデータ部分の位置・構造についての情報を保持しています。
データ部分には、セグメントが記録され、この中にセクション情報が複数個入っています。セグメントはアプリケーションをロードする際に仮想メモリにどのように領域をマッピングするかといった情報を定義しています。ページ属性もここに含まれます。

先頭のヘッダ部分についての構造は以下となります。32bit,64bitで最初から構造体が違うので注意が必要です。

これらの構造体は loader.h にて定義されています。
この cputypeメンバが CPU_TYPE_I386 や CPU_TYPE_x86_64 は Intel Macを示すとのことです。そもそも 32bit/64bitでは magicメンバがすでに違うようで、MH_MAGIC(0xfeedface) と MH_MAGIC_64(0xfeedfacf) とマジックが異なっていました。

この後続くロードコマンド部分は load_command構造体を先頭とするデータブロックの配列として記録されています。

このcmdメンバの値に応じて後続するデータの意味が変わり、ブロックのサイズとしては cmdSize が示す物となっています。cmdの指す内容は、 LC_SEGMENT, LC_SEGMENT_64, LC_SYMTAB,… と多岐にわたります。これについては公式リファレンスの Table 4 を参照して下さい。

mach_headerの部分を読み込んで表示してみるプログラムを作りました。
実行するとこのようになります。

Mach-O 64bit
  CPU TYPE      : x86_x64
  CPU SUB TYPE  : i386 ALL(Lib64)
  FileType      : MH_EXECUTE
  Cmd(s)        : 16
  CmdSize       : 1296
  Flag(s)       : MH_NOUNDEFS MH_TWOLEVEL MH_PIE (200085)

このプログラムはWindows(VisualStudio2012)で作成したので、定義を自前で行っているためコードが長めです。Mac環境ならば、loader.hやmach/machine.h のヘッダインクルードですむところが多いです。

このヘッダの後にはロードコマンドが続きます。とりあえずロードコマンドの中身のうち、後続のデータであるセクションに関する物(LC_SEGMENT,LC_SEGMENT_64)だけ表示して構造を追いかけてみます。

プログラムコードは以下のようになります。

実行結果

Load Command(s)
  [ 0] : LC_SEGMENT_64
    __PAGEZERO
    vmaddr=0x0000000000000000, vmsize=0x0000000100000000
    fileoff=0x0000000000000000, filesize=0x0000000000000000
    maxprot=0x00000000, initprot=0x00000000
    nSect(s)=0
  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
  [ 3] : LC_SEGMENT_64
    __LINKEDIT
    vmaddr=0x0000000100002000, vmsize=0x0000000000001000
    fileoff=0x0000000000002000, filesize=0x0000000000000130
    maxprot=0x00000007, initprot=0x00000001
    nSect(s)=0
  [ 4] : 0x80000022, 48
  [ 5] : 0x2, 24
  [ 6] : 0xb, 80
  [ 7] : 0xe, 32
  [ 8] : 0x1b, 24
  [ 9] : 0x24, 16
  [10] : 0x2a, 16
  [11] : 0x80000028, 24
  [12] : 0xc, 56
  [13] : 0x26, 16
  [14] : 0x29, 16
  [15] : 0x2b, 16

表示させてみてわかったことは LoadCommandのセグメント種が出現した後には、さらにセクション用のデータが続く場合があるということです。これも解釈して表示するようにしてみたところ以下の結果となりました。

Load Command(s)
  [ 0] : LC_SEGMENT_64
    __PAGEZERO
    vmaddr=0x0000000000000000, vmsize=0x0000000100000000
    fileoff=0x0000000000000000, filesize=0x0000000000000000
    maxprot=0x00000000, initprot=0x00000000
    nSect(s)=0
  [ 1] : LC_SEGMENT_64
    __TEXT
    vmaddr=0x0000000100000000, vmsize=0x0000000000001000
    fileoff=0x0000000000000000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000005
    nSect(s)=6
      section[0] : __TEXT [__text]
      section[1] : __TEXT [__stubs]
      section[2] : __TEXT [__stub_helper]
      section[3] : __TEXT [__cstring]
      section[4] : __TEXT [__unwind_info]
      section[5] : __TEXT [__eh_frame]
  [ 2] : LC_SEGMENT_64
    __DATA
    vmaddr=0x0000000100001000, vmsize=0x0000000000001000
    fileoff=0x0000000000001000, filesize=0x0000000000001000
    maxprot=0x00000007, initprot=0x00000003
    nSect(s)=2
      section[0] : __DATA [__nl_symbol_ptr]
      section[1] : __DATA [__la_symbol_ptr]
  [ 3] : LC_SEGMENT_64
    __LINKEDIT
    vmaddr=0x0000000100002000, vmsize=0x0000000000001000
    fileoff=0x0000000000002000, filesize=0x0000000000000130
    maxprot=0x00000007, initprot=0x00000001
    nSect(s)=0

このセクション用のデータとは以下の構造体です.
2014/05現在、Appleのリファレンスに記載してある section_64構造体が間違っています。下記のものが正しいです(ヘッダでもこうなっているので、ドキュメントのミスでしょう)

まとめ

PEフォーマットと比べるとシンプルな構造のように思えます。ここまでの関連性について図示してみるとこうなりました。ロードコマンドの部分もいわゆるチャンク構造のようになっています。このMach-o形式は実行体だけでなく中間形式でも使われているようです。

macho-format


PEフォーマットについて~インポート関数の列挙~


今回は依存するDLLの名前だけでなく関数の情報を取得する部分までやってみたいと思います。前回でインポートDLLの名前までは取得できたので、IMAGE_IMPORT_DESCRIPTORの情報まではたどり着けた前提で話を進めていきます。

IMAGE_IMPORT_DESCRIPTOR構造体は以下のようなものです。

インポート関数の情報は、この構造体のFirstThunkメンバが示す情報(IATテーブル)とOriginalFirstThunkメンバが示す情報(関数名のテーブル情報)にアクセスすることで取得できます。これらのデータはRVA(Relative Virtual Address)で記録されています。

これらの関係を図示すると以下のようになります。RVAをちゃんとしたアドレスとして計算して辿っていくと、関数名を記録している領域や、関数のアドレスを格納する領域へたどり着くことが出来ます。

pe-format-import-func-list

これらの情報をアクセスしてみたコードが以下のようになります。

実行結果は以下のようになります。前回同様適当な自作DLLを読み込ませてみたときの結果です。

DLL Dependency
KERNEL32.dll
  0x7530B24F : OutputDebugStringA
  0x752E3450 : GetModuleHandleW
  0x752E48F0 : GetModuleFileNameW
  0x752E3468 : FreeLibrary
  0x752E43FA : VirtualQuery
  0x752E14B9 : GetProcessHeap
  0x752E1499 : HeapFree
  0x779AE046 : HeapAlloc
  0x752FEE78 : GetTickCount64
  0x752E34A9 : GetSystemTimeAsFileTime
  0x752E1420 : GetCurrentThreadId
  0x752E16F5 : QueryPerformanceCounter
  0x752E16DD : WideCharToMultiByte
  0x752E18FE : MultiByteToWideChar
  0x752E48CB : LoadLibraryW
  0x752E59EB : lstrlenA
  0x752E48FD : LoadLibraryExW
  0x752E1222 : GetProcAddress
  0x752E11C0 : GetLastError
  0x752E5846 : RaiseException
  0x752E49FD : IsDebuggerPresent
  0x779B9DD5 : DecodePointer
  0x779C107B : EncodePointer
  0x752E51D5 : IsProcessorFeaturePresent
MSVCR110D.dll
  0x0FE18EF0 : __crtUnhandledException
  0x0FE224A0 : _crt_debugger_hook
  0x0FE27360 : _wsplitpath_s
  0x0FE25AB0 : _wmakepath_s
  0x0FD65EE0 : wcscpy_s
  0x0FE22700 : _except_handler4_common
  0x0FDFC2B0 : __clean_type_info_names_internal
  0x0FE15220 : _onexit
  0x0FE15090 : __dllonexit
  0x0FE17310 : _calloc_dbg
  0x0FE13710 : _CrtDbgReportW
  0x0FD3AD30 : _unlock
  0x0FD3AA90 : _lock
  0x0FD39D50 : _initterm_e
  0x0FD39D20 : _initterm
  0x0FE16740 : _CrtSetCheckCount
  0x0FE17540 : _free_dbg
  0x0FE17E80 : _malloc_dbg
  0x0FD399A0 : _amsg_exit
  0x0FE1C170 : __CppXcptFilter
  0x0FE20880 : _CRT_RTC_INITW
  0x0FE18ED0 : __crtTerminateProcess

まとめ

PEヘッダの情報から、依存するDLLやその関数の情報までデータを辿ることができました。これでPEヘッダ編もひとまずは終わりと出来そうです。RVAによる参照ってよくわからないなとかいくつもの構造体を辿って目的の情報にアクセスするといった面倒さにも慣れてきて、当初よりは楽に読めるようになってきたかなと思います。


PEフォーマットについて~DLLのExport/Importを列挙~


前回PEフォーマットについて概要を説明したので、今回はその情報にアクセスしてDLLのExport/Importしている情報を列挙してみようと思います。

まずは、IMAGE_DATA_DIRECTORY配列にアクセスしてIMAGE_EXPORT_DIRECTORYや IMAGE_IMPORT_DESCRIPTOR配列の位置を求める必要があります。

DLLがExportしている関数一覧

IMAGE_EXPORT_DIRECTORY配列は以下のような構造体です.

この構造体の情報から、関数名や関数のアドレス情報といったものにアクセスすることが出来ます。AddressOf*** というメンバがRVA単位で各データへのポインタを持っています。これらの関係性を図示すると以下のようになります。

pe-format-export-list

これらの情報からDLLがエクスポートしている関数の一覧を出すコードは以下のようになります。下記のコードではLoadLibrary関数が返すモジュールハンドルがDLLのベースアドレスとなる点を利用してデータ構造にアクセスしています。ファイルから処理する場合には、この部分をファイルから読み込んだデータのバッファ先頭とすればよいです。

実行結果はこんな感じになります。

IMAGE_EXPORT_DIRECTORY
 Name=KERNEL32.dll
 Base=00000001
 NumberOfFunctions = 1364
 NumberOfNames = 1364
 AddressOfFunctions = RVA:000BFA28 (7538FA28)
 AddressOfNames     = RVA:000C0F78 (75390F78)
 AddressOfNameOrdinals = RVA:000C24C8
ExportFunctions
  0 [00013358] AcquireSRWLockExclusive 
  1 [000C9E34] AcquireSRWLockShared 
  2 [000C98B5] ActivateActCtx 
  3 [000C98D6] AddAtomA 

   (省略)

  1359 [000218CA] lstrcpynA 
  1360 [0003D4F6] lstrcpynW 
  1361 [000159EB] lstrlen 
  1362 [000159EB] lstrlenA 
  1363 [000116D0] lstrlenW 

DLLがImportしているDLL一覧

今度は逆にDLLが依存している他のDLLへの情報についてリストアップしてみます。使用する構造体は IMAGE_IMPORT_DESCRIPTOR です。これもまたOptionalHeaderから辿ることが出来ますが、プログラム上ではimagehlpのユーティリティを使って取得してしまいます。この構造体は以下のような形をしています。この構造体が複数個繋がって、依存するDLLの情報を表現しています。

pe-format-import-list

個数が明示的に記録されているのではなく、Characteristicsメンバが0となるとき終端を意味する構造となっています。この構造体のNameメンバがRVA単位で依存するDLLの名前を示しています。
これにアクセスしてリストアップするコードを以下に示します。

実行結果はこのようになります。自作の適当なDLLを読み込ませてみました(しかもデバッグ版)。単純に加算関数しか実装していないので最低限度の依存になっています。

DLL Dependency
KERNEL32.dll
MSVCR110D.dll

さらに依存する関数そのものにアクセスする場合には、FirstThunkメンバを参照して関数のリストを取得します。DLLエクスポート転送が使用される場合には ForwarderChainメンバが有効な値になるようですが、実際にはどうなるか今のところ確認していません。今後の課題ということで今回は保留にしておきます。


PEフォーマットについて~構造概要~


Windowsの実行形式であるexeファイルは Portable Executable Format という形式で記録されています。このフォーマットについて調べてみたのでわかったことを書いておこうと思います。

まずPEフォーマットは MS-DOS用のヘッダとWindows用のヘッダから構成されます。Windowsアプリケーションではこの中のWindows用ヘッダでである IMAGE_NT_HEADERS 以降の情報を使用してアプリケーションを起動しています。

その後には各セクションの情報が格納されていたり、IMAGE_DATA_DIRECTORYで示されるデータ群で構成されています。これらのデータへのアクセスはファイル先頭からのオフセット、メモリ上ではベースアドレスからのオフセット値(相対値)として記録されています。このオフセット値はRVA(RelativeVirtualAddress)と呼ばれます。

これらの情報のつながりを図示してみると以下のような感じになります。気になるような部分だけ矢印でつないでみました。各ブロックは独立していて RVAが各ヘッダ内メンバに格納されているので、そこから他のブロックへつなぐことが出来ます。

pe-format

注意する点は IMAGE_DOS_HEADER構造体のサイズ分だけ読み進めるとNTヘッダが必ずしも出てくるわけではないようです。この構造体メンバである e_lfanew に読み進めるサイズが書いてあるので、これを参照してIMAGE_NT_HEADERまでのオフセットを取得します。
 またIMAGE_NT_HEADERS構造体は 32bit と 64bit のものでサイズが異なるのでこの点にも注意が必要です。自身も32bitで読み込んだ対象も32bitのものならば問題になりませんが、自身と対象のビット数が異なる場合にはそれに合わせる必要があるので気をつけましょう。

最後に意外な点は、IMAGE_NT_HEADERS構造体の中にある IMAGE_OPTIONAL_HEADER構造体についてです。これはオプショナルという名前がついているにもかかわらず必須の構造体で、IMAGE_DATA_DIRECTORY構造体の配列を保持しています。この情報が重要で依存するDLLの関数情報や自身がエクスポートする関数の情報といったもののデータを記録しています。


MMDの再生、物理演算いれてみた


MMDのデータの再生を相変わらず今更ですががんばって実装していました。
今回は、IKと物理演算について実装を進めてました。前の実装を一度捨ててます…。
そのため、表情モーフも無効となってしまっている状態ですが、今回の物は以下の点を盛り込んでみました。

  • DirectXの算術(D3DX)を用いたり、Bullet が持っている算術を使用したり切り替えられること
  • Bulletのバージョンに縛られないこと。

これらのメリットを説明しますと、扱っている行列並びや演算順序の部分での余計なトラブルを避けたいため、まずはBulletの算術に統一して扱おう!という考えによるものです。
また、Bulletのバージョンが変わっても(ある程度は)大丈夫というのも、MMDが当初 2.75らしく、それが今のBullet最新では2.81だそうで。なるべく追従していきたいと考えているからです。特に 2.81 ではSIMD最適化も入っているらしくここに実装を変えられるという点はかなり大事だと思っています。

今回は初めて、YouTubeに録画した動画をあげてみました。でもってこれをこのブログに貼ってみるという試みにチャレンジもしてみます。

画質の問題とキャプチャ速度の問題からツールを変えて再キャプチャしたものに差し替えました。 (5/20)
まずは、Bulletの算術を使って 2.81 を用いて再生したものです。

上記をベースにして、DirectXの算術で実装してみた物です。

※ 実時間による補正をいれてないため、使用している演算の速度でこんなに変わってます。DXのほうが速い。

今度はモーションを変えて、Bulletのバージョン違いを試してみました。どちらもBulletの算術使ってます.

こちらが 2.81 を使ってみたものです。

そしてこれが 2.79 を使ってみたものです。

感想など

今までは 2.79で試行錯誤した時間が長かったので一番最後のが安定して動く気はしてます。
ただ問題なさそうな範囲で 2.81 も動いているように見えるので、今回の目標は達成できたかなと思っています。

ちなみにこれらはDirectXの算術をつかっているにもかかわらず全てOpenGLの簡単な描画でやっています。
最後の状態だけ見るとかなりちゃんぽんしてる状態なので、覚えているうちにコードをリファクタリング&コメント付けしておこうと思います。なお、扱っている座標系は全て右手系にしています。これをさらに左手系に対応させるとなるとさらに倍のコード分岐が必要になると思うのでこれにはもう絶望ですね。

というか、Bulletを左手系でうまく駆動させる自信がないです・・・。


続MMD(PMD,VMD)をロードして描画してみた


前回からまだ不足していた物理演算部分を実装してみましたが、どうにもうまく再現できない状況が続いています。
今まではデータが左手系で格納されているために右手系に変換してロード&描画していましたが、物理演算においてはこのあたりの変換で失敗しているのかもしれません。一度右手系で無変換の方法でやってみたほうがいいのかな・・・。

世間からはかなり遅れて始めたMMDロードでこんなに苦労するとは予想外です。

ちなみに実装というか物理演算のタイプはここを参考にしました。
http://d.hatena.ne.jp/ousttrue/20100424/1272126923


MMD(PMD,VMD)をロードして描画してみた


ネット上の先人の情報からMMDのデータ(PMDとVMD)のデータをロードして、DirectX11を使って描画してみることに成功しました。
色々と厄介な点もありましたが、今のところなんとか動いているようです。

基本形状を出すまでは結構早くに実現できたのですが、モーションデータの再生にとても時間を食われました。
特に、足の部分のIK処理と表情についてのモーフィング処理、ですね。

足のIKについては、色々な人が既に試しているなかで、CCDによるIKをこちらも実装してみました。
若干足の曲げ具合や、パキパキと動いてしまう点がちょっとオリジナル挙動とかけ離れてしまうのですが、もう妥協してしまいました。

表情についても、全ての頂点データをCPUで処理して毎フレーム頂点バッファに転送!という方式だったらすごく楽なのですが、
有限のマトリックスパレットを想定してボーン分割処理が入っていたり、全頂点データは毎回送りたくないので、影響範囲だけにしたいなぁという思惑からちょっと厄介な感じになっていました。

↑この部分についてですが、
面データを走査して、表情モーフィングに関係する面かどうかで振り分け、その後マテリアルごとのデータに振り分け、念のためボーン分割処理を適用して、1回の描画データを構築しています。
この1回の描画データごとにとりあえずは頂点バッファを作成していますが、モーフ時にはここにCPU上で計算した表情データを注入しています。

MikuMikuDance付属のミクのデータと、http://ch.nicovideo.jp/azup で配布されていた、GO MY WAY!! のデータを、自分のプログラムで再生してみるとこんな感じに仕上がりました。

これから先は、物理の対応を入れて、スカートや髪の毛をヒラヒラでしょうか。

感想としては、毎回全頂点データを送るDirectX9世代のDrawPrimitiveUP を使うならば、MMDのデータって非常に楽に描画までたどり着けるようになっているなと思いました。ここを頂点バッファを使い出したり、ボーンを分割処理なんてことをやっていたりすると難しくなっていくように思いました。元のデータにある程度の想定をおけるなら、こんなややこしいことをしないに限りますね。