iOS 恢復調(diào)用棧(適配iOS14)
作者 | heitanBC
來源 | iOS'RE, 點擊閱讀原文查看作者更多文章
0x00 前言
之前楊君大佬寫過這個工具:iOS符號表恢復 65,恢復后堆??娠@示函數(shù)信息。
不知道為何我使用后,堆棧依舊沒有顯示出OC函數(shù)來。所以整理下資料,在restore-symbol 18工程上再適配 iOS14。
不同場景下,也可以用xia0大佬寫過的 Frida調(diào)用棧符號恢復 28,利用 runtime 機制動態(tài)獲取函數(shù)信息,近似匹配來恢復調(diào)用棧。
0x01 符號與調(diào)用棧
1. 調(diào)用棧
在分析程序 crash 時,一般會查看調(diào)用棧來定位問題。如果可執(zhí)行文件沒有經(jīng)過 strip,符號表還保存著符號信息,調(diào)用棧是可以顯示函數(shù)名稱的。
無符號調(diào)用棧
"0 AlipayWallet 0x0000000107545f18 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 29852464",
"1 AlipayWallet 0x0000000107567f94 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 29991852",
"2 AlipayWallet 0x0000000107571b98 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 30031792",
"3 AlipayWallet 0x00000001075a82d8 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 30254832",
"4 AlipayWallet 0x000000010758f778 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 30153616",
"5 AlipayWallet 0x00000001075745b4 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 30042572",
"6 AlipayWallet 0x0000000107557770 _ZN7gcanvas14GCommandBuffer10parseArrayIfEEbPT_m + 29924232",
"7 libsystem_pthread.dylib 0x0000000183459d8c _pthread_start + 156",
"8 libsystem_pthread.dylib 0x000000018345d76c thread_start + 8"
沒有符號的調(diào)用棧無法給出有效函數(shù)信息,導致無法定位問題所在。
2. 符號
符號(Symbol:類名、函數(shù)名或變量名稱為符號名 (Symbol Name);
按類型分,符號可以分三類:
全局符號:目標文件外可見的符號,可以被其他目標文件引用,或者需要其他目標文件定義;
局部符號:只在目標文件內(nèi)可見的符號,指只在目標文件內(nèi)可見的函數(shù)和變量;
調(diào)試符號:包括 行號 信息的調(diào)試符號信息,行號信息中記錄了函數(shù)和變量所對應(yīng)的 文件和文件行號。
符號表(Symbol Table:符號表是 內(nèi)存地址與函數(shù)名、文件名、行號 的映射表;每個定義的符號有一個對應(yīng)的值,叫做 符號值(Symbol Value),對于變量和函數(shù)來說,符號值就是他們的地址;符號表元素如下所示:
< 起始地址 > < 結(jié)束地址 > < 函數(shù) > [< 文件名: 行號 >]
dSYM(debug symbols):是 iOS 的 符號表文件,存儲 16 進制 地址信息和符號的映射文件;文件名通常為:xxx.app.dSYM,類似 Android 構(gòu)建 release 產(chǎn)生的 mapping 文件;利用 dSYM 文件文件,可以將堆棧信息中地址信息還原成對應(yīng)的符號,幫助問題排查;
App relaese 包一般會 strip 掉局部符號和調(diào)試符號以減小包體積,去掉這些符號不影響 App 正常運行,也可以一定程度上保護 App。
App 被 strip 掉符號后,調(diào)用棧無法顯示函數(shù)名稱。如果得到函數(shù)名稱與地址,恢復符號表,將符號表 patch 到可執(zhí)行文件后,理論上調(diào)用棧就可以顯示出信息。
0x02 獲取函數(shù)信息
1. objective-c 函數(shù)
一般情況下 App strip 掉符號后,如果不借助外部 dSYM 文件,是無法恢復的函數(shù)信息,比如 C 函數(shù)在可執(zhí)行文件僅剩地址。
objective-c 函數(shù)信息除了保存在符號表中,還保存在其他段中
__TEXT.__objc_methname - Method names for locally implemented methods
__TEXT.__objc_classname - Names for locally implemented classes
__TEXT.__objc_methtype - Types for locally implemented method types
__DATA.__objc_classlist - An array of pointers to ObjC classes
__DATA.__objc_nlclslist - An array of pointers to classes who implement +load
__DATA.__objc_catlist - List of ObjC categories
__DATA.__objc_protolist - List of ObjC protocols
__DATA.__objc_imageinfo - Version info, not really useful
__DATA.__objc_const - Constant data, i.e. class_ro_t data
__DATA.__objc_selrefs - External references to selectors
__DATA.__objc_protorefs - External references to protocols
__DATA.__objc_classrefs - External references to other classes
__DATA.__objc_superrefs - External references to super classes
__DATA.__objc_ivar - Offsets to ObjC properties
__DATA.__objc_data - Misc ObjC storage, notably ObjC classes
strip 僅刪除符號表中的數(shù)據(jù),所以 OC 符號可以恢復。
2. 解析 __objc_* section
從 objc 的開源代碼來看如何解析 __objc_* section。
i. 現(xiàn)用 class 結(jié)構(gòu)
typedef struct objc_class *Class;
struct objc_object {
private:
isa_t isa; // 一個指針大小
...
}
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 一個指針大小
cache_t cache; // 兩個指針大小 // formerly cache pointer and vtable
class_data_bits_t bits; // 一個指針大小 // class_rw_t * plus custom rr/alloc flags
...
}
class_data_bits_t 是一個指針大小的結(jié)構(gòu)體,保存著類的方法、屬性、遵循的協(xié)議等信息。
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
...
class_rw_t* data() {return (class_rw_t *)(bits & FAST_DATA_MASK);
}
const class_ro_t *safe_ro() const {class_rw_t *maybe_rw = data();
if (maybe_rw->flags & RW_REALIZED) {
// maybe_rw is rw
return maybe_rw->ro();} else {
// maybe_rw is actually ro
return (class_ro_t *)maybe_rw;
}
}
}
在編譯期間,class_ro_t 結(jié)構(gòu)體就已經(jīng)確定,objc_class 中的 bits 的 data 部分存放著該結(jié)構(gòu)體的地址。
運行 runtime 的 realizeClass 方法時,會生成 class_rw_t 結(jié)構(gòu)體,該結(jié)構(gòu)體包含了 class_ro_t,并且更新 data 部分,換成 class_rw_t 結(jié)構(gòu)體的地址。
所以保存在 Mach-O 可執(zhí)行文件中的結(jié)構(gòu)是 class_ro_t。
在 iOS14 后,class_rw_t 結(jié)構(gòu)體發(fā)生較大變化,class_ro_t 結(jié)構(gòu)體未變化。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
ii、方法、屬性、協(xié)議
class_ro_t 里面的 method_list_t 、 ivar_list_t 和 property_list_t 都使用 entsize_list_tt 的結(jié)構(gòu)體模板;iOS 14 method_list_t 有較大改變
// iOS 14 以下
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element first;
// other code
uint32_t entsize() const {return entsizeAndFlags & ~FlagMask;}
}
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> {...};
struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {...};
struct property_list_t : entsize_list_tt<property_t, property_list_t, 0> {
};
// iOS14 以下
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
...
};
方法
// iOS 14
template <typename Element, typename List, uint32_t FlagMask, typename PointerModifier = PointerModifierNop>
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
uint32_t entsize() const {return entsizeAndFlags & ~FlagMask;}
uint32_t flags() const {return entsizeAndFlags & FlagMask;}
};
struct method_list_t : entsize_list_tt<method_t, method_list_t, 0xffff0003, method_t::pointer_modifier> {uint32_t indexOfMethod(const method_t *meth) const {
uint32_t i =
(uint32_t)(((uintptr_t)meth - (uintptr_t)this)/ entsize());
ASSERT(i < count);
return i;
}
bool isSmallList() const {return flags() & method_t::smallMethodListFlag;
}
};
struct method_t {
static const uint32_t smallMethodListFlag = 0x80000000;
method_t(const method_t &other) = delete;
// The representation of a "big" method. This is the traditional
// representation of three pointers storing the selector, types
// and implementation.
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
private:
bool isSmall() const {return ((uintptr_t)this & 1)== 1;}
// The representation of a "small" method. This stores three
// relative offsets to the name, types, and implementation.
struct small {
// The name field either refers to a selector (in the shared
// cache)or a selref (everywhere else).
RelativePointer<const void *> name;
RelativePointer<const char *> types;
RelativePointer<IMP> imp;
bool inSharedCache() const {
return (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS &&
objc::inSharedCache((uintptr_t)this));}
};
small &small() const {ASSERT(isSmall());
return *(struct small *)((uintptr_t)this & ~(uintptr_t)1);}
};
方法以 method_t 結(jié)構(gòu)保存,iOS14 以上該結(jié)構(gòu)發(fā)生巨大變化。struct big 在 64 位的系統(tǒng)上會占用 24 字節(jié),name、types、imp 分別占用 64 bit 大小,與之前一樣。但是 struct small 占用 12 字節(jié),name、types、imp 分別占用 32 bit 大小。
name、types、imp 分別指向方法的 名稱、參數(shù)數(shù)據(jù)、函數(shù)指針,蘋果考慮到鏡像中的方法都是固定的,不會跑到其他鏡像中去。其實不需要 64 位尋址的指針,只需要 32 位即可 (多余 32 位尋址,可執(zhí)行文件在內(nèi)存中要超過 4G)。small 結(jié)構(gòu)里面的數(shù)據(jù),都是相對地址偏移,不是內(nèi)存中的具體位置。如果要還原,需要進行計算。
template <typename T>
struct RelativePointer: nocopy_t {
int32_t offset;
T get() const {if (offset == 0)
return nullptr;
uintptr_t base = (uintptr_t)&offset;
uintptr_t signExtendedOffset = (uintptr_t)(intptr_t)offset;
uintptr_t pointer = base + signExtendedOffset;
return (T)pointer;
}
};
3. 手動解析
現(xiàn)在從 mach-o 文件來看怎么一步步找到 method 信息。
__objc_classlist 節(jié)保存所有類的地址,以第一個 class 為例,地址為:0x010001a340
跳轉(zhuǎn)到 0x010001a340 地址處,紅框所包即 class 結(jié)構(gòu)所保存的值。方法等信息保存在 class_data_bits_t 處,class_data_bits_t 的值為 0x01000187a8。

