1. 背景
本篇文章完成于2023年初,公司內(nèi)部信息已做脫敏處理
2022年10月中旬Apple針對(duì)iOS逐步推出了16.1系統(tǒng)仪壮,動(dòng)態(tài)布局SDK的iOS端在穩(wěn)定運(yùn)行了很長(zhǎng)時(shí)間之后,出現(xiàn)了大面積的"objc_release + 8"的內(nèi)存訪問(wèn)異常崩潰(EXC_BAD_ACCESS)彼宠。隨著16.1及其以后版本的占比越來(lái)越高,對(duì)應(yīng)的崩潰量也出現(xiàn)逐步的攀升弟塞。
本文會(huì)結(jié)合線上崩潰日志以及線下復(fù)現(xiàn)的多種手段來(lái)定位問(wèn)題并解決問(wèn)題。
2. 現(xiàn)狀
2.1 問(wèn)題是什么
我們可以先來(lái)看一下上述崩潰的線程信息:
Incident Identifier: 9546113E-xxxx-xxxx-xxxx-C9F25815BFCA
CrashReporter Key: addxxxxxxxxx
Hardware Model: iPhone15,3
Process: demoe [2161]
Path: /private/var/containers/Bundle/Application/B286F1DB-xxxx-xxxx-9157-A99939E67462/demo.app/demo
Identifier: com.test.demo
Version: 160490 (12.6.204)
Code Type: ARM-64
Parent Process: ? [1]
Date/Time: 2023-01-05 23:59:38.028 +0800
OS Version: iOS 16.1.1 (20B101)
Report Version: 104
Monitor Type: Mach Exception
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x0000000000000010
Crashed Thread: 26
Thread 26 Crashed:
0 libobjc.A.dylib objc_release + 8
1 demo -[SAKDynamicLayout createElement:] (SAKDynamicLayout.m:775)
2 demo -[SAKDynamicLayout buildElementFromMetaXMLNode:parentElement:forVars:] (SAKDynamicLayout.m:1003)
3 demo __70-[SAKDynamicLayout buildElementFromMetaXMLNode:parentElement:forVars:]_block_invoke.242 (SAKDynamicLayout.m:1328)
...
對(duì)應(yīng)的代碼如下:
-(SAKXMLElement *)createElement:(NSString *)name {
SAKXMLElement *element = [[SAKXMLElement alloc] init];
element.name = name;
element.currentLayout = self;
[element createRenderObject];
return element;
}
崩潰信息和原始的代碼對(duì)比看起來(lái)拙已,沒(méi)辦法知道objc_release是發(fā)生在哪里决记。是發(fā)生在形參的retain對(duì)應(yīng)的release、還是發(fā)生在SAKXMLElement的初始化方法里面(內(nèi)聯(lián)了)倍踪、抑或是發(fā)生在element的setName方法里面系宫、又或者說(shuō)是element本身的release呢?
從現(xiàn)有的崩潰日志簡(jiǎn)單分析是沒(méi)辦法分析出來(lái)的建车,沒(méi)有辦法知道崩潰發(fā)生的點(diǎn)扩借,也就沒(méi)辦法去增加修復(fù)的兜底。
編譯器最終生成的代碼和程序員寫(xiě)的代碼有出入缤至,在崩潰日志上面沒(méi)有直觀地體現(xiàn)問(wèn)題發(fā)生的位置以及對(duì)應(yīng)的原因
怎么做呢潮罪?既然崩潰日志無(wú)法給出具體的錯(cuò)誤信息,那么我們剖析ARM的寄存器與函數(shù)調(diào)用規(guī)范獲取更加底層的信息领斥。
再來(lái)看看崩潰發(fā)生時(shí)寄存器狀態(tài)嫉到,一堆16進(jìn)制的數(shù)據(jù),粗看完全不知所云:
Thread 26 crashed with ARM-64 Thread State:
cpsr: 0x0000000000001000 fp: 0x000000016ed984c0 lr: 0x00000001063bddcc pc: 0x00000001c9d8b54c
sp: 0x000000016ed984a0 x0: 0x0000000000000010 x1: 0x0000000280d013c4 x10: 0x00000002270d0840
x11: 0x0000000000000072 x12: 0x00000000000001c3 x13: 0x00000001608abc20 x14: 0x0000000280d013c1
x15: 0x00000002270e4f08 x16: 0x00000001c9d8b544 x17: 0x000000022c169d30 x18: 0x0000000000000000
x19: 0x00000002812c8660 x2: 0x0000000000000000 x20: 0x0000000161210c20 x21: 0x0000000282f4b480
x22: 0x0000000281f99640 x23: 0x0000000000000000 x24: 0x0000000161210c20 x25: 0x00000002814e5fe0
x26: 0x0000000282f7f4e0 x27: 0x00000002812747b0 x28: 0x0000000000000001 x29: 0x000000016ed984c0
x3: 0x000000016ed97f9b x4: 0x0000000000000000 x5: 0x0000000000000000 x6: 0x0000000000000072
x7: 0x0000000000000000 x8: 0x0000000000000010 x9: 0x001b3ac2b573008d
此時(shí)可執(zhí)行程序以及映射的image如下:
Binary Images:
0x1020c8000 - 0x10851bfff +demo arm64 <45fce1ae1xxxxxxfd9b41780ad58> /private/var/containers/Bundle/Application/B286F1DB-xxxx-xxxx-xxxx-A99939E67462/demo.app/demo
0x10c43c000 - 0x10c443fff PanGu arm64 <3c5c92f3e89c3c1dbc6ce40d93992f47> /private/var/containers/Bundle/Application/B286F1DB-3F78-4291-9157-A99939E67462/demo.app/Frameworks/PanGu.framework/PanGu
0x10cdb0000 - 0x10cdbbfff libobjc-trampolines.dylib arm64e <3eb26cf9922139f583d40c8ae83d3424> /private/preboot/Cryptexes/OS/usr/lib/libobjc-trampolines.dylib
0x1c9d88000 - 0x1c9dcbe1f libobjc.A.dylib arm64e <ab79707faf643ba588993b711c6cff5c> /usr/lib/libobjc.A.dylib
0x1c9dcc000 - 0x1ca8acfff MetalPerformanceShadersGraph arm64e <b0605248e3443aca8f9c167c76255813> /System/Library/Frameworks/MetalPerformanceShadersGraph.framework/MetalPerformanceShadersGraph
0x1ca8ad000 - 0x1cae15fff libswiftCore.dylib arm64e <f896d145e02539d6afd3bc0a2ad4f839> /usr/lib/swift/libswiftCore.dylib
0x1cae16000 - 0x1cae47fff CoreServicesInternal arm64e <b3d3659c112d3305b1afd6119b6aed1e> /System/Library/PrivateFrameworks/CoreServicesInternal.framework/CoreServicesInternal
0x1cae48000 - 0x1cb791fff Foundation arm64e <c431acb6fe043d28b6774de6e1c7d81f> /System/Library/Frameworks/Foundation.framework/Foundation
0x1cb792000 - 0x1cb7d1fff WebGPU arm64e <1cc9f1f9198d3221ae9aa9e0413a5ec6> /System/Library/PrivateFrameworks/WebGPU.framework/WebGPU
0x1cb7d2000 - 0x1cb987fff Metal arm64e <8f062125748839efafda57db8cd80033> /System/Library/Frameworks/Metal.framework/Metal
0x1cb988000 - 0x1cbb7afff CoreServices arm64e <9b4971df95e5302b87290741b957daac> /System/Library/Frameworks/CoreServices.framework/CoreServices
0x1d0a68000 - 0x1d0e4dfff CoreFoundation arm64e <5cdc5d9ae5063740b64ebb30867b4f1b> /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
0x1d0e4e000 - 0x1d1be9fff Network arm64e <3bf445f9d58f3280b9c92feaf4daca6d> /System/Library/Frameworks/Network.framework/Network
0x1d1bea000 - 0x1d1fb1fff CFNetwork arm64e <edb0559fc996327f9b3a6616e316f24d> /System/Library/Frameworks/CFNetwork.framework/CFNetwork
0x1d1fb2000 - 0x1d20f5fff CoreTelephony arm64e <5da0a407b8723d8291ab16d63923fb47> /System/Library/Frameworks/CoreTelephony.framework/CoreTelephony
這里我摘錄了幾個(gè)關(guān)鍵image月洛,下面我們需要以這些信息為輸入并搭配lldb等工具來(lái)梳理其中的各個(gè)細(xì)節(jié)何恶。不過(guò)進(jìn)入具體的分析之前,我們需要一些前置的知識(shí)點(diǎn)嚼黔。
如果不需要閱讀ARM的函數(shù)調(diào)用約定以及寄存器的使用規(guī)則细层,可以直接跳過(guò)這部分內(nèi)容直接進(jìn)進(jìn)入到第3節(jié)惜辑。
2.2 匯編中數(shù)據(jù)與指令
在CPU里面直接和ALU交互的數(shù)據(jù)均是存儲(chǔ)在寄存器中,而如何操作這些寄存器就是通過(guò)定義相關(guān)指令疫赎。在ARM64上面一共有31個(gè)通用寄存器:
W是低32位寄存器的前綴盛撑,X是64位寄存器的前綴。LR和FP寄存器在下面的函數(shù)調(diào)用標(biāo)準(zhǔn)里面介紹虚缎。最下面的EL0~EL3是ARMv8的異常等級(jí)撵彻,我們應(yīng)用程序處于EL0,操作系統(tǒng)處于EL1实牡,內(nèi)核等其他安全相關(guān)的處于更高的異常等級(jí)(這里說(shuō)的異常等級(jí)可以理解為類(lèi)似于Linux里面經(jīng)常提到的特權(quán)級(jí))陌僵,不同的異常等級(jí)有不同的操作權(quán)限。
每一個(gè)異常等級(jí)都有對(duì)應(yīng)的幾個(gè)特殊寄存器:
- XZR/WZR是零寄存器(所有針對(duì)該寄存器的讀寫(xiě)都會(huì)得到0)创坞,針對(duì)ZR寄存器的讀操作得到的都是0碗短,針對(duì)ZR寄存器的寫(xiě)操作都是無(wú)意義被忽略的。
- PC寄存器保留的是當(dāng)前執(zhí)行指令的地址题涨;
- SP寄存器通常用于子函數(shù)調(diào)用時(shí)偎谁,棧的邊界(棧的基地址位于FP寄存器中);
- PSR寄存器纲堵,狀態(tài)寄存器在EL0級(jí)別下用的是CPSR巡雨,在其他異常等級(jí)下用的是對(duì)應(yīng)的SPSR。我們后面在分析if語(yǔ)句的執(zhí)行路徑時(shí)會(huì)詳細(xì)再看看席函;
- ELR寄存器是用在切換異常等級(jí)返回之后的地址铐望;
2.3 匯編中函數(shù)調(diào)用標(biāo)準(zhǔn)
下面的內(nèi)容都是基于A64架構(gòu)。看了上面的內(nèi)容,我們需要的是明確在函數(shù)調(diào)用(也叫子過(guò)程調(diào)用)過(guò)程中坪它,各個(gè)指令和寄存器的作用。我們要處理的任務(wù)包括有數(shù)據(jù)處理類(lèi)的乒验、內(nèi)存處理和流程控制類(lèi)的(詳細(xì)的指令可以在這里查詢(xún))
對(duì)于OC來(lái)說(shuō),大部分方法調(diào)用最終都會(huì)轉(zhuǎn)變?yōu)榛趏bjc_msgsend蒂阱,該函數(shù)的原型如下:
objc_msgSend(void /* id self, SEL op, ... */ )
第一個(gè)參數(shù)指明當(dāng)前方法調(diào)用的實(shí)力锻全,第二參數(shù)指明當(dāng)前調(diào)用方法名(對(duì)應(yīng)為SEL),在此之后就是各種參數(shù)的傳遞录煤。
參數(shù)寄存器:X0~X7作為參數(shù)傳遞和返回值保留的寄存器虱痕,超過(guò)部分將使用棧來(lái)進(jìn)行傳遞;
- X8(Indirect Result Location):間接結(jié)果寄存器辐赞,通常用于保留指針類(lèi)型返回值部翘;
- FP(X29):保留著棧的基地址指針(在x86里面稱(chēng)作bp);
- LR(X30):保留函數(shù)調(diào)用的返回地址指針(在x86里面是通過(guò)ip寄存器實(shí)現(xiàn)類(lèi)似的效果)响委;
- SP(X31):保留棧頂?shù)刂罚?/li>
我們看一個(gè)ARM官方文檔提供的一個(gè)例子:
struct struct_A {
int i0;
int i1;
double d0;
double d1;
} AA;
struct struct_A foo(int i0, int i1, double d0, double d1) {
struct struct_A A1;
A1.i0 = i0;
A1.i1 = i1;
A1.d0 = d0;
A1.d1 = d1;
return A1;
}
void bar() {
AA = foo(0, 1, 1.0, 2.0);
int a = 0;
return ;
}
對(duì)應(yīng)的匯編代碼如下:
foo//
SUB SP, SP, #0x30
STR W0, [SP, #0x2C]
STR W1, [SP, #0x28]
STR D0, [SP, #0x20]
STR D1, [SP, #0x18]
LDR W0, [SP, #0x2C]
STR W0, [SP, #0]
LDR W0, [SP, #0x28]
STR W0, [SP, #4]
LDR W0, [SP, #0x20]
STR W0, [SP, #8]
LDR W0, [SP, #0x18]
STR W0, [SP, #10]
LDR X9, [SP, #0x0] <------------------------------------------------------------------------ 開(kāi)始賦值結(jié)構(gòu)體值
STR X9, [X8, #0]
LDR X9, [SP, #8]
STR X9, [X8, #8]
LDR X9, [SP, #0x10]
STR X9, [X8, #0x10] <------------------------------------------------------------------------ 結(jié)束賦值結(jié)構(gòu)體值
ADD SP, SP, #0x30
RET
bar//
STP X29, X30, [SP, #0x10]!
MOV X29, SP
SUB SP, SP, #0x20
ADD X8, SP, #8
MOV W0, WZR
ORR W1, WZR, #1
FMOV D0, #1.00000000
FMOV D1, #2.00000000
BL foo:
ADRP X8, {PC}, 0x78
ADD X8, X8, #0
LDR X9, [SP, #8]
STR X9, [X8, #0]
LDR X9, [SP, #0x10]
STR X9, [X8, #8]
LDR X9, [SP, #0x18]
STR X9, [X8, #0x10]
MOV SP, X29
LDP X20, X30, [SP], #0x10
RET
下圖是函數(shù)調(diào)用關(guān)系:
2.4 內(nèi)聯(lián)匯編
在后面的調(diào)試中新思,我們需要使用一點(diǎn)簡(jiǎn)單的匯編代碼窖梁,僅僅是為了斷點(diǎn)使用。為了文章的完整性這里還是簡(jiǎn)單提及一下夹囚。通用基本格式:
__asm__/asm [volatile] (code); /* Basic inline assembly syntax */
擴(kuò)展格式:
/* Extended inline assembly syntax */
__asm__/asm [volatile] (
code_template
: outputs
[: inputs
[: clobber_list]]
);
這里提到的volatile和我們平時(shí)變量聲明的時(shí)候含義有點(diǎn)差異纵刘,它的意思避免編譯器優(yōu)化這部分(比如刪除掉),這么做的目的是為了我們的代碼能夠正常保留并執(zhí)行荸哟。一個(gè)完整的例子如下:
int res = 0;
__asm ("ADD %[result], %[input_i], %[input_j]"
: [result] "=r" (res)
: [input_i] "r" (i), [input_j] "r" (j)
);
在輸入和輸出的列表里面假哎,針對(duì)每個(gè)變量都有一個(gè)約束符號(hào)(比如r):
- 修飾符:這些修飾符不能使用于輸入的內(nèi)容里面,只能用于輸出的約束鞍历。比如這里的=符號(hào)舵抹,表示寫(xiě)操作;
- 約束符:約束當(dāng)前操作時(shí)針對(duì)寄存器的劣砍,比如這里的r表示的是惧蛹,針對(duì)寄存器的操作;
詳細(xì)內(nèi)容見(jiàn) Writing inline assembly code刑枝、Constraint codes for AArch64 state香嗓。
3. 定位
進(jìn)入正題,下面來(lái)看一下我們?nèi)绾我徊揭徊降恼业奖浑[藏起來(lái)的崩潰原因装畅。
3.1 線上日志分析
在本節(jié)我們會(huì)根據(jù)線上信息靠娱,并搭配上面提到的ARM知識(shí)來(lái)分析線上崩潰日志,看看能否找到問(wèn)題的蛛絲馬跡掠兄。本節(jié)會(huì)用到otool饱岸、MachOView以及LLDB等工具來(lái)協(xié)助排查問(wèn)題。
用到的工具主要是基于LLDB徽千,使用lldb還原線上崩潰發(fā)生時(shí)對(duì)應(yīng)的符號(hào)(參考LLVM Symbolication):
(lldb) target create -no-dependents --arch arm64 demo
Current executable set to 'demo' (arm64).
(lldb) target modules load --file imeituan __TEXT 0x1020c8000 /// 崩潰發(fā)生時(shí)imeituan的起始地址是0x1020c8000
(lldb) image lookup --address 0x00000001063bddcc /// 查看發(fā)生崩潰時(shí)的lr
如果能夠拿到對(duì)應(yīng)的可執(zhí)行文件,我們可以使用otool拿到反編譯的匯編代碼:
otool -tV -arch arm64 imeituan -> ~/Downloads/imeituan_text /// 也可以直接用MachOView汤锨,這里輸出到文本中便于搜索
拿到了反編譯的匯編代碼之后双抽,我們可以根據(jù)上面提到的LR寄存器的值,與imeituan的image的起始地址計(jì)算出當(dāng)前出現(xiàn)的代碼所在:
address=ASLR+offset
比如我們要關(guān)注LR寄存器里面保留的值(0x00000001063bddcc)闲礼,因此這里的偏移量為0x42F5DCC(0x00000001063bddcc - 0x1020c8000)牍汹,接著我們根據(jù)這個(gè)偏移量獲取到當(dāng)前LR指向的匯編代碼為:
由于LR是子過(guò)程調(diào)用發(fā)生時(shí),用來(lái)保留子過(guò)程返回時(shí)的匯編地址的柬泽。因此這里實(shí)際上是發(fā)生了指令跳轉(zhuǎn):
bl0x105c52a84
/// 0x105c52a84
0000000105c52a84movx2, x19
0000000105c52a88b0x10612fc00
0x10612fc00的地址已經(jīng)不包含在imeituan的__TEXT里面慎菲,所以我們需要加載其他的image:
target create --arch arm64 demo
使用lldb查看其地址信息:
lldb) image lookup -a 0x10612fc00 -v
Address: imeituan[0x000000010612fc00] (imeituan.__TEXT.__objc_stubs + 3761088)
Summary:
Module: file = "/Users/xxxx/Downloads/demo-1-160490.20221228170145/Payload/demo.app/demo", arch = "arm64"
發(fā)現(xiàn)其是處于__TEXT.__objc_stubs,也就是說(shuō)是一個(gè)樁調(diào)用锨并,此時(shí)會(huì)跳轉(zhuǎn)到stub_helper露该,等到正常調(diào)用發(fā)生之后才會(huì)修正為真實(shí)的函數(shù)地址。我們看看對(duì)應(yīng)地址的反匯編代碼:
000000010612fc00 adrpx1, 0x1080ee000
000000010612fc04 ldrx1, [x1, 0x268]
000000010612fc08 adrpx16, 0x106455000
000000010612fc0c ldrx16, [x16, 0xb40]
000000010612fc10 brx16
000000010612fc14 brk0x1
000000010612fc18 brk0x1
000000010612fc1c brk0x1
這里br指令是跳轉(zhuǎn)到對(duì)應(yīng)寄存器保留的地址第煮,0x106455000+0xb40 = 0x106455B40解幼。對(duì)應(yīng)地址處于 __DATA.__got section內(nèi)部對(duì)應(yīng)的符號(hào)為:_objc_msgsend
這里也可以用otool -s __DATA __got來(lái)讀取數(shù)據(jù)抑党,但是需要我們自己去將符號(hào)表(對(duì)于got來(lái)說(shuō)它在符號(hào)表中Type為N_UNDF,因此使用"nm -u imeituan")的下標(biāo)和__got的地址下標(biāo)關(guān)聯(lián)起來(lái)才能知道當(dāng)前下標(biāo)是指向的哪個(gè)符號(hào)撵摆,這并沒(méi)有MackOView省事兒底靠,因此這里的內(nèi)容是基于MachOView解析之后的截圖。
現(xiàn)在來(lái)看看x1寄存器里面保存的值特铝,0x1080ee000+0x268 = 0x1080EE268暑中。對(duì)應(yīng)地址處于__DATA.__objc_selrefs內(nèi)部對(duì)應(yīng)的值為:"setName:"
因此很明顯,發(fā)生異常時(shí)是發(fā)生在對(duì)setName的調(diào)用上面鲫剿。
再回到上面的寄存器值鳄逾,我們看到目前pc的值為0x00000001c9d8b54c,對(duì)應(yīng)到images查看處于libobjc.A.dylib內(nèi)部牵素。我們同樣根據(jù)ASLR算出其偏移量為:0x354C严衬。
由于這個(gè)偏移量是在libobjc.A.dylib里面,所以我們需要知道當(dāng)前l(fā)ibobjc.A.dylib的起始地址:
(lldb) image list | grep libobjc.A.dylib
[ 0] D6ECFB73-0CA2-3A21-A3A9-19E450D3B49C 0x00000001800b8000 /Users/xxxx/Library/Developer/Xcode/iOS DeviceSupport/16.2 (20C65) arm64e/Symbols/usr/lib/libobjc.A.dylib
看到它的起始地址是0x00000001800b8000笆呆,加上上面我們得到的偏移量请琳。因此最終的地址為:
(lldb) image lookup -a 0x00000001800b8000+0x354C libobjc.A.dylib
Address: libobjc.A.dylib[0x00000001800bb54c] (libobjc.A.dylib.__TEXT.__text + 8524)
Summary: libobjc.A.dylib`objc_release + 8
可以看到發(fā)生崩潰時(shí)正處于objc_release + 8,和我們?cè)诒罎⑷罩纠锩婵吹降膬?nèi)容是一致的赠幕,并得到了補(bǔ)充信息:
崩潰發(fā)生是由于應(yīng)用層調(diào)用了setName方法俄精,并且是崩潰在libobjc.A.dylib的objc_release + 8
3.2 線下代碼調(diào)試
線上的日志分析更多是來(lái)自于推斷,線下代碼我們可以編譯器的優(yōu)化等級(jí)(Optimization Level)更改為-Os盡可能地還原線上代碼的執(zhí)行路徑榕堰。在本節(jié)主要使用的工具是lldb搭配簡(jiǎn)單的匯編代碼分析(分析代碼可能的執(zhí)行路徑)以佐證我們根據(jù)線上日志推斷出來(lái)的結(jié)果竖慧。
崩潰的代碼是調(diào)用了release觸發(fā)的,因此我們想要類(lèi)似的代碼調(diào)用的話(huà)逆屡。是需要基于匯編來(lái)進(jìn)行DEBUG的:
為了能夠比較好的在匯編里面 做標(biāo)記圾旨,我們可以插入一個(gè)空指令:
-(SAKXMLElement *)createElement:(NSString *)name {
__asm__("nop\n");
SAKXMLElement *element = [[SAKXMLElement alloc] init];
if (name) {
element.name = name;
}
if (self) {
element.currentLayout = self;
}
[element createRenderObject];
__asm__("nop\n");
return element;
}
在空指令這里下一個(gè)斷點(diǎn)。接著我們需要查看是哪里調(diào)用了objc_release魏蔗,由于這個(gè)函數(shù)是系統(tǒng)函數(shù)砍的,調(diào)用的地方繁多,為了能夠盡可能的減少其他調(diào)用對(duì)我們的影響莺治。我們可以限制在指定的線程加這個(gè)斷點(diǎn):
(lldb) thread info
thread #15: tid = 0x1d3c2b, 0x0000000103219f88 SAKFlexboxLibrary_Example`-[SAKDynamicLayout createElement:](self=0x000000010475ca70, _cmd="createElement:", name=@"Var") at SAKDynamicLayout.m:771:5, queue = 'NSOperationQueue 0x104759f10 (QOS: USER_INITIATED)', stop reason = breakpoint 5.1
/// 對(duì)指定線程添加斷點(diǎn)
(lldb) br set -n "objc_release" -t 0x1d3c2b
Breakpoint 11: where = APFS`objc_release, address = 0x00000001e318505c
繼續(xù)執(zhí)行廓鞠,我們可以看到具體是哪些調(diào)用觸發(fā)了objc_release+8:
libobjc.A.dylib`objc_release:
-> 0x185647544 <+0>: ands x0, x0, x0
0x185647548 <+4>: b.le 0x185647534 ; objc_retain_x28 + 68
0x18564754c <+8>: ldr x16, [x0]
0x185647550 <+12>: and x2, x16, #0xffffffff8
0x185647554 <+16>: ldr x17, [x2, #0x20]
0x185647558 <+20>: tbz w17, #0x2, 0x1856475b8 ; <+116>
0x18564755c <+24>: tbz w16, #0x0, 0x1856475d4 ; <+144>
這里objc_release+4有個(gè)b.le的指令,它是屬于b.cond這一類(lèi)的(Conditional execution in A64 code):
In the A64 instruction set, there are a few instructions that are truly conditional. Truly conditional means that when the condition is false, the instruction advances the program counter but has no other effect.
The conditional branch, B.cond is a truly conditional instruction. The condition code is appended to the instruction with a '.' delimiter, for example B.EQ.
此時(shí)查看cpsr寄存器的值為0x00001000:
對(duì)于是否命中命中LE的條件谣旁,我們可以在Condition code suffixes and related flags對(duì)比來(lái)看(Z被設(shè)置床佳,或者說(shuō)N和V不一樣):
此時(shí)條件為false的時(shí)候就會(huì)向下執(zhí)行到objc_release+8。
通過(guò)增加objc_release的符號(hào)斷點(diǎn)以及查看CPSR的值榄审,我們可以發(fā)現(xiàn)觸發(fā)了objc_release的地方包含了:
- 形參的retain對(duì)應(yīng)的release砌们;
- SAKXMLElement的初始化方法;
- element的setName方法;
通過(guò)對(duì)比之后發(fā)現(xiàn)第3種是符合線上崩潰堆棧的怨绣。
通過(guò)線上日志分析和線下代碼調(diào)試崩潰位置均是setName方法內(nèi)壞內(nèi)存訪問(wèn)(該set方法由編譯器默認(rèn)實(shí)現(xiàn))
4. 復(fù)現(xiàn)與修復(fù)
在前面我們通過(guò)線上和線下都將崩潰的地方指向了setName方法角溃,我們基本上可以確定是在set方法內(nèi)部調(diào)用導(dǎo)致的異常內(nèi)存訪問(wèn)。那么我們能復(fù)現(xiàn)這種崩潰嗎篮撑?
4.1 堆棧復(fù)現(xiàn)
本節(jié)我們會(huì)在已確定發(fā)生錯(cuò)誤的地方减细,人為地構(gòu)造壞內(nèi)存訪問(wèn),以復(fù)現(xiàn)線上相同的崩潰赢笨。因此我們修改方法:
-(SAKXMLElement *)createElement:(NSString *)name {
__asm__("nop\n");
SAKXMLElement *element = [[SAKXMLElement alloc] init];
void *ptr = (__bridge void *)(element);
void *nameptr = (ptr + 0x10);
memset(nameptr, 0x10, 1);
if (name) {
element.name = name;
}
if (self) {
element.currentLayout = self;
}
[element createRenderObject];
__asm__("nop\n");
return element;
}
修改的大致含義如下:
name屬性的定義如下:
@property(nonatomic, copy) NSString *name;
因此對(duì)于它的set方法最終會(huì)調(diào)用c函數(shù)objc_setProperty_nonatomic_copy:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
}
objc_release(oldValue);
}
因此我們?cè)趇nit方法之后未蝌,setName之前。我們給偏移地址為0x10的地址上設(shè)置一個(gè)非法的內(nèi)核地址(name的成員變量在該類(lèi)的結(jié)構(gòu)里面偏移為0x10):
SAKXMLElement *element = [[SAKXMLElement alloc] init];
void *ptr = (__bridge void *)(element);
void *nameptr = (ptr + 0x10);
memset(nameptr, 0x10, 1);
我們可以通過(guò)lldb查看更改前后內(nèi)存里面的值:
(lldb) x/64b ptr
0x10889e8a0: 0x21 0x33 0xdd 0x05 0x01 0x00 0x00 0x01
0x10889e8a8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8b0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8b8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8d0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
更改之后:
(lldb) x/64b ptr
0x10889e8a0: 0x21 0x33 0xdd 0x05 0x01 0x00 0x00 0x01
0x10889e8a8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8b0: 0x10 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8b8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8c0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8c8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8d0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x10889e8d8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
可以看到地址0x10889e8a8之后8字節(jié)的數(shù)據(jù)為0x10茧妒。這時(shí)候我們繼續(xù)執(zhí)行萧吠,得到如下結(jié)果:
4.2 問(wèn)題修復(fù)
當(dāng)然上面復(fù)現(xiàn)是我們?nèi)藶槿バ薷膶?duì)應(yīng)內(nèi)存地址上的數(shù)據(jù),但是線上不會(huì)有人人為這么做的桐筏。那怎么解釋呢纸型?看了一下iOS16.1 release note,里面針對(duì)Memory Allocation有如下的改動(dòng):
通俗一點(diǎn)來(lái)說(shuō)就是有兩種場(chǎng)景:
釋放內(nèi)存后執(zhí)行讀操作:即我們?cè)谑褂弥羔樧x取某一指定內(nèi)存之后梅忌,觸發(fā)了free操作狰腌。我們所觀測(cè)到的內(nèi)存會(huì)是0;
釋放內(nèi)存后執(zhí)行寫(xiě)操作:即某一個(gè)指針指向的內(nèi)存地址被釋放了牧氮,在我們調(diào)用calloc的之前執(zhí)行了寫(xiě)操作琼腔,那么我們此時(shí)生成新的指針指向的內(nèi)存可能是非0的;
這個(gè)第二點(diǎn)就很好的解釋了為什么name會(huì)有一個(gè)非0的初值踱葛,即在美團(tuán)里面可能其他場(chǎng)景針對(duì)這塊兒內(nèi)存在釋放后執(zhí)行了寫(xiě)的操作丹莲。
如果此時(shí)我們調(diào)用setName方法,在釋放舊值的時(shí)候就有可能出現(xiàn)bad_access的問(wèn)題了尸诽。
解決的方案:
我們?cè)赟AKXMLElement的初始化方法里面甥材,將對(duì)應(yīng)的值顯式賦值為0即可
漲點(diǎn)姿勢(shì)
如果我們遇到了其他類(lèi)型ARM匯編的問(wèn)題了呢?授人以魚(yú)不如授人以漁性含,簡(jiǎn)單介紹一下我在ARM開(kāi)發(fā)者里面如何淘我需要的文檔的洲赵。
ARM針對(duì)不同用途而劃分出來(lái)的不同系列,對(duì)于不同的從業(yè)人員在查詢(xún)相關(guān)文檔時(shí)可以做到按需查找(比如基于移動(dòng)端的開(kāi)發(fā)人員胶滋,我們需要查看哪一系列的文檔)。下表是ARM的一個(gè)架構(gòu)演進(jìn)(具體見(jiàn)ARM Cortex-A Series Programmer's Guide for ARMv8-A)悲敷,每個(gè)架構(gòu)對(duì)應(yīng)了一系列的處理器:
指令集架構(gòu) | 處理器家族 |
---|---|
ARMv1 | ARM1 |
ARMv2 | ARM2究恤、ARM3 |
ARMv3 | ARM6、ARM7 |
ARMv4 | StrongARM后德、ARM7TDMI部宿、ARM9TDMI |
ARMv5 | ARM7EJ、ARM9E、ARM10E理张、XScale |
ARMv6 | ARM11赫蛇、ARM Cortex-M |
ARMv7 | ARM Cortex-A、ARM Cortex-M雾叭、ARM Cortex-R |
ARMv8 | Cortex-A35悟耘、Cortex-A50系列[18]、Cortex-A70系列织狐、Cortex-X1 |
ARMv9 | Cortex-A510暂幼、Cortex-A710、Cortex-A715移迫、Cortex-X2旺嬉、Cortex-X3、ARM Neoverse N2 |
從ARMv7開(kāi)始厨埋,Arm就開(kāi)始針對(duì)不同的領(lǐng)域進(jìn)行了處理器細(xì)分:
- Cortex-A(Application Processor cores):面向性能密集型系統(tǒng)的應(yīng)用處理器內(nèi)核邪媳,我們關(guān)心的智能手機(jī)則屬于這個(gè)系列;
- Cortex-R(Real Time Application cores):面向?qū)崟r(shí)應(yīng)用的高性能內(nèi)核荡陷;
- Cortex-M(Microcontroller Cores):面向各類(lèi)嵌入式應(yīng)用的微控制器內(nèi)核雨效;
因此后續(xù)我們?cè)谡蚁嚓P(guān)文檔的時(shí)候查看Cortex-A系列的即可,但是指令集架構(gòu)在不斷演進(jìn)亲善。我們目前主要是基于ArmV8-Cortex-A设易,以Cortex-A57處理器為例:
包含有4個(gè)核心(0~3),其內(nèi)部包含有處理器核心部分蛹头、NEON(用于處理SIMD顿肺,單指令多數(shù)據(jù),一般用于類(lèi)似矩陣等有規(guī)律的數(shù)據(jù)渣蜗,能夠減少處理器和內(nèi)存的數(shù)據(jù)交互次數(shù))屠尊、L1的指令和數(shù)據(jù)緩存等等。
對(duì)于ArmV8來(lái)說(shuō)耕拷,他相對(duì)于v7的變化如下:
其中最重要的就是引入了64位的運(yùn)行模式讼昆,因此在v8上面目前支持兩種執(zhí)行狀態(tài):
- AArch32:也簡(jiǎn)稱(chēng)為A32
- AArch64:也簡(jiǎn)稱(chēng)為A64,在macOS或者其他類(lèi)Unix的系統(tǒng)也會(huì)叫做ARM64骚烧。
希望對(duì)大家有所幫助浸赫。