
中文
Le débogage a une assez mauvaise réputation. Je veux dire, si le développeur avait une compréhension complète du programme, il n'y aurait pas de bugs et qu'ils ne débogueraient pas en premier lieu, non?
Ne pense pas comme ça.
Il y aura toujours des bogues dans votre logiciel - ou n'importe quel logiciel, d'ailleurs. Aucune quantité de couverture de test imposée par votre chef de produit ne va résoudre ce problème. En fait, considérer le débogage comme un simple processus de réparation de quelque chose qui est cassé est en fait une façon de penser toxique qui entravera mentalement vos capacités analytiques.
Au lieu de cela, vous devez considérer le débogage comme un simple processus pour mieux comprendre un programme . C'est une différence subtile, mais si vous le croyez vraiment, toute corvée précédente de débogage disparaît simplement.
Depuis que Grace Hopper, le fondateur de la langue COBOL , a découvert le premier bug au monde dans un ordinateur de relais, la génération de bogue dans le développement de logiciels n'a jamais cessé. En tant que préface du livre de 《Advanced Apple Debogging & Renverse Engineering》 nous dit: les développeurs ne veulent pas penser que s'il y a une bonne compréhension du fonctionnement des logiciels, il n'y aura pas de bogue. Par conséquent, le débogage est presque une phase inévitable du cycle de vie du développement logiciel.
Si vous demandez à un programmeur inexpérimenté sur la façon de définir le débogage, il pourrait dire que "le débogage est quelque chose que vous faites pour trouver une solution pour votre problème logiciel". Il a raison, mais ce n'est qu'une petite partie d'un véritable débogage.
Voici les étapes d'un véritable débogage:
Parmi les étapes ci-dessus, l'étape la plus importante est la première étape: découvrez le problème. Apparemment, c'est une condition préalable d'autres étapes.
La recherche montre que les programmeurs expérimentés en temps passé à déboguer pour localiser le même ensemble de défauts représentent environ un vingtième des programmeurs inexpérimentés. Cela signifie que l'expérience de débogage fait une énorme différence dans l'efficacité de la programmation. Nous avons beaucoup de livres sur la conception de logiciels, malheureusement, les rares d'entre eux ont une introduction sur le débogage, même les cours à l'école.
À mesure que le débogueur s'améliorant au fil des ans, le style de codage des programmeurs est modifié à fond. Bien sûr, le débogueur ne peut pas remplacer la bonne pensée, la pensée ne peut pas remplacer l'excellent débogueur, la combinaison la plus parfaite est un excellent débogueur avec une bonne réflexion.
Le graphique suivant est les neuf règles de débogage décrites dans le livre <débogage: les 9 règles indispensables pour trouver même les problèmes logiciels et matériels les plus insaisissables>.