跳轉(zhuǎn)到 0x01000187a8 地址處,里面保存的為 class_ro_t 結(jié)構(gòu)。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};

對照結(jié)構(gòu)體內(nèi)容,可知 baseMethodList 的地址為 0x0100010c78。

前 64 位 分別保存 baseMethodList 的 flag 和 count,flag 判斷后續(xù)保存的 method_t 結(jié)構(gòu)是 struct big 還是 struct small,
如果是 big 的話,以此取三個 64 位值,就可以得到 函數(shù)名稱、類型、函數(shù)地址。
struct big {
SEL name;
const char *types;
MethodListIMP imp;
};
smallMethodListFlag 值為 0x80000000, flag 值為 0x800000c0,進行 位與 運算,說明以下的 method_t 結(jié)構(gòu)是 small 結(jié)構(gòu)。
struct small {
// The name field either refers to a selector (in the shared
// cache)or a selref (everywhere else).
RelativePointer<const void *> name;
RelativePointer<const char *> types;
RelativePointer<IMP> imp;
bool inSharedCache() const {
return (CONFIG_SHARED_CACHE_RELATIVE_DIRECT_SELECTORS &&
objc::inSharedCache((uintptr_t)this));}
};
template <typename T>
struct RelativePointer: nocopy_t {
int32_t offset;
T get() const {if (offset == 0)
return nullptr;
uintptr_t base = (uintptr_t)&offset;
uintptr_t signExtendedOffset = (uintptr_t)(intptr_t)offset;
uintptr_t pointer = base + signExtendedOffset;
return (T)pointer;
}
};
name
name 的值為 0x9398,對應(yīng)到 RelativePointer 的源代碼,offset 值為 0x9398。
源碼需要取 offset 的地址,才能計算出真正 name 的地址。offset 的地址可以計算出為 0x0100010c80,baseMethodList 的地址 + sizeof(flag) + sizeof(count) = 0x0100010c78 + 0x4 + 0x4。
name 真正地址即為 (int)0x9398 + 0x0100010c80 = 0x10001A018

