
中文
การดีบักมีชื่อเสียงค่อนข้างแย่ ฉันหมายความว่าถ้านักพัฒนามีความเข้าใจที่สมบูรณ์เกี่ยวกับโปรแกรมจะไม่มีข้อบกพร่องใด ๆ และพวกเขาจะไม่ดีบั๊กในตอนแรกใช่ไหม?
อย่าคิดแบบนั้น
จะมีข้อบกพร่องในซอฟต์แวร์ของคุณเสมอหรือซอฟต์แวร์ใด ๆ สำหรับเรื่องนั้น ไม่มีจำนวนการทดสอบความครอบคลุมที่กำหนดโดยผู้จัดการผลิตภัณฑ์ของคุณจะแก้ไขได้ ในความเป็นจริงการดูการดีบักเป็นเพียงกระบวนการในการแก้ไขบางสิ่งที่แตกหักเป็นวิธีคิดที่เป็นพิษที่จะขัดขวางความสามารถในการวิเคราะห์ของคุณ
แต่คุณควรดูการดีบัก เป็นเพียงกระบวนการที่จะเข้าใจโปรแกรมได้ดีขึ้น มันเป็นความแตกต่างเล็กน้อย แต่ถ้าคุณเชื่ออย่างแท้จริงความน่าเบื่อหน่ายก่อนหน้าของการดีบักก็หายไป
ตั้งแต่ Grace Hopper ผู้ก่อตั้ง Cobol Language ค้นพบข้อผิดพลาดแรกของโลกในคอมพิวเตอร์รีเลย์การสร้างข้อผิดพลาดในการพัฒนาซอฟต์แวร์ไม่เคยหยุด ในฐานะที่เป็นคำนำของหนังสือ《 Apple Apple Debugging & Reverse Engineering》 บอกเราว่า: นักพัฒนาไม่ต้องการคิดว่าหากมีความเข้าใจที่ดีเกี่ยวกับวิธีการทำงานของซอฟต์แวร์จะไม่มีข้อบกพร่อง ดังนั้นการดีบักจึงเกือบจะเป็นช่วงที่หลีกเลี่ยงไม่ได้ในวงจรชีวิตการพัฒนาซอฟต์แวร์
หากคุณถามโปรแกรมเมอร์ที่ไม่มีประสบการณ์เกี่ยวกับวิธีการกำหนดการดีบักเขาอาจพูดว่า "การดีบักเป็นสิ่งที่คุณทำเพื่อหาวิธีแก้ปัญหาซอฟต์แวร์ของคุณ" เขาพูดถูก แต่นั่นเป็นเพียงส่วนเล็ก ๆ ของการดีบักที่แท้จริง
นี่คือขั้นตอนของการดีบักจริง:
ในขั้นตอนข้างต้นขั้นตอนที่สำคัญที่สุดคือขั้นตอนแรก: ค้นหาปัญหา เห็นได้ชัดว่ามันเป็นข้อกำหนดเบื้องต้นของขั้นตอนอื่น ๆ
การวิจัยแสดงให้เห็นถึงเวลาที่โปรแกรมเมอร์ที่มีประสบการณ์ใช้จ่ายในการดีบักเพื่อค้นหาชุดของข้อบกพร่องชุดเดียวกันคือประมาณหนึ่งยี่สิบของโปรแกรมเมอร์ที่ไม่มีประสบการณ์ นั่นหมายถึงประสบการณ์การดีบักทำให้ประสิทธิภาพการเขียนโปรแกรมแตกต่างอย่างมาก เรามีหนังสือมากมายเกี่ยวกับการออกแบบซอฟต์แวร์โชคไม่ดีที่หายากมีการแนะนำเกี่ยวกับการดีบักแม้แต่หลักสูตรในโรงเรียน
เมื่อดีบักเกอร์ดีขึ้นในช่วงหลายปีที่ผ่านมารูปแบบการเข้ารหัสของโปรแกรมเมอร์จะเปลี่ยนไปอย่างละเอียด แน่นอนว่าดีบักเกอร์ไม่สามารถแทนที่การคิดที่ดีการคิดไม่สามารถแทนที่ดีบักเกอร์ที่ยอดเยี่ยมการผสมผสานที่สมบูรณ์แบบที่สุดคือดีบักเกอร์ที่ยอดเยี่ยมด้วยการคิดที่ดี
กราฟต่อไปนี้เป็นกฎการดีบักเก้าที่อธิบายไว้ในหนังสือ <การดีบัก: กฎ 9 ข้อที่ขาดไม่ได้สำหรับการค้นหาแม้กระทั่งปัญหาซอฟต์แวร์และฮาร์ดแวร์ที่เข้าใจยากที่สุด>

