
中文
Отладка имеет довольно плохую репутацию. Я имею в виду, что если бы у разработчика было полное понимание программы, не было бы никаких ошибок, и они не будут отлаживать в первую очередь, верно?
Не думаю так.
В вашем программном обеспечении всегда будут ошибки - или в любом программном обеспечении. Никакое количество тестового покрытия, наложенного вашим менеджером продукта, не исправит это. На самом деле, просмотр отладки как просто процесс исправления чего -то сломанного, на самом деле является ядовитым способом мышления, который умственно препятствует вашим аналитическим способностям.
Вместо этого вы должны рассматривать отладку как просто процесс, чтобы лучше понять программу . Это тонкая разница, но если вы действительно верите в это, любая предыдущая нехватка отладки просто исчезает.
Со времени, как Грейс Хоппер, основатель языка Cobol , обнаружил первую в мире ошибку в ретрансляционном компьютере, генерация ошибок в разработке программного обеспечения никогда не остановилась. Как говорит нам предисловие к книге «Расширенная отладка Apple и обратная инженерия»: «Разработчики не хотят думать, что если есть хорошее понимание того, как работает программное обеспечение, ошибки не будет. Следовательно, отладка является почти неизбежной фазой в жизненном цикле разработки программного обеспечения.
Если вы спросите неопытного программиста о том, как определить отладку, он может сказать «отладка - это то, что вы делаете, чтобы найти решение для вашей проблемы с программным обеспечением». Он прав, но это лишь крошечная часть реальной отладки.
Вот шаги реальной отладки:
Среди вышеупомянутых шагов наиболее важным шагом является первый шаг: выясните проблему. Видимо, это предпосылка других шагов.
Исследования показывают, что время, которые опытные программисты тратят на отладку, чтобы найти один и тот же набор дефектов, составляет около одного двадцатого неопытных программистов. Это означает, что опыт отладки имеет огромное значение в эффективности программирования. У нас есть много книг по дизайну программного обеспечения, к сожалению, редкие из них вводятся в представление о отладке, даже о курсах в школе.
По мере того, как отладчик улучшается за эти годы, стиль кодирования программистов тщательно изменился. Конечно, отладчик не может заменить хорошее мышление, мышление не может заменить превосходного отладчика, самая идеальная комбинация - отличный отладчик с хорошим мышлением.
Следующий график - это девять правил отладки, описанные в книге <отладка: 9 незаменимых правил для поиска даже самых неуловимых проблем с программным и аппаратным обеспечением>.

