
中文
Debugging memiliki reputasi yang agak buruk. Maksud saya, jika pengembang memiliki pemahaman lengkap tentang program ini, tidak akan ada bug dan mereka tidak akan men -debugging di tempat pertama, bukan?
Jangan berpikir seperti itu.
Akan selalu ada bug di perangkat lunak Anda - atau perangkat lunak apa pun, dalam hal ini. Tidak ada jumlah cakupan tes yang dikenakan oleh manajer produk Anda yang akan memperbaikinya. Faktanya, melihat debugging hanya sebagai proses memperbaiki sesuatu yang rusak sebenarnya adalah cara berpikir beracun yang secara mental akan menghambat kemampuan analitis Anda.
Sebaliknya, Anda harus melihat debugging hanya sebagai proses untuk lebih memahami suatu program . Ini perbedaan yang halus, tetapi jika Anda benar -benar percaya, pekerjaan debugging sebelumnya hilang begitu saja.
Sejak Grace Hopper, pendiri bahasa COBOL , menemukan bug pertama di dunia di komputer estafet, generasi bug dalam pengembangan perangkat lunak tidak pernah berhenti. Sebagai kata pengantar untuk 《《《《《《《《《《《《《》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》》, tidak akan ada bug. Oleh karena itu, debugging hampir merupakan fase yang tak terhindarkan dalam siklus hidup pengembangan perangkat lunak.
Jika Anda bertanya kepada programmer yang tidak berpengalaman tentang bagaimana mendefinisikan debugging, ia mungkin mengatakan "debugging adalah sesuatu yang Anda lakukan untuk menemukan solusi untuk masalah perangkat lunak Anda". Dia benar, tapi itu hanya bagian kecil dari debugging nyata.
Berikut adalah langkah -langkah debugging nyata:
Di antara langkah -langkah di atas, langkah terpenting adalah langkah pertama: cari tahu masalahnya. Rupanya, ini adalah prasyarat dari langkah -langkah lain.
Penelitian menunjukkan waktu yang dihabiskan oleh programmer yang berpengalaman untuk debugging untuk menemukan serangkaian cacat yang sama adalah sekitar satu kedua puluh programmer yang tidak berpengalaman. Itu berarti pengalaman debugging membuat perbedaan besar dalam efisiensi pemrograman. Kami memiliki banyak buku tentang desain perangkat lunak, sayangnya, jarang dari mereka memiliki pengenalan tentang debugging, bahkan kursus di sekolah.
Ketika debugger membaik selama bertahun -tahun, gaya pengkodean programmer diubah secara menyeluruh. Tentu saja, debugger tidak dapat menggantikan pemikiran yang baik, pemikiran tidak dapat menggantikan debugger yang sangat baik, kombinasi yang paling sempurna adalah debugger yang sangat baik dengan pemikiran yang baik.
Grafik berikut adalah sembilan aturan debugging yang dijelaskan dalam buku <debugging: 9 aturan yang sangat diperlukan untuk menemukan bahkan perangkat lunak dan masalah perangkat keras yang paling sulit dipahami>.