แม้ว่าในฐานะโปรแกรมเมอร์ iOS ส่วนใหญ่ในการทำงานจะไม่จัดการกับภาษาแอสเซมบลี แต่เข้าใจว่าการชุมนุมยังคงมีประโยชน์มากโดยเฉพาะอย่างยิ่งเมื่อทำการดีบักกรอบระบบหรือเฟรมเวิร์กของบุคคลที่สามโดยไม่มีซอร์สโค้ด
Asssembly Language เป็นภาษาการเขียนโปรแกรมที่เน้นเครื่องจักรระดับต่ำซึ่งสามารถคิดได้ว่าเป็นคอลเลกชันของ mnemonics สำหรับคำแนะนำเครื่องสำหรับซีพียูต่างๆ โปรแกรมเมอร์สามารถใช้ภาษาแอสเซมบลีเพื่อควบคุมระบบฮาร์ดแวร์คอมพิวเตอร์โดยตรง และโปรแกรมที่เขียนในภาษาแอสเซมบลีมีข้อดีมากมายเช่นความเร็วในการดำเนินการที่รวดเร็วและหน่วยความจำที่น้อยลง
จนถึงขณะนี้มีการใช้สถาปัตยกรรมที่สำคัญสองอย่างบนแพลตฟอร์ม Apple, X86 และ ARM ในอุปกรณ์มือถือโดยใช้ภาษาแอสเซมบลีแขนซึ่งส่วนใหญ่เป็นเพราะแขนเป็นสถาปัตยกรรมชุดคำสั่งที่ลดลง (RISC) โดยมีความได้เปรียบในการใช้พลังงานต่ำ ในขณะที่แพลตฟอร์มเดสก์ท็อปเช่น Mac OS ใช้สถาปัตยกรรม x86 แอพที่ติดตั้งบน iOS Simulators กำลังทำงานเป็นแอพ Mac OS ภายในตัวจำลองซึ่งหมายความว่า Simulator ทำงานเหมือนคอนเทนเนอร์ เนื่องจากกรณีของเราถูกดีบักในเครื่องจำลอง iOS เป้าหมายการวิจัยหลักคือภาษาแอสเซมบลี x86
X86 ภาษาแอสเซมบลีกลายเป็นสองสาขาไวยากรณ์: Intel (ใช้ในเอกสารแพลตฟอร์ม x86) และ AT&T Intel ครองครอบครัว MS-DOS และ Windows ในขณะที่ AT&T เป็นเรื่องธรรมดาในตระกูล UNIX มีความแตกต่างอย่างมากเกี่ยวกับไวยากรณ์ระหว่าง Intel และ AT&T เช่นตัวแปรคงที่การเข้าถึงการลงทะเบียนการกำหนดที่อยู่ทางอ้อมและการชดเชย แม้ว่าความแตกต่างทางไวยากรณ์ของพวกเขาจะมหาศาล แต่ระบบฮาร์ดแวร์ก็เหมือนกันซึ่งหมายความว่าหนึ่งในนั้นสามารถย้ายไปยังอีกอย่างหนึ่งได้อย่างราบรื่น เนื่องจากภาษาแอสเซมบลี AT&T ใช้บน XCode เราจะมุ่งเน้นไปที่ AT&T ในส่วนล่าง
โปรดสังเกตว่า Intel Syntax ใช้ในเครื่องมือถอดประกอบของการถอดชิ้นส่วนกระโดดและ IDA Pro
ความเชื่อคือความแตกต่างระหว่าง Intel และ AT&T:
คำนำหน้าของตัวถูกดำเนินการ: ในไวยากรณ์ AT&T, % ถูกใช้เป็นคำนำหน้าของชื่อทะเบียนและ $ ใช้เป็นคำนำหน้าของตัวถูกดำเนินการทันทีในขณะที่ไม่มีคำนำหน้าใช้สำหรับทั้งการลงทะเบียนและตัวถูกดำเนินการทันทีใน Intel ความแตกต่างอื่น ๆ คือ 0x จะถูกเพิ่มเป็นคำนำหน้าสำหรับเลขฐานสิบหกใน AT&T แผนภูมิด้านล่างแสดงให้เห็นถึงความแตกต่างระหว่างคำนำหน้า:
| AT&T | Intel |
|---|---|
| movq %rax, %rbx | Mov Rbx, Rax |
| addq $ 0x10, %rsp | เพิ่ม rsp, 010h |
ใน Intel Syntax จะใช้
hต่อท้ายสำหรับ hexadecimal operand และbต่อท้าย B ใช้สำหรับ binary operand
ตัวถูกดำเนินการ: ในไวยากรณ์ AT&T ตัวดำเนินการแรกคือตัวถูกดำเนินการแหล่งที่มาตัวถูกดำเนินการที่สองคือตัวถูกดำเนินการปลายทาง อย่างไรก็ตามในไวยากรณ์ของ Intel ลำดับของตัวถูกดำเนินการอยู่ตรงข้าม จากจุดนี้ไวยากรณ์ของ AT&T นั้นสะดวกสบายมากขึ้นสำหรับเราตามนิสัยการอ่านของเรา
โหมดที่อยู่: การเปรียบเทียบกับ Intel Syntax โหมดที่อยู่ทางอ้อมของ 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) segreg: [base+index*scale+disp] AT&T ในความเป็นจริงข้างต้นสองคำแนะนำทั้งสองเป็นของโหมดการกำหนดที่อยู่เซ็กเมนต์ segreg หมายถึงการลงทะเบียนเซ็กเมนต์ซึ่งมักจะใช้ในโหมดจริงเมื่อความสามารถในการสร้างหลักของ CPU ที่อยู่นอกเหนือจากตัวเลขลงทะเบียน ' ตัวอย่างเช่น CPU สามารถระบุพื้นที่ 20 บิตได้ แต่การลงทะเบียนมีเพียง 16 บิต เพื่อให้ได้พื้นที่ 20 หลักจำเป็นต้องใช้โหมดที่อยู่อื่น: segreg:offset ด้วยโหมดที่อยู่นี้ที่อยู่ออฟเซ็ตจะถูก segreg * 16 + offset แต่มันซับซ้อนกว่าโหมดหน่วยความจำแบน ในโหมด Protect การกำหนดที่อยู่ภายใต้พื้นที่ที่อยู่เชิงเส้นซึ่งหมายความว่าสามารถละเว้นที่อยู่ฐานส่วนเซ็กเมนต์ได้
| AT&T | Intel |
|---|---|
| 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, opcodes ทั้งหมดมีคำต่อท้ายเพื่อระบุขนาด โดยทั่วไปมีคำต่อท้ายสี่ชนิด: b , w , l และ q b หมายถึงไบต์ 8 บิต, w หมายถึงคำ 16 บิต, l หมายถึงคำสองคำ 32 บิต คำ 32 หลักเรียกว่าเป็นคำยาวซึ่งมาจากวัน 16 บิต q หมายถึง quadword 64 บิต แผนภูมิด้านล่างแสดงให้เห็นถึงไวยากรณ์ของคำสั่งการเปลี่ยนข้อมูล (MOV) ใน AT&T และ Intel
| AT&T | Intel |
|---|---|
| movb %al, %bl | mov bl, al, al |
| movw %ขวาน, %bx | mov bx, ขวาน |
| movl %eax, %ebx | MOV EBX, EAX |
| movq %rax, %rbx | Mov Rbx, Rax |
อย่างที่เราทราบหน่วยความจำใช้ในการจัดเก็บคำแนะนำและข้อมูลสำหรับ CPU หน่วยความจำเป็นอาร์เรย์ของไบต์ แม้ว่าความเร็วในการเข้าถึงหน่วยความจำจะเร็วมาก แต่เราก็ยังต้องการหน่วยเก็บข้อมูลขนาดเล็กและเร็วขึ้นเพื่อเพิ่มความเร็วในการดำเนินการคำสั่งของ CPU ซึ่งเป็นการลงทะเบียน ในระหว่างการดำเนินการคำสั่งข้อมูลทั้งหมดจะถูกเก็บไว้ชั่วคราวในการลงทะเบียน นั่นเป็นเหตุผลที่การลงทะเบียนมีชื่ออยู่ใน
เมื่อโปรเซสเซอร์เพิ่มขึ้นจาก 16 บิตเป็น 32 บิตจะมีการขยาย 8 ลงทะเบียนเป็น 32 บิตด้วย หลังจากนั้นเมื่อมีการใช้การลงทะเบียนเพิ่มเติม E หน้าจะถูกเพิ่มลงในชื่อทะเบียนเดิม โปรเซสเซอร์ 32 บิตเป็นสถาปัตยกรรม Intel 32 บิตซึ่งเป็น IA32 วันนี้โปรเซสเซอร์หลักคือสถาปัตยกรรม Intel 64 บิตซึ่งขยายจาก IA32 และถูกเรียกว่า x86-64 เนื่องจาก IA32 เป็นที่ผ่านมาบทความนี้จะมุ่งเน้นไปที่ x86-64 เท่านั้น โปรดทราบว่าใน x86-64 จำนวนการลงทะเบียนจะขยายจาก 8 ถึง 16 เพียงเพราะส่วนขยายนี้สถานะโปรแกรมสามารถเก็บไว้ในการลงทะเบียน แต่ไม่กอง ดังนั้นความถี่ของการเข้าถึงหน่วยความจำจะลดลงอย่างมาก
ใน X86-64 มีการลงทะเบียนทั่วไป 64 บิต 16 บิตและ 16 ตัวชี้ลอยตัว นอกจากนี้ CPU ยังมีตัวชี้คำสั่ง 64 บิตอีกหนึ่งตัวที่เรียกว่า rip มันถูกออกแบบมาเพื่อจัดเก็บที่อยู่ของคำสั่งที่ดำเนินการต่อไป นอกจากนี้ยังมีการลงทะเบียนอื่น ๆ ที่ไม่ได้ใช้กันอย่างแพร่หลายเราไม่ได้ตั้งใจที่จะพูดคุยเกี่ยวกับพวกเขาในบทความนี้ ในบรรดาการลงทะเบียนทั่วไป 16 ครั้งแปดคนมาจาก IA32: RAX、 RCX、 RDX、 RBX、 RSI、 RDI、 RSP และ RBP การลงทะเบียนทั่วไปอีกแปดรายการจะถูกเพิ่มเข้ามาใหม่ตั้งแต่ x86-64 ซึ่งเป็น R8 - R15 การลงทะเบียนลอย 16 ครั้งคือ XMM0 - XMM15
ซีพียูปัจจุบันมาจาก 8088 การลงทะเบียนจะขยายจาก 16 บิตถึง 32 บิตและในที่สุดถึง 64 บิต ดังนั้นโปรแกรมยังสามารถเข้าถึงการลงทะเบียน 8 บิตต่ำหรือ 16 บิตหรือ 32 บิต
แผนภูมิด้านล่างแสดงให้เห็นถึงการลงทะเบียนทั่วไป 16 ครั้งของ x86-64:

การใช้คำสั่ง register read ใน LLDB สามารถทิ้งข้อมูลการลงทะเบียนของเฟรมสแต็กปัจจุบัน
ตัวอย่างเช่นเราสามารถใช้คำสั่งด้านล่างเพื่อแสดงข้อมูลทั้งหมดในการลงทะเบียน:
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
อย่างที่เราทราบมีการลงทะเบียนตัวชี้ลอย 16 ตัวใน X86-64: XMM0 - XMM15 ในความเป็นจริงมีรายละเอียดอื่น ๆ ของมัน ในผลลัพธ์ของคำสั่ง register read -a คุณอาจสังเกตเห็นว่ามีการลงทะเบียน STMM และ YMM นอกเหนือจากกลุ่ม XMM ลงทะเบียน ที่นี่ STMM เป็นนามแฝงของการลงทะเบียน ST และ ST เป็นทะเบียนของ FPU (หน่วยจุดลอย) ใน x86 เพื่อจัดการข้อมูลลอย FPU มีการลงทะเบียนตัวชี้ลอยหนึ่งตัวซึ่งมีตัวชี้ตัวชี้ลอย 80 บิตแปดตัว: ST0 - ST7 เราสามารถสังเกตได้ว่าการลงทะเบียน STMM คือ 80 บิตจากผลลัพธ์ซึ่งสามารถพิสูจน์ได้ว่าการลงทะเบียน STMM คือ ST Register XMM คือการลงทะเบียน 128 บิตและการลงทะเบียน YMM คือ 256 บิตซึ่งเป็นส่วนขยายของ XMM ในความเป็นจริง XMM Register คือการลงทะเบียน YMM 128 บิตต่ำ เช่นเดียวกับ EAX Register คือ RAX register 32 บิตต่ำ ใน Pentium III Intel เผยแพร่ชุดคำสั่งที่เรียกว่า SSE (การสตรีม SIMD SIMD ส่วนขยาย) ซึ่งเป็นส่วนขยายของ MMX มีการเพิ่มการลงทะเบียน 128 บิตใหม่แปดรายการ (XMM0 - XMM7) ใน SSE ชุดคำสั่ง AVX (Advanced Vector Extensions) เป็นสถาปัตยกรรมส่วนขยายของ SSE นอกจากนี้ใน AVX การลงทะเบียน 128 บิต XMM ถูกขยายไปยัง YMM ลงทะเบียน 256 บิต

