Mach-O Inside: 命令列工具集 otool objdump od 與 dwarfdump

2023-10-29 21:01:31

1 otool

otool 命令列工具用來檢視 Mach-O 檔案的結構。

1.1 檢視檔案頭

otool -h -v 檔案路徑

-h選項表明檢視 Mach-O 檔案頭。

-v 選項表明將展示的內容進行"符號化"處理。

上面命令列輸出的一個例子如下:

magic  cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64    ARM64        ALL  0x00     EXECUTE    23       3752   NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE

從輸出結果可以看出,完全符合 XNU 核心標頭檔案loader.h中定義的struct mach_header_64的結構:

struct mach_header_64 {
	uint32_t	magic;		/* mach magic number identifier */
	cpu_type_t	cputype;	/* cpu specifier */
	cpu_subtype_t	cpusubtype;	/* machine specifier */
	uint32_t	filetype;	/* type of file */
	uint32_t	ncmds;		/* number of load commands */
	uint32_t	sizeofcmds;	/* the size of all the load commands */
	uint32_t	flags;		/* flags */
	uint32_t	reserved;	/* reserved */
};

如果命令列沒有加 -v選項,輸出的結果會是這樣:

 magic  cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedfacf 16777228          0  0x00           2    23       3752 0x00218085

可以看到,使用-v 選項輸出結果會更讓人易讀。

如果一個 Mach-O 檔案是 Universal 型別的,也就是包含多重架構(一般 .o 目標檔案或者靜態庫會是 Universal 的,包含 ARM 和 X86_64 兩種架構),使用-h選項只會輸出其中一種架構的頭資訊。

想要將所有架構頭資訊都輸出,需要使用-f選項:

otool -f -v 檔案路徑

上面命令列輸出的一個例子是:

Fat headers
fat_magic FAT_MAGIC
nfat_arch 2
architecture x86_64
    cputype CPU_TYPE_X86_64
    cpusubtype CPU_SUBTYPE_X86_64_ALL
    capabilities 0x0
    offset 48
    size 36752
    align 2^3 (8)
architecture arm64
    cputype CPU_TYPE_ARM64
    cpusubtype CPU_SUBTYPE_ARM64_ALL
    capabilities 0x0
    offset 36800
    size 37384
    align 2^3 (8)

1.2 檢視組合程式碼

otool -t -v 檔案路徑

-t選項會展示 (__TEXT, __text) Section 的內容。

-v選項會反組合展示的內容,展示組合程式碼,前提是待展示的內容是程式碼指令。

上面命令列輸出的一個例子是:

