
中文
La depuración tiene una reputación bastante mala. Quiero decir, si el desarrollador tuviera una comprensión completa del programa, no habría ningún error y no estarían depurando en primer lugar, ¿verdad?
No pienses así.
Siempre habrá errores en su software, o cualquier software, para el caso. Ninguna cantidad de cobertura de prueba impuesta por su gerente de producto solucionará eso. De hecho, ver la depuración como un proceso de arreglar algo que está roto es en realidad una forma de pensar venenosa que obstaculizará mentalmente sus habilidades analíticas.
En cambio, debe ver la depuración como simplemente un proceso para comprender mejor un programa . Es una diferencia sutil, pero si realmente lo crees, cualquier trabajo pesado previo de depuración simplemente desaparece.
Desde Grace Hopper, el fundador del idioma Cobol , descubrió el primer error del mundo en una computadora de retransmisión, la generación de errores en el desarrollo de software nunca se ha detenido. Como el prefacio del Libro de 《La depuración avanzada de Apple y la ingeniería inversa》 nos dice: los desarrolladores no quieren pensar que si hay una buena comprensión de cómo funciona el software, no habrá errores. Por lo tanto, la depuración es casi una fase inevitable en el ciclo de vida del desarrollo de software.
Si le pregunta a un programador inexperto sobre cómo definir la depuración, él podría decir que "la depuración es algo que hace para encontrar una solución para su problema de software". Tiene razón, pero eso es solo una pequeña parte de una verdadera depuración.
Estos son los pasos de una depuración real:
Entre los pasos anteriores, el paso más importante es el primer paso: descubra el problema. Aparentemente, es un requisito previo de otros pasos.
La investigación muestra que el tiempo que los programadores experimentados gastan en la depuración para localizar el mismo conjunto de defectos es aproximadamente una vigésima de programadores inexpertos. Eso significa que la experiencia de depuración hace una enorme diferencia en la eficiencia de programación. Tenemos muchos libros sobre diseño de software, desafortunadamente, raros tienen una introducción sobre la depuración, incluso los cursos en la escuela.
A medida que el depurador mejora a lo largo de los años, el estilo de codificación de los programadores cambia a fondo. Por supuesto, el depurador no puede reemplazar el buen pensamiento, el pensamiento no puede reemplazar el excelente depurador, la combinación más perfecta es excelente depurador con buen pensamiento.
El siguiente gráfico son las nueve reglas de depuración descritas en el libro <depuración: las 9 reglas indispensables para encontrar incluso los problemas de software y hardware más evasivos>.

