
中文
デバッグの評判はかなり悪いです。つまり、開発者がプログラムを完全に理解していた場合、バグはありませんし、そもそも彼らはデバッグされないでしょう?
そのように考えないでください。
ソフトウェアには常にバグがあります。プロダクトマネージャーによって課されたテストカバレッジの量は、それを修正するつもりはありません。実際、デバッグを壊れたものを修正するプロセスとしての単なるプロセスとして見ることは、実際にはあなたの分析能力を精神的に妨げる有毒な考え方です。
代わりに、デバッグを単にプログラムをよりよく理解するプロセスとして表示する必要があります。それは微妙な違いですが、あなたが本当にそれを信じているなら、デバッグの以前のdrりは単に消えます。
COBOL言語の創設者であるGrace Hopperは、リレーコンピューターで世界初のバグを発見したため、ソフトウェア開発におけるバグの生成が止まりませんでした。本の序文であるAdvanced Apple Debugging&Reverse Engineeringの序文は、私たちに次のように語っています。開発者は、ソフトウェアの仕組みを十分に理解していれば、バグはないと考えたくありません。したがって、デバッグは、ソフトウェア開発ライフサイクルではほとんど避けられない段階です。
経験の浅いプログラマーにデバッグの定義方法について尋ねると、彼は「デバッグはあなたのソフトウェアの問題の解決策を見つけるためにあなたがすることです」と言うかもしれません。彼は正しいですが、それは本当のデバッグのほんの一部です。
これが実際のデバッグの手順です:
上記のステップの中で、最も重要なステップは最初のステップです。問題を見つけることです。どうやら、それは他のステップの前提条件です。
調査によると、経験豊富なプログラマーがデバッグに費やして、同じ欠陥のセットを見つけることは、経験の浅いプログラマーの約20個です。つまり、デバッグエクスペリエンスは、プログラミング効率に大きな違いをもたらすことを意味します。残念ながら、ソフトウェアのデザインに関する本がたくさんありますが、彼らの珍しいことは、学校のコースでさえ、デバッグについて紹介しています。
デバッガーが長年にわたって改善するにつれて、プログラマーのコーディングスタイルは徹底的に変更されます。もちろん、デバッガーは良い思考に取って代わることはできません。思考は優れたデバッガーに取って代わることはできません。最も完璧な組み合わせは、優れたデバッガーと良い思考です。
次のグラフは、本<デバッグ:最もとらえどころのないソフトウェアやハードウェアの問題を見つけるための9つの不可欠なルールで説明されている9つのデバッグルールです>。