Хотя как программист iOS, большую часть времени в работе не будет иметь дело с языком сборки, но понимание, что сборка все еще очень полезна, особенно при отладке системной структуры или сторонней структурой без исходного кода.
Язык Assembly-это низкоуровневый машинный язык программирования, который можно рассматривать как набор Mnemonics для машинных инструкций для различных процессоров. Программисты могут использовать язык сборки для непосредственного управления компьютерной аппаратной системой. И программа, написанная на языке сборки, имеет много достоинств, таких как быстрая скорость выполнения и меньше воспоминаний.
До настоящего времени на платформе Apple, x86 и ARM широко используются две основные архитектуры. В мобильном устройстве с использованием языка сборки ARM, который является главным образом потому, что ARM представляет собой сниженную архитектуру вычислительной (RISC), с низким преимуществом энергопотребления. В то время как настольная платформа, такая как Mac OS, используется архитектура x86. Приложения, установленные на симуляторах iOS, фактически работают в качестве приложения 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 используется в инструментах разборки Hopper Disassemble и Ida Pro.
Белоты - это различия между Intel и AT & T:
Префикс операнда: в AT & T Syntax используется % в качестве префикса имени регистров, а $ используется в качестве префикса непосредственного операнда, в то время как префикс не используется как для регистров, так и для непосредственного операнда в Intel. Другое отличие - 0x добавлен в качестве префикса для шестнадцатеричного в AT & T. Таблица ниже демонстрирует разницу между их префиксами:
| AT & T. | Intel |
|---|---|
| Movq %Rax, %RBX | MOV RBX, RAX |
| Addq $ 0x10, %rsp | Добавить RSP, 010H |
В синтаксисе Intel,
hСуффикс используется для шестигранного операнда, а суффиксbиспользуется для бинарного операнда.
Operand: В AT & T Syntax, первым операндом является источник операнда, второй операнд - пункт назначения. Однако в синтаксисе Intel порядок операнда противоположный. С этого момента синтаксис AT & T более удобен для нас в соответствии с нашей привычкой чтения.
Режим адресации: сравнение с синтаксисом Intel, режим косвенной адресации 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) для AT & T, а segreg: [base+index*scale+disp] для Intel. Фактически, выше двух инструкций принадлежат к режиму адресации сегмента. segreg обозначает регистр сегмента, который обычно используется в реальном режиме, когда цифровая емкость CPU, адресованная реестрам. Например, ЦП может обратиться к 20-битному пространству, но в реестре есть только 16-битный. Для достижения 20-значного пространства необходимо использовать другой режим адресации: segreg:offset . В этом режиме адресации адрес смещения будет segreg * 16 + offset , но он более сложный, чем режим плоской памяти. В режиме защиты адресация находится под линейным адресным пространством, что означает, что базовый адрес сегмента можно игнорировать.
| 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 Syntax, все Opcodes имеют суффикс для указания размера. Как правило, есть четыре вида суффиксов: b , w , l и q b представляет 8-битный байт, w означает 16-битное слово, l означает 32-разрядное двойное слово. 32-значное слово также называется длинным словом, которое происходит с 16-битных дней. q представляет 64-битное четырехворное слово. Приведенная ниже диаграмма иллюстрирует синтаксис инструкции по переходу данных (MOV) в AT & T и 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 |
Как мы знаем, память используется для хранения инструкций и данных для ЦП. Память по сути является массивом байтов. Несмотря на то, что скорость доступа к памяти очень быстрая, нам по -прежнему нужно меньший и более быстрый блок хранения, чтобы ускорить выполнение инструкции ЦП, которое является регистрацией. Во время выполнения инструкции все данные временно хранятся в регистрах. Вот почему регистр назван в.
Когда процессоры растут с 16-битных до 32-битных, 8 регистров также продлеваются до 32-битных. После этого, когда используются расширенные регистры, E -префикс добавляется в исходное имя регистра. 32-битный процессор-это 32-разрядная архитектура Intel, которая составляет IA32. Сегодня основными процессорами являются 64-битная архитектура Intel, которая расширена от IA32 и называется X86-64. Поскольку IA32 прошел, эта статья будет сосредоточена только на X86-64. Обратите внимание, что в X86-64 количество регистров расширяется с 8 до 16. Только из-за этого расширения состояние программы может храниться в регистрах, но не в стеке. Таким образом, частота доступа к памяти значительно уменьшается.
В X86-64 существует 16 64-разрядных общих регистров и 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
Как мы знаем, в x86-64: XMM0 - XMM15 есть 16 регистров с плавающим указателем. На самом деле, есть некоторые другие детали этого. В результате вывода команды register read -a вы можете заметить, что есть регистры STMM и YMM, кроме группы регистра XMM. Здесь STMM является псевдонимом ST Register, а ST - это реестр FPU (точечный блок плавания) в x86 для обработки данных поплавки. FPU содержит один регистр поплавкового указателя, который имеет восемь 80 -битных регистров поплавкового указателя: ST0 - ST7. Мы можем заметить, что регистр STMM составляет 80-битный на выходе, что может доказать, что регистр STMM является ST. XMM-128-битный регистр, а регистр YMM-256-битный, что является расширением XMM. Фактически, xmm Register-это низкий 128-битный регистр YMM. Как и регистр EAX, низкий 32-битный регистра RAX. В Pentium III Intel опубликовал набор инструкций под названием SSE (потоковая расширения SIMD), которая является расширением MMX. Восемь новых 128 -битных регистров (xmm0 - xmm7) добавляются в SSE. AVX (Advanced Vector Extensions) Набор инструкций является расширением архитектуры SSE. Также в AVX 128-битный регистр XMM был расширен до 256-битного регистра YMM.

Вызов функции включает в себя передачу параметров и передачу управления от одного блока компиляции в другой. В процедуре вызова функции, передача данных, локальное назначение переменных и выпуск выполняются стеком. И стеки, назначенные одному вызову функции, называются стеком Frame.
Конвенция о вызове функции OS X X86-64 такая же, когда Конвенция, описанная в статье: System V Приложение Бинарный интерфейс AMD64 Architecture Processor Processor. Поэтому вы можете ссылаться на это, если вы заинтересованы в этом.
Во время отладки 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, называемый как Frame Pointer, всегда указывает на конец последнего выделенного кадра стека (высокий адрес). Регистр RSP, называемый как указатель стека, всегда указывает на верхнюю часть последней распределенной кадры стека (низкий адрес). Ниже приведена таблица стека кадров:

Position левого столбца - это адрес памяти, который использует режим косвенной адресации. Content является значением адреса в Position точках. Согласно структуре кадра стека в диаграмме выше, процедура вызова функции может быть описана как несколько шагов следующим образом:
Шаг 2 и 3 на самом деле принадлежат инструкции call . Кроме того, шаг 4 и шаг 5 могут быть описаны в инструкции сборки следующим образом:
TestDemo`-[ViewController viewDidLoad]:
0x1054e09c0 <+0>: pushq %rbp //step 4
0x1054e09c1 <+1>: movq %rsp, %rbp //step 5
Легко заметить, что эти два шага вместе с каждой вызовом функции. Существует еще одна деталь вышеупомянутой диаграммы: ниже ABI есть красная область, которая называется Red Zone. Это зарезервировано и не должно быть изменено с помощью обработчиков сигнала или прерываний. Поскольку он может быть изменен во время вызова функций, поэтому листовые функции, что означает те функции, которые никогда не вызывают другие функции, могут использовать эту область для временных данных.
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. Существует своего рода регистры, называемые регистром Function Serveer, что означает, что они принадлежат к вызова функции, но вызывающая функция требуется для сохранения их значений. Внизу инструкции сборки мы можем увидеть 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:
0x10915c714 <+68>: callq 0x1093ca502 ; symbol stub for: objc_msgSend
0x105206433 <+66>: callq *0xb3cd47(%rip) ; (void *)0x000000010475e800: objc_msgSend
Приведенный выше код показывает два использования инструкции call . В первом использовании операнд представляет собой адрес памяти, который на самом деле является заглушкой символа файла Mach-O. Он может искать символ функции через динамический линкер. Во втором использовании операнд фактически получается в режиме косвенной адресации. Кроме того, в AT & T Syntax, * должен быть добавлен в непосредственный операнд в инструкции по прыжкам/вызову (или прыжки, связанные с счетчиком программиста) в качестве префикса.
В общем, инструкция ret используется для возврата процедуры из вызоваской функции к вызовой функции. Эта инструкция вызывает адрес с вершины стека и возвращается к этому адресу и продолжает выполнять. В примере выше, он возвращается в next_instruction . Прежде чем выполнять инструкцию ret , регистры принадлежат к вызовой функции. Это уже упоминается на шаге 6 процедуры вызова функции.
Большинство функций имеют параметр, который может быть целым числом, поплавка, указатель и так далее. Кроме того, функции обычно имеют возвращаемое значение, которое может указывать на результат выполнения, достигнут успеха или не сбои. В OSX не более 6 параметров могут быть переданы через регистры, которые являются RDI, RSI, RDX, RCX, R8 и R9 в порядке. Как насчет функции с более чем 6 параметрами? Конечно, это обстоятельство существует. Если это произойдет, стек может использоваться для сохранения оставшихся параметров в обратном порядке. OSX имеет восемь регистров с плавающей запятой, которые позволяют передавать до 8 параметров плавания.
Что касается возвращаемого значения функции, rax Register используется для сохранения целочисленного возвращаемого значения. Если возвращаемое значение - это плавание, должны использоваться xmm0 - xmm1 регистры. Ниже диаграмма четко иллюстрирует соглашение об использовании регистра во время вызова функции.

preserved across function calls указывают, должен ли регистр сохранен через вызов функций. Мы можем видеть, что, помимо RBX, R12 - R15 Регистры, упомянутые выше, регистры RSP и RBP также принадлежат к регистрам Callee. Это связано с тем, что эти два регистра оставляют важные указатели местоположения, которые указывают на стек программ.
Далее мы сведем реальный пример, чтобы продемонстрировать инструкции в функциональном вызове. Возьмите 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 Test:]" | функция | 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, чтобы требовать входа в систему, SDK QQ пытается запустить веб -страницу авторизации в нашем приложении. В этом случае возникает непризнанная ошибка селектора [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 : Этот метод предоставляет второй приемник для обработки Unnks Message, и он быстрее, чем 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 из стека трассировки, мы, естественно, связываем ее с Tencent SDK Tencentopenapi.framework , но недавно мы не обновили Tencent SDK, что означает, что авария не была вызвана Tencentopenapi.framework .
Сначала мы разбили код и получили структуру класса TCWebViewController
@class TCWebViewController : UIViewController<UIWebViewDelegate, NSURLConnectionDelegate, NSURLConnectionDataDelegate> {
@property webview
@property webTitle
@property requestURLStr
@property error
@property delegate
@property activityIndicatorView
@property finished
@property theData
@property retryCount
@property hash
@property superclass
@property description
@property debugDescription
ivar _nloadCount
ivar _webview
ivar _webTitle
ivar _requestURLStr
ivar _error
ivar _delegate
ivar _xo
ivar _activityIndicatorView
ivar _finished
ivar _theData
ivar _retryCount
-setError:
-initWithNibName:bundle:
-dealloc
-stopLoad
-doClose
-viewDidLoad
-loadReqURL
-viewDidDisappear:
-shouldAutorotateToInterfaceOrientation:
-supportedInterfaceOrientations
-shouldAutorotate
-webViewDidStartLoad:
-webViewDidFinishLoad:
-webView:didFailLoadWithError:
-webView:shouldStartLoadWithRequest:navigationType:
}
Из результата статического анализа TCWebViewController requestURLStr было сеттера и метода Getter. Поскольку в предыдущей версии приложения не было такого сбоя, мы вышли идеей: было ли внедрение свойства в TCWebViewController динамичным способом, который использует @dynamic , чтобы сообщить компилятору, не генерирует Getter и Setter для свойства во время компиляции, но динамически создается во время выполнения, как основные данные данных ? Затем мы решили глубоко пойти на эту идею, чтобы увидеть, было ли наше предположение правильным. Во время нашего отслеживания мы обнаружили, что была категория NSObject(MethodSwizzlingCategory) для NSObject в Tencentopenapi.framework , которая была очень подозрительной. В этой категории был метод switchMethodForCodeZipper , реализация которой заменила методы методов methodSignatureForSelector и forwardInvocation of 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 . Из имени мы можем легко понять, что этот метод должен динамически добавить метод сеттера и геттера для свойств. Это найдено, что может существенно проверить, что наше первоначальное предположение верно.
void * -[ NSObject QQmethodSignatureForSelector: ]( void * self, void * _cmd, void * arg2) {
r14 = arg2;
rbx = self;
rax = [ self QQmethodSignatureForSelector: rdx];
if (rax == 0x0 ) {
rax = sel_getName (r14);
_AddDynamicPropertysSetterAndGetter ();
rax = 0x0 ;
if ( 0x0 != 0x0 ) {
rax = [rbx methodSignatureForSelector: r14];
}
}
return rax;
} Но почему сеттер не может распознать в классе TCWebViewController ? Это потому, что метод QQMethodSignatureForSelector был освещен во время нашей разработки этой версии? Однако мы не смогли найти подсказку, даже мы проходили везде в коде. Это было очень разочаровывающим. Пока что статический анализ сделан. Следующим шагом является использование LLDB для динамической отладки Tencent SDK, чтобы выяснить, какой путь сломал создание Getter и Setter в процессе пересылки сообщений.
Если мы попытаемся установить точку останова в команде
setRequestURLStrчерез LLDB, мы обнаружим, что не сможем это сделать. Причина в том, что сеттер недоступен во время компиляции. Это также может проверить наше первоначальное предположение.
Согласно трассировке стека сбоев, мы можем заключить 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, код сборки должен быть ARM. Но метод анализа тот же для них, пожалуйста, обратите внимание.
Установите точку останова в строке 96, этот код сборки представляет собой вызов метода setRequestURLStr , а затем распечатайте содержимое rbx Register, затем мы можем заметить, что в этом регистре сохраняется экземпляр TCWebViewController .

Далее мы можем использовать LLDB для установки точки останова для метода QQmethodSignatureForSelector :
br s -n "-[NSObject QQmethodSignatureForSelector:]"
Введите c в LLDB, чтобы позволить точке останова продолжаться, затем точка остановки остановится в методе QQmethodSignatureForSelector , который может доказать наше предыдущее предположение о методе QQmethodSignatureForSelector , противоречающего нашему коду, является недействительным.

Установите точку останова в конце метода QQmethodSignatureForSelector , то есть команда retq в строке 31. Затем распечатайте адрес памяти Resgre rax , см. Ниже экрана: Screenshot:

Печать адреса памяти 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 . Затем Call forwardInvocation: Метод receiver и передайте предыдущий объект invocation в качестве аргумента. В конце концов, если метод methodSignatureForSelector не реализован, и selector уже зарегистрирован в системе времени выполнения, doesNotRecognizeSelector: будет вызван для вызывания ошибки.
Пристанивая ___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.