type
Type 保存函數(shù)的參數(shù)和返回值類型,同理可計算出 type 的真正地址:
(int)0x1ffa + 0x0100010c84 = 0x0100012c7e

imp
imp 是函數(shù)地址,同理可以用計算出:
(int)0xffff5fd8 + 0x0100010c88 = 0x0100006c60 (這里 0xffff5fd8 是負數(shù))
0x03 恢復符號表
符號在符號表中用 nlist 結(jié)構(gòu)體保存。
1. nlist 結(jié)構(gòu)體
// Describes an entry in the symbol table. It’s declared in /usr/include/mach-o/nlist.h.
struct nlist
{
union {
#ifndef __LP64__
char *n_name; /* for use when in-core */
#endif
long n_strx;
} n_un;
unsigned char n_type;
unsigned char n_sect;
short n_desc;
#ifdef __LP32__
/*32 位中 4byte*/
unsigned long n_value;
#else
/*64 位中 8byte*/
unsigned long long n_value;
#endif
};
i. u_un(n_strx)
u_un 的 n_strx 代表該符號在字符串表中偏移。
假如 n_strx 值為 0x22,那么該符號名稱字符串相當于文件的偏移為:stroff + 0x22,空串為偏移為 0,對應(yīng)著 0x00。
ii. n_type
n_type 字段主要用來標識符號的種類。n_type 擁有 8 個 bit,分配如下。
/*
* Symbols with a index into the string table of zero (n_un.n_strx == 0) are
* defined to have a null, "", name. Therefore all string indexes to non null
* names must not have a zero string index. This is bit historical information
* that has never been well documented.
*/
/*
* The n_type field really contains four fields:
* unsigned char N_STAB:3,
* N_PEXT:1,
* N_TYPE:3,
* N_EXT:1;
* which are used via the following masks.
*/
#define N_STAB 0xe0 /* if any of these bits set, a symbolic debugging entry */
#define N_PEXT 0x10 /* private external symbol bit */
#define N_TYPE 0x0e /* mask for the type bits */
#define N_EXT 0x01 /* external symbol bit, set for external symbols */
/*
* Only symbolic debugging entries have some of the N_STAB bits set and if any
* of these bits are set then it is a symbolic debugging entry (a stab). In
* which case then the values of the n_type field (the entire field) are given
* in <mach-o/stab.h>
*/
/*
* Values for N_TYPE bits of the n_type field.
*/
#define N_UNDF 0x0 /* undefined, n_sect == NO_SECT */
#define N_ABS 0x2 /* absolute, n_sect == NO_SECT */
#define N_SECT 0xe /* defined in section number n_sect */
#define N_PBUD 0xc /* prebound undefined (defined in a dylib) */
#define N_INDR 0xa /* indirect */
bit[0:1] 是 N_EXT,表示是外部符號。
bit[1:4] 是 N_TYPE,表示符號類型。
分 N_UNDF 未定義、N_ABS 絕對地址、N_SECT 本地符號、N_PBUD 預(yù)綁定符號、N_INDR 同名符號幾種類型。
bit[4:5] 是 N_PEXT,表示私有外部符號。
bit[5:8] 是 N_STAB,表示調(diào)試符號,具體定義在 /usr/include/mach-o/stab.h。
nlist_64 fields: n_value n_type n_sect n_desc n_strx
0000000100006944 0e 01 0000 00001bc4 -[ViewController locationManager:didUpdateLocations:]
00000001000077ac 0e 01 0000 00001bfa -[ViewController didReceiveMemoryWarning]
00000001000077f8 0e 01 0000 00001c24 -[ViewController locationManager:didUpdateHeading:]
0000000100007950 0e 01 0000 00001c58 -[ViewController writePermissionToFile:]
stab 類型
/*
* Symbolic debugger symbols. The comments give the conventional use for
*
* .stabs "n_name", n_type, n_sect, n_desc, n_value
*
* where n_type is the defined constant and not listed in the comment. Other
* fields not listed are zero. n_sect is the section ordinal the entry is
* refering to.
*/
#define N_GSYM 0x20 /* global symbol: name,,NO_SECT,type,0 */
#define N_FNAME 0x22 /* procedure name (f77 kludge): name,,NO_SECT,0,0 */
#define N_FUN 0x24 /* procedure: name,,n_sect,linenumber,address */
#define N_STSYM 0x26 /* static symbol: name,,n_sect,type,address */
#define N_LCSYM 0x28 /* .lcomm symbol: name,,n_sect,type,address */
#define N_BNSYM 0x2e /* begin nsect sym: 0,,n_sect,0,address */
#define N_AST 0x32 /* AST file path: name,,NO_SECT,0,0 */
#define N_OPT 0x3c /* emitted with gcc2_compiled and in gcc source */
#define N_RSYM 0x40 /* register sym: name,,NO_SECT,type,register */
#define N_SLINE 0x44 /* src line: 0,,n_sect,linenumber,address */
#define N_ENSYM 0x4e /* end nsect sym: 0,,n_sect,0,address */
#define N_SSYM 0x60 /* structure elt: name,,NO_SECT,type,struct_offset */
#define N_SO 0x64 /* source file name: name,,n_sect,0,address */
#define N_OSO 0x66 /* object file name: name,,0,0,st_mtime */
#define N_LSYM 0x80 /* local sym: name,,NO_SECT,type,offset */
#define N_BINCL 0x82 /* include file beginning: name,,NO_SECT,0,sum */
#define N_SOL 0x84 /* #included file name: name,,n_sect,0,address */
#define N_PARAMS 0x86 /* compiler parameters: name,,NO_SECT,0,0 */
#define N_VERSION 0x88 /* compiler version: name,,NO_SECT,0,0 */
#define N_OLEVEL 0x8A /* compiler -O level: name,,NO_SECT,0,0 */
#define N_PSYM 0xa0 /* parameter: name,,NO_SECT,type,offset */
#define N_EINCL 0xa2 /* include file end: name,,NO_SECT,0,0 */
#define N_ENTRY 0xa4 /* alternate entry: name,,n_sect,linenumber,address */
#define N_LBRAC 0xc0 /* left bracket: 0,,NO_SECT,nesting level,address */
#define N_EXCL 0xc2 /* deleted include file: name,,NO_SECT,0,sum */
#define N_RBRAC 0xe0 /* right bracket: 0,,NO_SECT,nesting level,address */
#define N_BCOMM 0xe2 /* begin common: name,,NO_SECT,0,0 */
#define N_ECOMM 0xe4 /* end common: name,,n_sect,0,0 */
#define N_ECOML 0xe8 /* end common (local name): 0,,n_sect,0,address */
#define N_LENG 0xfe /* second stab entry with length information */
/*
* for the berkeley pascal compiler, pc(1):
*/
#define N_PC 0x30 /* global pascal symbol: name,,NO_SECT,subtype,line */
可以用 nm -a 查看所有符號,顯示 stab 類型。
0000000100008414 - 01 0000 FUN -[ViewController .cxx_destruct]
0000000100008414 t -[ViewController .cxx_destruct]
0000000100006e64 t -[ViewController viewDidLoad]
0000000100006e64 - 01 0000 FUN -[ViewController viewDidLoad]
0000000100007cd4 t -[ViewController viewWillAppear:]
0000000100007cd4 - 01 0000 FUN -[ViewController viewWillAppear:]
0000000000000000 - 00 0000 GSYM _OBJC_CLASS_$_ViewController
000000010001a628 S _OBJC_CLASS_$_ViewController
0000000000000000 - 00 0000 GSYM _OBJC_METACLASS_$_ViewController
000000010001a650 S _OBJC_METACLASS_$_ViewController
0000000100018718 - 13 0000 STSYM __OBJC_$_INSTANCE_METHODS_ViewController
0000000100018718 s __OBJC_$_INSTANCE_METHODS_ViewController
0000000100018828 s __OBJC_$_INSTANCE_VARIABLES_ViewController
0000000100018828 - 13 0000 STSYM __OBJC_$_INSTANCE_VARIABLES_ViewController
0000000100018850 s __OBJC_$_PROP_LIST_ViewController
0000000100018850 - 13 0000 STSYM __OBJC_$_PROP_LIST_ViewController
00000001000186b0 s __OBJC_CLASS_PROTOCOLS_$_ViewController
00000001000186b0 - 13 0000 STSYM __OBJC_CLASS_PROTOCOLS_$_ViewController
00000001000188a8 - 13 0000 STSYM __OBJC_CLASS_RO_$_ViewController
00000001000188a8 s __OBJC_CLASS_RO_$_ViewController
00000001000186d0 s __OBJC_METACLASS_RO_$_ViewController
00000001000186d0 - 13 0000 STSYM __OBJC_METACLASS_RO_$_ViewController
iii. n_sect
section 索引,說明符號保存在哪一個 section 中,比如 -[ViewController .cxx_destruct] 保存在 TEXT 段text 節(jié)中,__text 節(jié)的索引為 1。

