
中文
A depuração tem uma reputação bastante ruim. Quero dizer, se o desenvolvedor tivesse uma compreensão completa do programa, não haveria insetos e eles não estariam depurando em primeiro lugar, certo?
Não pense assim.
Sempre haverá bugs em seu software - ou em qualquer software, nesse caso. Nenhuma quantidade de cobertura de teste imposta pelo seu gerente de produto vai consertar isso. De fato, ver a depuração como apenas um processo de consertar algo que está quebrado é realmente uma maneira venenosa de pensar que impedirá mentalmente suas habilidades analíticas.
Em vez disso, você deve ver a depuração como simplesmente um processo para entender melhor um programa . É uma diferença sutil, mas se você realmente acredita, qualquer labuta anterior de depuração simplesmente desaparece.
Desde Grace Hopper, o fundador da língua COBOL , descobriu o primeiro bug do mundo em um computador de revezamento, a geração de bug no desenvolvimento de software nunca parou. Como prefácio do livro de 《《《Apple Apple Debugging & Reverse Engineering》 nos diz: os desenvolvedores não querem pensar que, se houver uma boa compreensão de como o software funciona, não haverá bug. Portanto, a depuração é quase uma fase inevitável no ciclo de vida do desenvolvimento de software.
Se você perguntar a um programador inexperiente sobre como definir a depuração, ele pode dizer "depuração é algo que você faz para encontrar uma solução para o seu problema de software". Ele está certo, mas isso é apenas uma pequena parte de uma depuração real.
Aqui estão as etapas de uma depuração real:
Entre as etapas acima, a etapa mais importante é o primeiro passo: descubra o problema. Aparentemente, é um pré -requisito de outros passos.
Pesquisas mostram que os programadores experimentados pelo tempo gastam em depuração para localizar o mesmo conjunto de defeitos, é cerca de um vigésimo de programadores inexperientes. Isso significa que a experiência de depuração faz uma enorme diferença na eficiência da programação. Temos muitos livros sobre design de software, infelizmente, raros deles têm introdução sobre depuração, até os cursos da escola.
À medida que o depurador melhora ao longo dos anos, o estilo de codificação dos programadores é alterado minuciosamente. Obviamente, o depurador não pode substituir o pensamento bom, pensando que não pode substituir o excelente depurador, a combinação mais perfeita é um excelente depurador por um bom pensamento.
O gráfico a seguir são as nove regras de depuração descritas no livro <depuração: as 9 regras indispensáveis para encontrar até os problemas de software e hardware mais ilusórios>.