Aunque como programador de iOS, la mayor parte del tiempo en el trabajo no se ocupará del lenguaje de ensamblaje, pero comprender que el ensamblaje sigue siendo muy útil, especialmente cuando se depugga un marco del sistema o un marco de terceros sin el código fuente.
El lenguaje de Assombly es un lenguaje de programación orientado a máquina de bajo nivel, que puede considerarse como una colección de mnemónicas para las instrucciones de la máquina para varias CPU. Los programadores pueden usar el lenguaje de ensamblaje para controlar el sistema de hardware de la computadora directamente. y el programa escrito en lenguaje de ensamblaje tiene muchos méritos, como la velocidad de ejecución rápida y menos memoria ocupada.
Hasta ahora, dos arquitecturas principales se usan ampliamente en la plataforma Apple, X86 y ARM. En el dispositivo móvil, utilizando el lenguaje de ensamblaje del brazo, que se debe principalmente a que el brazo es una arquitectura de computación (RISC) de instrucciones reducidas, con baja ventaja de consumo de energía. Mientras que la plataforma de escritorio como Mac OS, se utiliza la arquitectura X86. Las aplicaciones instaladas en los simuladores iOS en realidad se ejecutan como una aplicación Mac OS dentro del simulador, lo que significa que el simulador funciona como un contenedor. Dado que nuestro caso fue depurado en los simuladores de iOS, el objetivo de investigación principal es el lenguaje de ensamblaje X86 .
El lenguaje de ensamblaje X86 evoluciona en dos ramas de sintaxis: Intel (utilizado orignialmente en la documentación de la plataforma X86) y AT&T. Intel domina la familia MS-DOS y Windows, mientras que AT&T es común en la familia Unix. Hay una gran diferencia en la sintaxis entre Intel y AT&T, como variable, constante, el acceso de los registros, el direccionamiento indirecto y el desplazamiento. Aunque su diferencia de sintaxis es enorme, el sistema de hardware es el mismo, lo que significa que uno de ellos puede migrarse a otro sin problemas. Dado que el lenguaje de ensamblaje de AT&T se usa en Xcode, nos centraremos en AT&T en la parte inferior.
Tenga en cuenta que la sintaxis Intel se usa en las herramientas de desmontaje de la tolva desmontar e Ida Pro.
Los bellezas son las diferencias entre Intel y AT&T:
El prefijo de operando: en la sintaxis de AT&T, % se usa como prefijo del nombre de los registros y $ se usa como prefijo de operando inmediato, mientras que no se usa ningún prefijo para los registros y el operando inmediato en Intel. La otra diferencia es 0x se agrega como el prefijo para hexadecimal en AT&T. El cuadro a continuación demuestra la diferencia entre sus prefijos:
| AT&T | Intel |
|---|---|
| Movq %RAX, %RBX | MoV RBX, RAX |
| ADDQ $ 0x10, %RSP | Agregar RSP, 010h |
En la sintaxis Intel, el sufijo
hse usa para operando hexadecimal y el sufijobse usa para operando binario.
Operando: en la sintaxis de AT&T, el primer operando es operando fuente, el segundo operando es el operando de destino. Sin embargo, en la sintaxis Intel, el orden de operando es opuesto. Desde este punto, la sintaxis de AT&T es más cómoda para nosotros según nuestro hábito de lectura.
Modo de direccionamiento: en comparación con la sintaxis Intel, el modo de direccionamiento indirecto de AT&T es difícil de leer. Sin embargo, el algoritmo del cálculo de la dirección es el mismo: address = disp + base + index * scale . base representa la dirección base, disp significa dirección de compensación, index * scale determina la ubicación de un elemento, scale es el tamaño de un elemento que solo puede ser una potencia de dos. disp/base/index/scale son todos opcionales, el valor predeterminado del index es 0, mientras que el valor predeterminado de scale es 1. Ahora veamos la instrucción de cálculo de la dirección: %segreg: disp(base,index,scale) es para AT&T, y segreg: [base+index*scale+disp] es para Intel. De hecho, por encima de dos instrucciones pertenecen al modo de direccionamiento del segmento. segreg significa Registro de segmentos que generalmente se usa en modo real cuando la capacidad de dígitos de la CPU se dirige más allá del dígito del registro. Por ejemplo, la CPU puede abordar el espacio de 20 bits, pero el registro solo tiene 16 bits. Para lograr un espacio de 20 dígitos, se necesita utilizar otro modo de direccionamiento: segreg:offset . Con este modo de direccionamiento, la dirección de compensación será segreg * 16 + offset , pero es más complicado que el modo de memoria plana. En el modo de protección, el direccionamiento está en el espacio de direcciones lineal, lo que significa que la dirección base del segmento puede ignorarse.
| 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] |
Si el operando inmediato llega en el lugar de
disposcale, se puede omitir$sufijo. En la sintaxis Intel,byte ptr,word ptr,dword ptryqword ptrdeben agregarse antes del operando de memoria.
Sufijo de Opcode: en la sintaxis de AT&T, todos los códigos de operación tienen un sufijo para especificar el tamaño. Generalmente hay cuatro tipos de sufijos: b , w , l y q b representa el byte de 8 bits, w significa palabra de 16 bits, l significa palabra doble de 32 bits. La palabra de 32 dígitos también se llama palabra larga que es de los días de 16 bits. q representa QuadWord de 64 bits. El cuadro a continuación ilustra la sintaxis de la instrucción de transición de datos (MOV) en AT&T e Intel.
| AT&T | Intel |
|---|---|
| movb %al, %bl | Mov Bl, Al |
| movw %ax, %bx | Mov Bx, Axe |
| movl %eax, %ebx | Mov Ebx, eax |
| Movq %RAX, %RBX | MoV RBX, RAX |
Como sabemos, la memoria se usa para almacenar instrucciones y datos para CPU. La memoria es esencialmente una variedad de bytes. Aunque la velocidad del acceso a la memoria es muy rápida, aún necesitamos una unidad de almacenamiento más pequeña y más rápida para acelerar la ejecución de instrucciones de la CPU, que está registrada. Durante la ejecución de la instrucción, todos los datos se almacenan temporalmente en registros. Es por eso que Registro se nombra.
Cuando los procesadores crecen de 16 bits a 32 bits, 8 registros también se extienden a 32 bits. Después de eso, cuando se utilizan los registros extendidos, el prefijo E se agrega al nombre de registro original. El procesador de 32 bits es la arquitectura Intel de 32 bits, que es IA32. Hoy, los procesadores principales son la arquitectura Intel de 64 bits, que se extiende desde IA32 y se llama X86-64. Como IA32 ha pasado, este artículo solo se centrará en x86-64. Tenga en cuenta que en x86-64, la cantidad de registros se extiende de 8 a 16. Solo por esta extensión, el estado del programa puede almacenarse en registros pero no pilas. Por lo tanto, la frecuencia del acceso a la memoria se reduce enormemente.
En x86-64, hay 16 registros generales de 64 bits y 16 registros de puntero flotantes. Además, la CPU tiene un registro de puntero de instrucciones más de 64 bits llamado rip . Está diseñado para almacenar la dirección de la siguiente instrucción ejecutada. También hay otros registros que no se usan ampliamente, no tenemos la intención de hablar de ellos en este artículo. Entre los 16 registros generales, ocho de ellos son del IA32: RAX 、 RCX 、 RDX 、 RBX 、 RSI 、 RDI 、 RSP y RBP. Los otros ocho registros generales son nuevos agregados desde x86-64 que son R8 - R15. Los 16 registros flotantes son XMM0 - XMM15.
Las CPU actuales son de 8088, el registro también se extiende de 16 bits a 32 bits y finalmente a 64 bits. Por lo tanto, el programa aún puede acceder a los registros bajos de 8 bits o 16 o 32 bits.
A continuación, la tabla ilustra los 16 registros generales de X86-64:

Uso del comando register read en LLDB puede descargar los datos de registro de la trama actual de la pila.
Por ejemplo, podemos usar el siguiente comando para mostrar todos los datos en el registro:
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
Como sabemos, hay 16 registros de puntero flotantes en x86-64: xmm0 - xmm15. De hecho, hay otros detalles de ello. En la salida del comando register read -a , puede notar que tiene registros STMM y YMM además del grupo de registro XMM. Aquí STMM es un alias de registro ST, y ST es un registro de FPU (unidad de punto flotante) en X86 para manejar los datos de flotación. La FPU contiene un registro de puntero de flotación que tiene ocho registros de puntero de flotador de 80 bits: ST0 - ST7. Podemos observar que el registro STMM está a 80 bits desde la salida, lo que puede probar que el registro STMM es el registro ST. XMM es un registro de 128 bits, y el registro YMM es de 256 bits, que es una extensión de XMM. De hecho, el registro XMM es el registro bajo de 128 bits de YMM. Al igual que el registro EAX es el bajo registro RAX de 32 bits. En Pentium III, Intel publicó un conjunto de instrucciones llamado SSE (extensiones SIMD de transmisión), que es una extensión de MMX. En SSE se agregan ocho nuevos registros de 128 bits (XMM0 - XMM7). El conjunto de instrucciones AVX (extensiones de vectores avanzados) es una arquitectura de extensión de SSE. También en AVX, el registro XMM de 128 bits se extendió a un registro de 256 bits ymm.