iv. n_desc
未定義符號和 weak 符號的類型等。鏈接相關(guān)。
v. n_value
隨著符號的種類,也就是 n_type 值的不同,n_value 也有不一樣的含義。
如果是 N_SECT 符號,n_value 是符號所在的地址。
0x04 patch 可執(zhí)行文件
1. Mach-O 簡介
Mach 則是一種操作系統(tǒng)內(nèi)核,Mach 內(nèi)核被 NeXT 公司的 NeXTSTEP 操作系統(tǒng)使用。在 Mach 上,一種可執(zhí)行的文件格是就是 Mach-O(Mach Object file format)。
Mach-O 文件的格式如下圖所示:

Header:保存了 Mach-O 的一些基本信息,包括了平臺、文件類型、LoadCommands 的個數(shù)等等。
LoadCommands:這一段緊跟 Header,加載 Mach-O 文件時會使用這里的數(shù)據(jù)來確定內(nèi)存的分布。
Data:每一個 segment 的具體數(shù)據(jù)都保存在這里,這里包含了具體的代碼、數(shù)據(jù)等等。
2. patch 加載命令
所有 load_command 都有的數(shù)據(jù)結(jié)構(gòu)為:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
cmd 字段代表當前加載命令的類型,cmdsize 字段代表當前加載命令的大小。
Load Commands 直接就跟在 Header 后面,所有 command 占用內(nèi)存的總和在 Mach-O Header 里面已經(jīng)給出了。
在加載過 Header 之后就是通過解析 LoadCommand 來加載接下來的數(shù)據(jù)了。
i. LC_SYMTAB
LC_SYMTAB 數(shù)據(jù)結(jié)構(gòu)如下,保存著符號表以及字符串表在 dylib 文件中的偏移和大小。
struct symtab_command
{
unsigned long cmd;
unsigned long cmdsize;
unsigned long symoff;
unsigned long nsyms;
unsigned long stroff;
unsigned long strsize;
};
/*
cmd 以及 cmdsize 如上文所說:cmd 字段代表當前加載命令的類型,cmdsize 字段代表當前加載命令的大小
symoff: image 文件開頭到符號表位置的字節(jié)偏移,符號表是 **nlist 結(jié)構(gòu)體** 數(shù)組
nsyms: 符號個數(shù)
stroff: image 文件開頭到字符串表位置的字節(jié)偏移
strsize: 字符串表所占大小
*/
獲取符號表和字符表的偏移后,將恢復的符號信息添加到符號表里,將函數(shù)名的字符串添加到字符串表。