Embora como programador do iOS, na maioria das vezes no trabalho não lide com o idioma da montagem, mas entenda que a assembléia ainda é muito útil, especialmente ao depurar uma estrutura do sistema ou uma estrutura de terceiros sem o código-fonte.
A linguagem Asssembly é uma linguagem de programação orientada para a máquina de baixo nível, que pode ser pensada como uma coleção de mnemônicas para instruções da máquina para várias CPUs. Os programadores podem usar a linguagem de montagem para controlar diretamente o sistema de hardware do computador. E o programa escrito em linguagem de montagem tem muitos méritos, como velocidade rápida de execução e menos memória ocupada.
Até agora, duas principais arquiteturas são amplamente utilizadas na plataforma da Apple, x86 e braço. No dispositivo móvel, usando a linguagem de montagem do braço, que ocorre principalmente porque o ARM é uma arquitetura de computação de conjunto de instruções reduzida (RISC), com baixa vantagem de consumo de energia. Enquanto a plataforma de desktop como o Mac OS, a arquitetura x86 é usada. Os aplicativos instalados nos simuladores iOS estão realmente sendo executados como um aplicativo Mac OS dentro do simulador, o que significa que o simulador está funcionando como um contêiner. Como o nosso caso foi depurado nos simuladores do iOS, o principal objetivo de pesquisa é o idioma da Assembléia X86 .
A linguagem de montagem x86 evolui para duas ramificações de sintaxe: Intel (usada orgenialmente na documentação da plataforma X86) e AT&T. A Intel domina a família MS-DOS e Windows, enquanto a AT&T é comum na família Unix. Há uma enorme diferença na sintaxe entre a Intel e a AT&T, como variável, constante, acesso de registros, endereçamento indireto e deslocamento. Embora sua diferença de sintaxe seja enorme, o sistema de hardware é o mesmo, o que significa que um deles pode ser migrado para o outro sem problemas. Como a linguagem de montagem da AT&T é usada no Xcode, vamos nos concentrar na AT&T abaixo da parte.
Observe que a sintaxe da Intel é usada nas ferramentas de desmontagem da desmontagem da tremonha e do IDA Pro.
Belins são as diferenças entre a Intel e a AT&T:
O prefixo do operando: na sintaxe da AT&T, % é usado como prefixo do nome dos registros e $ é usado como o prefixo do operando imediato, enquanto nenhum prefixo é usado para registros e operando imediato na Intel. A outra diferença é 0x é adicionada como o prefixo para hexadecimal na AT&T. O gráfico abaixo demonstra a diferença entre seus prefixos:
| AT&T | Intel |
|---|---|
| MOVQ %RAX, %RBX | MOV RBX, RAX |
| Addq $ 0x10, %rsp | Adicione RSP, 010H |
Na sintaxe Intel, o sufixo
hé usado para operando hexadecimal e o sufixobé usado para operando binário.
Operand: na sintaxe da AT&T, o primeiro operando é o operando de origem, o segundo operando é o operando de destino. No entanto, na sintaxe da Intel, a ordem do operando é oposta. A partir deste ponto, a sintaxe da AT&T é mais confortável para nós, de acordo com nosso hábito de leitura.
Modo de endereço: Comparando com a sintaxe Intel, é difícil ler o modo de endereçamento indireto da AT&T. No entanto, o algoritmo do cálculo do endereço é o mesmo: address = disp + base + index * scale . base representa o endereço base, disp significa endereço de deslocamento, index * scale determina a localização de um elemento, scale é o tamanho de um elemento que pode ser apenas uma potência de dois. disp/base/index/scale são todos opcionais, o valor padrão do index é 0, enquanto o valor padrão da scale é 1. Agora, vamos ver a instrução do cálculo do endereço: %segreg: disp(base,index,scale) é para AT&T e segreg: [base+index*scale+disp] é para Intel. De fato, acima de duas instruções pertencem ao modo de endereçamento do segmento. segreg significa registro de segmento, que geralmente é usado no modo real quando a capacidade de dígitos da CPU abordando além do registro 'Digit. Por exemplo, a CPU pode abordar espaço de 20 bits, mas o registro possui apenas 16 bits. Para obter espaço de 20 dígitos, outro modo de endereço precisa ser usado: segreg:offset . Com esse modo de endereço, o endereço de deslocamento será segreg * 16 + offset , mas é mais complicado que o modo de memória plana. No modo de proteção, o endereço está em espaço de endereço linear, o que significa que o endereço base do segmento pode ser ignorado.
| 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] |
Se o operando imediato estiver no local de
dispouscale,$sufixo poderá ser omitido. Na Sintaxe Intel,byte ptr,word ptr,dword ptreqword ptrprecisam ser adicionados antes do operando da memória.
Sufixo do código de opções: na sintaxe da AT&T, todos os códigos de operações têm um sufixo para especificar o tamanho. Geralmente, existem quatro tipos de sufixos: b , w , l e q b representa byte de 8 bits, w significa palavra de 16 bits, l significa uma palavra dupla de 32 bits. A palavra de 32 dígitos também é chamada de palavra longa, que é dos dias de 16 bits. q representa Quadword de 64 bits. O gráfico abaixo ilustra a sintaxe da instrução de transição de dados (MOV) na AT&T e Intel.
| AT&T | Intel |
|---|---|
| movb %al, %bl | Mov Bl, Al |
| MOVW %AX, %BX | mov bx, machado |
| movl %eax, %ebx | Mov EBX, Eax |
| MOVQ %RAX, %RBX | MOV RBX, RAX |
Como sabemos, a memória é usada para armazenar instruções e dados para a CPU. A memória é essencialmente uma matriz de bytes. Embora a velocidade do acesso à memória seja muito rápida, ainda precisamos de uma unidade de armazenamento menor e mais rápida para acelerar a execução de instruções da CPU, que é registrada. Durante a execução de instruções, todos os dados são temporariamente armazenados nos registros. É por isso que o registro é nomeado.
Quando os processadores crescem de 16 bits a 32 bits, 8 registros também são estendidos a 32 bits. Depois disso, quando os registros estendidos são usados, o prefixo E é adicionado ao nome do registro original. O processador de 32 bits é a Intel Architecture 32 bits, que é IA32. Hoje, os principais processadores são a arquitetura Intel de 64 bits, que é estendida da IA32 e foi chamada X86-64. Como o IA32 foi passado, este artigo se concentrará apenas no x86-64. Observe que, em X86-64, a quantidade de registros é estendida de 8 a 16. Apenas devido a essa extensão, o estado do programa pode ser armazenado em registros, mas não em pilhas. Assim, a frequência do acesso à memória é extremamente reduzida.
Em x86-64, existem 16 registros gerais de 64 bits e 16 registros de ponteiro flutuante. Além disso, a CPU possui mais um registro de ponteiro de instrução de 64 bits chamado rip . Ele foi projetado para armazenar o endereço da próxima instrução executada. Existem também outros registros que não são amplamente utilizados, não pretendemos falar sobre eles neste artigo. Entre os 16 registros gerais, oito deles são do IA32: Rax 、 rcx 、 rdx 、 rbx 、 rsi 、 rdi 、 rsp e rbp. Os outros oito registros gerais são novos adicionados desde x86-64, que são R8 - R15. Os 16 registros flutuantes são XMM0 - XMM15.
As CPUs atuais são de 8088, o registro também é estendido de 16 bits a 32 bits e finalmente para 64 bits. Assim, o programa ainda pode acessar os baixos 8 bits ou 16 bits ou 32 bits dos registros.
Abaixo, o gráfico ilustra os 16 registros gerais de x86-64:

O uso do comando register read no LLDB pode despejar os dados de registro do quadro de pilha atual.
Por exemplo, podemos usar o comando abaixo para mostrar todos os dados no 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, existem 16 registros de ponteiro flutuante em x86-64: xmm0 - xmm15. De fato, existem outros detalhes. Na saída do comando register read -a , você pode notar que existem registros STMM e YMM além do grupo de registros XMM. Aqui, o STMM é um alias do ST Register, e ST é um registro de FPU (unidade de ponto float) em x86 para lidar com dados de flutuação. O FPU contém um registro de ponteiro de flutuação que possui oito registros de ponteiro de float de 80 bits: ST0 - ST7. Podemos observar que o registro STMM é de 80 bits da saída, que pode provar que o registro STMM é o registro ST. O XMM é o registro de 128 bits e o registro YMM é de 256 bits, que é uma extensão do XMM. De fato, o registro XMM é o registro baixo de 128 bits de YMM. Como o registro EAX, é o baixo registro de 32 bits de Rax. No Pentium III, a Intel publicou um conjunto de instruções chamado SSE (Streaming SIMD Extensions), que é uma extensão do MMX. Oito novos registros de 128 bits (XMM0 - XMM7) são adicionados no SSE. O conjunto de instruções AVX (Avançado Vector Extensions) é uma arquitetura de extensão do SSE. Também no AVX, o registro de 128 bits XMM foi estendido para o registro de 256 bits YMM.