การเรียกใช้ฟังก์ชันรวมถึงพารามิเตอร์ผ่านและการถ่ายโอนการควบคุมจากหน่วยรวบรวมหนึ่งไปยังอีกหน่วยหนึ่ง ในขั้นตอนการเรียกใช้ฟังก์ชั่นการส่งข้อมูลการกำหนดตัวแปรท้องถิ่นและการเปิดตัวจะดำเนินการโดยสแต็ก และสแต็คที่กำหนดให้กับการเรียกใช้ฟังก์ชันเดียวเรียกว่าเฟรมสแต็ก
ฟังก์ชั่นการเรียกประชุมการประชุมของ OS X X86-64 นั้นเหมือนกันกับการประชุมที่อธิบายไว้ในบทความ: System V Application Binary Interface AMD64 PROCIDENTURE PROCENSOR ดังนั้นคุณสามารถอ้างอิงได้หากคุณสนใจ
ในระหว่างการดีบัก 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 ที่เรียกว่าเป็นตัวชี้สแต็กมักจะชี้ไปที่ด้านบนของกรอบสแต็กที่จัดสรรล่าสุด (ที่อยู่ต่ำ) ด้านล่างนี้เป็นแผนภูมิของ Frame Stack:

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 มันสงวนไว้และจะไม่ถูกแก้ไขโดยสัญญาณหรือตัวจัดการขัดจังหวะ เนื่องจากสามารถแก้ไขได้ในระหว่างการเรียกใช้ฟังก์ชั่นดังนั้นฟังก์ชั่น Leaf ซึ่งหมายถึงฟังก์ชั่นเหล่านั้นที่ไม่เรียกฟังก์ชั่นอื่นสามารถใช้พื้นที่นี้สำหรับข้อมูลชั่วคราว
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
ต่อไปนี้เป็นตัวอย่างของคำสั่ง call ใน iOS Simulator:
0x10915c714 <+68>: callq 0x1093ca502 ; symbol stub for: objc_msgSend
0x105206433 <+66>: callq *0xb3cd47(%rip) ; (void *)0x000000010475e800: objc_msgSend
รหัสด้านบนแสดงคำสั่ง call สองครั้ง ในการใช้งานครั้งแรกตัวถูกดำเนินการเป็นที่อยู่หน่วยความจำซึ่งเป็นสัญลักษณ์ของไฟล์ Mach-O มันสามารถค้นหาสัญลักษณ์ของฟังก์ชั่นผ่านตัวเชื่อมโยงแบบไดนามิก ในการใช้งานครั้งที่สองตัวถูกดำเนินการจะได้รับจริงโดยโหมดที่อยู่ทางอ้อม นอกจากนี้ในไวยากรณ์ AT&T * ต้องเพิ่มไปยังตัวถูกดำเนินการทันทีในคำสั่ง JUMP/CALL (หรือการกระโดดที่เกี่ยวข้องกับตัวนับโปรแกรมเมอร์) เป็นคำนำหน้า
โดยทั่วไปคำสั่ง ret ใช้เพื่อส่งคืนขั้นตอนจากฟังก์ชันที่เรียกไปยังฟังก์ชั่นการโทร คำสั่งนี้จะปรากฏที่อยู่จากด้านบนของสแต็กแล้วกระโดดกลับไปที่ที่อยู่นั้นและดำเนินการต่อไป ในตัวอย่างข้างต้นมันกระโดดกลับไปที่ next_instruction ก่อนที่จะดำเนินการคำสั่ง ret การลงทะเบียนจะเป็นของฟังก์ชั่นการโทรจะปรากฏขึ้น สิ่งนี้ถูกกล่าวถึงแล้วในขั้นตอนที่ 6 ของขั้นตอนการเรียกใช้ฟังก์ชัน
ฟังก์ชั่นส่วนใหญ่มีพารามิเตอร์ซึ่งอาจเป็นจำนวนเต็มลอยตัวชี้และอื่น ๆ นอกจากนี้ฟังก์ชั่นมักจะมีค่าส่งคืนซึ่งสามารถระบุผลลัพธ์การดำเนินการประสบความสำเร็จหรือล้มเหลว ใน OSX สามารถส่งผ่านพารามิเตอร์ได้มากที่สุด 6 ตัวผ่านการลงทะเบียนซึ่ง ได้แก่ RDI, RSI, RDX, RCX, R8 และ R9 ตามลำดับ ฟังก์ชั่นที่มีพารามิเตอร์มากกว่า 6 ตัวล่ะ? แน่นอนสถานการณ์นี้มีอยู่ หากสิ่งนี้เกิดขึ้นสแต็กสามารถใช้เพื่อรักษาพารามิเตอร์ที่เหลือในลำดับที่กลับด้าน OSX มีการลงทะเบียนจุดลอยตัวแปดจุดซึ่งอนุญาตให้ผ่านพารามิเตอร์ลอยได้ถึง 8 พารามิเตอร์
เกี่ยวกับค่าส่งคืนของฟังก์ชั่นการลงทะเบียน rax ใช้เพื่อบันทึกค่าผลตอบแทนจำนวนเต็ม หากค่าส่งคืนเป็นแบบลอยจะต้องใช้การลงทะเบียน XMM0 - XMM1 แผนภูมิด้านล่างแสดงให้เห็นอย่างชัดเจนว่าการประชุมการใช้งานการลงทะเบียนระหว่างการเรียกใช้ฟังก์ชั่น

