Apple 操作系統可執行文件 Mach-O
摘要:} // If main executable, add LC_LOAD_DYLINKER if (_file.fileType == llvm::MachO::MH_EXECUTE) { // Build LC_LOAD_DYLINKER load command. uint32_t size=pointerAlign(sizeof(dylinker_command)+dyldPath().size()+1)。符號綁定結果放到 LC_DYSYMTAB 指定的 section,解析後的地址會放到 DATA segment 的 nl_symbol_ptr 和 got 裏。
Mach-O 的全稱是 Mach Object File Format。可以是可執行文件,目標代碼或共享庫,動態庫。Mach 內核的操作系統比如 macOS,iPadOS 和 iOS 都是用的 Mach-O。Mach-O 包含程序的核心邏輯,以及入口點主要功能。
通過學習 Mach-O,可以瞭解應用程序是如何加載到系統的,如何執行的。還能瞭解符號查找,函數調用堆棧符號化等。更重要的是能夠了解如何設計數據結構,這對於日後開發生涯的收益是長期的。瞭解這些對於瞭解編譯和逆向工程都會有幫助,你還會了解到動態鏈接器的內部工作原理以及字節碼格式的信息,Leb128字節流,Mach 導出時 Trie 二進制 image 壓縮。
對於 Mach-O,你一定不陌生,但是對於它內部邏輯你一定會好奇,比如它是怎麼構建出來的,組織方式如何,怎麼加載的,如何工作,誰讓它工作的,怎樣導入和導出符號的。
接下來我們先看看怎麼構建一個 Mach-O 文件的吧。
構建
構建 Mach-O 文件,主要需要用到編譯器和靜態鏈接器,編譯器可以將編寫的高級語言代碼轉成中間目標文件,然後用靜態鏈接器把中間目標文件組合成 Mach-O。
編譯器驅動程序使用的是 clang,有編譯、組裝和鏈接的能力,調用 Xcode Tools 裏的其他工具來實現源碼到 Mach-O 文件生成。其他工具包括將彙編代碼創建爲中間目標文件的 as 彙編程序,組合中間目標文件成 Mach-O 文件的靜態鏈接器 ld,還有創建靜態庫或共享庫的 libtool。
構建成 Mach-O 包括中間對象文件、動態共享庫、框架、靜態庫、Bundle、內核擴展這幾種類型。其中框架會包含共享庫和圖片、文檔、接口等相關資源。
寫個 main.c 文件代碼:
#include<stdlib.h> int main(int argc, char *argv[]){ const char *name = argv[1]; printf("%s\n", name); return 0; }
通過 clang 構建成 Mach-O 文件 a.out。
xcrun clang main.c
如果有多個文件,先將多個文件生成中間目標文件,後綴是.o,使用 clang 的選項 -c。每個目標文件都是模塊。使用靜態鏈接器可以把多個模塊組合成一個動態共享庫。通過 ld 可以完成這個操作。使用 libtool 的選項 -static 可以構建靜態庫。
組合成動態庫可以使用 clang 的 -dynamiclib 選項,命令如下:
xcrun clang -dynamiclib command.c header.c -fvisibility=hidden -o mac.dylib
靜態鏈接就是把各個模塊組合成一個整體,生成新的 Mach-O,鏈接的內容就是把各個模塊間相互的引用能夠正確的鏈接好,原理就是把一些指令對其他符號的地址引用進行修正。過程包含地址和空間分配,符號解析和圍繞符號進行的重定位。核心是重定位,X86-64尋址方式是 RIP-relative 尋址,就是基於 RIP 來計算目標地址,通過 jumpq 跳轉目標地址,就是當前指令下一條指令地址來加偏移量。
構建完 Mach-O。那你一定好奇 Mach-O 裏面都有什麼呢?分析 Mach-O 的工具有分析體系結構的 lipo,顯式文件類型的 file,列 Data 內容的 otool,分析 image 每個邏輯信息符號的 pagestuff,符號表顯示的 nm。
組成
Mach-O 會將數據流分組,每組都會有自己的意義,主要分三大部分,分別是 Mach Header、Load Command、Data。
Header
Mach Header 裏會有 Mach-O 的 CPU 信息,以及 Load Command 的信息。可以使用 otool 查看內容:
otool -v -h a.out
結果如下:
Mach header magic cputype cpusubtype caps filetype ncmds sizeofcmds flags MH_MAGIC_64 X86_64 ALL 0x00 EXECUTE 16 1368 NOUNDEFS DYLDLINK TWOLEVEL PIE
通過 _dyld_get_image_header 函數可以獲取 mach_header 結構體。 GCDFetchFeed/SMCallStack.m at master · ming1016/GCDFetchFeed · GitHub 裏這段代碼裏有判斷 Mach Header 結構體魔數的函數 smCmdFirstPointerFromMachHeader,代碼如下:
uintptr_t smCmdFirstPointerFromMachHeader(const struct mach_header* const machHeader) { switch (machHeader->magic) { case MH_MAGIC: case MH_CIGAM: case MH_MAGIC_64: case MH_CIGAM_64: return (uintptr_t)(((machHeaderByCPU*)machHeader) + 1); default: return 0; // Header 不合法 } }
還有 Fat Header,裏面會包含多個架構的 Header。
LLVM 中生成 Mach Header 的代碼如下:
void MachOFileLayout::writeMachHeader() { auto cpusubtype = MachOLinkingContext::cpuSubtypeFromArch(_file.arch); // dynamic x86 executables on newer OS version should also set the // CPU_SUBTYPE_LIB64 mask in the CPU subtype. //FIXME:Check that this is a dynamic executable, not a static one. if (_file.fileType == llvm::MachO::MH_EXECUTE && cpusubtype == CPU_SUBTYPE_X86_64_ALL && _file.os == MachOLinkingContext::OS::macOSX) { uint32_t version; bool failed = MachOLinkingContext::parsePackedVersion("10.5", version); if (!failed && _file.minOSverson >= version) cpusubtype |= CPU_SUBTYPE_LIB64; } mach_header *mh = reinterpret_cast<mach_header*>(_buffer); mh->magic = _is64 ? llvm::MachO::MH_MAGIC_64 : llvm::MachO::MH_MAGIC; mh->cputype = MachOLinkingContext::cpuTypeFromArch(_file.arch); mh->cpusubtype = cpusubtype; mh->filetype = _file.fileType; mh->ncmds = _countOfLoadCommands; mh->sizeofcmds = _endOfLoadCommands - _startOfLoadCommands; mh->flags = _file.flags; if (_swap) swapStruct(*mh); }
Load Command
Load Command 包含 Mach-O 裏命令類型信息,名稱和二進制文件的位置。
使用 otool 命令可以查看詳細:
otool -v -l a.out
遍歷 Mach Header 裏的 ncmds 可以取到所有 Load Command。代碼如下:
for (uint32_t iCmd = 0; iCmd < machHeader->ncmds; iCmd++) { const struct load_command*loadCmd= (structload_command*)cmdPointer; }
load command 裏的 cmd 是以 LC 開頭定義的宏,可以參看 loader.h 裏的定義,有50多個,主要的是:
- LC_SEGMENT_64(_PAGEZERO)
- LC_SEGMENT_64(_TEXT)
- LC_SEGMENT_64(_DATA)
- LC_SEGMENT_64(_LINKEDIT)
- LC_DYLD_INFO_ONLY
- LC_SYMTAB
- LC_DYSYMTAB
- LC_LOAD_DYLINKER
- LC_UUID
- LC_BUILD_VERSION
- LC_SOURCE_VERSION
- LC_MAIN
- LC_LOAD_DYLIB(libSystem.B.dylib)
- LC_FUNCTION_STARTS
- LC_DATA_IN_CODE
每個 command 的結構都是獨立的,前兩個字段 cmd 和 cmdsize 是一樣的。
根據 Load Command 可以得到 Segment 的偏移量。
生成 Load Command 的代碼如下:
llvm::Error MachOFileLayout::writeLoadCommands() { uint8_t *lc = &_buffer[_startOfLoadCommands]; if (_file.fileType == llvm::MachO::MH_OBJECT) { // Object files have one unnamed segment which holds all sections. if (_is64) { if (auto ec = writeSingleSegmentLoadCommand<MachO64Trait>(lc)) return ec; } else { if (auto ec = writeSingleSegmentLoadCommand<MachO32Trait>(lc)) return ec; } // Add LC_SYMTAB with symbol table info symtab_command* st = reinterpret_cast<symtab_command*>(lc); st->cmd = LC_SYMTAB; st->cmdsize = sizeof(symtab_command); st->symoff = _startOfSymbols; st->nsyms = _file.stabsSymbols.size() + _file.localSymbols.size() + _file.globalSymbols.size() + _file.undefinedSymbols.size(); st->stroff = _startOfSymbolStrings; st->strsize = _endOfSymbolStrings - _startOfSymbolStrings; if (_swap) swapStruct(*st); lc += sizeof(symtab_command); // Add LC_VERSION_MIN_MACOSX, LC_VERSION_MIN_IPHONEOS, // LC_VERSION_MIN_WATCHOS, LC_VERSION_MIN_TVOS writeVersionMinLoadCommand(_file, _swap, lc); // Add LC_FUNCTION_STARTS if needed. if (_functionStartsSize != 0) { linkedit_data_command* dl = reinterpret_cast<linkedit_data_command*>(lc); dl->cmd = LC_FUNCTION_STARTS; dl->cmdsize = sizeof(linkedit_data_command); dl->dataoff = _startOfFunctionStarts; dl->datasize = _functionStartsSize; if (_swap) swapStruct(*dl); lc += sizeof(linkedit_data_command); } // Add LC_DATA_IN_CODE if requested. if (_file.generateDataInCodeLoadCommand) { linkedit_data_command* dl = reinterpret_cast<linkedit_data_command*>(lc); dl->cmd = LC_DATA_IN_CODE; dl->cmdsize = sizeof(linkedit_data_command); dl->dataoff = _startOfDataInCode; dl->datasize = _dataInCodeSize; if (_swap) swapStruct(*dl); lc += sizeof(linkedit_data_command); } } else { // Final linked images have sections under segments. if (_is64) { if (auto ec = writeSegmentLoadCommands<MachO64Trait>(lc)) return ec; } else { if (auto ec = writeSegmentLoadCommands<MachO32Trait>(lc)) return ec; } // Add LC_ID_DYLIB command for dynamic libraries. if (_file.fileType == llvm::MachO::MH_DYLIB) { dylib_command *dc = reinterpret_cast<dylib_command*>(lc); StringRef path = _file.installName; uint32_t size = sizeof(dylib_command) + pointerAlign(path.size() + 1); dc->cmd = LC_ID_DYLIB; dc->cmdsize = size; dc->dylib.name = sizeof(dylib_command); // offset // needs to be some constant value different than the one in LC_LOAD_DYLIB dc->dylib.timestamp = 1; dc->dylib.current_version = _file.currentVersion; dc->dylib.compatibility_version = _file.compatVersion; if (_swap) swapStruct(*dc); memcpy(lc + sizeof(dylib_command), path.begin(), path.size()); lc[sizeof(dylib_command) + path.size()] = '\0'; lc += size; } // Add LC_DYLD_INFO_ONLY. dyld_info_command* di = reinterpret_cast<dyld_info_command*>(lc); di->cmd = LC_DYLD_INFO_ONLY; di->cmdsize = sizeof(dyld_info_command); di->rebase_off = _rebaseInfo.size() ? _startOfRebaseInfo : 0; di->rebase_size = _rebaseInfo.size(); di->bind_off = _bindingInfo.size() ? _startOfBindingInfo : 0; di->bind_size = _bindingInfo.size(); di->weak_bind_off = 0; di->weak_bind_size = 0; di->lazy_bind_off = _lazyBindingInfo.size() ? _startOfLazyBindingInfo : 0; di->lazy_bind_size = _lazyBindingInfo.size(); di->export_off = _exportTrie.size() ? _startOfExportTrie : 0; di->export_size = _exportTrie.size(); if (_swap) swapStruct(*di); lc += sizeof(dyld_info_command); // Add LC_SYMTAB with symbol table info. symtab_command* st = reinterpret_cast<symtab_command*>(lc); st->cmd = LC_SYMTAB; st->cmdsize = sizeof(symtab_command); st->symoff = _startOfSymbols; st->nsyms = _file.stabsSymbols.size() + _file.localSymbols.size() + _file.globalSymbols.size() + _file.undefinedSymbols.size(); st->stroff = _startOfSymbolStrings; st->strsize = _endOfSymbolStrings - _startOfSymbolStrings; if (_swap) swapStruct(*st); lc += sizeof(symtab_command); // Add LC_DYSYMTAB if (_file.fileType != llvm::MachO::MH_PRELOAD) { dysymtab_command* dst = reinterpret_cast<dysymtab_command*>(lc); dst->cmd = LC_DYSYMTAB; dst->cmdsize = sizeof(dysymtab_command); dst->ilocalsym = _symbolTableLocalsStartIndex; dst->nlocalsym = _file.stabsSymbols.size() + _file.localSymbols.size(); dst->iextdefsym = _symbolTableGlobalsStartIndex; dst->nextdefsym = _file.globalSymbols.size(); dst->iundefsym = _symbolTableUndefinesStartIndex; dst->nundefsym = _file.undefinedSymbols.size(); dst->tocoff = 0; dst->ntoc = 0; dst->modtaboff = 0; dst->nmodtab = 0; dst->extrefsymoff = 0; dst->nextrefsyms = 0; dst->indirectsymoff = _startOfIndirectSymbols; dst->nindirectsyms = _indirectSymbolTableCount; dst->extreloff = 0; dst->nextrel = 0; dst->locreloff = 0; dst->nlocrel = 0; if (_swap) swapStruct(*dst); lc += sizeof(dysymtab_command); } // If main executable, add LC_LOAD_DYLINKER if (_file.fileType == llvm::MachO::MH_EXECUTE) { // Build LC_LOAD_DYLINKER load command. uint32_t size=pointerAlign(sizeof(dylinker_command)+dyldPath().size()+1); dylinker_command* dl = reinterpret_cast<dylinker_command*>(lc); dl->cmd = LC_LOAD_DYLINKER; dl->cmdsize = size; dl->name = sizeof(dylinker_command); // offset if (_swap) swapStruct(*dl); memcpy(lc+sizeof(dylinker_command), dyldPath().data(), dyldPath().size()); lc[sizeof(dylinker_command)+dyldPath().size()] = '\0'; lc += size; } // Add LC_VERSION_MIN_MACOSX, LC_VERSION_MIN_IPHONEOS, LC_VERSION_MIN_WATCHOS, // LC_VERSION_MIN_TVOS writeVersionMinLoadCommand(_file, _swap, lc); // Add LC_SOURCE_VERSION { // Note, using a temporary here to appease UB as we may not be aligned // enough for a struct containing a uint64_t when emitting a 32-bit binary source_version_command sv; sv.cmd = LC_SOURCE_VERSION; sv.cmdsize = sizeof(source_version_command); sv.version = _file.sourceVersion; if (_swap) swapStruct(sv); memcpy(lc, &sv, sizeof(source_version_command)); lc += sizeof(source_version_command); } // If main executable, add LC_MAIN. if (_file.fileType == llvm::MachO::MH_EXECUTE) { // Build LC_MAIN load command. // Note, using a temporary here to appease UB as we may not be aligned // enough for a struct containing a uint64_t when emitting a 32-bit binary entry_point_command ep; ep.cmd = LC_MAIN; ep.cmdsize = sizeof(entry_point_command); ep.entryoff = _file.entryAddress - _seg1addr; ep.stacksize = _file.stackSize; if (_swap) swapStruct(ep); memcpy(lc, &ep, sizeof(entry_point_command)); lc += sizeof(entry_point_command); } // Add LC_LOAD_DYLIB commands for (const DependentDylib &dep : _file.dependentDylibs) { dylib_command* dc = reinterpret_cast<dylib_command*>(lc); uint32_t size = sizeof(dylib_command) + pointerAlign(dep.path.size()+1); dc->cmd = dep.kind; dc->cmdsize = size; dc->dylib.name = sizeof(dylib_command); // offset // needs to be some constant value different than the one in LC_ID_DYLIB dc->dylib.timestamp = 2; dc->dylib.current_version = dep.currentVersion; dc->dylib.compatibility_version = dep.compatVersion; if (_swap) swapStruct(*dc); memcpy(lc+sizeof(dylib_command), dep.path.begin(), dep.path.size()); lc[sizeof(dylib_command)+dep.path.size()] = '\0'; lc += size; } // Add LC_RPATH for (const StringRef &path : _file.rpaths) { rpath_command *rpc = reinterpret_cast<rpath_command *>(lc); uint32_t size = pointerAlign(sizeof(rpath_command) + path.size() + 1); rpc->cmd = LC_RPATH; rpc->cmdsize = size; rpc->path = sizeof(rpath_command); // offset if (_swap) swapStruct(*rpc); memcpy(lc+sizeof(rpath_command), path.begin(), path.size()); lc[sizeof(rpath_command)+path.size()] = '\0'; lc += size; } // Add LC_FUNCTION_STARTS if needed. if (_functionStartsSize != 0) { linkedit_data_command* dl = reinterpret_cast<linkedit_data_command*>(lc); dl->cmd = LC_FUNCTION_STARTS; dl->cmdsize = sizeof(linkedit_data_command); dl->dataoff = _startOfFunctionStarts; dl->datasize = _functionStartsSize; if (_swap) swapStruct(*dl); lc += sizeof(linkedit_data_command); } // Add LC_DATA_IN_CODE if requested. if (_file.generateDataInCodeLoadCommand) { linkedit_data_command* dl = reinterpret_cast<linkedit_data_command*>(lc); dl->cmd = LC_DATA_IN_CODE; dl->cmdsize = sizeof(linkedit_data_command); dl->dataoff = _startOfDataInCode; dl->datasize = _dataInCodeSize; if (_swap) swapStruct(*dl); lc += sizeof(linkedit_data_command); } } return llvm::Error::success(); }
Data
Data 由 Segment 的數據組成,是 Mach-O 佔比最多的部分,有代碼有數據,比如符號表。Data 共三個 Segment, TEXT、 DATA、 LINKEDIT。其中 TEXT 和 DATA 對應一個或多個 Section, LINKEDIT 沒有 Section,需要配合 LC_SYMTAB 來解析 symbol table 和 string table。這些裏面是 Mach-O 的主要數據。
生成 __LINKEDIT 的代碼如下:
void MachOFileLayout::buildLinkEditInfo() { buildRebaseInfo(); buildBindInfo(); buildLazyBindInfo(); buildExportTrie(); computeSymbolTableSizes(); computeFunctionStartsSize(); computeDataInCodeSize(); } void MachOFileLayout::writeLinkEditContent() { if (_file.fileType == llvm::MachO::MH_OBJECT) { writeRelocations(); writeFunctionStartsInfo(); writeDataInCodeInfo(); writeSymbolTable(); } else { writeRebaseInfo(); writeBindingInfo(); writeLazyBindingInfo(); //TODO:add weak binding info writeExportInfo(); writeFunctionStartsInfo(); writeDataInCodeInfo(); writeSymbolTable(); } }
通過生成 LINKEDIT 的代碼可以看出 LINKEDIT 裏包含 dyld 所需各種數據,比如符號表、間接符號表、rebase 操作碼、綁定操作碼、導出符號、函數啓動信息、數據表、代碼簽名等。
__DATA 包含 lazy 和 non lazy 符號指針,還會包含靜態數據和全局變量等。可重定位的 Mach-O 文件還會有一個重定位的區域用來存儲重定位信息,如果哪個 section 有重定位字節,就會有一個 relocation table 對應。
生成 relocation 的代碼如下:
void MachOFileLayout::writeRelocations() { uint32_t relOffset = _startOfRelocations; for (Section sect : _file.sections) { for (Relocation r : sect.relocations) { any_relocation_info* rb = reinterpret_cast<any_relocation_info*>( &_buffer[relOffset]); *rb = packRelocation(r, _swap, _bigEndianArch); relOffset += sizeof(any_relocation_info); } } }
使用 size 命令可以看到內容的分佈,使用前面生成的 a.out 來看:
xcrun size -x -l -m a.out
結果如下:
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0) Segment __TEXT: 0x1000 (vmaddr 0x100000000 fileoff 0) Section __text: 0x41 (addr 0x100000f50 offset 3920) Section __stubs: 0x6 (addr 0x100000f92 offset 3986) Section __stub_helper: 0x1a (addr 0x100000f98 offset 3992) Section __cstring: 0x4 (addr 0x100000fb2 offset 4018) Section __unwind_info: 0x48 (addr 0x100000fb8 offset 4024) total 0xad Segment __DATA_CONST: 0x1000 (vmaddr 0x100001000 fileoff 4096) Section __got: 0x8 (addr 0x100001000 offset 4096) total 0x8 Segment __DATA: 0x1000 (vmaddr 0x100002000 fileoff 8192) Section __la_symbol_ptr: 0x8 (addr 0x100002000 offset 8192) Section __data: 0x8 (addr 0x100002008 offset 8200) total 0x10 Segment __LINKEDIT: 0x1000 (vmaddr 0x100003000 fileoff 12288) total 0x100004000
其中__TEXT Segment 的內容有:
- Section64( TEXT, text)
- Section64( TEXT, stubs)
- Section64( TEXT, stub_helper)
- Section64( TEXT, cstring)
- Section64( TEXT, unwind_info)
__DATA Segment 的內容有:
- Section64( DATA, nl_symbol_ptr)
- Section64( DATA, la_symbol_ptr)
__LINKEDIT 的內容是:
- Dynamic Loader Info
- Function Starts
- Symbol Table
- Data in Code Entries
- Dynamic Symbol Table
- String Table
如果是 Objective-C 代碼生成的 Mach-O 會多出很多和 Objective-C 相關的 Section ,我拿 已閱 項目生成的 Mach-O 來看。
xcrun size -x -l -m GCDFetchFeed
結果如下:
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0) Segment __TEXT: 0xa8000 (vmaddr 0x100000000 fileoff 0) Section __text: 0x89084 (addr 0x1000020e0 offset 8416) Section __stubs: 0x588 (addr 0x10008b164 offset 569700) Section __stub_helper: 0x948 (addr 0x10008b6ec offset 571116) Section __gcc_except_tab: 0x1318 (addr 0x10008c034 offset 573492) Section __cstring: 0xbebd (addr 0x10008d34c offset 578380) Section __objc_methname: 0xa20f (addr 0x100099209 offset 627209) Section __objc_classname: 0x11d9 (addr 0x1000a3418 offset 668696) Section __objc_methtype: 0x2185 (addr 0x1000a45f1 offset 673265) Section __const: 0x23c (addr 0x1000a6780 offset 681856) Section __ustring: 0x23e (addr 0x1000a69bc offset 682428) Section __entitlements: 0x184 (addr 0x1000a6bfa offset 683002) Section __unwind_info: 0x1274 (addr 0x1000a6d80 offset 683392) total 0xa5f08 Segment __DATA: 0x2f000 (vmaddr 0x1000a8000 fileoff 688128) Section __nl_symbol_ptr: 0x8 (addr 0x1000a8000 offset 688128) Section __got: 0x258 (addr 0x1000a8008 offset 688136) Section __la_symbol_ptr: 0x760 (addr 0x1000a8260 offset 688736) Section __const: 0x4238 (addr 0x1000a89c0 offset 690624) Section __cfstring: 0x9d80 (addr 0x1000acbf8 offset 707576) Section __objc_classlist: 0x510 (addr 0x1000b6978 offset 747896) Section __objc_nlclslist: 0x40 (addr 0x1000b6e88 offset 749192) Section __objc_catlist: 0x90 (addr 0x1000b6ec8 offset 749256) Section __objc_nlcatlist: 0x10 (addr 0x1000b6f58 offset 749400) Section __objc_protolist: 0x80 (addr 0x1000b6f68 offset 749416) Section __objc_imageinfo: 0x8 (addr 0x1000b6fe8 offset 749544) Section __objc_const: 0x182e8 (addr 0x1000b6ff0 offset 749552) Section __objc_selrefs: 0x2bf8 (addr 0x1000cf2d8 offset 848600) Section __objc_protorefs: 0x8 (addr 0x1000d1ed0 offset 859856) Section __objc_classrefs: 0x858 (addr 0x1000d1ed8 offset 859864) Section __objc_superrefs: 0x370 (addr 0x1000d2730 offset 862000) Section __objc_ivar: 0xb48 (addr 0x1000d2aa0 offset 862880) Section __objc_data: 0x32a0 (addr 0x1000d35e8 offset 865768) Section __data: 0x604 (addr 0x1000d6888 offset 878728) Section __bss: 0x158 (addr 0x1000d6e90 offset 0) total 0x2efe4 Segment __LINKEDIT: 0xae000 (vmaddr 0x1000d7000 fileoff 880640) total 0x100185000
可以看到 __objc 前綴的都是爲了支持 Objective-C 語言新增加的。
那麼 Swift 語言代碼構建的 Mach-O 是怎樣的呢?
使用我做啓動優化時用 Swift 寫的工具 MethodTraceAnalyze 看下內容有什麼。結果如下:
Segment __PAGEZERO: 0x100000000 (vmaddr 0x0 fileoff 0) Segment __TEXT: 0x115000 (vmaddr 0x100000000 fileoff 0) Section __text: 0xfd540 (addr 0x1000019b0 offset 6576) Section __stubs: 0x6f6 (addr 0x1000feef0 offset 1044208) Section __stub_helper: 0xbaa (addr 0x1000ff5e8 offset 1045992) Section __swift5_typeref: 0xf56 (addr 0x100100192 offset 1048978) Section __swift5_capture: 0x3b4 (addr 0x1001010e8 offset 1052904) Section __cstring: 0x7011 (addr 0x1001014a0 offset 1053856) Section __const: 0x4754 (addr 0x1001084c0 offset 1082560) Section __swift5_fieldmd: 0x2bf4 (addr 0x10010cc14 offset 1100820) Section __swift5_types: 0x1f0 (addr 0x10010f808 offset 1112072) Section __swift5_builtin: 0x78 (addr 0x10010f9f8 offset 1112568) Section __swift5_reflstr: 0x2740 (addr 0x10010fa70 offset 1112688) Section __swift5_proto: 0x154 (addr 0x1001121b0 offset 1122736) Section __swift5_assocty: 0x120 (addr 0x100112304 offset 1123076) Section __objc_methname: 0x7a5 (addr 0x100112424 offset 1123364) Section __swift5_protos: 0x8 (addr 0x100112bcc offset 1125324) Section __unwind_info: 0x1c70 (addr 0x100112bd4 offset 1125332) Section __eh_frame: 0x7b0 (addr 0x100114848 offset 1132616) total 0x11362c Segment __DATA_CONST: 0x4000 (vmaddr 0x100115000 fileoff 1134592) Section __got: 0x4a8 (addr 0x100115000 offset 1134592) Section __const: 0x32f8 (addr 0x1001154a8 offset 1135784) Section __objc_classlist: 0xd0 (addr 0x1001187a0 offset 1148832) Section __objc_protolist: 0x10 (addr 0x100118870 offset 1149040) Section __objc_imageinfo: 0x8 (addr 0x100118880 offset 1149056) total 0x3888 Segment __DATA: 0x8000 (vmaddr 0x100119000 fileoff 1150976) Section __la_symbol_ptr: 0x948 (addr 0x100119000 offset 1150976) Section __objc_const: 0x2018 (addr 0x100119948 offset 1153352) Section __objc_selrefs: 0xb0 (addr 0x10011b960 offset 1161568) Section __objc_protorefs: 0x10 (addr 0x10011ba10 offset 1161744) Section __objc_classrefs: 0x38 (addr 0x10011ba20 offset 1161760) Section __objc_data: 0x98 (addr 0x10011ba58 offset 1161816) Section __data: 0x1f88 (addr 0x10011baf0 offset 1161968) Section __bss: 0x2a68 (addr 0x10011da80 offset 0) Section __common: 0x50 (addr 0x1001204e8 offset 0) total 0x7530 Segment __LINKEDIT: 0x152000 (vmaddr 0x100121000 fileoff 1171456) total 0x100273000
可以看到 DATA Segment 部分還是有 objc 前綴的 Section, TEXT Segment 裏已經都是 swift5 爲前綴的 Section 了。
使用 otool 可以查看某個 Section 內容。比如查看 TEXT Segment 的 text Section 的內容,使用如下命令:
xcrun otool -s __TEXT __text a.out
使用 otool 可以直接看 Mach-O 彙編內容 :
xcrun otool -v -t a.out
結果如下:
a.out: (__TEXT,__text) section _main: 0000000100000f50 pushq %rbp 0000000100000f51 movq %rsp, %rbp 0000000100000f54 subq $0x20, %rsp 0000000100000f58 movl $0x0, -0x4(%rbp) 0000000100000f5f movl %edi, -0x8(%rbp) 0000000100000f62 movq %rsi, -0x10(%rbp) 0000000100000f66 movq -0x10(%rbp), %rax 0000000100000f6a movq 0x8(%rax), %rax 0000000100000f6e movq %rax, -0x18(%rbp) 0000000100000f72 movq -0x18(%rbp), %rsi 0000000100000f76 leaq 0x35(%rip), %rdi 0000000100000f7d movb $0x0, %al 0000000100000f7f callq 0x100000f92 0000000100000f84 xorl %ecx, %ecx 0000000100000f86 movl %eax, -0x1c(%rbp) 0000000100000f89 movl %ecx, %eax 0000000100000f8b addq $0x20, %rsp 0000000100000f8f popq %rbp 0000000100000f90 retq
構建中查看代碼生成彙編可以使用 clang 以下選項:
xcrun clang -S -o - main.c
生成彙編如下:
.section __TEXT,__text,regular,pure_instructions .build_version macos, 10, 15 sdk_version 10, 15, 4 .globl _main ## -- Begin function main .p2align 4, 0x90 _main: ## @main .cfi_startproc ## %bb.0: pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset %rbp, -16 movq %rsp, %rbp .cfi_def_cfa_register %rbp subq $32, %rsp movl $0, -4(%rbp) movl %edi, -8(%rbp) movq %rsi, -16(%rbp) movq -16(%rbp), %rax movq 8(%rax), %rax movq %rax, -24(%rbp) movq -24(%rbp), %rsi leaq L_.str(%rip), %rdi movb $0, %al callq _printf xorl %ecx, %ecx movl %eax, -28(%rbp) ## 4-byte Spill movl %ecx, %eax addq $32, %rsp popq %rbp retq .cfi_endproc ## -- End function .section __TEXT,__cstring,cstring_literals L_.str: ## @.str .asciz "%s\n"
可以發現兩者彙編邏輯是一樣的。點符號開頭的都是彙編指令,比如.section 就是告知會執行哪個 segment,.p2align 指令明確後面代碼對齊方式,這裏是16(2^4) 字節對齊,0x90 補齊。在 TEXT Segment 的 text Section 裏會創建一個調用幀堆棧,進行函數調用,callq printf 函數前會用到 L .str(%rip),L .str 標籤會指向字符串,leaq 會把字符串的指針加載到 rdi 寄存器。最後會銷燬調用幀堆棧,進行 retq 返回。
主要 Section:
- __nl_symbol_ptr:包含 non-lazy 符號指針,mach-o/loader.h 裏有詳細說明。服務 dyld_stub_binder 處理的符號。
- la_symbol_ptr: stubs 第一個 jump 目標地址。動態庫的符號指針地址。
- got:二進制文件的全局偏移表 GOT,也包含 S_NON_LAZY_SYMBOL_POINTERS 標記的 non-lazy 符號指針。服務於 TEXT Segment 裏的符號。可以將 got 看作一個表,裏面每項都是一個地址值。 got 的每項在加載期間都會被 dyld 重寫,所以會在 DATA Segment 中。 got 用來存放 non-lazy 符號最終地址,爲 dyld 所用。dylib 外部符號對於全局變量和常量引用地址會指到 __got。
- __lazy_symbol:包含 lazy 符號,首次使用時綁定。
- stubs:跳轉表,重定向到 lazy 和 non-lazy 符號的 section。被標記爲 S_SYMBOL_STUBS。 TEXT Segment 裏代碼和 dylib 外部符號的引用地址對函數符號的引用都指向了 stubs。其中每項都是 jmp 代碼間接尋址,可跳到 la_symbol_ptr Section 中。
- stub_helper:lazy 動態綁定符號的輔助函數。可跳到 nl_symbol_ptr Section 中。
- __text:機器碼,也是實際代碼,包含所有功能。
- __cstring:常量。只讀 C 字符串。
- __const:初始化過的常量。
- _ objc :Objective-C 語言 runtime 的支持。
- __data:初始化過的變量。
- __bss:未初始化的靜態變量。
- __unwind_info:生成異常處理信息。
- __eh_frame:DWARF2 unwind 可執行文件代碼信息,用於調試。
- string table:以空值終止的字符串序列。
- symbol table:通過 LC_SYMTAB 命令找到 symbol table,其包含所有用到的符號信息。結構體 nlist_64描述了符號的基本信息。nlist_64 結構體中 n_type 字段是一個8位複合字段,其中bit[0:1]表示是外部符號,bit[5:8]表調試符號,bit[4:5]表示私有 external 符號,bit[1:4]是符號類型,有 N_UNDF 未定義、N_ABS 絕對地址、N_SECT 本地符號、N_PBUD 預綁定符號、N_INDR 同名符號幾種類型。
- indirect symbol table:每項都是一個 index 值,指向 symbol table 中的項。由 LC_DYSYMTAB 定義,和 nl_symbol_ptr 和 lazy_symbol 一起爲 stubs 和 got 等 Section 服務。
生成 Section 的代碼如下:
void MachOFileLayout::writeSectionContent() { for (const Section &s : _file.sections) { // Copy all section content to output buffer. if (isZeroFillSection(s.type)) continue; if (s.content.empty()) continue; uint32_t offset = _sectInfo[&s].fileOffset; uint8_t *p = &_buffer[offset]; memcpy(p, &s.content[0], s.content.size()); p += s.content.size(); } }
其中 symble table 生成的代碼如下:
void MachOFileLayout::writeSymbolTable() { // Write symbol table and symbol strings in parallel. uint32_t symOffset = _startOfSymbols; uint32_t strOffset = _startOfSymbolStrings; // Reserve n_strx offset of zero to mean no name. _buffer[strOffset++] = ' '; _buffer[strOffset++] = '\0'; appendSymbols(_file.stabsSymbols, symOffset, strOffset); appendSymbols(_file.localSymbols, symOffset, strOffset); appendSymbols(_file.globalSymbols, symOffset, strOffset); appendSymbols(_file.undefinedSymbols, symOffset, strOffset); // Write indirect symbol table array. uint32_t *indirects = reinterpret_cast<uint32_t*> (&_buffer[_startOfIndirectSymbols]); if (_file.fileType == llvm::MachO::MH_OBJECT) { // Object files have sections in same order as input normalized file. for (const Section §ion : _file.sections) { for (uint32_t index : section.indirectSymbols) { if (_swap) *indirects++ = llvm::sys::getSwappedBytes(index); else *indirects++ = index; } } } else { // Final linked images must sort sections from normalized file. for (const Segment &seg : _file.segments) { SegExtraInfo &segInfo = _segInfo[&seg]; for (const Section *section : segInfo.sections) { for (uint32_t index : section->indirectSymbols) { if (_swap) *indirects++ = llvm::sys::getSwappedBytes(index); else *indirects++ = index; } } } } }
獲取 Segment 信息的代碼如下:
int segmentWalk(void *segment_command){ uint32_t nsects; void *section; section = segment_command + sizeof(struct segment_command); nsects = ((struct segment_command *) segment_command)->nsects; while (nsects--) { section += sizeof(struct s_section); } }
獲取對應符號的方法代碼如下:
// 定義參看 <mach-o/nlist.h> #defineN_UNDF 0x0// 未定義 #defineN_ABS 0x2// 絕對地址 #defineN_SECT 0xe// 本地符號 #defineN_PBUD 0xc// 預定義符號 #defineN_INDR 0xa// 同名符號 #defineN_STAB 0xe0// 調試符號 #defineN_PEXT 0x10// 私有 external 符號 #defineN_TYPE 0x0e// 類型位的掩碼 #defineN_EXT 0x01// external 符號 char symbolical(sym){ if (N_STAB & sym->type) return '-'; else if ((N_TYPE & sym->type) == N_UNDF) { if (sym->name_not_found) return 'C'; else if (sym->type & N_EXT) return = 'U'; else return = '?'; } else if ((N_TYPE & sym->type) == N_SECT) { return matched(saved_sections, sym); } else if ((N_TYPE & sym->type) == N_ABS) { return = 'A'; } else if ((N_TYPE & sym->type) == N_INDR) { return = 'I'; } } char matched(saved_sections, symbol) { if (sect = find_mysection(saved_sections, symbol->n_sect)) # { if (!ft_strcmp(sect->name, SECT_TEXT)) ret = 'T'; else if (!ft_strcmp(sect->name, SECT_DATA)) ret = 'D'; else if (!ft_strcmp(sect->name, SECT_BSS)) ret = 'B'; else ret = 'S'; if (!(mysym->type & N_EXT)) ret -= 'A' - 'a'; } }
加載運行
程序要和其他庫還有模塊一起運行,需要在運行時對這些庫和模塊的符號引用進行解析,運行時,你應用程序使用的模塊符號都在共享名稱空間。macOS 使用的是兩級名稱空間來確保不同模塊符號名不會衝突,同時增強向前兼容。
選擇要加載的 Mach-O 後,系統內核會先確定該文件是否是 Mach-O 文件。
文件的第一個字節是魔數,通過魔數可以推斷是不是 Mach-O,mach-o/loader.h 裏定義了四個魔數標識。
#defineMH_MAGIC 0xfeedface #defineMH_CIGAM NXSwapInt(MH_MAGIC) #defineMH_MAGIC_64 0xfeedfacf #defineMH_CIGAM_64 NXSwapInt(MH_MAGIC_64)
以上四個魔數標識是 Mach-O 文件。
然後內核系統會用 fork 函數創建一個進程,然後通過 execve 函數開始程序加載過程,execve 有多個種類,比如 execl、execv 等,只是在參數和環境變量上有不同,最終都會到內核的 execve 函數。
接着會檢查 Mach-O header,加載 dyld 和程序到 Load Command 指定的地址空間。執行動態鏈接器。動態鏈接器通過 dyld_stub_binder 調用,這個函數的參數不直接指定要綁定的符號,而是通過給 dyld_stub_binder 偏移量到 dyld 解釋的特殊字節碼 Segment 中。dyld_stub_binder 函數的代碼在這裏: dyld_stub_binder.s 。dyld 分爲 rebase、binding、lazy binding、導出幾個部分。dyld 可以 hook,使用 DYLD_INSERT_LIBRARIES,類似 ld 的 LD_PRELOAD 還有 DYLD_LIBRARY_PATH。
text 裏需要被 lazy binding 的符號引用,訪問時回到 stub 中,目標地址在la_symbol_ptr,對應 la_symbol_ptr 的內容會指向 stub_helper,其中邏輯會調到 dyld_stub_binder 函數,這個函數會通過 dyld 找到符號的真實地址,最後 dyld_stub_binder 會把得到的地址寫入 la_symbol_ptr 裏後,會跳轉到符號的真實地址。由於地址已經在 la_symbol_ptr 裏了,所以再訪問符號時會通過 stub 的 jum 指令直接跳轉到真實地址。
通過 dyld 加載主程序鏈接到的所有依賴庫,執行符號綁定也就是non lazy binding。綁定解析其他模塊的功能和數據的引用過程,也叫導入符號。
導入導出符號
執行綁定時,鏈接程序會用實際定義的地址替換程序的每個導入引用。通過構建時的選項設置,dyld 可以即時綁定,也叫延遲綁定,首次使用引用時的綁定,在使用符號前不會將程序的引用綁定到共享庫的符號。使用 -bind_at_load 可以加載時綁定,動態鏈接程序在加載程序時立即綁定所有導入的引用,如果沒有設置這個選項,默認按即時綁定來。設置 -prebind,程序引用的共享庫都會在指定的地址預先綁定。
根據 Code Fragment Manager 設計的弱引用允許程序有選擇的綁定到指定的共享庫,如果 dyld 找不到弱引用的定義,會設置爲 NULL,然後可以繼續加載程序。代碼上可以寫判斷,如果引用爲空進行相應的處理。
過程鏈接表 PLT,會在運行時確定函數地址。callq 指令在 dyld_stub 調用 PLT 條目,符號 stub 位於 TEXT Segment 的 stubs Section 中。每個 Mach-O 符號 stub 都是一個 jumpq 指令,它會調用 dyld 找到符號,然後執行。
Mach-O 的導入和導出都會存在 __LINKEDIT 裏。使用 FSA 接受 Leb128 參數,也就是綁定操作碼。LEB 會把整數值編碼成可變長度的字節序列,最後一個字節才設置最高有效位。
當 FSA 循環或遞歸時,會用0xF0對其進行掩碼獲得操作碼,所有導入綁定操作碼都會對應有宏名稱和對應的功能。比如 0xb0 對應宏是 BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED,功能是將記錄放到導入堆棧中,然後把當前記錄的地址偏移量設爲 seg_offset = seg_offset + (scale * sizeofptr) + sizeofptr ,其中 scale 是立即數中包含的值,sizeofptr 是指針對應平臺的大小。
Mach-O 導出符號是 trie 的數據結構,trie 節點最多有一個終端字符串信息,如果沒有終端信息,就以0x00字節標記。有的化,就用 Leb128 代替該節點的終端字符串信息大小。節點導出信息後,類型信息類型使用0x3對標誌進行位掩碼獲得。0x00表示常規符號,0x01表示線程本地符號,0x02標識絕對符號,0x4表示弱引用符號,0x8表示重新導出,0x10是 stub,具有 Leb128的 stub 偏移量。大部分符號都是常規符號,會將 Mach-O 的偏移量給符號。
生成 trie 數據結構的代碼如下:
void MachOFileLayout::buildExportTrie() { if (_file.exportInfo.empty()) return; // For all temporary strings and objects used building trie. BumpPtrAllocator allocator; // Build trie of all exported symbols. auto *rootNode = new (allocator) TrieNode(StringRef()); std::vector<TrieNode*> allNodes; allNodes.reserve(_file.exportInfo.size()*2); allNodes.push_back(rootNode); for (const Export& entry : _file.exportInfo) { rootNode->addSymbol(entry, allocator, allNodes); } std::vector<TrieNode*> orderedNodes; orderedNodes.reserve(allNodes.size()); for (const Export& entry : _file.exportInfo) rootNode->addOrderedNodes(entry, orderedNodes); // Assign each node in the vector an offset in the trie stream, iterating // until all uleb128 sizes have stabilized. bool more; do { uint32_t offset = 0; more = false; for (TrieNode* node : orderedNodes) { if (node->updateOffset(offset)) more = true; } } while (more); // Serialize trie to ByteBuffer. for (TrieNode* node : orderedNodes) { node->appendToByteBuffer(_exportTrie); } _exportTrie.align(_is64 ? 8 : 4); }
對於動態庫,有幾個易於理解的公共符號比導出所有符號更易於使用,讓公共符號集少,私有符號集豐富,維護起來更加方便。更新時也不會影響較早版本。導出最少數量的符號,還能夠優化動態加載程序到進程的時間,動態庫導出符號越少,dyld 加載就越快。
靜態存儲類是表明不想導出符號的最簡單的方法。將可見性屬性放置在實現文件中的符號定義裏,設置符號可見性也能夠更精確的控制哪些符號是公共符號還是私有符號。在編譯選項 -fvisbility 可以指定未指定可見性符號的可見性。使用 -weak_library 選項會告訴編譯器將庫裏所有導出符號都設爲弱鏈接符號。使用 nm 的 -gm 選項可以查看 Mach-O 導出的符號:
nm -gm header.dylib
結果如下:
(undefined) external ___cxa_atexit (from libSystem) (undefined) external _printf (from libSystem) (undefined) externaldyld_stub_binder(from libSystem)
另外可以通過導出的符號文件,列出要導出的符號來控制導出符號數量,其他符號都會被隱藏。導出符號文件 list 如下:
_foo _header
使用 -exported_symbols_list 選項編譯就可以僅導出文件中指定的符號:
clang -dynamiclib header.c -exported_symbols_list list -o header.dylib
符號綁定範圍
符號可能存在與多個作用域級別。未定義的外部符號是在當前文件之外的文件中,如下:
extern int count; extern void foo(void);
私有定義符號,其他模塊不可見
static int count;
私有外部符號可以使用 private_extern 關鍵字:
__private_extern__ int count = 0;
指定一個函數爲弱引用,可以使用 weak_import 屬性:
void foo(void) __attribute__((weak_import));
在符號聲明中添加 weak 屬性來指定將符號設置爲合併的弱引用:
void foo(void) __attribute__((weak));
入口點
符號綁定結果放到 LC_DYSYMTAB 指定的 section,解析後的地址會放到 DATA segment 的 nl_symbol_ptr 和 got 裏。dyld 使用 Load Command 指定 Mach-O 中的數據以各種方式鏈接依賴項。Mach-O 的 Segment 按照 Load Command 中指定映射到內存中。 初始化後,會調用 LC_MAIN 指定的入口點,這個點是 TEXT Segment 的 text Section 的開始。使用 stubs 將 la_symbol_ptr 指向 stub_helpers,dyld_stub_binder 執行解析,然後更新 __la_symbol_ptr 的地址。
Mach-O 和鏈接器之間是通過 assembly trampoline 進行的橋接,Mach-O 接口的 ABI 和 ELF 相同,但策略不同。macOS 在調用 dyld 前後都會保存和恢復 SSE 寄存器。
動態庫構造函數和析構函數
動態庫加載可能需要執行特殊的初始化或者需要做些準備工作,這裏可以使用初始化函數也就是構造函數。結束的時候可以加析構函數。
舉個例子,先定義一個 header.c,在裏面加上構造函數和析構函數:
#include<stdio.h> __attribute__((constructor)) static void prepare(){ printf("%s\n", "prepare"); } __attribute__((destructor)) static void end(){ printf("%s\n", "end"); } void showHeader(){ printf("%s\n", "header"); }
將 header.c 構建成一個動態庫 header.dylib。
xcrun clang -dynamiclib header.c -fvisibility=hidden -o header.dylib
將 header.dylib 和 main.c 構建成一箇中間目標文件 main.o。
xcrun clang main.c header.dylib -o main
運行看結果
ming@mingdeMacBook-Pro macho_demo % ./main "hi" prepare hi end
可以看到,動態庫的構造函數 prepare 和析構函數 end 都執行了。