Meskipun sebagai programmer iOS, sebagian besar waktu dalam pekerjaan tidak akan berurusan dengan bahasa perakitan, tetapi memahami perakitan masih sangat membantu, terutama ketika men-debug kerangka kerja sistem atau kerangka kerja pihak ketiga tanpa kode sumber.
Bahasa Asssembly adalah bahasa pemrograman berorientasi mesin tingkat rendah, yang dapat dianggap sebagai kumpulan mnemonik untuk instruksi mesin untuk berbagai CPU. Pemrogram dapat menggunakan bahasa perakitan untuk mengontrol sistem perangkat keras komputer secara langsung. Dan program yang ditulis dalam bahasa perakitan memiliki banyak kelebihan, seperti kecepatan eksekusi cepat dan lebih sedikit memori yang ditempati.
Sejauh ini, dua arsitektur utama banyak digunakan pada platform Apple, x86 dan ARM. Di perangkat seluler menggunakan bahasa perakitan ARM, yang terutama karena lengan adalah arsitektur komputasi set instruksi (RISC) yang dikurangi, dengan keunggulan konsumsi daya yang rendah. Sementara platform desktop seperti Mac OS, arsitektur x86 digunakan. Aplikasi yang diinstal pada simulator iOS sebenarnya berjalan sebagai aplikasi Mac OS di dalam simulator, yang berarti simulator berfungsi seperti wadah. Karena kasus kami didebug di simulator iOS, tujuan penelitian utama adalah X86 Assembly Language.
Bahasa perakitan x86 berkembang menjadi dua cabang sintaks: Intel (digunakan secara orignial dalam dokumentasi platform X86) dan AT&T. Intel mendominasi keluarga MS-DOS dan Windows, sementara AT&T adalah umum dalam keluarga UNIX. Ada perbedaan besar pada sintaks antara intel dan AT&T, seperti variabel, konstan, akses register, pengalamatan dan offset tidak langsung. Meskipun perbedaan sintaks mereka sangat besar, sistem perangkat kerasnya sama yang berarti salah satunya dapat dimigrasi ke yang lain dengan mulus. Karena AT&T Assembly Language digunakan pada XCODE, kami akan fokus pada AT&T di bagian di bawah ini.
Harap perhatikan bahwa sintaks Intel digunakan pada alat pembongkaran Hopper Disassemble dan Ida Pro.
Belows adalah perbedaan antara Intel dan AT&T:
Awalan operan: dalam sintaks AT&T, % digunakan sebagai awalan nama register dan $ digunakan sebagai awalan operan langsung, sementara tidak ada awalan yang digunakan untuk kedua register dan operan langsung di Intel. Perbedaan lainnya adalah 0x ditambahkan sebagai awalan untuk heksadesimal di AT&T. Bagan di bawah ini menunjukkan perbedaan antara awalannya:
| AT&T | Intel |
|---|---|
| MOVQ %RAX, %RBX | MOV RBX, RAX |
| Tambah $ 0x10, %RSP | Tambahkan RSP, 010H |
Dalam sintaks Intel, akhiran
hdigunakan untuk operan heksadesimal dan akhiranbdigunakan untuk operan biner.
Operan: Dalam Sintaks AT&T, operan pertama adalah operan sumber, operan kedua adalah operan tujuan. Namun, dalam sintaks Intel, urutan operan berlawanan. Dari titik ini, sintaks AT&T lebih nyaman bagi kami sesuai dengan kebiasaan membaca kami.
Mode pengalamatan: Membandingkan dengan sintaks Intel, mode pengalamatan AT&T tidak langsung sulit dibaca. Namun, algoritma perhitungan alamat adalah sama: address = disp + base + index * scale . base mewakili alamat dasar, disp berdiri untuk alamat offset, index * scale menentukan lokasi elemen, scale adalah ukuran elemen yang hanya bisa menjadi kekuatan dua. disp/base/index/scale semuanya opsional, nilai default index adalah 0, sedangkan nilai default scale adalah 1. Sekarang mari kita lihat instruksi perhitungan alamat: %segreg: disp(base,index,scale) adalah untuk AT&T, dan segreg: [base+index*scale+disp] untuk intel. Bahkan, di atas dua instruksi keduanya termasuk dalam mode pengalamatan segmen. segreg untuk register segmen yang biasanya digunakan dalam mode nyata ketika kapasitas digit CPU berbicara di luar digit register. Misalnya, CPU dapat mengatasi ruang 20-bit, tetapi register hanya memiliki 16-bit. Untuk mencapai ruang 20 digit, mode pengalamatan lain perlu digunakan: segreg:offset . Dengan mode pengalamatan ini, alamat offset akan segreg * 16 + offset , tetapi lebih rumit daripada mode memori datar. Dalam mode Protect, pengalamatan berada di bawah ruang alamat linier, yang berarti alamat dasar segmen dapat diabaikan.
| 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] |
Jika operan langsung datang di tempat
dispatauscale,$akhiran dapat dihilangkan. Dalam Sintaks Intel,byte ptr,word ptr,dword ptrdanqword ptrperlu ditambahkan sebelum operan memori.
Sufiks Opcode: Dalam Sintaks AT&T, semua opcode memiliki akhiran untuk menentukan ukurannya. Umumnya ada empat jenis sufiks: b , w , l dan q b mewakili byte 8-bit, w berarti kata 16-bit, l berarti kata ganda 32-bit. Kata 32 digit juga disebut kata panjang yang berasal dari 16-bit. q mewakili quadword 64-bit. Bagan di bawah ini menggambarkan sintaks instruksi transisi data (MOV) di AT&T dan Intel.
| AT&T | Intel |
|---|---|
| MOVB %AL, %BL | MOV BL, AL |
| MOVW %AX, %BX | MOV BX, AX |
| MOVL %EAX, %EBX | mov ebx, eax |
| MOVQ %RAX, %RBX | MOV RBX, RAX |
Seperti yang kita ketahui, memori digunakan untuk menyimpan instruksi dan data untuk CPU. Memori pada dasarnya adalah array byte. Meskipun kecepatan akses memori sangat cepat, kami masih membutuhkan unit penyimpanan yang lebih kecil dan lebih cepat untuk mempercepat eksekusi instruksi CPU, yang terdaftar. Selama eksekusi instruksi, semua data disimpan sementara dalam register. Itu sebabnya register dinamai masuk.
Ketika prosesor tumbuh dari 16-bit ke 32-bit, 8 register juga diperluas hingga 32-bit. Setelah itu, ketika register yang diperluas digunakan, awalan E ditambahkan ke nama register asli. Prosesor 32-bit adalah arsitektur intel 32-bit, yaitu IA32. Saat ini, prosesor utama adalah arsitektur Intel 64-bit, yang diperpanjang dari IA32 dan disebut x86-64. Karena IA32 sudah lewat, artikel ini hanya akan fokus pada x86-64. Perhatikan bahwa dalam x86-64, jumlah register diperpanjang dari 8 hingga 16. Hanya karena ekstensi ini, negara program dapat disimpan dalam register tetapi tidak tumpukan. Dengan demikian, frekuensi akses memori sangat berkurang.
Di x86-64, ada 16 register umum 64-bit dan 16 register pointer mengambang. Selain itu, CPU memiliki satu lagi register pointer instruksi 64-bit yang disebut rip . Ini dirancang untuk menyimpan alamat instruksi yang dieksekusi berikutnya. Ada juga beberapa register lain yang tidak banyak digunakan, kami tidak bermaksud untuk membicarakannya dalam artikel ini. Di antara 16 register umum, delapan dari mereka berasal dari IA32: RAX 、 RCX 、 RDX 、 RBX 、 RSI 、 RDI 、 RSP dan RBP. Delapan register umum lainnya ditambahkan baru sejak x86-64 yaitu R8 - R15. 16 register mengambang adalah xmm0 - xmm15.
CPU saat ini berasal dari 8088, register juga diperpanjang dari 16-bit ke 32-bit dan akhirnya menjadi 64-bit. Dengan demikian, program ini masih dapat mengakses register 8-bit atau 16-bit atau 32-bit rendah.
Bagan di bawah ini menggambarkan 16 register umum x86-64:

Menggunakan perintah register read di LLDB dapat membuang data register bingkai tumpukan saat ini.
Misalnya, kita dapat menggunakan perintah di bawah ini untuk menampilkan semua data dalam register:
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
Seperti yang kita ketahui, ada 16 register pointer mengambang di x86-64: xmm0 - xmm15. Bahkan, ada beberapa detail lainnya. Dalam output dari perintah register read -a , Anda mungkin melihat bahwa ada register STMM dan YMM selain grup register XMM. Di sini STMM adalah Alias of ST Register, dan ST adalah register FPU (Unit Float Point) di X86 untuk menangani data float. FPU berisi satu register pointer float yang memiliki delapan register pointer float 80 -bit: ST0 - ST7. Kami dapat mengamati bahwa register STMM adalah 80-bit dari output, yang dapat membuktikan register STMM adalah register ST. XMM adalah register 128-bit, dan YMM Register adalah 256-bit yang merupakan ekstensi XMM. Faktanya, register xmm adalah register YMM 128-bit rendah. Seperti register EAX adalah register RAX 32-bit rendah. Dalam Pentium III, Intel menerbitkan set instruksi yang disebut SSE (Streaming SIMD Extensions) yang merupakan perpanjangan dari MMX. Delapan register 128 -bit baru (xmm0 - xmm7) ditambahkan di SSE. AVX (Lanjutan Ekstensi Vektor) Set Instruksi adalah arsitektur ekstensi SSE. Juga di AVX, register 128-bit XMM diperpanjang menjadi register 256-bit YMM.