iOSプログラマとしては、ほとんどの場合、作業中のほとんどの場合、アセンブリ言語を扱うことはありませんが、特にソースコードなしでシステムフレームワークまたはサードパーティフレームワークをデバッグする場合、アセンブリを理解することは非常に役立ちます。
Assembly Languageは、低レベルの機械指向のプログラミング言語であり、さまざまなCPUの機械命令用のニーモニックのコレクションと考えることができます。プログラマーは、アセンブリ言語を使用してコンピューターハードウェアシステムを直接制御できます。また、アセンブリ言語で書かれたプログラムには、実行速度が速く、メモリが少ないなど、多くのメリットがあります。
これまでのところ、AppleプラットフォームX86とARMで2つの主要なアーキテクチャが広く使用されています。アームアセンブリ言語を使用したモバイルデバイスでは、主にアームが低電力消費の利点を持つ左命令セットコンピューティング(RISC)アーキテクチャであるためです。 Mac OSのようなデスクトッププラットフォームは、X86アーキテクチャが使用されています。 iOSシミュレーターにインストールされているアプリは、実際にはシミュレータ内のMac OSアプリとして実行されています。つまり、シミュレータはコンテナのように機能しています。私たちのケースはiOSシミュレーターでデバッグされたため、主な研究目標はX86アセンブリ言語です。
X86アセンブリ言語は、Intel(X86プラットフォームドキュメントでは文書化)とAT&Tの2つの構文分岐に発生します。 IntelはMS-DOSとWindowsファミリーを支配しますが、AT&TはUNIXファミリーで一般的です。変数、定数、レジスタのアクセス、間接的なアドレス指定、オフセットなど、IntelとAT&Tの間には構文に大きな違いがあります。構文の違いは膨大ですが、ハードウェアシステムは同じです。つまり、そのうちの1つをシームレスに移行できます。 AT&Tアセンブリ言語はXcodeで使用されるため、以下のAT&Tに焦点を当てます。
Intel構文は、Hopper分解およびIDA Proの分解ツールで使用されていることに注意してください。
BelowsはIntelとAT&Tの違いです。
オペランドのプレフィックス:AT&T構文では、 %はレジスタの名前のプレフィックスとして使用され、 $即時オペランドのプレフィックスとして使用されますが、インテルのレジスタと即時オペランドの両方にプレフィックスは使用されません。もう1つの違いは、AT&Tの16進数のプレフィックスとして0xが追加されることです。以下のチャートは、プレフィックスの違いを示しています。
| AT&T | インテル |
|---|---|
| movq%rax、%rbx | mov rbx、rax |
| addq $ 0x10、%rsp | RSP、010Hを追加します |
Intelの構文では、
hサフィックスが16進動物に使用され、bサフィックスはバイナリオペランドに使用されます。
オペランド:AT&T構文では、最初のオペランドはソースオペランドであり、2番目のオペランドは宛先オペランドです。ただし、Intel構文では、オペランドの順序は反対です。この時点から、AT&Tの構文は、読書の習慣に従って私たちにとってより快適です。
アドレス指定モード:Intelの構文と比較すると、AT&Tの間接的なアドレス指定モードを読みにくいです。ただし、アドレス計算のアルゴリズムは同じです。 address = disp + base + index * scale 。 baseベースアドレスを表し、 dispオフセットアドレスの略で、 index * scale要素の位置を決定し、 scale 2つのパワーである要素のサイズです。 disp/base/index/scaleすべてオプションです。 indexの%segreg: disp(base,index,scale)値は0、 scaleのデフォルト値は1です。アドレス計算のsegreg: [base+index*scale+disp]を見てみましょう。実際、2つの上記の2つの命令はどちらもセグメントアドレス指定モードに属します。 segreg 、CPUの桁容量が登録型 '桁にアドレス指定するときに通常実際のモードで使用されるセグメントレジスタの略です。たとえば、CPUは20ビットスペースに対応できますが、レジスタには16ビットしかありません。 20桁のスペースを達成するには、別のアドレス指定モードを使用する必要があります。Segreg 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の場所に来る場合、$サフィックスは省略できます。 Intel Syntaxでは、byte ptr、word ptr、dword ptr、qword ptrメモリオペランドの前に追加する必要があります。
OpCodeの接尾辞:AT&T構文では、すべてのオプコードにはサイズを指定するサフィックスがあります。一般に、4種類の接尾辞があります: 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、ax |
| movl%eax、%ebx | MOV EBX、EAX |
| movq%rax、%rbx | mov rbx、rax |
私たちが知っているように、メモリはCPUの命令とデータを保存するために使用されます。メモリは本質的にバイトの配列です。メモリアクセスの速度は非常に高速ですが、CPUの命令実行をスピードアップするために、より小さく、より高速なストレージユニットが必要です。命令実行中、すべてのデータは一時的にレジスタに保存されます。そのため、レジスタの名前が付けられています。
プロセッサが16ビットから32ビットに成長すると、8つのレジスタも32ビットに拡張されます。その後、拡張レジスタが使用されると、 Eプレフィックスが元のレジスタ名に追加されます。 32ビットプロセッサはIntelアーキテクチャ32ビットで、IA32です。今日、メインプロセッサは64ビットIntelアーキテクチャであり、IA32から拡張され、X86-64と呼ばれています。 IA32は過去であるため、この記事はX86-64にのみ焦点を当てます。 x86-64では、レジスタの量は8から16に拡張されます。この拡張のために、プログラム状態はレジスタに保存できますが、スタックではありません。したがって、メモリアクセスの頻度は大幅に減少します。
X86-64には、16の64ビットの一般レジスタと16のフローティングポインターレジスタがあります。また、CPUには、 ripと呼ばれるもう1つの64ビット命令ポインターレジスタがあります。次に実行された命令のアドレスを保存するように設計されています。また、広く使用されていない他のレジスタもいくつかあります。この記事ではそれらについて話すつもりはありません。 16の一般的なレジスタのうち、そのうち8つはIA32からのものです:Rax、rcx、rdx、rbx、 rsi rdi rdi rspおよびrbp。他の8つの一般的なレジスタは、R8 -R15であるX86-64以降、新しく追加されています。 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:xmm0 -xmm15には16のフローティングポインターレジスタがあります。実際、その他の詳細がいくつかあります。 register read -aコマンドの出力では、XMMレジスタグループ以外にSTMMおよびYMMレジスタがあることに気付く場合があります。ここでは、STMMはSTレジスタのエイリアスであり、STはX86のFPU(Float Point Unit)のレジスタであり、フロートデータを処理します。 FPUには、80ビットのフロートポインターレジスタが8つあるフロートポインターレジスタが1つ含まれています。ST0 -ST7。 STMMレジスタは出力から80ビットであることがわかります。これにより、STMMレジスタがSTレジスタであることが証明できます。 XMMは128ビットレジスタで、YMMレジスタはXMMの拡張機能である256ビットです。実際、XMMレジスタはYMMレジスタの128ビットの低いものです。 EAXレジスタのように、RAXレジスタの32ビットが低い。 Pentium IIIでは、IntelはMMXの拡張であるSSE(Streaming SIMD拡張機能)という命令セットを公開しました。 8つの新しい128ビットレジスタ(XMM0 -XMM7)がSSEに追加されています。 AVX(Advanced Vector Extensions)命令セットは、SSEの拡張アーキテクチャです。また、AVXでは、128ビットレジスタXMMが256ビットレジスタYMMに拡張されました。

