
中文
調試的聲譽很差。我的意思是,如果開發人員對該程序有完整的了解,那麼就不會有任何錯誤,並且首先不會調試他們,對嗎?
不要這樣思考。
就此而言,您的軟件或任何軟件中總會有錯誤。您的產品經理對您的測試覆蓋範圍沒有解決。實際上,將調試視為修復破碎的事物的過程實際上是一種有毒的思維方式,從而在心理上阻礙了您的分析能力。
相反,您應該將調試視為更好地理解程序的過程。這是一個微妙的差異,但是如果您真正相信它,任何以前的調試的苦惱都會消失。
自從COBOL語言的創始人格雷斯·霍珀(Grace Hopper)發現了中繼計算機中的世界第一個錯誤以來,軟件開發中的錯誤從未停止。正如《高級Apple調試與反向工程》的序言》告訴我們:開發人員不想認為如果對軟件的工作方式有很好的了解,就不會有錯誤。因此,調試幾乎是軟件開發生命週期中不可避免的階段。
如果您向沒有經驗的程序員詢問如何定義調試,他可能會說:“調試是您為軟件問題找到解決方案的事情”。他是對的,但這只是真正調試的一小部分。
這是真正調試的步驟:
在上述步驟中,最重要的步驟是第一步:找出問題。顯然,這是其他步驟的先決條件。
研究表明,經驗豐富的程序員花在調試上的時間來定位相同的缺陷,大約有200年的缺乏經驗的程序員。這意味著調試經驗在編程效率方面具有巨大的不同。我們有很多有關軟件設計的書籍,不幸的是,很少有人會介紹調試,甚至在學校的課程中。
隨著多年來的調試器的改善,程序員的編碼樣式會徹底更改。當然,調試器無法替代良好的思維,思維無法替代出色的調試器,最完美的組合是出色的調試器,具有良好的思維。
以下圖是書中描述的九個調試規則<debugging:即使找到最難以捉摸的軟件和硬件問題>的9個必不可少的規則>。

儘管作為iOS程序員,但工作中的大多數時間都不會處理組裝語言,但是了解程序集仍然非常有幫助,尤其是在調試系統框架或沒有源代碼的第三方框架時。
Assembly語言是一種低級機器的編程語言,可以將其視為用於各種CPU的機器說明的集合。程序員可以使用彙編語言直接控制計算機硬件系統。並且以彙編語言編寫的程序具有許多優點,例如快速執行速度和少佔據的內存。
到目前為止,在Apple平台X86和ARM上廣泛使用了兩個主要架構。在使用ARM組裝語言的移動設備中,這主要是因為ARM是一個減少的指令集計算(RISC)體系結構,具有低功耗優勢。當Mac OS之類的桌面平台時,使用X86體系結構。 iOS模擬器上安裝的應用程序實際上是在模擬器內部作為Mac OS應用程序運行,這意味著模擬器像容器一樣工作。由於我們的案件在iOS模擬器中進行了調試,因此主要的研究目標是X86組裝語言。
x86彙編語言演變成兩個語法分支:intel(x86平台文檔中的原始使用)和at&t。英特爾在MS-DOS和Windows家族中占主導地位,而AT&T在Unix家族中很常見。英特爾和AT&T之間的語法有很大的差異,例如變量,恆定,寄存器的訪問,間接地址和偏移。儘管它們的語法差異很大,但硬件系統是相同的,這意味著其中一個可以無縫遷移到另一個。由於XCode上使用了AT&T彙編語言,因此我們將在下面的AT&T上關注AT&T 。
請注意,Intel語法用於Hopper拆卸和IDA Pro的拆卸工具。
Belows是英特爾和AT&T之間的區別:
操作數的前綴:在AT&T語法中, %用作寄存器名稱的前綴, $用作即時操作數的前綴,而在Intel中不使用前綴和直接操作數。另一個區別是添加0x作為AT&T中十六進制的前綴。下圖展示了其前綴之間的區別:
| AT&T | 英特爾 |
|---|---|
| movq%rax,%rbx | MOV RBX,RAX |
| addq $ 0x10,%rsp | 添加RSP,010H |
在英特爾語法中,
h後綴用於十六進制操作數,b後綴用於二元操作數。
操作數:在AT&T語法中,第一個操作數是源操作數,第二操作數是目標操作數。但是,在英特爾語法中,操作數的順序相反。從這一點開始,根據我們的閱讀習慣,AT&T的語法對我們來說更舒適。
地址模式:與英特爾語法進行比較,很難讀取AT&T的間接地址模式。但是,地址計算的算法相同: address = disp + base + index * scale 。 base代表基地地址, disp代表偏移地址, index * scale確定元素的位置, scale是元素的大小,元素的大小只能是兩個元素。 disp/base/index/scale都是可選的, index的默認值為0,而scale的默認值為1。現在,讓我們查看地址計算的指令: %segreg: disp(base,index,scale) for at&t for at&t和segreg: [base+index*scale+disp] 。實際上,上面的兩個說明都屬於段地址模式。 segreg代表段寄存器,當CPU的數字能力超過寄存器'數字時,通常以實際模式使用。例如,CPU可以解決20位空間,但寄存器只有16位。要實現20位空間,需要使用另一種尋址模式: segreg:offset 。使用此地址模式,偏移地址將為segreg * 16 + offset ,但比平面內存模式更為複雜。在保護模式下,該地址在線性地址空間下,這意味著可以忽略段的基礎地址。
| AT&T | 英特爾 |
|---|---|
| MOVQ 0XB57751(%RIP),%RSI | MOV RSI,QWORD PTR [RIP+0xB57751H] |
| Leaq(%RAX,%RBX,8),%rdi | lea rdi,qword ptr [rax+rbx*8] |
如果立即操作數在
disp或scale的位置,則可以省略$。在英特爾語法中,需要在內存操作數之前添加byte ptr,word ptr,dword ptr和qword ptr。
opcode的後綴:在AT&T語法中,所有Opcodes都有一個後綴來指定大小。通常有四種後綴: b , w , l和q b表示8位字節, w表示16位單詞, l表示32位雙詞。 32位數字也稱為長達16位的長詞。 q代表64位四詞。下圖說明了AT&T和Intel中數據過渡指令(MOV)的語法。
| AT&T | 英特爾 |
|---|---|
| movb%al,%bl | MOV BL,AL |
| movw%ax,%bx | mov bx,斧頭 |
| movl%eax,%ebx | MOV EBX,EAX |
| movq%rax,%rbx | MOV RBX,RAX |
如我們所知,內存用於存儲CPU的指令和數據。內存本質上是字節數組。儘管內存訪問的速度非常快,但我們仍然需要一個較小,更快的存儲單元來加快CPU的指令執行,這是註冊。在執行指令期間,所有數據均臨時存儲在寄存器中。這就是為什麼登記為命名的原因。
當處理器從16位增長到32位時,也將8個寄存器擴展到32位。之後,當使用擴展寄存器時,將E前綴添加到原始寄存器名稱中。 32位處理器是英特爾體系結構32位,即IA32。如今,主要處理器是64位Intel體系結構,該體系結構從IA32擴展,稱為X86-64。由於IA32已經過去,因此本文僅關注X86-64。請注意,在X86-64中,寄存器的數量從8延長到16。僅僅因為該擴展名,程序狀態可以存儲在寄存器中而不是堆棧中。因此,內存訪問的頻率大大降低。
在X86-64中,有16位64位的通用寄存器和16個浮動指針登記冊。此外,CPU還有另外一個64位指令指針寄存器稱為rip 。它旨在存儲下一個執行指令的地址。還有其他一些未被廣泛使用的寄存器,我們不打算在本文中談論它們。在16個通用寄存器中,其中8個來自IA32:RAX,rcx,rdx rbx rbx rsi rsi rdi rdi rdi rsp和rbp。由於X86-64為R8 -R15,其他八個通用寄存器是新添加的。 16個浮動寄存器為XMM0 -XMM15。
當前的CPU為8088,該寄存器也從16位擴展到32位,最後延伸到64位。因此,該程序仍然可以訪問寄存器的低8位或16位或32位。
下圖說明了x86-64的16個一般寄存器:

在LLDB中使用register read命令可以將當前堆棧幀的寄存器數據轉儲。
例如,我們可以使用以下命令在寄存器中顯示所有數據:
register read -a or register read --all
General Purpose Registers:
rax = 0x00007ff8b680c8c0
rbx = 0x00007ff8b456fe30
rcx = 0x00007ff8b6804330
rdx = 0x00007ff8b6804330
rdi = 0x00007ff8b456fe30
rsi = 0x000000010cba6309 "initWithTask:delegate:delegateQueue:"
rbp = 0x000070000f1bcc90
rsp = 0x000070000f1bcc18
r8 = 0x00007ff8b680c8c0
r9 = 0x00000000ffff0000
r10 = 0x00e6f00100e6f080
r11 = 0x000000010ca13306 CFNetwork`-[__NSCFURLLocalSessionConnection initWithTask:delegate:delegateQueue:]
r12 = 0x00007ff8b4687c70
r13 = 0x000000010a051800 libobjc.A.dylib`objc_msgSend
r14 = 0x00007ff8b4433bd0
r15 = 0x00007ff8b6804330
rip = 0x000000010ca13306 CFNetwork`-[__NSCFURLLocalSessionConnection initWithTask:delegate:delegateQueue:]
rflags = 0x0000000000000246
cs = 0x000000000000002b
fs = 0x0000000000000000
gs = 0x0000000000000000
eax = 0xb680c8c0
ebx = 0xb456fe30
ecx = 0xb6804330
edx = 0xb6804330
edi = 0xb456fe30
esi = 0x0cba6309
ebp = 0x0f1bcc90
esp = 0x0f1bcc18
r8d = 0xb680c8c0
r9d = 0xffff0000
r10d = 0x00e6f080
r11d = 0x0ca13306
r12d = 0xb4687c70
r13d = 0x0a051800
r14d = 0xb4433bd0
r15d = 0xb6804330
ax = 0xc8c0
bx = 0xfe30
cx = 0x4330
dx = 0x4330
di = 0xfe30
si = 0x6309
bp = 0xcc90
sp = 0xcc18
r8w = 0xc8c0
r9w = 0x0000
r10w = 0xf080
r11w = 0x3306
r12w = 0x7c70
r13w = 0x1800
r14w = 0x3bd0
r15w = 0x4330
ah = 0xc8
bh = 0xfe
ch = 0x43
dh = 0x43
al = 0xc0
bl = 0x30
cl = 0x30
dl = 0x30
dil = 0x30
sil = 0x09
bpl = 0x90
spl = 0x18
r8l = 0xc0
r9l = 0x00
r10l = 0x80
r11l = 0x06
r12l = 0x70
r13l = 0x00
r14l = 0xd0
r15l = 0x30
Floating Point Registers:
fctrl = 0x037f
fstat = 0x0000
ftag = 0x00
fop = 0x0000
fioff = 0x00000000
fiseg = 0x0000
fooff = 0x00000000
foseg = 0x0000
mxcsr = 0x00001fa1
mxcsrmask = 0x0000ffff
stmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xff 0xff}
stmm1 = {0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0xff 0xff}
stmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
stmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
stmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0xbc 0x87 0x0b 0xc0}
stmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
stmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x78 0xbb 0x0b 0x40}
stmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm8 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm9 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm10 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm11 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm12 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm13 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm14 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
ymm15 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm0 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm1 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm2 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm3 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm4 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm5 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm6 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm7 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm8 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm9 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm10 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm11 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm12 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm13 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm14 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
xmm15 = {0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00}
Exception State Registers:
trapno = 0x00000003
err = 0x00000000
faultvaddr = 0x000000010bb91000
如我們所知,X86-64中有16個浮動指針寄存器:XMM0 -XMM15。實際上,還有其他一些細節。在register read -a命令的輸出中,您可能會注意到除XMM寄存器組外,還有STMM和YMM寄存器。 STMM是ST寄存器的別名,ST是X86中FPU(浮點數單元)的寄存器,以處理浮動數據。 FPU包含一個浮點指針寄存器,該寄存器具有8個80位浮動指針寄存器:ST0 -ST7。我們可以觀察到STMM寄存器的輸出為80位,這可以證明STMM寄存器是ST寄存器。 XMM為128位寄存器,YMM寄存器為256位,是XMM的擴展名。實際上,XMM寄存器是YMM寄存器的低128位。像EAX寄存器一樣,是RAX寄存器的低32位。在奔騰III中,英特爾發布了一個名為SSE(流simd擴展)的指令集,該指令是MMX的擴展。 SSE中添加了八個新的128位寄存器(XMM0 -XMM7)。 AVX(高級向量擴展)指令集是SSE的擴展架構。同樣在AVX中,將128位寄存器XMM擴展到256位寄存器YMM。

函數調用包括參數傳遞和控制從一個彙編單元到另一個彙編單元的傳輸。在功能調用過程中,數據傳遞,局部變量分配和釋放由Stack執行。分配給單個功能調用的堆棧稱為堆棧幀。
OS X X86-64的函數呼叫約定與文章中描述的約定相同:System V Application二進制接口AMD64架構處理器補充。因此,如果您對此感興趣,可以參考它。
在LLDB調試期間,我們可以使用bt命令打印當前線程的堆棧跟踪,如下:
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x00000001054e09d4 TestDemo`-[ViewController viewDidLoad](self=0x00007fd349558950, _cmd="viewDidLoad") at ViewController.m:18
frame #1: 0x00000001064a6931 UIKit`-[UIViewController loadViewIfRequired] + 1344
frame #2: 0x00000001064a6c7d UIKit`-[UIViewController view] + 27
frame #3: 0x00000001063840c0 UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 61
// many other frames are ommitted here
實際上, bt命令在堆棧框架上是可行的。堆棧框架保留了函數的返回地址和本地變量,這些函數可以看作是函數執行的上下文。眾所周知,堆堆積在上升,而堆棧向下生長,這是從數量的記憶地址到小編號的記憶地址。調用函數後,為函數調用分配了一個獨立的堆棧幀。 RBP寄存器(稱為幀指針)總是指向最新分配的堆棧框架(高地址)的末尾。 RSP寄存器(稱為堆棧指針)總是指向最新分配的堆棧框架(低地址)的頂部。以下是框架堆棧的圖表:

左列Position是使用間接地址模式的內存地址。 Content是地址在Position點上的值。根據上圖中堆棧框架的結構,函數調用過程可以描述為以下幾個步驟:
步驟2和3實際上屬於call指令。此外,步驟4和步驟5可以在彙編指令中描述如下:
TestDemo`-[ViewController viewDidLoad]:
0x1054e09c0 <+0>: pushq %rbp //step 4
0x1054e09c1 <+1>: movq %rsp, %rbp //step 5
很容易注意到,這兩個步驟都與每個函數調用一起。上圖有另一個細節:RSP寄存器下方有一個紅色區域,該區域被ABI稱為紅色區域。它是保留的,不得通過信號或中斷處理程序進行修改。因此,由於可以在函數調用過程中對其進行修改,因此,葉功能函數,這意味著那些從未調用其他功能的函數可以將此區域用於臨時數據。
UIKit`-[UIViewController loadViewIfRequired]:
0x1064a63f1 <+0>: pushq %rbp
0x1064a63f2 <+1>: movq %rsp, %rbp
0x1064a63f5 <+4>: pushq %r15
0x1064a63f7 <+6>: pushq %r14
0x1064a63f9 <+8>: pushq %r13
0x1064a63fb <+10>: pushq %r12
0x1064a63fd <+12>: pushq %rbx
在上面的說明中,從0x1064a63f5到0x1064a63fd的說明屬於步驟6。有一種稱為函數保存寄存器的寄存器,這意味著它們屬於呼叫函數,但需要調用函數才能保留其值。從下面的彙編說明中,我們可以看到RBX,RSP和R12 -R15都屬於此類寄存器。
0x1064a6c4b <+2138>: addq $0x1f8, %rsp ; imm = 0x1F8
0x1064a6c52 <+2145>: popq %rbx
0x1064a6c53 <+2146>: popq %r12
0x1064a6c55 <+2148>: popq %r13
0x1064a6c57 <+2150>: popq %r14
0x1064a6c59 <+2152>: popq %r15
0x1064a6c5b <+2154>: popq %rbp
0x1064a6c5c <+2155>: retq
0x1064a6c5d <+2156>: callq 0x106d69e9c ; symbol stub for: __stack_chk_fail
調用函數的指令是call ,請參閱以下:
call function
參數中的function是文本段中的過程。 Call指令可以分為兩個步驟。第一步是在堆棧上推開call指令的下一個指令地址。在這裡,下一個地址實際上是在調用函數完成後的返回地址。第二步是跳到function 。 call指令等同於以下兩個說明:
push next_instruction
jmp function
以下是iOS模擬器中call指令的示例:
0x10915c714 <+68>: callq 0x1093ca502 ; symbol stub for: objc_msgSend
0x105206433 <+66>: callq *0xb3cd47(%rip) ; (void *)0x000000010475e800: objc_msgSend
上面的代碼顯示了兩種call指令的用法。在第一個用法中,操作數是一個內存地址,它實際上是Mach-O文件的符號存根。它可以通過動態鏈接器搜索函數的符號。在第二次用法中,實際上是通過間接地址模式獲得的操作數。此外,在AT&T語法中,需要將“跳/呼叫指令(或與程序員計數器相關的跳躍)中的直接操作數*添加到前綴中。
通常, ret指令用於將過程從調用函數返回到調用函數。該指令從堆棧頂部彈出地址,然後跳回該地址並繼續執行。在上面的示例中,它跳回next_instruction 。在執行ret指令之前,寄存器屬於調用功能。在函數調用過程的步驟6中已經提到了這一點。
大多數功能都具有可以是整數,浮點,指針等的參數。此外,函數通常具有返回值,可以表明執行結果是成功或失敗的。在OSX中,最多可以通過RDI,RSI,RDX,RCX,R8和R9的寄存器傳遞。超過6個參數的函數怎麼樣?當然,這種情況存在。如果發生這種情況,則可以使用堆棧以相反的順序保留其餘參數。 OSX具有八個浮點寄存器,可允許超過8個浮點參數。
關於函數的返回值, rax寄存器用於保存整數返回值。如果返回值為浮點,則應使用XMM0 -XMM1寄存器。下圖清楚地說明了函數調用期間的寄存器使用情況。