Panggilan fungsi mencakup parameter passing dan transfer kontrol dari satu unit kompilasi ke yang lain. Dalam prosedur panggilan fungsi, lewat data, penugasan dan rilis variabel lokal dilakukan oleh Stack. Dan tumpukan yang ditugaskan untuk satu fungsi panggilan disebut bingkai tumpukan.
Konvensi panggilan fungsi OS X86-64 adalah sama dengan konvensi yang dijelaskan dalam artikel: Sistem V Aplikasi Biner Interface AMD64 Suplemen Prosesor Arsitektur. Oleh karena itu Anda dapat merujuknya jika Anda tertarik.
Selama debugging LLDB, kami dapat menggunakan perintah bt untuk mencetak jejak tumpukan utas saat ini, seperti di bawah ini:
(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
Faktanya, perintah bt dapat diterapkan pada bingkai stack. Bingkai Stack mempertahankan alamat pengembalian dan variabel lokal untuk fungsi yang dapat dilihat sebagai konteks eksekusi fungsi. Seperti yang kita ketahui, tumpukan tumbuh ke atas, sementara tumpukan tumbuh ke bawah yang berasal dari alamat memori bernomor besar ke yang bernomor kecil. Setelah fungsi dipanggil, satu bingkai tumpukan mandiri ditetapkan untuk panggilan fungsi. Daftar RBP, disebut sebagai bingkai pointer, selalu menunjuk ke akhir bingkai tumpukan yang dialokasikan terbaru (alamat tinggi). Register RSP, disebut sebagai Stack Pointer, selalu menunjuk ke atas bingkai tumpukan yang dialokasikan terbaru (alamat rendah). Di bawah ini adalah grafik bingkai tumpukan:

Position kolom kiri adalah alamat memori yang menggunakan mode pengalamatan tidak langsung. Content adalah nilai alamat dalam Position . Menurut struct of stack frame pada grafik di atas, prosedur panggilan fungsi dapat digambarkan sebagai beberapa langkah sebagai berikut:
Langkah 2 dan 3 sebenarnya termasuk instruksi call . Selain itu, langkah 4 dan langkah 5 dapat dijelaskan dalam instruksi perakitan sebagai berikut:
TestDemo`-[ViewController viewDidLoad]:
0x1054e09c0 <+0>: pushq %rbp //step 4
0x1054e09c1 <+1>: movq %rsp, %rbp //step 5
Sangat mudah untuk memperhatikan bahwa kedua langkah ini bersama dengan setiap panggilan fungsi. Ada detail lain dari grafik di atas: ada area merah di bawah register RSP, yang disebut zona merah oleh ABI. Ini adalah cadangan dan tidak boleh dimodifikasi oleh penangan sinyal atau interupsi. Karena dapat dimodifikasi selama panggilan fungsi, oleh karena itu, fungsi daun yang berarti fungsi -fungsi yang tidak pernah memanggil fungsi lain dapat menggunakan area ini untuk data sementara.
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
Di antara instruksi di atas, instruksi dari 0x1064a63f5 hingga 0x1064a63fd termasuk dalam langkah 6. Ada semacam register yang disebut register fungsi yang berarti mereka termasuk dalam fungsi panggilan, tetapi fungsi yang dipanggil diperlukan untuk menjaga nilai -nilainya. Dari instruksi perakitan di bawah ini, kita dapat melihat RBX, RSP dan R12 - R15 semuanya termasuk dalam register tersebut.
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
Instruksi untuk memanggil fungsi adalah call , lihat di bawah ini:
call function
function dalam parameter adalah prosedur dalam segmen teks . Instruksi Call dapat dibagi menjadi dua langkah. Langkah pertama adalah mendorong alamat instruksi berikutnya dari instruksi call di Stack. Di sini, alamat berikutnya sebenarnya adalah alamat pengembalian setelah fungsi yang dipanggil selesai. Langkah kedua adalah melompat function . Instruksi call setara dengan di bawah dua instruksi:
push next_instruction
jmp function
Berikut ini adalah contoh instruksi call dalam simulator iOS:
0x10915c714 <+68>: callq 0x1093ca502 ; symbol stub for: objc_msgSend
0x105206433 <+66>: callq *0xb3cd47(%rip) ; (void *)0x000000010475e800: objc_msgSend
Kode di atas menunjukkan dua penggunaan instruksi call . Dalam penggunaan pertama, operan adalah alamat memori yang sebenarnya merupakan simbol rintisan dari file Mach-O. Itu dapat mencari simbol fungsi melalui linker dinamis. Dalam penggunaan kedua, operan sebenarnya diperoleh dengan mode pengalamatan tidak langsung. Selanjutnya, dalam sintaks AT&T, * perlu ditambahkan ke operan langsung dalam instruksi lompat/panggilan (atau lompatan yang terkait dengan penghitung programmer) sebagai awalan.
Secara umum, instruksi ret digunakan untuk mengembalikan prosedur dari fungsi yang dipanggil ke fungsi panggilan. Instruksi ini muncul alamat dari bagian atas tumpukan dan melompat kembali ke alamat itu dan terus mengeksekusi. Dalam contoh di atas, ia melompat kembali ke next_instruction . Sebelum instruksi ret dijalankan, register termasuk dalam fungsi panggilan akan muncul. Ini sudah disebutkan dalam Langkah 6 dari Prosedur Panggilan Fungsi.
Sebagian besar fungsi memiliki parameter yang dapat berupa bilangan bulat, mengapung, penunjuk dan sebagainya. Selain itu, fungsi biasanya memiliki nilai pengembalian yang dapat menunjukkan hasil eksekusi berhasil atau gagal. Di OSX, paling banyak 6 parameter dapat dilewati melalui register yang merupakan RDI, RSI, RDX, RCX, R8 dan R9 secara berurutan. Bagaimana dengan fungsi dengan lebih dari 6 parameter? Tentu saja, keadaan ini ada. Jika ini terjadi, tumpukan dapat digunakan untuk melestarikan parameter yang tersisa dalam urutan terbalik. OSX memiliki delapan register titik mengambang yang memungkinkan untuk melewati parameter float hingga 8.
Tentang nilai pengembalian suatu fungsi, register rax digunakan untuk menyimpan nilai pengembalian integer. Jika nilai pengembalian adalah float, xmm0 - xmm1 register harus digunakan. Bagan di bawah ini dengan jelas menggambarkan konvensi penggunaan register selama panggilan fungsi.

preserved across function calls menunjukkan apakah register perlu dipertahankan di seluruh panggilan fungsi. Kita dapat melihat bahwa selain register RBX, R12 - R15 yang disebutkan di atas, register RSP dan RBP juga termasuk register callee -save. Ini karena kedua register ini memesan pointer lokasi penting yang menunjuk ke tumpukan program.
Selanjutnya kita akan mengikuti contoh nyata untuk menunjukkan instruksi dalam panggilan fungsi. Ambil DDLogError makro di CocoaLumberjack sebagai contoh. Ketika makro ini dipanggil, Metode Kelas log:level:flag:context:file:function:line:tag:format: dipanggil. Kode dan instruksi berikut adalah tentang panggilan DDLogError dan instruksi perakitan yang sesuai:
- (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
Karena semua fungsi Objective-C akan berubah menjadi doa fungsi objc_msgSend , jadi log:level:flag:context:file:function:line:tag:format: Metode akhirnya berubah menjadi kode di bawah ini:
objc_msgSend(DDLog, @selector(log:level:flag:context:file:function:line:tag:format:), asynchronous, level, flag, context, file, function, line, tag, format, sender)
Kami sudah menyebutkan paling banyak 6 register dapat digunakan untuk passing parameter. Parameter berlebih dapat menggunakan tumpukan untuk melakukan passing. Karena fungsi di atas memiliki lebih dari 6 parameter, passing parameter akan menggunakan register dan tumpukan. Di bawah dua tabel menjelaskan penggunaan detail register dan tumpukan untuk parameter lewat DDLogError fungsi doa.
| Daftar Umum | nilai | Parameter | Instruksi perakitan | Komentar |
|---|---|---|---|---|
| rdi | Ddlog | diri sendiri | 0x102c568eb <+171>: movq %r11, %rdi | |
| RSI | "Log: Level: Bendera: Konteks: File: Fungsi: Baris: Tag: Format:" | op | 0x102c568f2 <+178>: MOVQ %R15, %RSI | |
| rdx | 0 | asinkron | 0x102c568a3 <+99>: xorl %edx, %edx | Xorl adalah operasi eksklusif-OR. Di sini digunakan untuk menghapus register edx |
| rcx | 18446744073709551615 | tingkat | 0x102c568f9 <+185>: movq %rbx, %rcx | (Ddloglevelall atau Nsuintegermax) |
| R8 | 1 | bendera | 0x102c568aa <+106>: movl %eax, %r8d | Ddlogflagerror |
| R9 | 0 | konteks | 0x102c568af <+111>: movl %eax, %r9d |
| Stack Frame Offset | Nilai | Parameter | Instruksi perakitan | Komentar |
|---|---|---|---|---|
| (%RSP) | "/Users/dev-aozhimin/desktop/testddlog/testddlog/viewController.m" | mengajukan | 0x102c56900 <+192>: MOVQ %R11, ( %RSP) | |
| 0x8 (%RSP) | "-[Tes ViewController:]" | fungsi | 0x102c56908 <+200>: MOVQ %RBX, 0x8 ( %RSP) | |
| 0x10 (%RSP) | 0x22 | garis | 0x102c5690d <+205>: movq $ 0x22, 0x10 (%rsp) | Doa Ddlogerror yang sesuai berada di baris 34 |
| 0x18 (%RSP) | 0x0 | menandai | 0x102c56916 <+214>: movq $ 0x0, 0x18 (%rsp) | nol |
| 0x20 (%RSP) | "Testddlog:%@" | format | 0x102c5691f <+223>: MOVQ %R10, 0x20 ( %RSP) | |
| 0x28 (%RSP) | pengirim | Parameter pertama parameter variabel | 0x102c56924 <+228>: MOVQ %R14, 0x28 ( %RSP) | Sebuah contoh dari UIBUTTON |
Jika nilai register adalah string, seperti parameter
opdalam registerrsi, string dapat dicetak langsung di LLDB melalui perintahpo (char *) $rsi. Selain itu,po $rsidapat digunakan untuk mencetak nilai dalam format integer.
Dengan bantuan bahasa perakitan, kita dapat melihat beberapa pengetahuan tingkat rendah yang sangat diperlukan selama debugging. Saya berusaha sangat keras untuk memperkenalkan pengetahuan terkait perakitan sedetail mungkin. Namun, hierarki pengetahuan perakitan terlalu besar untuk dijelaskan dalam satu artikel. Silakan merujuk ke referensi yang disebutkan di atas. Selain itu, bab ketiga dari representasi level mesin CSAPP juga sangat disarankan. Ini bahan yang langka yang langka untuk referensi.
Artikel ini menggambarkan prosedur debugging melalui kasus nyata. Beberapa detail diubah untuk melindungi privasi pribadi.
Masalah yang akan kita bicarakan sedang terjadi ketika saya sedang mengembangkan SDK login. Seorang pengguna mengklaim aplikasi macet ketika dia menekan tombol "QQ" di halaman login. Ketika kami men -debug masalah ini, kami menemukan kecelakaan itu terjadi jika aplikasi QQ tidak diinstal pada saat yang sama. Ketika pengguna menekan tombol QQ untuk memerlukan login, QQ Login SDK mencoba meluncurkan halaman web otorisasi di aplikasi kami. Dalam hal ini, kesalahan pemilih yang tidak diakui [TCWebViewController setRequestURLStr:] terjadi.
PS: Untuk fokus pada masalah ini, informasi debug bisnis yang tidak perlu tidak tercantum di bawah ini. Sementara itu Aadebug digunakan sebagai nama aplikasi kami.
Ini jejak tumpukan dari kecelakaan ini:
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
Sebelum berbicara tentang debugging, mari kita terbiasa dengan penerusan pesan di Objective-C. Seperti yang kita ketahui Objective-C menggunakan struktur pesan daripada panggilan fungsi. Perbedaan utama adalah bahwa dalam struktur pesan, runtime memutuskan fungsi mana yang akan dieksekusi tidak menyusun waktu. Itu berarti jika pesan yang tidak dikenali dikirim ke satu objek, tidak ada yang akan terjadi selama penyusunan waktu. Dan selama runtime, ketika menerima metode yang tidak dipahami, suatu objek melewati penerusan pesan, suatu proses yang dirancang untuk memungkinkan Anda sebagai pengembang untuk memberi tahu pesan bagaimana menangani pesan yang tidak diketahui.
Di bawah empat metode biasanya terlibat selama penerusan pesan:
+ (BOOL)resolveInstanceMethod:(SEL)sel : Metode ini dipanggil ketika pesan yang tidak diketahui diteruskan ke suatu objek. Metode ini mengambil pemilih yang tidak ditemukan dan mengembalikan nilai boolean untuk menunjukkan apakah metode instance ditambahkan ke kelas yang sekarang dapat menangani pemilih itu. Jika kelas dapat menangani pemilih ini, kembalikan ya, maka proses penerusan pesan selesai. Metode ini sering digunakan untuk mengakses properti @dynamic dari nsmanagedObjects di coredata secara dinamis. + (BOOL)resolveClassMethod:(SEL)sel adalah serupa dengan metode di atas, satu -satunya perbedaan adalah metode satu kelas ini, yang lain adalah metode instan.
- (id)forwardingTargetForSelector:(SEL)aSelector : Metode ini menyediakan penerima kedua untuk menangani pesan yang tidak diketahui, dan lebih cepat daripada forwardInvocation: . Metode ini dapat digunakan untuk meniru beberapa fitur dari beberapa warisan. Perhatikan bahwa tidak ada cara untuk memanipulasi pesan menggunakan bagian jalur penerusan ini. Jika pesan perlu diubah sebelum dikirim ke penerima penggantian, mekanisme penerusan penuh harus digunakan.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector : Jika algoritma penerusan telah sampai sejauh ini, mekanisme penerusan penuh dimulai. NSMethodSignature dikembalikan dengan metode ini yang mencakup deskripsi metode dalam parameter Aselector. Perhatikan bahwa metode ini perlu diganti jika Anda ingin membuat objek NSInvocation yang berisi pemilih, target, dan argumen selama penerusan pesan.
- (void)forwardInvocation:(NSInvocation *)anInvocation : Implementasi metode ini harus berisi bagian -bagian di bawah ini: Cari tahu objek yang dapat menangani pesan aninvokasi; Mengirim pesan ke objek itu, anInvocation menyimpan nilai pengembalian, runtime kemudian mengirimkan nilai pengembalian ke pengirim pesan asli. Faktanya, metode ini dapat memiliki perilaku yang sama dengan forwardingTargetForSelector: Metode dengan hanya mengubah target doa dan memohonnya setelah itu, tetapi kami hampir tidak melakukannya.
Biasanya, dua metode pertama yang digunakan untuk penerusan pesan disebut sebagai penerusan cepat , karena memberikan cara yang jauh lebih cepat untuk melakukan penerusan pesan. Untuk membedakan dari penerusan cepat, Metode 3 dan 4 disebut sebagai penerusan normal atau penerusan reguler . Ini jauh lebih lambat karena harus membuat objek nsinvocation untuk menyelesaikan penerusan pesan.
CATATAN: Jika metode
methodSignatureForSelectortidak diganti atauNSMethodSignatureyang dikembalikan adalah nihil,forwardInvocationtidak akan dipanggil, dan penerusan pesan diakhiri dengandoesNotRecognizeSelectorkesalahan yang diangkat. Kita dapat melihatnya dari kode sumber__forwarding__di bawah ini.
Proses penerusan pesan dapat dijelaskan dengan diagram aliran, lihat di bawah.

Seperti yang dijelaskan dalam diagram aliran, pada setiap langkah, penerima diberi kesempatan untuk menangani pesan. Setiap langkah lebih mahal daripada yang sebelumnya. Praktik terbaik adalah menangani proses penerusan pesan sedini mungkin. Jika pesan tidak ditangani melalui seluruh proses, doesNotRecognizeSeletor kesalahan keliru yang diangkat untuk menyatakan pemilih tidak dapat dikenali oleh objek.
Saatnya menyelesaikan bagian teori dan kembali ke masalah ini.
Menurut informasi TCWebViewController dari Trace Stack, kami secara alami mengasosiasikannya dengan Tencent SDK Tencentopenapi.Framework , tetapi kami tidak memperbarui Tencent SDK baru -baru ini yang berarti kecelakaan itu tidak disebabkan oleh tencentopenapi.Framework .
Pertama, kami mendekompilasi kode dan mendapatkan struct dari kelas 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:
}
Dari hasil analisis statis, tidak ada metode setter dan getter dari requestURLStr di TCWebViewController . Karena tidak ada kerusakan seperti itu dalam versi aplikasi sebelumnya, kami mengeluarkan ide: Apakah properti di TCWebViewController diimplementasikan dengan cara yang dinamis yang menggunakan @dynamic untuk memberi tahu kompiler yang tidak menghasilkan pengambil dan setter untuk properti selama waktu menyusun tetapi secara dinamis dibuat dalam runtime seperti kerangka data inti ? Kemudian kami memutuskan untuk melakukan ide dalam untuk melihat apakah tebakan kami benar. Selama pelacakan kami, kami menemukan ada kategori NSObject(MethodSwizzlingCategory) untuk NSObject di tencentopenapi.framework yang sangat mencurigakan. Dalam kategori ini, ada metode switchMethodForCodeZipper yang implementasinya menggantikan methodSignatureForSelector dan Metode forwardInvocation dari QQmethodSignatureForSelector dan metode 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 ;
} Kemudian kami terus melacak metode QQmethodSignatureForSelector , dan ada metode bernama _AddDynamicPropertysSetterAndGetter di dalamnya. Dari namanya, kita dapat dengan mudah mendapatkan bahwa metode ini adalah menambahkan Metode Setter dan Getter untuk properti secara dinamis. Ini ditemukan secara substansial dapat memverifikasi tebakan asli kami benar.
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;
} Tetapi mengapa setter tidak dapat dikenali di kelas TCWebViewController ? Apakah karena metode QQMethodSignatureForSelector dibahas selama pengembangan versi ini? Namun kami tidak dapat menemukan petunjuk bahkan kami pergi ke mana -mana dalam kode. Itu sangat mengecewakan. Sejauh ini analisis statis dilakukan. Langkah selanjutnya adalah menggunakan LLDB untuk secara dinamis men -debug Tencent SDK untuk mengetahui jalur mana yang memecahkan penciptaan Getter dan Setter dalam proses penerusan pesan.
Jika kami mencoba mengatur breakpoint di
setRequestURLStrmelalui perintah LLDB, kami akan menemukan bahwa kami tidak dapat membuatnya. Alasannya adalah karena setter tidak tersedia selama penyusunan waktu. Ini juga dapat memverifikasi tebakan asli kami.
Menurut Trace Stack Crash, kita dapat menyimpulkan metode setRequestURLStr dipanggil -[TCWebViewKit open] , yang berarti crash terjadi selama Tencent SDK memeriksa apakah aplikasi QQ diinstal dan membuka kemajuan halaman web otentikasi.
Kemudian kami menggunakan perintah di bawah LLDB untuk mengatur breakpoint pada metode ini:
br s -n "-[TCWebViewKit open]"
br sadalah singkatan daribreakpoint set,-nmewakili set breakpoint sesuai dengan nama metode setelahnya, yang memiliki perilaku yang sama dengan breakpoint simbolik,br s -Fjuga dapat mengatur breakpoint.b -[TCWebViewKit open]juga berfungsi di sini, tetapibdi sini adalah singkatan dari_regexp-break, yang menggunakan ekspresi reguler untuk mengatur breakpoint. Pada akhirnya, kami juga dapat mengatur breakpoint pada alamat memori sepertibr s -a 0x000000010940b24e, yang dapat membantu mendebug blok jika alamat blok tersedia.
Sekarang breakpoint diatur dengan sukses.
Breakpoint 34: where = AADebug`-[TCWebViewKit open], address = 0x0000000103157f7d
Ketika aplikasi akan meluncurkan halaman otentikasi web, proyek dihentikan pada breakpoint ini. Lihat di bawah:

Tangkapan layar ini ditangkap saat aplikasi berjalan pada simulator, sehingga kode perakitan didasarkan pada x64. Jika Anda menggunakan perangkat iPhone, kode perakitan harus ARM. Tetapi metode analisisnya sama untuk mereka, harap perhatikan.
Tetapkan breakpoint pada baris 96, kode perakitan ini adalah doa metode setRequestURLStr , lalu cetak konten register rbx , maka kita dapat mengamati bahwa instance TCWebViewController disimpan dalam register ini.

Selanjutnya kita dapat menggunakan LLDB untuk mengatur Breakpoint untuk metode QQmethodSignatureForSelector :
br s -n "-[NSObject QQmethodSignatureForSelector:]"
Masukkan c di LLDB untuk membiarkan breakpoint berlanjut, maka Breakpoint akan berhenti di dalam metode QQmethodSignatureForSelector , yang dapat membuktikan tebakan kami sebelumnya tentang metode QQmethodSignatureForSelector yang bertentangan dengan kode kami tidak valid.

Tetapkan breakpoint di akhir metode QQmethodSignatureForSelector , yaitu perintah retq pada baris 31. Kemudian cetak alamat memori register rax , lihat di bawah tangkapan layar:

Dengan mencetak alamat memori 0x00007fdb36d38df0 dari Register rax , objek NSMethodSignature dikembalikan. Menurut Konvensi Desain tentang Bahasa Perakitan X86, nilai pengembalian disimpan dalam Register rax . Rupanya metode QQmethodSignatureForSelector dipanggil dan mengembalikan nilai yang benar, yang berarti kita perlu terus melacak masalah ini.
Atur breakpoint pada QQforwardInvocation melalui lldb:
br s -n "-[NSObject QQforwardInvocation:]"
Setelah breakpoint diatur, lanjutkan eksekusi program, aplikasi macet. Dan metode QQforwardInvocation belum dipanggil. Dengan ini, kita dapat menyimpulkan metode QQforwardInvocation dikonfirmasi oleh kode kita.

___forwarding___ Fungsi berisi seluruh implementasi mekanisme penerusan pesan, kode dekompilasi dipilih dari Objective-C 消息发送与转发机制原理. Dalam artikel ini, ada penilaian yang seharusnya salah antara forwarding dan receiver saat memanggil Metode forwardingTargetForSelector . Di sini harus menjadi penilaian antara forwardingTarget dan receiver . Lihat kode di bawah ini:
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);
}
Pada dasarnya, kita dapat memiliki pemahaman yang jelas melalui membaca kode dekompilasi: First Avoke forwardingTargetForSelector Metode selama proses penerusan pesan untuk mendapatkan penerima penggantian, yang juga disebut fase penerusan cepat. Jika forwardingTarget mengembalikan NIL atau mengembalikan penerima yang sama, penerusan pesan berubah menjadi fase penerusan reguler. Pada dasarnya, memohon metode methodSignatureForSelector untuk mendapatkan tanda tangan metode, kemudian menggunakannya dengan frameStackPointer untuk membuat objek invocation instantiate. Kemudian hubungi forwardInvocation: Metode receiver , dan lulus objek invocation sebelumnya sebagai argumen. Pada akhirnya jika metode methodSignatureForSelector tidak diimplementasikan dan selector sudah terdaftar dalam sistem runtime, doesNotRecognizeSelector: akan dipanggil untuk melempar kesalahan.
Meneliti ___forwarding___ dari jejak tumpukan crash, kita dapat melihat bahwa itu disebut sebagai jalur kedua di antara seluruh jalur penerusan pesan, yang berarti objek NSInvocation dipanggil ketika forwardInvocation dipanggil.
Anda juga dapat menjalankan perintah langkah demi langkah setelah breakpoint untuk mengamati jalur eksekusi kode perakitan, hasil yang sama harus diamati.

Dan metode mana yang dieksekusi ketika forwardInvocation dipanggil? Dari jejak tumpukan, kita dapat melihat metode bernama __ASPECTS_ARE_BEING_CALLED__ dieksekusi. Lihatlah metode seluruh proyek ini, kami akhirnya mengetahui bahwa forwardInvocation terpikat oleh kerangka 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 )];
} Karena TCWebViewController adalah kelas pribadi Tencent SDK, tidak mungkin ketagihan oleh kelas lain secara langsung. Tapi mungkin superclassnya ketagihan yang juga dapat mempengaruhi kelas ini. Dengan dugaan ini, kami terus menggali. Secara final, jawabannya muncul! Dengan menghapus atau mengomentari kode yang mengaitkan UIViewController , aplikasi tidak macet saat login melalui QQ. Sejauh ini, kami pasti yakin kecelakaan itu terlibat oleh kerangka 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.