Uma chamada de função inclui a passagem de parâmetros e a transferência de controle de uma unidade de compilação para outra. No procedimento de chamada de função, a passagem de dados, a atribuição e a liberação da variável local são realizadas pela pilha. E as pilhas atribuídas a uma única chamada de função são chamadas de quadro de pilha.
A Convenção de Chamada de Função do OS X X86-64 é a mesma da convenção descrita no artigo: System V Application Binisty Interface Binária AMD64 Suplemento de processador de arquitetura. Portanto, você pode se referir a ele se estiver interessado nele.
Durante a depuração do LLDB, podemos usar o comando bt para imprimir o rastreamento da pilha do thread atual, como abaixo:
(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 fato, o comando bt é viável no quadro da pilha. O quadro da pilha preserva o endereço de retorno e a variável local para funções que podem ser vistas como um contexto de execução de uma função. Como sabemos, a pilha cresce para cima, enquanto a pilha cresce para baixo, que é de endereços de memória de grande número para pequenos números. Depois que uma função é chamada, um quadro de pilha independente é atribuído para a chamada de função. O registro RBP, chamado de ponteiro de quadro, sempre aponta para o final do quadro de pilha alocada mais recente (endereço alto). O registro RSP, chamado de ponteiro de pilha, sempre aponta para o topo do mais recente quadro de pilha alocada (endereço baixo). Abaixo está um gráfico de pilha de quadros:

A Position da coluna esquerda é o endereço de memória que usa o modo de endereçamento indireto. Content é o valor do endereço no ponto Position para. De acordo com a estrutura do quadro de pilha no gráfico acima, o procedimento de chamada de função pode ser descrito como várias etapas da seguinte forma:
Os passo 2 e 3 realmente pertencem à instrução call . Além disso, a etapa 4 e a etapa 5 podem ser descritas nas instruções de montagem da seguinte forma:
TestDemo`-[ViewController viewDidLoad]:
0x1054e09c0 <+0>: pushq %rbp //step 4
0x1054e09c1 <+1>: movq %rsp, %rbp //step 5
É fácil perceber que essas duas etapas são junto com cada chamada de função. Há outro detalhe do gráfico acima: há uma área vermelha abaixo do registro RSP, chamado de zona vermelha pela ABI. É reservado e não deve ser modificado por manipuladores de sinal ou interrupção. Como pode ser modificado durante a chamada de função, portanto, as funções foliares, o que significa que essas funções que nunca chamam outras funções podem usar essa área para dados temporários.
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 as instruções acima, as instruções de 0x1064a63f5 a 0x1064a63fd pertencem à etapa 6. Existe um tipo de registro chamado registro de preservação da função, o que significa que eles pertencem à função de chamada, mas a função chamada é necessária para preservar seus valores. A partir das instruções de montagem abaixo, podemos ver RBX, RSP e R12 - R15, todos pertencem a esses 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
A instrução para chamar uma função é call , consulte abaixo:
call function
function no parâmetro está os procedimentos no segmento de texto . A instrução Call pode se dividir em duas etapas. A primeira etapa é pressionar o próximo endereço de instrução da instrução call na pilha. Aqui, o próximo endereço é na verdade o endereço de retorno após o término da função chamada. O segundo passo é o salto para function . A instrução call é equivalente a abaixo de duas instruções:
push next_instruction
jmp function
A seguir, o exemplo de instrução call no simulador iOS:
0x10915c714 <+68>: callq 0x1093ca502 ; symbol stub for: objc_msgSend
0x105206433 <+66>: callq *0xb3cd47(%rip) ; (void *)0x000000010475e800: objc_msgSend
O código acima mostra dois usos de instrução call . No primeiro uso, o operando é um endereço de memória que é na verdade um símbolo de um arquivo mach-o. Ele pode pesquisar o símbolo de uma função através do ligante dinâmico. No segundo uso, o operando é realmente obtido pelo modo de endereçamento indireto. Além disso, na sintaxe da AT&T, * precisa ser adicionado ao operando imediato na instrução de salto/chamada (ou os saltos relacionados ao contador de programador) como um prefixo.
Em geral, a instrução ret é usada para retornar o procedimento da função chamada para a função de chamada. Esta instrução retira o endereço da parte superior da pilha e volta a esse endereço e continua executando. No exemplo acima, ele salta de volta para next_instruction . Antes de a instrução ret ser executada, os registros pertencem à função de chamada serão exibidos. Isso já é mencionado na etapa 6 do procedimento de chamada de função.
A maioria das funções possui parâmetro que pode ser inteiro, flutuação, ponteiro e assim por diante. Além disso, as funções geralmente têm valor de retorno, o que pode indicar que o resultado da execução é bem -sucedido ou falhado. No OSX, no máximo 6 parâmetros podem ser passados através de registros que são RDI, RSI, RDX, RCX, R8 e R9 em ordem. Que tal uma função com mais de 6 parâmetros? Claro, essa circunstância existe. Se isso acontecer, a pilha pode ser usada para preservar os parâmetros restantes na ordem invertida. O OSX possui oito registros de ponto flutuante que permitem passar até 8 parâmetros de flutuação.
Sobre o valor de retorno de uma função, o registro rax é usado para salvar o valor de retorno inteiro. Se o valor de retorno for um flutuador, os registros XMM0 - XMM1 devem ser usados. Abaixo, o gráfico ilustra claramente a Convenção de Uso do Registro durante a chamada de função.

preserved across function calls indica se o registro precisa ser preservado na chamada de função. Podemos ver que, além dos registros RBX, R12 - R15 mencionados acima, os registros RSP e RBP também pertencem a registros salvos de Callee. Isso ocorre porque esses dois registros reservam os indicadores importantes de localização que apontam para a pilha do programa.
Em seguida, seguiremos um exemplo real para demonstrar as instruções em uma chamada de função. Pegue o macro DDLogError em CocoaLumberjack como exemplo. Quando essa macro é chamada, Class Método log:level:flag:context:file:function:line:tag:format: é chamado. A seguir, o código e as instruções são sobre a chamada do DDLogError e as instruções de montagem correspondentes:
- (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
Como todas as funções do Objective-C se transformarão na invocação da função objc_msgSend , então log:level:flag:context:file:function:line:tag:format: o método finalmente se transforma em códigos abaixo:
objc_msgSend(DDLog, @selector(log:level:flag:context:file:function:line:tag:format:), asynchronous, level, flag, context, file, function, line, tag, format, sender)
Já mencionamos que no máximo 6 registros podem ser usados para passagem de parâmetros. Os parâmetros em excesso podem usar a pilha para fazer a passagem. Como a função acima possui mais de 6 parâmetros, a passagem do parâmetro usaria os registros e a pilha. Abaixo, duas tabelas descrevem o uso detalhado dos registros e a pilha para a passagem do parâmetro da invocação da função DDLogError .
| Registro geral | valor | Parâmetros | Instruções de montagem | Comentário |
|---|---|---|---|---|
| rdi | Ddlog | auto | 0x102c568eb <+171>: movq %r11, %rdi | |
| RSI | "LOG: Nível: Sinalizador: Contexto: Arquivo: Função: Linha: Tag: Formato:" | op | 0x102c568f2 <+178>: movq %r15, %rsi | |
| rdx | 0 | assíncrono | 0x102c568a3 <+99>: xorl %edx, %edx | Xorl é uma operação exclusiva ou ou exclusiva. Aqui é usado para limpar o registro EDX |
| rcx | 18446744073709551615 | nível | 0x102c568f9 <+185>: movq %rbx, %rcx | (DDLoglevelall ou NsuintegerMax) |
| R8 | 1 | bandeira | 0x102c568aa <+106>: movl %eax, %r8d | DdLogflagerror |
| R9 | 0 | contexto | 0x102c568af <+111>: movl %eax, %r9d |
| Deslocamento da estrutura da pilha | Valor | Parâmetros | Instruções de montagem | Comentário |
|---|---|---|---|---|
| (%rsp) | "/Users/dev-aozhimin/desktop/testddlog/testddlog/viewcontroller.m" | arquivo | 0x102c56900 <+192>: movq %r11, ( %rsp) | |
| 0x8 (%rsp) | "-[teste ViewController:]" | função | 0x102c56908 <+200>: movq %rbx, 0x8 ( %rsp) | |
| 0x10 (%rsp) | 0x22 | linha | 0x102c5690d <+205>: movq $ 0x22, 0x10 (%rsp) | A invocação correspondente de ddlogerror está na linha 34 |
| 0x18 (%rsp) | 0x0 | marcação | 0x102c56916 <+214>: MOVQ $ 0x0, 0x18 (%rsp) | nil |
| 0x20 (%rsp) | "Testddlog:%@" | formatar | 0x102c5691f <+223>: movq %r10, 0x20 ( %rsp) | |
| 0x28 (%rsp) | remetente | O primeiro parâmetro de parâmetros variáveis | 0x102c56924 <+228>: movq %r14, 0x28 ( %rsp) | Uma instância de UIBUBILDTON |
Se o valor do registro for uma string, como o parâmetro
opno registrorsi, a string poderá ser impressa diretamente no comando lldb atravéspo (char *) $rsi. Caso contrário,po $rsipode ser usado para imprimir um valor no formato inteiro.
Com a ajuda da linguagem da Assembléia, podemos analisar algum conhecimento de baixo nível que é muito necessário durante a depuração. Eu tento muito introduzir o conhecimento relacionado à montagem o mais detalhado possível. No entanto, a hierarquia de conhecimento da montagem é enorme demais para descrever em um artigo. Consulte as referências mencionadas acima. Além disso, o terceiro capítulo do CSAPP - a representação do nível da máquina de um programa também é altamente recomendado. É um bom material raro para referência.
Este artigo ilustra o procedimento de depuração por meio de um caso real. Alguns dos detalhes são alterados para proteger a privacidade pessoal.
A questão sobre a qual vamos falar estava acontecendo quando eu estava desenvolvendo um SDK de login. Um usuário alegou que o aplicativo travou quando pressionou o botão "QQ" na página de login. Ao depurar esse problema, descobrimos que o acidente aconteceu se o aplicativo QQ não fosse instalado ao mesmo tempo. Quando o usuário pressiona o botão QQ para exigir um login, o QQ Login SDK tenta iniciar uma página da Web de autorização em nosso aplicativo. Nesse caso, ocorre um erro seletor não reconhecido [TCWebViewController setRequestURLStr:] .
PS: Para se concentrar na questão, as informações desnecessárias de depuração de negócios não estão listadas abaixo. Enquanto isso, o Aadebug é usado como nome do nosso aplicativo.
Aqui está o traço da pilha deste acidente:
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 falar sobre a depuração, vamos nos familiarizar com o encaminhamento de mensagens no Objective-C. Como sabemos, o Objective-C usa uma estrutura de mensagens em vez de chamadas de função. A principal diferença é que, na estrutura de mensagens, o tempo de execução decide qual função será executada e não com o tempo de compilação. Isso significa que, se uma mensagem não reconhecida for enviada a um objeto, nada acontecerá durante o tempo de compilação. E durante o tempo de execução, quando recebe um método que não entende, um objeto passa pelo encaminhamento de mensagens, um processo projetado para permitir que você como desenvolvedor diga a mensagem como lidar com a mensagem desconhecida.
Abaixo, quatro métodos geralmente estão envolvidos durante o encaminhamento de mensagens:
+ (BOOL)resolveInstanceMethod:(SEL)sel : Este método é chamado quando uma mensagem desconhecida é passada para um objeto. Esse método leva o seletor que não foi encontrado e retorna um valor booleano para indicar se um método de instância foi adicionado à classe que agora pode lidar com esse seletor. Se a classe puder lidar com esse seletor, retorne sim, o processo de avanço da mensagem será concluído. Esse método é frequentemente usado para acessar as propriedades @Dynamic de NSmanAgedObjects em Coredata de uma maneira dinamicamente. + (BOOL)resolveClassMethod:(SEL)sel o método é semelhante ao método acima, a única diferença é esse método de classe, o outro é o método da instância.
- (id)forwardingTargetForSelector:(SEL)aSelector : Este método fornece um segundo receptor para lidar com a mensagem desconhecida e é mais rápido que forwardInvocation: . Este método pode ser usado para imitar alguns recursos de herança múltipla. Observe que não há como manipular a mensagem usando esta parte do caminho de encaminhamento. Se a mensagem precisar ser alterada antes de enviar para o receptor de substituição, o mecanismo de encaminhamento completo deverá ser usado.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector : Se o algoritmo de encaminhamento chegou até aqui, o mecanismo de encaminhamento completo será iniciado. NSMethodSignature é retornado por esse método, que inclui a descrição do método no parâmetro Aselector. Observe que esse método precisa ser substituído se você deseja criar um objeto NSInvocation que contém seletor, destino e argumentos durante o encaminhamento da mensagem.
- (void)forwardInvocation:(NSInvocation *)anInvocation : A implementação deste método deve contém abaixo as partes: Descubra o objeto que pode lidar com a mensagem Aninvocation; Enviando mensagem para esse objeto, o AninVocation salva o valor de retorno, o tempo de execução envia o valor de retorno para o remetente de mensagem original. De fato, esse método pode ter o mesmo comportamento com forwardingTargetForSelector: Método simplesmente alterando o alvo de invocação e invocando -o depois, mas mal fazemos isso.
Geralmente, os dois primeiros métodos usados para encaminhamento de mensagens são chamados de encaminhamento rápido , porque fornece uma maneira muito mais rápida de fazer o encaminhamento de mensagens. Para distinguir do encaminhamento rápido, o método 3 e 4 é chamado de encaminhamento normal ou encaminhamento regular . É muito mais lento, porque precisa criar um objeto NSInvocation para concluir o encaminhamento da mensagem.
NOTA: Se o método
methodSignatureForSelectornão for substituído ou oNSMethodSignatureretornado for nulo,forwardInvocationnão será chamado e o encaminhamento da mensagem será encerrado com o errodoesNotRecognizeSelector. Podemos vê -lo a partir do código -fonte da função__forwarding__abaixo.
O processo de encaminhamento de mensagens pode ser descrito por um diagrama de fluxo, veja abaixo.

Como descrito no diagrama de fluxo, em cada etapa, o receptor tem a chance de lidar com a mensagem. Cada passo é mais caro que o antes. A melhor prática é lidar com o processo de encaminhamento de mensagens o mais cedo possível. Se a mensagem não for tratada por todo o processo, não é reconhecido o erro doesNotRecognizeSeletor para declarar que o seletor não poderá ser reconhecido pelo objeto.
É hora de terminar a parte da teoria e voltar ao problema.
De acordo com as informações TCWebViewController da pilha de rastreamentos, naturalmente as associamos ao tencent SDK tencentopenapi.Framework , mas não atualizamos o SDK tencent recentemente, o que significa que o acidente não foi causado pelo tencentopi.framework .
Primeiro, descompilamos o código e recebemos a estrutura da classe 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:
}
A partir do resultado da análise estática, não havia método de Setter e Getter de requestURLStr no TCWebViewController . Como não houve tal falha na versão anterior do aplicativo, tivemos uma idéia: a propriedade no TCWebViewController foi implementada de uma maneira dinâmica que usa @dynamic para dizer ao compilador que não gera getter e setter para a propriedade durante o tempo de compilação, mas criado dinamicamente no tempo de execução como a estrutura de dados principal ? Então decidimos ir profundamente na idéia de ver se nosso palpite estava correto. Durante o nosso rastreamento, descobrimos que havia uma categoria NSObject(MethodSwizzlingCategory) para NSObject em tencentopenapi.framework que foi muito suspeito. Nesta categoria, havia um método switchMethodForCodeZipper cuja implementação substituiu os métodos methodSignatureForSelector e forwardInvocation dos métodos QQmethodSignatureForSelector e 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 ;
} Em seguida, continuamos rastreando o método QQmethodSignatureForSelector , e havia um método chamado _AddDynamicPropertysSetterAndGetter nele. A partir do nome, podemos obter facilmente que esse método é adicionar o método Setter e Getter para propriedades dinamicamente. Isso encontrado pode verificar substancialmente que nosso palpite original está correto.
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;
} Mas por que o setter não pode ser reconhecido na classe TCWebViewController ? É porque o método QQMethodSignatureForSelector foi abordado durante o desenvolvimento desta versão? No entanto, não conseguimos encontrar uma pista, mesmo passamos por toda parte do código. Isso foi muito decepcionante. Até agora, a análise estática é feita. A próxima etapa é usar o LLDB para depurar dinamicamente o Tencent SDK para descobrir qual caminho quebrou a criação do processo Getter e Setter no encaminhamento de mensagens.
Se tentarmos definir o ponto de interrupção no
setRequestURLStratravés do comando LLDB, descobriremos que não podemos fazê -lo. O motivo é que o setter não está disponível durante o tempo de compilação. Isso também pode verificar nosso palpite original.
De acordo com o acidente, podemos concluir que setRequestURLStr é chamado no método -[TCWebViewKit open] , o que significa que a falha ocorre durante a verificação do tencent SDK se o aplicativo QQ estiver instalado e abrir o progresso da página da web de autenticação.
Em seguida, usamos abaixo o comando lldb para definir o ponto de interrupção neste método:
br s -n "-[TCWebViewKit open]"
br sé a abreviação parabreakpoint set,-nrepresenta definir o ponto de interrupção de acordo com o nome do método após ele, que tem o mesmo comportamento com ponto de interrupção simbólico,br s -Ftambém pode definir o ponto de interrupção.b -[TCWebViewKit open]também funciona aqui, masbAqui está a abreviação de_regexp-break, que usa expressão regular para definir o ponto de interrupção. No final, também podemos definir o ponto de interrupção no endereço de memória comobr s -a 0x000000010940b24e, que pode ajudar a depurar o bloco se o endereço do bloco estiver disponível.
A essa altura, o ponto de interrupção está definido com sucesso.
Breakpoint 34: where = AADebug`-[TCWebViewKit open], address = 0x0000000103157f7d
Quando o aplicativo lançará a página de autenticação da Web, o projeto é interrompido neste ponto de interrupção. Consulte abaixo:

Esta captura de tela é capturada quando o aplicativo é executado no simulador, portanto o código de montagem é baseado no x64. Se você estiver usando o dispositivo iPhone, o código de montagem deve ser o ARM. Mas o método de análise é o mesmo para eles, observe -o.
Defina um ponto de interrupção na linha 96, este código de montagem é a invocação do método setRequestURLStr e imprima o conteúdo do registro rbx e, em seguida, podemos observar que a instância TCWebViewController está salva neste registro.

Em seguida, podemos usar o LLDB para definir o ponto de interrupção do método QQmethodSignatureForSelector :
br s -n "-[NSObject QQmethodSignatureForSelector:]"
Digite c no LLDB para permitir que o ponto de interrupção continue, e o ponto de interrupção parará dentro do método QQmethodSignatureForSelector , que pode provar nosso palpite anterior sobre o método QQmethodSignatureForSelector que conflita com nosso código é inválido.

Defina um ponto de interrupção no final do método QQmethodSignatureForSelector , que é o comando retq na linha 31. Em seguida, imprima o endereço de memória do registro rax , consulte a CLESHOT ABAIXO:

Ao imprimir o endereço de memória 0x00007fdb36d38df0 do registro rax , o objeto NSMethodSignature é retornado. De acordo com a Convenção de Design sobre o idioma da Assembléia X86, o valor de retorno é salvo no Register rax . Aparentemente, o método QQmethodSignatureForSelector é chamado e retorna o valor correto, o que significa que precisamos continuar rastreando o problema.
Defina o ponto de interrupção no QQforwardInvocation via LLDB:
br s -n "-[NSObject QQforwardInvocation:]"
Depois que o ponto de interrupção é definido, continue a execução do programa, o aplicativo é travado. E o método QQforwardInvocation ainda não foi chamado. Com isso, podemos concluir que o método QQforwardInvocation está em conflito pelo nosso código.

___forwarding___ Função contém toda a implementação do mecanismo de encaminhamento de mensagens, o código de descompilação é selecionado em Objective-C 消息发送与转发机制原理. Neste artigo, há um julgamento que deve estar incorreto entre forwarding e receiver ao ligar para o método de forwardingTargetForSelector . Aqui deve ser um julgamento entre forwardingTarget e receiver . Consulte o código abaixo:
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);
}
Basicamente, podemos ter um entendimento claro através da leitura do Código de Decompilação: primeiro invocar o método de forwardingTargetForSelector durante o processo de encaminhamento de mensagens para obter o receptor de substituição, que também é chamado de fase de encaminhamento rápido. Se o forwardingTarget retornar nil ou retornar o mesmo receptor, o encaminhamento da mensagem se transformará em fase de encaminhamento regular. Basicamente, invocando o método methodSignatureForSelector para obter a assinatura do método e, em seguida, usá -lo com frameStackPointer para instanciar o objeto invocation . Em seguida, ligue para forwardInvocation: Método do receiver e passe o objeto invocation anterior como um argumento. No final, se o método methodSignatureForSelector não for implementado e o selector já estiver registrado no sistema de tempo de execução, doesNotRecognizeSelector: será invocado para apresentar um erro.
Examinando o ___forwarding___ para a direção do rastreamento da pilha de falhas, podemos notar que ele é chamado como o segundo caminho entre todo o caminho de encaminhamento de mensagens, o que significa que o objeto NSInvocation é chamado quando forwardInvocation é chamado.
Você também pode executar o comando passo a passo após o ponto de interrupção para observar o caminho de execução do código da montagem, o mesmo resultado deve ser observado.

E qual método é executado quando forwardInvocation é chamado? A partir do rastreamento da pilha, podemos ver um método chamado __ASPECTS_ARE_BEING_CALLED__ é executado. Veja esse método de todo o projeto, finalmente descobrimos que forwardInvocation é viciado pela estrutura 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 )];
} Como TCWebViewController é uma classe privada do Tencent SDK, é improvável que tenha sido viciado em outra classe diretamente. Mas é possível que sua superclasse esteja viciada que também pode afetar essa classe. Com essa conjectura, continuamos cavando. Finalmente, a resposta surgiu! Ao remover ou comentar o código que a enganche UIViewController , o aplicativo não travou quando o login via qq. Até agora, tínhamos certeza de que o acidente estava envolvido pela estrutura 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.