preserved across function calls ระบุว่าการลงทะเบียนจะต้องได้รับการเก็บรักษาไว้ในการเรียกใช้ฟังก์ชั่นหรือไม่ เราจะเห็นได้ว่านอกเหนือจาก RBX, R12 - R15 Registers ที่กล่าวถึงข้างต้น, RSP และ RBP Registers ยังเป็นของการลงทะเบียน Callee -Saved นี่เป็นเพราะการลงทะเบียนทั้งสองนี้ขอสงวนตัวชี้ตำแหน่งที่สำคัญซึ่งชี้ไปที่สแต็คโปรแกรม
ต่อไปเราจะทำตามตัวอย่างจริงเพื่อแสดงคำแนะนำในการเรียกใช้ฟังก์ชัน ใช้ macro DDLogError ใน CocoaLumberjack เป็นตัวอย่าง เมื่อมาโครนี้เรียกว่า 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 | "บันทึก: ระดับ: ธง: บริบท: ไฟล์: ฟังก์ชั่น: บรรทัด: แท็ก: รูปแบบ:" | หน้าตา | 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 |
หากค่าของการลงทะเบียนเป็นสตริงเช่นพารามิเตอร์
opในการลงทะเบียนrsiสตริงสามารถพิมพ์ได้โดยตรงใน LLDB ผ่านpo (char *) $rsiคำสั่ง อย่างอื่น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 : วิธีนี้เรียกว่าเมื่อข้อความที่ไม่รู้จักถูกส่งไปยังวัตถุ วิธีนี้ใช้ตัวเลือกที่ไม่พบและส่งคืนค่าบูลีนเพื่อระบุว่ามีการเพิ่มวิธีการอินสแตนซ์ลงในคลาสที่สามารถจัดการกับตัวเลือกนั้นได้หรือไม่ หากคลาสสามารถจัดการตัวเลือกนี้ได้ให้กลับมาใช่แล้วกระบวนการส่งต่อข้อความจะเสร็จสมบูรณ์ วิธีนี้มักจะใช้ในการเข้าถึงคุณสมบัติ @dynamic ของ nsmanagedObjects ใน coredata ในแบบไดนามิก + (BOOL)resolveClassMethod:(SEL)sel คล้ายคลึงกับวิธีการข้างต้นความแตกต่างเพียงอย่างเดียวคือวิธีการเรียนคลาสนี้อีกวิธีหนึ่งคือวิธีการอินสแตนซ์
- (id)forwardingTargetForSelector:(SEL)aSelector : วิธีนี้ให้ตัวรับสัญญาณที่สองสำหรับการจัดการข้อความที่ไม่ทราบและเร็วกว่า forwardInvocation: วิธีนี้สามารถใช้เพื่อเลียนแบบคุณสมบัติบางอย่างของการสืบทอดหลายอย่าง โปรดทราบว่าไม่มีวิธีที่จะจัดการกับข้อความโดยใช้ส่วนนี้ของเส้นทางการส่งต่อ หากข้อความจำเป็นต้องมีการเปลี่ยนแปลงก่อนที่จะส่งไปยังตัวรับสัญญาณทดแทนจะต้องใช้กลไกการส่งต่อแบบเต็ม
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector : หากอัลกอริทึมการส่งต่อมาไกลขนาดนี้กลไกการส่งต่ออย่างเต็มรูปแบบเริ่มต้นขึ้น NSMethodSignature จะถูกส่งคืนโดยวิธีนี้ซึ่งรวมถึงคำอธิบายวิธีการในพารามิเตอร์ aselector โปรดทราบว่าวิธีนี้จะต้องถูกแทนที่หากคุณต้องการสร้างวัตถุ NSInvocation ซึ่งมีตัวเลือกเป้าหมายและอาร์กิวเมนต์ในระหว่างการส่งต่อข้อความ
- (void)forwardInvocation:(NSInvocation *)anInvocation : การใช้วิธีนี้จะต้องมีส่วนด้านล่าง: ค้นหาวัตถุที่สามารถจัดการกับข้อความ aninvocation; การส่งข้อความไปยังวัตถุนั้นการตั้งค่า aninvocation จะบันทึกค่าส่งคืน, รันไทม์จากนั้นส่งค่าส่งคืนไปยังผู้ส่งข้อความต้นฉบับ ในความเป็นจริงวิธีนี้สามารถมีพฤติกรรมเดียวกันกับ forwardingTargetForSelector: วิธีการเพียงแค่เปลี่ยนเป้าหมายการเรียกร้องและเรียกใช้หลังจากนั้น แต่เราแทบจะทำเช่นนั้น
โดยปกติสองวิธีแรกที่ใช้สำหรับการส่งต่อข้อความเรียกว่าเป็นการ ส่งต่ออย่างรวดเร็ว เพราะมันให้วิธีที่เร็วกว่ามากในการส่งข้อความ เพื่อแยกความแตกต่างจากการส่งต่ออย่างรวดเร็ววิธีที่ 3 และ 4 เรียกว่าเป็นการ ส่งต่อปกติ หรือ การส่งต่อเป็นประจำ มันช้ากว่ามากเพราะต้องสร้างวัตถุ NSInvocation เพื่อทำการส่งต่อข้อความให้เสร็จสมบูรณ์
หมายเหตุ: หากวิธีการ
methodSignatureForSelectorไม่ได้ถูกแทนที่หรือNSMethodSignatureที่ส่งคืนนั้นเป็นศูนย์forwardInvocationจะไม่ถูกเรียกและการส่งต่อข้อความจะถูกยกเลิกด้วยข้อผิดพลาดdoesNotRecognizeSelectorเราสามารถดูได้จากซอร์สโค้ดของฟังก์ชั่น__forwarding__ด้านล่าง
กระบวนการส่งต่อข้อความสามารถอธิบายได้โดยแผนภาพการไหลดูด้านล่าง