(__TEXT,__text) section
0000000100004000        sub     sp, sp, #0x30
0000000100004004        stp     x20, x19, [sp, #0x10]
0000000100004008        stp     x29, x30, [sp, #0x20]
000000010000400c        add     x29, sp, #0x20
0000000100004010        adrp    x8, 12 ; 0x100010000
0000000100004014        ldr     x9, [x8]
0000000100004018        add     x9, x9, #0x1
000000010000401c        str     x9, [x8]
0000000100004020        add     x19, x3, x2
0000000100004024        str     x19, [sp]
0000000100004028        adrp    x0, 8 ; 0x10000c000
000000010000402c        add     x0, x0, #0x1f0 ; Objc cfstring ref: @"bad cfstring ref"
0000000100004030        bl      0x100007c68 ; symbol stub for: _NSLog
0000000100004034        mov     x0, x19
0000000100004038        ldp     x29, x30, [sp, #0x20]
000000010000403c        ldp     x20, x19, [sp, #0x10]
0000000100004040        add     sp, sp, #0x30
0000000100004044        ret

上面組合程式碼最左邊的列是指令的地址。

上面命令列會將 (__TEXT, __text) Section 全部反組合,如果內容較長不容易檢視,因此最好結合more命令:

otool -t -v 檔案路徑 | more

如果想展示組合程式碼的同時,展示組合指令的編碼以及指令的偏移量,可以結合-j-function_offsets選項:

otool -t -v -j -function_offsets 檔案路徑

-j選項展示組合指令的編碼。

-function_offsets選項展示指令的偏移量。

上面命令列輸出的一個例子是:

(__TEXT,__text) section
    +0 0000000100004000 d100c3ff        sub     sp, sp, #0x30
    +4 0000000100004004 a9014ff4        stp     x20, x19, [sp, #0x10]
    +8 0000000100004008 a9027bfd        stp     x29, x30, [sp, #0x20]
   +12 000000010000400c 910083fd        add     x29, sp, #0x20
   +16 0000000100004010 90000068        adrp    x8, 12 ; 0x100010000
   +20 0000000100004014 f9400109        ldr     x9, [x8]
   +24 0000000100004018 91000529        add     x9, x9, #0x1
   +28 000000010000401c f9000109        str     x9, [x8]
   +32 0000000100004020 8b020073        add     x19, x3, x2
   +36 0000000100004024 f90003f3        str     x19, [sp]
   +40 0000000100004028 90000040        adrp    x0, 8 ; 0x10000c000
   +44 000000010000402c 9107c000        add     x0, x0, #0x1f0 ; Objc cfstring ref: @"bad cfstring ref"
   +48 0000000100004030 94000f0e        bl      0x100007c68 ; symbol stub for: _NSLog
   +52 0000000100004034 aa1303e0        mov     x0, x19
   +56 0000000100004038 a9427bfd        ldp     x29, x30, [sp, #0x20]
   +60 000000010000403c a9414ff4        ldp     x20, x19, [sp, #0x10]
   +64 0000000100004040 9100c3ff        add     sp, sp, #0x30
   +68 0000000100004044 d65f03c0        ret

上面輸出從左起第 1 列是組合指令偏移量,第 2 列是指令地址,第 3 列是指令的編碼。

1.3 檢視不同架構的組合程式碼

對於 Universal 型別的 Mach-O 檔案,可以使用-arch選項檢視指定架構的組合程式碼,命令如下:

otool -t -v -arch arm64 檔案路徑
otool -t -v -arch x86_x64 檔案路徑

-arch選項指定架構,arm64 表示 64bit 的 ARM CPU 架構,x86_64 表示 64bit 的 Intel x86_64 CPU 架構。

-arch選項的值除了可以是 arm64 和 x86_64,其他的值可以通過下面命令列的輸出檢視:

man 3 arch

上面命令列輸出的一部分結果如下所示:

The currently known architectures are:

     Name          CPU Type              CPU Subtype                 Description
     x86_64        CPU_TYPE_X86_64       CPU_SUBTYPE_X86_64_ALL      Intel x86-64
     i386          CPU_TYPE_I386         CPU_SUBTYPE_I386_ALL        Intel 80x86
     arm           CPU_TYPE_ARM          CPU_SUBTYPE_ARM_ALL         ARM
     arm64         CPU_TYPE_ARM64        CPU_SUBTYPE_ARM64_ALL       ARM64
     arm64e        CPU_TYPE_ARM64        CPU_SUBTYPE_ARM64E          ARM64E
     arm64_32      CPU_TYPE_ARM64_32     CPU_SUBTYPE_ARM64_32_V8     ARM64_32
     ppc           CPU_TYPE_POWERPC      CPU_SUBTYPE_POWERPC_ALL     PowerPC

1.4 按符號檢視組合程式碼

如果 Mach-O 檔案是 DEBUG 型別,或者雖然是 Relese 型別但是沒有設定 Xcode 的如下 Build Setting:

那麼就可以使用-p選項使用符號檢視組合程式碼。

otool -t -v -p "-[X addi:d:]" 檔案路徑

上面命令列會從符號-[X addi:d:]開始進行反組合,一直展示到 (__TEXT, __text) Section 末尾,輸出內容如下:

(__TEXT,__text) section
-[X addi:d:]:
0000000100004134        sub     sp, sp, #0x30
0000000100004138        str     x0, [sp, #0x28]
000000010000413c        str     x1, [sp, #0x20]
0000000100004140        str     x2, [sp, #0x18]
0000000100004144        str     d0, [sp, #0x10]
0000000100004148        adrp    x9, 12 ; 0x100010000
000000010000414c        ldr     x8, [x9, #0x18]
0000000100004150        add     x8, x8, #0x1
0000000100004154        str     x8, [x9, #0x18]
0000000100004158        ldr     d0, [sp, #0x18]
000000010000415c        scvtf   d0, d0
0000000100004160        ldr     d1, [sp, #0x10]
0000000100004164        fadd    d0, d0, d1
0000000100004168        str     d0, [sp, #0x8]
000000010000416c        ldr     d0, [sp, #0x8]
0000000100004170        add     sp, sp, #0x30
0000000100004174        ret

1.5 檢視指定 Section

otool 可以通過-s選項檢視指定的 Section,-s選項的值形式為-s segmentName sectionName:

otool -v -s __TEXT __text 檔案路徑

上面命令列檢視 (__TEXT, __text) Section 的內容

otool -s __DATA __data 檔案路徑

上面命令列檢視 (__DATA, __data) Section 的內容。

1.6 檢視間接符號表

間接符號表是程式載入是動態聯結器 dyld 會使用到符號表,動態連結器使用這個符號表來完成符號的繫結。otool 可以使用-I選項來檢視間接符號表:

otool -I -v 檔案路徑

-I選項表明檢視間接符號表。

-v選項會讓展示的結果更詳細,更易讀。

上面命令列輸出的一個例子是:

Indirect symbols for (__TEXT,__stubs) 50 entries
address            index name
0x0000000100007c68     4 _NSLog
0x0000000100007c74     5 _NSStringFromClass
0x0000000100007c80    13 _UIApplicationMain
0x0000000100007c8c    15 ___error
0x0000000100007c98    16 ___stack_chk_fail
0x0000000100007ca4    20 _atexit
0x0000000100007cb0    21 _atoi

上面輸出從左起address代表符號的地址,index代表該符號在符號表裡的索引(符號表裡有所有符號),name是符號名。

1.7 檢視 Load Commands

otool 可以使用-l選項檢視 Mach-O 中的 Load Commands:

otool -l 檔案路徑

1.8 檢視依賴的動態庫

otool 可以使用-L選項檢視當前 Mach-O 依賴的動態庫:

otool -L 檔案路徑

一個輸出例子如下:

/System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 2048.1.101)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1336.0.0)
/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation (compatibility version 150.0.0, current version 2048.1.101)
/System/Library/Frameworks/UIKit.framework/UIKit (compatibility version 1.0.0, current version 7082.1.111)

更多 otool 功能可以參考man otool的輸出內容。

2 objdump

由於歷史原因,蘋果使用曾經使用 objdump 展示 Mach-O 資訊,因此 objdump 與 otool 許多功能類似。

2.1 檢視檔案頭

objdump -m --private-header 檔案路徑

-m選項表明是檢視 Mach-O 檔案。

--private-header表明檢視 Mach-O 檔案頭。

這個命令類似 otool 的otool -h -v

如果想要檢視 Univeral 型別 Mach-O 的檔案頭,可以使用:

objdump -m --universal-headers 檔案路徑

-m選項表明是檢視 Mach-O 檔案。

--universal-headers表明顯示 Mach-O 檔案中所有架構的檔案頭。

這個命令類似 otool 的otool -f -v

2.2 檢視組合程式碼

objdump 支援按照指定的地址範圍顯示組合程式碼,這個功能在實際中很有用處:

objdump -d --start-address=起始地址 --stop-address=結束地址 檔案路徑

-d選項表明要執行反組合。

--start-address選項是反組合的起始地址。

--stop-address選項是反組合的結束地址,需要注意的是,反組合內容不包含 --stop-address 指定的地址,也就是說反組合的範圍是[--start-address, --stop-address)。

上面命令列輸出的一個例子如下:

Disassembly of section __TEXT,__text:

0000000100004000 <__text>:
100004000: d100c3ff    	sub	sp, sp, #48
100004004: a9014ff4    	stp	x20, x19, [sp, #16]
100004008: a9027bfd    	stp	x29, x30, [sp, #32]
10000400c: 910083fd    	add	x29, sp, #32
100004010: 90000068    	adrp	x8, 0x100010000 <__text+0x40>
100004014: f9400109    	ldr	x9, [x8]
100004018: 91000529    	add	x9, x9, #1
10000401c: f9000109    	str	x9, [x8]

上面輸出從左起第 1 列是組合指令的地址,第 2 類是指令的編碼。和 otool 不同,objdump 預設就顯示組合指令的編碼。如果不想顯示組合指令編碼,加入--no-show-raw-insn選項即可。

objdump 不支援展示組合程式碼時,顯示偏移量。

如果反組合時只有-d選項,那麼 objdump 會反組合所有可執行的程式碼。

2.3 檢視不同結構的組合程式碼

objdump 可以使用--arch選項,選擇不同的架構進行反組合:

objdump -d --arch=arm64 檔案路徑
objdump -d --arch=x86_64 檔案路徑

--arch選項指明 CPU 架構,架構名和 otool 一樣,可以使用man 3 arch檢視。

這個功能類似 otool 的otool -arch

2.4 按符號檢視組合程式碼

和 otool 類似,objdump 支援按照符號進行反組合:

objdump --disassemble-symbols="符號1","符號2"... 檔案路徑

--disassemble-symbols選項可以有多個符號,符號之間使用逗號間隔。

這個功能類似 otool 的otool -p

2.5 反組合時加入原始碼行號資訊

如果有 DSYM 檔案,那麼使用 objdump 進行反組合時,可以顯示原始碼行號資訊:

objdump -l --dsym=DSYM檔案路徑 檔案路徑

-l選項表明反組合時展示原始碼行號資訊。

--dsym選項指定 DSYM 檔案路徑。需要注意的是,需要在 DSYM 檔案上右鍵'Show Package Contents',然後依次進入 Contents->Resources->DWARF 目錄,然後給目錄裡面的檔案新增.app字尾,否則會報如下錯誤:

上面命令列輸出的一個例子如下圖所示:

2.6 檢視指定的 Section

objdump 通過-j選項檢視指定的 Section:

objdump -m -j segmentName,sectionName 檔案路徑

-m選項表明檢視 Mach-O 檔案。

-j選項指定要檢視的 Section,比如 __TEXT,__text __DATA,__data。

這個命令類似 otool 的otool -s

2.7 檢視間接符號表

objdump 可以檢視間接符號表:

objdump -m --indirect-symbols 檔案路徑

-m選項表明檢視 Mach-O 檔案
--indirect-symbols選項表明檢視間接符號表。

這個命令類似 otool 的otool -Iv,輸出的格式也類似:

Indirect symbols for (__TEXT,__stubs) 50 entries
address            index name
0x0000000100007c68     4 _NSLog
0x0000000100007c74     5 _NSStringFromClass
0x0000000100007c80    13 _UIApplicationMain
0x0000000100007c8c    15 ___error
0x0000000100007c98    16 ___stack_chk_fail
0x0000000100007ca4    20 _atexit

2.8 檢視符號表

objdump 可以直接檢視整個符號表:

objdump -t 檔案路徑

其輸出格式如下:

SYMBOL TABLE:
0000000005614542      d  *UND* .hidden radr://5614542
000000010000d878  w    O __DATA,__data ___llvm_profile_filename
000000010000d880  w    O __DATA,__data ___llvm_profile_raw_version
0000000100000000 g     F __TEXT,__text __mh_execute_header
0000000000000000         *UND* _NSLog
0000000000000000         *UND* _NSStringFromClass
0000000000000000         *UND* _OBJC_CLASS_$_NSObject
0000000000000000         *UND* _OBJC_CLASS_$_UIResponder
0000000000000000  w      *UND* _OBJC_CLASS_$_UISceneConfiguration
0000000000000000         *UND* _OBJC_CLASS_$_UIViewController
0000000000000000         *UND* _OBJC_METACLASS_$_NSObject
0000000000000000         *UND* _OBJC_METACLASS_$_UIResponder
0000000000000000         *UND* _OBJC_METACLASS_$_UIViewController
0000000000000000         *UND* _UIApplicationMain
0000000000000000         *UND* ___CFConstantStringClassReference
0000000000000000         *UND* ___error
0000000000000000         *UND* ___stack_chk_fail
0000000000000000         *UND* ___stack_chk_guard
0000000000000000         *UND* ___stderrp
0000000000000000         *UND* __objc_empty_cache
0000000000000000         *UND* _atexit
0000000000000000         *UND* _atoi

從輸出上可以看出來,按照間接符號表的index列可以對應到符號表裡。比如 __NSLog在間接符號表的index 為 5,符號表的第 5 項(符號表索引以 0 開始),正好是__NSLog

2.9 檢視 Load Commands

objdump 可以檢視 Load Commands:

objdump -m -x 檔案路徑

-m表明要檢視 Mach-O 檔案。

-x表明展示 Load Commands。

這個命令類似 otool 的otool -l

2.10 檢視依賴的動態庫

objdump 可以檢視依賴的動態:

objdump -m --dylibs-used 檔案路徑

-m選項表明要檢視 Mach-O 檔案。

--dylibs-used選項表明檢視依賴的動態庫。

這個命令類似 otool 的otool -L

2.11 Demangle Symbol

原始碼中的符號在編譯之後,會被編譯器混淆:

objdump 支援將混淆的符號還原:

objdump -C 檔案路徑

-C選項表明要還原混淆的符號。

上圖中混淆的符號還原之後:

更多 objdump 功能可以參考man objdump的輸出內容。

3 od

od 命令可以用來檢視二進位制的內容,可以用它來檢視 Mach-O 檔案的原始位元組。

3.1 指定展示格式

od -A x -t x 檔案路徑

-A選項表明地址是用 10(d) 進位制、8(o) 進位制、16(x) 進位制展示,預設使用 8 進位制展示。

-t選項表明用哪種進位制展示內容,可以是 10(d) 進位制、8(o) 進位制、16(x) 進位制,預設是 8 進位制。

上面命令列輸出的一個例子是:

上圖從左邊起第 1 列就是地址或者說位元組的偏移量更準確,其他列都是 Mach-O 檔案的內容,可以看到第 2 列第 1 行 feedfacf 正式 Mach-O 裡面的魔數。

選項-t 值後面還可以接一個 10 進位制數,表示按多少個位元組一組展示內容,比如-t x1就表示一個位元組一個位元組的展示檔案內容:

od -A x -t x1 檔案路徑

上面命令列輸出的例子如下:

從上圖紅框可以看到,Mach-O 檔案是小端在前(Little-Endian)位元組序。

3.2 跳過部分位元組

od 支援跳過指定的位元組數然後展示:

od -A x -t x -j 0x4000 檔案路徑

-j選項表明跳過多少位元組,上面例子中要跳過 0x4000 個位元組。

更多 od 功能可以參考man od的輸出內容。

4 dwarfdump

dwarfdump 用來檢視 DWARF 檔案內容。

4.1 檢視 UUID

dwarfdump 可以檢視任何一個 Mach-O 檔案的 UUID,這在分析崩潰堆疊時十分有用:

dwarfdump --uuid 檔案路徑

4.2 查詢地址對應的符號資訊

dwarfdump 可以查詢地址對應的符號資訊:

dwarfdump --lookup=地址 檔案路徑

--lookup選項後面是要查詢的符號地址。

上面命令列一個輸出的例子為:

當在分析崩潰資訊時,可以使用這個方法找到地址對應的函數名。

4.3 查詢符號名對應的資訊

dwarfdump 可以查詢符號名對應的詳細資訊:

dwarfdump -f 符號名 檔案路徑

-f選項後面是要查詢的符號名,比如:

dwarfdump -f "-[X add:j:]" 檔案路徑

輸出的結果如下:

當在分析崩潰資訊時,可以用這個方法反查崩潰函數的地址,然後使用 objdump 工具檢視崩潰地址的具體組合程式碼。

更多 dwarfdump 功能可以參考man dwarfdump