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についても表示してみます。

struct dylib {
	union lc_str name;
	uint32_t timestamp;
	uint32_t current_version;
	uint32_t compatibility_version;
};
struct dylib_command {
	uint32_t cmd; // LC_ID_DYLIB, LC_LOAD_DYLIB, LC_LOAD_WEAK_DYLIB
	uint32_t cmdSize;
	struct dylib dylib;
};

void dispLCLoadDylib( dylib_command* lc ) {
	printf( "LC_LOAD_DYLIB\n" );
	printf( "    %s (compatibility ver %u.%u.%u, current ver %u.%u.%u)\n", 
		(char*)lc + lc->dylib.name.offset,
		(lc->dylib.compatibility_version >> 16) &0xFFu, 
		(lc->dylib.compatibility_version >> 8)&0xFFu,
		(lc->dylib.compatibility_version & 0xFFu),
		(lc->dylib.current_version >> 16) & 0xFFu,
		(lc->dylib.current_version >> 8) & 0xFFu,
		(lc->dylib.current_version & 0xFFu) );
}

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

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

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

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

struct symtab_commmand {
    uint32_t cmd; // LC_SYMTAB
    uint32_t cmdSize;
    uint32_t symOff;
    uint32_t nSyms;
    uint32_t strOff;
    uint32_t strSize;
};

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

struct nlist {
	union { uint32_t n_strx; } n_un;
	uint8_t nType;
	uint8_t nSect;
	uint16_t nDesc;
	uint32_t nValue;
};
struct nlist_64 {
	union { uint32_t n_strx; } n_un;
	uint8_t nType;
	uint8_t nSect;
	uint16_t nDesc;
	uint64_t nValue;
};

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

// 表示する部分
void dispSymbolTable(void* fileHead, load_command* cmdHead, int cmds) {
	printf( "---- Symbol table(s) ----\n" );
	nlist* tblSymbols = (nlist*)( (char*)fileHead + lc_symtab->symOff );
	char*  tblStrings = (char*)( (char*)fileHead + lc_symtab->strOff );
	int symbolCount = lc_symtab->nSyms;

	for( int i = 0; i < symbolCount; ++i ) {
		nlist* l = &(tblSymbols[i]);
		uint32_t pos = l->n_un.n_strx;
		char* symbolName = tblStrings + pos;
		printf( "      %s\n", symbolName );
	}
}

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

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

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

macho-format-symtab

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

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

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

struct nlist {
	union { uint32_t n_strx; } n_un;
	uint8_t nType;
	uint8_t nSect;
	uint16_t nDesc;
	uint32_t nValue;
};
struct nlist_64 {
	union { uint32_t n_strx; } n_un;
	uint8_t nType;
	uint8_t nSect;
	uint16_t nDesc;
	uint64_t nValue;
};

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

// nlistのnTypeメンバのフラグ値
#define NTYPE_FIELD_UNDF  (0x0)
#define NTYPE_FIELD_ABS   (0x02)
#define NTYPE_FIELD_SECT  (0x0e)
#define NTYPE_FIELD_PBUD  (0x0c)
#define NTYPE_FIELD_INDR  (0x0a)
// nlistのnTypeメンバ用マスク値
#define NTYPE_FIELD_STAB_MASK  (0xe0)
#define NTYPE_FIELD_PEXT_MASK  (0x10)
#define NTYPE_FIELD_EXT_MASK   (0x01)
#define NTYPE_FIELD_TYPE_MASK  (0x0e)

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

void dispSymbolTables( void* fileHead, load_command* cmdHead, int cmds ) {
	symtab_commmand* lc_symtab = findSymtabCommand( cmdHead, cmds );
	if( lc_symtab == NULL ) {
		return;
	}
	printf( "---- Symbol table(s) ----\n" );
	nlist* tblSymbols = (nlist*)( (char*)fileHead + lc_symtab->symOff );
	char*  tblStrings = (char*)( (char*)fileHead + lc_symtab->strOff );
	int symbolCount = lc_symtab->nSyms;

	for( int i = 0; i < symbolCount; ++i ) {
		nlist* l = &(tblSymbols[i]);
		uint32_t pos = l->n_un.n_strx;
		char* symbolName = tblStrings + pos;
		printf( "  " );
		printf( "      %s", symbolName );
		if( (l->nType & NTYPE_FIELD_TYPE_MASK) == NTYPE_FIELD_SECT ) {
			printf( " vaddr=0x%08x  ", l->nValue );
			printf( " (defined at sect%d) ", l->nSect );

			if( (l->nType & NTYPE_FIELD_EXT_MASK) != 0 ) {
				printf( " [Public] " );
			}
		}
		if( (l->nType & NTYPE_FIELD_TYPE_MASK) == NTYPE_FIELD_UNDF ) {
			printf( "  (undefined.) " ); // 別モジュールで定義されているシンボルで本モジュールからは参照している.
		}
		printf( "\n");
	}
}

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.)

まとめ

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

データ解析 プログラミング
すらりんをフォローする
すらりん日記

コメント

タイトルとURLをコピーしました