関数呼び出しには、パラメーターの渡されて、あるコンピレーションユニットから別のコンパイルユニットへの制御転送が含まれます。関数呼び出し手順では、データの合格、ローカル変数の割り当てとリリースがStackによって実行されます。単一の関数呼び出しに割り当てられたスタックは、スタックフレームと呼ばれます。
OS X X86-64の関数呼び出し条約は、記事に記載されている規則と同じです。SystemVアプリケーションバイナリインターフェイス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コマンドはスタックフレームで実行可能です。スタックフレームには、関数実行のコンテキストと見なすことができる関数用の返信アドレスとローカル変数を保持します。私たちが知っているように、ヒープは上向きに成長しますが、スタックは多数のメモリアドレスから少数のメモリアドレスまで下向きに成長します。関数が呼び出されると、関数呼び出しに1つのスタンドアロンスタックフレームが割り当てられます。フレームポインターと呼ばれる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
これらの2つのステップが各関数呼び出しに沿っていることに気付くのは簡単です。上記の別の詳細があります。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命令は2つのステップに分割できます。最初のステップは、Stackの次の命令call命令アドレスをプッシュすることです。ここで、次のアドレスは実際には、呼び出された関数が終了した後の返品アドレスです。 2番目のステップは、 functionへのジャンプです。 call命令は、2つの命令以下に相当します。
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命令の2つの使用法を示しています。最初の使用法では、オペランドは、実際にはMACH-Oファイルのシンボルスタブであるメモリアドレスです。動的リンカーを介して関数のシンボルを検索できます。 2番目の使用法では、オペランドは実際には間接アドレス指定モードによって取得されます。さらに、AT&T構文では、接頭辞としてジャンプ/コール命令(またはプログラマーカウンターに関連するジャンプ)の即時オペランドに*追加する必要があります。
一般に、 ret命令は、呼び出した関数から呼び出し関数に手順を返すために使用されます。この命令は、スタックの上部からアドレスをポップし、そのアドレスに戻り、実行を続けます。上記の例では、 next_instructionに戻ります。 ret命令が実行される前に、レジスタは呼び出し関数に属します。これは、関数呼び出し手順のステップ6ですでに言及されています。
ほとんどの関数には、整数、フロート、ポインターなどのパラメーターがあります。その上、関数は通常、実行結果が成功または失敗したことを示すことができる返品値を持っています。 OSXでは、最大で6つのパラメーターをRDI、RSI、RDX、RCX、R8、R9のレジスタを順番に渡すことができます。 6つ以上のパラメーターを持つ関数はどうですか?もちろん、この状況は存在します。これが発生した場合、スタックを使用して、残りのパラメーターを反転した順序で保存できます。 OSXには、最大8つのフロートパラメーターを渡すことができる8つのフローティングポイントレジスタがあります。
関数の返品値について、 raxレジスタは整数返品値を保存するために使用されます。戻り値がフロートの場合、XMM0 -XMM1レジスタを使用するものとします。以下のチャートは、関数呼び出し中のレジスタ使用規則を明確に示しています。