Bien qu'en tant que programmeur iOS, la plupart du temps dans le travail ne traitera pas le langage de l'assembly, mais comprendre que l'assemblage est toujours très utile, en particulier lors du débogage d'un cadre système ou d'un cadre tiers sans le code source.
Le langage AssEmbly est un langage de programmation orienté machine de bas niveau, qui peut être considéré comme une collection de mnémoniques pour les instructions de la machine pour divers processeurs. Les programmeurs peuvent utiliser le langage d'assemblage pour contrôler directement le système matériel informatique. Et le programme écrit en langage d'assemblage a de nombreux mérites, comme la vitesse d'exécution rapide et moins de mémoire occupée.
Jusqu'à présent, deux architectures principales sont largement utilisées sur la plate-forme Apple, x86 et ARM. Dans l'appareil mobile utilisant le langage d'assemblage du bras, ce qui est principalement dû au fait que le bras est une architecture de calcul d'instructions réduite (RISC), avec un avantage à faible consommation d'énergie. Alors que la plate-forme de bureau comme Mac OS, l'architecture x86 est utilisée. Les applications installées sur les simulateurs iOS s'exécutent en fait en tant qu'application Mac OS à l'intérieur du simulateur, ce qui signifie que le simulateur fonctionne comme un conteneur. Étant donné que notre cas a été débogué dans les simulateurs iOS, le principal objectif de recherche est le langage de l'assemblage x86 .
Le langage d'assemblage x86 évolue en deux branches de syntaxe: Intel (utilisé oriental dans la documentation de la plate-forme X86) et AT&T. Intel domine la famille MS-DOS et Windows, tandis que AT&T est courant dans la famille Unix. Il y a une énorme différence sur la syntaxe entre Intel et AT&T, comme la variable, constante, l'accès des registres, l'adressage indirect et le décalage. Bien que leur différence de syntaxe soit énorme, le système matériel est le même, ce qui signifie que l'un d'eux peut être migré vers l'autre de manière transparente. Étant donné que le langage d'assemblage AT&T est utilisé sur Xcode, nous nous concentrerons sur AT&T dans la partie ci-dessous.
Veuillez noter que la syntaxe Intel est utilisée sur les outils de démontage de Hopper Disassemble et IDA Pro.
Les vedettes sont les différences entre Intel et AT&T:
Le préfixe d'opérande: dans la syntaxe AT&T, % est utilisé comme préfixe du nom des registres et $ est utilisé comme préfixe de l'opérande immédiat, tandis qu'aucun préfixe n'est utilisé pour les registres et l'opérande immédiat dans Intel. L'autre différence est 0x est ajoutée comme le préfixe de l'hexadécimal dans AT&T. Le graphique ci-dessous démontre la différence entre leurs préfixes:
| AT&T | Intel |
|---|---|
| MOVQ% Rax,% RBX | MOV RBX, RAX |
| addq 0x10,% RSP | Ajouter RSP, 010H |
Dans la syntaxe Intel, le suffixe
hest utilisé pour l'opérande hexadécimal et le suffixebest utilisé pour l'opérande binaire.
Opérande: Dans la syntaxe AT&T, le premier opérande est l'opérande source, le deuxième opérande est l'opérande de destination. Cependant, dans la syntaxe Intel, l'ordre d'opérande est opposé. À partir de ce moment, la syntaxe d'AT&T est plus confortable pour nous selon notre habitude de lecture.
Mode d'adressage: en comparant avec la syntaxe Intel, le mode d'adressage indirect d'AT&T est difficile à lire. Cependant, l'algorithme du calcul d'adresse est le même: address = disp + base + index * scale . base représente l'adresse de base, disp signifie l'adresse de décalage, index * scale détermine l'emplacement d'un élément, scale est la taille d'un élément qui ne peut être qu'une puissance de deux. disp/base/index/scale est entièrement facultatif, la valeur par défaut de index est 0, tandis que la valeur par défaut de scale est 1. Voyons maintenant que l'instruction du calcul d'adresse: %segreg: disp(base,index,scale) est pour AT & T, et segreg: [base+index*scale+disp] est pour Intel. En fait, au-dessus de deux instructions appartiennent tous deux au mode d'adressage du segment. segreg signifie Registre des segments qui est généralement utilisé en mode réel lorsque la capacité du chiffre du CPU s'adressant au-delà du chiffre du registre. Par exemple, CPU peut aborder l'espace 20 bits, mais le registre n'a que 16 bits. Pour obtenir un espace à 20 chiffres, un autre mode d'adressage doit être utilisé: segreg:offset . Avec ce mode d'adressage, l'adresse de décalage sera segreg * 16 + offset , mais elle est plus compliquée que le mode mémoire plat. En mode Protect, l'adressage est sous l'espace d'adresse linéaire, ce qui signifie que l'adresse de base du segment peut être ignorée.
| 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 l'opérande immédiat se présente à la place de
dispouscale, le suffixe$peut être omis. Dans la syntaxe Intel,byte ptr,word ptr,dword ptretqword ptrdoivent être ajoutés avant l'opérande de mémoire.
Suffixe d'Opcode: dans la syntaxe AT&T, tous les opcodes ont un suffixe pour spécifier la taille. Il existe généralement quatre types de suffixes: b , w , l et q b représente un octet 8 bits, w signifie un mot 16 bits, l signifie 32 bits double mot. Le mot à 32 chiffres est également appelé le long mot qui est des jours 16 bits. q représente un quadword 64 bits. Le graphique ci-dessous illustre la syntaxe de l'instruction de transition des données (MOV) dans AT&T et Intel.
| AT&T | Intel |
|---|---|
| movb% al,% bl | MOV BL, AL |
| movw% hache,% bx | MOV BX, AX |
| movl% eax,% ebx | MOV EBX, EAX |
| MOVQ% Rax,% RBX | MOV RBX, RAX |
Comme nous le savons, la mémoire est utilisée pour stocker des instructions et des données pour le CPU. La mémoire est essentiellement un tableau d'octets. Bien que la vitesse d'accès à la mémoire soit très rapide, nous avons toujours besoin d'une unité de stockage plus petite et plus rapide pour accélérer l'exécution des instructions du CPU, qui est un enregistrement. Pendant l'exécution de l'instruction, toutes les données sont temporairement stockées dans les registres. C'est pourquoi le registre est nommé.
Lorsque les processeurs passent de 16 bits à 32 bits, 8 registres sont également étendus à 32 bits. Après cela, lorsque les registres étendus sont utilisés, le préfixe E est ajouté au nom du registre d'origine. Le processeur 32 bits est une architecture Intel 32 bits, qui est IA32. Aujourd'hui, les principaux processeurs sont l'architecture Intel 64 bits, qui est étendu de l'IA32 et a été appelé x86-64. Étant donné que l'IA32 est passé, cet article ne se concentrera que sur le x86-64. Notez que dans X86-64, le montant des registres est étendu de 8 à 16. Juste en raison de cette extension, l'état du programme peut être stocké dans des registres mais pas des piles. Ainsi, la fréquence d'accès à la mémoire est extrêmement réduite.
Dans X86-64, il y a 16 registres généraux 64 bits et 16 registres de pointeurs flottants. En outre, CPU a un registre de pointeur d'instructions 64 bits appelé rip . Il est conçu pour stocker l'adresse de la prochaine instruction exécutée. Il existe également d'autres registres qui ne sont pas largement utilisés, nous n'avons pas l'intention d'en parler dans cet article. Parmi les 16 registres généraux, huit d'entre eux proviennent de l'IA32: Rax 、 rcx 、 rdx 、 RBX 、 RSI 、 RDI 、 RSP et RBP. Les huit autres registres généraux sont nouveaux ajoutés depuis x86-64 qui sont R8 - R15. Les 16 registres flottants sont XMM0 - XMM15.
Les processeurs actuels sont de 8088, le registre est également étendu de 16 bits à 32 bits et enfin à 64 bits. Ainsi, le programme peut toujours accéder au faible 8 bits ou 16 bits ou 32 bits des registres.
Un graphique ci-dessous illustre les 16 registres généraux de x86-64:

L'utilisation de register read dans LLDB peut vider les données du registre de la trame de pile actuelle.
Par exemple, nous pouvons utiliser la commande ci-dessous pour afficher toutes les données du registre:
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
Comme nous le savons, il y a 16 registres de pointeurs flottants dans x86-64: xmm0 - xmm15. En fait, il y en a d'autres détails. Dans la sortie de register read -a Commande, vous remarquerez peut-être qu'il y a des registres STMM et YMM en plus du groupe de registres XMM. Ici, STMM est un alias de ST Register, et ST est un registre de FPU (Unité de point de flotteur) dans X86 pour gérer les données flottantes. Le FPU contient un registre de pointeur flottant qui a huit registres de pointeur flottant 80 bits: ST0 - ST7. Nous pouvons observer que le registre STMM est à 80 bits de la sortie, ce qui peut prouver que le registre STMM est le registre ST. XMM est un registre 128 bits et le registre YMM est de 256 bits, ce qui est une extension de XMM. En fait, le registre XMM est le bas 128 bits du registre YMM. Comme le registre EAX est le faible 32 bits du registre Rax. Dans Pentium III, Intel a publié un ensemble d'instructions appelé SSE (Streaming SIMD Extensions) qui est une extension de MMX. Huit nouveaux registres 128 bits (XMM0 - XMM7) sont ajoutés dans SSE. Le jeu d'instructions AVX (Advanced Vector Extensions) est une architecture d'extension de SSE. Également dans AVX, le registre 128 bits XMM a été étendu au registre 256 bits YMM.