并且修改 LC_SYMTAB 結(jié)構(gòu)中數(shù)據(jù):
符號的個數(shù)
一共新增 152 個符號
字符串表偏移
152 * 16 + 126024 = 128456
increase_sym_num * sizeof(nlist) + orig_str_off = new_str_off
字符串表的大小。
新增 152 個符號的名稱,4696 + 5456 = 10152
orig_str_size + increase_str_size = new_str_size

ii. LC_DYSYMTAB
LC_DYSYMTAB 記錄各種符號在符號表和動態(tài)符號表中的索引和個數(shù),一共記錄 9 種符號。
struct dysymtab_command {
uint32_t cmd; /* LC_DYSYMTAB */
uint32_t cmdsize; /* sizeof(struct dysymtab_command) */
uint32_t ilocalsym; /* index to local symbols */
uint32_t nlocalsym; /* number of local symbols */
uint32_t iextdefsym;/* index to externally defined symbols */
uint32_t nextdefsym;/* number of externally defined symbols */
uint32_t iundefsym; /* index to undefined symbols */
uint32_t nundefsym; /* number of undefined symbols */
uint32_t tocoff; /* file offset to table of contents */
uint32_t ntoc; /* number of entries in table of contents */
uint32_t modtaboff; /* file offset to module table */
uint32_t nmodtab; /* number of module table entries */
uint32_t extrefsymoff; /* offset to referenced symbol table */
uint32_t nextrefsyms; /* number of referenced symbol table entries */
uint32_t indirectsymoff; /* file offset to the indirect symbol table */
uint32_t nindirectsyms; /* number of indirect symbol table entries */
uint32_t extreloff; /* offset to external relocation entries */
uint32_t nextrel; /* number of external relocation entries */
uint32_t locreloff; /* offset to local relocation entries */
uint32_t nlocrel; /* number of local relocation entries */
};