preserved across function calls表示是否需要在函數調用中保存寄存器。我們可以看到,除了上述RBX,R12 -R15寄存器之外,RSP和RBP寄存器也屬於Callee Saved寄存器。這是因為這兩個寄存器保留了指向程序堆棧的重要位置指針。
接下來,我們將按照一個真實的示例來說明函數調用中的說明。以CocoaLumberjack中的宏DDLogError為例。當調用此宏時,類方法log:level:flag:context:file:function:line:tag:format:被調用。以下代碼和說明是關於DDLogError的呼叫和相應的彙編說明:
- (IBAction)test:(id)sender {
DDLogError(@"TestDDLog:%@", sender);
}
0x102c568a3 <+99>: xorl %edx, %edx
0x102c568a5 <+101>: movl $0x1, %eax
0x102c568aa <+106>: movl %eax, %r8d
0x102c568ad <+109>: xorl %eax, %eax
0x102c568af <+111>: movl %eax, %r9d
0x102c568b2 <+114>: leaq 0x2a016(%rip), %rcx ; "/Users/dev-aozhimin/Desktop/TestDDLog/TestDDLog/ViewController.m"
0x102c568b9 <+121>: leaq 0x2a050(%rip), %rsi ; "-[ViewController test:]"
0x102c568c0 <+128>: movl $0x22, %eax
0x102c568c5 <+133>: movl %eax, %edi
0x102c568c7 <+135>: leaq 0x2dce2(%rip), %r10 ; @"eTestDDLog:%@"
0x102c568ce <+142>: movq 0x33adb(%rip), %r11 ; (void *)0x0000000102c8ad18: DDLog
0x102c568d5 <+149>: movq 0x34694(%rip), %rbx ; ddLogLevel
0x102c568dc <+156>: movq -0x30(%rbp), %r14
0x102c568e0 <+160>: movq 0x332f9(%rip), %r15 ; "log:level:flag:context:file:function:line:tag:format:"
0x102c568e7 <+167>: movq %rdi, -0x48(%rbp)
0x102c568eb <+171>: movq %r11, %rdi
0x102c568ee <+174>: movq %rsi, -0x50(%rbp)
0x102c568f2 <+178>: movq %r15, %rsi
0x102c568f5 <+181>: movq %rcx, -0x58(%rbp)
0x102c568f9 <+185>: movq %rbx, %rcx
0x102c568fc <+188>: movq -0x58(%rbp), %r11
0x102c56900 <+192>: movq %r11, (%rsp)
0x102c56904 <+196>: movq -0x50(%rbp), %rbx
0x102c56908 <+200>: movq %rbx, 0x8(%rsp)
0x102c5690d <+205>: movq $0x22, 0x10(%rsp)
0x102c56916 <+214>: movq $0x0, 0x18(%rsp)
0x102c5691f <+223>: movq %r10, 0x20(%rsp)
0x102c56924 <+228>: movq %r14, 0x28(%rsp)
0x102c56929 <+233>: movb $0x0, %al
0x102c5692b <+235>: callq 0x102c7d2be ; symbol stub for: objc_msgSend
由於Objective-C的所有功能都將變成objc_msgSend函數的調用,因此log:level:flag:context:file:function:line:tag:format:方法最終變成以下代碼:
objc_msgSend(DDLog, @selector(log:level:flag:context:file:function:line:tag:format:), asynchronous, level, flag, context, file, function, line, tag, format, sender)
我們已經提到最多6個寄存器可以用於參數傳遞。多餘的參數可以使用堆棧進行通過。由於以上函數具有6個以上的參數,因此參數傳遞將同時使用寄存器和堆棧。下面有兩個表描述寄存器的詳細用法和堆棧,以通過DDLogError函數調用的參數傳遞。
| 一般登記冊 | 價值 | 參數 | 組裝說明 | 評論 |
|---|---|---|---|---|
| rdi | ddlog | 自己 | 0x102C568EB <+171>:MOVQ%R11,%rdi | |
| RSI | “日誌:級別:標誌:上下文:文件:函數:line:tag:格式:” | OP | 0x102C568F2 <+178>:MOVQ%R15,%RSI | |
| RDX | 0 | 異步 | 0x102C568A3 <+99>:XORL%EDX,%EDX | Xorl是獨家或操作。在這裡,它用於清除EDX寄存器 |
| rcx | 18446744073709551615 | 等級 | 0x102C568F9 <+185>:movq%rbx,%rcx | (ddloglevelall或nsuintegermax) |
| R8 | 1 | 旗幟 | 0x102C568AA <+106>:movl%eax,%r8d | ddlogflagerror |
| R9 | 0 | 情境 | 0x102C568AF <+111>:movl%eax,%r9d |
| 堆棧框架偏移 | 價值 | 參數 | 組裝說明 | 評論 |
|---|---|---|---|---|
| (%RSP) | “ /users/dev-aozhimin/desktop/testddlog/testddlog/viewcontroller.m” | 文件 | 0x102C56900 <+192>:MOVQ%R11,(%RSP) | |
| 0x8(%RSP) | “ - [ViewController測試:]” | 功能 | 0x102C56908 <+200>:MOVQ%RBX,0x8(%RSP) | |
| 0x10(%RSP) | 0x22 | 線 | 0x102C5690D <+205>:MOVQ $ 0x22,0x10(%RSP) | Ddlogerror的相應調用在第34行 |
| 0x18(%RSP) | 0x0 | 標籤 | 0x102C56916 <+214>:MOVQ $ 0x0,0x18(%RSP) | 零 |
| 0x20(%RSP) | “ testddlog:%@” | 格式 | 0x102C5691F <+223>:MOVQ%R10,0x20(%RSP) | |
| 0x28(%RSP) | 發件人 | 可變參數的第一個參數 | 0x102C56924 <+228>:MOVQ%R14,0x28(%RSP) | Uibutton的實例 |
如果寄存器的值是字符串,例如
rsi寄存器中的op參數,則可以通過po (char *) $rsi命令在LLDB中直接打印字符串。否則,po $rsi可用於以整數格式打印值。
在彙編語言的幫助下,我們可以研究一些低級知識,這些知識在調試過程中非常必要。我非常努力地介紹與集會相關的知識。但是,集會的知識層次結構太大了,無法在一篇文章中描述。請參考上述參考文獻。此外,也強烈建議使用CSAPP的第三章 - 機器級別表示。這是一種罕見的好材料。
本文說明了通過真實情況調試的過程。一些細節已更改以保護個人隱私。
當我開發登錄SDK時,我們要談論的問題正在發生。一位用戶聲稱該應用在登錄頁面中按了“ QQ”按鈕時崩潰了。當我們討論此問題時,我們發現如果未安裝QQ應用程序,則發生了崩潰。當用戶按QQ按鈕需要登錄時,QQ登錄SDK試圖在我們的應用程序中啟動授權網頁。在這種情況下,出現未認可的選擇器錯誤[TCWebViewController setRequestURLStr:] 。
PS:要關注這個問題,下面未列出不必要的業務調試信息。同時, Aadebug用作我們的應用程序名稱。
這是此崩潰的堆棧痕跡:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TCWebViewController setRequestURLStr:]: unrecognized selector sent to instance 0x7fe25bd84f90'
*** First throw call stack:
(
0 CoreFoundation 0x0000000112ce4f65 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x00000001125f7deb objc_exception_throw + 48
2 CoreFoundation 0x0000000112ced58d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
3 AADebug 0x0000000108cffefc __ASPECTS_ARE_BEING_CALLED__ + 6172
4 CoreFoundation 0x0000000112c3ad97 ___forwarding___ + 487
5 CoreFoundation 0x0000000112c3ab28 _CF_forwarding_prep_0 + 120
6 AADebug 0x000000010a663100 -[TCWebViewKit open] + 387
7 AADebug 0x000000010a6608d0 -[TCLoginViewKit loadReqURL:webTitle:delegate:] + 175
8 AADebug 0x000000010a660810 -[TCLoginViewKit openWithExtraParams:] + 729
9 AADebug 0x000000010a66c45e -[TencentOAuth authorizeWithTencentAppAuthInSafari:permissions:andExtraParams:delegate:] + 701
10 AADebug 0x000000010a66d433 -[TencentOAuth authorizeWithPermissions:andExtraParams:delegate:inSafari:] + 564
………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………
Lines of irrelevant information are removed here
………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………
236
14 libdispatch.dylib 0x0000000113e28ef9 _dispatch_call_block_and_release + 12
15 libdispatch.dylib 0x0000000113e4949b _dispatch_client_callout + 8
16 libdispatch.dylib 0x0000000113e3134b _dispatch_main_queue_callback_4CF + 1738
17 CoreFoundation 0x0000000112c453e9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
18 CoreFoundation 0x0000000112c06939 __CFRunLoopRun + 2073
19 CoreFoundation 0x0000000112c05e98 CFRunLoopRunSpecific + 488
20 GraphicsServices 0x0000000114a13ad2 GSEventRunModal + 161
21 UIKit 0x0000000110d3f676 UIApplicationMain + 171
22 AADebug 0x0000000108596d3f main + 111
23 libdyld.dylib 0x0000000113e7d92d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
在談論調試之前,讓我們熟悉Objective-C中的信息轉發。眾所周知,Objective-C使用消息傳遞結構而不是函數調用。關鍵區別在於,在消息結構中,運行時決定將執行哪些函數,而不是編譯時間。這意味著,如果將未識別的消息發送到一個對象,則在編譯時間內將不會發生任何事情。在運行時,當它收到一種不了解的方法時,一個對象會通過消息轉發,該過程旨在使您作為開發人員講述消息如何處理未知消息。
在消息轉發過程中,通常涉及四種方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel :當將未知消息傳遞給對象時,將調用此方法。此方法將選擇未找到的選擇器並返回布爾值,以指示現在是否可以將實例方法添加到可以處理該選擇器的類中。如果課程可以處理此選擇器,請返回是,則消息轉發過程已完成。此方法通常用於以動態的方式訪問Coredata中NSManagedObjects的@Dynamic屬性。 + (BOOL)resolveClassMethod:(SEL)sel方法與上述方法相似,唯一的區別是該類方法,另一種是實例方法。
- (id)forwardingTargetForSelector:(SEL)aSelector :此方法提供了用於處理未知消息的第二個接收器,並且比forwardInvocation: 。該方法可用於模仿多個繼承的某些功能。請注意,無法使用轉發路徑的這一部分來操縱消息。如果在發送到替換接收器之前需要更改消息,則必須使用完整的轉發機制。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector :如果轉發算法已經到了這麼遠,則開始了完整的轉發機制。 NSMethodSignature由此方法返回,該方法在ASElector參數中包括方法描述。請注意,如果要在消息轉發過程中創建一個包含選擇器,目標和參數的NSInvocation對象,則需要覆蓋此方法。
- (void)forwardInvocation:(NSInvocation *)anInvocation :該方法的實現必須包含以下部分:找出可以處理andevocation消息的對象;將消息發送到該對象,AnInInvocation保存返回值,運行時將返回值發送到原始消息發送者。實際上,通過簡單地更改調用目標並將其調用,我們幾乎沒有這樣做,可以通過forwardingTargetForSelector:方法。
通常,將用於消息轉發的前兩種方法稱為快速轉發,因為它提供了一種更快的方法來進行消息轉發。為了區分快速轉發,方法3和4被稱為正常轉發或常規轉發。它要慢得多,因為它必須創建NSInInvocation對象才能完成消息轉發。
注意:如果未覆蓋
methodSignatureForSelector方法,或者返回的NSMethodSignaturenil為零,則不會調用forwardInvocation,並且消息轉發已通過do do dodoesNotRecognizeSelectorextered終止。我們可以從下面的__forwarding__函數的源代碼中看到它。
消息轉發過程可以通過流程圖描述,請參見下文。

就像流程圖中所述,在每個步驟中,接收器都有機會處理消息。每個步驟都比之前的步驟昂貴。最好的做法是儘早處理消息轉發過程。如果通過整個過程未處理消息,則提出了doesNotRecognizeSeletor誤差以說明對象無法識別選擇器。
是時候完成理論部分並回到問題了。
根據Trace堆棧中的TCWebViewController信息,我們自然將其與Tencent SDK Tencentopenapi.Framework相關聯,但是我們最近沒有更新Tencent SDK,這意味著崩潰並不是由Tencentopenapi.framework引起的。
首先,我們對代碼進行了分配,並獲得了TCWebViewController類的結構
@class TCWebViewController : UIViewController<UIWebViewDelegate, NSURLConnectionDelegate, NSURLConnectionDataDelegate> {
@property webview
@property webTitle
@property requestURLStr
@property error
@property delegate
@property activityIndicatorView
@property finished
@property theData
@property retryCount
@property hash
@property superclass
@property description
@property debugDescription
ivar _nloadCount
ivar _webview
ivar _webTitle
ivar _requestURLStr
ivar _error
ivar _delegate
ivar _xo
ivar _activityIndicatorView
ivar _finished
ivar _theData
ivar _retryCount
-setError:
-initWithNibName:bundle:
-dealloc
-stopLoad
-doClose
-viewDidLoad
-loadReqURL
-viewDidDisappear:
-shouldAutorotateToInterfaceOrientation:
-supportedInterfaceOrientations
-shouldAutorotate
-webViewDidStartLoad:
-webViewDidFinishLoad:
-webView:didFailLoadWithError:
-webView:shouldStartLoadWithRequest:navigationType:
}
從靜態分析結果中, requestURLStr TCWebViewController沒有setter和getter方法。因為以前的應用程序中沒有這樣的崩潰,所以我們提出了一個想法:以動態的方式實現了TCWebViewController中的屬性,該屬性使用@dynamic告訴編譯器在編譯時間內為屬性生成getter和setter在編譯時間內生成getter and setter,但在運行時在運行時間中動態創建的核心數據框架框架像核心數據框架一樣?然後,我們決定深入了解我們的猜測是否正確。在我們的跟踪過程中,我們發現tencentopenapi.framework中的NSObject有一個類別NSObject(MethodSwizzlingCategory) 。在此類別中,有一個方法switchMethodForCodeZipper ,其實現取代了QQmethodSignatureForSelector和forwardInvocation方法的methodSignatureForSelector和QQforwardInvocation方法。
void +[ NSObject switchMethodForCodeZipper ]( void * self, void * _cmd) {
rbx = self;
objc_sync_enter (self);
if (*( int8_t *)_g_instance == 0x0 ) {
[ NSObject swizzleMethod: @selector ( methodSignatureForSelector: ) withMethod: @selector ( QQmethodSignatureForSelector: )];
[ NSObject swizzleMethod: @selector ( forwardInvocation: ) withMethod: @selector ( QQforwardInvocation: )];
*( int8_t *)_g_instance = 0x1 ;
}
rdi = rbx;
objc_sync_exit (rdi);
return ;
}然後,我們一直在跟踪QQmethodSignatureForSelector方法,其中有一個名為_AddDynamicPropertysSetterAndGetter的方法。從名稱來看,我們可以輕鬆地獲得此方法是動態添加setter和getter方法。發現的可以實質性地驗證我們的原始猜測是正確的。
void * -[ NSObject QQmethodSignatureForSelector: ]( void * self, void * _cmd, void * arg2) {
r14 = arg2;
rbx = self;
rax = [ self QQmethodSignatureForSelector: rdx];
if (rax == 0x0 ) {
rax = sel_getName (r14);
_AddDynamicPropertysSetterAndGetter ();
rax = 0x0 ;
if ( 0x0 != 0x0 ) {
rax = [rbx methodSignatureForSelector: r14];
}
}
return rax;
}但是,為什麼在TCWebViewController類中無法識別設置器?是因為在我們開發此版本期間涵蓋了QQMethodSignatureForSelector方法嗎?但是,即使我們在代碼中無處不在,我們也找不到線索。那非常令人失望。到目前為止,靜態分析已經進行。下一步是使用LLDB動態調試Tencent SDK,以找出在消息轉發過程中創建Getter和Setter的路徑。
如果我們嘗試通過LLDB命令在
setRequestURLStr上設置斷點,我們將發現我們無法做到。原因是因為在編譯時間內不可用二傳器。這也可以驗證我們的原始猜測。
根據崩潰堆棧跟踪,我們可以得出結論setRequestURLStr在-[TCWebViewKit open]方法中,這意味著崩潰發生在Tencent SDK期間發生,檢查是否已安裝了QQ應用程序並打開身份驗證網頁進度。
然後,我們使用下面的LLDB命令在此方法上設置斷點:
br s -n "-[TCWebViewKit open]"
br s是breakpoint set的縮寫,-n表示根據其後的方法名稱設置斷點,該方法具有相同的行為,其行為具有符號斷點,br s -F也可以設置斷點。b -[TCWebViewKit open]也可以在這里工作,但是b這是_regexp-break的縮寫,它使用正則表達式設置斷點。最後,我們還可以在內存地址(例如br s -a 0x000000010940b24e上設置斷點,如果可用的地址可用,它可以幫助您調試塊。
到現在為止,斷點已成功設置。
Breakpoint 34: where = AADebug`-[TCWebViewKit open], address = 0x0000000103157f7d
當應用程序啟動Web身份驗證頁面時,該項目將在此斷點上停止。請參閱以下:

當應用程序在模擬器上運行時,將捕獲此屏幕截圖,因此彙編代碼基於X64。如果您使用的是iPhone設備,則彙編代碼應為ARM。但是分析方法對他們來說是相同的,請注意。
在第96行上設置斷點,此彙編代碼是setRequestURLStr方法調用,然後打印rbx寄存器的內容,然後我們可以觀察到TCWebViewController實例保存在此寄存器中。

接下來,我們可以使用LLDB設置QQmethodSignatureForSelector方法的斷點:
br s -n "-[NSObject QQmethodSignatureForSelector:]"
在LLDB中輸入c以使斷點繼續下去,然後斷點將在QQmethodSignatureForSelector方法內停止,這可以證明我們先前關於QQmethodSignatureForSelector方法與代碼衝突的猜測是無效的。

在QQmethodSignatureForSelector方法的末尾設置一個斷點,即第31行的retq命令。然後打印寄存器rax的內存地址,請參閱下面的屏幕截圖:

通過打印內存地址0x00007fdb36d38df0寄存器RAX的rax , NSMethodSignature對象可以返回。根據X86彙編語言的設計大會,返回值保存在寄存器rax中。顯然,調用了QQmethodSignatureForSelector方法並返回正確的值,這意味著我們需要繼續跟踪問題。
通過LLDB在QQforwardInvocation上設置斷點:
br s -n "-[NSObject QQforwardInvocation:]"
設置斷點後,繼續執行程序,應用程序崩潰。並且尚未調用QQforwardInvocation方法。這樣,我們可以得出結論, QQforwardInvocation方法與我們的代碼衝突。

___forwarding___函數包含消息轉發機制的整個實現,從Objective-C消息發送與轉發機制原理中選擇了代碼。在本文中,在forwardingTargetForSelector forwarding和receiver時,有一個判斷是不正確的。這應該是forwardingTarget和receiver之間的判斷。請參閱下面的代碼:
int __forwarding__(void *frameStackPointer, int isStret) {
id receiver = *(id *)frameStackPointer;
SEL sel = *(SEL *)(frameStackPointer + 8);
const char *selName = sel_getName(sel);
Class receiverClass = object_getClass(receiver);
// call forwardingTargetForSelector:
if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) {
id forwardingTarget = [receiver forwardingTargetForSelector:sel];
if (forwardingTarget && forwardingTarget != receiver) {
if (isStret == 1) {
int ret;
objc_msgSend_stret(&ret,forwardingTarget, sel, ...);
return ret;
}
return objc_msgSend(forwardingTarget, sel, ...);
}
}
// Zombie Object
const char *className = class_getName(receiverClass);
const char *zombiePrefix = "_NSZombie_";
size_t prefixLen = strlen(zombiePrefix); // 0xa
if (strncmp(className, zombiePrefix, prefixLen) == 0) {
CFLog(kCFLogLevelError,
@"*** -[%s %s]: message sent to deallocated instance %p",
className + prefixLen,
selName,
receiver);
<breakpoint-interrupt>
}
// call methodSignatureForSelector first to get method signature , then call forwardInvocation
if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) {
NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel];
if (methodSignature) {
BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct;
if (signatureIsStret != isStret) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.",
selName,
signatureIsStret ? "" : not,
isStret ? "" : not);
}
if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) {
NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer];
[receiver forwardInvocation:invocation];
void *returnValue = NULL;
[invocation getReturnValue:&value];
return returnValue;
} else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message",
receiver,
className);
return 0;
}
}
}
SEL *registeredSel = sel_getUid(selName);
// if selector already registered in Runtime
if (sel != registeredSel) {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort",
sel,
selName,
registeredSel);
} // doesNotRecognizeSelector
else if (class_respondsToSelector(receiverClass,@selector(doesNotRecognizeSelector:))) {
[receiver doesNotRecognizeSelector:sel];
}
else {
CFLog(kCFLogLevelWarning ,
@"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort",
receiver,
className);
}
// The point of no return.
kill(getpid(), 9);
}
基本上,我們可以通過閱讀解碼代碼來清楚了解:在消息轉發過程中首次調用forwardingTargetForSelector方法以獲取替換接收器,這也稱為快速轉發階段。如果forwardingTarget返回零或返回相同的接收器,則消息轉發會變成常規轉發階段。基本上,調用methodSignatureForSelector方法獲取方法簽名,然後將其與frameStackPointer一起使用以實例invocation對象。然後呼叫forwardInvocation: receiver的方法,並將上一個invocation像作為參數傳遞。最後,如果未實現methodSignatureForSelector方法,並且已經在運行時系統中註冊了selector , doesNotRecognizeSelector:將被調用以丟棄錯誤。
從崩潰堆棧跟踪中仔細檢查___forwarding___ forwarding___,我們可以注意到,它被稱為整個消息轉發路徑中的第二個路徑,這意味著當調用forwardInvocation時調用NSInvocation對象。
您還可以在斷點之後逐步執行命令,以觀察彙編代碼的執行路徑,應觀察到相同的結果。

當調用forwardInvocation時執行哪種方法?從堆棧跟踪中,我們可以看到一個名為__ASPECTS_ARE_BEING_CALLED__的方法。查看整個項目的這種方法,我們最終發現了forwardInvocation是通過Aspects框架吸引的。
static void aspect_swizzleForwardInvocation ( Class klass) {
NSCParameterAssert (klass);
// If there is no method, replace will act like class_addMethod.
IMP originalImplementation = class_replaceMethod (klass, @selector ( forwardInvocation: ), ( IMP )__ASPECTS_ARE_BEING_CALLED__, " v@:@ " );
if (originalImplementation) {
class_addMethod (klass, NSSelectorFromString (AspectsForwardInvocationSelectorName), originalImplementation, " v@:@ " );
}
AspectLog ( @" Aspects: %@ is now aspect aware. " , NSStringFromClass (klass));
} // This is the swizzled forwardInvocation: method.
static void __ASPECTS_ARE_BEING_CALLED__ (__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
NSLog ( @" selector: %@ " , NSStringFromSelector (invocation. selector ));
NSCParameterAssert (self);
NSCParameterAssert (invocation);
SEL originalSelector = invocation. selector ;
SEL aliasSelector = aspect_aliasForSelector (invocation. selector );
invocation. selector = aliasSelector;
AspectsContainer *objectContainer = objc_getAssociatedObject (self, aliasSelector);
AspectsContainer *classContainer = aspect_getContainerForClass ( object_getClass (self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc ] initWithInstance: self invocation: invocation];
NSArray *aspectsToRemove = nil ;
// Before hooks.
aspect_invoke (classContainer. beforeAspects , info);
aspect_invoke (objectContainer. beforeAspects , info);
// Instead hooks.
BOOL respondsToAlias = YES ;
if (objectContainer. insteadAspects . count || classContainer. insteadAspects . count ) {
aspect_invoke (classContainer. insteadAspects , info);
aspect_invoke (objectContainer. insteadAspects , info);
} else {
Class klass = object_getClass (invocation. target );
do {
if ((respondsToAlias = [klass instancesRespondToSelector: aliasSelector])) {
[invocation invoke ];
break ;
}
} while (!respondsToAlias && (klass = class_getSuperclass (klass)));
}
// After hooks.
aspect_invoke (classContainer. afterAspects , info);
aspect_invoke (objectContainer. afterAspects , info);
// If no hooks are installed, call original implementation (usually to throw an exception)
if (!respondsToAlias) {
invocation. selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString (AspectsForwardInvocationSelectorName);
if ([ self respondsToSelector: originalForwardInvocationSEL]) {
(( void ( *)( id , SEL , NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
} else {
[ self doesNotRecognizeSelector: invocation.selector];
}
}
// Remove any hooks that are queued for deregistration.
[aspectsToRemove makeObjectsPerformSelector: @selector ( remove )];
}由於TCWebViewController是Tencent SDK的私人類,因此不太可能直接掛上其他類。但是它的超類可能會吸引,這也可能影響該課程。有了這個猜想,我們一直在挖掘。最終,答案浮出水面!通過刪除或評論掛接UIViewController代碼,該應用程序通過QQ登錄時不會崩潰。到目前為止,我們絕對確定墜機是由Aspects框架所涉及的。

doesNotRecognizeSelector:錯誤是由__ASPECTS_ARE_BEING_CALLED__方法用於替換forwardInvocation:通過方面的方法。 __ASPECTS_ARE_BEING_CALLED__方法的實現具有相應的時間切片,而不是在Aspect之後。在上面的代碼中, aliasSelector是一個由方面處理的SEL,例如aspects__setRequestURLStr:
而是在掛鉤部分中,請調用。將檢查標題是否可以響應別名驅動器。如果子類無法響應,則將檢查超級階級,即超類的超類,直到根類。由於無法做出響應,因此反應是錯誤的。然後,將原始序列分配為調用的選擇。下一個OBJC_MSGSEND調用調用以調用原始SEL。由於TCWebViewController無法響應originalSelector:setRequestURLStr:方法,因此它最終運行到方面的方面方法seacts_are_being_called方法,並且do notrecognizeSelector:方法是相應的,這是我們在本文開頭談到的崩潰的根本原因。
一些仔細的讀者可能已經意識到崩潰可能與方面有關,因為在崩潰堆棧跟踪的第3行中看到行teacts_are_being_call 。我仍然在這裡列出所有嘗試的原因是,我希望您可以通過靜態分析和動態分析來學習如何從第三部分框架中找到問題。希望本文提到的技巧和技術對您有所幫助。
有兩種可用方法來解決崩潰。一個人正在鉤上侵入性較小的方面方法,例如方法散佈,然後在tencentopenapi的消息轉發過程中創建的設置器不會中斷。另一個是替換forwardInvocation:使用我們的實現,如果aliasSelector和``OriginalSelector都無法響應消息轉發,我們可以將消息轉發路徑轉發回原始路徑。請參閱下面的代碼:
if (!respondsToAlias) {
invocation. selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString (AspectsForwardInvocationSelectorName);
(( void ( *)( id , SEL , NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}實際上,方面與JSPATCH有衝突。由於這兩個SDK的實現也很相似, doesNotRecognizeSelector:當它們一起使用時也會發生。請參考微信讀書的文章。
此崩潰的根本原因是方面和tencopenapi框架之間的衝突。 UIViewController類的生命週期方法被方面掛鉤,並且forwardInvocation方法被方面的實現所取代。另外,由於TCWebViewController的超類是UIViewController類。結果, TCWebViewController類的QQforwardInvocation方法也被方面掛鉤。這導致消息轉發過程失敗了,因此,Getter和Setter的創建也失敗了。
這種情況告訴我們,我們不僅應該學習如何使用第三部分框架,而且還需要研究其機制。只有這樣,我們才能輕鬆地找到工作中遇到的問題。
我們在本文中介紹了各種技巧,但是我們希望您還可以在調試時掌握一種思考方式。技能很容易學習,但是您認為解決問題時的思維方式並不容易形成。它需要時間和練習。除了採用多種調試技術外,您還必須具有良好的問題分析感,那麼問題對您來說很方便。
特別感謝下面的讀者,我非常感謝您的支持和寶貴的建議。