Un appel de fonction comprend le passage des paramètres et le transfert de contrôle d'une unité de compilation à une autre. Dans la procédure d'appel de fonction, le passage des données, l'attribution et la libération de variables locales sont effectuées par pile. Et les piles attribuées à un seul appel de fonction sont appelées trame de pile.
La fonction d'appel de fonction d'OS X86-64 est la même avec la convention décrite dans l'article: System V Application Binary Interface AMD64 Architecture Processor Supplement. Par conséquent, vous pouvez vous y référer si cela vous intéresse.
Pendant le débogage LLDB, nous pouvons utiliser la commande bt pour imprimer la trace de pile du thread actuel, comme ci-dessous:
(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
En fait, la commande bt est réalisable sur le cadre de pile. Le cadre de pile conserve l'adresse de retour et la variable locale pour les fonctions qui peuvent être considérées comme un contexte d'une exécution de fonction. Comme nous le savons, le tas pousse vers le haut, tandis que la pile pousse vers le bas, qui est des adresses de mémoire à grand nombre aux petits chiffres. Une fois qu'une fonction est appelée, une trame de pile autonome est attribuée pour l'appel de fonction. Le registre RBP, appelé pointeur de trame, pointe toujours vers la fin du dernier cadre de pile alloué (adresse haute). Le registre RSP, appelé pointeur de pile, pointe toujours vers le haut de la dernière trame de pile allouée (adresse basse). Vous trouverez ci-dessous un graphique de pile de trame:

La Position de la colonne de gauche est une adresse mémoire qui utilise le mode d'adressage indirect. Content est la valeur de l'adresse dans les points Position . Selon la structure de la trame de pile dans le graphique ci-dessus, la procédure d'appel de fonction peut être décrite comme plusieurs étapes comme suit:
Les étapes 2 et 3 appartiennent en fait à l'instruction call . De plus, les étapes 4 et 5 peuvent être décrites dans l'instruction d'assemblage comme suit:
TestDemo`-[ViewController viewDidLoad]:
0x1054e09c0 <+0>: pushq %rbp //step 4
0x1054e09c1 <+1>: movq %rsp, %rbp //step 5
Il est facile de remarquer que ces deux étapes sont avec chaque appel de fonction. Il y a un autre détail du graphique ci-dessus: il y a une zone rouge en dessous du registre RSP, qui est appelée zone rouge par ABI. Il est réservé et ne doit pas être modifié par des gestionnaires de signaux ou d'interruption. Étant donné qu'il peut être modifié lors de l'appel de la fonction, donc, les fonctions de feuilles, ce qui signifie que les fonctions qui n'appellent jamais d'autres fonctions peuvent utiliser ce domaine pour des données temporaires.
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
Parmi les instructions ci-dessus, les instructions de 0x1064a63f5 à 0x1064a63fd appartiennent à l'étape 6. Il existe un type de registres appelés registres de préserve de fonction, ce qui signifie qu'ils appartiennent à la fonction d'appel, mais la fonction appelée est nécessaire pour préserver leurs valeurs. À partir des instructions d'assemblage ci-dessous, nous pouvons voir que RBX, RSP et R12 - R15 appartiennent tous à de tels registres.
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
L'instruction pour appeler une fonction est call , reportez-vous ci-dessous:
call function
function dans le paramètre est les procédures du segment de texte . L'instruction Call peut se diviser en deux étapes. La première étape consiste à pousser l'adresse d'instruction suivante de l'instruction call sur la pile. Ici, l'adresse suivante est en fait l'adresse de retour une fois la fonction appelée terminée. La deuxième étape consiste à function . L'instruction call équivaut à ci-dessous deux instructions:
push next_instruction
jmp function
Voici l'exemple de l'instruction call dans le simulateur iOS:
0x10915c714 <+68>: callq 0x1093ca502 ; symbol stub for: objc_msgSend
0x105206433 <+66>: callq *0xb3cd47(%rip) ; (void *)0x000000010475e800: objc_msgSend
Le code ci-dessus montre deux usages de l'instruction call . Dans la première utilisation, l'opérande est une adresse mémoire qui est en fait un stub symbole d'un fichier Mach-O. Il peut rechercher le symbole d'une fonction via le linker dynamique. Dans la deuxième utilisation, l'opérande est en fait obtenu par mode d'adressage indirect. De plus, dans la syntaxe AT&T, * doit être ajouté à l'opérande immédiat dans l'instruction de saut / appel (ou les sauts liés au compteur du programmeur) comme préfixe.
En général, l'instruction ret est utilisée pour renvoyer la procédure de la fonction appelée à la fonction d'appel. Cette instruction apparaît de l'adresse du haut de la pile et saute à cette adresse et continue de s'exécuter. Dans l'exemple ci-dessus, il revient à next_instruction . Avant que l'instruction ret ne soit exécutée, les registres appartiennent à la fonction d'appel seront affichés. Ceci est déjà mentionné à l'étape 6 de la procédure d'appel des fonctions.
La plupart des fonctions ont un paramètre qui peut être entier, flottant, pointeur, etc. En outre, les fonctions ont généralement une valeur de retour qui peut indiquer que le résultat d'exécution est réussi ou échoué. Dans OSX, au maximum, 6 paramètres peuvent être passés à travers des registres qui sont RDI, RSI, RDX, RCX, R8 et R9 dans l'ordre. Que diriez-vous d'une fonction avec plus de 6 paramètres? Bien sûr, cette circonstance existe. Si cela se produit, la pile peut être utilisée pour préserver les paramètres restants dans l'ordre inversé. OSX a huit registres à virgule flottante qui permettent de passer jusqu'à 8 paramètres de flotteur.
À propos de la valeur de retour d'une fonction, rax Register est utilisé pour enregistrer la valeur de retour entier. Si la valeur de retour est un flotteur, les registres XMM0 - XMM1 doivent être utilisés. Ci-dessous le graphique illustre clairement la convention d'utilisation du registre lors de l'appel de la fonction.

preserved across function calls indique si le registre doit être conservé sur l'appel de fonction. Nous pouvons voir qu'en plus des registres RBX, R12 - R15 mentionnés ci-dessus, les registres RSP et RBP appartiennent également aux registres sauvés de Callee. En effet, ces deux registres réservent les pointeurs d'emplacement importants qui pointent vers la pile du programme.
Ensuite, nous suivrons un vrai exemple pour démontrer les instructions d'un appel de fonction. Prenez le macro DDLogError dans CocoaLumberjack comme exemple. Lorsque cette macro est appelée, Class Method log:level:flag:context:file:function:line:tag:format: est appelé. Le code et les instructions suivantes concernent l'appel de DDLogError et les instructions d'assemblage correspondantes:
- (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
Étant donné que toutes les fonctions de Objective-C se transforment en invocation de la fonction objc_msgSend , donc log:level:flag:context:file:function:line:tag:format: Méthode Formez enfin les codes ci-dessous:
objc_msgSend(DDLog, @selector(log:level:flag:context:file:function:line:tag:format:), asynchronous, level, flag, context, file, function, line, tag, format, sender)
Nous avons déjà mentionné au plus 6 registres peuvent être utilisés pour le passage des paramètres. Les paramètres excédentaires peuvent utiliser la pile pour faire le passage. Étant donné que la fonction ci-dessus a plus de 6 paramètres, le passage des paramètres utiliserait à la fois les registres et la pile. Ci-dessous, deux tableaux décrivent l'utilisation des détails des registres et de la pile pour le passage du paramètre de l'invocation de la fonction DDLogError .
| Registre général | valeur | Paramètres | Instructions d'assemblage | Commentaire |
|---|---|---|---|---|
| RDI | Ddlog | soi | 0x102c568eb <+171>: MOVQ% R11,% RDI | |
| RSI | "Journal: Niveau: Flag: Context: Fichier: Fonction: Ligne: Tag: Format:" | faire un coup de pouce | 0x102c568f2 <+178>: MOVQ% R15,% RSI | |
| rdx | 0 | asynchrone | 0x102c568a3 <+99>: xorl% edx,% edx | Xorl est une opération exclusive ou. Ici, il est utilisé pour effacer le registre EDX |
| rcx | 18446744073709551615 | niveau | 0x102c568f9 <+185>: MOVQ% RBX,% RCX | (Ddloglevelall ou nsuiintegerax) |
| R8 | 1 | drapeau | 0x102c568aa <+106>: movl% eax,% r8d | Ddlogfagerror |
| R9 | 0 | contexte | 0x102c568af <+111>: movl% eax,% r9d |
| Décalage du cadre de pile | Valeur | Paramètres | Instructions d'assemblage | Commentaire |
|---|---|---|---|---|
| (% RSP) | "/Users/dev-aozhimin/desktop/testddlog/testddlog/viewController.m" | déposer | 0x102c56900 <+192>: MOVQ% R11, (% RSP) | |
| 0x8 (% RSP) | "- [ViewController Test:]" | fonction | 0x102c56908 <+200>: MOVQ% RBX, 0x8 (% RSP) | |
| 0x10 (% RSP) | 0x22 | doubler | 0x102c5690d <+205>: MOVQ $ 0x22, 0x10 (% RSP) | L'invocation correspondante de ddlogenror est la ligne 34 |
| 0x18 (% RSP) | 0x0 | étiqueter | 0x102c56916 <+214>: MOVQ $ 0x0, 0x18 (% RSP) | néant |
| 0x20 (% RSP) | "Testddlog:% @" | format | 0x102c5691f <+223>: MOVQ% R10, 0x20 (% RSP) | |
| 0x28 (% RSP) | expéditeur | Le premier paramètre des paramètres variables | 0x102c56924 <+228>: MOVQ% R14, 0x28 (% RSP) | Une instance d'Uibutton |
Si la valeur du registre est une chaîne, comme le paramètre
opdans le registrersi, la chaîne peut être imprimée directement dans la commande LLDB viapo (char *) $rsi. Sinon,po $rsipeut être utilisé pour imprimer une valeur au format entier.
Avec l'aide du langage d'assemblage, nous pouvons examiner des connaissances de bas niveau qui sont très nécessaires lors du débogage. J'essaie très fort d'introduire les connaissances liées à l'assemblage aussi détaillées que possible. Cependant, la hiérarchie des connaissances de l'assemblage est trop énorme à décrire dans un article. Veuillez vous référer aux références mentionnées ci-dessus. De plus, le troisième chapitre de CSApp - La représentation au niveau de la machine d'un programme est également fortement recommandé. C'est un bon matériau rare pour référence.
Cet article illustre la procédure de débogage à travers un vrai cas. Certains détails sont modifiés pour protéger la confidentialité personnelle.
Le problème dont nous allons parler était de se produire lorsque je développais un SDK de connexion. Un utilisateur a affirmé que l'application s'est écrasée lorsqu'il a appuyé sur le bouton "QQ" dans la page de connexion. Alors que nous avons débogué ce problème, nous avons constaté que le crash s'était produit si l'application QQ n'était pas installée en même temps. Lorsque l'utilisateur appuie sur le bouton QQ pour nécessiter une connexion, le SDK de connexion QQ essaie de lancer une page Web d'autorisation dans notre application. Dans ce cas, une erreur de sélecteur non reconnue [TCWebViewController setRequestURLStr:] se produit.
PS: Pour se concentrer sur la question, les informations inutiles de débogage des entreprises ne sont pas répertoriées ci-dessous. Pendant ce temps, Aadebug est utilisé comme nom d'application.
Voici la trace de pile de ce crash:
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
Avant de parler du débogage, familiarisons le transfert du message dans Objective-C. Comme nous le savons, Objective-C utilise une structure de messagerie plutôt que des appels de fonction. La principale différence est que dans la structure de messagerie, le temps d'exécution décide quelle fonction sera exécutée et non le temps de compilation. Cela signifie que si un message non reconnu est envoyé à un objet, rien ne se passera pendant la compilation. Et pendant l'exécution, lorsqu'il reçoit une méthode qu'il ne comprend pas, un objet passe par le transfert de messages, un processus conçu pour vous permettre en tant que développeur de dire au message comment gérer le message inconnu.
Au-dessous de quatre méthodes sont généralement impliquées lors du transfert de messages:
+ (BOOL)resolveInstanceMethod:(SEL)sel : Cette méthode est appelée lorsqu'un message inconnu est transmis à un objet. Cette méthode prend le sélecteur qui n'a pas été trouvé et renvoie une valeur booléenne pour indiquer si une méthode d'instance a été ajoutée à la classe qui peut désormais gérer ce sélecteur. Si la classe peut gérer ce sélecteur, retournez oui, le processus de transfert de message est terminé. Cette méthode est souvent utilisée pour accéder aux propriétés @Dynamic de NSmanagedObjects dans CoreData de manière dynamique. + (BOOL)resolveClassMethod:(SEL)sel La méthode est similaire avec la méthode ci-dessus, la seule différence est cette méthode d'une classe, l'autre méthode d'instance.
- (id)forwardingTargetForSelector:(SEL)aSelector : Cette méthode fournit un deuxième récepteur pour gérer un message inconnu, et c'est plus rapide que forwardInvocation: . Cette méthode peut être utilisée pour imiter certaines caractéristiques de l'héritage multiple. Notez qu'il n'y a aucun moyen de manipuler le message en utilisant cette partie du chemin de transfert. Si le message doit être modifié avant d'envoyer au récepteur de remplacement, le mécanisme de transfert complet doit être utilisé.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector : Si l'algorithme de transfert est venu aussi loin, le mécanisme de transfert complet est démarré. NSMethodSignature est renvoyé par cette méthode qui inclut la description de la méthode dans le paramètre Aselector. Notez que cette méthode doit être remplacée si vous souhaitez créer un objet NSInvocation qui contient le sélecteur, la cible et les arguments lors du transfert de message.
- (void)forwardInvocation:(NSInvocation *)anInvocation : la mise en œuvre de cette méthode doit contenir des pièces ci-dessous: découvrez l'objet qui peut gérer le message Aninvocation; Envoi du message à cet objet, l'anInvocation enregistre la valeur de retour, l'exécution envoie puis envoie la valeur de retour à l'expéditeur de message d'origine. En fait, cette méthode peut avoir le même comportement avec forwardingTargetForSelector: méthode en modifiant simplement la cible d'invocation et en l'appelant par la suite, mais nous le faisons à peine.
Habituellement, les deux premières méthodes utilisées pour le transfert de messages sont appelées transfert rapidement , car il fournit un moyen beaucoup plus rapide de faire le transfert de messages. Pour distinguer le transfert rapide, les méthodes 3 et 4 sont appelées transfert normal ou transfert régulier . C'est beaucoup plus lent car il doit créer un objet NSinvocation pour terminer le transfert de message.
Remarque: Si la méthode
methodSignatureForSelectorn'est pas remplacée ou que leNSMethodSignaturerenvoyé est nulle,forwardInvocationne sera pas appelé et le transfert de message est terminé avec l'erreurdoesNotRecognizeSelectoraugmentée. Nous pouvons le voir à partir du code source de la fonction__forwarding__ci-dessous.
Le processus de transfert de messages peut être décrit par un diagramme de flux, voir ci-dessous.

Comme décrit dans le diagramme de flux, à chaque étape, le récepteur a la possibilité de gérer le message. Chaque étape est plus chère que celle avant elle. La meilleure pratique consiste à gérer le processus de transfert des messages le plus tôt possible. Si le message n'est pas traité tout au long du processus, l'erreur doesNotRecognizeSeletor est soulevée pour indiquer que le sélecteur ne peut pas être reconnu par l'objet.
Il est temps de terminer la partie théorique et de revenir au problème.
Selon les informations TCWebViewController de la pile de trace, nous l'associons naturellement au SDK Tencent TencentOpenapi.Framework , mais nous n'avons pas récemment mis à jour le SDK Tencent, ce qui signifie que l'accident n'a pas été causé par TencentOpenapi.Framework .
Tout d'abord, nous avons décompilé le code et obtenu la structure de la 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:
}
D'après le résultat de l'analyse statique, il n'y avait pas de méthode de setter et de Getter de requestURLStr dans TCWebViewController . Parce qu'il n'y avait pas un tel crash dans la version de l'application précédente, nous avons sorti une idée: la propriété dans TCWebViewController a-t-elle été implémentée de manière dynamique qui utilise @dynamic pour dire que le compilateur ne génère pas Getter et Setter pour la propriété pendant la compilation mais créé dynamiquement dans le cadre d'exécution comme Core Framework? Ensuite, nous avons décidé d'aller profondément à l'idée de voir si notre supposition était correcte. Au cours de notre suivi, nous avons constaté qu'il y avait une catégorie NSObject(MethodSwizzlingCategory) pour NSObject dans TencentOpenapi.framework qui était très suspecte. Dans cette catégorie, il y avait une méthode switchMethodForCodeZipper dont la mise en œuvre a remplacé les méthodes methodSignatureForSelector et forwardInvocation de QQmethodSignatureForSelector et 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 ;
} Ensuite, nous avons continué à suivre la méthode QQmethodSignatureForSelector , et il y avait une méthode nommée _AddDynamicPropertysSetterAndGetter . À partir du nom, nous pouvons facilement obtenir que cette méthode consiste à ajouter dynamiquement la méthode de setter et de Getter pour les propriétés. Cela peut vérifier considérablement notre supposition d'origine est correcte.
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;
} Mais pourquoi le secteur ne peut pas être reconnu dans la classe TCWebViewController ? Est-ce parce que la méthode QQMethodSignatureForSelector a été couverte lors de notre développement de cette version? Cependant, nous n'avons pas trouvé de moindre indice, même nous avons traversé partout dans le code. C'était très décevant. Jusqu'à présent, l'analyse statique est effectuée. La prochaine étape consiste à utiliser LLDB pour déboguer dynamiquement le SDK Tencent pour savoir quel chemin a brisé la création de Getter et du secteur dans le processus de transfert de messages.
Si nous essayons de définir le point d'arrêt sur
setRequestURLStrvia la commande LLDB, nous constaterons que nous ne pouvons pas le faire. La raison en est que le secteur n'est pas disponible pendant la compilation du temps. Cela peut également vérifier notre supposition d'origine.
Selon la trace de pile de crash, nous pouvons conclure que setRequestURLStr est appelé la méthode -[TCWebViewKit open] , ce qui signifie que le crash se produit lors de la vérification du SDK de Tencent si l'application QQ est installée et ouvrant les progrès de la page Web d'authentification.
Ensuite, nous utilisons la commande LLDB pour définir le point d'arrêt sur cette méthode:
br s -n "-[TCWebViewKit open]"
br sest l'abréviation pourbreakpoint set,-nreprésente le point d'arrêt en fonction du nom de la méthode après, qui a le même comportement avec un point d'arrêt symbolique,br s -Fpeut également définir le point d'arrêt.b -[TCWebViewKit open]fonctionne également ici, maisbvoici l'abréviation de_regexp-break, qui utilise une expression régulière pour définir le point d'arrêt. En fin de compte, nous pouvons également définir un point d'arrêt sur l'adresse mémoire commebr s -a 0x000000010940b24e, ce qui peut aider à déboguer le bloc si l'adresse du bloc est disponible.
À présent, le point d'arrêt est défini avec succès.
Breakpoint 34: where = AADebug`-[TCWebViewKit open], address = 0x0000000103157f7d
Lorsque l'application va lancer la page d'authentification Web, le projet est arrêté sur ce point d'arrêt. Reportez-vous ci-dessous:

Cette capture d'écran est capturée lorsque l'application fonctionne sur Simulator, de sorte que le code d'assemblage est basé sur x64. Si vous utilisez le périphérique iPhone, le code d'assemblage doit être ARM. Mais la méthode d'analyse est la même pour eux, veuillez le remarquer.
Définissez un point d'arrêt sur la ligne 96, ce code d'assemblage est l'invocation de la méthode setRequestURLStr , puis imprime le contenu du registre rbx , alors nous pouvons observer que l'instance TCWebViewController est enregistrée dans ce registre.

Ensuite, nous pouvons utiliser LLDB pour définir le point d'arrêt pour QQmethodSignatureForSelector Méthode:
br s -n "-[NSObject QQmethodSignatureForSelector:]"
Entrez c dans LLDB pour permettre au point d'arrêt de continuer, puis le point d'arrêt s'arrêtera à l'intérieur de la méthode QQmethodSignatureForSelector , qui peut prouver notre supposition précédente sur la méthode QQmethodSignatureForSelector en conflit avec notre code n'est pas valide.

Définissez un point d'arrêt à la fin de la méthode QQmethodSignatureForSelector , c'est-à-dire la commande retq sur la ligne 31. Ensuite, imprimez l'adresse mémoire du registre rax , reportez-vous à la capture d'écran ci-dessous:

En imprimant l'adresse mémoire 0x00007fdb36d38df0 de Register rax , l'objet NSMethodSignature est renvoyé. Selon la convention de conception sur le langage d'assemblage x86, la valeur de retour est enregistrée dans le registre rax . Apparemment, la méthode QQmethodSignatureForSelector est invoquée et renvoie la valeur correcte, ce qui signifie que nous devons continuer à suivre le problème.
Définissez le point d'arrêt sur QQforwardInvocation via LLDB:
br s -n "-[NSObject QQforwardInvocation:]"
Une fois le point d'arrêt défini, continuez l'exécution du programme, l'application est écrasée. Et la méthode QQforwardInvocation n'a pas encore été appelée. Avec cela, nous pouvons conclure que la méthode QQforwardInvocation est en conflit par notre code.

___forwarding___ Fonction contient toute la mise en œuvre du mécanisme de transfert de messages, le code de décompilation est sélectionné parmi l'objectif-c 消息发送与转发机制原理. Dans cet article, il y a un jugement qui devrait être incorrect entre forwarding et receiver lors de l'appel de la méthode de forwardingTargetForSelector . Ici, ce devrait être un jugement entre forwardingTarget et receiver . Reportez-vous au code ci-dessous:
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);
}
Fondamentalement, nous pouvons avoir une compréhension claire en lisant le code de décompilation: invoquez d'abord la méthode de forwardingTargetForSelector pendant le processus de transfert du message pour obtenir le récepteur de remplacement, qui est également appelé phase de transfert rapide. Si le forwardingTarget renvoie NIL ou renvoie le même récepteur, le transfert de message se transforme en phase de transfert régulière. Fondamentalement, invoquer la méthode methodSignatureForSelector pour obtenir la signature de la méthode, puis en l'utilisant avec frameStackPointer pour instancier un objet invocation . Ensuite, appelez forwardInvocation: Méthode du receiver et transmettez l'objet invocation précédent comme argument. En fin de compte, la méthode methodSignatureForSelector n'est pas mise en œuvre et le selector est déjà enregistré dans le système d'exécution, doesNotRecognizeSelector: il sera invoqué pour lancer une erreur.
En examinant le ___forwarding___ à partir de la trace de pile de crash, nous pouvons remarquer qu'il est appelé le deuxième chemin parmi l'ensemble du chemin de transfert de message, ce qui signifie que l'objet NSInvocation est invoqué lorsque forwardInvocation est appelée.
Vous pouvez également exécuter la commande étape par étape après le point d'arrêt pour observer le chemin d'exécution du code d'assemblage, le même résultat doit être observé.

Et quelle méthode est exécutée lorsque forwardInvocation est appelée? Dans la trace de pile, nous pouvons voir une méthode nommée __ASPECTS_ARE_BEING_CALLED__ est exécutée. Regardez cette méthode de l'ensemble du projet, nous découvrons enfin que forwardInvocation est accroché par 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 )];
} Étant donné que TCWebViewController est une classe privée de Tencent SDK, il est peu probable qu'il ait été accroché directement par une autre classe. Mais il est possible que sa superclasse est accrochée, ce qui peut également affecter cette classe. Avec cette conjecture, nous avons continué à creuser. Finalement, la réponse a fait surface! En supprimant ou en commentant le code qui accroche UIViewController , l'application ne s'est pas écrasée lors de la connexion via QQ. Jusqu'à présent, nous étions certainement sûrs que l'accident était impliqué par 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.