新增本地符號 140 個,外部符號 12 個,一共 152 個。修改相應(yīng)符號數(shù)據(jù)。
重定位項表偏移(indSym table offset)因為符號表和字符串表增大,所以也會增大。
125296 + 152 * 16 = 127728
Orig_indSym_offset + Increase_sym_num * sizeof(nlist) = new_indSym_offset
iii. LC_SEGMENT
表示一個段加載命令,需要將它加載到對應(yīng)的進程空間中。
/*
* The 64-bit segment load command indicates that a part of this file is to be
* mapped into a 64-bit task's address space. If the 64-bit segment has
* sections then section_64 structures directly follow the 64-bit segment
* command and their size is reflected in cmdsize.
*/
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
segname 16 字節(jié)大小,用來存儲段的名稱
vmaddr 段要加載的虛擬內(nèi)存的地址
vmsize 段所占的虛擬內(nèi)存的大小
fileoff 段數(shù)據(jù)所在文件中的偏移位置
filesize 段數(shù)據(jù)實際的大小
maxprot 頁面所需要的最高內(nèi)存保護
initprot 頁面初始的內(nèi)存保護
nsects 段所包含的節(jié)區(qū)
flags 段的標志信息
__LINKEDIT 包含需要被動態(tài)鏈接器使用的信息,包括符號表、字符串表、重定位項表等。