Una función de función incluye el paso de parámetros y la transferencia de control de una unidad de compilación a otra. En el procedimiento de llamadas de funciones, la aprobación de datos, la asignación de variables locales y la liberación se llevan a cabo por pila. Y las pilas asignadas a una sola llamada de la función se llaman marco de pila.
La convención de llamadas de función de OS X x86-64 es la misma con la Convención descrita en el artículo: Sistema V Aplicación Binaria Binary Interface AMD64 Suplemento del procesador de arquitectura. Por lo tanto, puede consultarlo si está interesado en él.
Durante la depuración de LLDB, podemos usar el comando bt para imprimir el rastro de la pila del hilo actual, como a continuación:
(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
De hecho, el comando bt es viable al marco de pila. El cuadro de pila preserva la dirección de retorno y la variable local para las funciones que pueden verse como un contexto de ejecución de funciones. Como sabemos, el montón crece hacia arriba, mientras que la pila crece hacia abajo, desde direcciones de memoria de gran número a pequeñas. Una vez que se llama a una función, se asigna un marco de pila independiente para la función de la función. El registro RBP, llamado como puntero de marco, siempre apunta al final del último marco de pila asignada (dirección alta). El registro RSP, llamado como puntero de pila, siempre apunta a la parte superior del último marco de pila asignado (dirección baja). A continuación se muestra una tabla de pila de cuadros:

La Position de la columna izquierda es la dirección de memoria que utiliza el modo de direccionamiento indirecto. Content es el valor de la dirección en puntos Position . Según la estructura del marco de la pila en la tabla anterior, el procedimiento de llamadas de función puede describirse como varios pasos de la siguiente manera:
El paso 2 y 3 en realidad pertenecen a la instrucción call . Además, el paso 4 y el paso 5 se pueden describir en la instrucción de ensamblaje de la siguiente manera:
TestDemo`-[ViewController viewDidLoad]:
0x1054e09c0 <+0>: pushq %rbp //step 4
0x1054e09c1 <+1>: movq %rsp, %rbp //step 5
Es fácil notar que estos dos pasos están junto con cada llamada de función. Hay otro detalle de la tabla anterior: hay un área roja debajo del registro RSP, que ABI se llama zona roja. Es reservado y no se modificará por señal o interrumpir manejadores. Dado que puede modificarse durante la llamada de función, por lo tanto, las funciones de hoja, lo que significa que aquellas funciones que nunca llaman a otras funciones pueden usar esta área para datos temporales.
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
Entre las instrucciones anteriores, las instrucciones de 0x1064a63f5 a 0x1064a63fd pertenecen al paso 6. Hay un tipo de registros llamados Registro de funciones que significa que pertenecen a la función de llamadas, pero se requiere la función llamada para preservar sus valores. A continuación, las instrucciones de ensamblaje, podemos ver que RBX, RSP y R12 - R15 pertenecen a dichos registros.
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
La instrucción para llamar a una función es call llamada a continuación:
call function
function en el parámetro son los procedimientos en el segmento de texto . La instrucción Call puede dividirse en dos pasos. El primer paso es presionar la siguiente dirección de instrucción de instrucción call en la pila. Aquí, la siguiente dirección es en realidad la dirección de retorno después de que se termine la función llamada. El segundo paso es saltar a function . La instrucción call es equivalente a las dos instrucciones a continuación:
push next_instruction
jmp function
El siguiente es el ejemplo de instrucción call en el simulador iOS:
0x10915c714 <+68>: callq 0x1093ca502 ; symbol stub for: objc_msgSend
0x105206433 <+66>: callq *0xb3cd47(%rip) ; (void *)0x000000010475e800: objc_msgSend
El código anterior muestra dos usos de instrucción call . En el primer uso, el operando es una dirección de memoria que en realidad es un símbolo de un archivo Mach-O. Puede buscar el símbolo de una función a través del enlazador dinámico. En el segundo uso, el operando se obtiene realmente mediante el modo de direccionamiento indirecto. Además, en la sintaxis de AT&T, * debe agregarse al operando inmediato en la instrucción de salto/llamada (o los saltos relacionados con el mostrador del programador) como prefijo.
En general, la instrucción ret se utiliza para devolver el procedimiento de la función llamada a la función de llamada. Esta instrucción aparece la dirección desde la parte superior de la pila y regresa a esa dirección y sigue ejecutándose. En el ejemplo anterior, regresa a next_instruction . Antes de que se ejecute la instrucción ret , los registros pertenecen a la función de llamadas se encontrarán. Esto ya se menciona en el paso 6 del procedimiento de llamadas de funciones.
La mayoría de las funciones tienen un parámetro que puede ser entero, flotante, puntero, etc. Además, las funciones generalmente tienen un valor de retorno que puede indicar que el resultado de la ejecución tiene éxito o fallado. En OSX, en la mayoría de los 6 parámetros se pueden pasar a través de registros que son RDI, RSI, RDX, RCX, R8 y R9 en orden. ¿Qué tal una función con más de 6 parámetros? Por supuesto, esta circunstancia existe. Si esto sucede, la pila se puede usar para preservar los parámetros restantes en orden invertido. OSX tiene ocho registros de puntos flotantes que permiten pasar hasta 8 parámetros flotantes.
Sobre el valor de retorno de una función, el registro rax se usa para guardar el valor de retorno de entero. Si el valor de retorno es un flotador, se utilizarán registros XMM0 - XMM1. A continuación, la tabla ilustra claramente la convención de uso del registro durante la llamada de función.

preserved across function calls indica si el registro debe conservarse en toda la llamada de función. Podemos ver que, además de RBX, los registros R12 - R15 mencionados anteriormente, los registros RSP y RBP también pertenecen a los registros salvados de la Callee. Esto se debe a que estos dos registros reservan los consejos de ubicación importantes que apuntan a la pila de programas.
A continuación, seguiremos un ejemplo real para demostrar las instrucciones en una llamada de función. Tome el macro DDLogError en CocoaLumberjack como ejemplo. Cuando se llama a esta macro, Método de clase log:level:flag:context:file:function:line:tag:format: se llama. El siguiente código y las instrucciones son sobre la llamada de DDLogError y las instrucciones de ensamblaje correspondientes:
- (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
Dado que todas las funciones de Objective-C se convertirán en la invocación de la función objc_msgSend , entonces log:level:flag:context:file:function:line:tag:format: Método Finalmente se convierte en Códigos a continuación:
objc_msgSend(DDLog, @selector(log:level:flag:context:file:function:line:tag:format:), asynchronous, level, flag, context, file, function, line, tag, format, sender)
Ya mencionamos en la mayoría de los 6 registros se pueden usar para el paso de los parámetros. Los parámetros excesivos pueden usar pila para hacer el paso. Dado que la función anterior tiene más de 6 parámetros, el pase de parámetros usaría registros y pila. Debajo de dos tablas describe el uso detallado de los registros y la pila para el paso de parámetros de la invocación de la función DDLogError .
| Registro general | valor | Parámetros | Instrucciones de ensamblaje | Comentario |
|---|---|---|---|---|
| RDI | Ddlog | ser | 0x102C568EB <+171>: Movq %R11, %RDI | |
| RSI | "Registro: Nivel: Flag: Contexto: Archivo: Función: Línea: Etiqueta: Formato:" | opción | 0x102C568F2 <+178>: Movq %R15, %RSI | |
| rdx | 0 | asincrónico | 0x102c568a3 <+99>: Xorl %edx, %EDX | Xorl es una operación exclusiva o. Aquí se usa para borrar el registro EDX |
| RCX | 184467444073709551615 | nivel | 0x102C568F9 <+185>: Movq %RBX, %RCX | (Ddloglevelall o nsuintegermax) |
| R8 | 1 | bandera | 0x102C568AA <+106>: Movl %EAX, %R8D | Ddlogflagerror |
| R9 | 0 | contexto | 0x102C568AF <+111>: Movl %EAX, %R9D |
| Desplazamiento del marco de la pila | Valor | Parámetros | Instrucciones de ensamblaje | Comentario |
|---|---|---|---|---|
| (%RSP) | "/Users/dev-aozhimin/desktop/testdlog/testdlog/viewcontroller.m" | archivo | 0x102C56900 <+192>: Movq %R11, ( %RSP) | |
| 0x8 (%RSP) | "-[ViewController Test:]" | función | 0x102C56908 <+200>: Movq %RBX, 0x8 ( %RSP) | |
| 0x10 (%RSP) | 0x22 | línea | 0x102c5690d <+205>: Movq $ 0x22, 0x10 (%RSP) | La invocación correspondiente de ddlogerror está en la línea 34 |
| 0x18 (%RSP) | 0x0 | etiqueta | 0x102C56916 <+214>: Movq $ 0x0, 0x18 (%RSP) | nulo |
| 0x20 (%RSP) | "TestDDLog:%@" | formato | 0x102C5691F <+223>: Movq %R10, 0x20 ( %RSP) | |
| 0x28 (%RSP) | remitente | El primer parámetro de parámetros variables | 0x102C56924 <+228>: Movq %R14, 0x28 ( %RSP) | Una instancia de Uibutton |
Si el valor del registro es una cadena, como el parámetro
open el registrorsi, la cadena se puede imprimir directamente en LLDB a través del comandopo (char *) $rsi. De lo contrario,po $rsise puede usar para imprimir un valor en formato entero.
Con la ayuda del lenguaje de ensamblaje, podemos analizar algún conocimiento de bajo nivel que sea muy necesario durante la depuración. Me esfuerzo mucho para introducir el conocimiento relacionado con la asamblea lo más detallado posible. Sin embargo, la jerarquía de conocimiento de la asamblea es demasiado enorme para describir en un artículo. Consulte las referencias mencionadas anteriormente. Además, el tercer capítulo de CSAPP - La representación de nivel de máquina de un programa también es muy recomendable. Es un material raro de buena referencia.
Este artículo ilustra el procedimiento de depuración a través de un caso real. Algunos de los detalles se cambian para proteger la privacidad personal.
El problema del que vamos a hablar fue que estaba sucediendo cuando estaba desarrollando un SDK de inicio de sesión. Un usuario reclamó que la aplicación se bloqueó cuando presionó el botón "QQ" en la página de inicio de sesión. Mientras depugamos este problema, descubrimos que el bloqueo ocurrió si la aplicación QQ no se instaló al mismo tiempo. Cuando el usuario presiona el botón QQ para requerir un inicio de sesión, el QQ Sogin SDK intenta iniciar una página web de autorización en nuestra aplicación. En este caso, se produce un error selector no reconocido [TCWebViewController setRequestURLStr:] .
PD: Para centrarse en el tema, la información innecesaria de depuración comercial no se enumera a continuación. Mientras tanto, Aadebug se usa como nombre de nuestra aplicación.
Aquí está el rastro de la pila de este choque:
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
Antes de hablar sobre la depuración, nos familiaricemos con el reenvío de mensajes en Objective-C. Como sabemos, Objective-C usa una estructura de mensajería en lugar de llamar a funciones. La diferencia clave es que en la estructura de mensajería, el tiempo de ejecución decide qué función se ejecutará no compilando el tiempo. Eso significa que si se envía un mensaje no reconocido a un objeto, no sucederá nada durante el tiempo de compilación. Y durante el tiempo de ejecución, cuando recibe un método que no entiende, un objeto pasa por el reenvío de mensajes, un proceso diseñado para permitirle como desarrollador decirle al mensaje cómo manejar el mensaje desconocido.
Por debajo de cuatro métodos generalmente participan durante el reenvío de mensajes:
+ (BOOL)resolveInstanceMethod:(SEL)sel : Este método se llama cuando se pasa un mensaje desconocido a un objeto. Este método toma el selector que no se encontró y devuelve un valor booleano para indicar si se agregó un método de instancia a la clase que ahora puede manejar ese selector. Si la clase puede manejar este selector, devuelva sí, entonces se completa el proceso de reenvío del mensaje. Este método a menudo se usa para acceder a las propiedades @dynamic de nsmanageBjects en coredata de manera dinámica. + (BOOL)resolveClassMethod:(SEL)sel es similar con el método anterior, la única diferencia es este método de clase, el otro es el método de instancia.
- (id)forwardingTargetForSelector:(SEL)aSelector : Este método proporciona un segundo receptor para manejar el mensaje de desconocimiento, y es más rápido que forwardInvocation: . Este método se puede utilizar para imitar algunas características de la herencia múltiple. Tenga en cuenta que no hay forma de manipular el mensaje utilizando esta parte de la ruta de reenvío. Si el mensaje debe modificarse antes de enviar al receptor de reemplazo, se debe usar el mecanismo de reenvío completo.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector : Si el algoritmo de reenvío ha llegado tan lejos, se inicia el mecanismo de reenvío completo. NSMethodSignature se devuelve mediante este método que incluye la descripción del método en el parámetro Aselector. Tenga en cuenta que este método debe anularse si desea crear un objeto NSInvocation que contenga selector, objetivo y argumentos durante el reenvío del mensaje.
- (void)forwardInvocation:(NSInvocation *)anInvocation : la implementación de este método debe contener a continuación las partes: averiguar el objeto que puede manejar el mensaje de aninvocation; Enviando un mensaje a ese objeto, el aninVocation guarda el valor de retorno, el tiempo de ejecución luego envía el valor de retorno al remitente del mensaje original. De hecho, este método puede tener el mismo comportamiento con forwardingTargetForSelector: método simplemente cambiando el objetivo de invocación e invocándolo después, pero apenas lo hacemos.
Por lo general, los dos primeros métodos utilizados para el reenvío de mensajes se llaman como reenvío rápido , ya que proporciona una forma mucho más rápida de hacer el reenvío del mensaje. Para distinguir del reenvío rápido, el método 3 y 4 se denominan reenvío normal o reenvío regular . Es mucho más lento porque tiene que crear objeto NSInVocation para completar el reenvío del mensaje.
Nota: Si el método
methodSignatureForSelectorno se anula o laNSMethodSignaturede devuelto es nulo, no se llamaráforwardInvocation, y el reenvío del mensaje se termina con el errordoesNotRecognizeSelectorque se plantea. Podemos verlo desde el código fuente de la función__forwarding__a continuación.
El proceso de reenvío de mensajes se puede describir mediante un diagrama de flujo, ver más abajo.

Como se describe en el diagrama de flujo, en cada paso, el receptor tiene la oportunidad de manejar el mensaje. Cada paso es más caro que el anterior. La mejor práctica es manejar el proceso de reenvío de mensajes lo antes posible. Si el mensaje no se maneja a través de todo el proceso, el objeto doesNotRecognizeSeletor reconocer que el error no se puede reconocer que el selector no puede reconocer el selector.
Es hora de terminar la parte de la teoría y volver al tema.
Según la información TCWebViewController de la pila Trace, naturalmente la asociamos con el Tencent SDK Tencentopenapi.framework , pero no actualizamos el SDK de Tencent recientemente, lo que significa que el accidente no fue causado por Tencentopenapi.Framework .
Primero, descompilamos el código y obtuvimos la estructura de la clase 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:
}
Desde el resultado del análisis estático, no hubo método Setter y Getter de requestURLStr en TCWebViewController . Debido a que no hubo tal bloqueo en la versión anterior de la aplicación, salimos una idea: ¿la propiedad en TCWebViewController se implementó de una manera dinámica que usa @dynamic para decirle al compilador que no genera Getter y Setter para la propiedad durante el tiempo de compilación pero creado dinámicamente en tiempo de ejecución como el marco de datos central ? Luego decidimos ir profundamente a la idea para ver si nuestra suposición era correcta. Durante nuestro seguimiento, descubrimos que había una categoría NSObject(MethodSwizzlingCategory) para NSObject en Tencentopenapi.framework que era muy sospechoso. En esta categoría, había un método switchMethodForCodeZipper cuya implementación reemplazó el methodSignatureForSelector y los métodos forwardInvocation de los métodos QQmethodSignatureForSelector y 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 ;
} Luego seguimos rastreando el método QQmethodSignatureForSelector , y había un método llamado _AddDynamicPropertysSetterAndGetter en él. A partir del nombre, podemos obtener fácilmente que este método es agregar el método Setter y Getter para propiedades dinámicamente. Esto encontrado puede verificar sustancialmente nuestra suposición original es correcta.
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;
} Pero, ¿por qué el setter no puede reconocerse en la clase TCWebViewController ? ¿Es porque el método QQMethodSignatureForSelector se cubrió durante nuestro desarrollo de esta versión? Sin embargo, no pudimos encontrar una pista que incluso pasamos por todas partes en el código. Eso fue muy decepcionante. Hasta ahora se realiza el análisis estático. El siguiente paso es usar LLDB para depurar dinámicamente el SDK de Tencent para averiguar qué ruta rompió la creación de Getter y Setter en el proceso de reenvío de mensajes.
Si intentamos establecer el punto de interrupción en el comando
setRequestURLStra través del comando LLDB, encontraremos que no podemos hacerlo. La razón es porque el setter no está disponible durante el tiempo de compilación. Esto también puede verificar nuestra suposición original.
Según el trace de la pila de choque, podemos concluir que setRequestURLStr se llama -[TCWebViewKit open] , lo que significa que el bloqueo ocurre durante la verificación de Tencent SDK si la aplicación QQ está instalada y abriendo el progreso de la página web de autenticación.
Luego usamos el comando debajo de LLDB para establecer el punto de interrupción en este método:
br s -n "-[TCWebViewKit open]"
br ses la abreviatura delbreakpoint set,-nrepresenta establecer el punto de interrupción de acuerdo con el nombre del método después de él, que tiene el mismo comportamiento con el punto de interrupción simbólico,br s -Ftambién puede establecer el punto de interrupción.b -[TCWebViewKit open]también funciona aquí, perobaquí está la abreviatura de_regexp-break, que utiliza la expresión regular para establecer el punto de interrupción. Al final, también podemos establecer el punto de interrupción en la dirección de memoria comobr s -a 0x000000010940b24e, lo que puede ayudar a depurar el bloque si la dirección del bloque está disponible.
A estas alturas, el punto de interrupción se establece con éxito.
Breakpoint 34: where = AADebug`-[TCWebViewKit open], address = 0x0000000103157f7d
Cuando la aplicación va a iniciar la página de autenticación web, el proyecto se detiene en este punto de ruptura. Consulte a continuación:

Esta captura de pantalla se captura cuando la aplicación se ejecuta en simulador, por lo que el código de ensamblaje se basa en x64. Si está utilizando el dispositivo iPhone, el código de ensamblaje debe ser ARM. Pero el método de análisis es el mismo para ellos, por favor, note.
Establezca un punto de interrupción en la línea 96, este código de ensamblaje es la invocación del método setRequestURLStr , luego imprima el contenido del registro rbx , luego podemos observar que la instancia TCWebViewController se guarda en este registro.

A continuación, podemos usar LLDB para establecer el punto de interrupción para el método QQmethodSignatureForSelector :
br s -n "-[NSObject QQmethodSignatureForSelector:]"
Ingrese c en LLDB para dejar que el punto de interrupción continúe, luego el punto de interrupción se detendrá dentro del método QQmethodSignatureForSelector , lo que puede probar nuestra suposición anterior sobre QQmethodSignatureForSelector Method que conflicto con nuestro código no es válido.

Establezca un punto de interrupción al final del método QQmethodSignatureForSelector , ese es el comando retq en la línea 31. Luego imprima la dirección de memoria del registro rax , consulte a la siguiente captura de pantalla:

Al imprimir la dirección de memoria 0x00007fdb36d38df0 del registro rax , se devuelve el objeto NSMethodSignature . Según la Convención de Diseño sobre el lenguaje de ensamblaje X86, el valor de retorno se guarda en el registro rax . Aparentemente, se invoca el método QQmethodSignatureForSelector y devuelve el valor correcto, lo que significa que debemos seguir rastreando el problema.
Establezca el punto de interrupción en QQforwardInvocation a través de LLDB:
br s -n "-[NSObject QQforwardInvocation:]"
Después de establecer el punto de interrupción, continúe con la ejecución del programa, la aplicación se bloquea. Y el método QQforwardInvocation aún no se ha llamado. Con esto, podemos concluir que el método QQforwardInvocation está en conflicto con nuestro código.

___forwarding___ La función contiene toda la implementación del mecanismo de reenvío de mensajes, el código de descompilación se selecciona de Objective-C 消息发送与转发机制原理. En este artículo, hay un juicio que debe ser incorrecto entre forwarding y receiver al llamar al método forwardingTargetForSelector . Aquí debería ser un juicio entre forwardingTarget y receiver . Consulte el código a continuación:
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);
}
Básicamente, podemos tener una comprensión clara mediante la lectura del código de descomposición: Invoca el método de forwardingTargetForSelector durante el proceso de reenvío de mensajes para obtener el receptor de reemplazo, que también se llama fase de reenvío rápido. Si el forwardingTarget devuelve nulo o devuelve el mismo receptor, el reenvío del mensaje se convierte en la fase de reenvío regular. Básicamente, invocar el método methodSignatureForSelector para obtener la firma del método, luego usarlo con frameStackPointer para instanciar el objeto invocation . Luego llame forwardInvocation: Método del receiver y pase el objeto invocation anterior como argumento. Al final, no se implementa el método de methodSignatureForSelector y el selector ya está registrado en el sistema de tiempo de ejecución, doesNotRecognizeSelector: se invocará para lanzar un error.
Examinando el ___forwarding___ de la traza de la pila de choque, podemos notar que se llama como la segunda ruta entre toda la ruta de reenvío del mensaje, lo que significa que el objeto NSInvocation se invoca cuando se llama forwardInvocation .
También puede ejecutar el comando paso a paso después del punto de interrupción para observar la ruta de ejecución del código de ensamblaje, se debe observar el mismo resultado.

¿Y qué método se ejecuta cuando se llama forwardInvocation ? Desde el rastro de la pila, podemos ver que se ejecuta un método llamado __ASPECTS_ARE_BEING_CALLED__ . Revise este método de todo el proyecto, finalmente descubrimos que forwardInvocation está enganchado por el marco 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 )];
} Dado que TCWebViewController es una clase privada de Tencent SDK, es poco probable que otra clase sea enganchada directamente por otra clase. Pero es posible que su superclase esté enganchado, lo que también puede afectar esta clase. Con esta conjetura, seguimos cavando. Finialmente, surgió la respuesta! Al eliminar o comentar el código que enganchaba UIViewController , la aplicación no se bloqueó al iniciar sesión a través de QQ. Hasta ahora, definitivamente estábamos seguros de que el accidente estuvo involucrado por el marco 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.