เช่นเดียวกับที่อธิบายไว้ในแผนภาพการไหลในแต่ละขั้นตอนตัวรับสัญญาณจะได้รับโอกาสในการจัดการกับข้อความ แต่ละขั้นตอนมีราคาแพงกว่าขั้นตอนก่อนหน้า แนวปฏิบัติที่ดีที่สุดคือการจัดการกระบวนการส่งต่อข้อความโดยเร็วที่สุด หากข้อความไม่ได้รับการจัดการผ่านกระบวนการทั้งหมดข้อผิดพลาด doesNotRecognizeSeletor จะถูกยกขึ้นเพื่อระบุตัวเลือกไม่สามารถรับรู้ได้จากวัตถุ
ถึงเวลาเสร็จสิ้นส่วนทฤษฎีและย้ายกลับไปที่ปัญหา
ตามข้อมูล TCWebViewController จาก TRACE Stack เราเชื่อมโยงกับ Tencent SDK Tencentopenapi.framework โดยธรรมชาติ แต่เราไม่ได้อัปเดต Tencent SDK เมื่อเร็ว ๆ นี้ซึ่งหมายความว่าการชนไม่ได้เกิดจาก Tencentopenapi
ก่อนอื่นเราถอดรหัสรหัสและได้โครงสร้างของคลาส 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:
}
จากผลการวิเคราะห์แบบคงที่ไม่มีวิธีการ setter และ getter ของ requestURLStr ใน TCWebViewController เนื่องจากไม่มีความผิดพลาดดังกล่าวในเวอร์ชันแอพก่อนหน้านี้เราจึงออกมาเป็นแนวคิด: คุณสมบัติใน TCWebViewController จะถูกนำไปใช้อย่างมีชีวิตชีวาซึ่งใช้ @dynamic เพื่อบอกคอมไพเลอร์ที่ไม่ได้สร้าง Getter และ Setter สำหรับคุณสมบัติในระหว่างการรวบรวมเวลา แต่สร้างขึ้นใน รันไทม์ จากนั้นเราตัดสินใจที่จะคิดอย่างลึกซึ้งว่าการคาดเดาของเรานั้นถูกต้องหรือไม่ ในระหว่างการติดตามของเราเราพบว่ามีหมวดหมู่ NSObject(MethodSwizzlingCategory) สำหรับ NSObject ใน tencentopenapi.framework ซึ่งน่าสงสัยมาก ในหมวดหมู่นี้มีวิธีการ switchMethodForCodeZipper ซึ่งการใช้งานแทนที่วิธีการจัดเรียง methodSignatureForSelector และวิธี forwardInvocation ของ QQmethodSignatureForSelector และวิธีการ 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 อยู่ในนั้น จากชื่อเราสามารถรับได้อย่างง่ายดายว่าวิธีนี้คือการเพิ่มวิธีการตั้งค่าและ 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;
} แต่ทำไม Setter จึงไม่สามารถจดจำได้ในคลาส TCWebViewController เป็นเพราะวิธี QQMethodSignatureForSelector ได้รับการคุ้มครองในระหว่างการพัฒนาเวอร์ชันนี้ของเราหรือไม่? อย่างไรก็ตามเราไม่พบเบาะแสแม้ว่าเราจะผ่านทุกที่ในรหัส นั่นน่าผิดหวังมาก จนถึงตอนนี้การวิเคราะห์แบบคงที่เสร็จสิ้น ขั้นตอนต่อไปคือการใช้ LLDB เพื่อแก้ไขข้อบกพร่องของ Tencent SDK แบบไดนามิกเพื่อค้นหาว่าเส้นทางใดทำลายการสร้าง Getter และ Setter ในกระบวนการส่งต่อข้อความ
หากเราพยายามตั้งค่าเบรกพอยต์บนคำสั่ง
setRequestURLStrผ่านคำสั่ง LLDB เราจะพบว่าเราไม่สามารถทำได้ เหตุผลเป็นเพราะ setter ไม่สามารถใช้ได้ในระหว่างการรวบรวมเวลา สิ่งนี้ยังสามารถตรวจสอบการคาดเดาดั้งเดิมของเรา
ตามการติดตาม Crash Stack เราสามารถสรุปได้ว่า 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
เมื่อแอปกำลังจะเปิดตัวหน้าเว็บตรวจสอบเว็บโครงการจะหยุดลงบนจุดพักนี้ อ้างถึงด้านล่าง:

ภาพหน้าจอนี้ถูกจับเมื่อแอปทำงานบนตัวจำลองดังนั้นรหัสประกอบจะขึ้นอยู่กับ X64 หากคุณใช้อุปกรณ์ iPhone รหัสแอสเซมบลีควรเป็นแขน แต่วิธีการวิเคราะห์เหมือนกันสำหรับพวกเขาโปรดสังเกต
ตั้งค่าเบรกพอยต์บนบรรทัด 96 รหัสแอสเซมบลีนี้คือการเรียกใช้เมธอด setRequestURLStr จากนั้นพิมพ์เนื้อหาของการลงทะเบียน rbx จากนั้นเราสามารถสังเกตได้ว่าอินสแตนซ์ TCWebViewController ถูกบันทึกไว้ในการลงทะเบียนนี้

ต่อไปเราสามารถใช้ LLDB เพื่อตั้งค่าเบรกพอยต์สำหรับวิธีการ QQmethodSignatureForSelector :
br s -n "-[NSObject QQmethodSignatureForSelector:]"
ป้อน c ใน LLDB เพื่อให้จุดพักยังคงดำเนินต่อไปจากนั้นเบรกพอยต์จะหยุดลงในวิธี QQmethodSignatureForSelector ซึ่งสามารถพิสูจน์การคาดเดาก่อนหน้าของเราเกี่ยวกับ QQmethodSignatureForSelector วิธีการที่ขัดแย้งกับรหัสของเราไม่ถูกต้อง