preserved across function callsは、レジスタを関数呼び出し全体で保存する必要があるかどうかを示します。 RBX、R12 -R15上記のR15レジスタ以外に、RSPおよびRBPレジスタもCalleeが採用したレジスタに属していることがわかります。これは、これらの2つのレジスタがプログラムスタックを指す重要な位置ポインターを予約するためです。
次に、実際の例に従って、関数呼び出しの指示を示します。例として、 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: methodが最終的にコードに変換します。
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つ以上のパラメーターがあるため、パラメーターの通過はレジスタとスタックの両方を使用します。 2つのテーブルの下には、 DDLogError関数の呼び出しのパラメーターの通過のためのレジスタの詳細な使用法とスタックについて説明します。
| 一般登録 | 価値 | パラメーター | アセンブリの指示 | コメント |
|---|---|---|---|---|
| RDI | ddlog | 自己 | 0x102C568EB <+171>:movq%r11、%rdi | |
| rsi | 「ログ:レベル:フラグ:コンテキスト:ファイル:function:line:tag:format: " | 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) | nil |
| 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使用して、整数形式で値を印刷できます。
アセンブリ言語の助けを借りて、デバッグ中に非常に必要な低レベルの知識を調べることができます。できる限り詳細なアセンブリ関連の知識を紹介するように一生懸命努力しています。ただし、アセンブリの知識階層は、1つの記事で説明するには膨大すぎます。上記の参照を参照してください。さらに、 CSAPPの第3章 - プログラムの機械レベル表現も強くお勧めします。それは参照のための珍しい良い資料です。
この記事では、実際のケースを介してデバッグする手順を示しています。一部の詳細は、個人のプライバシーを保護するために変更されます。
私たちが話したい問題は、私がログインSDKを開発していたときに起こっていました。 1人のユーザーが、ログインページの「QQ」ボタンを押したときにアプリがクラッシュしたと主張しました。この問題をデバッグしたとき、QQアプリが同時にインストールされていない場合、クラッシュが発生することがわかりました。ユーザーがQQボタンを押してログインを要求すると、QQログインSDKはアプリで承認Webページを起動しようとします。この場合、認識されていないセレクターエラー[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は関数呼び出しではなくメッセージング構造を使用します。重要な違いは、メッセージング構造で、ランタイムが時間をコンパイルしないで実行される関数を決定することです。つまり、認識されていないメッセージが1つのオブジェクトに送信された場合、コンパイル時に何も起こりません。また、ランタイム中に、理解できない方法を受信すると、オブジェクトはメッセージ転送を通過します。これは、開発者としてメッセージをメッセージに伝えることができるように設計されたプロセスです。
通常、メッセージの転送中に4つのメソッドが関係しています。
+ (BOOL)resolveInstanceMethod:(SEL)sel :この方法は、不明なメッセージがオブジェクトに渡されたときに呼び出されます。このメソッドは、見つからなかったセレクターを取り、ブール値を返して、そのセレクターを処理できるインスタンスメソッドがクラスに追加されたかどうかを示します。クラスがこのセレクターを処理できる場合は、はいを返し、メッセージフォワードプロセスが完了します。この方法は、CoreDataのNSManagedObjectsの@Dynamicプロパティに動的にアクセスするためによく使用されます。 + (BOOL)resolveClassMethod:(SEL)selメソッドは上記の方法で類似しています。唯一の違いは、この1つのクラスの方法、もう1つはインスタンスメソッドです。
- (id)forwardingTargetForSelector:(SEL)aSelector :この方法は、UNKNOKメッセージを処理するための2番目の受信機を提供しますforwardInvocation:この方法は、複数の継承のいくつかの機能を模倣するために使用できます。転送パスのこの部分を使用してメッセージを操作する方法はないことに注意してください。交換用受信機に送信する前にメッセージを変更する必要がある場合は、完全な転送メカニズムを使用する必要があります。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector :転送アルゴリズムがここまで来た場合、完全な転送メカニズムが開始されます。 NSMethodSignatureは、Aselectorパラメーターのメソッド説明を含むこの方法によって返されます。メッセージ転送中にセレクター、ターゲット、および引数を含むNSInvocationオブジェクトを作成する場合は、この方法をオーバーライドする必要があることに注意してください。
- (void)forwardInvocation:(NSInvocation *)anInvocation :このメソッドの実装には、以下に含まれている必要があります。そのオブジェクトにメッセージを送信すると、Aninvocationは返品値を保存し、ランタイムは元のメッセージ送信者に返品値を送信します。実際、この方法は、 forwardingTargetForSelector: 「呼び出しターゲットを変更してその後呼び出すだけですが、それはほとんど行いません。
通常、メッセージ転送に使用される最初の2つの方法は、メッセージ転送を行うためのはるかに高速な方法を提供するため、早送りと呼ばれます。早送りと区別するために、方法3と4は通常の転送または定期的な転送と呼ばれます。メッセージ転送を完了するためにnsinvocationオブジェクトを作成する必要があるため、はるかに遅いです。
注:
methodSignatureForSelectorメソッドがオーバーライドされていない場合、または返されたNSMethodSignatureがゼロである場合、forwardInvocationは呼び出されず、doesNotRecognizeSelector転送が終了します。以下の__forwarding__関数のソースコードから見ることができます。
メッセージ転送のプロセスは、フロー図で説明できます。以下を参照してください。

フロー図に記載されているように、各ステップで、受信機にメッセージを処理する機会が与えられます。各ステップは、その前のステップよりも高価です。ベストプラクティスは、できるだけ早くメッセージ転送プロセスを処理することです。メッセージがプロセス全体を通じて処理されない場合、セレクターがオブジェクトによって認識できないと述べるために、 doesNotRecognizeSeletorエラーが発生しないことを認識していません。
理論の部分を完了し、問題に戻る時が来ました。
TCWebViewController情報によると、Trace Stackからの情報によると、自然にTencent SDK Tencentopenapi.frameworkに関連付けられていますが、最近Tencent SDKを更新しませんでした。
最初に、コードを逆コンパイルして、 TCWebViewControllerクラスのstructを取得しました
@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:
}
静的分析結果から、 TCWebViewControllerにはrequestURLStrのセッターとゲッター方法がありませんでした。以前のアプリバージョンにはそのようなクラッシュがなかったため、アイデアが出てきましたTCWebViewControllerのプロパティは、 @dynamic Dynamicを使用してコンパイラがコンパイル時にプロパティのGetterとSetterを生成しないが、コアデータフレームワークのように動的に作成されていないことを伝える動的な方法で実装されましたか?それから、私たちの推測が正しいかどうかを確認するために、アイデアを深く進めることにしました。追跡中、 Tencentopenapi.frameworkのNSObjectのカテゴリNSObject(MethodSwizzlingCategory)があり、非常に疑わしいことがわかりました。このカテゴリには、 QQmethodSignatureForSelectorおよびQQforwardInvocationメソッドのmethodSignatureForSelectorおよびforwardInvocationメソッドを実装したswitchMethodForCodeZipperメソッドがありました。
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という名前の方法がありました。名前から、この方法は、プロパティのセッターとゲッターメソッドを動的に追加することであることを簡単に入手できます。これにより、元の推測が正しいことを大幅に確認できます。
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のBreakPointを設定しようとすると、作成できないことがわかります。その理由は、コンパイル時にセッターが利用できないためです。これにより、元の推測を検証することもできます。
Crash Stack Traceによると、 setRequestURLStrが呼び出されていると結論付けることができます-[TCWebViewKit open]メソッドは、QQアプリがインストールされているかどうかを確認し、認証Webページの進行状況を開くTencent SDKの間にクラッシュが発生することを意味します。
次に、以下のLLDBコマンドを使用して、このメソッドのブレークポイントを設定します。
br s -n "-[TCWebViewKit open]"
br sbreakpoint setの略語であり、-n、その後のメソッド名に従ってbr s -Fポイントのセットを表します。b -[TCWebViewKit open]もここで動作しますが、bは_regexp-breakの略語であり、正規表現を使用してブレークポイントを設定します。最後に、br s -a 0x000000010940b24eのようなメモリアドレスにブレークポイントを設定することもできます。これは、ブロックのアドレスが使用可能な場合にブロックをデバッグするのに役立ちます。
今では、ブレークポイントが正常に設定されています。
Breakpoint 34: where = AADebug`-[TCWebViewKit open], address = 0x0000000103157f7d
アプリがWeb認証ページを起動する場合、このブレークポイントでプロジェクトが停止します。以下を参照してください:

このスクリーンショットは、AppがSimulatorで実行されているときにキャプチャされるため、アセンブリコードはX64に基づいています。 iPhoneデバイスを使用している場合、アセンブリコードはARMでなければなりません。しかし、分析方法は彼らにとって同じです、それに注意してください。
96行でブレークポイントを設定すると、このアセンブリコードはsetRequestURLStrメソッドの呼び出しであり、 rbxレジスタのコンテンツを印刷してから、 TCWebViewControllerインスタンスがこのレジスタに保存されていることを確認できます。

次に、LLDBを使用して、 QQmethodSignatureForSelectorメソッドのブレークポイントを設定できます。
br s -n "-[NSObject QQmethodSignatureForSelector:]"
LLDBにcを入力してブレークポイントを継続させ、ブレークポイントはQQmethodSignatureForSelectorメソッド内で停止します。これは、コードと矛盾するQQmethodSignatureForSelectorメソッドに関する以前の推測が無効であることを証明できます。

QQmethodSignatureForSelectorメソッドの最後にブレークポイントを設定します。つまり、31行目のretqコマンドです。次に、レジスタraxのメモリアドレスを印刷して、以下のスクリーンショットを参照してください。

メモリアドレスを印刷することにより、 0x00007fdb36d38df0レジスタrax 、 NSMethodSignatureオブジェクトが返されます。 X86アセンブリ言語に関する設計条約によると、リターン値はRegister 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がnilを返したり、同じ受信機を返したりすると、メッセージ転送は通常の転送段階に変わります。基本的に、 methodSignatureForSelectorメソッドを呼び出してメソッドの署名を取得し、 frameStackPointerで使用してinvocationをインスタンス化します。次に、 forwardInvocation: receiverのメソッドを呼び出し、以前のinvocationオブジェクトを引数として渡します。最終的に、 methodSignatureForSelectorメソッドが実装されておらず、 selectorがランタイムシステムに既に登録されている場合、エラーをスローするためにdoesNotRecognizeSelector:れます。
クラッシュスタックトレースから___forwarding___精査すると、 NSInvocation forwardInvocationパス全体の2番目のパスと呼ばれていることがわかります。
また、ブレークポイントの後にコマンドを段階的に実行して、アセンブリコードの実行パスを観察することもできます。同じ結果を観察する必要があります。

そして、 forwardInvocationが呼び出されたときにどの方法が実行されますか?スタックトレースから、 __ASPECTS_ARE_BEING_CALLED__という名前のメソッドが実行されます。プロジェクト全体のこの方法を見ると、最終的には、 Aspects FrameworkにforwardInvocationがフックされていることがわかります。
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: error is thrown by __ASPECTS_ARE_BEING_CALLED__ method which is used to replace the IMP of forwardInvocation: method by Aspects . The implementation of __ASPECTS_ARE_BEING_CALLED__ method has the corresponding time slice for before, instead and after the hooking in Aspect . Among above code, aliasSelector is a SEL which is handled by Aspects , like aspects__setRequestURLStr: .
In Instead hooks part, invocation.target will be checked if it can respond to aliasSelector. If subclass cannot respond, the superclass will be checked, the superclass's superclass, until root class. Since the aliasSelector cannot be responded, respondsToAlias is false. Then originalSelector is assigned to be a selector of invocation. Next objc_msgSend invokes the invocation to call the original SEL. Since TCWebViewController cannot respond the originalSelector:setRequestURLStr: method, it finally runs to ASPECTS_ARE_BEING_CALLED method of Aspects and doesNotRecognizeSelector: method is threw accordingly, which is the root cause of the crash we talked about in the beginning of this article.
Some careful reader might already realize the crash could be involved with Aspects, since seeing line ASPECTS_ARE_BEING_CALLED at line 3 of the crash stack trace. The reason I still listed all the attempts here is that I hope you can learn how to locate a problem from a third-part framework without source code through static analysis and dynamic analysis. Hope the tricks and technology mentioned in this article can be helpful for you.
There are two available ways to fix the crash. One is hooking the method of Aspects which is less invasive, for example Method Swizzling, then the setter creation during the message forwarding process for TencentOpenAPI would not be interrupted. Another is replace forwardInvocation: with ours implementation, if both aliasSelector and ``originalSelector cannot response to the message forwarding, we can forward the message forwarding path back into the original path. Refer to the code below:
if (!respondsToAlias) {
invocation. selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString (AspectsForwardInvocationSelectorName);
(( void ( *)( id , SEL , NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
} In fact, Aspects has conflicts with JSPatch . Since the implementation of these two SDK are similar too, doesNotRecognizeSelector: happens too when they are used together. Please Refer to 微信读书的文章.
The root cause of this crash is the conflict between Aspects and TencentOpenAPI frameworks. The life cycle method of UIViewController class is hooked by Aspects, and the forwardInvocation method is replaced with the Aspects's implementation. Also, because of the superclass of TCWebViewController is UIViewController class. As a result, QQforwardInvocation method of TCWebViewController class is hooked by Aspects too. That leads to the message forwarding process failed, thus, the creation of getter and setter fails too.
This case tells us, we should not only learn how to use a third-part framework, but also need to look into the mechanism of it. Only then, we can easily to locate the problem we meet during our work.
We introduce different kinds of tips in this article, but we hope you can also master a way of thinking when debugging. Skills are easy to be learned, but the way you think when resolving problem is not easy to be formed. It takes time and practice. Besides kinds of debugging techniques, you also have to have a good sense of problem analysis, then the problem will be handy for you.
Special thanks to below readers, I really appreciate your support and valuable suggestions.