__LINKEDIT 的 LC_SEGMENT 只需要修改段的大小:
36048 + 152 * 16 + 5456 = 43936
file_size + Increase_sym_num * sizeof(nlist) + increase_str_size = new_file_size
0x05 效果
恢復支付寶符號并打印調(diào)用棧:
"0 AlipayWallet 0x000000010a5e0660 +[AUNetworkInfo bssid] + 232",
"1 AlipayWallet 0x000000010a5dfe90 +[AUNetworkInfo networkInfo] + 160",
"2 AlipayWallet 0x000000010a5d99e0 +[AUDeviceInfo deviceInfo] + 132",
"3 AlipayWallet 0x000000010a5d8c10 +[AUDeviceInfo deviceInfoWithoutAsyncData] + 84",
"4 AlipayWallet 0x000000010a5d8934 +[AUDeviceInfo deviceInfoWithBlock:] + 224",
"5 libdispatch.dylib 0x000000018340a610 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 374288",
"6 libdispatch.dylib 0x000000018340b184 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 377220",
"7 libdispatch.dylib 0x00000001833e4b50 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 219984",
"8 libdispatch.dylib 0x00000001833f1110 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 270608",
"9 libdispatch.dylib 0x00000001833f18b0 A3849F96-1C9F-36C5-A15F-70C566F14CFF + 272560",
"10 libsystem_pthread.dylib 0x000000018345ab48 _pthread_wqthread + 212",
"11 libsystem_pthread.dylib 0x000000018345d760 start_wqthread + 8"
代碼:https://github.com/HeiTanBc/restore-symbol
參考資料
http://blog.imjun.net/posts/restore-symbol-of-iOS-app/
https://juejin.cn/post/6844904133321818126#heading-17
https://opensource.apple.com/source/objc4/objc4-781/
https://opensource.apple.com/source/objc4/objc4-818.2/
推薦閱讀
? 為 iPad 部署基于 VS Code 的遠程開發(fā)環(huán)境
? “And away we code. ” WWDC21 超 200 個 Session 等著你
? Google 正式發(fā)布 Fuchsia OS,F(xiàn)lutter 集成尚存問題
? 京東APP訂單業(yè)務(wù)Swift優(yōu)化總結(jié)
就差您點一下了 ??????