ตั้งค่าเบรกพอยต์เมื่อสิ้นสุด QQmethodSignatureForSelector วิธีนั่นคือคำสั่ง retq ในบรรทัด 31 จากนั้นพิมพ์ที่อยู่หน่วยความจำของ register rax อ้างอิงจากด้านล่างภาพหน้าจอ:

โดยการพิมพ์ที่อยู่หน่วยความจำ 0x00007fdb36d38df0 ของการลงทะเบียน rax วัตถุ NSMethodSignature จะถูกส่งกลับ ตามอนุสัญญาการออกแบบเกี่ยวกับภาษาแอสเซมบลี X86 ค่าส่งคืนจะถูกบันทึกไว้ใน register rax เห็นได้ชัดว่าวิธี QQmethodSignatureForSelector ถูกเรียกใช้และส่งคืนค่าที่ถูกต้องซึ่งหมายความว่าเราจำเป็นต้องติดตามปัญหา
ตั้งค่าเบรกพอยต์บน QQforwardInvocation ผ่าน LLDB:
br s -n "-[NSObject QQforwardInvocation:]"
หลังจากตั้งค่าเบรกพอยต์ให้ดำเนินการต่อการดำเนินการโปรแกรมแอปจะล้มเหลว และวิธีการ QQforwardInvocation ยังไม่ได้รับการเรียก ด้วยสิ่งนี้เราสามารถสรุปวิธีการ QQforwardInvocation นั้นขัดแย้งกับรหัสของเรา

___forwarding___ ฟังก์ชั่นมีการใช้กลไกการส่งต่อข้อความทั้งหมดรหัสการถอดรหัสจะถูกเลือกจาก Objective-C 消息发送与转发机制原理ในบทความนี้มีการตัดสินซึ่งควรไม่ถูกต้องระหว่าง forwarding และ receiver เมื่อเรียกใช้วิธี forwardingTargetForSelector ที่นี่ควรเป็นการตัดสินระหว่าง 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___ forwarding___ จากร่องรอยสแต็คขัดข้องเราสามารถสังเกตได้ว่ามันถูกเรียกว่าเป็นเส้นทางที่สองในเส้นทางการส่งต่อข้อความทั้งหมดซึ่งหมายถึงวัตถุ NSInvocation จะถูกเรียกใช้เมื่อเรียกว่า forwardInvocation
นอกจากนี้คุณยังสามารถดำเนินการตามขั้นตอนคำสั่งหลังจากจุดพักเพื่อสังเกตเส้นทางการดำเนินการของรหัสแอสเซมบลีควรสังเกตผลลัพธ์เดียวกัน

และวิธีการใดที่ดำเนินการเมื่อเรียกว่า 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: 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.