Re4b FR
Re4b FR
_____ _ _
| ____|_ __ __ _(_)_ __ ___ ___ _ __(_)_ __ __ _
| _| | '_ \ / _` | | '_ \ / _ \/ _ \ '__| | '_ \ / _` |
| |___| | | | (_| | | | | | __/ __/ | | | | | | (_| |
|_____|_| |_|\__, |_|_| |_|\___|\___|_| |_|_| |_|\__, |
|___/ |___/
__
/ _| ___ _ __
| |_ / _ \| '__|
| _| (_) | |
|_| \___/|_|
____ _
| __ ) ___ __ _(_)_ __ _ __ ___ _ __ ___
| _ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__/ __|
| |_) | __/ (_| | | | | | | | | __/ | \__ \
|____/ \___|\__, |_|_| |_|_| |_|\___|_| |___/
|___/
Rétro-ingénierie pour Débutants
(Comprendre le langage d’assemblage)
Dennis Yurichev
<first_name @ last_name . com>
cba
©2013-2020, Dennis Yurichev.
Ce travail est sous licence Creative Commons Attribution-ShareAlike 4.0
International (CC BY-SA 4.0). Pour voir une copie de cette licence, rendez vous sur
https://creativecommons.org/licenses/by-sa/4.0/.
Version du texte (9 septembre 2020).
La dernière version (et l’édition en russe) de ce texte est accessible sur
https://beginners.re/.
A la recherche de traducteurs !
i
Contenus abrégés
1 Pattern de code 1
4 Java 870
7 Outils 1038
13 Communautés 1329
ii
Épilogue 1331
Appendice 1333
Glossaire 1378
Index 1381
1 Pattern de code 1
1.1 La méthode . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2 Quelques bases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.2.1 Une courte introduction sur le CPU . . . . . . . . . . . . . . . . . . . . 2
1.2.2 Systèmes de numération . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.3 Conversion d’une base à une autre . . . . . . . . . . . . . . . . . . . . 4
1.3 Fonction vide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.3 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3.4 Fonctions vides en pratique . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.4 Valeur de retour . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.4.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.4.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4.3 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4.4 En pratique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.5 Hello, world! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.5.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.5.2 x86-64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
1.5.3 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.5.4 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
1.5.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
1.5.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
1.6 Fonction prologue et épilogue . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
1.6.1 Récursivité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
1.7 Une fonction vide: redux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
iii
1.8 Renvoyer des valeurs: redux . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
1.9 Pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
1.9.1 Pourquoi la pile grandit en descendant ? . . . . . . . . . . . . . . . . 43
1.9.2 Quel est le rôle de la pile ? . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.9.3 Une disposition typique de la pile . . . . . . . . . . . . . . . . . . . . 52
1.9.4 Bruit dans la pile . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
1.9.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.10 Fonction presque vide . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.11 printf() avec plusieurs arguments . . . . . . . . . . . . . . . . . . . . . . . . 58
1.11.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
1.11.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
1.11.3 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
1.11.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
1.11.5 À propos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
1.12 scanf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
1.12.1 Exemple simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
1.12.2 Erreur courante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
1.12.3 Variables globales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
1.12.4 scanf() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
1.12.5 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
1.13 Intéressant à noter: variables globales vs. locales . . . . . . . . . . . . . . 131
1.14 Accéder aux arguments passés . . . . . . . . . . . . . . . . . . . . . . . . . . 131
1.14.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
1.14.2 x64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
1.14.3 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
1.14.4 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
1.15 Plus loin sur le renvoi des résultats . . . . . . . . . . . . . . . . . . . . . . . 144
1.15.1 Tentative d’utilisation du résultat d’une fonction renvoyant void 144
1.15.2 Que se passe-t-il si on n’utilise pas le résultat de la fonction? . . 146
1.15.3 Renvoyer une structure . . . . . . . . . . . . . . . . . . . . . . . . . . 146
1.16 Pointeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
1.16.1 Renvoyer des valeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
1.16.2 Échanger les valeurs en entrée . . . . . . . . . . . . . . . . . . . . . 158
1.17 Opérateur GOTO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
1.17.1 Code mort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
1.17.2 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
1.18 Sauts conditionnels . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
1.18.1 Exemple simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
1.18.2 Calcul de valeur absolue . . . . . . . . . . . . . . . . . . . . . . . . . . 185
1.18.3 Opérateur conditionnel ternaire . . . . . . . . . . . . . . . . . . . . . 187
1.18.4 Trouver les valeurs minimale et maximale . . . . . . . . . . . . . . 191
1.18.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 197
1.18.6 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
1.19 Déplombage de logiciel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199
1.20 Blague de l’arrêt impossible (Windows 7) . . . . . . . . . . . . . . . . . . . 202
1.21 switch()/case/default . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
1.21.1 Petit nombre de cas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
1.21.2 De nombreux cas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
1.21.3 Lorsqu’il y a quelques déclarations case dans un bloc . . . . . . . 233
iv
1.21.4 Fall-through . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
1.21.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
1.22 Boucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
1.22.1 Exemple simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
1.22.2 Routine de copie de blocs de mémoire . . . . . . . . . . . . . . . . . 255
1.22.3 Vérification de condition . . . . . . . . . . . . . . . . . . . . . . . . . . 258
1.22.4 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 259
1.22.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
1.23 Plus d’information sur les chaînes . . . . . . . . . . . . . . . . . . . . . . . . 262
1.23.1 strlen() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262
1.23.2 Limites de chaînes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
1.24 Remplacement de certaines instructions arithmétiques par d’autres . . 276
1.24.1 Multiplication . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 277
1.24.2 Division . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283
1.24.3 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
1.25 Unité à virgule flottante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
1.25.1 IEEE 754 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
1.25.2 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 284
1.25.3 ARM, MIPS, x86/x64 SIMD . . . . . . . . . . . . . . . . . . . . . . . . . 285
1.25.4 C/C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
1.25.5 Exemple simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 285
1.25.6 Passage de nombres en virgule flottante par les arguments . . . 297
1.25.7 Exemple de comparaison . . . . . . . . . . . . . . . . . . . . . . . . . 300
1.25.8 Quelques constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
1.25.9 Copie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
1.25.10 Pile, calculateurs et notation polonaise inverse . . . . . . . . . . 339
1.25.11 80 bits? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
1.25.12 x64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
1.25.13 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
1.26 Tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
1.26.1 Exemple simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
1.26.2 Débordement de tampon . . . . . . . . . . . . . . . . . . . . . . . . . 349
1.26.3 Méthodes de protection contre les débordements de tampon . . 357
1.26.4 Encore un mot sur les tableaux . . . . . . . . . . . . . . . . . . . . . 362
1.26.5 Tableau de pointeurs sur des chaînes . . . . . . . . . . . . . . . . . . 363
1.26.6 Tableaux multidimensionnels . . . . . . . . . . . . . . . . . . . . . . . 373
1.26.7 Ensemble de chaînes comme un tableau à deux dimensions . . 383
1.26.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
1.26.9 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
1.27 Exemple: un bogue dans Angband . . . . . . . . . . . . . . . . . . . . . . . . 388
1.28 Manipulation de bits spécifiques . . . . . . . . . . . . . . . . . . . . . . . . . 391
1.28.1 Test d’un bit spécifique . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
1.28.2 Mettre (à 1) et effacer (à 0) des bits spécifiques . . . . . . . . . . . 396
1.28.3 Décalages . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
1.28.4 Mettre et effacer des bits spécifiques: exemple avec le FPU1 . . 406
1.28.5 Compter les bits mis à 1 . . . . . . . . . . . . . . . . . . . . . . . . . . 412
1.28.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 431
1.28.7 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 434
1. Floating-Point Unit
v
1.29 Générateur congruentiel linéaire . . . . . . . . . . . . . . . . . . . . . . . . . 435
1.29.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 436
1.29.2 x64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
1.29.3 ARM 32-bit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 437
1.29.4 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 438
1.29.5 Version thread-safe de l’exemple . . . . . . . . . . . . . . . . . . . . 441
1.30 Structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 441
1.30.1 MSVC: exemple SYSTEMTIME . . . . . . . . . . . . . . . . . . . . . . . 442
1.30.2 Allouons de l’espace pour une structure avec malloc() . . . . . . 446
1.30.3 UNIX: struct tm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 449
1.30.4 Organisation des champs dans la structure . . . . . . . . . . . . . . 462
1.30.5 Structures imbriquées . . . . . . . . . . . . . . . . . . . . . . . . . . . 471
1.30.6 Champs de bits dans une structure . . . . . . . . . . . . . . . . . . . 474
1.30.7 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 483
1.31 Le bogue struct classique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 483
1.32 Unions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 485
1.32.1 Exemple de générateur de nombres pseudo-aléatoires . . . . . . 485
1.32.2 Calcul de l’epsilon de la machine . . . . . . . . . . . . . . . . . . . . 489
1.32.3 Remplacement de FSCALE . . . . . . . . . . . . . . . . . . . . . . . . . 492
1.32.4 Calcul rapide de racine carré . . . . . . . . . . . . . . . . . . . . . . . 493
1.33 Pointeurs sur des fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 494
1.33.1 MSVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 495
1.33.2 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
1.33.3 Danger des pointeurs sur des fonctions . . . . . . . . . . . . . . . . 508
1.34 Valeurs 64-bit dans un environnement 32-bit . . . . . . . . . . . . . . . . . 508
1.34.1 Renvoyer une valeur 64-bit . . . . . . . . . . . . . . . . . . . . . . . . 508
1.34.2 Passage d’arguments, addition, soustraction . . . . . . . . . . . . . 509
1.34.3 Multiplication, division . . . . . . . . . . . . . . . . . . . . . . . . . . . 513
1.34.4 Décalage à droite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 518
1.34.5 Convertir une valeur 32-bit en 64-bit . . . . . . . . . . . . . . . . . . 520
1.35 Cas d’une structure LARGE_INTEGER . . . . . . . . . . . . . . . . . . . . . . 521
1.36 SIMD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524
1.36.1 Vectorisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 525
1.36.2 Implémentation SIMD de strlen() . . . . . . . . . . . . . . . . . . . 538
1.37 64 bits . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543
1.37.1 x86-64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 543
1.37.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 552
1.37.3 Nombres flottants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 552
1.37.4 Critiques concernant l’architecture 64 bits . . . . . . . . . . . . . . 552
1.38 Travailler avec des nombres à virgule flottante en utilisant SIMD . . . . 553
1.38.1 Simple exemple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 553
1.38.2 Passer des nombres à virgule flottante via les arguments . . . . 561
1.38.3 Exemple de comparaison . . . . . . . . . . . . . . . . . . . . . . . . . 562
1.38.4 Calcul de l’epsilon de la machine: x64 et SIMD . . . . . . . . . . . 565
1.38.5 Exemple de générateur de nombre pseudo-aléatoire revisité . . 566
1.38.6 Résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 567
1.39 Détails spécifiques à ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 567
1.39.1 Signe (#) avant un nombre . . . . . . . . . . . . . . . . . . . . . . . . 567
1.39.2 Modes d’adressage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 567
vi
1.39.3 Charger une constante dans un registre . . . . . . . . . . . . . . . . 569
1.39.4 Relogement en ARM64 . . . . . . . . . . . . . . . . . . . . . . . . . . . 571
1.40 Détails spécifiques MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 573
1.40.1 Charger une constante 32-bit dans un registre . . . . . . . . . . . 573
1.40.2 Autres lectures sur les MIPS . . . . . . . . . . . . . . . . . . . . . . . . 575
vii
2.11 Fonctions de hachage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 605
2.11.1 Comment fonctionnent les fonctions à sens unique? . . . . . . . . 605
viii
3.17.3 Cas Pin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 687
3.17.4 Exploitation de chaîne de format . . . . . . . . . . . . . . . . . . . . 688
3.18 Ajustement de chaînes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 690
3.18.1 x64: MSVC 2013 avec optimisation . . . . . . . . . . . . . . . . . . . 691
3.18.2 x64: GCC 4.9.1 sans optimisation . . . . . . . . . . . . . . . . . . . . 693
3.18.3 x64: GCC 4.9.1 avec optimisation . . . . . . . . . . . . . . . . . . . . 694
3.18.4 ARM64: GCC (Linaro) 4.9 sans optimisation . . . . . . . . . . . . . . 696
3.18.5 ARM64: GCC (Linaro) 4.9 avec optimisation . . . . . . . . . . . . . . 697
3.18.6 ARM: avec optimisation Keil 6/2013 (Mode ARM) . . . . . . . . . . 698
3.18.7 ARM: avec optimisation Keil 6/2013 (Mode Thumb) . . . . . . . . . 699
3.18.8 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 700
3.19 Fonction toupper() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 701
3.19.1 x64 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 702
3.19.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 704
3.19.3 Utilisation d’opérations sur les bits . . . . . . . . . . . . . . . . . . . 706
3.19.4 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 707
3.20 Obfuscation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 707
3.20.1 Chaînes de texte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 707
3.20.2 Code exécutable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 708
3.20.3 Machine virtuelle / pseudo-code . . . . . . . . . . . . . . . . . . . . . 713
3.20.4 Autres choses à mentionner . . . . . . . . . . . . . . . . . . . . . . . 713
3.20.5 Exercice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 713
3.21 C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 713
3.21.1 Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 713
3.21.2 ostream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 736
3.21.3 Références . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 737
3.21.4 STL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 738
3.21.5 Mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 784
3.22 Index de tableau négatifs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 785
3.22.1 Accéder à une chaîne depuis la fin . . . . . . . . . . . . . . . . . . . 785
3.22.2 Accéder à un bloc quelconque depuis la fin . . . . . . . . . . . . . . 785
3.22.3 Tableaux commençants à 1 . . . . . . . . . . . . . . . . . . . . . . . . 786
3.23 Plus loin avec les pointeurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . 789
3.23.1 Travailler avec des adresses au lieu de pointeurs . . . . . . . . . . 789
3.23.2 Passer des valeurs en tant que pointeurs; tagged unions . . . . . 792
3.23.3 Abus de pointeurs dans le noyau Windows . . . . . . . . . . . . . . 794
3.23.4 Pointeurs nuls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 800
3.23.5 Tableaux comme argument de fonction . . . . . . . . . . . . . . . . 806
3.23.6 Pointeur sur une fonction . . . . . . . . . . . . . . . . . . . . . . . . . 807
3.23.7 Pointeur sur une fonction: protection contre la copie . . . . . . . . 808
3.23.8 Pointeur sur une fonction: un bogue courant (ou une typo) . . . . 809
3.23.9 Pointeur comme un identificateur d’objet . . . . . . . . . . . . . . . 809
3.23.10 Oracle RDBMS et un simple ramasse miette pour C/C++ . . . . 811
3.24 Optimisations de boucle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 812
3.24.1 Optimisation étrange de boucle . . . . . . . . . . . . . . . . . . . . . 812
3.24.2 Autre optimisation de boucle . . . . . . . . . . . . . . . . . . . . . . . 814
3.25 Plus sur les structures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 817
3.25.1 Parfois une structure C peut être utilisée au lieu d’un tableau . . 817
3.25.2 Tableau non dimensionné dans une structure C . . . . . . . . . . . 818
ix
3.25.3 Version de structure C . . . . . . . . . . . . . . . . . . . . . . . . . . . 820
3.25.4 Fichier des meilleurs scores dans le jeu «Block out » et sérialisa-
tion basique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 822
3.26 memmove() et memcpy() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 828
3.26.1 Stratagème anti-debugging . . . . . . . . . . . . . . . . . . . . . . . . 829
3.27 setjmp/longjmp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 830
3.28 Autres hacks bizarres de la pile . . . . . . . . . . . . . . . . . . . . . . . . . . 833
3.28.1 Accéder aux arguments/variables locales de l’appelant . . . . . . 833
3.28.2 Renvoyer une chaîne . . . . . . . . . . . . . . . . . . . . . . . . . . . . 835
3.29 OpenMP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 837
3.29.1 MSVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 840
3.29.2 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 843
3.30 Division signée en utilisant des décalages . . . . . . . . . . . . . . . . . . . 845
3.31 Un autre heisenbug . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 847
3.32 Le cas du return oublié . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 849
3.33 Exercice: un peu plus loin avec les pointeur et les unions . . . . . . . . . 854
3.34 Windows 16-bit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 855
3.34.1 Exemple#1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 855
3.34.2 Exemple #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 856
3.34.3 Exemple #3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 857
3.34.4 Exemple #4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 858
3.34.5 Exemple #5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 861
3.34.6 Exemple #6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 866
4 Java 870
4.1 Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 870
4.1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 870
4.1.2 Renvoyer une valeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 871
4.1.3 Fonctions de calculs simples . . . . . . . . . . . . . . . . . . . . . . . . 877
4.1.4 Modèle de mémoire de la JVM3 . . . . . . . . . . . . . . . . . . . . . . . 880
4.1.5 Appel de fonction simple . . . . . . . . . . . . . . . . . . . . . . . . . . 880
4.1.6 Appel de beep() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 883
4.1.7 Congruentiel linéaire PRNG4 . . . . . . . . . . . . . . . . . . . . . . . . 883
4.1.8 Conditional jumps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 885
4.1.9 Passer des paramètres . . . . . . . . . . . . . . . . . . . . . . . . . . . . 888
4.1.10 Champs de bit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 889
4.1.11 Boucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 891
4.1.12 switch() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 893
4.1.13 Tableaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 894
4.1.14 Chaînes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 906
4.1.15 Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 908
4.1.16 Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 912
4.1.17 Correction simple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 915
4.1.18 Résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 915
x
5.1.1 Microsoft Visual C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 917
5.1.2 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 918
5.1.3 Intel Fortran . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 918
5.1.4 Watcom, OpenWatcom . . . . . . . . . . . . . . . . . . . . . . . . . . . . 918
5.1.5 Borland . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 918
5.1.6 Autres DLLs connues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 920
5.2 Communication avec le monde extérieur (niveau fonction) . . . . . . . . . 920
5.3 Communication avec le monde extérieur (win32) . . . . . . . . . . . . . . . 921
5.3.1 Fonctions souvent utilisées dans l’API Windows . . . . . . . . . . . . 921
5.3.2 Étendre la période d’essai . . . . . . . . . . . . . . . . . . . . . . . . . . 922
5.3.3 Supprimer la boite de dialogue nag . . . . . . . . . . . . . . . . . . . . 922
5.3.4 tracer: Intercepter toutes les fonctions dans un module spécifique 922
5.4 Chaînes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 923
5.4.1 Chaînes de texte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 923
5.4.2 Trouver des chaînes dans un binaire . . . . . . . . . . . . . . . . . . . 930
5.4.3 Messages d’erreur/de débogage . . . . . . . . . . . . . . . . . . . . . . 931
5.4.4 Chaînes magiques suspectes . . . . . . . . . . . . . . . . . . . . . . . . 932
5.5 Appels à assert() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 933
5.6 Constantes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 933
5.6.1 Nombres magiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 934
5.6.2 Constantes spécifiques . . . . . . . . . . . . . . . . . . . . . . . . . . . . 936
5.6.3 Chercher des constantes . . . . . . . . . . . . . . . . . . . . . . . . . . 937
5.7 Trouver les bonnes instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . 937
5.8 Patterns de code suspect . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 939
5.8.1 instructions XOR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 939
5.8.2 Code assembleur écrit à la main . . . . . . . . . . . . . . . . . . . . . 940
5.9 Utilisation de nombres magiques lors du tracing . . . . . . . . . . . . . . . 941
5.10 Boucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 942
5.10.1 Quelques schémas de fichier binaire . . . . . . . . . . . . . . . . . . 943
5.10.2 Comparer des «snapshots » mémoire . . . . . . . . . . . . . . . . . 951
5.11 Détection de l’ISA5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 953
5.11.1 Code mal désassemblé . . . . . . . . . . . . . . . . . . . . . . . . . . . 953
5.11.2 Code désassemblé correctement . . . . . . . . . . . . . . . . . . . . 960
5.12 Autres choses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 960
5.12.1 Idée générale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 960
5.12.2 Ordre des fonctions dans le code binaire . . . . . . . . . . . . . . . 960
5.12.3 Fonctions minuscules . . . . . . . . . . . . . . . . . . . . . . . . . . . . 961
5.12.4 C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 961
5.12.5 Crash délibéré . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 961
xi
6.1.7 Modification des arguments . . . . . . . . . . . . . . . . . . . . . . . . . 970
6.1.8 Recevoir un argument par adresse . . . . . . . . . . . . . . . . . . . . 971
6.1.9 Problème des ctypes en Python (devoir à la maison en assembleur
x86) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 973
6.1.10 Exemple cdecl: DLL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 974
6.2 Thread Local Storage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 974
6.2.1 Amélioration du générateur linéaire congruent . . . . . . . . . . . . 975
6.3 Appels systèmes (syscall-s) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 981
6.3.1 Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 982
6.3.2 Windows . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 983
6.4 Linux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 983
6.4.1 Code indépendant de la position . . . . . . . . . . . . . . . . . . . . . 983
6.4.2 Hack LD_PRELOAD sur Linux . . . . . . . . . . . . . . . . . . . . . . . . 986
6.5 Windows NT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 990
6.5.1 CRT (win32) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 990
6.5.2 Win32 PE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 994
6.5.3 Windows SEH . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1005
6.5.4 Windows NT: Section critique . . . . . . . . . . . . . . . . . . . . . . . .1035
7 Outils 1038
7.1 Analyse statique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1038
7.1.1 Désassembleurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1039
7.1.2 Décompilateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1039
7.1.3 Comparaison de versions . . . . . . . . . . . . . . . . . . . . . . . . . .1039
7.2 Analyse dynamique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1040
7.2.1 Débogueurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1040
7.2.2 Tracer les appels de librairies . . . . . . . . . . . . . . . . . . . . . . . .1040
7.2.3 Tracer les appels système . . . . . . . . . . . . . . . . . . . . . . . . . .1041
7.2.4 Sniffer le réseau . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1041
7.2.5 Sysinternals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1041
7.2.6 Valgrind . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1041
7.2.7 Emulateurs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1042
7.3 Autres outils . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1042
7.3.1 Solveurs SMT . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1042
7.3.2 Calculatrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1042
7.4 Un outil manquant ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1043
xii
8.7 Blague FreeCell (Windows 7) . . . . . . . . . . . . . . . . . . . . . . . . . . . .1083
8.7.1 Partie I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1083
8.7.2 Partie II: casser le sous-menu Select Game . . . . . . . . . . . . . . .1088
8.8 Dongles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1090
8.8.1 Exemple #1: MacOS Classic et PowerPC . . . . . . . . . . . . . . . . .1090
8.8.2 Exemple #2: SCO OpenServer . . . . . . . . . . . . . . . . . . . . . . .1100
8.8.3 Exemple #3: MS-DOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1114
8.9 Cas de base de données chiffrée #1 . . . . . . . . . . . . . . . . . . . . . . .1121
8.9.1 Base64 et entropie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1121
8.9.2 Est-ce que les données sont compressées? . . . . . . . . . . . . . . .1123
8.9.3 Est-ce que les données sont chiffrées? . . . . . . . . . . . . . . . . . .1124
8.9.4 CryptoPP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1125
8.9.5 Mode Cipher Feedback . . . . . . . . . . . . . . . . . . . . . . . . . . . .1128
8.9.6 Initializing Vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1131
8.9.7 Structure du buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1132
8.9.8 Bruit en fin de buffer . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1135
8.9.9 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1136
8.9.10 Post Scriptum: brute-force IV6 . . . . . . . . . . . . . . . . . . . . . .1136
8.10 Overclocker le mineur de Bitcoin Cointerra . . . . . . . . . . . . . . . . . .1137
8.11 Casser le simple exécutable cryptor . . . . . . . . . . . . . . . . . . . . . . .1143
8.11.1 Autres idées à prendre en considération . . . . . . . . . . . . . . . .1149
8.12 SAP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1149
8.12.1 À propos de la compression du trafic réseau par le client SAP . .1149
8.12.2 Fonctions de vérification de mot de passe de SAP 6.0 . . . . . . .1164
8.13 Oracle RDBMS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1169
8.13.1 Table V$VERSION dans Oracle RDBMS . . . . . . . . . . . . . . . . . .1169
8.13.2 Table X$KSMLRU dans Oracle RDBMS . . . . . . . . . . . . . . . . . .1179
8.13.3 Table V$TIMER dans Oracle RDBMS . . . . . . . . . . . . . . . . . . .1182
8.14 Code assembleur écrit à la main . . . . . . . . . . . . . . . . . . . . . . . . .1186
8.14.1 Fichier test EICAR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1186
8.15 Démos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1188
8.15.1 10 PRINT CHR$(205.5+RND(1)) ; : GOTO 10 . . . . . . . . . . . . .1188
8.15.2 Ensemble de Mandelbrot . . . . . . . . . . . . . . . . . . . . . . . . . .1192
8.16 Un méchant bogue dans MSVCRT.DLL . . . . . . . . . . . . . . . . . . . . . .1205
8.17 Autres exemples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1212
xiii
9.2.4 Un mot à propos des primitives de chiffrement comme le XORage1251
9.2.5 Plus sur l’entropie de code exécutable . . . . . . . . . . . . . . . . . .1251
9.2.6 PRNG . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1252
9.2.7 Plus d’exemples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1252
9.2.8 Entropie de fichiers variés . . . . . . . . . . . . . . . . . . . . . . . . . .1252
9.2.9 Réduire le niveau d’entropie . . . . . . . . . . . . . . . . . . . . . . . .1254
9.3 Fichier de sauvegarde du jeu Millenium . . . . . . . . . . . . . . . . . . . . .1255
9.4 fortune programme d’indexation de fichier . . . . . . . . . . . . . . . . . . .1262
9.4.1 Hacking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1268
9.4.2 Les fichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1269
9.5 Oracle RDBMS : fichiers .SYM . . . . . . . . . . . . . . . . . . . . . . . . . . . .1269
9.6 Oracle RDBMS : fichiers .MSB-files . . . . . . . . . . . . . . . . . . . . . . . . .1282
9.6.1 Résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1288
9.7 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1288
9.8 Pour aller plus loin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1288
xiv
11.8.9 Résumé . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1323
11.9 Complexité cyclomatique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1323
13 Communautés 1329
Épilogue 1331
13.1 Des questions ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1331
Appendice 1333
.1 x86 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1333
.1.1 Terminologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1333
.1.2 Registres à usage général . . . . . . . . . . . . . . . . . . . . . . . . . . .1333
.1.3 registres FPU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1338
.1.4 registres SIMD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1340
.1.5 Registres de débogage . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1340
.1.6 Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1342
.1.7 npad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1359
.2 ARM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1361
.2.1 Terminologie . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1361
.2.2 Versions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1361
.2.3 ARM 32-bit (AArch32) . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1361
.2.4 ARM 64-bit (AArch64) . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1363
.2.5 Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1363
.3 MIPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1364
.3.1 Registres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1364
.3.2 Instructions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1365
.4 Quelques fonctions de la bibliothèque de GCC . . . . . . . . . . . . . . . . . .1366
.5 Quelques fonctions de la bibliothèque MSVC . . . . . . . . . . . . . . . . . . .1366
.6 Cheatsheets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1366
.6.1 IDA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1366
.6.2 OllyDbg . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1367
.6.3 MSVC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1367
.6.4 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1368
.6.5 GDB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1368
xv
Acronymes utilisés 1371
Glossaire 1378
Index 1381
xvi
Préface
C’est quoi ces deux titres?
Le livre a été appelé “Reverse Engineering for Beginners” en 2014-2018, mais j’ai
toujours suspecté que ça rendait son audience trop réduite.
Les gens de l’infosec connaissent le “reverse engineering”, mais j’ai rarement en-
tendu le mot “assembleur” de leur part.
De même, le terme “reverse engineering” est quelque peu cryptique pour une au-
dience générale de programmeurs, mais qui ont des connaissances à propos de
l’“assembleur”.
En juillet 2018, à titre d’expérience, j’ai changé le titre en “Assembly Language for
Beginners” et publié le lien sur le site Hacker News7 , et le livre a été plutôt bien
accueilli.
Donc, c’est ainsi que le livre a maintenant deux titres.
Toutefois, j’ai changé le second titre à “Understanding Assembly Language”, car
quelqu’un a déjà écrit le livre “Assembly Language for Beginners”. De même, des
gens disent que “for Beginners” sonne sarcastique pour un livre de ~1000 pages.
Les deux livres diffèrent seulement par le titre, le nom du fichier (UAL-XX.pdf versus
RE4B-XX.pdf), l’URL et quelques-une des première pages.
À propos de la rétro-ingénierie
Il existe plusieurs définitions pour l’expression «ingénierie inverse ou rétro-ingénierie
reverse engineering » :
1) L’ingénierie inverse de logiciels : examiner des programmes compilés;
2) Le balayage des structures en 3D et la manipulation numérique nécessaire afin
de les reproduire;
3) Recréer une structure de base de données.
Ce livre concerne la première définition.
Prérequis
Connaissance basique du C LP8 . Il est recommandé de lire: 12.1.3 on page 1327.
Exercices et tâches
…ont été déplacés sur un site différent : http://challenges.re.
7. https://news.ycombinator.com/item?id=17549050
8. Langage de programmation
xvii
Éloges de ce livre
https://beginners.re/#praise.
Universités
Ce livre est recommandé par au moins ces universités: https://beginners.re/
#uni.
Remerciements
Pour avoir patiemment répondu à toutes mes questions : SkullC0DEr.
Pour m’avoir fait des remarques par rapport à mes erreurs ou manques de préci-
sion : Alexander Lysenko, Alexander «Solar Designer » Peslyak, Federico Ramondino,
Mark Wilson, Razikhova Meiramgul Kayratovna, Anatoly Prokofiev, Kostya Begunets,
Valentin “netch” Nechayev, Aleksandr Plakhov, Artem Metla, Alexander Yastrebov,
Vlad Golovkin9 , Evgeny Proshin, Alexander Myasnikov, Alexey Tretiakov, Zhu Rui-
jin, Changmin Heo, Vitor Vidal, Stijn Crevits, Jean-Gregoire Foulon10 , Ben L., Etienne
Khan, Norbert Szetei11 , Marc Remy, Michael Hansen, Derk Barten, The Renaissance12 ,
Hugo Chan, Emil Mursalimov, Tanner Hoke, Tan90909090@GitHub, Ole Petter Orha-
gen, Sourav Punoriyar, Vitor Oliveira, Alexis Ehret, Maxim Shlochiski, Greg Paton,
Pierrick Lebourgeois..
Pour m’avoir aidé de toute autre manière : Andrew Zubinski, Arnaud Patard (rtp
on #debian-arm IRC), noshadow on #gcc IRC, Aliaksandr Autayeu, Mohsen Mostafa
Jokar, Peter Sovietov, Misha “tiphareth” Verbitsky.
Pour avoir traduit le livre en chinois simplifié : Antiy Labs (antiy.cn), Archer.
Pour avoir traduit le livre en coréen : Byungho Min.
Pour avoir traduit le livre en néerlandais : Cedric Sambre (AKA Midas).
Pour avoir traduit le livre en espagnol : Diego Boy, Luis Alberto Espinosa Calvo, Fer-
nando Guida, Diogo Mussi, Patricio Galdames.
Pour avoir traduit le livre en portugais : Thales Stevan de A. Gois, Diogo Mussi, Luiz
Filipe, Primo David Santini.
Pour avoir traduit le livre en italien : Federico Ramondino13 , Paolo Stivanin14 , twyK,
Fabrizio Bertone, Matteo Sticco, Marco Negro15 , bluepulsar.
Pour avoir traduit le livre en français : Florent Besnard16 , Marc Remy17 , Baudouin
9. goto-vlad@github
10. https://github.com/pixjuan
11. https://github.com/73696e65
12. https://github.com/TheRenaissance
13. https://github.com/pinkrab
14. https://github.com/paolostivanin
15. https://github.com/Internaut401
16. https://github.com/besnardf
17. https://github.com/mremy
xviii
Landais, Téo Dacquet18 , BlueSkeye@GitHub19 .
Pour avoir traduit le livre en allemand : Dennis Siekmeier20 , Julius Angres21 , Dirk
Loser22 , Clemens Tamme, Philipp Schweinzer.
Pour avoir traduit le livre en polonais: Kateryna Rozanova, Aleksander Mistewicz,
Wiktoria Lewicka, Marcin Sokołowski.
Pour avoir traduit le livre en japonais: shmz@github23 ,4ryuJP@github24 .
Pour la relecture : Vladimir Botov, Andrei Brazhuk, Mark “Logxen” Cooper, Yuan Jo-
chen Kang, Mal Malakov, Lewis Porter, Jarle Thorsen, Hong Xie.
Vasil Kolev25 a réalisé un gros travail de relecture et a corrigé beaucoup d’erreurs.
Merci également à toutes les personnes sur github.com qui ont contribué aux re-
marques et aux corrections.
De nombreux packages LATEX ont été utilisé : j’aimerais également remercier leurs
auteurs.
Donateurs
xix
mini-FAQ
Q: Est-ce que ce livre est plus simple/facile que les autres?
R: Non, c’est à peu près le même niveau que les autres livres sur ce sujet.
Q: J’ai trop peur de commencer à lire ce livre, il fait plus de 1000 pages. ”...for Be-
ginners” dans le nom sonne un peu sarcastique.
R: Toutes sortes de listings constituent le gros de ce livre. Le livre est en effet pour
les débutants, il manque (encore) beaucoup de choses.
Q: Quels sont les pré-requis nécessaires avant de lire ce livre ?
R: Une compréhension de base du C/C++ serait l’idéal.
Q: Dois-je apprendre x86/x64/ARM et MIPS en même temps ? N’est-ce pas un peu
trop ?
R: Je pense que les débutants peuvent seulement lire les parties x86/x64, tout en
passant/feuilletant celles ARM/MIPS.
Q: Puis-je acheter une version papier du livre en russe / anglais ?
R: Malheureusement non, aucune maison d’édition n’a été intéressée pour publier
une version en russe ou en anglais du livre jusqu’à présent. Cependant, vous pou-
vez demander à votre imprimerie préférée de l’imprimer et de le relier. https://
yurichev.com/news/20200222_printed_RE4B/.
Q: Y a-il une version ePub/Mobi ?
R: Le livre dépend majoritairement de TeX/LaTeX, il n’est donc pas évident de le
convertir en version ePub/Mobi.
Q: Pourquoi devrait-on apprendre l’assembleur de nos jours ?
R: A moins d’être un développeur d’OS26 , vous n’aurez probablement pas besoin
d’écrire en assembleur—les derniers compilateurs (ceux de notre décennie) sont
meilleurs que les êtres humains en terme d’optimisation. 27 .
De plus, les derniers CPU28 s sont des appareils complexes et la connaissance de
l’assembleur n’aide pas vraiment à comprendre leurs mécanismes internes.
Cela dit, il existe au moins deux domaines dans lesquels une bonne connaissance
de l’assembleur peut être utile : Tout d’abord, pour de la recherche en sécurité ou
sur des malwares. C’est également un bon moyen de comprendre un code compilé
lorsqu’on le debug. Ce livre est donc destiné à ceux qui veulent comprendre l’assem-
bleur plutôt que d’écrire en assembleur, ce qui explique pourquoi il y a de nombreux
exemples de résultats issus de compilateurs dans ce livre.
Q: J’ai cliqué sur un lien dans le document PDF, comment puis-je retourner en ar-
rière ?
R: Dans Adobe Acrobat Reader, appuyez sur Alt + Flèche gauche. Dans Evince, ap-
puyez sur le bouton “<”.
26. Système d’exploitation (Operating System)
27. Un très bon article à ce sujet : [Agner Fog, The microarchitecture of Intel, AMD and VIA CPUs, (2016)]
28. Central Processing Unit
xx
Q: Puis-je imprimer ce livre / l’utiliser pour de l’enseignement ?
R: Bien sûr ! C’est la raison pour laquelle le livre est sous licence Creative Commons
(CC BY-SA 4.0).
Q: Pourquoi ce livre est-il gratuit ? Vous avez fait du bon boulot. C’est suspect, comme
nombre de choses gratuites.
R: D’après ma propre expérience, les auteurs d’ouvrages techniques font cela pour
l’auto-publicité. Il n’est pas possible de se faire beaucoup d’argent d’une telle ma-
nière.
Q: Comment trouver du travail dans le domaine de la rétro-ingénierie ?
R: Il existe des sujets d’embauche qui apparaissent de temps en temps sur Reddit,
dédiés à la rétro-ingénierie (cf. reverse engineering ou RE)29 . Jetez un œil ici.
Un sujet d’embauche quelque peu lié peut être trouvé dans le subreddit «netsec ».
Q: Les versions des compilateurs sont déjà obsolètes…
R: Vous ne devez pas reproduire précisement les étapes. Utilisez les compilateurs
que vous avez déjà sur votre OS. En outre, il y a: Compiler Explorer.
Q: J’ai une question...
R: Envoyez-la moi par email (<first_name @ last_name . com> / <first_name . last_name
@ gmail . com>).
xxi
Enregistrement du livre à la Bibliothèque Nationale d’Iran: http://opac.nlai.ir/
opac-prod/bibliographic/4473995.
xxii
Chapitre 1
Pattern de code
1.1 La méthode
Lorsque j’ai commencé à apprendre le C, et plus tard, le C++, j’ai pris l’habitude
d’écrire des petits morceaux de code, de les compiler et de regarder le langage
d’assemblage généré. Cela m’a permis de comprendre facilement ce qui se passe
dans le code que j’écris. 1 . Je l’ai fait si souvent que la relation entre le code C++
et ce que le compilateur produit a été imprimée profondément dans mon esprit. Ça
m’est facile d’imaginer immédiatement l’allure de la fonction et du code C. Peut-être
que cette méthode pourrait être utile à d’autres.
Parfois, des anciens compilateurs sont utilisés, afin d’obtenir des extraits de code le
plus court (ou le plus simple) possible.
À propos, il y a un bon site où vous pouvez faire la même chose, avec de nombreux
compilateurs, au lieu de les installer sur votre système. Vous pouvez également
l’utiliser: https://godbolt.org/.
Exercices
Lorsque j’étudiais le langage d’assemblage, j’ai souvent compilé des petites fonc-
tions en C et les ai ensuite récrites peu à peu en assembleur, en essayant d’obtenir
un code aussi concis que possible. Cela n’en vaut probablement plus la peine aujour-
d’hui, car il est difficile de se mesurer aux derniers compilateurs en terme d’efficacité.
Cela reste par contre un excellent moyen d’approfondir ses connaissances de l’as-
sembleur. N’hésitez pas à prendre n’importe quel code assembleur de ce livre et à
essayer de le rendre plus court. Toutefois, n’oubliez pas de tester ce que vous aurez
écrit.
1. En fait, je le fais encore cela lorsque je ne comprends pas ce qu’un morceau de code fait. Exemple
récent de 2019: p += p+(i&1)+2; tiré de “SAT0W” solveur SAT par D.Knuth.
1
Niveau d’optimisation et information de débogage
Le code source peut être compilé par différents compilateurs, avec des niveaux d’op-
timisation variés. Un compilateur en a typiquement trois, où le niveau 0 désactive
l’optimisation. L’optimisation peut se faire en ciblant la taille du code ou la vitesse
d’exécution. Un compilateur sans optimisation est plus rapide et produit un code
plus compréhensible (quoique verbeux), alors qu’un compilateur avec optimisation
est plus lent et essaye de produire un code qui s’exécute plus vite (mais pas forcé-
ment plus compact). En plus des niveaux d’optimisation, un compilateur peut inclure
dans le fichier généré des informations de débogage, qui produit un code facilitant
le débogage. Une des caractéristiques importante du code de ’debug’, est qu’il peut
contenir des liens entre chaque ligne du code source et les adresses du code machine
associé. D’un autre côté, l’optimisation des compilateurs tend à générer du code où
des lignes du code source sont modifiées, et même parfois absentes du code ma-
chine résultant. Les rétro-ingénieurs peuvent rencontrer n’importe quelle version,
simplement parce que certains développeurs mettent les options d’optimisation, et
d’autres pas. Pour cette raison, et lorsque c’est possible, nous allons essayer de tra-
vailler sur des exemples avec les versions de débogage et finale du code présenté
dans ce livre.
2
C/C++, Java, Python, etc., mais c’est plus simple pour un CPU d’utiliser un niveau
d’abstraction de beaucoup plus bas niveau. Peut-être qu’il serait possible d’inventer
un CPU qui puisse exécuter du code d’un LP de haut niveau, mais il serait beaucoup
plus complexe que les CPUs que nous connaissons aujourd’hui. D’une manière simi-
laire, c’est moins facile pour les humains d’écrire en langage d’assemblage à cause
de son bas niveau et de la difficulté d’écrire sans faire un nombre énorme de fautes
agaçantes. Le programme qui convertit d’un LP haut niveau vers l’assemblage est
appelé un compilateur.
Le jeu d’instructions ISA x86 a toujours été avec des instructions de taille variable.
Donc quand l’époque du 64-bit arriva, les extensions x64 n’ont pas impacté le ISA
très significativement. En fait, le ISA x86 contient toujours beaucoup d’instructions
apparues pour la première fois dans un CPU 8086 16-bit, et que l’on trouve encore
dans beaucoup de CPUs aujourd’hui. ARM est un CPU RISC3 conçu avec l’idée d’ins-
tructions de taille fixe, ce qui présentait quelques avantages dans le passé. Au tout
début, toutes les instructions ARM étaient codés sur 4 octets4 . C’est maintenant
connu comme le «ARM mode ». Ensuite ils sont arrivés à la conclusion que ce n’était
pas aussi économique qu’ils l’avaient imaginé sur le principe. En réalité, la majo-
rité des instructions CPU utilisées5 dans le monde réel peuvent être encodées en
utilisant moins d’informations. Ils ont par conséquent ajouté un autre ISA, appelé
Thumb, où chaque instruction était encodée sur seulement 2 octets. C’est mainte-
nant connu comme le «Thumb mode ». Cependant, toutes les instructions ne peuvent
être encodées sur seulement 2 octets, donc les instructions Thumb sont un peu limi-
tées. On peut noter que le code compilé pour le mode ARM et pour le mode Thumb
peut, évidemment, coexister dans un seul programme. Les créateurs de ARM pen-
sèrent que Thumb pourrait être étendu, donnant naissance à Thumb-2, qui apparut
dans ARMv7. Thumb-2 utilise toujours des instructions de 2 octets, mais a de nou-
velles instructions dont la taille est de 4 octets. Une erreur couramment répandue
est que Thumb-2 est un mélange de ARM et Thumb. C’est incorrect. Plutôt, Thumb-
2 fut étendu pour supporter totalement toutes les caractéristiques du processeur
afin qu’il puisse rivaliser avec le mode ARM—un objectif qui a été clairement réus-
si, puisque la majorité des applications pour iPod/iPhone/iPad est compilée pour le
jeu d’instructions de Thumb-2 (il est vrai que c’est largement dû au fait que Xcode
le faisait par défaut). Plus tard, le ARM 64-bit sortit. Ce ISA a des instructions de 4
octets, et enlevait le besoin d’un mode Thumb supplémentaire. Cependant, les pré-
requis de 64-bit affectèrent le ISA, résultant maintenant au fait que nous avons trois
jeux d’instructions ARM: ARM mode, Thumb mode (incluant Thumb-2) et ARM64. Ces
ISAs s’intersectent partiellement, mais on peut dire que ce sont des ISAs différents,
plutôt que des variantes du même. Par conséquent, nous essayerons d’ajouter des
fragments de code dans les trois ISAs de ARM dans ce livre. Il y a, d’ailleurs, bien
d’autres ISAs RISC avec des instructions de taille fixe de 32-bit, comme MIPS, Po-
werPC et Alpha AXP.
3. Reduced Instruction Set Computing
4. D’ailleurs, les instructions de taille fixe sont pratiques parce qu’il est possible de calculer l’instruction
suivante (ou précédente) sans effort. Cette caractéristique sera discutée dans la section de l’opérateur
switch() ( 1.21.2 on page 226).
5. Ce sont MOV/PUSH/CALL/Jcc
3
1.2.2 Systèmes de numération
4
Les nombres binaires sont volumineux lorsqu’ils sont représentés dans le code source
et les dumps, c’est pourquoi le système hexadécimal peut être utilisé. La base hexa-
décimale utilise les nombres 0..9 et aussi 6 caractères latins : A..F. Chaque chiffre
hexadécimal prend 4 bits ou 4 chiffres binaires, donc c’est très simple de conver-
tir un nombre binaire vers l’hexadécimal et inversement, même manuellement, de
tête.
5
Peut-être que les nombres hexadécimaux les plus visibles sont dans les URL10 s. C’est
la façon d’encoder les caractères non-Latin. Par exemple: https://en.wiktionary.
org/wiki/na%C3%AFvet%C3%A9 est l’URL de l’article de Wiktionary à propos du mot
«naïveté ».
Base octale
Ainsi chaque bit correspond à un droit: lecture (r) / écriture (w) / exécution (x).
L’importance de chmod est que le nombre entier en argument peut être écrit comme
un nombre octal. Prenons par exemple, 644. Quand vous tapez chmod 644 file,
vous définissez les droits de lecture/écriture pour le propriétaire, les droits de lecture
pour le groupe et encore les droits de lecture pour tous les autres. Convertissons le
nombre octal 644 en binaire, ça donne 110100100, ou (par groupe de 3 bits) 110
100 100.
Maintenant que nous savons que chaque triplet sert à décrire les permissions pour
le propriétaire/groupe/autres : le premier est rw-, le second est r-- et le troisième
est r--.
Le système de numération octal était aussi populaire sur les vieux ordinateurs comme
le PDP-8 parce que les mots pouvaient être de 12, 24 ou de 36 bits et ces nombres
sont divisibles par 3, donc la représentation octale était naturelle dans cet environne-
ment. Aujourd’hui, tous les ordinateurs populaires utilisent des mots/taille d’adresse
de 16, 32 ou de 64 bits et ces nombres sont divisibles par 4, donc la représentation
hexadécimale était plus naturelle ici.
10. Uniform Resource Locator
6
Le système de numération octal est supporté par tous les compilateurs C/C++ stan-
dards. C’est parfois une source de confusion parce que les nombres octaux sont
notés avec un zéro au début. Par exemple, 0377 est 255. Et parfois, vous faites une
faute de frappe et écrivez ”09” au lieu de 9, et le compilateur renvoie une erreur.
GCC peut renvoyer quelque chose comme ça:
erreur: chiffre 9 invalide dans la constante en base 8.
De même, le système octal est assez populaire en Java. Lorsque IDA11 affiche des
chaînes Java avec des caractères non-imprimables, ils sont encodés dans le système
octal au lieu d’hexadécimal. Le décompilateur Java JAD se comporte de la même
façon.
Divisibilité
Quand vous voyez un nombre décimal comme 120, vous en déduisez immédiate-
ment qu’il est divisible par 10, parce que le dernier chiffre est zéro. De la même
façon, 123400 est divisible par 100 parce que les deux derniers chiffres sont zéros.
Pareillement, le nombre hexadécimal 0x1230 est divisible par 0x10 (ou 16), 0x123000
est divisible par 0x1000 (ou 4096), etc.
Un nombre binaire 0b1000101000 est divisible par 0b1000 (8), etc.
Cette propriété peut être souvent utilisée pour déterminer rapidement si l’adresse
ou la taille d’un bloc mémoire correspond à une limite. Par exemple, les sections
dans les fichiers PE12 commencent quasiment toujours à une adresse finissant par
3 zéros hexadécimaux: 0x41000, 0x10001000, etc. La raison sous-jacente est que
la plupart des sections PE sont alignées sur une limite de 0x1000 (4096) octets.
L’arithmétique multi-précision utilise des nombres très grands et chacun peut être
stocké sur plusieurs octets. Par exemple, les clés RSA, tant publique que privée,
utilisent jusqu’à 4096 bits et parfois plus encore.
Dans [Donald E. Knuth, The Art of Computer Programming, Volume 2, 3rd ed., (1997),
265] nous trouvons l’idée suivante: quand vous stockez un nombre multi-précision
dans plusieurs octets, le nombre complet peut être représenté dans une base de
28 = 256, et chacun des chiffres correspond à un octet. De la même manière, si
vous sauvegardez un nombre multi-précision sur plusieurs entiers de 32 bits, chaque
chiffre est associé à l’emplacement de 32 bits et vous pouvez penser à ce nombre
comme étant stocké dans une base 232 .
Les nombres dans une base non décimale sont généralement prononcés un chiffre à
la fois : “un-zéro-zéro-un-un-...”. Les mots comme “dix“, “mille“, etc, ne sont géné-
ralement pas prononcés, pour éviter d’être confondus avec ceux en base décimale.
11. Désassembleur interactif et débogueur développé par Hex-Rays
12. Portable Executable
7
Nombres à virgule flottante
Pour distinguer les nombres à virgule flottante des entiers, ils sont souvent écrits
avec avec un “.0“ à la fin, comme 0.0, 123.0, etc.
Compilons-la!
1.3.1 x86
Voici ce que les compilateurs GCC et MSVC produisent sur une plateforme x86:
Listing 1.2: GCC/MSVC avec optimisation (résultat en sortie de l’assembleur)
f :
ret
1.3.2 ARM
Listing 1.3: avec optimisation Keil 6/2013 (Mode ARM) ASM Output
f PROC
BX lr
ENDP
L’adresse de retour n’est pas stockée sur la pile locale avec l’ISA ARM, mais dans le
”link register” (registre de lien), donc l’instruction BX LR force le flux d’exécution à
sauter à cette adresse—renvoyant effectivement l’exécution vers l’appelant.
1.3.3 MIPS
Il y a deux conventions de nommage utilisées dans le monde MIPS pour nommer les
registres: par numéro (de $0 à $31) ou par un pseudo-nom ($V0, $A0, etc.).
La sortie de l’assembleur GCC ci-dessous liste les registres par numéro:
Listing 1.4: GCC 4.4.5 avec optimisation (résultat en sortie de l’assembleur)
j $31
nop
8
…tandis qu’IDA le fait—avec les pseudo noms:
La première instruction est l’instruction de saut (J ou JR) qui détourne le flux d’exé-
cution vers l’appelant, sautant à l’adresse dans le registre $31 (ou $RA).
Ce registre est similaire à LR13 en ARM.
La seconde instruction est NOP14 , qui ne fait rien. Nous pouvons l’ignorer pour l’ins-
tant.
Les registres et les noms des instructions dans le monde de MIPS sont traditionnel-
lement écrits en minuscules. Cependant, dans un souci d’homogénéité, nous allons
continuer d’utiliser les lettres majuscules, étant donné que c’est la convention suivie
par tous les autres ISAs présentés dans ce livre.
void some_function()
{
...
...
};
Dans une compilation en non-debug (e.g., “release”), _DEBUG n’est pas défini, donc
la fonction dbg_print(), bien qu’elle soit appelée pendant l’exécution, sera vide.
13. Link Register
14. No Operation
9
Un autre moyen de protection logicielle est de faire plusieurs compilations: une pour
les clients, une de démonstration. La compilation de démonstration peut omettre
certaines fonctions importantes, comme ici:
La fonction save_file() peut être appelée lorsque l’utilisateur clique sur le menu
Fichier->Enregistrer. La version de démo peut être livrée avec cet item du menu
désactivé, mais même si un logiciel cracker pourra l’activer, une fonction vide sans
code utile sera appelée.
IDA signale de telles fonctions avec des noms comme nullsub_00, nullsub_01, etc.
Compilons la!
1.4.1 x86
Voici ce que les compilateurs GCC et MSVC produisent sur une plateforme x86:
Il y a juste deux instructions: la première place la valeur 123 dans le registre EAX, qui
est par convention le registre utilisé pour stocker la valeur renvoyée d’une fonction
et la seconde est RET, qui retourne l’exécution vers l’appelant.
L’appelant prendra le résultat de cette fonction dans le registre EAX.
10
1.4.2 ARM
Il y a quelques différences sur la platforme ARM:
Listing 1.10: avec optimisation Keil 6/2013 (Mode ARM) ASM Output
f PROC
MOV r0,#0x7b ; 123
BX lr
ENDP
ARM utilise le registre R0 pour renvoyer le résultat d’une fonction, donc 123 est copié
dans R0.
Il est à noter que l’instruction MOV est trompeuse pour les plateformes x86 et ARM
ISAs.
La donnée n’est en réalité pas déplacée (moved) mais copiée.
1.4.3 MIPS
La sortie de l’assembleur GCC ci-dessous indique les registres par numéro:
Le registre $2 (ou $V0) est utilisé pour stocker la valeur de retour de la fonction. LI
signifie “Load Immediate” et est l’équivalent MIPS de MOV.
L’autre instruction est l’instruction de saut (J ou JR) qui retourne le flux d’exécution
vers l’appelant.
Vous pouvez vous demander pourquoi la position de l’instruction d’affectation de va-
leur immédiate (LI) et l’instruction de saut (J ou JR) sont échangées. Ceci est dû à une
fonctionnalité du RISC appelée “branch delay slot” (slot de délai de branchement).
La raison de cela est du à une bizarrerie dans l’architecture de certains RISC ISAs et
n’est pas importante pour nous. Nous gardons juste en tête qu’en MIPS, l’instruction
qui suit une instruction de saut ou de branchement est exécutée avant l’instruction
de saut ou de branchement elle-même.
Par conséquent, les instructions de branchement échangent toujours leur place avec
l’instruction qui doit être exécutée avant.
11
1.4.4 En pratique
Les fonctions qui retournent simplement 1 (true) ou 0 (false) sont vraiment fré-
quentes en pratique.
Les plus petits utilitaires UNIX standard, /bin/true et /bin/false renvoient respective-
ment 0 et 1, comme code de retour. (un code retour de zéro signifie en général
succès, non-zéro une erreur).
int main()
{
printf("hello, world\n") ;
return 0;
}
1.5.1 x86
MSVC
12
_TEXT ENDS
MSVC génère des listings assembleur avec la syntaxe Intel. La différence entre la
syntaxe Intel et la syntaxe AT&T sera discutée dans 1.5.1 on page 15.
Le compilateur a généré le fichier object 1.obj, qui sera lié dans l’exécutable 1.exe.
Dans notre cas, le fichier contient deux segments: CONST (pour les données constantes)
et _TEXT (pour le code).
La chaîne hello, world en C/C++ a le type const char[][Bjarne Stroustrup, The
C++ Programming Language, 4th Edition, (2013)p176, 7.3.2], mais elle n’a pas de
nom. Le compilateur doit pouvoir l’utiliser et lui défini donc le nom interne $SG3830
à cette fin.
C’est pourquoi l’exemple pourrait être récrit comme suit:
#include <stdio.h>
int main()
{
printf($SG3830) ;
return 0;
}
13
Cette instruction a presque le même effet mais le contenu du registre ECX sera écra-
sé. Le compilateur C++ d’Intel utilise probablement POP ECX car l’opcode de cette
instruction est plus court que celui de ADD ESP, x (1 octet pour POP contre 3 pour
ADD).
Voici un exemple d’utilisation de POP à la place de ADD dans Oracle RDBMS :
GCC
Maintenant compilons le même code C/C++ avec le compilateur GCC 4.4.1 sur Linux:
gcc 1.c -o 1. Ensuite, avec l’aide du désassembleur IDA, regardons comment la
fonction main() a été créée. IDA, comme MSVC, utilise la syntaxe Intel19 .
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
17. Wikipédia
18. C Runtime library
19. GCC peut aussi produire un listing assembleur utilisant la syntaxe Intel en lui passant les options
-S -masm=intel.
14
sub esp, 10h
mov eax, offset aHelloWorld ; "hello, world\n"
mov [esp+10h+var_10], eax
call _printf
mov eax, 0
leave
retn
main endp
Le résultat est presque le même. L’adresse de la chaîne hello, world (stockée dans
le segment de donnée) est d’abord chargée dans le registre EAX puis sauvée sur la
pile.
En plus, le prologue de la fonction comprend AND ESP, 0FFFFFFF0h —cette instruc-
tion aligne le registre ESP sur une limite de 16-octet. Ainsi, toutes les valeurs sur la
pile seront alignées de la même manière (Le CPU est plus performant si les adresses
avec lesquelles il travaille en mémoire sont alignées sur des limites de 4-octet ou
16-octet).
SUB ESP, 10h réserve 16 octets sur la pile. Pourtant, comme nous allons le voir,
seuls 4 sont nécessaires ici.
C’est parce que la taille de la pile allouée est alignée sur une limite de 16-octet.
L’adresse de la chaîne est (ou un pointeur vers la chaîne) est stockée directement
sur la pile sans utiliser l’instruction PUSH. var_10 —est une variable locale et est aussi
un argument pour printf(). Lisez à ce propos en dessous.
Ensuite la fonction printf() est appelée.
Contrairement à MSVC, lorsque GCC compile sans optimisation, il génère MOV EAX,
0 au lieu d’un opcode plus court.
La dernière instruction, LEAVE —est équivalente à la paire d’instruction MOV ESP,
EBP et POP EBP —en d’autres mots, cette instruction déplace le pointeur de pile
(ESP) et remet le registre EBP dans son état initial. Ceci est nécessaire puisque nous
avons modifié les valeurs de ces registres (ESP et EBP) au début de la fonction (en
exécutant MOV EBP, ESP / AND ESP, …).
15
.string "hello, world\n"
.text
.globl main
.type main, @function
main :
.LFB0 :
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
andl $-16, %esp
subl $16, %esp
movl $.LC0, (%esp)
call printf
movl $0, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0 :
.size main, .-main
.ident "GCC : (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
.section .note.GNU-stack,"",@progbits
16
• Opérandes source et destination sont écrites dans l’ordre inverse.
En syntaxe Intel: <instruction> <opérande de destination> <opérande source>.
En syntaxe AT&T: <instruction> <opérande source> <opérande de destina-
tion>.
Voici un moyen simple de mémoriser la différence: lorsque vous avez affaire
avec la syntaxe Intel, vous pouvez imaginer qu’il y a un signe égal (=) entre les
opérandes et lorsque vous avez affaire avec la syntaxe AT&T imaginez qu’il y
a un flèche droite (→) 21 .
• AT&T: Avant les noms de registres, un signe pourcent doit être écrit (%) et avant
les nombres, un signe dollar ($). Les parenthèses sont utilisées à la place des
crochets.
• AT&T: un suffixe est ajouté à l’instruction pour définir la taille de l’opérande:
– q — quad (64 bits)
– l — long (32 bits)
– w — word (16 bits)
– b — byte (8 bits)
Retournons au résultat compilé: il est identique à ce que l’on voit dans IDA. Avec une
différence subtile: 0FFFFFFF0h est représenté avec $-16. C’est la même chose: 16
dans le système décimal est 0x10 en hexadécimal. -0x10 est équivalent à 0xFFFFFFF0
(pour un type de donnée sur 32-bit).
Encore une chose: la valeur de retour est mise à 0 en utilisant un MOV usuel, pas
un XOR. MOV charge seulement la valeur dans le registre. Le nom est mal choisi (la
donnée n’est pas déplacée, mais plutôt copiée). Dans d’autres architectures, cette
instruction est nommée «LOAD » ou «STORE » ou quelque chose de similaire.
Nous pouvons facilement trouver la chaîne “hello, world” dans l’exécutable en utili-
sant Hiew:
21. À propos, dans certaine fonction C standard (e.g., memcpy(), strcpy()) les arguments sont listés
de la même manière que dans la syntaxe Intel: en premier se trouve le pointeur du bloc mémoire de
destination, et ensuite le pointeur sur le bloc mémoire source.
17
Fig. 1.1: Hiew
Le texte en espagnol est un octet plus court que celui en anglais, nous ajoutons
l’octet 0x0A à la fin (\n) ainsi qu’un octet à zéro.
Ça fonctionne.
Comment faire si nous voulons insérer un message plus long ? Il y a quelques octets à
zéro après le texte original en anglais. Il est difficile de dire s’ils peuvent être écrasés:
ils peuvent être utilisés quelque part dans du code CRT, ou pas. De toutes façons,
écrasez-les seulement si vous savez vraiment ce que vous faîtes.
18
Searching 5 bytes in [0x400000-0x601040]
hits : 1
0x004005c4 hit0_0 .HHhello, world ;0.
[0x00400430]> s 0x004005c4
[0x004005c4]> px
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x004005c4 6865 6c6c 6f2c 2077 6f72 6c64 0000 0000 hello, world....
0x004005d4 011b 033b 3000 0000 0500 0000 1cfe ffff ...;0...........
0x004005e4 7c00 0000 5cfe ffff 4c00 0000 52ff ffff |...\...L...R...
0x004005f4 a400 0000 6cff ffff c400 0000 dcff ffff ....l...........
0x00400604 0c01 0000 1400 0000 0000 0000 017a 5200 .............zR.
0x00400614 0178 1001 1b0c 0708 9001 0710 1400 0000 .x..............
0x00400624 1c00 0000 08fe ffff 2a00 0000 0000 0000 ........*.......
0x00400634 0000 0000 1400 0000 0000 0000 017a 5200 .............zR.
0x00400644 0178 1001 1b0c 0708 9001 0000 2400 0000 .x..........$...
0x00400654 1c00 0000 98fd ffff 3000 0000 000e 1046 ........0......F
0x00400664 0e18 4a0f 0b77 0880 003f 1a3b 2a33 2422 ..J..w...?.;*3$"
0x00400674 0000 0000 1c00 0000 4400 0000 a6fe ffff ........D.......
0x00400684 1500 0000 0041 0e10 8602 430d 0650 0c07 .....A....C..P..
0x00400694 0800 0000 4400 0000 6400 0000 a0fe ffff ....D...d.......
0x004006a4 6500 0000 0042 0e10 8f02 420e 188e 0345 e....B....B....E
0x004006b4 0e20 8d04 420e 288c 0548 0e30 8606 480e . ..B.(..H.0..H.
[0x004005c4]> oo+
File a.out reopened in read-write mode
[0x004005c4]> q
19
Traduction de logiciel à l’ère MS-DOS
La méthode que je viens de décrire était couramment employée pour traduire des
logiciels sous MS-DOS en russe dans les années 1980 et 1990. Cette technique est
accessible même pour ceux qui ne connaissent pas le code machine et les formats
de fichier exécutable. La nouvelle chaîne ne doit être pas être plus longue que l’an-
cienne, car il y a un risque d’écraser une autre valeur ou du code ici. Les mots et
les phrases russes sont en général un peu plus longs qu’en anglais, c’est pourquoi
les logiciels traduits sont pleins d’acronymes sibyllins et d’abréviations difficilement
lisibles.
Peut-être que cela s’est produit pour d’autres langages durant cette période.
En ce qui concerne Delphi, la taille de la chaîne de caractères doit elle aussi être
ajustée.
1.5.2 x86-64
MSVC: x86-64
main PROC
sub rsp, 40
lea rcx, OFFSET FLAT :$SG2989
call printf
xor eax, eax
add rsp, 40
ret 0
main ENDP
En x86-64, tous les registres ont été étendus à 64-bit et leurs noms ont maintenant
le préfixe R-. Afin d’utiliser la pile moins souvent (en d’autres termes, pour accéder
moins souvent à la mémoire externe/au cache), il existe un moyen répandu de passer
les arguments aux fonctions par les registres (fastcall) 6.1.3 on page 964. I.e., une
partie des arguments de la fonction est passée par les registres, le reste—par la pile.
En Win64, 4 arguments de fonction sont passés dans les registres RCX, RDX, R8, R9.
C’est ce que l’on voit ci-dessus: un pointeur sur la chaîne pour printf() est passé
non pas par la pile, mais par le registre RCX. Les pointeurs font maintenant 64-bit, ils
sont donc passés dans les registres 64-bit (qui ont le préfixe R-). Toutefois, pour la
20
rétrocompatibilité, il est toujours possible d’accéder à la partie 32-bits des registres,
en utilisant le préfixe E-. Voici à quoi ressemblent les registres RAX/EAX/AX/AL en
x86-64:
Octet d’indice
7 6 5 4 3 2 1 0
RAXx64
EAX
AX
AH AL
La fonction main() renvoie un type int, qui est, en C/C++, pour une meilleure rétro-
compatibilité et portabilité, toujours 32-bit, c’est pourquoi le registre EAX est mis à
zéro à la fin de la fonction (i.e., la partie 32-bit du registre) au lieu de RAX. Il y aussi
40 octets alloués sur la pile locale. Cela est appelé le «shadow space », dont nous
parlerons plus tard: 1.14.2 on page 136.
GCC: x86-64
Une méthode de passage des arguments à la fonction dans des registres est aussi
utilisée sur Linux, *BSD et Mac OS X est [Michael Matz, Jan Hubicka, Andreas Jaeger,
Mark Mitchell, System V Application Binary Interface. AMD64 Architecture Processor
Supplement, (2013)] 22 . Linux, *BSD et Mac OS X utilisent aussi une méthode pour
passer les arguments d’une fonction par les registres: [Michael Matz, Jan Hubicka,
Andreas Jaeger, Mark Mitchell, System V Application Binary Interface. AMD64 Archi-
tecture Processor Supplement, (2013)] 23 .
Les 6 premiers arguments sont passés dans les registres RDI, RSI, RDX, RCX, R8, R9
et les autres—par la pile.
Donc le pointeur sur la chaîne est passé dans EDI (la partie 32-bit du registre). Mais
pourquoi ne pas utiliser la partie 64-bit, RDI ?
Il est important de garder à l’esprit que toutes les instructions MOV en mode 64-bit qui
écrivent quelque chose dans la partie 32-bit inférieuaer du registre efface également
22. Aussi disponible en https://software.intel.com/sites/default/files/article/402129/
mpx-linux64-abi.pdf
23. Aussi disponible en https://software.intel.com/sites/default/files/article/402129/
mpx-linux64-abi.pdf
21
les 32-bit supérieurs (comme indiqué dans les manuels Intel: 12.1.4 on page 1327).
I.e., l’instruction MOV EAX, 011223344h écrit correctement une valeur dans RAX, puisque
que les bits supérieurs sont mis à zéro.
Si nous ouvrons le fichier objet compilé (.o), nous pouvons voir tous les opcodes des
instructions 24 :
Comme on le voit, l’instruction qui écrit dans EDI en 0x4004D4 occupe 5 octets. La
même instruction qui écrit une valeur sur 64-bit dans RDI occupe 7 octets. Il semble
que GCC essaye d’économiser un peu d’espace. En outre, cela permet d’être sûr
que le segment de données contenant la chaîne ne sera pas alloué à une adresse
supérieure à 4 GiB.
Nous voyons aussi que le registre EAX est mis à zéro avant l’appel à la fonction
printf(). Ceci, car conformément à l’ABI25 standard mentionnée plus haut, le nombre
de registres vectoriel utilisés est passé dans EAX sur les systèmes *NIX en x86-64.
Lorsque notre exemple est compilé sous MSVC 2013 avec l’option /MD (générant un
exécutable plus petit du fait du lien avec MSVCR*.DLL), la fonction main() vient en
premier et est trouvée facilement:
24. Ceci doit être activé dans Options → Disassembly → Number of opcode bytes
25. Application Binary Interface
22
Fig. 1.4: Hiew
23
Fig. 1.5: Hiew
Hiew montre la chaîne «ello, world ». Et lorsque nous lançons l’exécutable modifié,
la chaîne raccourcie est affichée.
Le fichier binaire que j’obtiens en compilant notre exemple avec GCC 5.4.0 sur un
système Linux x64 contient de nombreuses autres chaînes: la plupart sont des noms
de fonction et de bibliothèque importées.
Je lance objdump pour voir le contenu de toutes les sections du fichier compilé:
$ objdump -s a.out
24
400274 04000000 14000000 03000000 474e5500 ............GNU.
400284 fe461178 5bb710b4 bbf2aca8 5ec1ec10 .F.x[.......^...
400294 cf3f7ae4 .?z.
...
int main()
{
printf(0x400238) ;
return 0;
}
1.5.3 ARM
Pour mes expérimentations avec les processeurs ARM, différents compilateurs ont
été utilisés:
• Très courant dans le monde de l’embarqué: Keil Release 6/2013.
26
• Apple Xcode 4.6.3 IDE avec le compilateur LLVM-GCC 4.2
• GCC 4.9 (Linaro) (pour ARM64), disponible comme exécutable win32 ici http:
//www.linaro.org/projects/armv8/.
C’est du code ARM 32-bit qui est utilisé (également pour les modes Thumb et Thumb-
2) dans tous les cas dans ce livre, sauf mention contraire.
25
pour nous de voir les instructions «telles quelles », nous regardons le résultat compilé
dans IDA.
Dans l’exemple, nous voyons facilement que chaque instruction a une taille de 4
octets. En effet, nous avons compilé notre code en mode ARM, pas pour Thumb.
La toute première instruction, STMFD SP!, {R4,LR}28 , fonctionne comme une ins-
truction PUSH en x86, écrivant la valeur de deux registres (R4 et LR) sur la pile.
En effet, dans le listing de la sortie du compilateur armcc, dans un souci de simplifica-
tion, il montre l’instruction PUSH {r4,lr}. Mais ce n’est pas très précis. L’instruction
PUSH est seulement disponible dans le mode Thumb. Donc, pour rendre les choses
moins confuses, nous faisons cela dans IDA.
Cette instruction décrémente d’abord le pointeur de pile SP30 pour qu’il pointe sur de
l’espace libre pour de nouvelles entrées, ensuite elle sauve les valeurs des registres
R4 et LR à cette adresse.
Cette instruction (comme l’instruction PUSH en mode Thumb) est capable de sauve-
garder plusieurs valeurs de registre à la fois, ce qui peut être très utile. À propos, elle
n’a pas d’équivalent en x86. On peut noter que l’instruction STMFD est une générali-
sation de l’instruction PUSH (étendant ses fonctionnalités), puisqu’elle peut travailler
avec n’importe quel registre, pas seulement avec SP. En d’autres mots, l’instruction
STMFD peut être utilisée pour stocker un ensemble de registres à une adresse don-
née.
L’instruction ADR R0, aHelloWorld ajoute ou soustrait la valeur dans le registre
PC31 à l’offset où la chaîne hello, world se trouve. On peut se demander comment
le registre PC est utilisé ici ? C’est appelé du «code indépendant de la position »32 .
Un tel code peut être exécuté à n’importe quelle adresse en mémoire. En d’autres
mots, c’est un adressage PC-relatif. L’instruction ADR prend en compte la différence
entre l’adresse de cette instruction et l’adresse où est située la chaîne. Cette diffé-
rence (offset) est toujours la même, peu importe à quelle adresse notre code est char-
gé par l’OS. C’est pourquoi tout ce dont nous avons besoin est d’ajouter l’adresse de
l’instruction courante (du PC) pour obtenir l’adresse absolue en mémoire de notre
chaîne C.
28. STMFD29
30. pointeur de pile. SP/ESP/RSP dans x86/x64. SP dans ARM.
31. Program Counter. IP/EIP/RIP dans x86/64. PC dans ARM.
32. Lire à ce propos la section( 6.4.1 on page 983)
26
L’instruction BL __2printf33 appelle la fonction printf(). Voici comment fonctionne
cette instruction:
• sauve l’adresse suivant l’instruction BL (0xC) dans LR ;
• puis passe le contrôle à printf() en écrivant son adresse dans le registre PC.
Lorsque la fonction printf() termine son exécution elle doit avoir savoir où elle doit
redonner le contrôle. C’est pourquoi chaque fonction passe le contrôle à l’adresse
se trouvant dans le registre LR.
C’est une différence entre un processeur RISC «pur » comme ARM et un processeur
CISC34 comme x86, où l’adresse de retour est en général sauvée sur la pile. Pour
aller plus loin, lire la section ( 1.9 on page 42) suivante.
À propos, une adresse absolue ou un offset de 32-bit ne peuvent être encodés dans
l’instruction 32-bit BL car il n’y a qu’un espace de 24 bits. Comme nous devons
nous en souvenir, toutes les instructions ont une taille de 4 octets (32 bits). Par
conséquent, elles ne peuvent se trouver qu’à des adresses alignées dur des limites
de 4 octets. Cela implique que les 2 derniers bits de l’adresse d’une instruction (qui
sont toujours des bits à zéro) peuvent être omis. En résumé, nous avons 26 bits pour
encoder l’offset. C’est assez pour encoder current_P C ± ≈ 32M .
Ensuite, l’instruction MOV R0, #035 écrit juste 0 dans le registre R0. C’est parce que
notre fonction C renvoie 0 et la valeur de retour doit être mise dans le registre R0.
La dernière instruction est LDMFD SP!, R4,PC36 . Elle prend des valeurs sur la pile (ou
de toute autre endroit en mémoire) afin de les sauver dans R4 et PC, et incrémente
le pointeur de pile SP. Cela fonctionne ici comme POP.
N.B. La toute première instruction STMFD a sauvé la paire de registres R4 et LR sur
la pile, mais R4 et PC sont restaurés pendant l’exécution de LDMFD.
Comme nous le savons déjà, l’adresse où chaque fonction doit redonner le contrôle
est usuellement sauvée dans le registre LR. La toute première instruction sauve sa
valeur sur la pile car le même registre va être utilisé par notre fonction main() lors
de l’appel à printf(). A la fin de la fonction, cette valeur peut être écrite directe-
ment dans le registre PC, passant ainsi le contrôle là où notre fonction a été appelée.
Comme main() est en général la première fonction en C/C++, le contrôle sera re-
donné au chargeur de l’OS ou a un point dans un CRT, ou quelque chose comme
ça.
Tout cela permet d’omettre l’instruction BX LR à la fin de la fonction.
DCB est une directive du langage d’assemblage définissant un tableau d’octets ou
des chaînes ASCII, proche de la directive DB dans le langage d’assemblage x86.
27
armcc.exe --thumb --c90 -O0 1.c
Nous pouvons repérer facilement les opcodes sur 2 octets (16-bit). C’est, comme
déjà noté, Thumb. L’instruction BL, toutefois, consiste en deux instructions 16-bit.
C’est parce qu’il est impossible de charger un offset pour la fonction printf() en
utilisant seulement le petit espace dans un opcode 16-bit. Donc, la première instruc-
tion 16-bit charge les 10 bits supérieurs de l’offset et la seconde instruction les 11
bits inférieurs de l’offset.
Comme il a été écrit, toutes les instructions en mode Thumb ont une taille de 2
octets (ou 16 bits). Cela implique qu’il impossible pour une instruction Thumb d’être
à une adresse impaire, quelle qu’elle soit. En tenant compte de cela, le dernier bit
de l’adresse peut être omis lors de l’encodage des instructions.
En résumé, l’instruction Thumb BL peut encoder une adresse en current_P C ± ≈ 2M .
Comme pour les autres instructions dans la fonction: PUSH et POP fonctionnent ici
comme les instructions décrites STMFD/LDMFD seul le registre SP n’est pas mentionné
explicitement ici. ADR fonctionne comme dans l’exemple précédent. MOVS écrit 0 dans
le registre R0 afin de renvoyer zéro.
Xcode 4.6.3 sans l’option d’optimisation produit beaucoup de code redondant c’est
pourquoi nous allons étudier le code généré avec optimisation, où le nombre d’ins-
truction est aussi petit que possible, en mettant l’option -O3 du compilateur.
Listing 1.26: avec optimisation Xcode 4.6.3 (LLVM) (Mode ARM)
__text :000028C4 _hello_world
__text :000028C4 80 40 2D E9 STMFD SP !, {R7,LR}
__text :000028C8 86 06 01 E3 MOV R0, #0x1686
__text :000028CC 0D 70 A0 E1 MOV R7, SP
__text :000028D0 00 00 40 E3 MOVT R0, #0
__text :000028D4 00 00 8F E0 ADD R0, PC, R0
__text :000028D8 C3 05 00 EB BL _puts
__text :000028DC 00 00 A0 E3 MOV R0, #0
__text :000028E0 80 80 BD E8 LDMFD SP !, {R7,PC}
28
Les instructions STMFD et LDMFD nous sont déjà familières.
L’instruction MOV écrit simplement le nombre 0x1686 dans le registre R0. C’est l’offset
pointant sur la chaîne «Hello world! ».
Le registre R7 (tel qu’il est standardisé dans [iOS ABI Function Call Guide, (2010)]38 )
est un pointeur de frame. Voir plus loin.
L’instruction MOVT R0, #0 (MOVe Top) écrit 0 dans les 16 bits de poids fort du registre.
Le problème ici est que l’instruction générique MOV en mode ARM peut n’écrire que
dans les 16 bits de poids faible du registre.
Il faut garder à l’esprit que tout les opcodes d’instruction en mode ARM sont limités
en taille à 32 bits. Bien sûr, cette limitation n’est pas relative au déplacement de
données entre registres. C’est pourquoi une instruction supplémentaire existe MOVT
pour écrire dans les bits de la partie haute (de 16 à 31 inclus). Son usage ici, toutefois,
est redondant car l’instruction MOV R0, #0x1686 ci dessus a effacé la partie haute
du registre. C’est soi-disant un défaut du compilateur.
L’instruction ADD R0, PC, R0 ajoute la valeur dans PC à celle de R0, pour calculer
l’adresse absolue de la chaîne «Hello world! ». Comme nous l’avons déjà vu, il s’agit
de «code indépendant de la position » donc la correction est essentielle ici.
L’instruction BL appelle la fonction puts() au lieu de printf().
LLVM a remplacé le premier appel à printf() par un à puts(). Effectivement: printf()
avec un unique argument est presque analogue à puts().
Presque, car les deux fonctions produisent le même résultat uniquement dans le cas
où la chaîne ne contient pas d’identifiants de format débutant par %. Dans le cas où
elle en contient, l’effet de ces deux fonctions est différent39 .
Pourquoi est-ce que le compilateur a remplacé printf() par puts() ? Probablement
car puts() est plus rapide40 .
Car il envoie seulement les caractères dans sortie standard sans comparer chacun
d’entre eux avec le symbole %.
Ensuite, nous voyons l’instruction familière MOV R0, #0 pour mettre le registre R0 à
0.
Par défaut Xcode 4.6.3 génère du code pour Thumb-2 de cette manière:
29
__text :00002B74 C0 F2 00 00 MOVT.W R0, #0
__text :00002B78 78 44 ADD R0, PC
__text :00002B7A 01 F0 38 EA BLX _puts
__text :00002B7E 00 20 MOVS R0, #0
__text :00002B80 80 BD POP {R7,PC}
...
Les instructions BL et BLX en mode Thumb, comme on s’en souvient, sont encodées
comme une paire d’instructions 16 bits. En Thumb-2 ces opcodes substituts sont
étendus de telle sorte que les nouvelles instructions puissent être encodées comme
des instructions 32-bit.
C’est évident en considérant que les opcodes des instructions Thumb-2 commencent
toujours avec 0xFx ou 0xEx.
Mais dans le listing d’IDA les octets d’opcodes sont échangés car pour le processeur
ARM les instructions sont encodées comme ceci: dernier octet en premier et ensuite
le premier (pour les modes Thumb et Thumb-2) ou pour les instructions en mode
ARM le quatrième octet vient en premier, ensuite le troisième, puis le second et
enfin le premier (à cause des différents endianness).
C’est ainsi que les octets se trouvent dans le listing d’IDA:
• pour les modes ARM et ARM64: 4-3-2-1;
• pour le mode Thumb: 2-1;
• pour les paires d’instructions 16-bit en mode Thumb-2: 2-1-4-3.
Donc, comme on peut le voir, les instructions MOVW, MOVT.W et BLX commencent par
0xFx.
Une des instructions Thumb-2 est MOVW R0, #0x13D8 —elle stocke une valeur 16-bit
dans la partie inférieure du registre R0, effaçant les bits supérieurs.
Aussi, MOVT.W R0, #0 fonctionne comme MOVT de l’exemple précédent mais il fonc-
tionne en Thumb-2.
Parmi les autres différences, l’instruction BLX est utilisée dans ce cas à à la place de
BL.
La différence est que, en plus de sauver RA41 dans le registre LR et de passer le
contrôle à la fonction puts(), le processeur change du mode Thumb/Thumb-2 au
mode ARM (ou inversement).
Cette instruction est placée ici, car l’instruction à laquelle est passée le contrôle
ressemble à (c’est encodé en mode ARM) :
__symbolstub1 :00003FEC _puts ; CODE XREF: _hello_world+E
__symbolstub1 :00003FEC 44 F0 9F E5 LDR PC, =__imp__puts
30
Il s’agit principalement d’un saut à l’endroit où l’adresse de puts() est écrit dans la
section import.
Mais alors, le lecteur attentif pourrait demander: pourquoi ne pas appeler puts()
depuis l’endroit dans le code où on en a besoin ?
Parce que ce n’est pas très efficace en terme d’espace.
Presque tous les programmes utilisent des bibliothèques dynamiques externes (comme
les DLL sous Windows, les .so sous *NIX ou les .dylib sous Mac OS X). Les biblio-
thèques dynamiques contiennent les bibliothèques fréquemment utilisées, incluant
la fonction C standard puts().
Dans un fichier binaire exécutable (Windows PE .exe, ELF ou Mach-O) se trouve une
section d’import. Il s’agit d’une liste des symboles (fonctions ou variables globales)
importées depuis des modules externes avec le nom des modules eux-même.
Le chargeur de l’OS charge tous les modules dont il a besoin, tout en énumérant
les symboles d’import dans le module primaire, il détermine l’adresse correcte de
chaque symbole.
Dans notre cas, __imp__puts est une variable 32-bit utilisée par le chargeur de l’OS
pour sauver l’adresse correcte d’une fonction dans une bibliothèque externe. Ensuite
l’instruction LDR lit la valeur 32-bit depuis cette variable et l’écrit dans le registre PC,
lui passant le contrôle.
Donc, pour réduire le temps dont le chargeur de l’OS à besoin pour réaliser cette
procédure, c’est une bonne idée d’écrire l’adresse de chaque symbole une seule
fois, à une place dédiée.
À côté de ça, comme nous l’avons déjà compris, il est impossible de charger une
valeur 32-bit dans un registre en utilisant seulement une instruction sans un accès
mémoire.
Donc, la solution optimale est d’allouer une fonction séparée fonctionnant en mode
ARM avec le seul but de passer le contrôle à la bibliothèque dynamique et ensuite de
sauter à cette petite fonction d’une instruction (ainsi appelée fonction thunk) depuis
le code Thumb.
À propos, dans l’exemple précédent (compilé en mode ARM), le contrôle est pas-
sé par BL à la même fonction thunk. Le mode du processeur, toutefois, n’est pas
échangé (d’où l’absence d’un «X » dans le mnémonique de l’instruction).
Les fonctions thunk sont difficile à comprendre, apparemment, à cause d’un mau-
vais nom. La manière la plus simple est de les voir comme des adaptateurs ou des
convertisseurs d’un type jack à un autre. Par exemple, un adaptateur permettant
l’insertion d’un cordon électrique britannique sur une prise murale américaine, ou
vice-versa. Les fonctions thunk sont parfois appelées wrappers.
Voici quelques autres descriptions de ces fonctions:
31
“Un morceau de code qui fournit une adresse:”, d’après P. Z. In-
german, qui inventa thunk en 1961 comme un moyen de lier les para-
mètres réels à leur définition formelle dans les appels de procédures en
Algol-60. Si une procédure est appelée avec une expression à la place
d’un paramètre formel, le compilateur génère un thunk qui calcule l’ex-
pression et laisse l’adresse du résultat dans une place standard.
…
Microsoft et IBM ont tous les deux défini, dans systèmes basés sur
Intel, un ”environnement 16-bit” (avec leurs horribles registres de seg-
ment et la limite des adresses à 64K) et un ”environnement 32-bit”
(avec un adressage linéaire et une gestion semi-réelle de la mémoire).
Les deux environnements peuvent fonctionner sur le même ordinateur
et OS (grâce à ce qui est appelé, dans le monde Microsoft, WOW qui
signifie Windows dans Windows). MS et IBM ont tous deux décidé que
le procédé de passer de 16-bit à 32-bit et vice-versa est appelé un
”thunk”; pour Window 95, il y a même un outil, THUNK.EXE, appelé un
”compilateur thunk”.
ARM64
GCC
32
4 400598: 90000000 adrp x0, 400000 <_init-0x3b8>
5 40059c : 91192000 add x0, x0, #0x648
6 4005a0 : 97ffffa0 bl 400420 <puts@plt>
7 4005a4 : 52800000 mov w0, #0x0 // #0
8 4005a8 : a8c17bfd ldp x29, x30, [sp],#16
9 4005ac : d65f03c0 ret
10
11 ...
12
13 Contents of section .rodata :
14 400640 01000200 00000000 48656c6c 6f210a00 ........Hello !..
Il n’y a pas de mode Thumb ou Thumb-2 en ARM64, seulement en ARM, donc il n’y
a que des instructions 32-bit. Le nombre de registres a doublé: .2.4 on page 1363.
Les registres 64-bit ont le préfixe X-, tandis que leurs partie 32-bit basse—W-.
L’instruction STP (Store Pair stocke une paire) sauve deux registres sur la pile simul-
tanément: X29 et X30.
Bien sûr, cette instruction peut sauvegarder cette paire à n’importe quelle endroit
en mémoire, mais le registre SP est spécifié ici, donc la paire est sauvé sur le pile.
Les registres ARM64 font 64-bit, chacun a une taille de 8 octets, donc il faut 16 octets
pour sauver deux registres.
Le point d’exclamation (“!”) après l’opérande signifie que 16 octets doivent d’abord
être soustrait de SP, et ensuite les valeurs de la paire de registres peuvent être
écrites sur la pile. Ceci est appelé le pre-index. À propos de la différence entre post-
index et pre-index lisez ceci: 1.39.2 on page 567.
Dans la gamme plus connue du x86, la première instruction est analogue à la paire
PUSH X29 et PUSH X30. En ARM64, X29 est utilisé comme FP42 et X30 comme LR,
c’est pourquoi ils sont sauvegardés dans le prologue de la fonction et remis dans
l’épilogue.
La seconde instruction copie SP dans X29 (ou FP). Cela sert à préparer la pile de la
fonction.
Les instructions ADRP et ADD sont utilisées pour remplir l’adresse de la chaîne «Hello! »
dans le registre X0, car le premier argument de la fonction est passé dans ce registre.
Il n’y a pas d’instruction, quelqu’elle soit, en ARM qui puisse stocker un nombre large
dans un registre (car la longueur des instructions est limitée à 4 octets, cf: 1.39.3 on
page 569). Plusieurs instructions doivent donc être utilisées. La première instruction
(ADRP) écrit l’adresse de la page de 4KiB, où se trouve la chaîne, dans X0, et la
seconde (ADD) ajoute simplement le reste de l’adresse. Plus d’information ici: 1.39.4
on page 571.
0x400000 + 0x648 = 0x400648, et nous voyons notre chaîne C «Hello! » dans le
.rodata segment des données à cette adresse.
puts() est appelée après en utilisant l’instruction BL. Cela a déjà été discuté: 1.5.3
on page 29.
42. Frame Pointer
33
MOV écrit 0 dans W0. W0 est la partie basse 32 bits du registre 64-bit X0 :
Partie 32 bits haute Partie 32 bits basse
X0
W0
Le résultat de la fonction est retourné via X0 et main renvoie 0, donc c’est ainsi que
la valeur de retour est préparée. Mais pourquoi utiliser la partie 32-bit?
Parce que le type de donnée int en ARM64, tout comme en x86-64, est toujours
32-bit, pour une meilleure compatibilité.
Donc si la fonction renvoie un int 32-bit, seul les 32 premiers bits du registre X0
doivent être remplis.
Pour vérifier ceci, changeons un peu cet exemple et recompilons-le. Maintenant,
main() renvoie une valeur sur 64-bit:
uint64_t main()
{
printf ("Hello !\n") ;
return 0;
}
Le résultat est le même, mais c’est à quoi ressemble MOV à cette ligne maintenant:
1.5.4 MIPS
Un mot à propos du «pointeur global »
Un concept MIPS important est le «pointeur global ». Comme nous le savons déjà,
chaque instruction MIPS a une taille de 32-bit, donc il est impossible d’avoir une
adresse 32-bit dans une instruction: il faut pour cela utiliser une paire. (comme le
34
fait GCC dans notre exemple pour le chargement de l’adresse de la chaîne de texte).
Il est possible, toutefois, de charger des données depuis une adresse dans l’interval
register − 32768...register + 32767 en utilisant une seule instruction (car un offset signé
de 16 bits peut être encodé dans une seule instruction). Nous pouvons alors allouer
un registre dans ce but et dédier un bloc de 64KiB pour les données les plus utili-
sées. Ce registre dédié est appelé un «pointeur global » et il pointe au milieu du
bloc de 64 KiB. Ce bloc contient en général les variables globales et les adresses
des fonctions importées, comme printf(), car les développeurs de GCC ont déci-
dé qu’obtenir l’adresse d’une fonction devait se faire en une instruction au lieu de
deux. Dans un fichier ELF ce bloc de 64KiB se trouve en partie dans une section .sbss
(«small BSS43 ») pour les données non initialisées et .sdata («small data ») pour celles
initialisées. Cela implique que le programmeur peut choisir quelle donnée il/elle sou-
haite rendre accessible rapidement et doit les stocker dans .sdata/.sbss. Certains
programmeurs old-school peuvent se souvenir du modèle de mémoire MS-DOS 11.6
on page 1309 ou des gestionnaires de mémoire MS-DOS comme XMS/EMS où toute
la mémoire était divisée en bloc de 64KiB.
Ce concept n’est pas restreint à MIPS. Au moins les PowerPC utilisent aussi cette
technique.
35
26 addiu $sp,$sp,32 ; slot de retard de branchement + libérer la pile
locale
Comme on le voit, le registre $GP est défini dans le prologue de la fonction pour
pointer au milieu de ce bloc. Le registre RA est sauvé sur la pile locale. puts() est
utilisé ici au lieu de printf(). L’adresse de la fonction puts() est chargée dans $25
en utilisant l’instruction LW («Load Word »). Ensuite l’adresse de la chaîne de texte
est chargée dans $4 avec la paire d’instructions LUI ((«Load Upper Immediate »)
et ADDIU («Add Immediate Unsigned Word »). LUI défini les 16 bits de poids fort du
registre (d’où le mot «upper » dans le nom de l’instruction) et ADDIU ajoute les 16
bits de poids faible de l’adresse.
ADDIU suit JALR (vous n’avez pas déjà oublié le slot de délai de branchement ?). Le
registre $4 est aussi appelé $A0, qui est utilisé pour passer le premier argument
d’une fonction 44 .
JALR (« Jump and Link Register ») saute à l’adresse stockée dans le registre $25
(adresse de puts()) en sauvant l’adresse de la prochaine instruction (LW) dans RA.
C’est très similaire à ARM. Oh, encore une chose importante, l’adresse sauvée dans
RA n’est pas l’adresse de l’instruction suivante (car c’est celle du slot de délai et
elle est exécutée avant l’instruction de saut), mais l’adresse de l’instruction après la
suivante (après le slot de délai). Par conséquent, P C + 8 est écrit dans RA pendant
l’exécution de JALR, dans notre cas, c’est l’adresse de l’instruction LW après ADDIU.
LW («Load Word ») à la ligne 20 restaure RA depuis la pile locale (cette instruction
fait partie de l’épilogue de la fonction).
MOVE à la ligne 22 copie la valeur du registre $0 ($ZERO) dans $2 ($V0).
MIPS a un registre constant, qui contient toujours zéro. Apparemment, les dévelop-
peurs de MIPS avaient à l’esprit que zéro est la constante la plus utilisée en program-
mation, utilisons donc le registre $0 à chaque fois que zéro est requis.
Un autre fait intéressant est qu’il manque en MIPS une instruction qui transfère
des données entre des registres. En fait, MOVE DST, SRC est ADD DST, SRC, $ZERO
(DST = SRC + 0), qui fait la même chose. Manifestement, les développeurs de MIPS
voulaient une table des opcodes compacte. Cela ne signifie pas qu’il y a une ad-
dition à chaque instruction MOVE. Très probablement, le CPU optimise ces pseudo-
instructions et l’UAL45 n’est jamais utilisé.
J à la ligne 24 saute à l’adresse dans RA, qui effectue effectivement un retour de
la fonction. ADDIU après J est en fait exécutée avant J (vous vous rappeler du slot
de délai de branchement ?) et fait partie de l’épilogue de la fonction. Voici un listing
généré par IDA. Chaque registre a son propre pseudo nom:
44. La table des registres MIPS est disponible en appendice .3.1 on page 1364
45. Unité arithmétique et logique
36
6 ; prologue de la fonction.
7 ; définir GP:
8 .text :00000000 lui $gp, (__gnu_local_gp >> 16)
9 .text :00000004 addiu $sp, -0x20
10 .text :00000008 la $gp, (__gnu_local_gp & 0xFFFF)
11 ; sauver RA sur la pile locale:
12 .text :0000000C sw $ra, 0x20+var_4($sp)
13 ; sauver GP sur la pile locale:
14 ; pour une raison, cette instruction manque dans la sortie en assembleur de
GCC:
15 .text :00000010 sw $gp, 0x20+var_10($sp)
16 ; charger l'adresse de la fonction puts() dans $9 depuis GP:
17 .text :00000014 lw $t9, (puts & 0xFFFF)($gp)
18 ; générer l'adresse de la chaîne de texte dans $a0:
19 .text :00000018 lui $a0, ($LC0 >> 16) # "Hello, world!"
20 ; sauter à puts(), en sauvant l'adresse de retour dans le register link:
21 .text :0000001C jalr $t9
22 .text :00000020 la $a0, ($LC0 & 0xFFFF) # "Hello,
world!"
23 ; restaurer RA:
24 .text :00000024 lw $ra, 0x20+var_4($sp)
25 ; copier 0 depuis $zero dans $v0:
26 .text :00000028 move $v0, $zero
27 ; retourner en sautant à la valeur dans RA:
28 .text :0000002C jr $ra
29 ; épilogue de la fonction:
30 .text :00000030 addiu $sp, 0x20
46. Apparemment, les fonctions générant les listings ne sont pas si critique pour les utilisateurs de GCC,
donc des erreurs peuvent toujours subsister.
37
11 ; définir GP:
12 lui $28,%hi(__gnu_local_gp)
13 addiu $28,$28,%lo(__gnu_local_gp)
14 ; charger l'adresse de la chaîne de texte:
15 lui $2,%hi($LC0)
16 addiu $4,$2,%lo($LC0)
17 ; charger l'adresse de puts() en utilisant GP:
18 lw $2,%call16(puts)($28)
19 nop
20 ; appeler puts() :
21 move $25,$2
22 jalr $25
23 nop ; slot de retard de branchement
24
25 ; restaurer GP depuis la pile locale:
26 lw $28,16($fp)
27 ; mettre le registre $2 ($V0) à zéro:
28 move $2,$0
29 ; épilogue de la fonction.
30 ; restaurer SP:
31 move $sp,$fp
32 ; restaurer RA:
33 lw $31,28($sp)
34 ; restaurer FP:
35 lw $fp,24($sp)
36 addiu $sp,$sp,32
37 ; sauter en RA:
38 j $31
39 nop ; slot de délai de branchement
Nous voyons ici que le registre FP est utilisé comme un pointeur sur la pile. Nous
voyons aussi 3 NOPs. Le second et le troisième suivent une instruction de branche-
ment. Peut-être que le compilateur GCC ajoute toujours des NOPs (à cause du slot de
retard de branchement) après les instructions de branchement, et, si l’optimisation
est demandée, il essaye alors de les éliminer. Donc, dans ce cas, ils sont laissés en
place.
Voici le listing IDA :
38
14 ; définir GP:
15 .text :00000010 la $gp, __gnu_local_gp
16 .text :00000018 sw $gp, 0x20+var_10($sp)
17 ; charger l'adresse de la chaîne de texte:
18 .text :0000001C lui $v0, (aHelloWorld >> 16) # "Hello,
world!"
19 .text :00000020 addiu $a0, $v0, (aHelloWorld & 0xFFFF) #
"Hello, world!"
20 ; charger l'adresse de puts() en utilisant GP:
21 .text :00000024 lw $v0, (puts & 0xFFFF)($gp)
22 .text :00000028 or $at, $zero ; NOP
23 ; appeler puts() :
24 .text :0000002C move $t9, $v0
25 .text :00000030 jalr $t9
26 .text :00000034 or $at, $zero ; NOP
27 ; restaurer GP depuis la pile locale:
28 .text :00000038 lw $gp, 0x20+var_10($fp)
29 ; mettre le registre $2 ($V0) à zéro:
30 .text :0000003C move $v0, $zero
31 ; épilogue de la fonction.
32 ; restaurer SP:
33 .text :00000040 move $sp, $fp
34 ; restaurer RA:
35 .text :00000044 lw $ra, 0x20+var_4($sp)
36 ; restaurer FP:
37 .text :00000048 lw $fp, 0x20+var_8($sp)
38 .text :0000004C addiu $sp, 0x20
39 ; sauter en RA:
40 .text :00000050 jr $ra
41 .text :00000054 or $at, $zero ; NOP
Intéressant, IDA a reconnu les instructions LUI/ADDIU et les a agrégées en une pseu-
do instruction LA («Load Address ») à la ligne 15. Nous pouvons voir que cette pseudo
instruction a une taille de 8 octets! C’est une pseudo instruction (ou macro) car ce
n’est pas une instruction MIPS réelle, mais plutôt un nom pratique pour une paire
d’instructions.
Une autre chose est qu’IDA ne reconnaît pas les instructions NOP, donc ici elles
se trouvent aux lignes 22, 26 et 41. C’est OR $AT, $ZERO. Essentiellement, cette
instruction applique l’opération OR au contenu du registre $AT avec zéro, ce qui,
bien sûr, est une instruction sans effet. MIPS, comme beaucoup d’autres ISAs, n’a
pas une instruction NOP.
L’adresse de la chaîne de texte est passée dans le registre. Pourquoi définir une pile
locale quand même? La raison de cela est que la valeur des registres RA et GP doit
être sauvée quelque part (car printf() est appelée), et que la pile locale est utilisée
pour cela. Si cela avait été une fonction leaf, il aurait été possible de se passer du
prologue et de l’épilogue de la fonction, par exemple: 1.4.3 on page 11.
39
Listing 1.35: extrait d’une session GDB
root@debian-mips :~# gcc hw.c -O3 -o hw
1.5.5 Conclusion
La différence principale entre le code x86/ARM et x64/ARM64 est que le pointeur sur
la chaîne a une taille de 64 bits. Le fait est que les CPUs modernes sont maintenant
64-bit à cause le la baisse du coût de la mémoire et du grand besoin de cette dernière
par les applications modernes. Nous pouvons ajouter bien plus de mémoire à nos
ordinateurs que les pointeurs 32-bit ne peuvent en adresser. Ainsi, tous les pointeurs
sont maintenant 64-bit.
40
1.5.6 Exercices
• http://challenges.re/48
• http://challenges.re/49
Ce que ces instructions font: sauvent la valeur du registre EBP dans la pile (push
ebp), sauvent la valeur actuelle du registre ESP dans le registre EBP (mov ebp, esp)
et enfin allouent de la mémoire dans la pile pour les variables locales de la fonction
(sub esp, X).
La valeur du registre EBP reste la même durant la période où la fonction s’exécute
et est utilisée pour accéder aux variables locales et aux arguments de la fonction.
Le registre ESP peut aussi être utilisé pour accéder aux variables locales et aux ar-
guments de la fonction, cependant cette approche n’est pas pratique car sa valeur
est susceptible de changer au cours de l’exécution de cette fonction.
L’épilogue de fonction libère la mémoire allouée dans la pile (mov esp, ebp), restaure
l’ancienne valeur de EBP précédemment sauvegardée dans la pile (pop ebp) puis
rend l’exécution à l’appelant (ret 0).
mov esp, ebp
pop ebp
ret 0
Les prologues et épilogues de fonction sont généralement détectés par les désas-
sembleurs pour déterminer où une fonction commence et où elle se termine.
1.6.1 Récursivité
Les prologues et épilogues de fonction peuvent affecter négativement les perfor-
mances de la récursion.
Plus d’information sur la récursivité dans ce livre: 3.7.3 on page 629.
41
Listing 1.36: GCC 8.2 x64 sans optimisation (résultat en sortie de l’assembleur)
f :
push rbp
mov rbp, rsp
nop
pop rbp
ret
C’est RET, mais le prologue et l’épilogue de la fonction, probablement, n’ont pas été
optimisés et laissés tels quels. NOP semble être un autre artefact du compilateur. De
toutes façons, la seule instruction effective ici est RET. Toutes les autres instructions
peuvent être supprimées (ou optimisées).
Listing 1.37: GCC 8.2 x64 sans optimisation (résultat en sortie de l’assembleur)
f :
push rbp
mov rbp, rsp
mov eax, 123
pop rbp
ret
Les seules instructions efficaces ici sont MOV et RET, les autres sont – prologue et
épilogue.
1.9 Pile
La pile est une des structures de données les plus fondamentales en informatique
47
. AKA48 LIFO49 .
Techniquement, il s’agit d’un bloc de mémoire situé dans l’espace d’adressage d’un
processus et qui est utilisé par le registre ESP en x86, RSP en x64 ou par le registre
SP en ARM comme un pointeur dans ce bloc mémoire.
Les instructions d’accès à la pile sont PUSH et POP (en x86 ainsi qu’en ARM Thumb-
mode). PUSH soustrait à ESP/RSP/SP 4 en mode 32-bit (ou 8 en mode 64-bit) et
écrit ensuite le contenu de l’opérande associé à l’adresse mémoire pointée par
ESP/RSP/SP.
47. wikipedia.org/wiki/Call_stack
48. Also Known As — Aussi connu sous le nom de
49. Dernier entré, premier sorti
42
POP est l’opération inverse: elle récupère la donnée depuis l’adresse mémoire poin-
tée par SP, l’écrit dans l’opérande associé (souvent un registre) puis ajoute 4 (ou 8)
au pointeur de pile.
Après une allocation sur la pile, le pointeur de pile pointe sur le bas de la pile. PUSH
décrémente le pointeur de pile et POP l’incrémente.
Le bas de la pile représente en réalité le début de la mémoire allouée pour le bloc
de pile. Cela semble étrange, mais c’est comme ça.
ARM supporte à la fois les piles ascendantes et descendantes.
Par exemple les instructions STMFD/LDMFD, STMED50 /LDMED51 sont utilisées pour
gérer les piles descendantes (qui grandissent vers le bas en commençant avec une
adresse haute et évoluent vers une plus basse).
Les instructions STMFA52 /LDMFA53 , STMEA54 /LDMEA55 sont utilisées pour gérer les
piles montantes (qui grandissent vers les adresses hautes de l’espace d’adressage,
en commençant avec une adresse située en bas de l’espace d’adressage).
Heap Pile
Dans [D. M. Ritchie and K. Thompson, The UNIX Time Sharing System, (1974)]56 on
peut lire:
43
a single copy of it is shared among all processes executing the same
program. At the first 8K byte boundary above the program text seg-
ment in the virtual address space begins a nonshared, writable data
segment, the size of which may be extended by a system call. Starting
at the highest address in the virtual address space is a pile segment,
which automatically grows downward as the hardware’s pile pointer
fluctuates.
Cela nous rappelle comment certains étudiants prennent des notes pour deux cours
différents dans un seul et même cahier en prenant un cours d’un côté du cahier,
et l’autre cours de l’autre côté. Les notes de cours finissent par se rencontrer à un
moment dans le cahier quand il n’y a plus de place.
x86
Lorsque l’on appelle une fonction avec une instruction CALL, l’adresse du point exac-
tement après cette dernière est sauvegardée sur la pile et un saut inconditionnel à
l’adresse de l’opérande CALL est exécuté.
L’instruction CALL est équivalente à la paire d’instructions
PUSH address_after_call / JMP operand.
RET va chercher une valeur sur la pile et y saute —ce qui est équivalent à la paire
d’instructions POP tmp / JMP tmp.
Déborder de la pile est très facile. Il suffit de lancer une récursion éternelle:
void f()
{
f() ;
};
ss.cpp
c :\tmp6\ss.cpp(4) : warning C4717 : 'f' : recursive on all control paths⤦
Ç , function will cause runtime stack overflow
44
push ebp
mov ebp, esp
; Line 3
call ?f@@YAXXZ ; f
; Line 4
pop ebp
ret 0
?f@@YAXXZ ENDP ; f
…Si nous utilisons l’option d’optimisation du compilateur (option /Ox) le code optimi-
sé ne va pas déborder de la pile et au lieu de cela va fonctionner correctemment 57 :
?f@@YAXXZ PROC ; f
; Line 2
$LL3@f :
; Line 3
jmp SHORT $LL3@f
?f@@YAXXZ ENDP ; f
GCC 4.4.1 génère un code similaire dans les deux cas, sans, toutefois émettre d’aver-
tissement à propos de ce problème.
ARM
Les programmes ARM utilisent également la pile pour sauver les adresses de retour,
mais différemment. Comme mentionné dans «Hello, world! » ( 1.5.3 on page 25), RA
est sauvegardé dans LR (link register). Si l’on a toutefois besoin d’appeler une autre
fonction et d’utiliser le registre LR une fois de plus, sa valeur doit être sauvegardée.
Usuellement, cela se fait dans le prologue de la fonction.
Souvent, nous voyons des instructions comme PUSH R4-R7,LR en même temps que
cette instruction dans l’épilogue POP R4-R7,PC—ces registres qui sont utilisés dans
la fonction sont sauvegardés sur la pile, LR inclus.
Néanmoins, si une fonction n’appelle jamais d’autre fonction, dans la terminologie
RISC elle est appelée fonction leaf58 . Ceci a comme conséquence que les fonctions
leaf ne sauvegardent pas le registre LR (car elles ne le modifient pas). Si une telle
fonction est petite et utilise un petit nombre de registres, elle peut ne pas utiliser du
tout la pile. Ainsi, il est possible d’appeler des fonctions leaf sans utiliser la pile. Ce
qui peut être plus rapide sur des vieilles machines x86 car la mémoire externe n’est
pas utilisée pour la pile 59 . Cela peut être utile pour des situations où la mémoire
pour la pile n’est pas encore allouée ou disponible.
Quelques exemples de fonctions leaf: 1.14.3 on page 139, 1.14.3 on page 140, 1.281
on page 404, 1.297 on page 427, 1.28.5 on page 427, 1.191 on page 273, 1.189 on
page 271, 1.208 on page 294.
57. ironique ici
58. infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka13785.html
59. Il y a quelque temps, sur PDP-11 et VAX, l’instruction CALL (appel d’autres fonctions) était coû-
teuse; jusqu’à 50% du temps d’exécution pouvait être passé à ça, il était donc considéré qu’avoir un
grand nombre de petites fonctions était un anti-pattern [Eric S. Raymond, The Art of UNIX Programming,
(2003)Chapter 4, Part II].
45
Passage des arguments d’une fonction
Le moyen le plus utilisé pour passer des arguments en x86 est appelé «cdecl » :
push arg3
push arg2
push arg1
call f
add esp, 12 ; 4*3=12
printf() va afficher 1234, et deux autres nombres aléatoires60 , qui sont situés à
côté dans la pile.
C’est pourquoi la façon dont la fonction main() est déclarée n’est pas très impor-
tante: comme main(),
main(int argc, char *argv[]) ou main(int argc, char *argv[], char *envp[]).
En fait, le code-CRT appelle main(), schématiquement, de cette façon:
push envp
push argv
push argc
call main
...
Si vous déclarez main() comme main() sans argument, ils sont néanmoins tou-
jours présents sur la pile, mais ne sont pas utilisés. Si vous déclarez main() as
comme main(int argc, char *argv[]), vous pourrez utiliser les deux premiers
arguments, et le troisième restera «invisible » pour votre fonction. Il est même pos-
sible de déclarer main() comme main(int argc), cela fonctionnera.
Un autre exemple apparenté: 6.1.10.
60. Pas aléatoire dans le sens strict du terme, mais plutôt imprévisibles: ?? on page ??
46
Autres façons de passer les arguments
Il est à noter que rien n’oblige les programmeurs à passer les arguments à travers
la pile. Ce n’est pas une exigence. On peut implémenter n’importe quelle autre mé-
thode sans utiliser du tout la pile.
Une méthode répandue chez les débutants en assembleur est de passer les argu-
ments par des variables globales, comme:
mov X, 123
mov Y, 456
call do_something
...
X dd ?
Y dd ?
61. Correctement implémenté, chaque thread aurait sa propre pile avec ses propres
arguments/variables.
47
msg db 'Hello, World !\$'
C’est presque similaire à la méthode 6.1.3 on page 964. Et c’est aussi très similaire
aux appels systèmes sous Linux ( 6.3.1 on page 982) et Windows.
Si une fonction MS-DOS devait renvoyer une valeur booléenne (i.e., un simple bit,
souvent pour indiquer un état d’erreur), le flag CF était souvent utilisé.
Par exemple:
mov ah, 3ch ; create file
lea dx, filename
mov cl, 1
int 21h
jc error
mov file_handle, ax
...
error :
...
En cas d’erreur, le flag CF est mis. Sinon, le handle du fichier nouvellement créé est
retourné via AX.
Cette méthode est encore utilisée par les programmeurs en langage d’assemblage.
Dans le code source de Windows Research Kernel (qui est très similaire à Windows
2003) nous pouvons trouver quelque chose comme ça (file base/ntos/ke/i386/cpu.asm) :
public Get386Stepping
Get386Stepping proc
G3s05 :
call Check386D1 ; Check for D1 stepping
jc short G3s10 ; if c, it is NOT D1
mov ax, 301h ; It is D1/later stepping
ret
G3s10 :
mov ax, 101h ; assume it is B1 stepping
ret
...
MultiplyTest proc
48
xor cx,cx ; 64K times is a nice round number
mlt00 : push cx
call Multiply ; does this chip's multiply work?
pop cx
jc short mltx ; if c, No, exit
loop mlt00 ; if nc, YEs, loop to try again
clc
mltx :
ret
MultiplyTest endp
Une fonction peut allouer de l’espace sur la pile pour ses variables locales simple-
ment en décrémentant le pointeur de pile vers le bas de la pile.
Donc, c’est très rapide, peu importe combien de variables locales sont définies. Ce
n’est pas une nécessité de stocker les variables locales sur la pile. Vous pouvez les
stocker où bon vous semble, mais c’est traditionnellement fait comme cela.
void f()
{
char *buf=(char*)alloca (600) ;
#ifdef __GNUC__
snprintf (buf, 600, "hi ! %d, %d, %d\n", 1, 2, 3) ; // GCC
62. Avec MSVC, l’implémentation de cette fonction peut être trouvée dans les fichiers alloca16.asm et
chkstk.asm dans
C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\src\intel
49
#else
_snprintf (buf, 600, "hi ! %d, %d, %d\n", 1, 2, 3) ; // MSVC
#endif
puts (buf) ;
};
MSVC
push 3
push 2
push 1
push OFFSET $SG2672
push 600 ; 00000258H
push esi
call __snprintf
push esi
call _puts
add esp, 28
...
63
Le seul argument d’alloca() est passé via EAX (au lieu de le mettre sur la pile) .
GCC 4.4.1 fait la même chose sans effectuer d’appel à des fonctions externes :
63. C’est parce que alloca() est plutôt une fonctionnalité intrinsèque du compilateur ( 11.3 on
page 1302) qu’une fonction normale. Une des raisons pour laquelle nous avons besoin d’une fonction
séparée au lieu de quelques instructions dans le code, est parce que l’implémentation d’alloca() par
MSVC64 a également du code qui lit depuis la mémoire récemment allouée pour laisser l’OS mapper la
mémoire physique vers la VM65 . Aprés l’appel à la fonction alloca(), ESP pointe sur un bloc de 600 octets
que nous pouvons utiliser pour le tableau buf.
50
Listing 1.40: GCC 4.7.3
.LC0 :
.string "hi ! %d, %d, %d\n"
f :
push ebp
mov ebp, esp
push ebx
sub esp, 660
lea ebx, [esp+39]
and ebx, -16 ; align pointer by 16-byte border
mov DWORD PTR [esp], ebx ; s
mov DWORD PTR [esp+20], 3
mov DWORD PTR [esp+16], 2
mov DWORD PTR [esp+12], 1
mov DWORD PTR [esp+8], OFFSET FLAT :.LC0 ; "hi! %d, %d, %d\n"
mov DWORD PTR [esp+4], 600 ; maxlen
call _snprintf
mov DWORD PTR [esp], ebx ; s
call puts
mov ebx, DWORD PTR [ebp-4]
leave
ret
51
Le code est le même que le précédent.
Au fait, movl $3, 20(%esp) correspond à mov DWORD PTR [esp+20], 3 avec la syn-
taxe intel. Dans la syntaxe AT&T, le format registre+offset pour l’adressage mémoire
ressemble à offset(%register).
(Windows) SEH
Les enregistrements SEH66 sont aussi stockés dans la pile (s’ils sont présents). Lire
à ce propos: ( 6.5.3 on page 1005).
Peut-être que la raison pour laquelle les variables locales et les enregistrements SEH
sont stockés dans la pile est qu’ils sont automatiquement libérés quand la fonction se
termine en utilisant simplement une instruction pour corriger la position du pointeur
de pile (souvent ADD). Les arguments de fonction sont aussi désalloués automati-
quement à la fin de la fonction. À l’inverse, toutes les données allouées sur le heap
doivent être désallouées de façon explicite.
52
Dans ce livre les valeurs dites «bruitée » ou «poubelle » présente dans la pile ou
dans la mémoire sont souvent mentionnées.
D’où viennent-elles ? Ces valeurs ont été laissées sur la pile après l’exécution de
fonctions précédentes. Par exemple:
#include <stdio.h>
void f1()
{
int a=1, b=2, c=3;
};
void f2()
{
int a, b, c ;
printf ("%d, %d, %d\n", a, b, c) ;
};
int main()
{
f1() ;
f2() ;
};
Compilons …
Listing 1.42: sans optimisation MSVC 2010
$SG2752 DB '%d, %d, %d', 0aH, 00H
53
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp]
push edx
push OFFSET $SG2752 ; '%d, %d, %d'
call DWORD PTR __imp__printf
add esp, 16
mov esp, ebp
pop ebp
ret 0
_f2 ENDP
_main PROC
push ebp
mov ebp, esp
call _f1
call _f2
xor eax, eax
pop ebp
ret 0
_main ENDP
st.c
c :\polygon\c\st.c(11) : warning C4700 : uninitialized local variable 'c' ⤦
Ç used
c :\polygon\c\st.c(11) : warning C4700 : uninitialized local variable 'b' ⤦
Ç used
c :\polygon\c\st.c(11) : warning C4700 : uninitialized local variable 'a' ⤦
Ç used
Microsoft (R) Incremental Linker Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
/out :st.exe
st.obj
Quel résultat étrange ! Aucune variables n’a été initialisées dans f2(). Ce sont des
valeurs «fantômes » qui sont toujours dans la pile.
54
Chargeons cet exemple dans OllyDbg :
Quand f1() assigne les variable a, b et c, leurs valeurs sont stockées à l’adresse
0x1FF860 et ainsi de suite.
55
Et quand f2() s’exécute:
... a, b et c de la fonction f2() sont situées à la même adresse ! Aucunes autre fonc-
tion n’a encore écrasées ces valeurs, elles sont donc encore inchangées. Pour que
cette situation arrive, il faut que plusieurs fonctions soit appelées les unes après les
autres et que SP soit le même à chaque début de fonction (i.e., les fonctions doivent
avoir le même nombre d’arguments). Les variables locales seront donc positionnées
au même endroit dans la pile. Pour résumer, toutes les valeurs sur la pile sont des
valeurs laissées par des appels de fonction précédents. Ces valeurs laissées sur la
pile ne sont pas réellement aléatoires dans le sens strict du terme, mais elles sont
imprévisibles. Y a t’il une autre option ? Il serait probablement possible de nettoyer
des parties de la pile avant chaque nouvelle exécution de fonction, mais cela engen-
drerait du travail et du temps d’exécution (non nécessaire) en plus.
MSVC 2013
Cet exemple a été compilé avec MSVC 2010. Si vous essayez de compiler cet exemple
avec MSVC 2013 et de l’exécuter, ces 3 nombres seront inversés:
c :\Polygon\c>st
3, 2, 1
Pourquoi ? J’ai aussi compilé cet exemple avec MSVC 2013 et constaté ceci:
56
_a$ = -12 ; size = 4
_b$ = -8 ; size = 4
_c$ = -4 ; size = 4
_f2 PROC
...
_f2 ENDP
...
_f1 ENDP
Contrairement à MSVC 2010, MSVC 2013 alloue les variables a/b/c dans la fonction
f2() dans l’ordre inverse puisqu’il se comporte différemment en raison d’un change-
ment supposé dans son fonctionnement interne.Ceci est correct, car le standard du
C/C++ n’a aucune règle sur l’ordre d’allocation des variables locales sur la pile.
1.9.5 Exercices
• http://challenges.re/51
• http://challenges.re/52
// executable
int main (int argc, char **argv)
{
return boolector_main (argc, argv) ;
}
Pourquoi quelqu’un ferait-il comme ça? Je ne sais pas mais mon hypothèse est que
boolector_main() peut être compilée dans une sorte de DLL ou bibliothèque dyna-
mique, et appelée depuis une suite de test. Certainement qu’une suite de test peut
préparer les variables argc/argv comme le ferait CRT.
Il est intéressant de voir comment c’est compilé:
67. https://boolector.github.io/
57
Listing 1.44: GCC 8.2 x64 sans optimisation (résultat en sortie de l’assembleur)
main :
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR -4[rbp], edi
mov QWORD PTR -16[rbp], rsi
mov rdx, QWORD PTR -16[rbp]
mov eax, DWORD PTR -4[rbp]
mov rsi, rdx
mov edi, eax
call boolector_main
leave
ret
Ceci est OK, le prologue (non optimisé) déplace inutilement deux arguments, CALL,
épilogue, RET. Mais regardons la version optimisée:
Listing 1.45: GCC 8.2 x64 avec optimisation (résultat en sortie de l’assembleur)
main :
jmp boolector_main
Aussi simple que ça: la pile et les registres ne sont pas touchés et boolector_main()
a le même ensemble d’arguments. Donc, tout ce que nous avons à faire est de passer
l’exécution à une autre adresse.
Ceci est proche d’une fonction thunk.
Nous verons queelque chose de plus avancé plus tard: 1.11.2 on page 74, 1.21.1 on
page 204.
int main()
{
printf("a=%d ; b=%d ; c=%d", 1, 2, 3) ;
return 0;
};
1.11.1 x86
x86: 3 arguments
MSVC
58
En le compilant avec MSVC 2010 Express nous obtenons:
$SG3830 DB 'a=%d ; b=%d ; c=%d', 00H
...
push 3
push 2
push 1
push OFFSET $SG3830
call _printf
add esp, 16 ; 00000010H
Presque la même chose, mais maintenant nous voyons que les arguments de printf()
sont poussés sur la pile en ordre inverse. Le premier argument est poussé en dernier.
À propos, dans un environnement 32-bit les variables de type int ont une taille de
32-bit. ce qui fait 4 octets.
Donc, nous avons 4 arguments ici. 4 ∗ 4 = 16 —ils occupent exactement 16 octets
dans la pile: un pointeur 32-bit sur une chaîne et 3 nombres de type int.
Lorsque le pointeur de pile (registre ESP) est re-modifié par l’instruction ADD ESP, X
après un appel de fonction, souvent, le nombre d’arguments de la fonction peut-être
déduit en divisant simplement X par 4.
Bien sûr, cela est spécifique à la convention d’appel cdecl, et seulement pour un
environnement 32-bit.
Voir aussi la section sur les conventions d’appel ( 6.1 on page 962).
Dans certains cas, plusieurs fonctions se terminent les une après les autres, le com-
pilateur peut concaténer plusieurs instructions «ADD ESP, X » en une seule, après
le dernier appel:
push a1
push a2
call ...
...
push a1
call ...
...
push a1
push a2
push a3
call ...
add esp, 24
59
.text :100113F8 push 1
.text :100113FA call sub_100018B0 ; prendre un argument (1)
.text :100113FF add esp, 8 ; supprimer deux arguments de la
pile à la fois
60
MSVC et OllyDbg
Maintenant, essayons de charger cet exemple dans OllyDbg. C’est l’un des débug-
gers en espace utilisateur win32 les plus populaire. Nous pouvons compiler notre
exemple avec l’option /MD de MSVC 2012, qui signifie lier avec MSVCR*.DLL, ainsi
nous verrons clairement les fonctions importées dans le debugger.
Ensuite chargeons l’exécutable dans OllyDbg. Le tout premier point d’arrêt est dans
ntdll.dll, appuyez sur F9 (run). Le second point d’arrêt est dans le code CRT. Nous
devons maintenant trouver la fonction main().
Trouvez ce code en vous déplaçant au tout début du code (MSVC alloue la fonction
main() au tout début de la section de code) :
Clickez sur l’instruction PUSH EBP, pressez F2 (mettre un point d’arrêt) et pressez F9
(lancer le programme). Nous devons effectuer ces actions pour éviter le code CRT,
car il ne nous intéresse pas pour le moment.
61
Presser F8 (enjamber) 6 fois, i.e. sauter 6 instructions:
Où sont les valeurs dans la pile? Regardez en bas à droite de la fenêtre du debugger:
Fig. 1.10: OllyDbg : pile après que les valeurs des arguments aient été poussées (Le
rectangle rouge a été ajouté par l’auteur dans un éditeur graphique)
Nous pouvons voir 3 colonnes ici: adresse dans la pile, valeur dans la pile et quelques
commentaires additionnels d’OllyDbg. OllyDbg reconnaît les chaînes de type printf(),
donc il signale ici la chaîne et les 3 valeurs attachées à elle.
Il est possible de faire un clic-droit sur la chaîne de format, cliquer sur «Follow in
dump », et la chaîne de format va apparaître dans la fenêtre en bas à gauche du
debugger. qui affiche toujours des parties de la mémoire. Ces valeurs en mémoire
peuvent être modifiées. Il est possible de changer la chaîne de format, auquel cas le
résultat de notre exemple sera différent. Cela n’est pas très utile dans le cas présent,
62
mais ce peut-être un bon exercice pour commencer à comprendre comment tout
fonctionne ici.
63
Appuyer sur F8 (enjamber).
Nous voyons la sortie suivante dans la console:
a=1; b=2; c=3
Le registre EAX contient maintenant 0xD (13). C’est correct, puisque printf() ren-
voie le nombre de caractères écrits. La valeur de EIP a changé: en effet, il contient
maintenant l’adresse de l’instruction venant après CALL printf. Les valeurs de ECX
et EDX ont également changé. Apparemment, le mécanisme interne de la fonction
printf() les a utilisés pour dans ses propres besoins.
Un fait très important est que ni la valeur de ESP, ni l’état de la pile n’ont été changés!
Nous voyons clairement que la chaîne de format et les trois valeurs correspondantes
sont toujours là. C’est en effet le comportement de la convention d’appel cdecl : l’ap-
pelée ne doit pas remettre ESP à sa valeur précédente. L’appelant est responsable
de le faire.
64
Appuyer sur F8 à nouveau pour exécuter l’instruction ADD ESP, 10 :
ESP a changé, mais les valeurs sont toujours dans la pile! Oui, bien sûr; il n’y a pas
besoin de mettre ces valeurs à zéro ou quelque chose comme ça. Tout ce qui se
trouve au dessus du pointeur de pile (SP) est du bruit ou des déchets et n’a pas du
tout de signification. Ça prendrait beaucoup de temps de mettre à zéro les entrées
inutilisées de la pile, et personne n’a vraiment besoin de le faire.
GCC
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov eax, offset aADBDCD ; "a=%d; b=%d; c=%d"
mov [esp+10h+var_4], 3
mov [esp+10h+var_8], 2
mov [esp+10h+var_C], 1
mov [esp+10h+var_10], eax
65
call _printf
mov eax, 0
leave
retn
main endp
Il est visible que la différence entre le code MSVC et celui de GCC est seulement dans
la manière dont les arguments sont stockés sur la pile. Ici GCC manipule directement
la pile sans utiliser PUSH/POP.
$ gdb 1
GNU gdb (GDB) 7.6.1-ubuntu
...
Reading symbols from /home/dennis/polygon/1...done.
Lançons le programme. Nous n’avons pas la code source de la fonction printf() ici,
donc GDB ne peut pas le montrer, mais pourrait.
(gdb) run
Starting program : /home/dennis/polygon/1
66
(gdb) x/5i 0x0804844a
0x804844a <main+45> : mov $0x0,%eax
0x804844f <main+50> : leave
0x8048450 <main+51> : ret
0x8048451 : xchg %ax,%ax
0x8048453 : xchg %ax,%ax
Les deux instructions XCHG sont des instructions sans effet, analogues à NOPs.
Le second élément (0x080484f0) est l’adresse de la chaîne de format:
(gdb) x/s 0x080484f0
0x80484f0 : "a=%d ; b=%d ; c=%d"
Les 3 éléments suivants (1, 2, 3) sont les arguments de printf(). Le reste des
éléments sont juste des «restes » sur la pile, mais peuvent aussi être des valeurs
d’autres fonctions, leurs variables locales, etc. Nous pouvons les ignorer pour le
moment.
Lancer la commande «finish ». Cette commande ordonne à GDB d’«exécuter toutes
les instructions jusqu’à la fin de la fonction ». Dans ce cas: exécuter jusqu’à la fin de
printf().
(gdb) finish
Run till exit from #0 __printf (format=0x80484f0 "a=%d ; b=%d ; c=%d") at ⤦
Ç printf.c :29
main () at 1.c :6
6 return 0;
Value returned is $2 = 13
GDB montre ce que printf() a renvoyé dans EAX (13). C’est le nombre de caractères
écrits, exactement comme dans l’exemple avec OllyDbg.
Nous voyons également «return 0; » et l’information que cette expression se trouve
à la ligne 6 du fichier 1.c. En effet, le fichier 1.c se trouve dans le répertoire cou-
rant, et GDB y a trouvé la chaîne. Comment est-ce que GDB sait quelle ligne est
exécutée à un instant donné? Cela est du au fait que lorsque le compilateur génère
les informations de debug, il sauve également une table contenant la relation entre
le numéro des lignes du code source et les adresses des instructions. GDB est un
debugger niveau source, après tout.
Examinons les registres. 13 in EAX :
(gdb) info registers
eax 0xd 13
ecx 0x0 0
edx 0x0 0
ebx 0xb7fc0000 -1208221696
esp 0xbffff120 0xbffff120
ebp 0xbffff138 0xbffff138
esi 0x0 0
edi 0x0 0
eip 0x804844a 0x804844a <main+45>
67
...
GDB utilise la syntaxe AT&T par défaut. Mais il est possible de choisir la syntaxe Intel:
(gdb) set disassembly-flavor intel
(gdb) disas
Dump of assembler code for function main :
0x0804841d <+0> : push ebp
0x0804841e <+1> : mov ebp,esp
0x08048420 <+3> : and esp,0xfffffff0
0x08048423 <+6> : sub esp,0x10
0x08048426 <+9> : mov DWORD PTR [esp+0xc],0x3
0x0804842e <+17> : mov DWORD PTR [esp+0x8],0x2
0x08048436 <+25> : mov DWORD PTR [esp+0x4],0x1
0x0804843e <+33> : mov DWORD PTR [esp],0x80484f0
0x08048445 <+40> : call 0x80482f0 <printf@plt>
=> 0x0804844a <+45> : mov eax,0x0
0x0804844f <+50> : leave
0x08048450 <+51> : ret
End of assembler dump.
Examinons les registres après l’exécution de l’instruction MOV EAX, 0. En effet, EAX
est à zéro à ce stade.
(gdb) info registers
eax 0x0 0
ecx 0x0 0
edx 0x0 0
68
ebx 0xb7fc0000 -1208221696
esp 0xbffff120 0xbffff120
ebp 0xbffff138 0xbffff138
esi 0x0 0
edi 0x0 0
eip 0x804844f 0x804844f <main+50>
...
x64: 8 arguments
Pour voir comment les autres arguments sont passés par la pile, changeons encore
notre exemple en augmentant le nombre d’arguments à 9 (chaîne de format de
printf() + 8 variables int) :
#include <stdio.h>
int main()
{
printf("a=%d ; b=%d ; c=%d ; d=%d ; e=%d ; f=%d ; g=%d ; h=%d\n", 1, 2, ⤦
Ç 3, 4, 5, 6, 7, 8) ;
return 0;
};
MSVC
Comme il a déjà été mentionné, les 4 premiers arguments sont passés par les re-
gistres RCX, RDX, R8, R9 sous Win64, tandis les autres le sont—par la pile. C’est exac-
tement de que l’on voit ici. Toutefois, l’instruction MOV est utilisée ici à la place de
PUSH, donc les valeurs sont stockées sur la pile d’une manière simple.
main PROC
sub rsp, 88
; renvoyer 0
xor eax, eax
69
add rsp, 88
ret 0
main ENDP
_TEXT ENDS
END
Le lecteur observateur pourrait demander pourquoi 8 octets sont alloués sur la pile
pour les valeurs int, alors que 4 suffisent? Oui, il faut se rappeler: 8 octets sont
alloués pour tout type de données plus petit que 64 bits. Ceci est instauré pour
des raisons de commodités: cela rend facile le calcul de l’adresse de n’importe quel
argument. En outre, ils sont tous situés à des adresses mémoires alignées. Il en est
de même dans les environnements 32-bit: 4 octets sont réservés pour tout types de
données.
GCC
Le tableau est similaire pour les OS x86-64 *NIX, excepté que les 6 premiers argu-
ments sont passés par les registres RDI, RSI, RDX, RCX, R8, R9. Tout les autres—par
la pile. GCC génère du code stockant le pointeur de chaîne dans EDI au lieu de RDI—
nous l’avons noté précédemment: 1.5.2 on page 22.
Nous avions également noté que le registre EAX a été vidé avant l’appel à printf() : 1.5.2
on page 22.
main :
sub rsp, 40
mov r9d, 5
mov r8d, 4
mov ecx, 3
mov edx, 2
mov esi, 1
mov edi, OFFSET FLAT :.LC0
xor eax, eax ; nombre de registres vectoriels
mov DWORD PTR [rsp+16], 8
mov DWORD PTR [rsp+8], 7
mov DWORD PTR [rsp], 6
call printf
; renvoyer 0
70
GCC + GDB
$ gdb 2
GNU gdb (GDB) 7.6.1-ubuntu
...
Reading symbols from /home/dennis/polygon/2...done.
Les registres RSI/RDX/RCX/R8/R9 ont les valeurs attendues. RIP contient l’adresse de
la toute première instruction de la fonction printf().
(gdb) info registers
rax 0x0 0
rbx 0x0 0
rcx 0x3 3
rdx 0x2 2
rsi 0x1 1
rdi 0x400628 4195880
rbp 0x7fffffffdf60 0x7fffffffdf60
rsp 0x7fffffffdf38 0x7fffffffdf38
r8 0x4 4
r9 0x5 5
r10 0x7fffffffdce0 140737488346336
r11 0x7ffff7a65f60 140737348263776
r12 0x400440 4195392
r13 0x7fffffffe040 140737488347200
r14 0x0 0
r15 0x0 0
rip 0x7ffff7a65f60 0x7ffff7a65f60 <__printf>
...
Affichons la pile avec la commande x/g cette fois—g est l’unité pour giant words, i.e.,
mots de 64-bit.
71
(gdb) x/10g $rsp
0x7fffffffdf38 : 0x0000000000400576 0x0000000000000006
0x7fffffffdf48 : 0x0000000000000007 0x00007fff00000008
0x7fffffffdf58 : 0x0000000000000000 0x0000000000000000
0x7fffffffdf68 : 0x00007ffff7a33de5 0x0000000000000000
0x7fffffffdf78 : 0x00007fffffffe048 0x0000000100000000
Le tout premier élément de la pile, comme dans le cas précédent, est la RA. 3 valeurs
sont aussi passées par la pile: 6, 7, 8. Nous voyons également que 8 est passé
avec les 32-bits de poids fort non effacés: 0x00007fff00000008. C’est en ordre, car
les valeurs sont d’un type int, qui est 32-bit. Donc, la partie haute du registre ou
l’élément de la pile peuvent contenir des «restes de données aléatoires ».
Si vous regardez où le contrôle reviendra après l’exécution de printf(), GDB affiche
la fonction main() en entier:
(gdb) set disassembly-flavor intel
(gdb) disas 0x0000000000400576
Dump of assembler code for function main :
0x000000000040052d <+0> : push rbp
0x000000000040052e <+1> : mov rbp,rsp
0x0000000000400531 <+4> : sub rsp,0x20
0x0000000000400535 <+8> : mov DWORD PTR [rsp+0x10],0x8
0x000000000040053d <+16> : mov DWORD PTR [rsp+0x8],0x7
0x0000000000400545 <+24> : mov DWORD PTR [rsp],0x6
0x000000000040054c <+31> : mov r9d,0x5
0x0000000000400552 <+37> : mov r8d,0x4
0x0000000000400558 <+43> : mov ecx,0x3
0x000000000040055d <+48> : mov edx,0x2
0x0000000000400562 <+53> : mov esi,0x1
0x0000000000400567 <+58> : mov edi,0x400628
0x000000000040056c <+63> : mov eax,0x0
0x0000000000400571 <+68> : call 0x400410 <printf@plt>
0x0000000000400576 <+73> : mov eax,0x0
0x000000000040057b <+78> : leave
0x000000000040057c <+79> : ret
End of assembler dump.
72
rbx 0x0 0
rcx 0x26 38
rdx 0x7ffff7dd59f0 140737351866864
rsi 0x7fffffd9 2147483609
rdi 0x0 0
rbp 0x7fffffffdf60 0x7fffffffdf60
rsp 0x7fffffffdf40 0x7fffffffdf40
r8 0x7ffff7dd26a0 140737351853728
r9 0x7ffff7a60134 140737348239668
r10 0x7fffffffd5b0 140737488344496
r11 0x7ffff7a95900 140737348458752
r12 0x400440 4195392
r13 0x7fffffffe040 140737488347200
r14 0x0 0
r15 0x0 0
rip 0x40057b 0x40057b <main+78>
...
1.11.2 ARM
ARM: 3 arguments
ARM 32-bit
Donc, les 4 premiers arguments sont passés par les registres R0-R3 dans cet ordre:
un pointeur sur la chaîne de format de printf() dans R0, puis 1 dans R1, 2 dans R2
et 3 dans R3. L’instruction en 0x18 écrit 0 dans R0—c’est la déclaration C de return
0.
avec optimisation Keil 6/2013 génère le même code.
73
avec optimisation Keil 6/2013 (Mode Thumb)
Il n’y a pas de différence significative avec le code non optimisé pour le mode ARM.
void main()
{
printf("a=%d ; b=%d ; c=%d", 1, 2, 3) ;
};
C’est la version optimisée (-O3) pour le mode ARM et cette fois nous voyons B comme
dernière instruction au lieu du BL habituel. Une autre différence entre cette version
optimisée et la précédente (compilée sans optimisation) est l’absence de fonctions
prologue et épilogue (les instructions qui préservent les valeurs des registres R0 et
LR). L’instruction B saute simplement à une autre adresse, sans manipuler le registre
LR, de façon similaire au JMP en x86. Pourquoi est-ce que fonctionne? Parce ce code
est en fait bien équivalent au précédent. Il y a deux raisons principales: 1) Ni la pile
ni SP (pointeur de pile) ne sont modifiés; 2) l’appel à printf() est la dernière instruc-
tion, donc il ne se passe rien après. A la fin, la fonction printf() rend simplement
le contrôle à l’adresse stockée dans LR. Puisque LR contient actuellement l’adresse
du point depuis lequel notre fonction a été appelée alors le contrôle après printf()
sera redonné à ce point. Par conséquent, nous n’avons pas besoin de sauver LR car
74
il ne nous est pas nécessaire de le modifier. Et il ne nous est non plus pas néces-
saire de modifier LR car il n’y a pas d’autre appel de fonction excepté printf(). Par
ailleurs, après cet appel nous ne faisons rien d’autre! C’est la raison pour laquelle
une telle optimisation est possible.
Cette optimisation est souvent utilisée dans les fonctions où la dernière déclaration
est un appel à une autre fonction. Un exemple similaire est présenté ici: 1.21.1 on
page 205.
Un cas un peu plus simple a été décrit plus haut: 1.10 on page 57.
ARM64
La première instruction STP (Store Pair) sauve FP (X29) et LR (X30) sur la pile.
La seconde instruction, ADD X29, SP, 0 crée la pile. Elle écrit simplement la valeur
de SP dans X29.
Ensuite nous voyons la paire d’instructions habituelle ADRP/ADD, qui crée le pointeur
sur la chaîne. lo12 signifie les 12 bits de poids faible, i.e., le linker va écrire les 12
bits de poids faible de l’adresse LC1 dans l’opcode de l’instruction ADD. %d dans la
chaîne de format de printf() est un int 32-bit, les 1, 2 et 3 sont chargés dans les
parties 32-bit des registres.
GCC (Linaro) 4.9 avec optimisation génère le même code.
75
ARM: 8 arguments
int main()
{
printf("a=%d ; b=%d ; c=%d ; d=%d ; e=%d ; f=%d ; g=%d ; h=%d\n", 1, 2, ⤦
Ç 3, 4, 5, 6, 7, 8) ;
return 0;
};
76
La seconde instruction SUB SP, SP, #0x14 décrémente SP (le pointeur de pile)
afin d’allouer 0x14 (20) octets sur la pile. En effet, nous devons passer 5 valeurs
de 32-bit par la pile à la fonction printf(), et chacune occupe 4 octets, ce qui
fait exactement 5 ∗ 4 = 20. Les 4 autres valeurs de 32-bit sont passées par les
registres.
• Passer 5, 6, 7 et 8 par la pile: ils sont stockés dans les registres R0, R1, R2 et R3
respectivement.
Ensuite, l’instruction ADD R12, SP, #0x18+var_14 écrit l’adresse de la pile où
ces 4 variables doivent être stockées dans le registre R12. var_14 est une macro
d’assemblage, égal à -0x14, créée par IDA pour afficher commodément le code
accédant à la pile. Les macros var_? générée par IDA reflètent les variables
locales dans la pile.
Donc, SP+4 doit être stocké dans le registre R12.
L’instruction suivante STMIA R12, R0-R3 écrit le contenu des registres R0-R3
dans la mémoire pointée par R12. STMIA est l’abréviation de Store Multiple Incre-
ment After (stocker plusieurs incrémenter après). Increment After signifie que
R12 doit être incrémenté de 4 après l’écriture de chaque valeur d’un registre.
• Passer 4 par la pile: 4 est stocké dans R0 et ensuite, cette valeur, avec l’aide
de
l’instruction STR R0, [SP,#0x18+var_18] est sauvée dans la pile. var_18 est
-0x18, donc l’offset est 0, donc la valeur du registre R0 (4) est écrite à l’adresse
écrite dans SP.
• Passer 1, 2 et 3 par des registres: Les valeurs des 3 premiers nombres (a,b,c)
(respectivement 1, 2, 3) sont passées par les registres R1, R2 et R3 juste avant
l’appel de printf().
• appel de printf()
• Épilogue de fonction:
L’instruction ADD SP, SP, #0x14 restaure le pointeur SP à sa valeur précé-
dente, nettoyant ainsi la pile. Bien sûr, ce qui a été stocké sur la pile y reste,
mais sera récrit lors de l’exécution ultérieure de fonctions.
L’instruction LDR PC, [SP+4+var_4],#4 charge la valeur sauvée de LR depuis
la pile dans le registre PC, provoquant ainsi la sortie de la fonction. Il n’y a pas
de point d’exclamation—effectivement, PC est d’abord chargé depuis l’adresse
stockées dans SP (4 + var_4 = 4 + (−4) = 0), donc cette instruction est analogue à
INSLDR PC, [SP],#4), et ensuite SP est incrémenté de 4. Il s’agit de post-index69 .
Pourquoi est-ce qu’IDA affiche l’instruction comme ça? Parce qu’il veut illustrer
la disposition de la pile et le fait que var_4 est alloué pour sauver la valeur de
LR dans la pile locale. Cette instruction est quelque peu similaire à POP PC en
x8670 .
77
.text :0000001C printf_main2
.text :0000001C
.text :0000001C var_18 = -0x18
.text :0000001C var_14 = -0x14
.text :0000001C var_8 = -8
.text :0000001C
.text :0000001C 00 B5 PUSH {LR}
.text :0000001E 08 23 MOVS R3, #8
.text :00000020 85 B0 SUB SP, SP, #0x14
.text :00000022 04 93 STR R3, [SP,#0x18+var_8]
.text :00000024 07 22 MOVS R2, #7
.text :00000026 06 21 MOVS R1, #6
.text :00000028 05 20 MOVS R0, #5
.text :0000002A 01 AB ADD R3, SP, #0x18+var_14
.text :0000002C 07 C3 STMIA R3 !, {R0-R2}
.text :0000002E 04 20 MOVS R0, #4
.text :00000030 00 90 STR R0, [SP,#0x18+var_18]
.text :00000032 03 23 MOVS R3, #3
.text :00000034 02 22 MOVS R2, #2
.text :00000036 01 21 MOVS R1, #1
.text :00000038 A0 A0 ADR R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d;
d=%d; e=%d; f=%d; g=%"...
.text :0000003A 06 F0 D9 F8 BL __2printf
.text :0000003E
.text :0000003E loc_3E ; CODE XREF: example13_f+16
.text :0000003E 05 B0 ADD SP, SP, #0x14
.text :00000040 00 BD POP {PC}
La sortie est presque comme dans les exemples précédents. Toutefois, c’est du code
Thumb et les valeurs sont arrangées différemment dans la pile: 8 vient en premier,
puis 5, 6, 7 et 4 vient en troisième.
78
__text :0000293C 08 90 A0 E3 MOV R9, #8
__text :00002940 01 10 A0 E3 MOV R1, #1
__text :00002944 02 20 A0 E3 MOV R2, #2
__text :00002948 03 30 A0 E3 MOV R3, #3
__text :0000294C 10 90 8D E5 STR R9, [SP,#0x1C+var_C]
__text :00002950 A4 05 00 EB BL _printf
__text :00002954 07 D0 A0 E1 MOV SP, R7
__text :00002958 80 80 BD E8 LDMFD SP !, {R7,PC}
Presque la même chose que ce que nous avons déjà vu, avec l’exception de l’ins-
truction STMFA (Store Multiple Full Ascending), qui est un synonyme de l’instruction
STMIB (Store Multiple Increment Before). Cette instruction incrémente la valeur du
registre SP et écrit seulement après la valeur registre suivant dans la mémoire, plutôt
que d’effectuer ces deux actions dans l’ordre inverse.
Une autre chose qui accroche le regard est que les instructions semblent être arran-
gées de manière aléatoire. Par exemple, la valeur dans le registre R0 est manipulée
en trois endroits, aux adresses 0x2918, 0x2920 et 0x2928, alors qu’il serait possible
de le faire en un seul endroit.
Toutefois, le compilateur qui optimise doit avoir ses propres raisons d’ordonner les
instructions pour avoir une plus grande efficacité à l’exécution.
D’habitude, le processeur essaye d’exécuter simultanément des instructions situées
côte à côte.
Par exemple, des instructions comme MOVT R0, #0 et ADD R0, PC, R0 ne peuvent
pas être exécutées simultanément puisqu’elles modifient toutes deux le registre R0.
D’un autre côté, les instructions MOVT R0, #0 et MOV R2, #4 peuvent être exécutées
simultanément puisque leurs effets n’interfèrent pas l’un avec l’autre lors de leurs
exécution. Probablement que le compilateur essaye de générer du code arrangé de
cette façon (lorsque c’est possible).
79
__text :00002BBE 00 92 STR R2, [SP,#0x1C+var_1C]
__text :00002BC0 4F F0 08 09 MOV.W R9, #8
__text :00002BC4 8E E8 0A 10 STMIA.W LR, {R1,R3,R12}
__text :00002BC8 01 21 MOVS R1, #1
__text :00002BCA 02 22 MOVS R2, #2
__text :00002BCC 03 23 MOVS R3, #3
__text :00002BCE CD F8 10 90 STR.W R9, [SP,#0x1C+var_C]
__text :00002BD2 01 F0 0A EA BLX _printf
__text :00002BD6 05 B0 ADD SP, SP, #0x14
__text :00002BD8 80 BD POP {R7,PC}
La sortie est presque la même que dans l’exemple précédent, avec l’exception que
des instructions Thumb/Thumb-2 sont utilisées à la place.
ARM64
Les 8 premiers arguments sont passés dans des registres X- ou W-: [Procedure Call
80
Standard for the ARM 64-bit Architecture (AArch64), (2013)]71 . Un pointeur de chaîne
nécessite un registre 64-bit, donc il est passé dans X0. Toutes les autres valeurs ont
un type int 32-bit, donc elles sont stockées dans la partie 32-bit des registres (W-). Le
9ème argument (8) est passé par la pile. En effet: il n’est pas possible de passer un
grand nombre d’arguments par les registres, car le nombre de registres est limité.
GCC (Linaro) 4.9 avec optimisation génère le même code.
1.11.3 MIPS
3 arguments
La différence principale avec l’exemple «Hello, world! » est que dans ce cas, printf()
est appelée à la place de puts() et 3 arguments de plus sont passés à travers les
registres $5…$7 (ou $A0…$A2). C’est pourquoi ces registres sont préfixés avec A-,
ceci sous-entend qu’ils sont utilisés pour le passage des arguments aux fonctions.
; épilogue de la fonction:
lw $31,28($sp)
; mettre la valeur de retour à 0:
move $2,$0
; retourner
j $31
81
addiu $sp,$sp,32 ; slot de délai de branchement
IDA a agrégé la paire d’instructions LUI et ADDIU en une pseudo instruction LA. C’est
pourquoi il n’y a pas d’instruction à l’adresse 0x1C: car LA occupe 8 octets.
82
move $fp,$sp
lui $28,%hi(__gnu_local_gp)
addiu $28,$28,%lo(__gnu_local_gp)
; charger l'adresse de la chaîne de texte:
lui $2,%hi($LC0)
addiu $2,$2,%lo($LC0)
; mettre le 1er argument de printf() :
move $4,$2
; mettre le 2nd argument de printf() :
li $5,1 # 0x1
; mettre le 3ème argument de printf() :
li $6,2 # 0x2
; mettre le 4ème argument de printf() :
li $7,3 # 0x3
; charger l'adresse de printf() :
lw $2,%call16(printf)($28)
nop
; appeler printf() :
move $25,$2
jalr $25
nop
; épilogue de la fonction:
lw $28,16($fp)
; mettre la valeur de retour à 0:
move $2,$0
move $sp,$fp
lw $31,28($sp)
lw $fp,24($sp)
addiu $sp,$sp,32
; retourner
j $31
nop
83
.text :00000028 li $a1, 1
; mettre le 3ème argument de printf() :
.text :0000002C li $a2, 2
; mettre le 4ème argument de printf() :
.text :00000030 li $a3, 3
; charger l'adresse de printf() :
.text :00000034 lw $v0, (printf & 0xFFFF)($gp)
.text :00000038 or $at, $zero
; appeler printf() :
.text :0000003C move $t9, $v0
.text :00000040 jalr $t9
.text :00000044 or $at, $zero ; NOP
; épilogue de la fonction:
.text :00000048 lw $gp, 0x20+var_10($fp)
; mettre la valeur de retour à 0:
.text :0000004C move $v0, $zero
.text :00000050 move $sp, $fp
.text :00000054 lw $ra, 0x20+var_4($sp)
.text :00000058 lw $fp, 0x20+var_8($sp)
.text :0000005C addiu $sp, 0x20
; retourner
.text :00000060 jr $ra
.text :00000064 or $at, $zero ; NOP
8 arguments
int main()
{
printf("a=%d ; b=%d ; c=%d ; d=%d ; e=%d ; f=%d ; g=%d ; h=%d\n", 1, 2, ⤦
Ç 3, 4, 5, 6, 7, 8) ;
return 0;
};
Seul les 4 premiers arguments sont passés dans les registres $A0 …$A3, les autres
sont passés par la pile.
C’est la convention d’appel O32 (qui est la plus commune dans le monde MIPS).
D’autres conventions d’appel (comme N32) peuvent utiliser les registres à d’autres
fins.
SW est l’abbréviation de «Store Word » (depuis un registre vers la mémoire). En MIPS,
il manque une instructions pour stocker une valeur dans la mémoire, donc une paire
d’instruction doit être utilisée à la place (LI/SW).
84
Listing 1.61: GCC 4.4.5 avec optimisation (résultat en sortie de l’assembleur)
$LC0 :
.ascii "a=%d ; b=%d ; c=%d ; d=%d ; e=%d ; f=%d ; g=%d ; h=%d\012\000"
main :
; prologue de la fonction:
lui $28,%hi(__gnu_local_gp)
addiu $sp,$sp,-56
addiu $28,$28,%lo(__gnu_local_gp)
sw $31,52($sp)
; passer le 5ème argument dans la pile:
li $2,4 # 0x4
sw $2,16($sp)
; passer le 6ème argument dans la pile:
li $2,5 # 0x5
sw $2,20($sp)
; passer le 7ème argument dans la pile:
li $2,6 # 0x6
sw $2,24($sp)
; passer le 8ème argument dans la pile:
li $2,7 # 0x7
lw $25,%call16(printf)($28)
sw $2,28($sp)
; passer le 1er argument dans $a0:
lui $4,%hi($LC0)
; passer le 9ème argument dans la pile:
li $2,8 # 0x8
sw $2,32($sp)
addiu $4,$4,%lo($LC0)
; passer le 2nd argument dans $a1:
li $5,1 # 0x1
; passer le 3ème argument dans $a2:
li $6,2 # 0x2
; appeler printf() :
jalr $25
; passer le 4ème argument dans $a3 (slot de délai de branchement) :
li $7,3 # 0x3
; épilogue de la fonction:
lw $31,52($sp)
; mettre la valeur de retour à 0:
move $2,$0
; retourner
j $31
addiu $sp,$sp,56 ; slot de délai de branchement
85
.text :00000000 var_18 = -0x18
.text :00000000 var_10 = -0x10
.text :00000000 var_4 = -4
.text :00000000
; prologue de la fonction:
.text :00000000 lui $gp, (__gnu_local_gp >> 16)
.text :00000004 addiu $sp, -0x38
.text :00000008 la $gp, (__gnu_local_gp & 0xFFFF)
.text :0000000C sw $ra, 0x38+var_4($sp)
.text :00000010 sw $gp, 0x38+var_10($sp)
; passer le 5ème argument dans la pile:
.text :00000014 li $v0, 4
.text :00000018 sw $v0, 0x38+var_28($sp)
; passer le 6ème argument dans la pile:
.text :0000001C li $v0, 5
.text :00000020 sw $v0, 0x38+var_24($sp)
; passer le 7ème argument dans la pile:
.text :00000024 li $v0, 6
.text :00000028 sw $v0, 0x38+var_20($sp)
; passer le 8ème argument dans la pile:
.text :0000002C li $v0, 7
.text :00000030 lw $t9, (printf & 0xFFFF)($gp)
.text :00000034 sw $v0, 0x38+var_1C($sp)
; préparer le 1er argument dans $a0:
.text :00000038 lui $a0, ($LC0 >> 16) # "a=%d; b=%d;
c=%d; d=%d; e=%d; f=%d; g=%"...
; passer le 9ème argument dans la pile:
.text :0000003C li $v0, 8
.text :00000040 sw $v0, 0x38+var_18($sp)
; passer le 1er argument in $a0:
.text :00000044 la $a0, ($LC0 & 0xFFFF) # "a=%d; b=%d;
c=%d; d=%d; e=%d; f=%d; g=%"...
; passer le 2nd argument dans $a1:
.text :00000048 li $a1, 1
; passer le 3ème argument dans $a2:
.text :0000004C li $a2, 2
; appeler printf() :
.text :00000050 jalr $t9
; passer le 4ème argument dans $a3 (slot de délai de branchement) :
.text :00000054 li $a3, 3
; épilogue de la fonction:
.text :00000058 lw $ra, 0x38+var_4($sp)
; mettre la valeur de retour à 0:
.text :0000005C move $v0, $zero
; retourner
.text :00000060 jr $ra
.text :00000064 addiu $sp, 0x38 ; slot de délai de
branchement
86
Listing 1.63: sans optimisation GCC 4.4.5 (résultat en sortie de l’assembleur)
$LC0 :
.ascii "a=%d ; b=%d ; c=%d ; d=%d ; e=%d ; f=%d ; g=%d ; h=%d\012\000"
main :
; prologue de la fonction:
addiu $sp,$sp,-56
sw $31,52($sp)
sw $fp,48($sp)
move $fp,$sp
lui $28,%hi(__gnu_local_gp)
addiu $28,$28,%lo(__gnu_local_gp)
lui $2,%hi($LC0)
addiu $2,$2,%lo($LC0)
; passer le 5ème argument dans la pile:
li $3,4 # 0x4
sw $3,16($sp)
; passer le 6ème argument dans la pile:
li $3,5 # 0x5
sw $3,20($sp)
; passer le 7ème argument dans la pile:
li $3,6 # 0x6
sw $3,24($sp)
; passer le 8ème argument dans la pile:
li $3,7 # 0x7
sw $3,28($sp)
; passer le 9ème argument dans la pile:
li $3,8 # 0x8
sw $3,32($sp)
; passer le 1er argument dans $a0:
move $4,$2
; passer le 2nd argument dans $a1:
li $5,1 # 0x1
; passer le 3ème argument dans $a2:
li $6,2 # 0x2
; passer le 4ème argument dans $a3:
li $7,3 # 0x3
; appeler printf() :
lw $2,%call16(printf)($28)
nop
move $25,$2
jalr $25
nop
; épilogue de la fonction:
lw $28,40($fp)
; mettre la valeur de retour à 0:
move $2,$0
move $sp,$fp
lw $31,52($sp)
lw $fp,48($sp)
addiu $sp,$sp,56
; retourner
j $31
nop
87
Listing 1.64: sans optimisation GCC 4.4.5 (IDA)
.text :00000000 main :
.text :00000000
.text :00000000 var_28 = -0x28
.text :00000000 var_24 = -0x24
.text :00000000 var_20 = -0x20
.text :00000000 var_1C = -0x1C
.text :00000000 var_18 = -0x18
.text :00000000 var_10 = -0x10
.text :00000000 var_8 = -8
.text :00000000 var_4 = -4
.text :00000000
; prologue de la fonction:
.text :00000000 addiu $sp, -0x38
.text :00000004 sw $ra, 0x38+var_4($sp)
.text :00000008 sw $fp, 0x38+var_8($sp)
.text :0000000C move $fp, $sp
.text :00000010 la $gp, __gnu_local_gp
.text :00000018 sw $gp, 0x38+var_10($sp)
.text :0000001C la $v0, aADBDCDDDEDFDGD # "a=%d; b=%d;
c=%d; d=%d; e=%d; f=%d; g=%"...
; passer le 5ème argument dans la pile:
.text :00000024 li $v1, 4
.text :00000028 sw $v1, 0x38+var_28($sp)
; passer le 6ème argument dans la pile:
.text :0000002C li $v1, 5
.text :00000030 sw $v1, 0x38+var_24($sp)
; passer le 7ème argument dans la pile:
.text :00000034 li $v1, 6
.text :00000038 sw $v1, 0x38+var_20($sp)
; passer le 8ème argument dans la pile:
.text :0000003C li $v1, 7
.text :00000040 sw $v1, 0x38+var_1C($sp)
; passer le 9ème argument dans la pile:
.text :00000044 li $v1, 8
.text :00000048 sw $v1, 0x38+var_18($sp)
; passer le 1er argument dans $a0:
.text :0000004C move $a0, $v0
; passer le 2nd argument dans $a1:
.text :00000050 li $a1, 1
; passer le 3ème argument dans $a2:
.text :00000054 li $a2, 2
; passer le 4ème argument dans $a3:
.text :00000058 li $a3, 3
; appeler printf() :
.text :0000005C lw $v0, (printf & 0xFFFF)($gp)
.text :00000060 or $at, $zero
.text :00000064 move $t9, $v0
.text :00000068 jalr $t9
.text :0000006C or $at, $zero ; NOP
; épilogue de la fonction:
.text :00000070 lw $gp, 0x38+var_10($fp)
; mettre la valeur de retour à 0:
88
.text :00000074 move $v0, $zero
.text :00000078 move $sp, $fp
.text :0000007C lw $ra, 0x38+var_4($sp)
.text :00000080 lw $fp, 0x38+var_8($sp)
.text :00000084 addiu $sp, 0x38
; retourner
.text :00000088 jr $ra
.text :0000008C or $at, $zero ; NOP
1.11.4 Conclusion
Voici un schéma grossier de l’appel de la fonction:
89
BL fonction
; modifier le pointeur de pile (si besoin)
1.11.5 À propos
À propos, cette différence dans le passage des arguments entre x86, x64, fastcall,
ARM et MIPS est une bonne illustration du fait que le CPU est inconscient de comment
les arguments sont passés aux fonctions. Il est aussi possible de créer un hypothé-
tique compilateur capable de passer les arguments via une structure spéciale sans
utiliser du tout la pile.
Les registres MIPS $A0 …$A3 sont appelés comme ceci par commodité (c’est dans
la convention d’appel O32). Les programmeurs peuvent utiliser n’importe quel autre
registre, (bon, peut-être à l’exception de $ZERO) pour passer des données ou n’im-
porte quelle autre convention d’appel.
Le CPU n’est pas au courant de quoi que ce soit des conventions d’appel.
Nous pouvons aussi nous rappeler comment les débutants en langage d’assemblage
passent les arguments aux autres fonctions: généralement par les registres, sans
ordre explicite, ou même par des variables globales. Bien sûr, cela fonctionne.
1.12 scanf()
Maintenant utilisons la fonction scanf().
90
1.12.1 Exemple simple
#include <stdio.h>
int main()
{
int x ;
printf ("Enter X :\n") ;
return 0;
};
Il n’est pas astucieux d’utiliser scanf() pour les interactions utilisateurs de nos jours.
Mais nous pouvons, toutefois, illustrer le passage d’un pointeur sur une variable de
type int.
Les pointeurs sont l’un des concepts fondamentaux de l’informatique. Souvent, pas-
ser un gros tableau, structure ou objet comme argument à une autre fonction est
trop coûteux, tandis que passer leur adresse l’est très peu. Par exemple, si vous
voulez afficher une chaîne de texte sur la console, il est plus facile de passer son
adresse au noyau de l’OS.
En plus, si la fonction appelée doit modifier quelque chose dans un gros tableau
ou structure reçu comme paramètre et renvoyer le tout, la situation est proche de
l’absurde. Donc, la chose la plus simple est de passer l’adresse du tableau ou de la
structure à la fonction appelée, et de la laisser changer ce qui doit l’être.
Un pointeur en C/C++—est simplement l’adresse d’un emplacement mémoire quel-
conque.
En x86, l’adresse est représentée par un nombre de 32-bit (i.e., il occupe 4 octets),
tandis qu’en x86-64 c’est un nombre de 64-bit (occupant 8 octets). À propos, c’est la
cause de l’indignation de certaines personnes concernant le changement vers x86-
64—tous les pointeurs en architecture x64 ayant besoin de deux fois plus de place,
incluant la mémoire cache, qui est de la mémoire “coûteuse”.
Il est possible de travailler seulement avec des pointeurs non typés, moyennant
quelques efforts; e.g. la fonction C standard memcpy(), qui copie un bloc de mé-
moire d’un endroit à un autre, prend 2 pointeurs de type void* comme arguments,
puisqu’il est impossible de prévoir le type de données qu’il faudra copier. Les types
de données ne sont pas importants, seule la taille du bloc compte.
Les pointeurs sont aussi couramment utilisés lorsqu’une fonction doit renvoyer plus
d’une valeur (nous reviendrons là-dessus plus tard ( 3.23 on page 789)).
La fonction scanf()—en est une telle.
91
Hormis le fait que la fonction doit indiquer combien de valeurs ont été lues avec
succès, elle doit aussi renvoyer toutes ces valeurs.
En C/C++ le type du pointeur est seulement nécessaire pour la vérification de type
lors de la compilation.
Il n’y a aucune information du tout sur le type des pointeurs à l’intérieur du code
compilé.
x86
MSVC
Voici ce que l’on obtient après avoir compilé avec MSVC 2010:
CONST SEGMENT
$SG3831 DB 'Enter X :', 0aH, 00H
$SG3832 DB '%d', 00H
$SG3833 DB 'You entered %d...', 0aH, 00H
CONST ENDS
PUBLIC _main
EXTRN _scanf :PROC
EXTRN _printf :PROC
; Options de compilation de la fonction: /Odtp
_TEXT SEGMENT
_x$ = -4 ; size = 4
_main PROC
push ebp
mov ebp, esp
push ecx
push OFFSET $SG3831 ; 'Enter X:'
call _printf
add esp, 4
lea eax, DWORD PTR _x$[ebp]
push eax
push OFFSET $SG3832 ; '%d'
call _scanf
add esp, 8
mov ecx, DWORD PTR _x$[ebp]
push ecx
push OFFSET $SG3833 ; 'You entered %d...'
call _printf
add esp, 8
; retourner 0
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
92
D’après le standard C/C++ elle ne doit être visible que dans cette fonction et dans
aucune autre portée. Traditionnellement, les variables locales sont stockées sur la
pile. Il y a probablement d’autres moyens de les allouer, mais en x86, c’est la façon
de faire.
Le but de l’instruction suivant le prologue de la fonction, PUSH ECX, n’est pas de
sauver l’état de ECX (noter l’absence d’un POP ECX à la fin de la fonction).
En fait, cela alloue 4 octets sur la pile pour stocker la variable x.
x est accédée à l’aide de la macro _x$ (qui vaut -4) et du registre EBP qui pointe sur
la structure de pile courante.
Pendant la durée de l’exécution de la fonction, EBP pointe sur la structure locale de
pile courante, rendant possible l’accès aux variables locales et aux arguments de la
fonction via EBP+offset.
Il est aussi possible d’utiliser ESP dans le même but, bien que ça ne soit pas très
commode, car il change fréquemment. La valeur de EBP peut être perçue comme un
état figé de la valeur de ESP au début de l’exécution de la fonction.
Voici une structure de pile typique dans un environnement 32-bit:
… …
EBP-8 variable locale #2, marqué dans IDA comme var_8
EBP-4 variable locale #1, marqué dans IDA comme var_4
EBP valeur sauvée de EBP
EBP+4 adresse de retour
EBP+8 argument#1, marqué dans IDA comme arg_0
EBP+0xC argument#2, marqué dans IDA comme arg_4
EBP+0x10 argument#3, marqué dans IDA comme arg_8
… …
La fonction scanf() de notre exemple a deux arguments.
Le premier est un pointeur sur la chaîne contenant %d et le second est l’adresse de
la variable x.
Tout d’abord, l’adresse de la variable x est chargée dans le registre EAX par l’instruc-
tion
lea eax, DWORD PTR _x$[ebp].
LEA signifie load effective address (charger l’adresse effective) et est souvent utilisée
pour composer une adresse ( .1.6 on page 1345).
Nous pouvons dire que dans ce cas, LEA stocke simplement la somme de la valeur
du registre EBP et de la macro _x$ dans le registre EAX.
C’est la même chose que lea eax, [ebp-4].
Donc, 4 est soustrait de la valeur du registre EBP et le résultat est chargé dans le
registre EAX. Ensuite, la valeur du registre EAX est poussée sur la pile et scanf() est
appelée.
printf() est appelée ensuite avec son premier argument — un pointeur sur la
chaîne: You entered %d...\n.
93
Le second argument est préparé avec: mov ecx, [ebp-4]. L’instruction stocke la
valeur de la variable x et non son adresse, dans le registre ECX.
Puis, la valeur de ECX est stockée sur la pile et le dernier appel à printf() est
effectué.
94
MSVC + OllyDbg
Cliquer droit sur EAX dans la fenêtre des registres et choisir «Follow in stack ».
Cette adresse va apparaître dans la fenêtre de la pile. La flèche rouge a été ajoutée,
pointant la variable dans la pile locale. A ce point, cet espace contient des restes de
données (0x6E494714). Maintenant. avec l’aide de l’instruction PUSH, l’adresse de cet
élément de pile va être stockée sur la même pile à la position suivante. Appuyons sur
F8 jusqu’à la fin de l’exécution de scanf(). Pendant l’exécution de scanf(), entrons,
par exemple, 123, dans la fenêtre de la console:
Enter X :
123
95
scanf() a déjà fini de s’exécuter:
scanf() renvoie 1 dans EAX, ce qui indique qu’elle a lu avec succès une valeur. Si
nous regardons de nouveau l’élément de la pile correspondant à la variable locale,
il contient maintenant 0x7B (123).
96
Plus tard, cette valeur est copiée de la pile vers le registre ECX et passée à printf() :
GCC
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 20h
mov [esp+20h+var_20], offset aEnterX ; "Enter X:"
call _puts
mov eax, offset aD ; "%d"
lea edx, [esp+20h+var_4]
mov [esp+20h+var_1C], edx
mov [esp+20h+var_20], eax
call ___isoc99_scanf
mov edx, [esp+20h+var_4]
mov eax, offset aYouEnteredD___ ; "You entered %d...\n"
mov [esp+20h+var_1C], edx
mov [esp+20h+var_20], eax
call _printf
mov eax, 0
leave
97
retn
main endp
GCC a remplacé l’appel à printf() avec un appel à puts(). La raison de cela a été
expliquée dans ( 1.5.3 on page 29).
Comme dans l’exemple avec MSVC—les arguments sont placés dans la pile avec
l’instruction MOV.
À propos
Ce simple exemple est la démonstration du fait que le compilateur traduit une liste
d’expression en bloc-C/C++ en une liste séquentielle d’instructions. Il n’y a rien entre
les expressions en C/C++, et le résultat en code machine, il n’y a rien entre le dé-
roulement du flux de contrôle d’une expression à la suivante.
x64
Le schéma est ici similaire, avec la différence que les registres, plutôt que la pile,
sont utilisés pour le passage des arguments.
MSVC
_TEXT SEGMENT
x$ = 32
main PROC
$LN3 :
sub rsp, 56
lea rcx, OFFSET FLAT :$SG1289 ; 'Enter X:'
call printf
lea rdx, QWORD PTR x$[rsp]
lea rcx, OFFSET FLAT :$SG1291 ; '%d'
call scanf
mov edx, DWORD PTR x$[rsp]
lea rcx, OFFSET FLAT :$SG1292 ; 'You entered %d...'
call printf
; retourner 0
xor eax, eax
add rsp, 56
ret 0
main ENDP
98
_TEXT ENDS
GCC
main :
sub rsp, 24
mov edi, OFFSET FLAT :.LC0 ; "Enter X:"
call puts
lea rsi, [rsp+12]
mov edi, OFFSET FLAT :.LC1 ; "%d"
xor eax, eax
call __isoc99_scanf
mov esi, DWORD PTR [rsp+12]
mov edi, OFFSET FLAT :.LC2 ; "You entered %d...\n"
xor eax, eax
call printf
; retourner 0
xor eax, eax
add rsp, 24
ret
ARM
99
.text :0000005C 08 BD POP {R3,PC}
Afin que scanf() puisse lire l’item, elle a besoin d’un paramètre—un pointeur sur un
int. Le type int est 32-bit, donc nous avons besoin de 4 octets pour le stocker quelque
part en mémoire, et il tient exactement dans un registre 32-bit. De l’espace pour la
variable locale x est allouée sur la pile et IDA l’a nommée var_8. Il n’est toutefois
pas nécessaire de définir cette macro, puisque le SP (pointeur de pile) pointe déjà
sur cet espace et peut être utilisé directement.
Donc, la valeur de SP est copiée dans la registre R1 et, avec la chaîne de format,
passée à scanf().
Les instructions PUSH/POP se comportent différemment en ARM et en x86 (c’est l’in-
verse) Il y a des sysnonymes aux instructions STM/STMDB/LDM/LDMIA. Et l’instruction
PUSH écrit d’abord une valeur sur la pile, et ensuite soustrait 4 de SP. De ce fait, après
PUSH, SP pointe sur de l’espace inutilisé sur la pile. Il est utilisé par scanf(), et après
par printf().
LDMIA signifie Load Multiple Registers Increment address After each transfer (charge
plusieurs registres incrémente l’adresse après chaque transfert). STMDB signifie Store
Multiple Registers Decrement address Before each transfer (socke plusieurs registres
décrémente l’adresse avant chaque transfert).
Plus tard, avec l’aide de l’instruction LDR, cette valeur est copiée depuis la pile vers
le registre R1 afin de la passer à printf().
ARM64
100
22 add x1, x29, 28
23 ; X1=adresse de la variable "x"
24 ; passer l'adresse de scanf() et l'appeler:
25 bl __isoc99_scanf
26 ; charger la valeur 32-bit de la variable dans la partie de pile:
27 ldr w1, [x29,28]
28 ; W1=x
29 ; charger le pointeur sur la chaîne "You entered %d...\n"
30 ; printf() va prendre la chaîne de texte de X0 et de la variable "x" de X1
(ou W1)
31 adrp x0, .LC2
32 add x0, x0, :lo12 :.LC2
33 bl printf
34 ; retourner 0
35 mov w0, 0
36 ; restaurer FP et LR, puis ajouter 32 à SP:
37 ldp x29, x30, [sp], 32
38 ret
Il y a 32 octets alloués pour la structure de pile, ce qui est plus que nécessaire. Peut-
être dans un soucis d’alignement de mémoire? La partie la plus intéressante est de
trouver de l’espace pour la variable x dans la structure de pile (ligne 22). Pourquoi
28? Pour une certaine raison, le compilateur a décidé de stocker cette variable à la
fin de la structure de pile locale au lieu du début. L’adresse est passée à scanf(),
qui stocke l’entrée de l’utilisateur en mémoire à cette adresse. Il s’agit d’une valeur
sur 32-bit de type int. La valeur est prise à la ligne 27 puis passée à printf().
MIPS
Une place est allouée sur la pile locale pour la variable x, et elle doit être appelée
par $sp + 24.
Son adresse est passée à scanf(), et la valeur entrée par l’utilisateur est chargée
en utilisant l’instruction LW («Load Word »), puis passée à printf().
101
addiu $4,$4,%lo($LC0) ; slot de délai de branchement
; appel de scanf() :
lw $28,16($sp)
lui $4,%hi($LC1)
lw $25,%call16(__isoc99_scanf)($28)
; définir le 2nd argument de scanf(), $a1=$sp+24:
addiu $5,$sp,24
jalr $25
addiu $4,$4,%lo($LC1) ; slot de délai de branchement
; appel de printf() :
lw $28,16($sp)
; définir le 2nd argument de printf(),
; charger un mot à l'adresse $sp+24:
lw $5,24($sp)
lw $25,%call16(printf)($28)
lui $4,%hi($LC2)
jalr $25
addiu $4,$4,%lo($LC2) ; slot de délai de branchement
; épilogue de la fonction:
lw $31,36($sp)
; mettre la valeur de retour à 0:
move $2,$0
; retourner:
j $31
addiu $sp,$sp,40 ; slot de délai de branchement
102
; définir le 2nd argument de scanf(), $a1=$sp+24:
.text :00000030 addiu $a1, $sp, 0x28+var_10
.text :00000034 jalr $t9 ; slot de délai de branchement
.text :00000038 la $a0, ($LC1 & 0xFFFF) # "%d"
; appel de printf() :
.text :0000003C lw $gp, 0x28+var_18($sp)
; définir le 2nd argument de printf(),
; charger un mot à l'adresse $sp+24:
.text :00000040 lw $a1, 0x28+var_10($sp)
.text :00000044 lw $t9, (printf & 0xFFFF)($gp)
.text :00000048 lui $a0, ($LC2 >> 16) # "You entered %d...\n"
.text :0000004C jalr $t9
.text :00000050 la $a0, ($LC2 & 0xFFFF) # "You entered
%d...\n" ; slot de délai de branchement
; épilogue de la fonction:
.text :00000054 lw $ra, 0x28+var_4($sp)
; mettre la valeur de retour à 0:
.text :00000058 move $v0, $zero
; retourner:
.text :0000005C jr $ra
.text :00000060 addiu $sp, 0x28 ; slot de délai de branchement
int main()
{
int x ;
printf ("Enter X :\n") ;
return 0;
};
Donc que se passe-t-il ici? x n’est pas initialisée et contient des données aléatoires
de la pile locale. Lorsque scanf() est appelée, elle prend la chaîne de l’utilisateur, la
convertit en nombre et essaye de l’écrire dans x, la considérant comme une adresse
en mémoire. Mais il s’agit de bruit aléatoire, donc scanf() va essayer d’écrire à une
adresse aléatoire. Très probablement, le processus va planter.
Assez intéressant, certaines bibliothèques CRT compilées en debug, mettent un signe
distinctif lors de l’allocation de la mémoire, comme 0xCCCCCCCC ou 0x0BADF00D
etc. Dans ce cas, x peut contenir 0xCCCCCCCC, et scanf() va essayer d’écrire à
l’adresse 0xCCCCCCCC. Et si vous remarquez que quelque chose dans votre pro-
cessus essaye d’écrire à l’adresse 0xCCCCCCCC, vous saurez qu’une variable non
103
initialisée (ou un pointeur) a été utilisée sans initialisation préalable. C’est mieux
que si la mémoire nouvellement allouée est juste mise à zéro.
int main()
{
printf ("Enter X :\n") ;
return 0;
};
MSVC: x86
_DATA SEGMENT
COMM _x :DWORD
$SG2456 DB 'Enter X :', 0aH, 00H
$SG2457 DB '%d', 00H
$SG2458 DB 'You entered %d...', 0aH, 00H
_DATA ENDS
PUBLIC _main
EXTRN _scanf :PROC
EXTRN _printf :PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_main PROC
push ebp
mov ebp, esp
push OFFSET $SG2456
call _printf
add esp, 4
push OFFSET _x
push OFFSET $SG2457
call _scanf
add esp, 8
mov eax, DWORD PTR _x
push eax
push OFFSET $SG2458
104
call _printf
add esp, 8
xor eax, eax
pop ebp
ret 0
_main ENDP
_TEXT ENDS
Dans ce cas, la variable x est définie dans la section _DATA et il n’y a pas de mémoire
allouée sur la pile locale. Elle est accédée directement, pas par la pile. Les variables
globales non initialisées ne prennent pas de place dans le fichier exécutable (en effet,
pourquoi aurait-on besoin d’allouer de l’espace pour des variables initialement mises
à zéro ?), mais lorsque quelqu’un accède à leur adresse, l’OS va y allouer un bloc de
zéros72 .
Maintenant, assignons explicitement une valeur à la variable:
int x=10; // valeur par défaut
Nous obtenons:
_DATA SEGMENT
_x DD 0aH
...
Ici nous voyons une valeur 0xA de type DWORD (DD signifie DWORD = 32 bit) pour
cette variable.
Si vous ouvrez le .exe compilé dans IDA, vous pouvez voir la variable x placée au
début du segment _DATA, et après elle vous pouvez voir la chaîne de texte.
Si vous ouvrez le .exe compilé de l’exemple précédent dans IDA, où la valeur de x
n’était pas mise, vous verrez quelque chose comme ça:
_x est marquée avec ? avec le reste des variables qui ne doivent pas être initialisées.
Ceci implique qu’après avoir chargé le .exe en mémoire, de l’espace pour toutes
72. C’est comme ça que se comportent les VM
105
ces variables doit être alloué et rempli avec des zéros [ISO/IEC 9899:TC3 (C C99
standard), (2007)6.7.8p10]. Mais dans le fichier .exe, ces variables non initialisées
n’occupent rien du tout. C’est pratique pour les gros tableaux, par exemple.
106
MSVC: x86 + OllyDbg
107
Dans OllyDbg nous pouvons examiner l’espace mémoire du processus (Alt-M) et
nous pouvons voir que cette adresse se trouve dans le segment PE .data de notre
programme:
GCC: x86
Le schéma sous Linux est presque le même, avec la différence que les variables
non initialisées se trouvent dans le segment _bss. Dans un fichier ELF73 ce segment
possède les attributs suivants:
; Segment type : Uninitialized
; Segment permissions : Read/Write
Si toutefois vous initialisez la variable avec une valeur quelconque, e.g. 10, elle sera
placée dans le segment _data, qui possède les attributs suivants:
; Segment type : Pure data
; Segment permissions : Read/Write
73. Executable and Linkable Format: Format de fichier exécutable couramment utilisé sur les systèmes
*NIX, Linux inclus
108
MSVC: x64
_TEXT SEGMENT
main PROC
$LN3 :
sub rsp, 40
; retourner 0
xor eax, eax
add rsp, 40
ret 0
main ENDP
_TEXT ENDS
Le code est presque le même qu’en x86. Notez toutefois que l’adresse de la variable
x est passée à scanf() en utilisant une instruction LEA, tandis que la valeur de
la variable est passée au second printf() en utilisant une instruction MOV. DWORD
PTR—fait partie du langage d’assemblage (aucune relation avec le code machine),
indique que la taille de la variable est 32-bit et que l’instruction MOV doit être encodée
en conséquence.
109
.text :0000000C BL __0scanf
.text :00000010 LDR R0, =x
.text :00000012 LDR R1, [R0]
.text :00000014 ADR R0, aYouEnteredD___ ; "You entered %d...\n"
.text :00000016 BL __2printf
.text :0000001A MOVS R0, #0
.text :0000001C POP {R4,PC}
...
.text :00000020 aEnterX DCB "Enter X :",0xA,0 ; DATA XREF: main+2
.text :0000002A DCB 0
.text :0000002B DCB 0
.text :0000002C off_2C DCD x ; DATA XREF: main+8
.text :0000002C ; main+10
.text :00000030 aD DCB "%d",0 ; DATA XREF: main+A
.text :00000033 DCB 0
.text :00000034 aYouEnteredD___ DCB "You entered %d...",0xA,0 ; DATA XREF:
main+14
.text :00000047 DCB 0
.text :00000047 ; .text ends
.text :00000047
...
.data :00000048 ; Segment type: Pure data
.data :00000048 AREA .data, DATA
.data :00000048 ; ORG 0x48
.data :00000048 EXPORT x
.data :00000048 x DCD 0xA ; DATA XREF: main+8
.data :00000048 ; main+10
.data :00000048 ; .data ends
Donc, la variable x est maintenant globale, et pour cette raison, elle se trouve dans
un autre segment, appelé le segment de données (.data). On pourrait demander
pour quoi les chaînes de textes sont dans le segment de code (.text) et x là. C’est
parque c’est une variable et que par définition sa valeur peut changer. En outre, elle
peut même changer souvent. Alors que les chaînes de texte ont un type constant,
elles ne changent pas, donc elles sont dans le segment .text.
Le segment de code peut parfois se trouver dans la ROM74 d’un circuit (gardez à
l’esprit que nous avons maintenant affaire avec de l’électronique embarquée, et
que la pénurie de mémoire y est courante), et les variables —en RAM.
Il n’est pas très économique de stocker des constantes en RAM quand vous avez de
la ROM.
En outre, les variables en RAM doivent être initialisées, car après le démarrage, la
RAM, évidemment, contient des données aléatoires.
En avançant, nous voyons un pointeur sur la variable x (off_2C) dans le segment de
code, et que toutes les opérations avec cette variable s’effectuent via ce pointeur.
Car la variable x peut se trouver loin de ce morceau de code, donc son adresse doit
être sauvée proche du code.
74. Mémoire morte
110
L’instruction LDR en mode Thumb ne peut adresser des variables que dans un inter-
valle de 1020 octets de son emplacement.
et en mode ARM —l’intervalle des variables est de ±4095 octets.
Et donc l’adresse de la variable x doit se trouver quelque part de très proche, car
il n’y a pas de garantie que l’éditeur de liens pourra stocker la variable proche du
code, elle peut même se trouver sur un module de mémoire externe.
Encore une chose: si une variable est déclarée comme const, le compilateur Keil va
l’allouer dans le segment .constdata.
Peut-être qu’après, l’éditeur de liens mettra ce segment en ROM aussi, à côté du
segment de code.
ARM64
111
36 ldp x29, x30, [sp], 16
37 ret
Dans ce car la variable x est déclarée comme étant globale et son adresse est cal-
culée en utilisant la paire d’instructions ADRP/ADD (lignes 21 et 25).
MIPS
112
.text :00400714 la $a0, aYouEnteredD___ # "You entered
%d...\n" ; slot de délai de branchement
; épilogue de la fonction:
.text :00400718 lw $ra, 0x20+var_4($sp)
.text :0040071C move $v0, $zero
.text :00400720 jr $ra
.text :00400724 addiu $sp, 0x20 ; slot de délai de branchement
...
IDA réduit le volume des informations, donc nous allons générer un listing avec obj-
dump et le commenter:
113
34 400720: 03e00008 jr ra
35 400724: 27bd0020 addiu sp,sp,32 ; slot de délai de branchement
36 ; groupe de NOPs servant à aligner la prochaine fonction sur un bloc de
16-octet:
37 400728: 00200825 move at,at
38 40072c : 00200825 move at,at
Nous voyons maintenant que l’adresse de la variable x est lue depuis un buffer de
64KiB en utilisant GP et en lui ajoutant un offset négatif (ligne 18). Plus que ça, les
adresses des trois fonctions externes qui sont utilisées dans notre exemple (puts(),
scanf(), printf()), sont aussi lues depuis le buffer de données globale en utilisant
GP (lignes 9, 16 et 26). GP pointe sur le milieu du buffer, et de tels offsets suggèrent
que les adresses des trois fonctions, et aussi l’adresse de la variable x, sont toutes
stockées quelque part au début du buffer. Cela fait du sens, car notre exemple est
minuscule.
Une autre chose qui mérite d’être mentionnée est que la fonction se termine avec
deux NOPs (MOVE $AT,$AT — une instruction sans effet), afin d’aligner le début de
la fonction suivante sur un bloc de 16-octet.
114
.text :004006D8 addiu $a1, $s0, (x - 0x410000)
; maintenant l'adresse de x est dans $a1.
.text :004006DC jalr $t9 ; __isoc99_scanf
.text :004006E0 la $a0, aD # "%d"
.text :004006E4 lw $gp, 0x20+var_10($sp)
; prendre un mot dans la mémoire:
.text :004006E8 lw $a1, x
; la valeur de x est maintenant dans $a1.
.text :004006EC la $t9, printf
.text :004006F0 lui $a0, 0x40
.text :004006F4 jalr $t9 ; printf
.text :004006F8 la $a0, aYouEnteredD___ # "You entered %d...\n"
.text :004006FC lw $ra, 0x20+var_4($sp)
.text :00400700 move $v0, $zero
.text :00400704 lw $s0, 0x20+var_8($sp)
.text :00400708 jr $ra
.text :0040070C addiu $sp, 0x20
...
Pourquoi pas .sdata? Peut-être que cela dépend d’une option de GCC?
Néanmoins, maintenant x est dans .data, qui une zone mémoire générale, et nous
pouvons regarder comment y travailler avec des variables.
L’adresse de la variable doit être formée en utilisant une paire d’instructions.
Dans notre cas, ce sont LUI («Load Upper Immediate ») et ADDIU («Add Immediate
Unsigned Word »).
Voici le listing d’objdump pour y regarder de plus près:
115
; maintenant l'adresse de x est dans $a1.
4006dc : 0320f809 jalr t9
4006e0 : 248408dc addiu a0,a0,2268
4006e4 : 8fbc0010 lw gp,16(sp)
; la partie haute de l'adresse de x est toujours dans $s0.
; lui ajouter la partie basse et charger un mot de la mémoire:
4006e8 : 8e050920 lw a1,2336(s0)
; la valeur de x est maintenant dans $a1.
4006ec : 8f99803c lw t9,-32708(gp)
4006f0 : 3c040040 lui a0,0x40
4006f4 : 0320f809 jalr t9
4006f8 : 248408e0 addiu a0,a0,2272
4006fc : 8fbf001c lw ra,28(sp)
400700: 00001021 move v0,zero
400704: 8fb00018 lw s0,24(sp)
400708: 03e00008 jr ra
40070c : 27bd0020 addiu sp,sp,32
Nous voyons que l’adresse est formée en utilisant LUI et ADDIU, mais la partie haute
de l’adresse est toujours dans le registre $S0, et il est possible d’encoder l’offset en
une instruction LW («Load Word »), donc une seule instruction LW est suffisante pour
charger une valeur de la variable et la passer à printf().
Les registres contenant des données temporaires sont préfixés avec T-, mais ici nous
en voyons aussi qui sont préfixés par S-, leur contenu doit être doit être sauvegardé
quelque part avant de les utiliser dans d’autres fonctions.
C’est pourquoi la valeur de $S0 a été mise à l’adresse 0x4006cc et utilisée de nou-
veau à l’adresse 0x4006e8, après l’appel de scanf(). La fonction scanf() ne change
pas cette valeur.
1.12.4 scanf()
Comme il a déjà été écrit, il est plutôt dépassé d’utiliser scanf() aujourd’hui. Mais
si nous devons, il faut vérifier si scanf() se termine correctement sans erreur.
#include <stdio.h>
int main()
{
int x ;
printf ("Enter X :\n") ;
return 0;
};
116
Par norme, la fonction scanf()75 renvoie le nombre de champs qui ont été lus avec
succès.
Dans notre cas, si tout se passe bien et que l’utilisateur entre un nombre scanf()
renvoie 1, ou en cas d’erreur (ou EOF76 ) — 0.
Ajoutons un peu de code C pour vérifier la valeur de retour de scanf() et afficher
un message d’erreur en cas d’erreur.
Cela fonctionne comme attendu:
C :\...>ex3.exe
Enter X :
123
You entered 123...
C :\...>ex3.exe
Enter X :
ouch
What you entered ? Huh ?
MSVC: x86
117
Une instruction de saut conditionnelle JNE suit l’instruction CMP. JNE signifie Jump if
Not Equal (saut si non égal).
Donc, si la valeur dans le registre EAX n’est pas égale à 1, le CPU va poursuivre
l’exécution à l’adresse mentionnée dans l’opérande JNE, dans notre cas $LN2@main.
Passez le contrôle à cette adresse résulte en l’exécution par le CPU de printf() avec
l’argument What you entered? Huh?. Mais si tout est bon, le saut conditionnel n’est
pas pris, et un autre appel à printf() est exécuté, avec deux arguments:
'You entered %d...' et la valeur de x.
Puisque dans ce cas le second printf() n’a pas été exécuté, il y a un JMP qui le
précède (saut inconditionnel). Il passe le contrôle au point après le second printf()
et juste avant l’instruction XOR EAX, EAX, qui implémente return 0.
Donc, on peut dire que comparer une valeur avec une autre est usuellement im-
plémenté par la paire d’instructions CMP/Jcc, où cc est un code de condition. CMP
compare deux valeurs et met les flags77 du processeur. Jcc vérifie ces flags et dé-
cide de passer le contrôle à l’adresse spécifiée ou non.
Cela peut sembler paradoxal, mais l’instruction CMP est en fait un SUB (soustraction).
Toutes les instructions arithmétiques mettent les flags du processeur, pas seulement
CMP. Si nous comparons 1 et 1, 1 − 1 donne 0 donc le flag ZF va être mis (signifiant
que le dernier résultat est 0). Dans aucune autre circonstance ZF ne sera mis, à
l’exception que les opérandes ne soient égaux. JNE vérifie seulement le flag ZF et
saute seulement si il n’est pas mis. JNE est un synonyme pour JNZ (Jump if Not Zero
(saut si non zéro)). L’assembleur génère le même opcode pour les instructions JNE et
JNZ. Donc, l’instruction CMP peut être remplacée par une instruction SUB et presque
tout ira bien, à la différence que SUB altère la valeur du premier opérande. CMP est
un SUB sans sauver le résultat, mais modifiant les flags.
C’est le moment de lancer IDA et d’essayer de faire quelque chose avec. À propos,
pour les débutants, c’est une bonne idée d’utiliser l’option /MD de MSVC, qui signifie
que toutes les fonctions standards ne vont pas être liées avec le fichier exécutable,
mais vont à la place être importées depuis le fichier MSVCR*.DLL. Ainsi il est plus
facile de voir quelles fonctions standards sont utilisées et où.
En analysant du code dans IDA, il est très utile de laisser des notes pour soi-même
(et les autres). En la circonstance, analysons cet exemple, nous voyons que JNZ sera
déclenché en cas d’erreur. Donc il est possible de déplacer le curseur sur le label, de
presser «n » et de lui donner le nom «error ». Créons un autre label—dans «exit ».
Voici mon résultat:
.text :00401000 _main proc near
.text :00401000
.text :00401000 var_4 = dword ptr -4
.text :00401000 argc = dword ptr 8
.text :00401000 argv = dword ptr 0Ch
.text :00401000 envp = dword ptr 10h
.text :00401000
118
.text :00401000 push ebp
.text :00401001 mov ebp, esp
.text :00401003 push ecx
.text :00401004 push offset Format ; "Enter X:\n"
.text :00401009 call ds :printf
.text :0040100F add esp, 4
.text :00401012 lea eax, [ebp+var_4]
.text :00401015 push eax
.text :00401016 push offset aD ; "%d"
.text :0040101B call ds :scanf
.text :00401021 add esp, 8
.text :00401024 cmp eax, 1
.text :00401027 jnz short error
.text :00401029 mov ecx, [ebp+var_4]
.text :0040102C push ecx
.text :0040102D push offset aYou ; "You entered %d...\n"
.text :00401032 call ds :printf
.text :00401038 add esp, 8
.text :0040103B jmp short exit
.text :0040103D
.text :0040103D error : ; CODE XREF: _main+27
.text :0040103D push offset aWhat ; "What you entered? Huh?\n"
.text :00401042 call ds :printf
.text :00401048 add esp, 4
.text :0040104B
.text :0040104B exit : ; CODE XREF: _main+3B
.text :0040104B xor eax, eax
.text :0040104D mov esp, ebp
.text :0040104F pop ebp
.text :00401050 retn
.text :00401050 _main endp
119
.text :00401048 add esp, 4
.text :0040104B
.text :0040104B exit : ; CODE XREF: _main+3B
.text :0040104B xor eax, eax
.text :0040104D mov esp, ebp
.text :0040104F pop ebp
.text :00401050 retn
.text :00401050 _main endp
Pour étendre les parties de code précédemment cachées. utilisez «+ » sur le pavé
numérique.
120
En appuyant sur «space », nous voyons comment IDA représente une fonction sous
forme de graphe:
Il y a deux flèches après chaque saut conditionnel: une verte et une rouge. La flèche
verte pointe vers le bloc qui sera exécuté si le saut est déclenché, et la rouge sinon.
121
Il est possible de replier des nœuds dans ce mode et de leurs donner aussi un nom
(«group nodes »). Essayons avec 3 blocs:
C’est très pratique. On peut dire qu’une part importante du travail des rétro-ingénieurs
(et de tout autre chercheur également) est de réduire la quantité d’information avec
laquelle travailler.
122
MSVC: x86 + OllyDbg
Essayons de hacker notre programme dans OllyDbg, pour le forcer à penser que
scanf() fonctionne toujours sans erreur. Lorsque l’adresse d’une variable locale est
passée à scanf(), la variable contient initiallement toujours des restes de données
aléatoires, dans ce cas 0x6E494714 :
123
Lorsque scanf() s’exécute dans la console, entrons quelque chose qui n’est pas du
tout un nombre, comme «asdasd ». scanf() termine avec 0 dans EAX, ce qui indique
qu’une erreur s’est produite:
Nous pouvons vérifier la variable locale dans le pile et noter qu’elle n’a pas changé.
En effet, qu’aurait écrit scanf() ici? Elle n’a simplement rien fait à part renvoyer
zéro.
Essayons de «hacker » notre programme. Clique-droit sur EAX, parmi les options il y
a «Set to 1 » (mettre à 1). C’est ce dont nous avons besoin.
Nous avons maintenant 1 dans EAX, donc la vérification suivante va s’exécuter comme
souhaiter et printf() va afficher la valeur de la variable dans la pile.
Lorsque nous lançons le programme (F9) nous pouvons voir ceci dans la fenêtre de
la console:
124
MSVC: x86 + Hiew
Cela peut également être utilisé comme un exemple simple de modification de fichier
exécutable. Nous pouvons essayer de modifier l’exécutable de telle sorte que le
programme va toujours afficher notre entrée, quelle quelle soit.
En supposant que l’exécutable est compilé avec la bibliothèque externe MSVCR*.DLL
(i.e., avec l’option /MD) 78 , nous voyons la fonction main() au début de la section
.text. Ouvrons l’exécutable dans Hiew et cherchons le début de la section .text
(Enter, F8, F6, Enter, Enter).
Nous pouvons voir cela:
Hiew trouve les chaîne ASCIIZ79 et les affiche, comme il le fait avec le nom des
fonctions importées.
125
Déplacez le curseur à l’adresse .00401027 (où se trouve l’instruction JNZ, que l’on
doit sauter), appuyez sur F3, et ensuite tapez «9090 » (qui signifie deux NOPs) :
126
MSVC: x64
Puisque nous travaillons ici avec des variables typées int, qui sont toujours 32-bit en
x86-64, nous voyons comment la partie 32-bit des registres (préfixés avec E-) est
également utilisée ici. Lorsque l’on travaille avec des ponteurs, toutefois, les parties
64-bit des registres sont utilisées, préfixés avec R-.
_TEXT SEGMENT
x$ = 32
main PROC
$LN5 :
sub rsp, 56
lea rcx, OFFSET FLAT :$SG2924 ; 'Enter X:'
call printf
lea rdx, QWORD PTR x$[rsp]
lea rcx, OFFSET FLAT :$SG2926 ; '%d'
call scanf
cmp eax, 1
jne SHORT $LN2@main
mov edx, DWORD PTR x$[rsp]
lea rcx, OFFSET FLAT :$SG2927 ; 'You entered %d...'
call printf
jmp SHORT $LN1@main
$LN2@main :
lea rcx, OFFSET FLAT :$SG2929 ; 'What you entered? Huh?'
call printf
$LN1@main :
; retourner 0
xor eax, eax
add rsp, 56
ret 0
main ENDP
_TEXT ENDS
END
ARM
PUSH {R3,LR}
127
ADR R0, aEnterX ; "Enter X:\n"
BL __2printf
MOV R1, SP
ADR R0, aD ; "%d"
BL __0scanf
CMP R0, #1
BEQ loc_1E
ADR R0, aWhatYouEntered ; "What you entered? Huh?\n"
BL __2printf
ARM64
128
15 adrp x0, .LC0
16 add x0, x0, :lo12 :.LC0
17 bl puts
18 ; charger le pointeur sur la chaîne "%d":
19 adrp x0, .LC1
20 add x0, x0, :lo12 :.LC1
21 ; calculer l'adresse de la variable x dans la pile locale
22 add x1, x29, 28
23 bl __isoc99_scanf
24 ; scanf() renvoie son résultat dans W0.
25 ; le vérifier:
26 cmp w0, 1
27 ; BNE est Branch if Not Equal (branchement si non égal)
28 ; donc if W0<>0, un saut en L2 sera effectué
29 bne .L2
30 ; à ce point W0=1, signifie pas d'erreur
31 ; charger la valeur de x depuis la pile locale
32 ldr w1, [x29,28]
33 ; charger le pointeur sur la chaîne "You entered %d...\n":
34 adrp x0, .LC2
35 add x0, x0, :lo12 :.LC2
36 bl printf
37 ; sauter le code, qui affiche la chaîne "What you entered? Huh?":
38 b .L3
39 .L2 :
40 ; charger le pointeur sur la chaîne "What you entered? Huh?":
41 adrp x0, .LC3
42 add x0, x0, :lo12 :.LC3
43 bl puts
44 .L3 :
45 ; retourner 0
46 mov w0, 0
47 ; restaurer FP et LR:
48 ldp x29, x30, [sp], 32
49 ret
MIPS
129
.text :004006B0 sw $gp, 0x28+var_18($sp)
.text :004006B4 la $t9, puts
.text :004006B8 lui $a0, 0x40
.text :004006BC jalr $t9 ; puts
.text :004006C0 la $a0, aEnterX # "Enter X:"
.text :004006C4 lw $gp, 0x28+var_18($sp)
.text :004006C8 lui $a0, 0x40
.text :004006CC la $t9, __isoc99_scanf
.text :004006D0 la $a0, aD # "%d"
.text :004006D4 jalr $t9 ; __isoc99_scanf
.text :004006D8 addiu $a1, $sp, 0x28+var_10 # branch delay slot
.text :004006DC li $v1, 1
.text :004006E0 lw $gp, 0x28+var_18($sp)
.text :004006E4 beq $v0, $v1, loc_40070C
.text :004006E8 or $at, $zero # branch delay slot, NOP
.text :004006EC la $t9, puts
.text :004006F0 lui $a0, 0x40
.text :004006F4 jalr $t9 ; puts
.text :004006F8 la $a0, aWhatYouEntered # "What you entered?
Huh?"
.text :004006FC lw $ra, 0x28+var_4($sp)
.text :00400700 move $v0, $zero
.text :00400704 jr $ra
.text :00400708 addiu $sp, 0x28
scanf() renvoie le résultat de son traitement dans le registre $V0. Il est testé à
l’adresse 0x004006E4 en comparant la valeur dans $V0 avec celle dans $V1 (1 a
été stocké dans $V1 plus tôt, en 0x004006DC). BEQ signifie «Branch Equal » (bran-
chement si égal). Si les deux valeurs sont égales (i.e., succès), l’exécution saute à
l’adresse 0x0040070C.
Exercice
Comme nous pouvons voir, les instructions JNE/JNZ peuvent facilement être rempla-
cées par JE/JZ et vice-versa (ou BNE par BEQ et vice-versa). Mais les blocs de base
doivent aussi être échangés. Essayez de faire cela pour quelques exemples.
1.12.5 Exercice
• http://challenges.re/53
130
1.13 Intéressant à noter: variables globales vs. lo-
cales
Maintenant vous savez que les variables globales sont remplies avec des zéros par
l’OS au début ( 1.12.3 on page 105, [ISO/IEC 9899:TC3 (C C99 standard), (2007)6.7.8p10]),
mais que les variables locales ne le sont pas.
Parfois, vous avez une variable globale que vous avez oublié d’initialiser et votre pro-
gramme fonctionne grâce au fait qu’elle est à zéro au début. Puis, vous éditez votre
programme et déplacez la variable globale dans une fonction pour la rendre locale.
Elle ne sera plus initialisée à zéro et ceci peut résulter en de méchants bogues.
int main()
{
printf ("%d\n", f(1, 2, 3)) ;
return 0;
};
1.14.1 x86
MSVC
131
ret 0
_f ENDP
_main PROC
push ebp
mov ebp, esp
push 3 ; 3ème argument
push 2 ; 2ème argument
push 1 ; 1er argument
call _f
add esp, 12
push eax
push OFFSET $SG2463 ; '%d', 0aH, 00H
call _printf
add esp, 8
; retourner 0
xor eax, eax
pop ebp
ret 0
_main ENDP
Ce que l’on voit, c’est que la fonction main() pousse 3 nombres sur la pile et appelle
f(int,int,int).
L’accès aux arguments à l’intérieur de f() est organisé à l’aide de macros comme:
_a$ = 8, de la même façon que pour les variables locales, mais avec des offsets
positifs (accédés avec plus). Donc, nous accédons à la partie hors de la structure
locale de pile en ajoutant la macro _a$ à la valeur du registre EBP.
Ensuite, la valeur de a est stockée dans EAX. Après l’exécution de l’instruction IMUL,
la valeur de EAX est le produit de la valeur de EAX et du contenu de _b.
Après cela, ADD ajoute la valeur dans _c à EAX.
La valeur dans EAX n’a pas besoin d’être déplacée/copiée : elle est déjà là où elle
doit être. Lors du retour dans la fonction appelante, elle prend la valeur dans EAX et
l’utilise comme argument pour printf().
MSVC + OllyDbg
Illustrons ceci dans OllyDbg. Lorsque nous traçons jusqu’à la première instruction
de f() qui utilise un des arguments (le premier), nous voyons qu’EBP pointe sur la
structure de pile locale, qui est entourée par un rectangle rouge.
Le premier élément de la structure de pile locale est la valeur sauvegardée de EBP,
le second est RA, le troisième est le premier argument de la fonction, puis le second
et le troisième.
Pour accéder au premier argument de la fonction, on doit ajouter exactement 8 (2
mots de 32-bit) à EBP.
OllyDbg est au courant de cela, c’est pourquoi il a ajouté des commentaires aux
éléments de la pile comme
132
«RETURN from » et «Arg1 = … », etc.
N.B.: Les arguments de la fonction ne font pas partie de la structure de pile de la
fonction, ils font plutôt partie de celle de la fonction appelante.
Par conséquent, OllyDbg a marqué les éléments comme appartenant à une autre
structure de pile.
GCC
Compilons le même code avec GCC 4.4.1 et regardons le résultat dans IDA :
push ebp
mov ebp, esp
mov eax, [ebp+arg_0] ; 1er argument
imul eax, [ebp+arg_4] ; 2ème argument
add eax, [ebp+arg_8] ; 3ème argument
pop ebp
retn
f endp
public main
main proc near
133
var_10 = dword ptr -10h
var_C = dword ptr -0Ch
var_8 = dword ptr -8
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov [esp+10h+var_8], 3 ; 3ème argument
mov [esp+10h+var_C], 2 ; 2ème argument
mov [esp+10h+var_10], 1 ; 1er argument
call f
mov edx, offset aD ; "%d\n"
mov [esp+10h+var_C], eax
mov [esp+10h+var_10], edx
call _printf
mov eax, 0
leave
retn
main endp
Le résultat est presque le même, avec quelques différences mineures discutées pré-
cédemment.
Le pointeur de pile n’est pas remis après les deux appels de fonction (f et printf), car
la pénultième instruction LEAVE ( .1.6 on page 1344) s’en occupe à la fin.
1.14.2 x64
Le scénario est un peu différent en x86-64. Les arguments de la fonction (les 4 ou 6
premiers d’entre eux) sont passés dans des registres i.e. l’appelée les lit depuis des
registres au lieu de les lire dans la pile.
MSVC
main PROC
sub rsp, 40
mov edx, 2
lea r8d, QWORD PTR [rdx+1] ; R8D=3
lea ecx, QWORD PTR [rdx-1] ; ECX=1
call f
lea rcx, OFFSET FLAT :$SG2997 ; '%d'
mov edx, eax
call printf
xor eax, eax
add rsp, 40
134
ret 0
main ENDP
f PROC
; ECX - 1er argument
; EDX - 2ème argument
; R8D - 3ème argument
imul ecx, edx
lea eax, DWORD PTR [r8+rcx]
ret 0
f ENDP
Comme on peut le voir, la fonction compacte f() prend tous ses arguments dans
des registres.
La fonction LEA est utilisée ici pour l’addition, apparemment le compilateur considère
qu’elle plus rapide que ADD.
LEA est aussi utilisée dans la fonction main() pour préparer le premier et le troisième
argument de f(). Le compilateur doit avoir décidé que cela s’exécutera plus vite que
la façon usuelle ce charger des valeurs dans les registres, qui utilise l’instruction MOV.
Regardons ce qu’a généré MSVC sans optimisation:
; shadow space:
arg_0 = dword ptr 8
arg_8 = dword ptr 10h
arg_10 = dword ptr 18h
135
; retourner 0
xor eax, eax
add rsp, 28h
retn
main endp
C’est un peu déroutant, car les 3 arguments dans des registres sont sauvegardés
sur la pile pour une certaine raison. Ceci est appelé «shadow space » 81 : chaque
Win64 peut (mais ce n’est pas requis) y sauver les 4 registres. Ceci est fait pour deux
raisons: 1) c’est trop généreux d’allouer un registre complet (et même 4 registres)
pour un argument en entrée, donc il sera accédé par la pile; 2) le debugger sait
toujours où trouver les arguments de la fonction lors d’un arrêt 82 .
Donc, de grosses fonctions peuvent sauvegarder leurs arguments en entrée dans
le «shadows space » si elle veulent les utiliser pendant l’exécution, mais quelques
petites fonctions (comme la notre) peuvent ne pas le faire.
C’est la responsabilité de l’appelant d’allouer le «shadow space » sur la pile.
GCC
main :
sub rsp, 8
mov edx, 3
mov esi, 2
mov edi, 1
call f
mov edi, OFFSET FLAT :.LC0 ; "%d\n"
mov esi, eax
xor eax, eax ; nombre de registres vectoriel passés
call printf
xor eax, eax
add rsp, 8
ret
81. MSDN
82. MSDN
136
f :
; EDI - 1er argument
; ESI - 2ème argument
; EDX - 3ème argument
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov DWORD PTR [rbp-12], edx
mov eax, DWORD PTR [rbp-4]
imul eax, DWORD PTR [rbp-8]
add eax, DWORD PTR [rbp-12]
leave
ret
main :
push rbp
mov rbp, rsp
mov edx, 3
mov esi, 2
mov edi, 1
call f
mov edx, eax
mov eax, OFFSET FLAT :.LC0 ; "%d\n"
mov esi, edx
mov rdi, rax
mov eax, 0 ; nombre de registres vectoriel passés
call printf
mov eax, 0
leave
ret
Il n’y a pas d’exigeance de «shadow space » en System V *NIX ([Michael Matz, Jan Hu-
bicka, Andreas Jaeger, Mark Mitchell, System V Application Binary Interface. AMD64
Architecture Processor Supplement, (2013)] 83 ), mais l’appelée peut vouloir sauve-
garder ses arguments quelque part en cas de manque de registres.
Notre exemple fonctionne avec des int 32-bit, c’est pourquoi c’est la partie 32-bit
des registres qui est utilisée (préfixée par E-).
Il peut être légèrement modifié pour utiliser des valeurs 64-bit:
#include <stdio.h>
#include <stdint.h>
137
int main()
{
printf ("%lld\n", f(0x1122334455667788,
0x1111111122222222,
0x3333333344444444)) ;
return 0;
};
Le code est le même, mais cette fois les registres complets (préfixés par R-) sont
utilisés.
1.14.3 ARM
sans optimisation Keil 6/2013 (Mode ARM)
138
.text :000000D0 E3 18 00 EB BL __2printf
.text :000000D4 00 00 A0 E3 MOV R0, #0
.text :000000D8 10 80 BD E8 LDMFD SP !, {R4,PC}
La fonction main() appelle simplement deux autres fonctions, avec trois valeurs
passées à la première —(f()).
Comme il y déjà été écrit, en ARM les 4 premières valeurs sont en général passées
par les 4 premiers registres (R0-R3).
La fonction f(), comme il semble, utilise les 3 premiers registres (R0-R2) comme
arguments.
L’instruction MLA (Multiply Accumulate) multiplie ses deux premiers opérandes (R3
et R1), additionne le troisième opérande (R2) au produit et stocke le résultat dans
le registre zéro (R0), par lequel, d’après le standard, les fonctions retournent leur
résultat.
La multiplication et l’addition en une fois (Fused multiply–add) est une instruction
très utile. À propos, il n’y avait pas une telle instruction en x86 avant les instructions
FMA apparues en SIMD 84 .
La toute première instruction MOV R3, R0, est, apparemment, redondante (car une
seule instruction MLA pourrait être utilisée à la place ici). Le compilateur ne l’a pas
optimisé, puisqu’il n’y a pas l’option d’optimisation.
L’instruction BX rend le contrôle à l’adresse stockée dans le registre LR et, si néces-
saire, change le mode du processeur de Thumb à ARM et vice versa. Ceci peut être
nécessaire puisque, comme on peut le voir, la fonction f() n’est pas au courant
depuis quel sorte de code elle peut être appelée, ARM ou Thumb. Ainsi, si elle est
appelée depuis du code Thumb, BX ne va pas seulement retourner le contrôle à la
fonction appelante, mais également changer le mode du processeur à Thumb. Ou ne
pas changer si la fonction a été appelée depuis du code ARM [ARM(R) Architecture
Reference Manual, ARMv7-A and ARMv7-R edition, (2012)A2.3.2].
.text :00000098 f
.text :00000098 91 20 20 E0 MLA R0, R1, R0, R2
.text :0000009C 1E FF 2F E1 BX LR
Et voilà la fonction f() compilée par le compilateur Keil en mode optimisation maxi-
male (-O3).
L’instruction MOV a été supprimée par l’optimisation (ou réduite) et maintenant MLA
utilise tout les registres contenant les données en entrée et place ensuite le résultat
directement dans R0, exactement où la fonction appelante va le lire et l’utiliser.
139
.text :0000005E 48 43 MULS R0, R1
.text :00000060 80 18 ADDS R0, R0, R2
.text :00000062 70 47 BX LR
L’instruction MLA n’est pas disponible dans le mode Thumb, donc le compilateur gé-
nère le code effectuant ces deux opérations (multiplication et addition) séparément.
Tout d’abord, la première instruction MULS multiplie R0 par R1, laissant le résultat
dans le registreR0. La seconde instruction (ADDS) ajoute le résultat et R2 laissant le
résultat dans le registre R0.
ARM64
Tout ce qu’il y a ici est simple. MADD est juste une instruction qui effectue une multi-
plication/addition fusionnées (similaire à l’instruction MLA que nous avons déjà vue).
Tous les 3 arguments sont passés dans la partie 32-bit de X-registres. Effectivement,
le type des arguments est int 32-bit. Le résultat est renvoyé dans W0.
main :
; sauver FP et LR dans la pile locale:
stp x29, x30, [sp, -16]!
mov w2, 3
mov w1, 2
add x29, sp, 0
mov w0, 1
bl f
mov w1, w0
adrp x0, .LC7
add x0, x0, :lo12 :.LC7
bl printf
; retourner 0
mov w0, 0
; restaurer FP et LR
ldp x29, x30, [sp], 16
ret
.LC7 :
.string "%d\n"
140
#include <stdint.h>
int main()
{
printf ("%lld\n", f(0x1122334455667788,
0x1111111122222222,
0x3333333344444444)) ;
return 0;
};
f :
madd x0, x0, x1, x2
ret
main :
mov x1, 13396
adrp x0, .LC8
stp x29, x30, [sp, -16]!
movk x1, 0x27d0, lsl 16
add x0, x0, :lo12 :.LC8
movk x1, 0x122, lsl 32
add x29, sp, 0
movk x1, 0x58be, lsl 48
bl printf
mov w0, 0
ldp x29, x30, [sp], 16
ret
.LC8 :
.string "%lld\n"
La fonction f() est la même, seulement les X-registres 64-bit sont utilisés entière-
ment maintenant. Les valeurs longues sur 64-bit sont chargées dans les registres
par partie, c’est également décrit ici: 1.39.3 on page 569.
141
ldr w0, [sp,4]
add w0, w1, w0
add sp, sp, 16
ret
Le code sauve ses arguments en entrée dans la pile locale, dans le cas où quelqu’un
(ou quelque chose) dans cette fonction aurait besoin d’utiliser les registres W0...W2.
Cela évite d’écraser les arguments originels de la fonction, qui pourraient être de
nouveau utilisés par la suite.
Cela est appelé Zone de sauvegarde de registre. [Procedure Call Standard for the
ARM 64-bit Architecture (AArch64), (2013)]85 . L’appelée, toutefois, n’est pas obligée
de les sauvegarder. C’est un peu similaire au «Shadow Space » : 1.14.2 on page 136.
Pourquoi est-ce que GCC 4.9 avec l’option d’optimisation supprime ce code de sau-
vegarde? Parce qu’il a fait plus d’optimisation et en a conclu que les arguments de
la fonction n’allaient pas être utilisés par la suite et donc que les registres W0...W2
ne vont pas être utilisés.
Nous avons donc une paire d’instructions MUL/ADD au lieu d’un seul MADD.
1.14.4 MIPS
Listing 1.98: GCC 4.4.5 avec optimisation
.text :00000000 f :
; $a0=a
; $a1=b
; $a2=c
.text :00000000 mult $a1, $a0
.text :00000004 mflo $v0
.text :00000008 jr $ra
.text :0000000C addu $v0, $a2, $v0 ; slot de délai de
branchement
; au retour le résultat est dans $v0
.text :00000010 main :
.text :00000010
.text :00000010 var_10 = -0x10
.text :00000010 var_4 = -4
.text :00000010
.text :00000010 lui $gp, (__gnu_local_gp >> 16)
.text :00000014 addiu $sp, -0x20
.text :00000018 la $gp, (__gnu_local_gp & 0xFFFF)
.text :0000001C sw $ra, 0x20+var_4($sp)
.text :00000020 sw $gp, 0x20+var_10($sp)
; définir c:
.text :00000024 li $a2, 3
; définir a:
.text :00000028 li $a0, 1
.text :0000002C jal f
; définir b:
142
.text :00000030 li $a1, 2 ; slot de délai de
branchement
; le résultat est maintenant dans $v0
.text :00000034 lw $gp, 0x20+var_10($sp)
.text :00000038 lui $a0, ($LC0 >> 16)
.text :0000003C lw $t9, (printf & 0xFFFF)($gp)
.text :00000040 la $a0, ($LC0 & 0xFFFF)
.text :00000044 jalr $t9
; prend le résultat de la fonction f() et le passe
; en second argument à printf() :
.text :00000048 move $a1, $v0 ; slot de délai de
branchement
.text :0000004C lw $ra, 0x20+var_4($sp)
.text :00000050 move $v0, $zero
.text :00000054 jr $ra
.text :00000058 addiu $sp, 0x20 ; slot de délai de branchement
Les quatre premiers arguments de la fonction sont passés par quatre registres pré-
fixés par A-.
Il y a deux registres spéciaux en MIPS: HI et LO qui sont remplis avec le résultat
64-bit de la multiplication lors de l’exécution d’une instruction MULT.
Ces registres sont accessibles seulement en utilisant les instructions MFLO et MFHI.
Ici MFLO prend la partie basse du résultat de la multiplication et le stocke dans $V0.
Donc la partie haute du résultat de la multiplication est abandonnée (le contenu
du registre HI n’est pas utilisé). Effectivement: nous travaillons avec des types de
données int 32-bit ici.
Enfin, ADDU (« Add Unsigned » addition non signée) ajoute la valeur du troisième
argument au résultat.
Il y a deux instructions différentes pour l’addition en MIPS: ADD et ADDU. La diffé-
rence entre les deux n’est pas relative au fait d’être signé, mais aux exceptions. ADD
peut déclencher une exception lors d’un débordement, ce qui est parfois utile86 et
supporté en ADA LP, par exemple. ADDU ne déclenche pas d’exception lors d’un dé-
bordement. Comme C/C++ ne supporte pas ceci, dans notre exemple nous voyons
ADDU au lieu de ADD.
Le résultat 32-bit est laissé dans $V0.
Il y a une instruction nouvelle pour nous dans main() : JAL («Jump and Link »).
La différence entre JAL et JALR est qu’un offset relatif est encodé dans la première
instruction, tandis que JALR saute à l’adresse absolue stockée dans un registre
(«Jump and Link Register »).
Les deux fonctions f() et main() sont stockées dans le même fichier objet, donc
l’adresse relative de f() est connue et fixée.
86. http://blog.regehr.org/archives/1154
143
1.15 Plus loin sur le renvoi des résultats
87
En x86, le résultat de l’exécution d’une fonction est d’habitude renvoyé dans le
registre EAX.
Si il est de type octet ou un caractère (char), alors la partie basse du registre EAX
(AL) est utilisée. Si une fonction renvoie un nombre de type float, le registre ST(0)
du FPU est utilisé. En ARM, d’habitude, le résultat est renvoyé dans le registre R0.
En d’autres mots:
exit(main(argc,argv,envp)) ;
Si vous déclarez main() comme renvoyant void, rien ne sera renvoyé explicitement
(en utilisant la déclaration return), alors quelque chose d’inconnu, qui aura été sto-
cké dans la registre EAX lors de l’exécution de main() sera l’unique argument de la
fonction exit(). Il y aura probablement une valeur aléatoire, laissée lors de l’exécu-
tion de la fonction, donc le code de retour du programme est pseudo-aléatoire.
Illustrons ce fait: Notez bien que la fonction main() a un type de retour void :
#include <stdio.h>
void main()
{
printf ("Hello, world !\n") ;
};
144
Listing 1.99: GCC 4.8.1
.LC0 :
.string "Hello, world !"
main :
push ebp
mov ebp, esp
and esp, -16
sub esp, 16
mov DWORD PTR [esp], OFFSET FLAT :.LC0
call puts
leave
ret
Et lançons le:
$ tst.sh
Hello, world !
14
Dans le standard C++, le destructeur ne renvoie rien, mais lorsque Hex-Rays n’en
sait rien, et pense que le destructeur et cette fonction renvoient tout deux un int
...
145
1.15.2 Que se passe-t-il si on n’utilise pas le résultat de la
fonction?
printf() renvoie le nombre de caractères écrit avec succès, mais, en pratique, ce
résultat est rarement utilisé.
Il est aussi possible d’appeler une fonction dont la finalité est de renvoyer une valeur,
et de ne pas l’utiliser:
int f()
{
// skip first 3 random values:
rand() ;
rand() ;
rand() ;
// and use 4th:
return rand() ;
};
Le résultat de la fonction rand() est mis dans EAX, dans les quatre cas.
Mais dans les 3 premiers, la valeur dans EAX n’est pas utilisée.
146
rt.a=a+1;
rt.b=a+2;
rt.c=a+3;
return rt ;
};
Ici, le nom de la macro interne pour passer le pointeur sur une structure est $T3853.
Cet exemple peut être récrit en utilisant les extensions C99 du langage:
struct s
{
int a ;
int b ;
int c ;
};
147
retn
_get_some_values endp
1.16 Pointeurs
1.16.1 Renvoyer des valeurs
Les pointeurs sont souvent utilisés pour renvoyer des valeurs depuis les fonctions
(rappelez-vous le cas ( 1.12 on page 90) de scanf()).
Par exemple, lorsqu’une fonction doit renvoyer deux valeurs.
#include <stdio.h>
void main()
{
f1(123, 456, &sum, &product) ;
printf ("sum=%d, product=%d\n", sum, product) ;
};
_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_sum$ = 16 ; size = 4
_product$ = 20 ; size = 4
_f1 PROC
mov ecx, DWORD PTR _y$[esp-4]
mov eax, DWORD PTR _x$[esp-4]
lea edx, DWORD PTR [eax+ecx]
imul eax, ecx
148
mov ecx, DWORD PTR _product$[esp-4]
push esi
mov esi, DWORD PTR _sum$[esp]
mov DWORD PTR [esi], edx
mov DWORD PTR [ecx], eax
pop esi
ret 0
_f1 ENDP
_main PROC
push OFFSET _product
push OFFSET _sum
push 456 ; 000001c8H
push 123 ; 0000007bH
call _f1
mov eax, DWORD PTR _product
mov ecx, DWORD PTR _sum
push eax
push ecx
push OFFSET $SG2803
call DWORD PTR __imp__printf
add esp, 28
xor eax, eax
ret 0
_main ENDP
149
Regardons ceci dans OllyDbg :
Fig. 1.25: OllyDbg : les adresses des variables globales sont passées à f1()
Tout d’abord, les adresses des variables globales sont passées à f1(). Nous pouvons
cliquer sur «Follow in dump » sur l’élément de la pile, et nous voyons l’espace alloué
dans le segment de données pour les deux variables.
150
Ces variables sont mises à zéro, car les données non-initialisées (de BSS) sont effa-
cées avant le début de l’exécution, [voir ISO/IEC 9899:TC3 (C C99 standard), (2007)
6.7.8p10].
Elles se trouvent dans le segment de données, nous pouvons le vérifier en appuyant
sur Alt-M et en regardant la carte de la mémoire:
151
Traçons l’exécution (F7) jusqu’au début de f1() :
Deux valeurs sont visibles sur la pile: 456 (0x1C8) et 123 (0x7B), et aussi les adresses
des deux variables globales.
152
Suivons l’exécution jusqu’à la fin de f1(). Dans la fenêtre en bas à gauche, nous
voyons comment le résultat du calcul apparaît dans les variables globales:
153
Maintenant les valeurs des variables globales sont chargées dans des registres,
prêtes à être passées à printf() (via la pile) :
Fig. 1.29: OllyDbg : les adresses des variables globales sont passées à printf()
154
push 456 ; 000001c8H
push 123 ; 0000007bH
call _f1
; Line 14
mov edx, DWORD PTR _product$[esp+24]
mov eax, DWORD PTR _sum$[esp+24]
push edx
push eax
push OFFSET $SG2803
call DWORD PTR __imp__printf
; Line 15
xor eax, eax
add esp, 36
ret 0
155
Regardons à nouveau avec OllyDbg. Les adresses des variables locales dans la pile
sont 0x2EF854 et 0x2EF858. Voyons comment elles sont poussées sur la pile:
Fig. 1.30: OllyDbg : les adresses des variables locales sont poussées sur la pile
156
f1() commence. Jusqu’ici, il n’y a que des restes de données sur la pile en 0x2EF854
et 0x2EF858 :
157
f1() se termine:
Conclusion
f1() peut renvoyer des pointeurs sur n’importe quel emplacement en mémoire, si-
tués n’importe où.
C’est par essence l’utilité des pointeurs.
À propos, les references C++ fonctionnent exactement pareil. Voir à ce propos:
( 3.21.3 on page 737).
tmp1=*first ;
tmp2=*second ;
158
*first=tmp2 ;
*second=tmp1 ;
};
int main()
{
// copy string into heap, so we will be able to modify it
char *s=strdup("string") ;
printf ("%s\n", s) ;
};
Comme on le voit, les octets sont chargés dans la partie 8-bit basse de ECX et EBX
en utilisant MOVZX (donc les parties hautes de ces registres vont être effacées) et
ensuite les octets échangés sont récrits.
Les adresses des deux octets sont lues depuis les arguments et durant l’exécution
de la fonction sont copiés dans EDX et EAX.
Donc nous utilisons des pointeurs, il n’y a sans doute pas de meilleure façon de
réaliser cette tâche sans eux.
88. http://yurichev.com/mirrors/Dijkstra68.pdf
89. http://yurichev.com/mirrors/KnuthStructuredProgrammingGoTo.pdf
90. [Dennis Yurichev, C/C++ programming language notes] a aussi quelques exemples.
159
int main()
{
printf ("begin\n") ;
goto exit ;
printf ("skip me !\n") ;
exit :
printf ("end\n") ;
};
_main PROC
push ebp
mov ebp, esp
push OFFSET $SG2934 ; 'begin'
call _printf
add esp, 4
jmp SHORT $exit$3
push OFFSET $SG2936 ; 'skip me!'
call _printf
add esp, 4
$exit$3 :
push OFFSET $SG2937 ; 'end'
call _printf
add esp, 4
xor eax, eax
pop ebp
ret 0
_main ENDP
L’instruction goto a simplement été remplacée par une instruction JMP, qui a le
même effet: un saut inconditionnel à un autre endroit. Le second printf() peut
seulement être exécuté avec une intervention humaine, en utilisant un débogueur
ou en modifiant le code.
160
Cela peut être utile comme exercice simple de patching. Ouvrons l’exécutable géné-
ré dans Hiew:
161
Placez le curseur à l’adresse du JMP (0x410), pressez F3 (edit), pressez deux fois
zéro, donc l’opcode devient EB 00 :
Le second octet de l’opcode de JMP indique l’offset relatif du saut, 0 signifie le point
juste après l’instruction courante.
Donc maintenant JMP n’évite plus le second printf().
Pressez F9 (save) et quittez. Maintenant, si nous lançons l’exécutable, nous verrons
ceci:
begin
skip me !
end
Le même résultat peut être obtenu en remplaçant l’instruction JMP par 2 instructions
NOP.
NOP a un opcode de 0x90 et une longueur de 1 octet, donc nous en avons besoin de
2 pour remplacer JMP (qui a une taille de 2 octets).
162
Cela signifie que le code ne sera jamais exécuté. Donc lorsque vous compilez cet
exemple avec les optimisations, le compilateur supprime le «code mort », n’en lais-
sant aucune trace:
_main PROC
push OFFSET $SG2981 ; 'begin'
call _printf
push OFFSET $SG2984 ; 'end'
$exit$4 :
call _printf
add esp, 8
xor eax, eax
ret 0
_main ENDP
1.17.2 Exercice
Essayez d’obtenir le même résultat en utilisant votre compilateur et votre débogueur
favoris.
163
printf ("a<b\n") ;
};
int main()
{
f_signed(1, 2) ;
f_unsigned(1, 2) ;
return 0;
};
x86
x86 + MSVC
164
premier, le flux d’exécution ne sera pas altéré et le premier printf() sera exécuté.
Le second test est JNE : Jump if Not Equal (saut si non égal). Le flux d’exécution ne
changera pas si les opérandes sont égaux.
Le troisième test est JGE : Jump if Greater or Equal—saute si le premier opérande
est supérieur ou égal au deuxième. Donc, si les trois sauts conditionnels sont ef-
fectués, aucun des appels à printf() ne sera exécuté. Ceci est impossible sans
intervention spéciale. Regardons maintenant la fonction f_unsigned(). La fonction
f_unsigned() est la même que f_signed(), à la différence que les instructions JBE
et JAE sont utilisées à la place de JLE et JGE, comme suit:
165
Listing 1.111: main()
_main PROC
push ebp
mov ebp, esp
push 2
push 1
call _f_signed
add esp, 8
push 2
push 1
call _f_unsigned
add esp, 8
xor eax, eax
pop ebp
ret 0
_main ENDP
166
x86 + MSVC + OllyDbg
Nous pouvons voir comment les flags sont mis en lançant cet exemple dans OllyDbg.
Commençons par f_unsigned(), qui utilise des entiers non signés.
CMP est exécuté trois fois ici, mais avec les même arguments, donc les flags sont les
même à chaque fois.
Résultat de la première comparaison:
Donc, les flags sont: C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.
Ils sont nommés avec une seule lettre dans OllyDbg par concision.
OllyDbg indique que le saut (JBE) va être suivi maintenant. En effet, si nous regar-
dons dans le manuel d’Intel ( 12.1.4 on page 1327), nous pouvons lire que JBE est
déclenchée si CF=1 ou ZF=1. La condition est vraie ici, donc le saut est effectué.
167
Le saut conditionnel suivant:
OllyDbg indique que le saut JNZ va être effectué maintenant. En effet, JNZ est dé-
clenché si ZF=0 (Zero Flag).
168
Le troisième saut conditionnel, JNB :
Dans le manuel d’Intel ( 12.1.4 on page 1327) nous pouvons voir que JNB est dé-
clenché si CF=0 (Carry Flag - flag de retenue). Ce qui n’est pas vrai dans notre cas,
donc le troisième printf() sera exécuté.
169
Maintenant, regardons la fonction f_signed(), qui fonctionne avec des entiers non
signés. Les flags sont mis de la même manière: C=1, P=1, A=1, Z=0, S=1, T=0,
D=0, O=0. Le premier saut conditionnel JLE est effectué:
Dans les manuels d’Intel ( 12.1.4 on page 1327) nous trouvons que cette instruction
est déclenchée si ZF=1 ou SF≠OF. SF≠OF dans notre cas, donc le saut est effectué.
170
Le second saut conditionnel, JNZ, est effectué: si ZF=0 (Zero Flag) :
171
Le troisième saut conditionnel, JGE, ne sera pas effectué car il ne l’est que si SF=OF,
et ce n’est pas vrai dans notre cas:
172
x86 + MSVC + Hiew
173
Dans cette instruction, l’offset est ajouté à l’adresse de l’instruction suivante.
Donc si l’offset est 0, le saut va transférer l’exécution à l’instruction suivante.
• Le troisième saut est remplacé par JMP comme nous l’avons fait pour le premier,
il sera donc toujours effectué.
174
Voici le code modifié:
GCC 4.4.1 sans optimisation produit presque le même code, mais avec puts() ( 1.5.3
on page 29) à la place de printf().
Le lecteur attentif pourrait demander pourquoi exécuter CMP plusieurs fois, si les
flags ont les mêmes valeurs après l’exécution ?
Peut-être que l’optimiseur de de MSVC ne peut pas faire cela, mais celui de GCC
4.8.1 peut aller plus loin:
175
f_signed :
mov eax, DWORD PTR [esp+8]
cmp DWORD PTR [esp+4], eax
jg .L6
je .L7
jge .L1
mov DWORD PTR [esp+4], OFFSET FLAT :.LC2 ; "a<b"
jmp puts
.L6 :
mov DWORD PTR [esp+4], OFFSET FLAT :.LC0 ; "a>b"
jmp puts
.L1 :
rep ret
.L7 :
mov DWORD PTR [esp+4], OFFSET FLAT :.LC1 ; "a==b"
jmp puts
176
mov DWORD PTR [esp], OFFSET FLAT :.LC0 ; "a>b"
call puts
cmp esi, ebx
jne .L10
.L14 :
mov DWORD PTR [esp+32], OFFSET FLAT :.LC1 ; "a==b"
add esp, 20
pop ebx
pop esi
jmp puts
ARM
ARM 32-bit
Beaucoup d’instructions en mode ARM ne peuvent être exécutées que lorsque cer-
tains flags sont mis. E.g, ceci est souvent utilisé lorsque l’on compare les nombres
Par exemple, l’instruction ADD est en fait appelée ADDAL en interne, où AL signifie
Always, i.e., toujours exécuter. Les prédicats sont encodés dans les 4 bits du haut
des instructions ARM 32-bit. (condition field). L’instruction de saut inconditionnel B
est en fait conditionnelle et encodée comme toutes les autres instructions de saut
177
conditionnel, mais a AL dans le champ de condition, et s’exécute toujours (ALways),
ignorants les flags.
L’instruction ADRGT fonctionne comme ADR mais ne s’exécute que dans le cas où
l’instruction CMP précédente a trouvé un des nombres plus grand que l’autre, en
comparant les deux (Greater Than).
L’instruction BLGT se comporte exactement comme BL et n’est effectuée que si le
résultat de la comparaison était Greater Than (plus grand). ADRGT écrit un pointeur
sur la chaîne a>b\n dans R0 et BLGT appelle printf(). Donc, les instructions suf-
fixées par -GT ne sont exécutées que si la valeur dans R0 (qui est a) est plus grande
que la valeur dans R4 (qui est b).
En avançant, nous voyons les instructions ADREQ et BLEQ. Elles se comportent comme
ADR et BL, mais ne sont exécutées que si les opérandes étaient égaux lors de la der-
nière comparaison. Un autre CMP se trouve avant elles (car l’exécution de printf()
pourrait avoir modifiée les flags).
Ensuite nous voyons LDMGEFD, cette instruction fonctionne comme LDMFD91 , mais
n’est exécutée que si l’une des valeurs est supérieure ou égale à l’autre (Greater or
Equal).
L’instruction LDMGEFD SP!, {R4-R6,PC} se comporte comme une fonction épilogue,
mais elle ne sera exécutée que si a >= b, et seulement lorsque l’exécution de la
fonction se terminera.
Mais si cette condition n’est pas satisfaite, i.e., a < b, alors le flux d’exécution continue
à l’instruction suivante, «LDMFD SP!, {R4-R6,LR} », qui est aussi un épilogue de
la fonction. Cette instruction ne restaure pas seulement l’état des registres R4-R6,
mais aussi LR au lieu de PC, donc il ne retourne pas de la fonction. Les deux der-
nières instructions appellent printf() avec la chaîne «a<b\n» comme unique ar-
gument. Nous avons déjà examiné un saut inconditionnel à la fonction printf() au
lieu d’un appel avec retour dans «printf() avec plusieurs arguments» section ( 1.11.2
on page 74).
f_unsigned est très similaire, à part les instructions ADRHI, BLHI, et LDMCSFD utili-
sées ici, ces prédicats (HI = Unsigned higher, CS = Carry Set (greater than or equal))
sont analogues à ceux examinés avant, mais pour des valeurs non signées.
Il n’y a pas grand chose de nouveau pour nous dans la fonction main() :
91. LDMFD
178
.text :00000148 ; End of function main
C’est ainsi que vous pouvez vous débarrasser des sauts conditionnels en mode ARM.
Pourquoi est-ce que c’est si utile? Lire ici: 2.10.1 on page 604.
Il n’y a pas de telle caractéristique en x86, exceptée l’instruction CMOVcc, qui est
comme un MOV, mais effectuée seulement lorsque certains flags sont mis, en général
mis par CMP.
En mode Thumb, seules les instructions B peuvent être complétées par un condition
codes, (code de condition) donc le code Thumb paraît plus ordinaire.
BLE est un saut conditionnel normal Less than or Equal (inférieur ou égal), BNE—Not
Equal (non égal), BGE—Greater than or Equal (supérieur ou égal).
f_unsigned est similaire, seules d’autres instructions sont utilisées pour travailler
avec des valeurs non-signées: BLS (Unsigned lower or same non signée, inférieur ou
égal) et BCS (Carry Set (Greater than or equal) supérieur ou égal).
179
Listing 1.117: f_signed()
f_signed :
; W0=a, W1=b
cmp w0, w1
bgt .L19 ; Branch if Greater Than
; branchement is supérieur (a>b)
beq .L20 ; Branch if Equal
; branchement si égal (a==b)
bge .L15 ; Branch if Greater than or Equal
; branchement si supérieur ou égal (a>=b) (impossible ici)
; a<b
adrp x0, .LC11 ; "a<b"
add x0, x0, :lo12 :.LC11
b puts
.L19 :
adrp x0, .LC9 ; "a>b"
add x0, x0, :lo12 :.LC9
b puts
.L15 : ; impossible d'arriver ici
ret
.L20 :
adrp x0, .LC10 ; "a==b"
add x0, x0, :lo12 :.LC10
b puts
180
adrp x0, .LC9 ; "a>b"
str x1, [x29,40]
add x0, x0, :lo12 :.LC9
bl puts
ldr x1, [x29,40]
cmp w19, w1
bne .L23 ; Branch if Not Equal
; branchement si non égal
.L26 :
ldr x19, [sp,16]
adrp x0, .LC10 ; "a==b"
ldp x29, x30, [sp], 48
add x0, x0, :lo12 :.LC10
b puts
Les commentaires ont été ajoutés par l’auteur de ce livre. Ce qui frappe ici, c’est que
le compilateur n’est pas au courant que certaines conditions ne sont pas possible
du tout, donc il y a du code mort par endroit, qui ne sera jamais exécuté.
Exercice
MIPS
181
.text :00000010 la $gp, __gnu_local_gp
.text :00000018 sw $gp, 0x20+var_10($sp)
; stocker les valeurs en entrée sur la pile locale:
.text :0000001C sw $a0, 0x20+arg_0($fp)
.text :00000020 sw $a1, 0x20+arg_4($fp)
; reload them.
.text :00000024 lw $v1, 0x20+arg_0($fp)
.text :00000028 lw $v0, 0x20+arg_4($fp)
; $v0=b
; $v1=a
.text :0000002C or $at, $zero ; NOP
; ceci est une pseudo-instructions. en fait, c'est "slt $v0,$v0,$v1" ici.
; donc $v0 sera mis à 1 si $v0<$v1 (b<a) ou à 0 autrement:
.text :00000030 slt $v0, $v1
; saut en loc_5c, si la condition n'est pas vraie.
; ceci est une pseudo-instruction. en fait, c'est "beq $v0,$zero,loc_5c"
ici:
.text :00000034 beqz $v0, loc_5C
; afficher "a>b" et terminer
.text :00000038 or $at, $zero ; slot de délai de branchement,
NOP
.text :0000003C lui $v0, (unk_230 >> 16) # "a>b"
.text :00000040 addiu $a0, $v0, (unk_230 & 0xFFFF) # "a>b"
.text :00000044 lw $v0, (puts & 0xFFFF)($gp)
.text :00000048 or $at, $zero ; NOP
.text :0000004C move $t9, $v0
.text :00000050 jalr $t9
.text :00000054 or $at, $zero ; slot de délai de branchement,
NOP
.text :00000058 lw $gp, 0x20+var_10($fp)
.text :0000005C
.text :0000005C loc_5C : # CODE XREF: f_signed+34
.text :0000005C lw $v1, 0x20+arg_0($fp)
.text :00000060 lw $v0, 0x20+arg_4($fp)
.text :00000064 or $at, $zero ; NOP
; tester si a==b, sauter en loc_90 si ce n'est pas vrai:
.text :00000068 bne $v1, $v0, loc_90
.text :0000006C or $at, $zero ; slot de délai de branchement,
NOP
; la condition est vraie, donc afficher "a==b" et terminer:
.text :00000070 lui $v0, (aAB >> 16) # "a==b"
.text :00000074 addiu $a0, $v0, (aAB & 0xFFFF) # "a==b"
.text :00000078 lw $v0, (puts & 0xFFFF)($gp)
.text :0000007C or $at, $zero ; NOP
.text :00000080 move $t9, $v0
.text :00000084 jalr $t9
.text :00000088 or $at, $zero ; slot de délai de branchement,
NOP
.text :0000008C lw $gp, 0x20+var_10($fp)
.text :00000090
.text :00000090 loc_90 : # CODE XREF: f_signed+68
.text :00000090 lw $v1, 0x20+arg_0($fp)
.text :00000094 lw $v0, 0x20+arg_4($fp)
.text :00000098 or $at, $zero ; NOP
; tester si $v1<$v0 (a<b), mettre $v0 à 1 si la condition est vraie:
182
.text :0000009C slt $v0, $v1, $v0
; si la condition n'est pas vraie (i.e., $v0==0), sauter en loc_c8:
.text :000000A0 beqz $v0, loc_C8
.text :000000A4 or $at, $zero ; slot de délai de branchement,
NOP
; la condition est vraie, afficher "a<b" et terminer
.text :000000A8 lui $v0, (aAB_0 >> 16) # "a<b"
.text :000000AC addiu $a0, $v0, (aAB_0 & 0xFFFF) # "a<b"
.text :000000B0 lw $v0, (puts & 0xFFFF)($gp)
.text :000000B4 or $at, $zero ; NOP
.text :000000B8 move $t9, $v0
.text :000000BC jalr $t9
.text :000000C0 or $at, $zero ; slot de délai de branchement,
NOP
.text :000000C4 lw $gp, 0x20+var_10($fp)
.text :000000C8
; toutes les 3 conditions étaient fausses, donc simplement terminer:
.text :000000C8 loc_C8 : # CODE XREF:
f_signed+A0
.text :000000C8 move $sp, $fp
.text :000000CC lw $ra, 0x20+var_4($sp)
.text :000000D0 lw $fp, 0x20+var_8($sp)
.text :000000D4 addiu $sp, 0x20
.text :000000D8 jr $ra
.text :000000DC or $at, $zero ; slot de délai de branchement,
NOP
.text :000000DC # Fin de la fonction f_signed
SLT REG0, REG0, REG1 est réduit par IDA à sa forme plus courte:
SLT REG0, REG1.
Nous voyons également ici la pseudo instruction BEQZ («Branch if Equal to Zero »
branchement si égal à zéro),
qui est en fait BEQ REG, $ZERO, LABEL.
La version non signée est la même, mais SLTU (version non signée, d’où le «U » de
unsigned) est utilisée au lieu de SLT :
Listing 1.120: GCC 4.4.5 sans optimisation (IDA)
.text :000000E0 f_unsigned : # CODE XREF: main+28
.text :000000E0
.text :000000E0 var_10 = -0x10
.text :000000E0 var_8 = -8
.text :000000E0 var_4 = -4
.text :000000E0 arg_0 = 0
.text :000000E0 arg_4 = 4
.text :000000E0
.text :000000E0 addiu $sp, -0x20
.text :000000E4 sw $ra, 0x20+var_4($sp)
.text :000000E8 sw $fp, 0x20+var_8($sp)
.text :000000EC move $fp, $sp
.text :000000F0 la $gp, __gnu_local_gp
.text :000000F8 sw $gp, 0x20+var_10($sp)
.text :000000FC sw $a0, 0x20+arg_0($fp)
.text :00000100 sw $a1, 0x20+arg_4($fp)
183
.text :00000104 lw $v1, 0x20+arg_0($fp)
.text :00000108 lw $v0, 0x20+arg_4($fp)
.text :0000010C or $at, $zero
.text :00000110 sltu $v0, $v1
.text :00000114 beqz $v0, loc_13C
.text :00000118 or $at, $zero
.text :0000011C lui $v0, (unk_230 >> 16)
.text :00000120 addiu $a0, $v0, (unk_230 & 0xFFFF)
.text :00000124 lw $v0, (puts & 0xFFFF)($gp)
.text :00000128 or $at, $zero
.text :0000012C move $t9, $v0
.text :00000130 jalr $t9
.text :00000134 or $at, $zero
.text :00000138 lw $gp, 0x20+var_10($fp)
.text :0000013C
.text :0000013C loc_13C : # CODE XREF: f_unsigned+34
.text :0000013C lw $v1, 0x20+arg_0($fp)
.text :00000140 lw $v0, 0x20+arg_4($fp)
.text :00000144 or $at, $zero
.text :00000148 bne $v1, $v0, loc_170
.text :0000014C or $at, $zero
.text :00000150 lui $v0, (aAB >> 16) # "a==b"
.text :00000154 addiu $a0, $v0, (aAB & 0xFFFF) # "a==b"
.text :00000158 lw $v0, (puts & 0xFFFF)($gp)
.text :0000015C or $at, $zero
.text :00000160 move $t9, $v0
.text :00000164 jalr $t9
.text :00000168 or $at, $zero
.text :0000016C lw $gp, 0x20+var_10($fp)
.text :00000170
.text :00000170 loc_170 : # CODE XREF: f_unsigned+68
.text :00000170 lw $v1, 0x20+arg_0($fp)
.text :00000174 lw $v0, 0x20+arg_4($fp)
.text :00000178 or $at, $zero
.text :0000017C sltu $v0, $v1, $v0
.text :00000180 beqz $v0, loc_1A8
.text :00000184 or $at, $zero
.text :00000188 lui $v0, (aAB_0 >> 16) # "a<b"
.text :0000018C addiu $a0, $v0, (aAB_0 & 0xFFFF) # "a<b"
.text :00000190 lw $v0, (puts & 0xFFFF)($gp)
.text :00000194 or $at, $zero
.text :00000198 move $t9, $v0
.text :0000019C jalr $t9
.text :000001A0 or $at, $zero
.text :000001A4 lw $gp, 0x20+var_10($fp)
.text :000001A8
.text :000001A8 loc_1A8 : # CODE XREF: f_unsigned+A0
.text :000001A8 move $sp, $fp
.text :000001AC lw $ra, 0x20+var_4($sp)
.text :000001B0 lw $fp, 0x20+var_8($sp)
.text :000001B4 addiu $sp, 0x20
.text :000001B8 jr $ra
.text :000001BC or $at, $zero
184
.text :000001BC # End of function f_unsigned
185
Il manque une instruction de négation en ARM, donc le compilateur Keil utilise l’ins-
truction «Reverse Subtract », qui soustrait la valeur du registre de l’opérande.
Maintenant, il n’y a plus de saut conditionnel et c’est mieux: 2.10.1 on page 604.
MIPS
186
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
locret_10 :
; prendre l'opposée de la valeur entrée et la stocker dans $v0:
jr $ra
; ceci est une pseudo-instruction. En fait, ceci est "subu $v0,$zero,$a0"
($v0=0-$a0)
negu $v0, $a0
Nous voyons ici une nouvelle instruction: BLTZ («Branch if Less Than Zero » bran-
chement si plus petit que zéro).
Il y a aussi la pseudo-instruction NEGU, qui effectue une soustraction à zéro. Le suffixe
«U » dans les deux instructions SUBU et NEGU indique qu’aucune exception ne sera
levée en cas de débordement de la taille d’un entier.
Vous pouvez aussi avoir une version sans branchement de ce code. Ceci sera revu
plus tard: 3.16 on page 679.
Voici un exemple:
const char* f (int a)
{
return a==10 ? "it is ten" : "it is not ten" ;
};
x86
187
; sauter en $LN3@f si non égal
jne SHORT $LN3@f
; stocker le pointeur sur la chaîne dans la variable temporaire:
mov DWORD PTR tv65[ebp], OFFSET $SG746 ; 'it is ten'
; sauter à la sortie
jmp SHORT $LN4@f
$LN3@f :
; stocker le pointeur sur la chaîne dans la variable temporaire:
mov DWORD PTR tv65[ebp], OFFSET $SG747 ; 'it is not ten'
$LN4@f :
; ceci est la sortie.
; copier le pointeur sur la chaîne depuis la variable temporaire dans EAX.
mov eax, DWORD PTR tv65[ebp]
mov esp, ebp
pop ebp
ret 0
_f ENDP
_a$ = 8 ; taille = 4
_f PROC
; comparer la valeur en entrée avec 10
cmp DWORD PTR _a$[esp-4], 10
mov eax, OFFSET $SG792 ; 'it is ten'
; sauter en $LN4@f si égal
je SHORT $LN4@f
mov eax, OFFSET $SG793 ; 'it is not ten'
$LN4@f :
ret 0
_f ENDP
a$ = 8
f PROC
; charger les pointeurs sur les deux chaînes
lea rdx, OFFSET FLAT :$SG1355 ; 'it is ten'
lea rax, OFFSET FLAT :$SG1356 ; 'it is not ten'
; comparer la valeur en entrée avec 10
cmp ecx, 10
; si égal, copier la valeur dans RDX ("it is ten")
; si non, ne rien faire. le pointeur sur la chaîne
; "it is not ten" est encore dans RAX à ce stade.
cmove rax, rdx
ret 0
188
f ENDP
GCC 4.8 avec optimisation pour x86 utilise également l’instruction CMOVcc, tandis
que GCC 4.8 sans optimisation utilise des sauts conditionnels.
ARM
Keil avec optimisation pour le mode ARM utilise les instructions conditionnelles ADRcc :
Listing 1.129: avec optimisation Keil 6/2013 (Mode ARM)
f PROC
; comparer la valeur en entrée avec 10
CMP r0,#0xa
; si le résultat de la comparaison est égal (EQual), copier le pointeur sur
la chaîne
; "it is ten" dans R0
ADREQ r0,|L0.16| ; "it is ten"
; si le résultat de la comparaison est non égal (Not EQual), copier le
pointeur sur la chaîne
; "it is not ten" dans R0
ADRNE r0,|L0.28| ; "it is not ten"
BX lr
ENDP
|L0.16|
DCB "it is ten",0
|L0.28|
DCB "it is not ten",0
Sans intervention manuelle, les deux instructions ADREQ et ADRNE ne peuvent être
exécutées lors de la même exécution.
Keil avec optimisation pour le mode Thumb à besoin d’utiliser des instructions de
saut conditionnel, puisqu’il n’y a pas d’instruction qui supporte le flag conditionnel.
Listing 1.130: avec optimisation Keil 6/2013 (Mode Thumb)
f PROC
; comparer la valeur entrée avec 10
CMP r0,#0xa
; sauter en |L0.8| si égal (EQual)
BEQ |L0.8|
ADR r0,|L0.12| ; "it is not ten"
BX lr
|L0.8|
ADR r0,|L0.28| ; "it is ten"
BX lr
ENDP
|L0.12|
DCB "it is not ten",0
|L0.28|
DCB "it is ten",0
189
ARM64
GCC (Linaro) 4.9 avec optimisation pour ARM64 utilise aussi des sauts conditionnels:
Listing 1.131: GCC (Linaro) 4.9 avec optimisation
f :
cmp x0, 10
beq .L3 ; branchement si égal
adrp x0, .LC1 ; "it is ten"
add x0, x0, :lo12 :.LC1
ret
.L3 :
adrp x0, .LC0 ; "it is not ten"
add x0, x0, :lo12 :.LC0
ret
.LC0 :
.string "it is ten"
.LC1 :
.string "it is not ten"
C’est parce qu’ARM64 n’a pas d’instruction de chargement simple avec le flag condi-
tionnel comme ADRcc en ARM 32-bit ou CMOVcc en x86.
Il a toutefois l’instruction «Conditional SELect » (CSEL)[ARM Architecture Reference
Manual, ARMv8, for ARMv8-A architecture profile, (2013)p390, C5.5], mais GCC 4.9
ne semble pas assez malin pour l’utiliser dans un tel morceau de code.
MIPS
Malheureusement, GCC 4.4.5 pour MIPS n’est pas très malin non plus:
Listing 1.132: GCC 4.4.5 avec optimisation (résultat en sortie de l’assembleur)
$LC0 :
.ascii "it is not ten\000"
$LC1 :
.ascii "it is ten\000"
f :
li $2,10 # 0xa
; comparer $a0 et 10, sauter si égal:
beq $4,$2,$L2
nop ; slot de délai de branchement
$L2 :
; charger l'adresse de la chaîne "it is ten" dans $v0 et sortir:
lui $2,%hi($LC1)
j $31
addiu $2,$2,%lo($LC1)
190
Récrivons-le à l’aide d’unif/else
Conclusion
Pourquoi est-ce que les compilateurs qui optimisent essayent de se débarrasser des
sauts conditionnels? Voir à ce propos: 2.10.1 on page 604.
191
if (a<b)
return a ;
else
return b ;
};
_a$ = 8
_b$ = 12
_my_max PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _a$[ebp]
; comparer A et B:
cmp eax, DWORD PTR _b$[ebp]
; sauter si A est inférieur ou égal à B:
jle SHORT $LN2@my_max
; recharger A dans EAX si autrement et sauter à la sortie
mov eax, DWORD PTR _a$[ebp]
jmp SHORT $LN3@my_max
jmp SHORT $LN3@my_max ; ce JMP est redondant
$LN2@my_max :
; renvoyer B
mov eax, DWORD PTR _b$[ebp]
$LN3@my_max :
pop ebp
ret 0
_my_max ENDP
Ces deux fonctions ne diffèrent que de l’instruction de saut conditionnel: JGE («Jump
192
if Greater or Equal » saut si supérieur ou égal) est utilisée dans la première et JLE
(«Jump if Less or Equal » saut si inférieur ou égal) dans la seconde.
Il y a une instruction JMP en trop dans chaque fonction, que MSVC a probablement
mise par erreur.
Sans branchement
my_min PROC
; R0=A
; R1=B
; comparer A et B:
CMP r0,r1
; branchement si A est inférieur à B:
BLT |L0.14|
; autrement (A>=B) renvoyer R1 (B) :
MOVS r0,r1
|L0.14|
; retourner
BX lr
ENDP
193
; cette instruction ne s'exécutera que si A<=B (en effet, LE Less or Equal,
inférieur ou égal)
; si l'instruction n'est pas exécutée (dans le cas où A>B),
; A est toujours dans le registre R0
MOVLE r0,r1
BX lr
ENDP
my_min PROC
; R0=A
; R1=B
; comparer A et B:
CMP r0,r1
; renvoyer B au lieu de A en copiant B dans R0
; cette instruction ne s'exécutera que si A>=B (GE Greater or Equal,
supérieur ou égal)
; si l'instruction n'est pas exécutée (dans le cas où A<B),
; A est toujours dans le registre R0
MOVGE r0,r1
BX lr
ENDP
GCC 4.8.1 avec optimisation et MSVC 2013 avec optimisation peuvent utiliser l’ins-
truction CMOVcc, qui est analogue à MOVcc en ARM:
my_min :
mov edx, DWORD PTR [esp+4]
mov eax, DWORD PTR [esp+8]
; EDX=A
; EAX=B
; comparer A et B:
cmp edx, eax
; si A<=B, charger la valeur A dans EAX
; l'instruction ne fait rien autrement (si A>B)
cmovle eax, edx
ret
64-bit
194
#include <stdint.h>
my_min :
sub sp, sp, #16
str x0, [sp,8]
str x1, [sp]
ldr x1, [sp,8]
ldr x0, [sp]
cmp x1, x0
bge .L5
ldr x0, [sp,8]
b .L6
.L5 :
ldr x0, [sp]
.L6 :
add sp, sp, 16
ret
195
Sans branchement
Il n’y a pas besoin de lire les arguments dans la pile, puisqu’ils sont déjà dans les
registres:
my_min :
; RDI=A
; RSI=B
; comparer A et B:
cmp rdi, rsi
; préparer B pour le renvoyer dans RAX:
mov rax, rsi
; si A<=B, mettre A (RDI) dans RAX pour le renvoyer.
; cette instruction ne fait rien autrement (si A>B)
cmovle rax, rdi
ret
my_min :
; X0=A
; X1=B
; comparer A et B:
cmp x0, x1
196
; copier X0 (A) dans X0 si X0<=X1 ou A<=B (Less or Equal, inférieur ou égal)
; copier X1 (B) dans X0 si A>B
csel x0, x0, x1, le
ret
MIPS
locret_10 :
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
locret_28 :
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
N’oubliez pas le slot de délai de branchement (branch delay slots) : le premier MOVE
est exécuté avant BEQZ, le second MOVE n’est exécuté que si la branche n’a pas été
prise.
1.18.5 Conclusion
x86
197
;... le code qui sera exécuté si le résultat de la comparaison est faux
(false) ...
JMP exit
true :
;... le code qui sera exécuté si le résultat de la comparaison est vrai
(true) ...
exit :
ARM
MIPS
Listing 1.145: Teste si plus petit que zéro (Branch if Less Than Zero) en utilisant une
pseudo instruction
BLTZ REG, label
...
Listing 1.147: Teste si les valeurs ne sont pas égales (Branch if Not Equal)
BNE REG1, REG2, label
...
Listing 1.148: Teste si REG2 est plus petit que REG3 (signé)
SLT REG1, REG2, REG3
BEQ REG1, label
...
198
Listing 1.149: Teste si REG2 est plus petit que REG3 (non signé)
SLTU REG1, REG2, REG3
BEQ REG1, label
...
Sans branchement
ARM
Il est possible d’utiliser les suffixes conditionnels pour certaines instructions ARM:
Bien sûr, il n’y a pas de limite au nombre d’instructions avec un suffixe de code
conditionnel, tant que les flags du CPU ne sont pas modifiés par l’une d’entre elles.
Le mode Thumb possède l’instruction IT, permettant d’ajouter le suffixe conditionnel
pour les quatre instructions suivantes. Lire à ce propos: 1.25.7 on page 333.
1.18.6 Exercice
(ARM64) Essayez de récrire le code pour listado.1.131 en supprimant toutes les ins-
tructions de saut conditionnel et en utilisant l’instruction CSEL.
199
Souvent, ça ressemble à ça:
...
call check_protection
jz all_OK
call message_box_protection_missing
call exit
all_OK :
; proceed
...
Donc, si vous voyez un patch (ou “crack”), qui déplombe un logiciel, et que ce patch
remplace un ou des octets 0x74/0x75 (JZ/JNZ) par 0xEB (JMP), c’est ça.
Le processus de déplombage de logiciel revient à une recherche de ce JMP.
Il y a aussi les cas où le logiciel vérifie la protection de temps à autre, ceci peut être
un dongle, ou un serveur de licence qui peut être interrogé depuis Internet. Dans
ce cas, vous devez chercher une fonction qui vérifie la protection. Puis, la modifier,
pour y mettre xor eax, eax / retn, ou mov eax, 1 / retn.
Il est important de comprendre qu’après avoir patché le début d’une fonction, sou-
vent, il y a des octets résiduels qui suivent ces deux instructions. Ces restes consistent
en une partie d’une instruction et les instructions suivantes.
Ceci est un cas réel. Le début de la fonction que nous voulons remplacer par return
1;
Listing 1.152: Before
8BFF mov edi,edi
55 push ebp
8BEC mov ebp,esp
81EC68080000 sub esp,000000868
A110C00001 mov eax,[00100C010]
33C5 xor eax,ebp
8945FC mov [ebp][-4],eax
53 push ebx
8B5D08 mov ebx,[ebp][8]
...
200
Quelques instructions incorrectes apparaissent — IN, PUSH, ADC, ADD, après lesquelles,
le désassembleur Hiew (que j’ai utilisé) s’est synchronisé et a continué de désassem-
bler le reste.
Ceci n’est pas important — toutes ces instructions qui suivent RETN ne seront jamais
exécutées, à moins qu’un saut direct se produise quelque part, et ça ne sera pas
possible en général.
Il peut aussi y avoir une variable globale booléenne, un flag indiquant si le logiciel
est enregistré ou non.
init_etc proc
...
call check_protection_or_license_file
mov is_demo, eax
...
retn
init_etc endp
...
save_file proc
...
mov eax, is_demo
cmp eax, 1
jz all_OK1
call message_box_it_is_a_demo_no_saving_allowed
retn
:all_OK1
; continuer en sauvant le fichier
...
save_proc endp
somewhere_else proc
:all_OK2
; continuer
somewhere_else endp
201
Le début de la fonction check_protection_or_license_file() peut être patché,
afin qu’elle renvoie toujours 1, ou, si c’est mieux pour une raison quelconques, toutes
les instructions JZ/JNZ peuvent être patchées de même
Plus sur le patching: 11.1.
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]
"SFCDisable"=dword :ffffff9d
1.21 switch()/case/default
1.21.1 Petit nombre de cas
#include <stdio.h>
void f (int a)
202
{
switch (a)
{
case 0: printf ("zero\n") ; break ;
case 1: printf ("one\n") ; break ;
case 2: printf ("two\n") ; break ;
default : printf ("something unknown\n") ; break ;
};
};
int main()
{
f (2) ; // test
};
x86
203
jmp SHORT $LN7@f
$LN1@f :
push OFFSET $SG745 ; 'something unknown', 0aH, 00H
call _printf
add esp, 4
$LN7@f :
mov esp, ebp
pop ebp
ret 0
_f ENDP
Notre fonction avec quelques cas dans switch() est en fait analogue à cette construc-
tion:
void f (int a)
{
if (a==0)
printf ("zero\n") ;
else if (a==1)
printf ("one\n") ;
else if (a==2)
printf ("two\n") ;
else
printf ("something unknown\n") ;
};
Si nous utilisons switch() avec quelques cas, il est impossible de savoir si il y avait
un vrai switch() dans le code source, ou un ensemble de directives if().
Ceci indique que switch() est comme un sucre syntaxique pour un grand nombre de
if() imbriqués.
Il n’y a rien de particulièrement nouveau pour nous dans le code généré, à l’ex-
ception que le compilateur déplace la variable d’entrée a dans une variable locale
temporaire tv64 92 .
Si nous compilons ceci avec GCC 4.4.1, nous obtenons presque le même résultat,
même avec le niveau d’optimisation le plus élevé (-O3 option).
Maintenant compilons dans MSVC avec l’optimisation (/Ox) : cl 1.c /Fa1.asm /Ox
92. Les variables locales sur la pile sont préfixées avec tv—c’est ainsi que MSVC appelle les variables
internes dont il a besoin.
204
sub eax, 1
je SHORT $LN3@f
sub eax, 1
je SHORT $LN2@f
mov DWORD PTR _a$[esp-4], OFFSET $SG791 ; 'something unknown', 0aH,
00H
jmp _printf
$LN2@f :
mov DWORD PTR _a$[esp-4], OFFSET $SG789 ; 'two', 0aH, 00H
jmp _printf
$LN3@f :
mov DWORD PTR _a$[esp-4], OFFSET $SG787 ; 'one', 0aH, 00H
jmp _printf
$LN4@f :
mov DWORD PTR _a$[esp-4], OFFSET $SG785 ; 'zero', 0aH, 00H
jmp _printf
_f ENDP
205
Tout ceci est possible car printf() est appelée, dans tous les cas, tout à la fin de la
fonction f(). Dans un certain sens, c’est similaire à la fonction longjmp()93 . Et bien
sûr, c’est fait dans un but de vitesse d’exécution.
Un cas similaire avec le compilateur ARM est décrit dans la section «printf() avec
plusieurs arguments », ici ( 1.11.2 on page 74).
93. Wikipédia
206
OllyDbg
Fig. 1.43: OllyDbg : EAX contient maintenant le premier (et unique) argument de la
fonction
207
0 est soustrait de 2 dans EAX. Bien sûr, EAX contient toujours 2. Mais le flag ZF est
maintenant à 0, indiquant que le résultat est différent de zéro:
208
DEC est exécuté et EAX contient maintenant 1. Mais 1 est différent de zéro, donc le
flag ZF est toujours à 0:
209
Le DEC suivant est exécuté. EAX contient maintenant 0 et le flag ZF est mis, car le
résultat devient zéro:
210
Un pointeur sur la chaîne «two » est maintenant écrit sur la pile:
Fig. 1.47: OllyDbg : pointeur sur la chaîne qui va être écrite à la place du premier
argument
211
MOV écrit le pointeur sur la chaîne à l’adresse 0x001EF850 (voir la fenêtre de la pile).
Puis, le saut est effectué. Ceci est la première instruction de la fonction printf()
dans MSVCR100.DLL (Cet exemple a été compilé avec le switch /MD) :
Maintenant printf() traite la chaîne à l’adresse 0x00FF3010 comme c’est son seul
argument et l’affiche.
212
Ceci est la dernière instruction de printf() :
213
Maintenant, appuyez sur F7 ou F8 (enjamber) et le retour ne se fait pas sur f(), mais
sur main() :
Oui, le saut a été direct, depuis les entrailles de printf() vers main(). Car RA
dans la pile pointe non pas quelque part dans f(), mais en fait sur main(). Et CALL
0x00FF1000 a été l’instruction qui a appelé f().
.text :0000014C f1 :
.text :0000014C 00 00 50 E3 CMP R0, #0
.text :00000150 13 0E 8F 02 ADREQ R0, aZero ; "zero\n"
.text :00000154 05 00 00 0A BEQ loc_170
.text :00000158 01 00 50 E3 CMP R0, #1
.text :0000015C 4B 0F 8F 02 ADREQ R0, aOne ; "one\n"
.text :00000160 02 00 00 0A BEQ loc_170
.text :00000164 02 00 50 E3 CMP R0, #2
.text :00000168 4A 0F 8F 12 ADRNE R0, aSomethingUnkno ; "something
unknown\n"
.text :0000016C 4E 0F 8F 02 ADREQ R0, aTwo ; "two\n"
.text :00000170
.text :00000170 loc_170 : ; CODE XREF: f1+8
.text :00000170 ; f1+14
.text :00000170 78 18 00 EA B __2printf
214
En tout cas, nous voyons ici des instructions conditionnelles (comme ADREQ (Equal))
qui ne sont exécutées que si R0 = 0, et qui chargent ensuite l’adresse de la chaîne
«zero\n» dans R0. L’instruction suivante BEQ redirige le flux d’exécution en loc_170,
si R0 = 0.
Le lecteur attentif peut se demander si BEQ s’exécute correctement puisque ADREQ
a déjà mis une autre valeur dans le registre R0.
Oui, elle s’exécutera correctement, car BEQ vérifie les flags mis par l’instruction CMP
et ADREQ ne modifie aucun flag.
Les instructions restantes nous sont déjà familières. Il y a seulement un appel à
printf(), à la fin, et nous avons déjà examiné cette astuce ici ( 1.11.2 on page 74).
A la fin, il y a trois chemins vers printf().
La dernière instruction, CMP R0, #2, est nécessaire pour vérifier si a = 2.
Si ce n’est pas vrai, alors ADRNE charge un pointeur sur la chaîne «something unk-
nown \n» dans R0, puisque a a déjà été comparée pour savoir s’elle est égale à 0 ou
1, et nous sommes sûrs que la variable a n’est pas égale à l’un de ces nombres, à
ce point. Et si R0 = 2, un pointeur sur la chaîne «two\n» sera chargé par ADREQ dans
R0.
Comme il y déjà été dit, il n’est pas possible d’ajouter un prédicat conditionnel à la
plupart des instructions en mode Thumb, donc ce dernier est quelque peu similaire
215
au code CISC-style x86, facilement compréhensible.
.LC12 :
.string "zero"
.LC13 :
.string "one"
.LC14 :
.string "two"
.LC15 :
.string "something unknown"
f12 :
stp x29, x30, [sp, -32]!
add x29, sp, 0
str w0, [x29,28]
ldr w0, [x29,28]
cmp w0, 1
beq .L34
cmp w0, 2
beq .L35
cmp w0, wzr
bne .L38 ; sauter au label par défaut
adrp x0, .LC12 ; "zero"
add x0, x0, :lo12 :.LC12
bl puts
b .L32
.L34 :
adrp x0, .LC13 ; "one"
add x0, x0, :lo12 :.LC13
bl puts
b .L32
.L35 :
adrp x0, .LC14 ; "two"
add x0, x0, :lo12 :.LC14
bl puts
b .L32
.L38 :
adrp x0, .LC15 ; "something unknown"
add x0, x0, :lo12 :.LC15
bl puts
nop
.L32 :
ldp x29, x30, [sp], 32
ret
Le type de la valeur d’entrée est int, par conséquent le registre W0 est utilisé pour
garder la valeur au lieu du registre complet X0.
Les pointeurs de chaîne sont passés à puts() en utilisant la paire d’instructions
ADRP/ADD comme expliqué dans l’exemple «Hello, world! » : 1.5.3 on page 33.
216
ARM64: GCC (Linaro) 4.9 avec optimisation
f12 :
cmp w0, 1
beq .L31
cmp w0, 2
beq .L32
cbz w0, .L35
; cas par défaut
adrp x0, .LC15 ; "something unknown"
add x0, x0, :lo12 :.LC15
b puts
.L35 :
adrp x0, .LC12 ; "zero"
add x0, x0, :lo12 :.LC12
b puts
.L32 :
adrp x0, .LC14 ; "two"
add x0, x0, :lo12 :.LC14
b puts
.L31 :
adrp x0, .LC13 ; "one"
add x0, x0, :lo12 :.LC13
b puts
Ce morceau de code est mieux optimisé. L’instruction CBZ (Compare and Branch on
Zero comparer et sauter si zéro) effectue un saut si W0 vaut zéro. Il y a alors un saut
direct à puts() au lieu de l’appeler, comme cela a été expliqué avant: 1.21.1 on
page 204.
MIPS
217
la $a0, ($LC0 & 0xFFFF) # "zero" ; slot de délai de
branchement
Conclusion
Un switch() avec peu de cas est indistinguable d’une construction avec if/else, par
exemple: listado.1.21.1.
218
#include <stdio.h>
void f (int a)
{
switch (a)
{
case 0: printf ("zero\n") ; break ;
case 1: printf ("one\n") ; break ;
case 2: printf ("two\n") ; break ;
case 3: printf ("three\n") ; break ;
case 4: printf ("four\n") ; break ;
default : printf ("something unknown\n") ; break ;
};
};
int main()
{
f (2) ; // test
};
x86
219
call _printf
add esp, 4
jmp SHORT $LN9@f
$LN3@f :
push OFFSET $SG745 ; 'three', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN9@f
$LN2@f :
push OFFSET $SG747 ; 'four', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN9@f
$LN1@f :
push OFFSET $SG749 ; 'something unknown', 0aH, 00H
call _printf
add esp, 4
$LN9@f :
mov esp, ebp
pop ebp
ret 0
npad 2 ; aligner le label suivant
$LN11@f :
DD $LN6@f ; 0
DD $LN5@f ; 1
DD $LN4@f ; 2
DD $LN3@f ; 3
DD $LN2@f ; 4
_f ENDP
Ce que nous voyons ici est un ensemble d’appels à printf() avec des arguments
variés. Ils ont tous, non seulement des adresses dans la mémoire du processus, mais
aussi des labels symboliques internes assignés par le compilateur. Tous ces labels
ont aussi mentionnés dans la table interne $LN11@f.
Au début de la fonctions, si a est supérieur à 4, l’exécution est passée au labal $LN1@f,
où printf() est appelé avec l’argument 'something unknown'.
Mais si la valeur de a est inférieure ou égale à 4, elle est alors multipliée par 4 et
ajoutée à l’adresse de la table $LN11@f. C’est ainsi qu’une adresse à l’intérieur de
la table est construite, pointant exactement sur l’élément dont nous avons besoin.
Par exemple, supposons que a soit égale à 2. 2 ∗ 4 = 8 (tous les éléments de la table
sont adressés dans un processus 32-bit, c’est pourquoi les éléments ont une taille
de 4 octets). L’adresse de la table $LN11@f + 8 est celle de l’élément de la table où
le label $LN4@f est stocké. JMP prend l’adresse de $LN4@f dans la table et y saute.
Cette table est quelquefois appelée jumptable (table de saut) ou branch table (table
de branchement)94 .
Le printf() correspondant est appelé avec l’argument 'two'.
Littéralement, l’instruction jmp DWORD PTR $LN11@f[ecx*4] signifie sauter au DWORD
94. L’ensemble de la méthode était appelé computed GOTO (GOTO calculés) dans les premières ver-
sions de ForTran: Wikipédia. Pas très pertinent de nos jours, mais quel terme!
220
qui est stocké à l’adresse $LN11@f + ecx * 4.
npad ( .1.7 on page 1359) est une macro du langage d’assemblage qui aligne le
label suivant de telle sorte qu’il soit stocké à une adresse alignée sur une limite de
4 octets (ou 16 octets). C’est très adapté pour le processeur puisqu’il est capable
d’aller chercher des valeurs 32-bit dans la mémoire à travers le bus mémoire, la
mémoire cache, etc., de façons beaucoup plus efficace si c’est aligné.
221
OllyDbg
Essayons cet exemple dans OllyDbg. La valeur d’entrée de la fonction (2) est chargée
dans EAX :
Fig. 1.51: OllyDbg : la valeur d’entrée de la fonction est chargée dans EAX
222
La valeur entrée est testée, est-elle plus grande que 4? Si non, le saut par «défaut »
n’est pas pris:
Fig. 1.52: OllyDbg : 2 n’est pas plus grand que 4: le saut n’est pas pris
223
Ici, nous voyons une table des sauts:
Fig. 1.53: OllyDbg : calcul de l’adresse de destination en utilisant la table des sauts
Ici, nous avons cliqué «Follow in Dump » → «Address constant », donc nous voyons
maintenant la jumptable dans la fenêtre des données. Il y a 5 valeurs 32-bit95 . ECX
contient maintenant 2, donc le troisième élément (peut être indexé par 296 ) de la
table va être utilisé. Il est également possible de cliquer sur «Follow in Dump » →
«Memory address » et OllyDbg va montrer l’élément adressé par l’instruction JMP. Il
s’agit de 0x010B103A.
95. Elles sont soulignées par OllyDbg car ce sont aussi des FIXUPs: 6.5.2 on page 998, nous y reviendrons
plus tard
96. À propos des index de tableaux, lire aussi: 3.22.3 on page 786
224
Après le saut, nous sommes en 0x010B103A : le code qui affiche « two » va être
exécuté:
push ebp
mov ebp, esp
sub esp, 18h
cmp [ebp+arg_0], 4
ja short loc_8048444
mov eax, [ebp+arg_0]
shl eax, 2
mov eax, ds :off_804855C[eax]
jmp eax
225
loc_804840C : ; DATA XREF: .rodata:08048560
mov [esp+18h+var_18], offset aOne ; "one"
call _puts
jmp short locret_8048450
C’est presque la même chose, avec une petite nuance: l’argument arg_0 est mul-
tiplié par 4 en décalant de 2 bits vers la gauche (c’est presque comme multiplier
par 4) ( 1.24.2 on page 283). Ensuite l’adresse du label est prise depuis le tableau
off_804855C, stockée dans EAX, et ensuite JMP EAX effectue le saut réel.
00000180
226
00000180 loc_180 ; CODE XREF: f2+4
00000180 03 00 00 EA B zero_case ; jumptable 00000178 case 0
00000184
00000184 loc_184 ; CODE XREF: f2+4
00000184 04 00 00 EA B one_case ; jumptable 00000178 case 1
00000188
00000188 loc_188 ; CODE XREF: f2+4
00000188 05 00 00 EA B two_case ; jumptable 00000178 case 2
0000018C
0000018C loc_18C ; CODE XREF: f2+4
0000018C 06 00 00 EA B three_case ; jumptable 00000178 case 3
00000190
00000190 loc_190 ; CODE XREF: f2+4
00000190 07 00 00 EA B four_case ; jumptable 00000178 case 4
00000194
00000194 zero_case ; CODE XREF: f2+4
00000194 ; f2:loc_180
00000194 EC 00 8F E2 ADR R0, aZero ; jumptable 00000178 case 0
00000198 06 00 00 EA B loc_1B8
0000019C
0000019C one_case ; CODE XREF: f2+4
0000019C ; f2:loc_184
0000019C EC 00 8F E2 ADR R0, aOne ; jumptable 00000178 case 1
000001A0 04 00 00 EA B loc_1B8
000001A4
000001A4 two_case ; CODE XREF: f2+4
000001A4 ; f2:loc_188
000001A4 01 0C 8F E2 ADR R0, aTwo ; jumptable 00000178 case 2
000001A8 02 00 00 EA B loc_1B8
000001AC
000001AC three_case ; CODE XREF: f2+4
000001AC ; f2:loc_18C
000001AC 01 0C 8F E2 ADR R0, aThree ; jumptable 00000178 case 3
000001B0 00 00 00 EA B loc_1B8
000001B4
000001B4 four_case ; CODE XREF: f2+4
000001B4 ; f2:loc_190
000001B4 01 0C 8F E2 ADR R0, aFour ; jumptable 00000178 case 4
000001B8
000001B8 loc_1B8 ; CODE XREF: f2+24
000001B8 ; f2+2C
000001B8 66 18 00 EA B __2printf
000001BC
227
000001BC default_case ; CODE XREF: f2+4
000001BC ; f2+8
000001BC D4 00 8F E2 ADR R0, aSomethingUnkno ; jumptable 00000178
default case
000001C0 FC FF FF EA B loc_1B8
Ce code utilise les caractéristiques du mode ARM dans lequel toutes les instructions
ont une taille fixe de 4 octets.
Gardons à l’esprit que la valeur maximale de a est 4 et que toute autre valeur supé-
rieure provoquera l’affichage de la chaîne «something unknown\n»
La première instruction CMP R0, #5 compare la valeur entrée dans a avec 5.
97
L’instruction suivante, ADDCC PC, PC, R0,LSL#2, est exécutée si et seulement si
R0 < 5 (CC=Carry clear / Less than retenue vide, inférieur à). Par conséquent, si ADDCC
n’est pas exécutée (c’est le cas R0 ≥ 5), un saut au label default_case se produit.
Mais si R0 < 5 et que ADDCC est exécuté, voici ce qui se produit:
La valeur dans R0 est multipliée par 4. En fait, le suffixe de l’instruction LSL#2 signifie
« décalage à gauche de 2 bits ». Mais comme nous le verrons plus tard ( 1.24.2
on page 283) dans la section «Décalages », décaler de 2 bits vers la gauche est
équivalent à multiplier par 4.
Puis, nous ajoutons R0 ∗ 4 à la valeur courante du PC, et sautons à l’une des instruc-
tions B (Branch) situées plus bas.
Au moment de l’exécution de ADDCC, la valeur du PC est en avance de 8 octets
(0x180) sur l’adresse à laquelle l’instruction ADDCC se trouve (0x178), ou, autrement
dit, en avance de 2 instructions.
C’est ainsi que le pipeline des processeurs ARM fonctionne: lorsque ADDCC est exé-
cutée, le processeur, à ce moment, commence à préparer les instructions après la
suivante, c’est pourquoi PC pointe ici. Cela doit être mémorisé.
Si a = 0, elle sera ajoutée à la valeur de PC, et la valeur courante de PC sera écrite
dans PC (qui est 8 octets en avant) et un saut au label loc_180 sera effectué, qui est
8 octets en avant du point où l’instruction se trouve.
Si a = 1, alors P C + 8 + a ∗ 4 = P C + 8 + 1 ∗ 4 = P C + 12 = 0x184 sera écrit dans PC, qui
est l’adresse du label loc_184.
A chaque fois que l’on ajoute 1 à a, le PC résultant est incrémenté de 4.
4 est la taille des instructions en mode ARM, et donc, la longueur de chaque instruc-
tion B desquelles il y a 5 à la suite.
Chacune de ces cinq instructions B passe le contrôle plus loin, à ce qui a été pro-
grammé dans le switch().
Le chargement du pointeur sur la chaîne correspondante se produit ici, etc.
97. ADD—addition
228
ARM: avec optimisation Keil 6/2013 (Mode Thumb)
000000FE 05 DCB 5
000000FF 04 06 08 0A 0C 10 DCB 4, 6, 8, 0xA, 0xC, 0x10 ; jump table for
switch statement
00000105 00 ALIGN 2
00000106
00000106 zero_case ; CODE XREF: f2+4
00000106 8D A0 ADR R0, aZero ; jumptable 000000FA case 0
00000108 06 E0 B loc_118
0000010A
0000010A one_case ; CODE XREF: f2+4
0000010A 8E A0 ADR R0, aOne ; jumptable 000000FA case 1
0000010C 04 E0 B loc_118
0000010E
0000010E two_case ; CODE XREF: f2+4
0000010E 8F A0 ADR R0, aTwo ; jumptable 000000FA case 2
00000110 02 E0 B loc_118
00000112
00000112 three_case ; CODE XREF: f2+4
00000112 90 A0 ADR R0, aThree ; jumptable 000000FA case
3
00000114 00 E0 B loc_118
00000116
00000116 four_case ; CODE XREF: f2+4
00000116 91 A0 ADR R0, aFour ; jumptable 000000FA case 4
00000118
00000118 loc_118 ; CODE XREF: f2+12
00000118 ; f2+16
00000118 06 F0 6A F8 BL __2printf
0000011C 10 BD POP {R4,PC}
0000011E
0000011E default_case ; CODE XREF: f2+4
0000011E 82 A0 ADR R0, aSomethingUnkno ; jumptable
000000FA default case
00000120 FA E7 B loc_118
229
000061D2 00 00 ALIGN 4
000061D2 ; End of function __ARM_common_switch8_thumb
000061D2
000061D4 __32__ARM_common_switch8_thumb ; CODE XREF:
__ARM_common_switch8_thumb
000061D4 01 C0 5E E5 LDRB R12, [LR,#-1]
000061D8 0C 00 53 E1 CMP R3, R12
000061DC 0C 30 DE 27 LDRCSB R3, [LR,R12]
000061E0 03 30 DE 37 LDRCCB R3, [LR,R3]
000061E4 83 C0 8E E0 ADD R12, LR, R3,LSL#1
000061E8 1C FF 2F E1 BX R12
000061E8 ; End of function __32__ARM_common_switch8_thumb
On ne peut pas être sûr que toutes ces instructions en mode Thumb et Thumb-2
ont la même taille. On peut même dire que les instructions dans ces modes ont une
longueur variable, tout comme en x86.
Donc, une table spéciale est ajoutée, qui contient des informations sur le nombre de
cas (sans inclure celui par défaut), et un offset pour chaque label auquel le contrôle
doit être passé dans chaque cas.
Une fonction spéciale est présente ici qui s’occupe de la table et du passage du
contrôle, appelée
__ARM_common_switch8_thumb. Elle commence avec BX PC, dont la fonction est
de passer le mode du processeur en ARM. Ensuite, vous voyez la fonction pour le
traitement de la table.
C’est trop avancé pour être détaillé ici, donc passons cela.
Il est intéressant de noter que la fonction utilise le registre LR comme un pointeur
sur la table.
En effet, après l’appel de cette fonction, LR contient l’adresse après l’instruction
BL __ARM_common_switch8_thumb, où la table commence.
Il est intéressant de noter que le code est généré comme une fonction indépendante
afin de la ré-utiliser, donc le compilateur ne générera pas le même code pour chaque
déclaration switch().
IDA l’a correctement identifié comme une fonction de service et une table, et a ajouté
un commentaire au label comme jumptable 000000FA case 0.
MIPS
230
; afficher "something unknown" et terminer:
lui $a0, ($LC5 >> 16) # "something unknown"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; NOP
jr $t9
la $a0, ($LC5 & 0xFFFF) # "something unknown"
; slot de délai de branchement
231
or $at, $zero ; NOP
jr $t9
la $a0, ($LC1 & 0xFFFF) # "one" ; slot de délai de
branchement
La nouvelle instruction pour nous est SLTIU («Set on Less Than Immediate Unsigned »
Mettre si inférieur à la valeur immédiate non signée).
Ceci est la même que SLTU («Set on Less Than Unsigned »), mais «I » signifie «im-
mediate », i.e., un nombre doit être spécifié dans l’instruction elle-même.
BNEZ est «Branch if Not Equal to Zero ».
Le code est très proche de l’autre ISAs. SLL («Shift Word Left Logical ») effectue une
multiplication par 4.
MIPS est un CPU 32-bit après tout, donc toutes les adresses de la jumtable sont
32-bits.
Conclusion
case1 :
; faire quelque chose
JMP exit
case2 :
; faire quelque chose
JMP exit
case3 :
232
; faire quelque chose
JMP exit
case4 :
; faire quelque chose
JMP exit
case5 :
; faire quelque chose
JMP exit
default :
...
exit :
....
jump_table dd case1
dd case2
dd case3
dd case4
dd case5
Le saut à une adresse de la table de saut peut aussi être implémenté en utilisant
cette instruction:
JMP jump_table[REG*4]. Ou JMP jump_table[REG*8] en x64.
Une table de saut est juste un tableau de pointeurs, comme celle décrite plus loin: 1.26.5
on page 363.
void f(int a)
{
switch (a)
{
case 1:
case 2:
case 7:
case 10:
printf ("1, 2, 7, 10\n") ;
break ;
case 3:
case 4:
case 5:
case 6:
printf ("3, 4, 5\n") ;
break ;
case 8:
233
case 9:
case 20:
case 21:
printf ("8, 9, 21\n") ;
break ;
case 22:
printf ("22\n") ;
break ;
default :
printf ("default\n") ;
break ;
};
};
int main()
{
f(4) ;
};
C’est souvent du gaspillage de générer un bloc pour chaque cas possible, c’est pour-
quoi ce qui se fait d’habitude, c’est de générer un bloc et une sorte de répartiteur.
MSVC
234
28 mov DWORD PTR _a$[esp-4], OFFSET $SG2806 ; 'default'
29 jmp DWORD PTR __imp__printf
30 npad 2 ; aligner la table $LN11@f sur une limite de 16-octet
31 $LN11@f :
32 DD $LN5@f ; afficher '1, 2, 7, 10'
33 DD $LN4@f ; afficher '3, 4, 5'
34 DD $LN3@f ; afficher '8, 9, 21'
35 DD $LN2@f ; afficher '22'
36 DD $LN1@f ; afficher 'default'
37 $LN10@f :
38 DB 0 ; a=1
39 DB 0 ; a=2
40 DB 1 ; a=3
41 DB 1 ; a=4
42 DB 1 ; a=5
43 DB 1 ; a=6
44 DB 0 ; a=7
45 DB 2 ; a=8
46 DB 2 ; a=9
47 DB 0 ; a=10
48 DB 4 ; a=11
49 DB 4 ; a=12
50 DB 4 ; a=13
51 DB 4 ; a=14
52 DB 4 ; a=15
53 DB 4 ; a=16
54 DB 4 ; a=17
55 DB 4 ; a=18
56 DB 4 ; a=19
57 DB 2 ; a=20
58 DB 2 ; a=21
59 DB 3 ; a=22
60 _f ENDP
Nous voyons deux tables ici: la première ($LN10@f) est une table d’index, et la se-
conde ($LN11@f) est un tableau de pointeurs sur les blocs.
Tout d’abord, la valeur entrée est utilisée comme un index dans la table d’index
(ligne 13).
Voici un petit récapitulatif pour les valeurs dans la table: 0 est le premier bloc case
(pour les valeurs 1, 2, 7, 10), 1 est le second (pour les valeurs 3, 4, 5), 2 est le
troisième (pour les valeurs 8, 9, 21), 3 est le quatrième (pour la valeur 22), 4 est
pour le bloc par défaut.
Ici, nous obtenons un index pour la seconde table de pointeurs sur du code et nous
y sautons (ligne 14).
Il est intéressant de remarquer qu’il n’y a pas de cas pour une valeur d’entrée de 0.
C’est pourquoi nous voyons l’instruction DEC à la ligne 10, et la table commence à
a = 1, car il n’y a pas besoin d’allouer un élément dans la table pour a = 0.
C’est un pattern très répandu.
235
Donc, pourquoi est-ce que c’est économique ? Pourquoi est-ce qu’il n’est pas possible
de faire comme avant ( 1.21.2 on page 225), avec une seule table consistant en des
pointeurs vers les blocs? La raison est que les index des éléments de la table sont
8-bit, donc c’est plus compact.
GCC
GCC génère du code de la façon dont nous avons déjà discuté ( 1.21.2 on page 225),
en utilisant juste une table de pointeurs.
Il n’y a pas de code à exécuter si la valeur entrée est 0, c’est pourquoi GCC essaye de
rendre la table des sauts plus compacte et donc il commence avec la valeur d’entrée
1.
GCC 4.9.1 pour ARM64 utilise un truc encore plus astucieux. Il est capable d’encoder
tous les offsets en octets 8-bit.
Rappelons-nous que toutes les instructions ARM64 ont une taille de 4 octets.
GCC utilise le fait que tous les offsets de mon petit exemple sont tous proche l’un
de l’autre. Donc la table des sauts consiste en de simple octets.
236
; tout ce qui se trouve après la déclaration ".section" est alloué dans le
segment de données
; en lecture seule (rodata) :
.L4 :
.byte (.L3 - .Lrtx4) / 4 ; case 1
.byte (.L3 - .Lrtx4) / 4 ; case 2
.byte (.L5 - .Lrtx4) / 4 ; case 3
.byte (.L5 - .Lrtx4) / 4 ; case 4
.byte (.L5 - .Lrtx4) / 4 ; case 5
.byte (.L5 - .Lrtx4) / 4 ; case 6
.byte (.L3 - .Lrtx4) / 4 ; case 7
.byte (.L6 - .Lrtx4) / 4 ; case 8
.byte (.L6 - .Lrtx4) / 4 ; case 9
.byte (.L3 - .Lrtx4) / 4 ; case 10
.byte (.L2 - .Lrtx4) / 4 ; case 11
.byte (.L2 - .Lrtx4) / 4 ; case 12
.byte (.L2 - .Lrtx4) / 4 ; case 13
.byte (.L2 - .Lrtx4) / 4 ; case 14
.byte (.L2 - .Lrtx4) / 4 ; case 15
.byte (.L2 - .Lrtx4) / 4 ; case 16
.byte (.L2 - .Lrtx4) / 4 ; case 17
.byte (.L2 - .Lrtx4) / 4 ; case 18
.byte (.L2 - .Lrtx4) / 4 ; case 19
.byte (.L6 - .Lrtx4) / 4 ; case 20
.byte (.L6 - .Lrtx4) / 4 ; case 21
.byte (.L7 - .Lrtx4) / 4 ; case 22
.text
; tout ce qui se trouve après la déclaration ".text" est alloué dans le
segment de code (text) :
.L7 :
; afficher "22"
adrp x0, .LC3
add x0, x0, :lo12 :.LC3
b puts
.L6 :
; afficher "8, 9, 21"
adrp x0, .LC2
add x0, x0, :lo12 :.LC2
b puts
.L5 :
; afficher "3, 4, 5"
adrp x0, .LC1
add x0, x0, :lo12 :.LC1
b puts
.L3 :
; afficher "1, 2, 7, 10"
adrp x0, .LC0
add x0, x0, :lo12 :.LC0
b puts
.LC0 :
.string "1, 2, 7, 10"
.LC1 :
.string "3, 4, 5"
.LC2 :
237
.string "8, 9, 21"
.LC3 :
.string "22"
.LC4 :
.string "default"
Compilons cet exemple en un fichier objet et ouvrons-le dans IDA. Voici la table des
sauts:
Donc dans le cas de 1, 9 est multiplié par 4 et ajouté à l’adresse du label Lrtx4.
Dans le cas de 22, 0 est multiplié par 4, ce qui donne 0.
Juste après le label Lrtx4 se trouve le label L7, où se trouve le code qui affiche «22 ».
Il n’y a pas de table des sauts dans le segment de code, elle est allouée dans la
section .rodata (il n’y a pas de raison de l’allouer dans le segment de code).
Il y a aussi des octets négatifs (0xF7), ils sont utilisés pour sauter en arrière dans le
code qui affiche la chaîne «default » (en .L2).
238
1.21.4 Fall-through
Un autre usage très répandu de l’opérateur switch() est ce qu’on appelle un «fall-
through » (passer à travers). Voici un exemple simple98 :
1 bool is_whitespace(char c) {
2 switch (c) {
3 case ' ' : // fallthrough
4 case '\t' : // fallthrough
5 case '\r' : // fallthrough
6 case '\n' :
7 return true ;
8 default : // not whitespace
9 return false ;
10 }
11 }
239
31 };
Nous atteignons le label .L5 si la fonction a reçue le nombre 3250 en entrée. Mais
nous pouvons atteindre ce label d’une autre façon: nous voyons qu’il n’y a pas de
240
saut entre l’appel à printf() et le label .L5.
Nous comprenons maintenant pourquoi la déclaration switch() est parfois une source
de bug: un break oublié va transformer notre déclaration switch() en un fallthrough,
et plusieurs blocs seront exécutés au lieu d’un seul.
1.21.5 Exercices
Exercice#1
Il est possible de modifier l’exemple en C de 1.21.2 on page 219 de telle sorte que
le compilateur produise un code plus concis, mais qui fonctionne toujours pareil.
1.22 Boucles
1.22.1 Exemple simple
x86
Il y a une instruction LOOP spéciale en x86 qui teste le contenu du registre ECX et
si il est différent de 0, le décrémente et continue l’exécution au label de l’opérande
LOOP. Probablement que cette instruction n’est pas très pratique, et il n’y a aucun
compilateur moderne qui la génère automatiquement. Donc, si vous la rencontrez
dans du code, il est probable qu’il s’agisse de code assembleur écrit manuellement.
En C/C++ les boucles sont en général construites avec une déclaration for(), while()
ou do/while().
Commençons avec for().
Cette déclaration définit l’initialisation de la boucle (met le compteur à sa valeur ini-
tiale), la condition de boucle (est-ce que le compteur est plus grand qu’une limite?),
qu’est-ce qui est fait à chaque itération (incrémenter/décrémenter) et bien sûr le
corps de la boucle.
for (initialisation ; condition ; à chaque itération)
{
corps_de_la_boucle ;
}
void printing_function(int i)
{
printf ("f(%d)\n", i) ;
};
int main()
{
241
int i ;
return 0;
};
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
242
sub esp, 20h
mov [esp+20h+var_4], 2 ; initialiser (i)
jmp short loc_8048476
loc_8048465 :
mov eax, [esp+20h+var_4]
mov [esp+20h+var_20], eax
call printing_function
add [esp+20h+var_4], 1 ; incrémenter (i)
loc_8048476 :
cmp [esp+20h+var_4], 9
jle short loc_8048465 ; si i<=9, continuer la boucle
mov eax, 0
leave
retn
main endp
Ce qui se passe alors, c’est que l’espace pour la variable i n’est plus alloué sur la
pile locale, mais utilise un registre individuel pour cela, ESI. Ceci est possible pour
ce genre de petites fonctions, où il n’y a pas beaucoup de variables locales.
Il est très important que la fonction f() ne modifie pas la valeur de ESI. Notre com-
pilateur en est sûr ici. Et si le compilateur décide d’utiliser le registre ESI aussi dans
la fonction f(), sa valeur devra être sauvegardée lors du prologue de la fonction et
restaurée lors de son épilogue, presque comme dans notre listing: notez les PUSH
ESI/POP ESI au début et à la fin de la fonction.
Essayons GCC 4.4.1 avec l’optimisation la plus performante (option -O3) :
243
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
mov [esp+10h+var_10], 2
call printing_function
mov [esp+10h+var_10], 3
call printing_function
mov [esp+10h+var_10], 4
call printing_function
mov [esp+10h+var_10], 5
call printing_function
mov [esp+10h+var_10], 6
call printing_function
mov [esp+10h+var_10], 7
call printing_function
mov [esp+10h+var_10], 8
call printing_function
mov [esp+10h+var_10], 9
call printing_function
xor eax, eax
leave
retn
main endp
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
push ebx
mov ebx, 2 ; i=2
sub esp, 1Ch
100. Un très bon article à ce sujet: [Ulrich Drepper, What Every Programmer Should Know About Memory,
(2007)]101 . D’autres recommandations sur l’expansion des boucles d’Intel sont ici: [Intel® 64 and IA-32
Architectures Optimization Reference Manual, (2014)3.4.1.7].
244
; aligner le label loc_80484D0 (début du corps de la boucle) sur une limite
de 16-octet:
nop
loc_80484D0 :
; passer (i) comme premier argument à printing_function() :
mov [esp+20h+var_20], ebx
add ebx, 1 ; i++
call printing_function
cmp ebx, 64h ; i==100?
jnz short loc_80484D0 ; si non, continuer
add esp, 1Ch
xor eax, eax ; renvoyer 0
pop ebx
mov esp, ebp
pop ebp
retn
main endp
C’est assez similaire à ce que MSVC 2010 génère avec l’optimisation (/Ox), avec
l’exception que le registre EBX est utilisé pour la variable i.
GCC est sûr que ce registre ne sera pas modifié à l’intérieur de la fonction f(), et si
il l’était, il serait sauvé dans le prologue de la fonction et restauré dans l’épilogue,
tout comme dans la fonction main().
245
x86: OllyDbg
Compilons notre exemple dans MSVC 2010 avec les options /Ox et /Ob0, puis char-
geons le dans OllyDbg.
Il semble qu’OllyDbg soit capable de détecter des boucles simples et les affiche entre
parenthèses, par commodité.
En traçant (F8 — enjamber) nous voyons ESI s’incrémenter. Ici, par exemple, ESI =
i=6:
246
Fig. 1.57: OllyDbg : ESI = 10, fin de la boucle
x86: tracer
Comme nous venons de le voir, il n’est pas très commode de tracer manuellement
dans le débogueur. C’est pourquoi nous allons essayer tracer.
Nous ouvrons dans IDA l’exemple compilé, trouvons l’adresse de l’instruction PUSH
ESI (qui passe le seul argument à f()), qui est 0x401026 dans ce cas et nous lançons
le tracer :
tracer.exe -l :loops_2.exe bpx=loops_2.exe !0x00401026
BPX met juste un point d’arrêt à l’adresse et tracer va alors afficher l’état des re-
gistres.
Voici ce que l’on voit dans tracer.log :
PID=12884|New process loops_2.exe
(0) loops_2.exe !0x401026
EAX=0x00a328c8 EBX=0x00000000 ECX=0x6f0f4714 EDX=0x00000000
ESI=0x00000002 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=PF ZF IF
(0) loops_2.exe !0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000003 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF PF AF SF IF
(0) loops_2.exe !0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000004 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF PF AF SF IF
(0) loops_2.exe !0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000005 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
247
FLAGS=CF AF SF IF
(0) loops_2.exe !0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000006 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF PF AF SF IF
(0) loops_2.exe !0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000007 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF AF SF IF
(0) loops_2.exe !0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000008 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF AF SF IF
(0) loops_2.exe !0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000009 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF PF AF SF IF
PID=12884|Process loops_2.exe exited. ExitCode=0 (0x0)
248
Nous chargeons loops_2.exe.idc dans IDA et voyons:
Nous voyons que ESI varie de 2 à 9 au début du corps de boucle, mais de 3 à 0xA
(10) après l’incrément. Nous voyons aussi que main() se termine avec 0 dans EAX.
tracer génère également loops_2.exe.txt, qui contient des informations sur le
nombre de fois qu’une instruction a été exécutée et les valeurs du registre:
ARM
249
main
STMFD SP !, {R4,LR}
MOV R4, #2
B loc_368
loc_35C ; CODE XREF: main+1C
MOV R0, R4
BL printing_function
ADD R4, R4, #1
Le compteur de boucle i est stocké dans le registre R4. L’instruction MOV R4, #2
initialise i. Les instructions MOV R0, R4 et BL printing_function composent le
corps de la boucle, la première instruction préparant l’argument pour la fonction
f() et la seconde l’appelant. L’instruction ADD R4, R4, #1 ajoute 1 à la variable i
à chaque itération. CMP R4, #0xA compare i avec 0xA (10). L’instruction suivante,
BLT (Branch Less Than) saute si i est inférieur à 10. Autrement, 0 est écrit dans R0
(puisque notre fonction renvoie 0) et l’exécution de la fonction se termine.
_main
PUSH {R4,LR}
MOVS R4, #2
_main
PUSH {R4,R7,LR}
MOVW R4, #0x1124 ; "%d\n"
MOVS R1, #2
MOVT.W R4, #0
ADD R7, SP, #4
ADD R4, PC
250
MOV R0, R4
BLX _printf
MOV R0, R4
MOVS R1, #3
BLX _printf
MOV R0, R4
MOVS R1, #4
BLX _printf
MOV R0, R4
MOVS R1, #5
BLX _printf
MOV R0, R4
MOVS R1, #6
BLX _printf
MOV R0, R4
MOVS R1, #7
BLX _printf
MOV R0, R4
MOVS R1, #8
BLX _printf
MOV R0, R4
MOVS R1, #9
BLX _printf
MOVS R0, #0
POP {R4,R7,PC}
Donc, non seulement LLVM déroule la boucle, mais aussi inline ma fonction très
simple et insère son corps 8 fois au lieu de l’appeler.
Ceci est possible lorsque la fonction est très simple (comme la mienne) et lorsqu’elle
n’est pas trop appelée (comme ici).
251
; sauver FP et LR dans la pile locale:
stp x29, x30, [sp, -32]!
; préparer une structure de pile:
add x29, sp, 0
; sauver le contenu du registre X19 dans la pile locale:
str x19, [sp,16]
; nous allons utiliser le registre W19 comme compteur.
; lui assigner une valeur initiale de 2:
mov w19, 2
.L3 :
; préparer le premier argument de printing_function() :
mov w0, w19
; incrémenter le registre compteur.
add w19, w19, 1
; ici W0 contient toujours la valeur du compteur avant incrémentation.
bl printing_function
; est-ce terminé?
cmp w19, 10
; non, sauter au début du corps de boucle:
bne .L3
; renvoyer 0
mov w0, 0
; restaurer le contenu du registre X19:
ldr x19, [sp,16]
; restaurer les valeurs de FP et LR:
ldp x29, x30, [sp], 32
ret
.LC0 :
.string "f(%d)\n"
252
ret
main :
; sauvegarder FP et LR sur la pile locale:
stp x29, x30, [sp, -32]!
; préparer la structure de pile:
add x29, sp, 0
; initialiser le compteur
mov w0, 2
; le stocker dans l'espace alloué pour lui dans la pile locale:
str w0, [x29,28]
; passer le corps de la boucle et sauter aux instructions de vérification de
la condition de boucle:
b .L3
.L4 :
; charger la valeur du compteur dans W0.
; ce sera le premier argument de printing_function() :
ldr w0, [x29,28]
; appeler printing_function() :
bl printing_function
; incrémenter la valeur du compteur:
ldr w0, [x29,28]
add w0, w0, 1
str w0, [x29,28]
.L3 :
; tester condition de boucle.
; charger la valeur du compteur:
ldr w0, [x29,28]
; est-ce 9?
cmp w0, 9
; inférieur ou égal? alors sauter au début du corps de boucle:
; autrement, ne rien faire.
ble .L4
; renvoyer 0
mov w0, 0
; restaurer les valeurs de FP et LR:
ldp x29, x30, [sp], 32
ret
MIPS
; prologue de la fonction:
addiu $sp, -0x28
sw $ra, 0x28+saved_RA($sp)
253
sw $fp, 0x28+saved_FP($sp)
move $fp, $sp
; initialiser le compteur à 2 et stocker cette valeur dans la pile locale
li $v0, 2
sw $v0, 0x28+i($fp)
; pseudo-instruction. "BEQ $ZERO, $ZERO, loc_9C" c'est en fait:
b loc_9C
or $at, $zero ; slot de délai de branchement, NOP
L’instruction qui est nouvelle pour nous est B. C’est la pseudo instruction (BEQ).
Dans le code généré, nous pouvons voir: après avoir initialisé i, le corps de la boucle
n’est pas exécuté, car la condition sur i est d’abord vérifiée, et c’est seulement après
cela que le corps de la boucle peut être exécuté. Et cela est correct.
Ceci car si la condition de boucle n’est pas remplie au début, le corps de la boucle
ne doit pas être exécuté. Ceci est possible dans le cas suivant:
for (i=0; i<nombre_total_d_element_à_traiter ; i++)
corps_de_la_boucle ;
254
Si nombre_total_d_element_à_traiter est 0, le corps de la boucle ne sera pas exécuté
du tout.
C’est pourquoi la condition est testée avant l’exécution.
Toutefois, un compilateur qui optimise pourrait échanger le corps de la boucle et la
condition, si il est certain que la situation que nous venons de décrire n’est pas pos-
sible (comme dans le cas de notre exemple simple, et en utilisant des compilateurs
comme Keil, Xcode (LLVM) et MSVC avec le flag d’optimisation.
void my_memcpy (unsigned char* dst, unsigned char* src, size_t cnt)
{
size_t i ;
for (i=0; i<cnt ; i++)
dst[i]=src[i];
};
Implémentation simple
255
Listing 1.177: GCC 4.9 ARM64 optimisé pour la taille (-Os)
my_memcpy :
; X0 = adresse de destination
; X1 = adresse source
; X2 = taille de bloc
PUSH {r4,lr}
; initialiser le compteur (i) à 0
MOVS r3,#0
; la condition est testée à la fin de la fonction, donc y sauter:
B |L0.12|
|L0.6|
; charger l'octet en R1+i:
LDRB r4,[r1,r3]
; stocker l'octet en R0+i:
STRB r4,[r0,r3]
; i++
ADDS r3,r3,#1
|L0.12|
; i<taille?
CMP r3,r2
; sauter au début de la boucle si c'est le cas:
BCC |L0.6|
POP {r4,pc}
ENDP
256
Listing 1.179: avec optimisation Keil 6/2013 (Mode ARM)
my_memcpy PROC
; R0 = adresse de destination
; R1 = adresse source
; R2 = taille de bloc
MIPS
257
; former l'adresse de l'octet dans le bloc source:
addu $t0, $a1, $v0
; $t0 = $a1+$v0 = src+i
; sauter au corps de la boucle si le compteur est toujours inférieur à
"cnt":
bnez $v1, loc_8
; former l'adresse de l'octet dans le bloc de destination ($a3 = $a0+$v0 =
dst+i) :
addu $a3, $a0, $v0 ; slot de délai de branchement
; terminer si BNEZ n'a pas exécuté de saut:
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
Nous avons ici deux nouvelles instructions: LBU («Load Byte Unsigned » charger un
octet non signé) et SB («Store Byte » stocker un octet).
Tout comme en ARM, tous les registres MIPS ont une taille de 32-bit, il n’y en a pas
d’un octet de large comme en x86.
Donc, lorsque l’on travaille avec des octets seuls, nous devons utiliser un registre
de 32-bit pour chacun d’entre eux.
LBU charge un octet et met les autres bits à zéro («Unsigned »).
En revanche, l’instruction LB (« Load Byte ») étend le signe de l’octet chargé sur
32-bit.
SB écrit simplement un octet depuis les 8 bits de poids faible d’un registre dans la
mémoire.
Vectorisation
GCC avec optimisation peut faire beaucoup mieux avec cet exemple: 1.36.1 on
page 533.
258
f :
; check condition (1) :
cmp edi, esi
jge .L9
push rbp
push rbx
mov ebp, esi
mov ebx, edi
sub rsp, 8
.L5 :
mov edx, ebx
xor eax, eax
mov esi, OFFSET FLAT :.LC0 ; "%d\n"
mov edi, 1
add ebx, 1
call __printf_chk
; check condition (2) :
cmp ebp, ebx
jne .L5
add rsp, 8
pop rbx
pop rbp
.L9 :
rep ret
Dans le cas présent, il ne fait aucun doute que la structure do/while() peut être
remplacée par une construction for(), et que le premier contrôle peut être supprimé.
1.22.4 Conclusion
Squelette grossier d’une boucle de 2 à 9 inclus:
259
Listing 1.181: x86
mov [counter], 2 ; initialisation
jmp check
body :
; corps de la boucle
; faire quelque chose ici
; utiliser la variable compteur dans la pile locale
add [counter], 1 ; incrémenter
check :
cmp [counter], 9
jle body
Si le corps de la boucle est court, un registre entier peut être dédié à la variable
compteur:
Certaines parties de la boucle peuvent être générées dans un ordre différent par le
compilateur:
260
ADD [counter], 1 ; incrémenter
label_check :
CMP [counter], 10
JGE exit
; corps de la boucle
; faire quelque cose ici
; utiliser la variable compteur dans la pile locale
JMP label_increment
exit :
En utilisant l’instruction LOOP. Ceci est rare, les compilateurs ne l’utilisent pas. Lorsque
vous la voyez, c’est le signe que le morceau de code a été écrit à la main:
ARM.
Le registre R4 est dédié à la variable compteur dans cet exemple:
261
CMP R4, #10
BLT body
1.22.5 Exercices
• http://challenges.re/54
• http://challenges.re/55
• http://challenges.re/56
• http://challenges.re/57
while( *eos++ ) ;
int main()
{
// test
return my_strlen("hello !") ;
};
x86
Compilons:
_eos$ = -4 ; size = 4
_str$ = 8 ; size = 4
_strlen PROC
push ebp
mov ebp, esp
push ecx
262
mov eax, DWORD PTR _str$[ebp] ; copier le pointeur sur la chaîne
"str"
mov DWORD PTR _eos$[ebp], eax ; le copier dans la variable locale
"eos"
$LN2@strlen_ :
mov ecx, DWORD PTR _eos$[ebp] ; ECX=eos
263
instruction teste simplement si la valeur dans EDX est égale à 0.
push ebp
mov ebp, esp
sub esp, 10h
mov eax, [ebp+arg_0]
mov [ebp+eos], eax
loc_80483F0 :
mov eax, [ebp+eos]
movzx eax, byte ptr [eax]
test al, al
setnz al
add [ebp+eos], 1
test al, al
jnz short loc_80483F0
mov edx, [ebp+eos]
mov eax, [ebp+arg_0]
mov ecx, edx
sub ecx, eax
mov eax, ecx
sub eax, 1
leave
retn
strlen endp
Le résultat est presque le même qu’avec MSVC, mais ici nous voyons MOVZX au lieu
de MOVSX. MOVZX signifie MOV with Zero-Extend (déplacement avec extension à 0).
Cette instruction copie une valeur 8-bit ou 16-bit dans un registre 32-bit et met les
bits restant à 0. En fait, cette instructions n’est pratique que pour nous permettre
de remplacer cette paire d’instructions:
xor eax, eax / mov al, [...].
D’un autre côté, il est évident que le compilateur pourrait produire ce code:
mov al, byte ptr [eax] / test al, al—c’est presque le même, toutefois, les
bits les plus haut du registre EAX vont contenir des valeurs aléatoires. Mais, admet-
tons que c’est un inconvénient du compilateur—-il ne peut pas produire du code
plus compréhensible. À strictement parler, le compilateur n’est pas du tout obligé
de générer du code compréhensible par les humains.
La nouvelle instruction suivante est SETNZ. Ici, si AL ne contient pas zéro, test al,
al met le flag ZF à 0, mais SETNZ, si ZF==0 (NZ signifie not zero, non zéro) met AL
264
à 1. En langage naturel, si AL n’est pas zéro, sauter en loc_80483F0. Le compilateur
génère du code redondant, mais n’oublions pas qu’il n’est pas en mode optimisation.
Maintenant, compilons tout cela avec MSVC 2012, avec le flag d’optimisation (/Ox) :
C’est plus simple maintenant. Inutile de préciser que le compilateur ne peut utiliser
les registres aussi efficacement que dans une petite fonction, avec peu de variables
locales.
INC/DEC—sont des instructions de incrémentation/décrémentation, en d’autres mots:
ajouter ou soustraire 1 d’une/à une variable.
265
MSVC avec optimisation + OllyDbg
Nous pouvons essayer cet exemple (optimisé) dans OllyDbg. Voici la première itéra-
tion:
Nous voyons qu’OllyDbg a trouvé une boucle et, par facilité, a mis ses instructions
entre crochets. En cliquant sur le bouton droit sur EAX, nous pouvons choisir «Follow
in Dump » et la fenêtre de la mémoire se déplace jusqu’à la bonne adresse. Ici, nous
voyons la chaîne «hello! » en mémoire. Il y a au moins un zéro après cette dernière
et ensuite des données aléatoires.
Si OllyDbg voit un registre contenant une adresse valide, qui pointe sur une chaîne,
il montre cette chaîne.
266
Appuyons quelques fois sur F8 (enjamber), pour aller jusqu’au début du corps de la
boucle:
267
Nous devons appuyons un certain nombre de fois sur F8 afin de sortir de la boucle:
Nous voyons qu’EAX contient l’adresse de l’octet à zéro situé juste après la chaîne.
Entre temps, EDX n’a pas changé, donc il pointe sur le début de la chaîne.
La différence entre ces deux valeurs est maintenant calculée.
268
L’instruction SUB vient juste d’être effectuée:
La différence entre les deux pointeurs est maintenant dans le registre EAX—7. Effec-
tivement, la longueur de la chaîne «hello! » est 6, mais avec l’octet à zéro inclus—7.
Mais strlen() doit renvoyer le nombre de caractère non-zéro dans la chaîne. Donc
la décrémentation est effectuée et ensuite la fonction sort.
push ebp
mov ebp, esp
mov ecx, [ebp+arg_0]
mov eax, ecx
loc_8048418 :
movzx edx, byte ptr [eax]
add eax, 1
test dl, dl
jnz short loc_8048418
269
not ecx
add eax, ecx
pop ebp
retn
strlen endp
mov dl, byte ptr [eax]. Ici GCC génère presque le même code que MSVC, à l’ex-
ception de la présence de MOVZX. Toutefois, ici, MOVZX pourrait être remplacé par
mov dl, byte ptr [eax].
Peut-être est-il plus simple pour le générateur de code de GCC se se rappeler que
le registre 32-bit EDX est alloué entièrement pour une variable char et il est sûr que
les bits en partie haute ne contiennent pas de bruit indéfini.
Après cela, nous voyons une nouvelle instruction—NOT. Cette instruction inverse tout
les bits de l’opérande.
Elle peut être vu comme un synonyme de l’instruction XOR ECX, 0ffffffffh. NOT
et l’instruction suivante ADD calcule la différence entre les pointeurs et soustrait 1,
d’une façon différente. Au début, ECX, où le pointeur sur str est stocké, est inversé
et 1 en est soustrait.
Voir aussi: «Représentations des nombres signés » ( 2.2 on page 585).
En d’autres mots, à la fin de la fonction juste après le corps de la boucle, ces opéra-
tions sont exécutées:
ecx=str ;
eax=eos ;
ecx=(-ecx)-1;
eax=eax+ecx
return eax
Pourquoi est-ce que GCC décide que cela est mieux? Difficile à deviner. Mais peut-
être que les deux variantes sont également efficaces.
ARM
ARM 32-bit
270
Listing 1.189: sans optimisation Xcode 4.6.3 (LLVM) (Mode ARM)
_strlen
eos = -8
str = -4
LLVM sans optimisation génère beaucoup trop de code, toutefois, ici nous pouvons
voir comment la fonction travaille avec les variables locales. Il y a seulement deux
variables locales dans notre fonction: eos et str. Dans ce listing, généré par IDA, nous
avons renommé manuellement var_8 et var_4 en eos et str.
La première instruction sauve simplement les valeurs d’entrée dans str et eos.
Le corps de la boucle démarre au label loc_2CB8.
Les trois première instructions du corps de la boucle (LDR, ADD, STR) chargent la
valeur de eos dans R0. Puis la valeur est incrémentée et sauvée dans eos, qui se
trouve sur la pile.
L’instruction suivante, LDRSB R0, [R0] («Load Register Signed Byte »), charge un
octet depuis la mémoire à l’adresse stockée dans RR0 et étend le signe à 32-bit104 .
Ceci est similaire à l’instruction MOVSX en x86.
Le compilateur traite cet octet comme signé, puisque le type char est signé selon la
norme C. Il a déjà été écrit à propos de cela ( 1.23.1 on page 263) dans cette section,
en relation avec le x86.
Il est à noter qu’il est impossible en ARM d’utiliser séparément la partie 8- ou 16-bit
d’un registre 32-bit complet, comme c’est le cas en x86.
Apparemment, c’est parce que le x86 à une énorme histoire de rétro-compatibilité
104. Le compilateur Keil considère le type char comme signé, tout comme MSVC et GCC.
271
avec ses ancêtres, jusqu’au 8086 16-bit et même 8080 8-bit, mais ARM a été déve-
loppé à partir de zéro comme un processeur RISC 32-bit.
Par conséquent, pour manipuler des octets séparés en ARM, on doit tout de même
utiliser des registres 32-bit.
Donc, LDRSB charge des octets depuis la chaîne vers R0, un par un. Les instructions
suivantes, CMP et BEQ vérifient si l’octet chargé est 0. Si il n’est pas à 0, le contrôle
passe au début du corps de la boucle. Et si c’est 0, la boucle est terminée.
À la fin de la fonction, la différence entre eos et str est calculée, 1 en est soustrait,
et la valeur résultante est renvoyée via R0.
N.B. Les registres n’ont pas été sauvés dans cette fonction.
C’est parce que dans la convention d’appel ARM, les registres R0-R3 sont des «re-
gistres scratch », destinés à passer les arguments, et il n’est pas requis de restaurer
leur valeur en sortant de la fonction, puisque la fonction appelante ne va plus les
utiliser. Par conséquent, ils peuvent être utilisés comme bien nous semble.
Il n’y a pas d’autres registres utilisés ici, c’est pourquoi nous n’avons rien à sauve-
garder sur la pile.
Ainsi, le contrôle peut être rendu à la fonction appelante par un simple saut (BX), à
l’adresse contenue dans le registre LR.
loc_2DF6
LDRB.W R2, [R1],#1
CMP R2, #0
BNE loc_2DF6
MVNS R0, R0
ADD R0, R1
BX LR
Comme le conclut LLVM avec l’optimisation, eos et str n’ont pas besoin d’espace
dans la pile, et peuvent toujours être stockés dans les registres.
Avant le début du corps de la boucle, str est toujours dans R0, et eos—dans R1.
L’instruction LDRB.W R2, [R1],#1 charge, dans R2, un octet de la mémoire à l’adresse
stockée dans R1, en étendant le signe à une valeur 32-bit, mais pas seulement cela.
#1 à la fin de l’instruction indique un «Adressage post-indexé » («Post-indexed ad-
dressing »), qui signifie que 1 doit être ajouté à R1 après avoir chargé l’octet. Pour
en lire plus à ce propos: 1.39.2 on page 567.
272
Ensuite vous pouvez voir CMP et BNE105 dans le corps de la boucle, ces instructions
continuent de boucler jusqu’à ce que 0 soit trouvé dans la chaîne.
Les instructions MVNS106 (inverse tous les bits, comme NOT en x86) et ADD calculent
eos−str−1. ( 1.23.1 on page 270). En fait, ces deux instructions calculent R0 = str+eos,
qui est effectivement équivalent à ce qui est dans le code source, et la raison de ceci
à déjà été expliquée ici ( 1.23.1 on page 270).
Apparemment, LLVM, tout comme GCC, conclu que ce code peut être plus court (ou
plus rapide).
loc_2C8
LDRB R2, [R1],#1
CMP R2, #0
SUBEQ R0, R1, R0
SUBEQ R0, R0, #1
BNE loc_2C8
BX LR
Presque la même chose que ce que nous avions vu avant, à l’exception que l’expres-
sion str − eos − 1 peut être calculée non pas à la fin de la fonction, mais dans le corps
de la boucle. Le suffixe -EQ, comme nous devrions nous en souvenir, implique que
l’instruction ne s’exécute que si les opérandes de la dernière instruction CMP qui a
été exécutée avant étaient égaux. Ainsi, si R0 contient 0, les deux instructions SUBEQ
sont exécutées et le résultat est laissé dans le registre R0.
ARM64
my_strlen :
mov x1, x0
; X1 est maintenant un pointeur temporaire (eos), se comportant
comme un curseur
.L58 :
; charger un octet de X1 dans W2, incrémenter X1 (post-index)
ldrb w2, [x1],1
; Compare and Branch if NonZero: comparer W2 avec 0,
; sauter en .L58 si il ne l'est pas
cbnz w2, .L58
273
; calculer la différence entre le pointeur initial dans X0
; et l'adresse courante dans X1
sub x0, x1, x0
; decrement lowest 32-bit of result
sub w0, w0, #1
ret
L’algorithme est le même que dans 1.23.1 on page 265 : trouver un octet à zéro,
calculer la différence antre les pointeurs et décrémenter le résultat de 1.size_t
Quelques commentaires ont été ajouté par l’auteur de ce livre.
La seule différence notable est que cet exemple est un peu faux:
my_strlen() renvoie une valeur int 32-bit, tandis qu’elle devrait renvoyer un type
size_t ou un autre type 64-bit.
La raison est que, théoriquement, strlen() peut-être appelée pour un énorme bloc
de mémoire qui dépasse 4GB, donc elle doit être capable de renvoyer une valeur
64-bit sur une plate-forme 64-bit.
À cause de cette erreur, la dernière instruction SUB opère sur la partie 32-bit du re-
gistre, tandis que la pénultième instruction SUB travaille sur un registre 64-bit com-
plet (elle calcule la différence entre les pointeurs).
C’est une erreur de l’auteur, il est mieux de la laisser ainsi, comme un exemple de
ce à quoi ressemble le code dans un tel cas.
my_strlen :
; prologue de la fonction
sub sp, sp, #32
; le premier argument (str) va être stocké dans [sp,8]
str x0, [sp,8]
ldr x0, [sp,8]
; copier "str" dans la variable "eos"
str x0, [sp,24]
nop
.L62 :
; eos++
ldr x0, [sp,24] ; charger "eos" dans X0
add x1, x0, 1 ; incrémenter X0
str x1, [sp,24] ; sauver X0 dans "eos"
; charger dans W0 un octet de la mémoire à l'adresse dans X0
ldrb w0, [x0]
; est-ce zéro? (WZR est le registre 32-bit qui contient toujours zéro)
cmp w0, wzr
; sauter si différent de zéro (Branch Not Equal)
bne .L62
; octet à zéro trouvé. calculer maintenant la différence
; charger "eos" dans X1
ldr x1, [sp,24]
; charger "str" dans X0
274
ldr x0, [sp,8]
; calculer la différence
sub x0, x1, x0
; décrémenter le résultat
sub w0, w0, #1
; épilogue de la fonction
add sp, sp, 32
ret
C’est plus verbeux. Les variables sont beaucoup manipulées vers et depuis la mé-
moire (pile locale). Il y a la même erreur ici: l’opération de décrémentation se produit
sur la partie 32-bit du registre.
MIPS
loc_4 :
; charger l'octet à l'adresse dans "eos" dans $a1:
lb $a1, 0($v1)
or $at, $zero ; slot de délai de branchement, NOP
; si l'octet chargé n'est pas zéro, sauter en loc_4:
bnez $a1, loc_4
; incrémenter "eos" de toutes façons:
addiu $v1, 1 ; slot de délai de branchement
; boucle terminée. inverser variable "str":
nor $v0, $zero, $a0
; $v0=-str-1
jr $ra
; valeur de retour = $v1 + $v0 = eos + ( -str-1 ) = eos - str - 1
addu $v0, $v1, $v0 ; slot de délai de branchement
Il manque en MIPS une instruction NOT, mais il y a NOR qui correspond à l’opération
OR + NOT.
Cette opération est largement utilisée en électronique digitale107 . Par exemple, l’Apol-
lo Guidance Computer (ordinateur de guidage Apollo) utilisé dans le programme
Apollo, a été construit en utilisant seulement 5600 portes NOR: [Jens Eickhoff, On-
board Computers, Onboard Software and Satellite Operations: An Introduction, (2011)].
Mais l’élément NOT n’est pas très populaire en programmation informatique.
Donc, l’opération NOT est implémentée ici avec NOR DST, $ZERO, SRC.
D’après le chapitre sur les fondamentaux 2.2 on page 585 nous savons qu’une in-
version des bits d’un nombre signé est la même chose que changer son signe et
soustraire 1 du résultat.
107. NOR est appelé «porte universelle »
275
Donc ce que NOT fait ici est de prendre la valeur de str et de la transformer en −str −1.
L’opération d’addition qui suit prépare le résultat.
if(GetOpenFileName(LPOPENFILENAME))
{
...
Ce qui se passe ici, c’est que la liste de chaînes est passée à GetOpenFileName().
Ce n’est pas un problème de l’analyser: à chaque fois que l’on rencontre un octet
nul, c’est un élément. Quand on rencontre deux octets nul, c’est la fin de la liste. Si
vous passez cette chaîne à printf(), elle traitera le premier élément comme une
simple chaîne.
Donc, ceci est un chaîne, ou...? Il est plus juste de dire que c’est un buffer contenants
plusieurs chaînes-C terminées par zéro, qui peut être stocké et traité comme un tout.
Un autre exemple est la fonction strtok(). Elle prend une chaîne et y écrit des octets
nul. C’est ainsi qu’elle transforme la chaîne d’entrée en une sorte de buffer, qui
contient plusieurs chaînes-C terminées par zéro.
276
1.24.1 Multiplication
Multiplication en utilisant l’addition
Les instructions de multiplication et de division par un nombre qui est une puissance
de 2 sont souvent remplacées par des instructions de décalage.
unsigned int f(unsigned int a)
{
return a*4;
};
277
C’est ainsi que fonctionne l’instruction de décalage vers la gauche:
7 6 5 4 3 2 1 0
CF 7 6 5 4 3 2 1 0 0
Il est aussi possible de se passer des opérations de multiplication lorsque l’on multi-
plie par des nombres comme 7 ou 17, toujours en utilisant le décalage. Les mathé-
matiques utilisées ici sont assez faciles.
32-bit
#include <stdint.h>
int f1(int a)
{
return a*7;
};
int f2(int a)
{
return a*28;
};
int f3(int a)
{
return a*17;
};
278
x86
; a*28
_a$ = 8
_f2 PROC
mov ecx, DWORD PTR _a$[esp-4]
; ECX=a
lea eax, DWORD PTR [ecx*8]
; EAX=ECX*8
sub eax, ecx
; EAX=EAX-ECX=ECX*8-ECX=ECX*7=a*7
shl eax, 2
; EAX=EAX<<2=(a*7)*4=a*28
ret 0
_f2 ENDP
; a*17
_a$ = 8
_f3 PROC
mov eax, DWORD PTR _a$[esp-4]
; EAX=a
shl eax, 4
; EAX=EAX<<4=EAX*16=a*16
add eax, DWORD PTR _a$[esp-4]
; EAX=EAX+a=a*16+a=a*17
ret 0
_f3 ENDP
ARM
Keil pour le mode ARM tire partie du décalage de registre du second opérande:
279
BX lr
ENDP
; a*28
||f2|| PROC
RSB r0,r0,r0,LSL #3
; R0=R0<<3-R0=R0*8-R0=a*8-a=a*7
LSL r0,r0,#2
; R0=R0<<2=R0*4=a*7*4=a*28
BX lr
ENDP
; a*17
||f3|| PROC
ADD r0,r0,r0,LSL #4
; R0=R0+R0<<4=R0+R0*16=R0*17=a*17
BX lr
ENDP
Mais ce n’est pas disponible en mode Thumb. Il ne peut donc pas l’optimiser:
; a*28
||f2|| PROC
MOVS r1,#0x1c ; 28
; R1=28
MULS r0,r1,r0
; R0=R1*R0=28*a
BX lr
ENDP
; a*17
||f3|| PROC
LSLS r1,r0,#4
; R1=R0<<4=R0*16=a*16
ADDS r0,r0,r1
; R0=R0+R1=a+a*16=a*17
BX lr
ENDP
MIPS
280
Listing 1.200: GCC 4.4.5 avec optimisation (IDA)
_f1 :
sll $v0, $a0, 3
; $v0 = $a0<<3 = $a0*8
jr $ra
subu $v0, $a0 ; branch delay slot
; $v0 = $v0-$a0 = $a0*8-$a0 = $a0*7
_f2 :
sll $v0, $a0, 5
; $v0 = $a0<<5 = $a0*32
sll $a0, 2
; $a0 = $a0<<2 = $a0*4
jr $ra
subu $v0, $a0 ; branch delay slot
; $v0 = $a0*32-$a0*4 = $a0*28
_f3 :
sll $v0, $a0, 4
; $v0 = $a0<<4 = $a0*16
jr $ra
addu $v0, $a0 ; branch delay slot
; $v0 = $a0*16+$a0 = $a0*17
64-bit
#include <stdint.h>
int64_t f1(int64_t a)
{
return a*7;
};
int64_t f2(int64_t a)
{
return a*28;
};
int64_t f3(int64_t a)
{
return a*17;
};
x64
281
lea rax, [0+rdi*8]
; RAX=RDI*8=a*8
sub rax, rdi
; RAX=RAX-RDI=a*8-a=a*7
ret
; a*28
f2 :
lea rax, [0+rdi*4]
; RAX=RDI*4=a*4
sal rdi, 5
; RDI=RDI<<5=RDI*32=a*32
sub rdi, rax
; RDI=RDI-RAX=a*32-a*4=a*28
mov rax, rdi
ret
; a*17
f3 :
mov rax, rdi
sal rax, 4
; RAX=RAX<<4=a*16
add rax, rdi
; RAX=a*16+a=a*17
ret
ARM64
GCC 4.9 pour ARM64 est aussi concis, grâce au modificateur de décalage:
; a*28
f2 :
lsl x1, x0, 5
; X1=X0<<5=a*32
sub x0, x1, x0, lsl 2
; X0=X1-X0<<2=a*32-a<<2=a*32-a*4=a*28
ret
; a*17
f3 :
add x0, x0, x0, lsl 4
; X0=X0+X0<<4=a+a*16=a*17
282
ret
Il fût un temps où les ordinateurs étaient si gros et chers, que certains d’entre eux
ne disposaient pas de la multiplication dans le CPU, comme le Data General Nova.
Et lorsque l’on avait besoin de l’opérateur de multiplication, il pouvait être fourni
au niveau logiciel, par exemple, en utilisant l’algorithme de multiplication de Booth.
C’est un algorithme de multiplication qui utilise seulement des opérations d’addition
et de décalage.
Ce que les optimiseurs des compilateurs modernes font n’est pas la même chose,
mais le but (multiplication) et les ressources (des opérations plus rapides) sont les
mêmes.
1.24.2 Division
Division en utilisant des décalages
L’instruction SHR (SHift Right décalage à droite) dans cet exemple décale un nombre
de 2 bits vers la droite. Les deux bits libérés à gauche (i.e., les deux bits les plus
significatifs) sont mis à zéro. Les deux bits les moins significatifs sont perdus. En fait,
ces deux bits perdus sont le reste de la division.
L’instruction SHR fonctionne tout comme SHL, mais dans l’autre direction.
7 6 5 4 3 2 1 0
0 7 6 5 4 3 2 1 0 CF
283
Il est facile de comprendre si vous imaginez le nombre 23 dans le système décimal.
23 peut être facilement divisé par 10, juste en supprimant le dernier chiffre (3—le
reste de la division). Il reste 2 après l’opération, qui est le quotient.
Donc, le reste est perdu, mais c’est OK, nous travaillons de toutes façons sur des
valeurs entières, ce sont sont pas des nombres réels !
Division par 4 en ARM:
Listing 1.204: sans optimisation Keil 6/2013 (Mode ARM)
f PROC
LSR r0,r0,#2
BX lr
ENDP
1.24.3 Exercice
• http://challenges.re/59
1.25.2 x86
Ça vaut la peine de jeter un œil sur les machines à base de piles ou d’apprendre les
bases du langage Forth, avant d’étudier le FPU en x86.
Il est intéressant de savoir que dans le passé (avant le CPU 80486) le coprocesseur
était une puce séparée et n’était pas toujours préinstallé sur la carte mère. Il était
possible de l’acheter séparément et de l’installer108 .
108. Par exemple, John Carmack a utilisé des valeurs arithmétiques à virgule fixe dans son jeu vidéo
Doom, stockées dans des registres 32-bit GPR (16 bit pour la partie entière et 16 bit pour la partie frac-
tionnaire), donc Doom pouvait fonctionner sur des ordinateurs 32-bit sans FPU, i.e., 80386 et 80486 SX.
284
A partir du CPU 80486 DX, le FPU est intégré dans le CPU.
L’instruction FWAIT nous rappelle le fait qu’elle passe le CPU dans un état d’attente,
jusqu’à ce que le FPU ait fini son traitement.
Un autre rudiment est le fait que les opcodes d’instruction FPU commencent avec
ce qui est appelé l’opcode-«d’échappement » (D8..DF), i.e., opcodes passés à un
coprocesseur séparé.
Le FPU a une pile capable de contenir 8 registres de 80-bit, et chaque registre peut
contenir un nombre au format IEEE 754.
Ce sont ST(0)..ST(7). Par concision, IDA et OllyDbg montrent ST(0) comme ST, qui
est représenté dans certains livres et manuels comme «Stack Top ».
1.25.4 C/C++
Le standard des langages C/C++ offre au moins deux types de nombres à virgule
flottante, float (simple-précision, 32 bits) 109 et double (double-précision, 64 bits).
Dans [Donald E. Knuth, The Art of Computer Programming, Volume 2, 3rd ed., (1997)246]
nous pouvons trouver que simple-précision signifie que la valeur flottante peut être
stockée dans un simple mot machine [32-bit], double-précision signifie qu’elle peut
être stockée dans deux mots (64 bits).
GCC supporte également le type long double (précision étendue, 80 bit), que MSVC
ne supporte pas.
Le type float nécessite le même nombre de bits que le type int dans les environne-
ments 32-bit, mais la représentation du nombre est complètement différente.
int main()
{
109. le format des nombres à virgule flottante simple précision est aussi abordé dans la section Travailler
avec le type float comme une structure ( 1.30.6 on page 480)
285
printf ("%f\n", f(1.2, 3.4)) ;
};
x86
MSVC
pop ebp
ret 0
_f ENDP
FLD prend 8 octets depuis la pile et charge le nombre dans le registre ST(0), en
286
le convertissant automatiquement dans le format interne sur 80-bit (précision éten-
due) :
FDIV divise la valeur dans ST(0) par le nombre stocké à l’adresse
__real@40091eb851eb851f —la valeur 3.14 est encodée ici. La syntaxe assembleur
ne supporte pas les nombres à virgule flottante, donc ce que l’on voit ici est la re-
présentation hexadécimale de 3.14 au format 64-bit IEEE 754.
Après l’exécution de FDIV, ST(0) contient le quotient.
À propos, il y a aussi l’instruction FDIVP, qui divise ST(1) par ST(0), prenant ces
deux valeurs dans la pile et poussant le résultant. Si vous connaissez le langage
Forth, vous pouvez comprendre rapidement que ceci est une machine à pile.
L’instruction FLD subséquente pousse la valeur de b sur la pile.
Après cela, le quotient est placé dans ST(1), et ST(0) a la valeur de b.
L’instruction suivante effectue la multiplication: b de ST(0) est multiplié par la valeur
en
__real@4010666666666666 (le nombre 4.1 est là) et met le résultat dans le registre
ST(0).
La dernière instruction FADDP ajoute les deux valeurs au sommet de la pile, stockant
le résultat dans ST(1) et supprimant la valeur de ST(0), laissant ainsi le résultat au
sommet de la pile, dans ST(0).
La fonction doit renvoyer son résultat dans le registre ST(0), donc il n’y a aucune
autre instruction après FADDP, excepté l’épilogue de la fonction.
287
MSVC + OllyDbg
2 paires de mots 32-bit sont marquées en rouge sur la pile. Chaque paire est un
double au format IEEE 754 et est passée depuis main().
Nous voyons comment le premier FLD charge une valeur (1.2) depuis la pile et la
stocke dans ST(0) :
288
Continuons l’exécution pas à pas. FDIV a été exécuté, maintenant ST(0) contient
0.382…(quotient) :
289
Troisième étape: le FLD suivant a été exécuté, chargeant 3.4 dans ST(0) (ici nous
voyons la valeur approximative 3.39999…) :
En même temps, le quotient est poussé dans ST(1). Exactement maintenant, EIP
pointe sur la prochaine instruction: FMUL. Ceci charge la constante 4.1 depuis la
mémoire, ce que montre OllyDbg.
290
Suivante: FMUL a été exécutée, donc maintenant le produit est dans ST(0) :
291
Suivante: FADDP a été exécutée, maintenant le résultat de l’addition est dans ST(0),
et ST(1) est vidé.
Le résultat est laissé dans ST(0), car la fonction renvoie son résultat dans ST(0).
main() prend cette valeur depuis le registre plus loin.
Nous voyons quelque chose d’inhabituel: la valeur 13.93…se trouve maintenant
dans ST(7). Pourquoi?
Comme nous l’avons lu il y a quelque temps dans ce livre, les registres FPU sont une
pile: 1.25.2 on page 285. Mais ceci est une simplification.
Imaginez si cela était implémenté en hardware comme cela est décrit, alors tout le
contenu des 7 registres devrait être déplacé (ou copié) dans les registres adjacents
lors d’un push ou d’un pop, et ceci nécessite beaucoup de travail.
En réalité, le FPU a seulement 8 registres et un pointeur (appelé TOP) qui contient
un numéro de registre, qui est le «haut de la pile » courant.
Lorsqu’une valeur est poussée sur la pile, TOP est déplacé sur le registre disponible
suivant, et une valeur est écrite dans ce registre.
292
La procédure est inversée si la valeur est lue, toutefois, le registre qui a été libéré
n’est pas vidé (il serait possible de le vider, mais ceci nécessite plus de travail qui
peut dégrader les performances). Donc, c’est ce que nous voyons ici.
On peut dire que FADDP sauve la somme sur la pile, et y supprime un élément.
Mais en fait, cette instruction sauve la somme et ensuite décale TOP.
Plus précisément, les registres du FPU sont un tampon circulaire.
GCC
GCC 4.4.1 (avec l’option -O3) génère le même code, juste un peu différent:
push ebp
fld ds :dbl_8048608 ; 3.14
fmul [ebp+arg_8]
pop ebp
faddp st(1), st
retn
f endp
La différence est que, tout d’abord, 3.14 est poussé sur la pile (dans ST(0)), et
ensuite la valeur dans arg_0 est divisée par la valeur dans ST(0).
FDIVR signifie Reverse Divide —pour diviser avec le diviseur et le dividende échan-
gés l’un avec l’autre. Il n’y a pas d’instruction de ce genre pour la multiplication
293
puisque c’est une opération commutative, donc nous avons seulement FMUL sans
son homologue -R.
FADDP ajoute les deux valeurs mais supprime aussi une valeur de la pile. Après cette
opération, ST(0) contient la somme.
Donc, nous voyons ici que des nouveaux registres sont utilisés, avec le préfixe D.
Ce sont des registres 64-bits, il y en a 32, et ils peuvent être utilisés tant pour des
nombres à virgules flottantes (double) que pour des opérations SIMD (c’est appelé
NEON ici en ARM).
Il y a aussi 32 S-registres 32 bits, destinés à être utilisés pour les nombres à virgules
flottantes simple précision (float).
C’est facile à retenir: les registres D sont pour les nombres en double précision, tandis
que les registres S—-pour les nombres en simple précision Pour aller plus loin: .2.3
on page 1362.
Les deux constantes (3.14 et 4.1) sont stockées en mémoire au format IEEE 754.
VLDR et VMOV, comme il peut en être facilement déduit, sont analogues aux instruc-
tions LDR et MOV, mais travaillent avec des registres D.
Il est à noter que ces instructions, tout comme les registres D, sont destinées non
seulement pour les nombres à virgules flottantes, mais peuvent aussi être utilisées
pour des opérations SIMD (NEON) et cela va être montré bientôt.
Les arguments sont passés à la fonction de manière classique, via les R-registres,
toutefois, chaque nombre en double précision a une taille de 64 bits, donc deux
R-registres sont nécessaires pour passer chacun d’entre eux.
294
VMOV D17, R0, R1 au début, combine les deux valeurs 32-bit de R0 et R1 en une
valeur 64-bit et la sauve dans D17.
VMOV R0, R1, D16 est l’opération inverse: ce qui est dans D16 est séparé dans deux
registres, R0 et R1, car un nombre en double précision qui nécessite 64 bit pour le
stockage, est renvoyé dans R0 et R1.
VDIV, VMUL and VADD, sont des instructions pour traiter des nombres à virgule flot-
tante, qui calculent respectivement le quotient, produit et la somme.
Le code pour Thumb-2 est similaire.
f
PUSH {R3-R7,LR}
MOVS R7, R2
MOVS R4, R3
MOVS R5, R0
MOVS R6, R1
LDR R2, =0x66666666 ; 4.1
LDR R3, =0x40106666
MOVS R0, R7
MOVS R1, R4
BL __aeabi_dmul
MOVS R7, R0
MOVS R4, R1
LDR R2, =0x51EB851F ; 3.14
LDR R3, =0x40091EB8
MOVS R0, R5
MOVS R1, R6
BL __aeabi_ddiv
MOVS R2, R7
MOVS R3, R4
BL __aeabi_dadd
POP {R3-R7,PC}
Code généré par Keil pour un processeur sans FPU ou support pour NEON.
Les nombres en virgule flottante double précision sont passés par des R-registres
génériques et au lieu d’instructions FPU, des fonctions d’une bibliothèque de service
sont appelées (comme __aeabi_dmul, __aeabi_ddiv, __aeabi_dadd) qui émulent
la multiplication, la division et l’addition pour les nombres à virgule flottante.
Bien sûr, c’est plus lent qu’un coprocesseur FPU, mais toujours mieux que rien.
295
À propos, de telles bibliothèques d’émulation de FPU étaient très populaires dans
le monde x86 lorsque les coprocesseurs étaient rares et chers, et étaient installés
seulement dans des ordinateurs coûteux.
L’émulation d’un coprocesseur FPU est appelée soft float ou armel (emulation) dans
le monde ARM, alors que l’utilisation des instructions d’un coprocesseur FPU est
appelée hard float ou armhf.
fmov x1, d0
296
; X1 = a/3.14
ldr x2, [sp]
; X2 = b
ldr x0, .LC26
; X0 = 4.1
fmov d0, x2
; D0 = b
fmov d1, x0
; D1 = 4.1
fmul d0, d0, d1
; D0 = D0*D1 = b*4.1
fmov x0, d0
; X0 = D0 = b*4.1
fmov d0, x1
; D0 = a/3.14
fmov d1, x0
; D1 = X0 = b*4.1
fadd d0, d0, d1
; D0 = D0+D1 = a/3.14 + b*4.1
297
int main ()
{
printf ("32.01 ^ 1.54 = %lf\n", pow (32.01,1.54)) ;
return 0;
}
x86
_main PROC
push ebp
mov ebp, esp
sub esp, 8 ; allouer de l'espace pour la première variable
fld QWORD PTR __real@3ff8a3d70a3d70a4
fstp QWORD PTR [esp]
sub esp, 8 ; allouer de l'espace pour la seconde variable
fld QWORD PTR __real@40400147ae147ae1
fstp QWORD PTR [esp]
call _pow
add esp, 8 ; rendre l'espace d'une variable.
FLD et FSTP déplacent des variables entre le segment de données et la pile du FPU.
pow()110 prend deux valeurs depuis la pile et renvoie son résultat dans le registre
ST(0). printf() prend 8 octets de la pile locale et les interprète comme des va-
riables de type double.
À propos, une paire d’instructions MOV pourrait être utilisée ici pour déplacer les
valeurs depuis la mémoire vers la pile, car les valeurs en mémoire sont stockées au
110. une fonction C standard, qui élève un nombre à la puissance donnée (puissance)
298
format IEEE 754, et pow() les prend aussi dans ce format, donc aucune conversion
n’est nécessaire. C’est fait ainsi dans l’exemple suivant, pour ARM: 1.25.6.
_main
var_C = -0xC
PUSH {R7,LR}
MOV R7, SP
SUB SP, SP, #4
VLDR D16, =32.01
VMOV R0, R1, D16
VLDR D16, =1.54
VMOV R2, R3, D16
BLX _pow
VMOV D16, R0, R1
MOV R0, 0xFC1 ; "32.01 ^ 1.54 = %lf\n"
ADD R0, PC
VMOV R1, R2, D16
BLX _printf
MOVS R1, 0
STR R0, [SP,#0xC+var_C]
MOV R0, R1
ADD SP, SP, #4
POP {R7,PC}
Comme nous l’avons déjà mentionné, les pointeurs sur des nombres flottants 64-bit
sont passés dans une paire de R-registres.
Ce code est un peu redondant (probablement car l’optimisation est désactivée), puis-
qu’il est possible de charger les valeurs directement dans les R-registres sans tou-
cher les D-registres.
Donc, comme nous le voyons, la fonction _pow reçoit son premier argument dans R0
et R1, et le second dans R2 et R3. La fonction laisse son résultat dans R0 et R1. Le
résultat de _pow est déplacé dans D16, puis dans la paire R1 et R2, d’où printf()
prend le nombre résultant.
_main
STMFD SP !, {R4-R6,LR}
LDR R2, =0xA3D70A4 ; y
LDR R3, =0x3FF8A3D7
LDR R0, =0xAE147AE1 ; x
LDR R1, =0x40400147
BL pow
299
MOV R4, R0
MOV R2, R4
MOV R3, R1
ADR R0, a32_011_54Lf ; "32.01 ^ 1.54 = %lf\n"
BL __2printf
MOV R0, #0
LDMFD SP !, {R4-R6,PC}
Les D-registres ne sont pas utilisés ici, juste des paires de R-registres.
Les constantes sont chargées dans D0 et D1 : pow() les prend d’ici. Le résultat sera
dans D0 après l’exécution de pow(). Il est passé à printf() sans aucune modification
ni déplacement, car printf() prend ces arguments de type intégral et pointeurs
depuis des X-registres, et les arguments en virgule flottante depuis des D-registres.
300
#include <stdio.h>
return b ;
};
int main()
{
printf ("%f\n", d_max (1.2, 3.4)) ;
printf ("%f\n", d_max (5.6, -4)) ;
};
x86
fnstsw ax
test ah, 5
jp SHORT $LN1@d_max
301
fld QWORD PTR _b$[ebp]
$LN2@d_max :
pop ebp
ret 0
_d_max ENDP
C3 C2C1C0
C3 C2C1C0
Après l’exécution de test ah, 5112 , seul les bits C0 et C2 (en position 0 et 2) sont
considérés, tous les autres bits sont simplement ignorés.
Parlons maintenant du parity flag (flag de parité), un autre rudiment historique re-
marquable.
Ce flag est mis à 1 si le nombre de un dans le résultat du dernier calcul est pair, et
à 0 s’il est impair.
111. Intel P6 comprend les Pentium Pro, Pentium II, etc.
112. 5=101b
302
Regardons sur Wikipédia113 :
Une raison commune de tester le bit de parité n’a rien à voir avec
la parité. Le FPU possède quatre flags de condition (C0 à C3), mais ils
ne peuvent pas être testés directement, et doivent d’abord être copiés
dans le registre d’états. Lorsque ça se produit, C0 est mis dans le flag
de retenue, C2 dans le flag de parité et C3 dans le flag de zéro. Le
flag C2 est mis lorsque e.g. des valeurs en virgule flottantes incompa-
rable (NaN ou format non supporté) sont comparées avec l’instruction
FUCOM.
Comme indiqué dans Wikipédia, le flag de parité est parfois utilisé dans du code FPU,
voyons comment.
Le flag PF est mis à 1 si à la fois C0 et C2 sont mis à 0 ou si les deux sont à 1, auquel
cas le JP (jump if PF==1) subséquent est déclenché. Si l’on se rappelle les valeurs
de C3/C2/C0 pour différents cas, nous pouvons voir que le saut conditionnel JP est
déclenché dans deux cas: si b > a ou a = b (le bit C3 n’est pris en considération ici,
puisqu’il a été mis à 0 par l’instruction test ah, 5).
C’est très simple ensuite. Si le saut conditionnel a été déclenché, FLD charge la
valeur de _b dans ST(0), et sinon, la valeur de _a est chargée ici.
Et à propos du test de C2 ?
Le flag C2 est mis en cas d’erreur (NaN, etc.), mais notre code ne le teste pas.
Si le programmeur veut prendre en compte les erreurs FPU, il doit ajouter des tests
supplémentaires.
113. https://en.wikipedia.org/wiki/Parity_flag
303
Premier exemple sous OllyDbg : a=1.2 et b=3.4
Arguments courants de la fonction: a = 1.2 et b = 3.4 (Nous pouvons les voir dans la
pile: deux paires de valeurs 32-bit). b (3.4) est déjà chargé dans ST(0). Maintenant
FCOMP est train d’être exécutée. OllyDbg montre le second argument de FCOMP, qui
se trouve sur la pile à ce moment.
304
FCOMP a été exécutée:
Nous voyons l’état des flags de condition du FPU : tous à zéro. La valeur dépilée est
vue ici comme ST(7), la raison a été décrite ici: 1.25.5 on page 292.
305
FNSTSW a été exécutée:
Nous voyons que le registre AX contient des zéro: en effet, tous les flags de condition
sont à zéro. (OllyDbg désassemble l’instruction FNSTSW comme FSTSW—elles sont
synonymes).
306
TEST a été exécutée:
307
JPE déclenchée, FLD charge la valeur de b (3.4) dans ST(0) :
308
Second exemple sous OllyDbg : a=5.6 et b=-4
Arguments de la fonction courante: a = 5.6 et b = −4. b (-4) est déjà chargé dans ST(0).
FCOMP va s’exécuter maintenant. OllyDbg montre le second argument de FCOMP, qui
est sur la pile juste maintenant.
309
FCOMP a été exécutée:
Nous voyons l’état des flags de condition du FPU : tous à zéro sauf C0.
310
FNSTSW a été exécutée:
Nous voyons que le registre AX contient 0x100 : le flag C0 est au 8ième bit.
311
TEST a été exécutée:
312
JPE n’a pas été déclenchée, donc FLD charge la valeur de a (5.6) dans ST(0) :
313
; copier ST(0) dans ST(1) et dépiler le registre,
; laisser (_a) au sommet
fstp ST(1)
ret 0
$LN5@d_max :
; copier ST(0) dans ST(0) et dépiler le registre,
; laisser (_b) au sommet
fstp ST(0)
ret 0
_d_max ENDP
FCOM diffère de FCOMP dans le sens où il compare seulement les deux valeurs, et ne
change pas la pile du FPU. Contrairement à l’exemple précédent, ici les opérandes
sont dans l’ordre inverse, c’est pourquoi le résultat de la comparaison dans C3/C2/C0
est différent.
• si a > b dans notre exemple, alors les bits C3/C2/C0 sont mis comme suit: 0, 0, 0.
• si b > a, alors les bits sont: 0, 0, 1.
• si a = b, alors les bits sont: 1, 0, 0.
L’instruction test ah, 65 laisse seulement deux bits —C3 et C0. Les deux seront à
zéro si a > b : dans ce cas le saut JNE ne sera pas effectué. Puis FSTP ST(1) suit —
cette instruction copie la valeur de ST(0) dans l’opérande et supprime une valeur
de la pile du FPU. En d’autres mots, l’instruction copie ST(0) (où la valeur de _a se
trouve) dans ST(1). Après cela, deux copies de _a sont sur le sommet de la pile. Puis,
une valeur est supprimée. Après cela, ST(0) contient _a et la fonction se termine.
Le saut conditionnel JNE est effectué dans deux cas: si b > a ou a = b. ST(0) est
copié dans ST(0), c’est comme une opération sans effet (NOP), puis une valeur est
supprimée de la pile et le sommet de la pile (ST(0)) contient la valeur qui était
avant dans ST(1) (qui est _b). Puis la fonction se termine. La raison pour laquelle
cette instruction est utilisée ici est sans doute que le FPU n’a pas d’autre instruction
pour prendre une valeur sur la pile et la supprimer.
314
Premier exemple sous OllyDbg : a=1.2 et b=3.4
315
FCOM a été exécutée:
316
FNSTSW a été exécutée, AX=0x3100:
317
TEST est exécutée:
318
FSTP ST (ou FSTP ST(0)) a été exécuté —1.2 a été dépilé, et 3.4 laissé au sommet
de la pile:
319
Second exemple sous OllyDbg : a=5.6 et b=-4
320
FCOM a été exécutée:
321
FNSTSW fait, AX=0x3000:
322
TEST a été exécutée:
323
FSTP ST(1) a été exécutée: une valeur de 5.6 est maintenant au sommet de la pile
du FPU.
Nous voyons maintenant que l’instruction FSTP ST(1) fonctionne comme suit: elle
laisse ce qui était au sommet de la pile, mais met ST(1) à zéro.
GCC 4.4.1
push ebp
324
mov ebp, esp
sub esp, 10h
fld [ebp+a]
fld [ebp+b]
loc_8048453 :
fld [ebp+b]
locret_8048456 :
leave
retn
d_max endp
FUCOMPP est presque comme FCOM, mais dépile deux valeurs de la pile et traite les
«non-nombres » différemment.
Quelques informations à propos des not-a-numbers (non-nombres).
Le FPU est capable de traiter les valeurs spéciales que sont les not-a-numbers (non-
nombres) ou NaNs. Ce sont les infinis, les résultat de division par 0, etc. Les non-
nombres peuvent être «quiet » et «signaling ». Il est possible de continuer à travailler
avec les «quiet » NaNs, mais si l’on essaye de faire une opération avec un «signaling »
NaNs, une exception est levée.
325
FCOM lève une exception si un des opérandes est NaN. FUCOM lève une exception
seulement si un des opérandes est un signaling NaN (SNaN).
L’instruction suivante est SAHF (Store AH into Flags stocker AH dans les Flags) —est
une instruction rare dans le code non relatif au FPU. 8 bits de AH sont copiés dans
les 8-bits bas dans les flags du CPU dans l’ordre suivant:
7 6 4 2 0
SFZF AF PF CF
Rappelons que FNSTSW déplace des bits qui nous intéressent (C3/C2/C0) dans AH et
qu’ils sont aux positions 6, 2, 0 du registre AH.
6 2 1 0
C3 C2C1C0
326
arg_8 = qword ptr 10h
push ebp
mov ebp, esp
fld [ebp+arg_0] ; _a
fld [ebp+arg_8] ; _b
loc_8048448 :
; stocker _a dans ST(1), dépiler une valeur du sommet de la pile, laisser _a
au sommet
fstp st(1)
loc_804844A :
pop ebp
retn
d_max endp
C’est presque le même, à l’exception que JA est utilisé après SAHF. En fait, les ins-
tructions de sauts conditionnels qui vérifient «plus », «moins » ou «égal » pour les
comparaisons de nombres non signés (ce sont JA, JAE, JB, JBE, JE/JZ, JNA, JNAE,
JNB, JNBE, JNE/JNZ) vérifient seulement les flags CF et ZF.
Rappelons comment les bits C3/C2/C0 sont situés dans le registre AH après l’exé-
cution de FSTSW/FNSTSW :
6 2 1 0
C3 C2C1C0
Rappelons également, comment les bits de AH sont stockés dans les flags du CPU
après l’exécution de SAHF :
7 6 4 2 0
SFZF AF PF CF
Après la comparaison, les bits C3 et C0 sont copiés dans ZF et CF, donc les sauts
conditionnels peuvent fonctionner après. st déclenché si CF et ZF sont tout les deux
à zéro.
327
Ainsi, les instructions de saut conditionnel listées ici peuvent être utilisées après une
paire d’instructions FNSTSW/SAHF.
Apparemment, les bits d’état du FPU C3/C2/C0 ont été mis ici intentionnellement,
pour facilement les relier aux flags du CPU de base sans permutations supplémen-
taires?
De nouvelles instructions FPU ont été ajoutées avec la famille Intel P6116 . Ce sont
FUCOMI (comparer les opérandes et positionner les flags du CPU principal) et FCMOVcc
(fonctionne comme CMOVcc, mais avec les registres du FPU).
Apparemment, les mainteneurs de GCC ont décidé de supprimer le support des CPUs
Intel pré-P6 (premier Pentium, 80486, etc.).
Et donc, le FPU n’est plus une unité séparée dans la famille Intel P6, ainsi il est
possible de modifier/vérifier un flag du CPU principal depuis le FPU.
Voici ce que nous obtenons:
328
Listing 1.218: GCC 4.8.1 avec optimisation and GDB
1 dennis@ubuntuvm :~/polygon$ gcc -O3 d_max.c -o d_max -fno-inline
2 dennis@ubuntuvm :~/polygon$ gdb d_max
3 GNU gdb (GDB) 7.6.1-ubuntu
4 ...
5 Reading symbols from /home/dennis/polygon/d_max...(no debugging symbols ⤦
Ç found)...done.
6 (gdb) b d_max
7 Breakpoint 1 at 0x80484a0
8 (gdb) run
9 Starting program : /home/dennis/polygon/d_max
10
11 Breakpoint 1, 0x080484a0 in d_max ()
12 (gdb) ni
13 0x080484a4 in d_max ()
14 (gdb) disas $eip
15 Dump of assembler code for function d_max :
16 0x080484a0 <+0> : fldl 0x4(%esp)
17 => 0x080484a4 <+4> : fldl 0xc(%esp)
18 0x080484a8 <+8> : fxch %st(1)
19 0x080484aa <+10> : fucomi %st(1),%st
20 0x080484ac <+12> : fcmovbe %st(1),%st
21 0x080484ae <+14> : fstp %st(1)
22 0x080484b0 <+16> : ret
23 End of assembler dump.
24 (gdb) ni
25 0x080484a8 in d_max ()
26 (gdb) info float
27 R7 : Valid 0x3fff9999999999999800 +1.199999999999999956
28 =>R6 : Valid 0x4000d999999999999800 +3.399999999999999911
29 R5 : Empty 0x00000000000000000000
30 R4 : Empty 0x00000000000000000000
31 R3 : Empty 0x00000000000000000000
32 R2 : Empty 0x00000000000000000000
33 R1 : Empty 0x00000000000000000000
34 R0 : Empty 0x00000000000000000000
35
36 Status Word : 0x3000
37 TOP : 6
38 Control Word : 0x037f IM DM ZM OM UM PM
39 PC : Extended Precision (64-bits)
40 RC : Round to nearest
41 Tag Word : 0x0fff
42 Instruction Pointer : 0x73 :0x080484a4
43 Operand Pointer : 0x7b :0xbffff118
44 Opcode : 0x0000
45 (gdb) ni
46 0x080484aa in d_max ()
47 (gdb) info float
48 R7 : Valid 0x4000d999999999999800 +3.399999999999999911
49 =>R6 : Valid 0x3fff9999999999999800 +1.199999999999999956
50 R5 : Empty 0x00000000000000000000
51 R4 : Empty 0x00000000000000000000
329
52 R3 : Empty 0x00000000000000000000
53 R2 : Empty 0x00000000000000000000
54 R1 : Empty 0x00000000000000000000
55 R0 : Empty 0x00000000000000000000
56
57 Status Word : 0x3000
58 TOP : 6
59 Control Word : 0x037f IM DM ZM OM UM PM
60 PC : Extended Precision (64-bits)
61 RC : Round to nearest
62 Tag Word : 0x0fff
63 Instruction Pointer : 0x73 :0x080484a8
64 Operand Pointer : 0x7b :0xbffff118
65 Opcode : 0x0000
66 (gdb) disas $eip
67 Dump of assembler code for function d_max :
68 0x080484a0 <+0> : fldl 0x4(%esp)
69 0x080484a4 <+4> : fldl 0xc(%esp)
70 0x080484a8 <+8> : fxch %st(1)
71 => 0x080484aa <+10> : fucomi %st(1),%st
72 0x080484ac <+12> : fcmovbe %st(1),%st
73 0x080484ae <+14> : fstp %st(1)
74 0x080484b0 <+16> : ret
75 End of assembler dump.
76 (gdb) ni
77 0x080484ac in d_max ()
78 (gdb) info registers
79 eax 0x1 1
80 ecx 0xbffff1c4 -1073745468
81 edx 0x8048340 134513472
82 ebx 0xb7fbf000 -1208225792
83 esp 0xbffff10c 0xbffff10c
84 ebp 0xbffff128 0xbffff128
85 esi 0x0 0
86 edi 0x0 0
87 eip 0x80484ac 0x80484ac <d_max+12>
88 eflags 0x203 [ CF IF ]
89 cs 0x73 115
90 ss 0x7b 123
91 ds 0x7b 123
92 es 0x7b 123
93 fs 0x0 0
94 gs 0x33 51
95 (gdb) ni
96 0x080484ae in d_max ()
97 (gdb) info float
98 R7 : Valid 0x4000d999999999999800 +3.399999999999999911
99 =>R6 : Valid 0x4000d999999999999800 +3.399999999999999911
100 R5 : Empty 0x00000000000000000000
101 R4 : Empty 0x00000000000000000000
102 R3 : Empty 0x00000000000000000000
103 R2 : Empty 0x00000000000000000000
104 R1 : Empty 0x00000000000000000000
330
105 R0 : Empty 0x00000000000000000000
106
107 Status Word : 0x3000
108 TOP : 6
109 Control Word : 0x037f IM DM ZM OM UM PM
110 PC : Extended Precision (64-bits)
111 RC : Round to nearest
112 Tag Word : 0x0fff
113 Instruction Pointer : 0x73 :0x080484ac
114 Operand Pointer : 0x7b :0xbffff118
115 Opcode : 0x0000
116 (gdb) disas $eip
117 Dump of assembler code for function d_max :
118 0x080484a0 <+0> : fldl 0x4(%esp)
119 0x080484a4 <+4> : fldl 0xc(%esp)
120 0x080484a8 <+8> : fxch %st(1)
121 0x080484aa <+10> : fucomi %st(1),%st
122 0x080484ac <+12> : fcmovbe %st(1),%st
123 => 0x080484ae <+14> : fstp %st(1)
124 0x080484b0 <+16> : ret
125 End of assembler dump.
126 (gdb) ni
127 0x080484b0 in d_max ()
128 (gdb) info float
129 =>R7 : Valid 0x4000d999999999999800 +3.399999999999999911
130 R6 : Empty 0x4000d999999999999800
131 R5 : Empty 0x00000000000000000000
132 R4 : Empty 0x00000000000000000000
133 R3 : Empty 0x00000000000000000000
134 R2 : Empty 0x00000000000000000000
135 R1 : Empty 0x00000000000000000000
136 R0 : Empty 0x00000000000000000000
137
138 Status Word : 0x3800
139 TOP : 7
140 Control Word : 0x037f IM DM ZM OM UM PM
141 PC : Extended Precision (64-bits)
142 RC : Round to nearest
143 Tag Word : 0x3fff
144 Instruction Pointer : 0x73 :0x080484ae
145 Operand Pointer : 0x7b :0xbffff118
146 Opcode : 0x0000
147 (gdb) quit
148 A debugging session is active.
149
150 Inferior 1 [process 30194] will be killed.
151
152 Quit anyway ? (y or n) y
153 dennis@ubuntuvm :~/polygon$
331
Comme cela a déjà été mentionné, l’ensemble des registres FPU est un buffeur cir-
culaire plutôt qu’une pile ( 1.25.5 on page 292). Et GDB ne montre pas les registres
STx, mais les registre internes du FPU (Rx). La flèche (à la ligne 35) pointe sur le haut
courant de la pile.
Vous pouvez voir le contenu du registre TOP dans le Status Word (ligne 36-37)—c’est
6 maintenant, donc le haut de la pile pointe maintenant sur le registre interne 6.
Les valeurs de a et b sont échangées après l’exécution de FXCH (ligne 54).
FUCOMI est exécuté (ilgne 83). Regardons les flags: CF est mis (ligne 95).
FCMOVBE a copié la valeur de b (voir ligne 104).
FSTP dépose une valeur au sommet de la pile (ligne 139). La valeur de TOP est
maintenant 7, donc le sommet de la pile du FPU pointe sur le registre interne 7.
ARM
Un cas très simple. Les valeurs en entrée sont placées dans les registres D17 et D16
puis comparées en utilisant l’instruction VCMPE.
Tout comme dans le coprocesseur x86, le coprocesseur ARM a son propre registre
de flags (FPSCR117 ), puisqu’il est nécessaire de stocker des flags spécifique au co-
processeur. Et tout comme en x86, il n’y a pas d’instruction de saut conditionnel qui
teste des bits dans le registre de status du coprocesseur. Donc il y a VMRS, qui copie
4 bits (N, Z, C, V) du mot d’état du coprocesseur dans les bits du registre de status
général (APSR118 ).
VMOVGT est l’analogue de l’instruction MOVGT pour D-registres, elle s’exécute si un
opérande est plus grand que l’autre lors de la comparaison (GT—Greater Than).
Si elle est exécutée, la valeur de a sera écrite dans D16 (ce qui est écrit en ce moment
dans D17). Sinon, la valeur de b reste dans le registre D16.
La pénultième instruction VMOV prépare la valeur dans la registre D16 afin de la ren-
voyer dans la paire de registres R0 et R1.
117. (ARM) Floating-Point Status and Control Register
118. (ARM) Application Program Status Register
332
avec optimisation Xcode 4.6.3 (LLVM) (Mode Thumb-2)
ITE est l’acronyme de if-then-else et elle encode un suffixe pour les deux prochaines
instructions.
La première instruction est exécutée si la condition encodée dans ITE (NE, not equal)
est vraie, et la seconde—si la condition n’est pas vraie (l’inverse de la condition NE
est EQ (equal)).
L’instruction qui suit le second VMOV (ou VMOVEQ) est normale, non suffixée (BLX).
Un autre exemple qui est légèrement plus difficile, qui est aussi d’Angry Birds:
333
Listing 1.222: Angry Birds Classic
...
ITTTT EQ
MOVEQ R0, R4
ADDEQ SP, SP, #0x20
POPEQ.W {R8,R10}
POPEQ {R4-R7,PC}
BLX ___stack_chk_fail ; not suffixed
...
ITTE (if-then-then-else)
implique que les 1ère et 2ème instructions seront exécutées si la condition LE (Less
or Equal moins ou égal) est vraie, et que la 3ème—si la condition inverse (GT—
Greater Than plus grand que) est vraie.
En général, les compilateurs ne génèrent pas toutes les combinaisons possible.
Par exemple, dans le jeu Angry Birds mentionné ((classic version pour iOS) seules
les les variantes suivantes de l’instruction IT sont utilisées: IT, ITE, ITT, ITTE, ITTT,
ITTTT. Comment savoir cela? Dans IDA, il est possible de produire un listing dans
un fichier, ce qui a été utilisé pour en créer un avec l’option d’afficher 4 octets pour
chaque opcode. Ensuite, en connaissant la partie haute de l’opcode de 16-bit (0xBF
pour IT), nous utilisons grep ainsi:
cat AngryBirdsClassic.lst | grep " BF" | grep "IT" > results.lst
334
À propos, si vous programmez en langage d’assemblage ARM pour le mode Thumb-2,
et que vous ajoutez des suffixes conditionnels, l’assembleur ajoutera automatique-
ment l’instruction IT avec les flags là où ils sont nécessaires.
loc_2E08
VLDR D16, [SP,#0x20+b]
VSTR D16, [SP,#0x20+val_to_return]
loc_2E10
VLDR D16, [SP,#0x20+val_to_return]
VMOV R0, R1, D16
MOV SP, R7
LDR R7, [SP+0x20+b],#4
BX LR
Presque la même chose que nous avons déjà vu, mais ici il y a beaucoup de code
redondant car les variables a et b sont stockées sur la pile locale, tout comme la
valeur de retour.
335
MOVS R5, R3
MOVS R6, R0
MOVS R7, R1
BL __aeabi_cdrcmple
BCS loc_1C0
MOVS R0, R6
MOVS R1, R7
POP {R3-R7,PC}
loc_1C0
MOVS R0, R4
MOVS R1, R5
POP {R3-R7,PC}
Keil ne génère pas les instructions pour le FPU car il ne peut pas être sûr qu’elles sont
supportées sur le CPU cible, et cela ne peut pas être fait directement en comparant
les bits. Donc il appelle une fonction d’une bibliothèque externe pour effectuer la
comparaison: __aeabi_cdrcmple.
N.B. Le résultat de la comparaison est laissé dans les flags par cette fonction, donc
l’instruction BCS (Carry set—Greater than or equal plus grand ou égal) fonctionne
sans code additionnel.
ARM64
d_max :
; D0 - a, D1 - b
fcmpe d0, d1
fcsel d0, d0, d1, gt
; maintenant le résultat est dans D0
ret
L’ARM64 ISA possède des instructions FPU qui mettent les flags CPU APSR au lieu de
FPSCR, par commodité. Le FPU n’est plus un device séparé (au moins, logiquement).
Ici, nous voyons FCMPE. Ceci compare les deux valeurs passées dans D0 et D1 (qui
sont le premier et le second argument de la fonction) et met les flags APSR (N, Z, C,
V).
FCSEL (Floating Conditional Select (sélection de flottant conditionnelle) copie la va-
leur de D0 ou D1 dans D0 suivant le résultat de la comparaison (GT—Greater Than),
et de nouveau, il utilise les flags dans le registre APSR au lieu de FPSCR.
Ceci est bien plus pratique, comparé au jeu d’instructions des anciens CPUs.
Si la condition est vraie (GT), alors la valeur de D0 est copiée dans D0 (i.e., il ne se
passe rien). Si la condition n’est pas vraie, la valeur de D1 est copiée dans D0.
336
d_max :
; sauver les arguments en entrée dans la "Register Save Area"
; "zone de sauvegarde des registres"
sub sp, sp, #16
str d0, [sp,8]
str d1, [sp]
; recharger les valeurs
ldr x1, [sp,8]
ldr x0, [sp]
fmov d0, x1
fmov d1, x0
; D0 - a, D1 - b
fcmpe d0, d1
ble .L76
; a>b; charger D0 (a) dans X0
ldr x0, [sp,8]
b .L74
.L76 :
; a<=b; charger D1 (b) dans X0
ldr x0, [sp]
.L74 :
; résultat dans X0
fmov d0, x0
; résultat dans D0
add sp, sp, 16
ret
Exercice
337
Ré-écrivons cet exemple en utilisant des float à la place de double.
float f_max (float a, float b)
{
if (a>b)
return a ;
return b ;
};
f_max :
; S0 - a, S1 - b
fcmpe s0, s1
fcsel s0, s0, s1, gt
; maintenant le résultat est dans S0
ret
C’est le même code, mais des S-registres sont utilisés à la place de D-registres. C’est
parce que les nombres de type float sont passés dans des S-registres de 32-bit (qui
sont en fait la partie basse des D-registres 64-bit).
MIPS
Le coprocesseur du processeur MIPS possède un bit de condition qui peut être mis
par le FPU et lu par le CPU.
Les premiers MIPSs avaient seulement un bit de condition (appelé FCC0), les derniers
en ont 8 (appelés FCC7-FCC0).
Ce bit (ou ces bits) sont situés dans un registre appelé FCCR.
Listing 1.226: avec optimisation GCC 4.4.5 (IDA)
d_max :
; mettre le bit de condition du FPU si $f14<$f12 (b<a) :
c.lt.d $f14, $f12
or $at, $zero ; NOP
; sauter en locret_14 si le bit de condition est mis
bc1t locret_14
; cette instruction est toujours exécutée (mettre la valeur de retour à
"a") :
mov.d $f0, $f12 ; slot de délai de branchement
; cette instruction est exécutée seulement si la branche n'a pas été prise
; (i.e., si b>=a)
; mettre la valeur de retour à "b":
mov.d $f0, $f14
locret_14 :
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
C.LT.D compare deux valeurs. LT est la condition «Less Than » (plus petit que). D
implique des valeurs de type double. Suivant le résultat de la comparaison, le bit de
condition FCC0 est mis à 1 ou à 0.
338
BC1T teste le bit FCC0 et saute si le bit est mis à 1. T signifie que le saut sera effectué
si le bit est mis à 1 («True »). Il y a aussi une instruction BC1F qui saute si le bit n’est
pas mis (donc est à 0) («False »).
Dépendant du saut, un des arguments de la fonction est placé dans $F0.
1.25.9 Copie
On peut tout d’abord penser qu’il faut utiliser les instructions FLD/FST pour charger
et stocker (et donc, copier) des valeurs IEEE 754. Néanmoins, la même chose peut-
être effectuée plus facilement avec l’instruction usuelle MOV, qui, bien sûr, copie les
valeurs au niveau binaire.
1.25.11 80 bits?
Représentation interne des nombres dans le FPU — 80-bit. Nombre étrange, car il
n’est pas de la forme 2n . Il y a une hypothèse que c’est probablement dû à des
raisons historiques—le standard IBM de carte perforée peut encoder 12 lignes de 80
bits. La résolution en mode texte de 80 ⋅ 25 était aussi très populaire dans le passé.
Il y a une autre explication sur Wikipédia: https://en.wikipedia.org/wiki/Extended_
precision.
Si vous en savez plus, s’il vous plaît, envoyez-moi un email: <first_name @ last_name
. com> / <first_name . last_name @ gmail . com>.
339
1.25.12 x64
Sur la manière dont sont traités les nombres à virgules flottante en x86-64, lire
ici: 1.38 on page 553.
1.25.13 Exercices
• http://challenges.re/60
• http://challenges.re/61
1.26 Tableaux
Un tableau est simplement un ensemble de variables en mémoire qui sont situées
les unes à côté des autres et qui ont le même type119 .
int main()
{
int a[20];
int i ;
return 0;
};
x86
MSVC
Compilons:
340
sub esp, 84 ; 00000054H
mov DWORD PTR _i$[ebp], 0
jmp SHORT $LN6@main
$LN5@main :
mov eax, DWORD PTR _i$[ebp]
add eax, 1
mov DWORD PTR _i$[ebp], eax
$LN6@main :
cmp DWORD PTR _i$[ebp], 20 ; 00000014H
jge SHORT $LN4@main
mov ecx, DWORD PTR _i$[ebp]
shl ecx, 1
mov edx, DWORD PTR _i$[ebp]
mov DWORD PTR _a$[ebp+edx*4], ecx
jmp SHORT $LN5@main
$LN4@main :
mov DWORD PTR _i$[ebp], 0
jmp SHORT $LN3@main
$LN2@main :
mov eax, DWORD PTR _i$[ebp]
add eax, 1
mov DWORD PTR _i$[ebp], eax
$LN3@main :
cmp DWORD PTR _i$[ebp], 20 ; 00000014H
jge SHORT $LN1@main
mov ecx, DWORD PTR _i$[ebp]
mov edx, DWORD PTR _a$[ebp+ecx*4]
push edx
mov eax, DWORD PTR _i$[ebp]
push eax
push OFFSET $SG2463
call _printf
add esp, 12 ; 0000000cH
jmp SHORT $LN2@main
$LN1@main :
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
Rien de très particulier, juste deux boucles: la première est celle de remplissage et la
seconde celle d’affichage. L’instruction shl ecx, 1 est utilisée pour la multiplication
par 2 de la valeur dans ECX, voir à ce sujet ci-après 1.24.2 on page 283.
80 octets sont alloués sur la pile pour le tableau, 20 éléments de 4 octets.
341
Essayons cet exemple dans OllyDbg.
Nous voyons comment le tableau est rempli:
chaque élément est un mot de 32-bit de type int et sa valeur est l’index multiplié
par 2:
Puisque le tableau est situé sur la pile, nous pouvons voir ses 20 éléments ici.
GCC
342
var_70 = dword ptr -70h
var_6C = dword ptr -6Ch
var_68 = dword ptr -68h
i_2 = dword ptr -54h
i = dword ptr -4
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 70h
mov [esp+70h+i], 0 ; i=0
jmp short loc_804840A
loc_80483F7 :
mov eax, [esp+70h+i]
mov edx, [esp+70h+i]
add edx, edx ; edx=i*2
mov [esp+eax*4+70h+i_2], edx
add [esp+70h+i], 1 ; i++
loc_804840A :
cmp [esp+70h+i], 13h
jle short loc_80483F7
mov [esp+70h+i], 0
jmp short loc_8048441
loc_804841B :
mov eax, [esp+70h+i]
mov edx, [esp+eax*4+70h+i_2]
mov eax, offset aADD ; "a[%d]=%d\n"
mov [esp+70h+var_68], edx
mov edx, [esp+70h+i]
mov [esp+70h+var_6C], edx
mov [esp+70h+var_70], eax
call _printf
add [esp+70h+i], 1
loc_8048441 :
cmp [esp+70h+i], 13h
jle short loc_804841B
mov eax, 0
leave
retn
main endp
À propos, la variable a est de type int* (un pointeur sur un int)—vous pouvez passer
un pointeur sur un tableau à une autre fonction, mais c’est plus juste de dire qu’un
pointeur sur le premier élément du tableau est passé (les adresses du reste des
éléments sont calculées de manière évidente).
Si vous indexez ce pointeur en a[idx], il suffit d’ajouter idx au pointeur et l’élément
placé ici (sur lequel pointe le pointeur calculé) est renvoyé.
343
Un exemple intéressant: une chaîne de caractères comme string est un tableau de
caractères et a un type const char[].
Un index peut aussi être appliqué à ce pointeur.
Et c’est pourquoi il est possible d’écrire des choses comme «string »[i]—c’est
une expression C/C++ correcte!
ARM
EXPORT _main
_main
STMFD SP !, {R4,LR}
SUB SP, SP, #0x50 ; allouer de la place pour 20 variables
int
; première boucle
MOV R4, #0 ; i
B loc_4A0
loc_494
MOV R0, R4,LSL#1 ; R0=R4*2
STR R0, [SP,R4,LSL#2] ; stocker R0 dans SP+R4<<2 (pareil que
SP+R4*4)
ADD R4, R4, #1 ; i=i+1
loc_4A0
CMP R4, #20 ; i<20?
BLT loc_494 ; oui, effectuer encore le corps de la
boucle
; seconde boucle
MOV R4, #0 ; i
B loc_4C4
loc_4B0
LDR R2, [SP,R4,LSL#2] ; (second argument de printf)
R2=*(SP+R4<<4)
; (pareil que *(SP+R4*4))
MOV R1, R4 ; (premier argument de printf) R1=i
ADR R0, aADD ; "a[%d]=%d\n"
BL __2printf
ADD R4, R4, #1 ; i=i+1
loc_4C4
CMP R4, #20 ; i<20?
BLT loc_4B0 ; oui, effectuer encore le corps de la
boucle
MOV R0, #0 ; valeur à renvoyer
ADD SP, SP, #0x50 ; libérer le chunk, alloué pour 20
variables int
LDMFD SP !, {R4,PC}
344
Le type int nécessite 32 bits pour le stockage (ou 4 octets).
donc pour stocker 20 variables, int 80 (0x50) octets sont nécessaires.
C’est pourquoi l’instruction SUB SP, SP, #0x50 dans le prologue de la fonction al-
loue exactement cet espace sur la pile.
Dans la première et la seconde boucle, la variable de boucle i se trouve dans le
registre R4.
Le nombre qui doit être écrit dans le tableau est calculé comme i ∗ 2, ce qui est
effectivement équivalent à décaler d’un bit vers la gauche, ce que fait l’instruction
MOV R0, R4,LSL#1.
STR R0, [SP,R4,LSL#2] écrit le contenu de R0 dans le tableau.
Voici comment le pointeur sur un élément du tableau est calculé: SP pointe sur le
début du tableau, R4 est i.
Donc décaler i de 2 bits vers la gauche est effectivement équivalent à la multiplica-
tion par 4 (puisque chaque élément du tableau a une taille de 4 octets) et ensuite
on l’ajoute à l’adresse du début du tableau.
La seconde boucle a l’instruction inverse LDR R2, [SP,R4,LSL#2]. Elle charge la
valeur du tableau dont nous avons besoin, et le pointeur est calculé de même.
_main
PUSH {R4,R5,LR}
; allouer de l'espace pour 20 variables int + une variable supplémentaire
SUB SP, SP, #0x54
; première boucle
MOVS R0, #0 ; i
MOV R5, SP ; pointeur sur le premier élément du tableau
loc_1CE
LSLS R1, R0, #1 ; R1=i<<1 (pareil que i*2)
LSLS R2, R0, #2 ; R2=i<<2 (pareil que i*4)
ADDS R0, R0, #1 ; i=i+1
CMP R0, #20 ; i<20?
STR R1, [R5,R2] ; stocker R1 dans *(R5+R2) (pareil que R5+i*4)
BLT loc_1CE ; oui, i<20, effectuer encore le corps de la
boucle
; seconde boucle
345
BL __2printf
ADDS R4, R4, #1 ; i=i+1
CMP R4, #20 ; i<20?
BLT loc_1DC ; oui, i<20, effectuer encore le corps de la
boucle
MOVS R0, #0 ; valeur à renvoyer
; libérer le chunk, alloué pour 20 variables int
; + une variable supplémentaire
ADD SP, SP, #0x54
POP {R4,R5,PC}
346
.L2 :
; tester si la boucle est finie:
ldr w0, [x29,108]
cmp w0, 19
; sauter en L3 (début du corps de la boucle) si non:
ble .L3
; La seconde partie de la fonction commence ici.
; mettre la valeur initiale da la variable compteur à 0.
; à propos, le même espace est utilisé dans la pile locale,
; car la même variable locale (i) est utilisée comme compteur.
str wzr, [x29,108]
b .L4
.L5 :
; calculer l'adresse dans le tableau:
add x0, x29, 24
; charger la valeur de "i":
ldrsw x1, [x29,108]
; charger la valeur du tableau à l'adresse (X0+X1<<2 = adresse du tableau +
i*4)
ldr w2, [x0,x1,lsl 2]
; charger l'adresse de la chaîne "a[%d]=%d\n" :
adrp x0, .LC0
add x0, x0, :lo12 :.LC0
; charger la variable "i" dans W1 et la passer à printf() comme second
argument:
ldr w1, [x29,108]
; W2 contient toujours la valeur de l'élément du tableau qui vient d'être
chargée.
; appeler printf() :
bl printf
; incrémenter la variable "i":
ldr w0, [x29,108]
add w0, w0, 1
str w0, [x29,108]
.L4 :
; est-ce fini?
ldr w0, [x29,108]
cmp w0, 19
; sauter au début du corps de la boucle si non:
ble .L5
; renvoyer 0
mov w0, 0
; restaurer FP et LR:
ldp x29, x30, [sp], 112
ret
MIPS
La fonction utilise beaucoup de S- registres qui doivent être préservés, c’est pourquoi
leurs valeurs sont sauvegardées dans la prologue de la fonction et restaurées dans
l’épilogue.
Listing 1.230: GCC 4.4.5 avec optimisation (IDA)
347
main :
var_70 = -0x70
var_68 = -0x68
var_14 = -0x14
var_10 = -0x10
var_C = -0xC
var_8 = -8
var_4 = -4
; prologue de la fonction:
lui $gp, (__gnu_local_gp >> 16)
addiu $sp, -0x80
la $gp, (__gnu_local_gp & 0xFFFF)
sw $ra, 0x80+var_4($sp)
sw $s3, 0x80+var_8($sp)
sw $s2, 0x80+var_C($sp)
sw $s1, 0x80+var_10($sp)
sw $s0, 0x80+var_14($sp)
sw $gp, 0x80+var_70($sp)
addiu $s1, $sp, 0x80+var_68
move $v1, $s1
move $v0, $zero
; cette valeur va être utilisée comme fin de boucle.
; elle a été pré-calculée par le compilateur GCC à l'étape de la
compilation:
li $a0, 0x28 # '('
348
bne $s0, $s2, loc_54
; déplacer le pointeur mémoire au prochain mot de 32-bit:
addiu $s1, 4
; épilogue de la fonction
lw $ra, 0x80+var_4($sp)
move $v0, $zero
lw $s3, 0x80+var_8($sp)
lw $s2, 0x80+var_C($sp)
lw $s1, 0x80+var_10($sp)
lw $s0, 0x80+var_14($sp)
jr $ra
addiu $sp, 0x80
Donc, indexer un tableau est juste array[index]. Si vous étudiez le code généré avec
soin, vous remarquerez sans doute l’absence de test sur les bornes de l’index, qui
devrait vérifier si il est inférieur à 20. Que ce passe-t-il si l’index est supérieur à 20?
C’est une des caractéristiques de C/C++ qui est souvent critiquée.
Voici un code qui compile et fonctionne:
#include <stdio.h>
int main()
{
int a[20];
int i ;
349
return 0;
};
C’est juste quelque chose qui se trouvait sur la pile à côté du tableau, 80 octets
après le début de son premier élément.
350
Essayons de trouver d’où vient cette valeur, en utilisant OllyDbg.
Chargeons et trouvons la valeur située juste après le dernier élément du tableau:
351
Exécutons encore et voyons comment il est restauré:
Ok, nous avons lu quelques valeurs de la pile illégalement, mais que se passe-t-il si
nous essayons d’écrire quelque chose?
Voici ce que nous avons:
#include <stdio.h>
int main()
352
{
int a[20];
int i ;
return 0;
};
MSVC
353
Chargeons le dans OllyDbg, et traçons le jusqu’à ce que les 30 éléments du tableau
soient écrits:
354
Exécutons pas à pas jusqu’à la fin de la fonction:
Fig. 1.92: OllyDbg : EIP a été restauré, mais OllyDbg ne peut pas désassembler en
0x15
355
ESP 4 octets alloués pour la variable i
ESP+4 80 octets alloués pour le tableau a[20]
ESP+84 valeur sauvegardée de EBP
ESP+88 adresse de retour
L’expression a[19]=quelquechose écrit le dernier int dans des bornes du tableau
(dans les limites jusqu’ici!)
L’expression a[20]=quelquechose écrit quelquechose à l’endroit où la valeur sau-
vegardée de EBP se trouve.
S’il vous plaît, regardez l’état du registre lors du plantage. Dans notre cas, 20 a été
écrit dans le 20ème élément. À la fin de la fonction, l’épilogue restaure la valeur
d’origine de EBP. (20 en décimal est 0x14 en hexadécimal). Ensuite RET est exécuté,
qui est équivalent à l’instruction POP EIP.
L’instruction RET prend la valeur de retour sur la pile (c’est l’adresse dans CRT), qui a
appelé main()), et 21 est stocké ici (0x15 en hexadécimal). Le CPU trape à l’adresse
0x15, mais il n’y a pas de code exécutable ici, donc une exception est levée.
Bienvenu! Ça s’appelle un buffer overflow (débordement de tampon)121 .
Remplacez la tableau de int avec une chaîne (char array), créez délibérément une
longue chaîne et passez-là au programme, à la fonction, qui ne teste pas la longueur
de la chaîne et la copie dans un petit buffer et vous serez capable de faire pointer le
programme à une adresse où il devra sauter. C’est pas aussi simple dans la réalité,
mais c’est comme cela que ça a apparu. L’article classique à propos de ça: [Aleph
One, Smashing The Stack For Fun And Profit, (1996)]122 .
GCC
push ebp
mov ebp, esp
sub esp, 60h ; 96
mov [ebp+i], 0
jmp short loc_80483D1
loc_80483C3 :
mov eax, [ebp+i]
mov edx, [ebp+i]
mov [ebp+eax*4+a], edx
add [ebp+i], 1
loc_80483D1 :
cmp [ebp+i], 1Dh
121. Wikipédia
122. Aussi disponible en http://yurichev.com/mirrors/phrack/p49-0x0e.txt
356
jle short loc_80483C3
mov eax, 0
leave
retn
main endp
Les valeurs des registres sont légèrement différentes de l’exemple win32, puisque
la structure de la pile est également légèrement différente.
Une des méthodes est d’écrire une valeur aléatoire entre les variables locales sur la
pile dans le prologue de la fonction et de la vérifier dans l’épilogue, avant de sortir
de la fonction. Si la valeur n’est pas la même, ne pas exécuter la dernière instruction
123. méthode de protection contre les débordements de tampons côté compila-
teur:wikipedia.org/wiki/Buffer_overflow_protection
357
RET, mais stopper (ou bloquer). Le processus va s’arrêter, mais c’est mieux qu’une
attaque distante sur votre ordinateur.
Cette valeur aléatoire est parfois appelé un «canari », c’est lié au canari124 que les
mineurs utilisaient dans le passé afin de détecter rapidement les gaz toxiques.
Les canaris sont très sensibles aux gaz, ils deviennent très agités en cas de danger,
et même meurent.
Si nous compilons notre exemple de tableau très simple ( 1.26.1 on page 340) dans
MSVC avec les options RTC1 et RTCs, nous voyons un appel à @_RTC_CheckStackVars@8
une fonction à la fin de la fonction qui vérifie si le «canari » est correct.
Voyons comment GCC gère ceci. Prenons un exemple alloca() ( 1.9.2 on page 49) :
#ifdef __GNUC__
#include <alloca.h> // GCC
#else
#include <malloc.h> // MSVC
#endif
#include <stdio.h>
void f()
{
char *buf=(char*)alloca (600) ;
#ifdef __GNUC__
snprintf (buf, 600, "hi ! %d, %d, %d\n", 1, 2, 3) ; // GCC
#else
_snprintf (buf, 600, "hi ! %d, %d, %d\n", 1, 2, 3) ; // MSVC
#endif
puts (buf) ;
};
Par défaut, sans option supplémentaire, GCC 4.7.3 insère un test de «canari » dans
le code:
Listing 1.234: GCC 4.7.3
.LC0 :
.string "hi ! %d, %d, %d\n"
f :
push ebp
mov ebp, esp
push ebx
sub esp, 676
lea ebx, [esp+39]
and ebx, -16
mov DWORD PTR [esp+20], 3
mov DWORD PTR [esp+16], 2
mov DWORD PTR [esp+12], 1
mov DWORD PTR [esp+8], OFFSET FLAT :.LC0 ; "hi! %d, %d, %d\n"
mov DWORD PTR [esp+4], 600
mov DWORD PTR [esp], ebx
124. wikipedia.org/wiki/Domestic_canary#Miner.27s_canary
358
mov eax, DWORD PTR gs :20 ; canari
mov DWORD PTR [ebp-12], eax
xor eax, eax
call _snprintf
mov DWORD PTR [esp], ebx
call puts
mov eax, DWORD PTR [ebp-12]
xor eax, DWORD PTR gs :20 ; teste le canari
jne .L5
mov ebx, DWORD PTR [ebp-4]
leave
ret
.L5 :
call __stack_chk_fail
La valeur aléatoire se trouve en gs:20. Elle est écrite sur la pile et à la fin de la
fonction, la valeur sur la pile est comparée avec le «canari » correct dans gs:20. Si
les valeurs ne sont pas égales, la fonction __stack_chk_fail est appelée et nous
voyons dans la console quelque chose comme ça (Ubuntu 13.04 x86) :
*** buffer overflow detected *** : ./2_1 terminated
======= Backtrace : =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x63)[0xb7699bc3]
/lib/i386-linux-gnu/libc.so.6(+0x10593a)[0xb769893a]
/lib/i386-linux-gnu/libc.so.6(+0x105008)[0xb7698008]
/lib/i386-linux-gnu/libc.so.6(_IO_default_xsputn+0x8c)[0xb7606e5c]
/lib/i386-linux-gnu/libc.so.6(_IO_vfprintf+0x165)[0xb75d7a45]
/lib/i386-linux-gnu/libc.so.6(__vsprintf_chk+0xc9)[0xb76980d9]
/lib/i386-linux-gnu/libc.so.6(__sprintf_chk+0x2f)[0xb7697fef]
./2_1[0x8048404]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0xb75ac935]
======= Memory map : ========
08048000-08049000 r-xp 00000000 08:01 2097586 /home/dennis/2_1
08049000-0804a000 r--p 00000000 08:01 2097586 /home/dennis/2_1
0804a000-0804b000 rw-p 00001000 08:01 2097586 /home/dennis/2_1
094d1000-094f2000 rw-p 00000000 00:00 0 [heap]
b7560000-b757b000 r-xp 00000000 08:01 1048602 /lib/i386-linux-gnu/⤦
Ç libgcc_s.so.1
b757b000-b757c000 r--p 0001a000 08:01 1048602 /lib/i386-linux-gnu/⤦
Ç libgcc_s.so.1
b757c000-b757d000 rw-p 0001b000 08:01 1048602 /lib/i386-linux-gnu/⤦
Ç libgcc_s.so.1
b7592000-b7593000 rw-p 00000000 00:00 0
b7593000-b7740000 r-xp 00000000 08:01 1050781 /lib/i386-linux-gnu/libc⤦
Ç -2.17.so
b7740000-b7742000 r--p 001ad000 08:01 1050781 /lib/i386-linux-gnu/libc⤦
Ç -2.17.so
b7742000-b7743000 rw-p 001af000 08:01 1050781 /lib/i386-linux-gnu/libc⤦
Ç -2.17.so
b7743000-b7746000 rw-p 00000000 00:00 0
b775a000-b775d000 rw-p 00000000 00:00 0
b775d000-b775e000 r-xp 00000000 00:00 0 [vdso]
b775e000-b777e000 r-xp 00000000 08:01 1050794 /lib/i386-linux-gnu/ld⤦
359
Ç -2.17.so
b777e000-b777f000 r--p 0001f000 08:01 1050794 /lib/i386-linux-gnu/ld⤦
Ç -2.17.so
b777f000-b7780000 rw-p 00020000 08:01 1050794 /lib/i386-linux-gnu/ld⤦
Ç -2.17.so
bff35000-bff56000 rw-p 00000000 00:00 0 [stack]
Aborted (core dumped)
gs est ainsi appelé registre de segment. Ces registres étaient beaucoup utilisés du
temps de MS-DOS et des extensions de DOS. Aujourd’hui, sa fonction est différente.
Dit brièvement, le registre gs dans Linux pointe toujours sur le TLS125 ( 6.2 on
page 974)—des informations spécifiques au thread sont stockées là. À propos, en
win32 le registre fs joue le même rôle, pointant sur TIB126 127 .
Il y a plus d’information dans le code source du noyau Linux (au moins dans la ver-
sion 3.11), dans
arch/x86/include/asm/stackprotector.h cette variable est décrite dans les commen-
taires.
var_64 = -0x64
var_60 = -0x60
var_5C = -0x5C
var_58 = -0x58
var_54 = -0x54
var_50 = -0x50
var_4C = -0x4C
var_48 = -0x48
var_44 = -0x44
var_40 = -0x40
var_3C = -0x3C
var_38 = -0x38
var_34 = -0x34
var_30 = -0x30
var_2C = -0x2C
var_28 = -0x28
var_24 = -0x24
var_20 = -0x20
var_1C = -0x1C
var_18 = -0x18
canary = -0x14
360
var_10 = -0x10
PUSH {R4-R7,LR}
ADD R7, SP, #0xC
STR.W R8, [SP,#0xC+var_10]!
SUB SP, SP, #0x54
MOVW R0, #aObjc_methtype ; "objc_methtype"
MOVS R2, #0
MOVT.W R0, #0
MOVS R5, #0
ADD R0, PC
LDR.W R8, [R0]
LDR.W R0, [R8]
STR R0, [SP,#0x64+canari]
MOVS R0, #2
STR R2, [SP,#0x64+var_64]
STR R0, [SP,#0x64+var_60]
MOVS R0, #4
STR R0, [SP,#0x64+var_5C]
MOVS R0, #6
STR R0, [SP,#0x64+var_58]
MOVS R0, #8
STR R0, [SP,#0x64+var_54]
MOVS R0, #0xA
STR R0, [SP,#0x64+var_50]
MOVS R0, #0xC
STR R0, [SP,#0x64+var_4C]
MOVS R0, #0xE
STR R0, [SP,#0x64+var_48]
MOVS R0, #0x10
STR R0, [SP,#0x64+var_44]
MOVS R0, #0x12
STR R0, [SP,#0x64+var_40]
MOVS R0, #0x14
STR R0, [SP,#0x64+var_3C]
MOVS R0, #0x16
STR R0, [SP,#0x64+var_38]
MOVS R0, #0x18
STR R0, [SP,#0x64+var_34]
MOVS R0, #0x1A
STR R0, [SP,#0x64+var_30]
MOVS R0, #0x1C
STR R0, [SP,#0x64+var_2C]
MOVS R0, #0x1E
STR R0, [SP,#0x64+var_28]
MOVS R0, #0x20
STR R0, [SP,#0x64+var_24]
MOVS R0, #0x22
STR R0, [SP,#0x64+var_20]
MOVS R0, #0x24
STR R0, [SP,#0x64+var_1C]
MOVS R0, #0x26
STR R0, [SP,#0x64+var_18]
361
MOV R4, 0xFDA ; "a[%d]=%d\n"
MOV R0, SP
ADDS R6, R0, #4
ADD R4, PC
B loc_2F1C
loc_2F14
ADDS R0, R5, #1
LDR.W R2, [R6,R5,LSL#2]
MOV R5, R0
loc_2F1C
MOV R0, R4
MOV R1, R5
BLX _printf
CMP R5, #0x13
BNE loc_2F14
LDR.W R0, [R8]
LDR R1, [SP,#0x64+canari]
CMP R0, R1
ITTTT EQ ; est-ce que le canari est toujours correct?
MOVEQ R0, #0
ADDEQ SP, SP, #0x54
LDREQ.W R8, [SP+0x64+var_64],#4
POPEQ {R4-R7,PC}
BLX ___stack_chk_fail
Tout d’abord, on voit que LLVM a «déroulé » la boucle et que toutes les valeurs sont
écrites une par une, pré-calculée, car LLVM a conclu que c’est plus rapide. À propos,
des instructions en mode ARM peuvent aider à rendre cela encore plus rapide, et les
trouver peut être un exercice pour vous.
À la fin de la fonction, nous voyons la comparaison des «canaris »—celui sur la pile
locale et le correct.
S’ils sont égaux, un bloc de 4 instructions est exécuté par ITTTT EQ, qui contient
l’écriture de 0 dans R0, l’épilogue de la fonction et la sortie. Si les « canaris » ne
sont pas égaux, le bloc est passé, et la fonction saute en ___stack_chk_fail, qui,
peut-être, stoppe l’exécution.
362
C’est simplement parce que le compilateur doit connaître la taille exacte du tableau
pour lui allouer de l’espace sur la pile locale lors de l’étape de compilation.
Si vous avez besoin d’un tableau de taille arbitraire, il faut l’allouer en utilisant
malloc(), puis en accédant aux blocs de mémoire allouée comme un tableau de
variables du type dont vous avez besoin.
Ou utiliser la caractéristique du standart C99 [ISO/IEC 9899:TC3 (C C99 standard),
(2007)6.7.5/2], et qui fonctionne comme alloca() ( 1.9.2 on page 49) en interne.
Il est aussi possible d’utiliser des bibliothèques de ramasse-miettes pour C.
Et il y a aussi des bibliothèques supportant les pointeurs intelligents pour C++.
x64
128. NDT: attention à l’encodage des fichiers, en ASCII ou en ISO-8859, un caractère occupe un octet,
alors qu’en UTF-8, notamment, il peut en occuper plusieurs. Par exemple, ’û’ est codé $fb (1 octet) en
ISO-8859 et $c3$bb (2 octets) en UTF-8. J’ai donc volontairement mis des caractères non accentués dans
le code.
363
DQ FLAT :$SG3131
DQ FLAT :$SG3132
DQ FLAT :$SG3133
$SG3122 DB 'January', 00H
$SG3123 DB 'February', 00H
$SG3124 DB 'March', 00H
$SG3125 DB 'April', 00H
$SG3126 DB 'May', 00H
$SG3127 DB 'June', 00H
$SG3128 DB 'July', 00H
$SG3129 DB 'August', 00H
$SG3130 DB 'September', 00H
$SG3156 DB '%s', 0aH, 00H
$SG3131 DB 'October', 00H
$SG3132 DB 'November', 00H
$SG3133 DB 'December', 00H
_DATA ENDS
month$ = 8
get_month1 PROC
movsxd rax, ecx
lea rcx, OFFSET FLAT :month1
mov rax, QWORD PTR [rcx+rax*8]
ret 0
get_month1 ENDP
364
Listing 1.237: GCC 4.9 avec optimisation x64
movsx rdi, edi
mov rax, QWORD PTR month1[0+rdi*8]
ret
MSVC 32-bit
La valeur en entrée n’a pas besoin d’être étendue sur 64-bit, donc elle est utilisée
telle quelle.
Et elle est multipliée par 4, car les éléments de la table sont larges de 32-bit (ou 4
octets).
ARM 32-bit
|L0.100|
DCD ||.data||
DCB "January",0
DCB "February",0
DCB "March",0
DCB "April",0
DCB "May",0
DCB "June",0
DCB "July",0
DCB "August",0
DCB "September",0
DCB "October",0
365
DCB "November",0
DCB "December",0
Le code est essentiellement le même, mais moins dense, car le suffixe LSL ne peut
pas être spécifié dans l’instruction LDR ici:
get_month1 PROC
LSLS r0,r0,#2
LDR r1,|L0.64|
LDR r0,[r1,r0]
BX lr
ENDP
ARM64
.LANCHOR0 = . + 0
.type month1, %object
366
.size month1, 96
month1 :
.xword .LC2
.xword .LC3
.xword .LC4
.xword .LC5
.xword .LC6
.xword .LC7
.xword .LC8
.xword .LC9
.xword .LC10
.xword .LC11
.xword .LC12
.xword .LC13
.LC2 :
.string "January"
.LC3 :
.string "February"
.LC4 :
.string "March"
.LC5 :
.string "April"
.LC6 :
.string "May"
.LC7 :
.string "June"
.LC8 :
.string "July"
.LC9 :
.string "August"
.LC10 :
.string "September"
.LC11 :
.string "October"
.LC12 :
.string "November"
.LC13 :
.string "December"
MIPS
367
la $v0, month1
; prendre la valeur en entrée et la multiplier par 4:
sll $a0, 2
; ajouter l'adresse de la table et la valeur multipliée:
addu $a0, $v0
; charger l'élément de la table à cette adresse dans $v0:
lw $v0, 0($a0)
; sortir
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
.data # .data.rel.local
.globl month1
month1 : .word aJanuary # "janvier"
.word aFebruary # "fevrier"
.word aMarch # "mars"
.word aApril # "avril"
.word aMay # "mai"
.word aJune # "juin"
.word aJuly # "juillet"
.word aAugust # "aout"
.word aSeptember # "septembre"
.word aOctober # "octobre"
.word aNovember # "novembre"
.word aDecember # "decembre"
.data # .rodata.str1.4
aJanuary : .ascii "janvier"<0>
aFebruary : .ascii "fevrier"<0>
aMarch : .ascii "mars"<0>
aApril : .ascii "avril"<0>
aMay : .ascii "mai"<0>
aJune : .ascii "juin"<0>
aJuly : .ascii "juillet"<0>
aAugust : .ascii "aout"<0>
aSeptember : .ascii "septembre"<0>
aOctober : .ascii "octobre"<0>
aNovember : .ascii "novembre"<0>
aDecember : .ascii "decembre"<0>
Débordement de tableau
Notre fonction accepte des valeurs dans l’intervalle 0..11, mais que se passe-t-il si
12 est passé? Il n’y a pas d’élément dans la table à cet endroit.
Donc la fonction va charger la valeur qui se trouve là, et la renvoyer.
Peu après, une autre fonction pourrait essayer de lire une chaîne de texte depuis
cette adresse et pourrait planter.
Compilons l’exemple dans MSVC pour win64 et ouvrons le dans IDA pour voir ce que
l’éditeur de lien à stocker après la table:
368
Listing 1.242: Fichier exécutable dans IDA
off_140011000 dq offset aJanuary_1 ; DATA XREF: .text:0000000140001003
; "January"
dq offset aFebruary_1 ; "February"
dq offset aMarch_1 ; "March"
dq offset aApril_1 ; "April"
dq offset aMay_1 ; "May"
dq offset aJune_1 ; "June"
dq offset aJuly_1 ; "July"
dq offset aAugust_1 ; "August"
dq offset aSeptember_1 ; "September"
dq offset aOctober_1 ; "October"
dq offset aNovember_1 ; "November"
dq offset aDecember_1 ; "December"
aJanuary_1 db 'January',0 ; DATA XREF: sub_140001020+4
; .data:off_140011000
aFebruary_1 db 'February',0 ; DATA XREF: .data:0000000140011008
align 4
aMarch_1 db 'March',0 ; DATA XREF: .data:0000000140011010
align 4
aApril_1 db 'April',0 ; DATA XREF: .data:0000000140011018
369
Et c’est 0x797261756E614A.
Peu après, une autre fonction (supposons, une qui traite des chaînes) pourrait es-
sayer de lire des octets à cette adresse, y attendant une chaîne-C.
Plus probablement, ça planterait, car cette valeur ne ressemble pas à une adresse
valide.
Loi de Murphy
Il est un peu naïf de s’attendre à ce que chaque programmeur qui utilisera votre
fonction ou votre bibliothèque ne passera jamais un argument plus grand que 11.
Il existe une philosophie qui dit «échouer tôt et échouer bruyamment » ou «échouer
rapidement », qui enseigne de remonter les problèmes le plus tôt possible et d’arrê-
ter.
Une telle méthode en C/C++ est les assertions.
Nous pouvons modifier notre programme pour qu’il échoue si une valeur incorrecte
est passée:
La macro assertion vérifie que la validité des valeurs à chaque démarrage de fonction
et échoue si l’expression est fausse.
month$ = 48
get_month1_checked PROC
$LN5 :
push rbx
sub rsp, 32
movsxd rbx, ecx
cmp ebx, 12
jl SHORT $LN3@get_month1
lea rdx, OFFSET FLAT :$SG3143
370
lea rcx, OFFSET FLAT :$SG3144
mov r8d, 29
call _wassert
$LN3@get_month1 :
lea rcx, OFFSET FLAT :month1
mov rax, QWORD PTR [rcx+rbx*8]
add rsp, 32
pop rbx
ret 0
get_month1_checked ENDP
En fait, assert() n’est pas une fonction, mais une macro. Elle teste une condition,
puis passe le numéro de ligne et le nom du fichier à une autre fonction qui rapporte
cette information à l’utilisateur.
Ici nous voyons qu’à la fois le nom du fichier et la condition sont encodés en UTF-16.
Le numéro de ligne est aussi passé (c’est 29).
Le mécanisme est sans doute le même dans tous les compilateurs. Voici ce que fait
GCC:
get_month1_checked :
cmp edi, 11
jg .L6
movsx rdi, edi
mov rax, QWORD PTR month1[0+rdi*8]
ret
.L6 :
push rax
mov ecx, OFFSET FLAT :__PRETTY_FUNCTION__.2423
mov edx, 29
mov esi, OFFSET FLAT :.LC1
mov edi, OFFSET FLAT :.LC2
call __assert_fail
__PRETTY_FUNCTION__.2423:
.string "get_month1_checked"
Donc la macro dans GCC passe aussi le nom de la fonction par commodité.
Rien n’est vraiment gratuit, et c’est également vrai pour les tests de validité.
Ils rendent votre programme plus lent, en particulier si la macro assert() est utilisée
dans des petites fonctions à durée critique.
Donc MSCV, par exemple, laisse les tests dans les compilations debug, mais ils dis-
paraissent dans celles de release.
371
Les noyaux de Microsoft Windows NT existent en versions «checked » et «free ». 131 .
Le premier a des tests de validation (d’où, «checked »), le second n’en a pas (d’où,
«free/libre » de tests).
Bien sûr, le noyau «checked » fonctionne plus lentement à cause de ces tests, donc
il n’est utilisé que pour des sessions de debug.
Un tableau de pointeurs sur des chaînes peut être accédé comme ceci132 :
#include <stdio.h>
int main()
{
// 4ème mois, 5ème caractère:
printf ("%c\n", month[3][4]) ;
};
Il est très important de comprendre, que, malgré la syntaxe similaire, c’est différent
d’un tableau à deux dimensions, dont nous allons parler plus tard.
Une autre chose importante à noter: les chaînes considérées doivent être encodées
dans un système où chaque caractère occupe un seul octet, comme l’ASCII133 ou
l’ASCII étendu. UTF-8 ne fonctionnera pas ici.
131. msdn.microsoft.com/en-us/library/windows/hardware/ff543450(v=vs.85).aspx
132. Lisez l’avertissement dans la NDT ici 1.26.5 on page 363
133. American Standard Code for Information Interchange
372
1.26.6 Tableaux multidimensionnels
En interne, un tableau multidimensionnel est pratiquement la même chose qu’un
tableau linéaire.
Puisque la mémoire d’un ordinateur est linéaire, c’est un tableau uni-dimensionnel.
Par commodité, ce tableau multidimensionnel peut facilement être représenté comme
un uni-dimensionnel.
Par exemple, voici comment les éléments du tableau 3*4 sont placés dans un tableau
uni-dimensionnel de 12 éléments:
Voici comment chacun des éléments du tableau 3*4 sont placés en mémoire:
0 1 2 3
4 5 6 7
8 9 10 11
Tab. 1.4: Adresse mémoire de chaque élément d’un tableau à deux dimensions
Donc, afin de calculer l’adresse de l’élément voulu, nous devons d’abord multiplier
le premier index par 4 (largeur du tableau) et puis ajouter le second index. Ceci est
appelé row-major order (ordre ligne d’abord), et c’est la méthode de représentation
des tableaux et des matrices au moins en C/C++ et Python. Le terme row-major
order est de l’anglais signifiant: « d’abord, écrire les éléments de la première ligne,
puis ceux de la seconde ligne …et enfin les éléments de la dernière ligne ».
Une autre méthode de représentation est appelée column-major order (ordre co-
lonne d’abord) (les indices du tableau sont utilisés dans l’ordre inverse) et est utilisé
au moins en ForTran, MATLAB et R. Le terme column-major order est de l’anglais
signifiant: « d’abord, écrire les éléments de la première colonne, puis ceux de la
seconde colonne …et enfin les éléments de la dernière colonne ».
Quelle méthode est la meilleure?
373
En général, en termes de performance et de mémoire cache, le meilleur schéma
pour l’organisation des données est celui dans lequel les éléments sont accédés
séquentiellement.
Donc si votre fonction accède les données par ligne, row-major order est meilleur,
et vice-versa.
Nous allons travailler avec un tableau de type char, qui implique que chaque élément
n’a besoin que d’un octet en mémoire.
char a[3][4];
int main()
{
int x, y ;
// effacer le tableau
for (x=0; x<3; x++)
for (y=0; y<4; y++)
a[x][y]=0;
Les trois lignes sont entourées en rouge. Nous voyons que la seconde ligne a main-
tenant les valeurs 0, 1, 2 et 3:
374
Listing 1.248: Exemple de remplissage d’une colonne
#include <stdio.h>
char a[3][4];
int main()
{
int x, y ;
// effacer le tableau
for (x=0; x<3; x++)
for (y=0; y<4; y++)
a[x][y]=0;
Nous pouvons facilement nous assurer qu’il est possible d’accéder à un tableau en
deux dimensions d’au moins deux façons:
#include <stdio.h>
char a[3][4];
375
{
// traiter le tableau en entrée comme un pointeur
// calculer l'adresse, y prendre une valeur
// 4 est ici la largeur du tableau
return *(array+a*4+b) ;
};
int main()
{
a[2][3]=123;
printf ("%d\n", get_by_coordinates1(a, 2, 3)) ;
printf ("%d\n", get_by_coordinates2(a, 2, 3)) ;
printf ("%d\n", get_by_coordinates3(a, 2, 3)) ;
};
array$ = 8
a$ = 16
b$ = 24
get_by_coordinates2 PROC
movsxd rax, r8d
movsxd r9, edx
add rax, rcx
movzx eax, BYTE PTR [rax+r9*4]
ret 0
get_by_coordinates2 ENDP
array$ = 8
134. Ce programme doit être compilé comme un programme C, pas C++, sauvegardez-le dans un
fichier avecl’extention .c pour le compiler avec MSVC
376
a$ = 16
b$ = 24
get_by_coordinates1 PROC
movsxd rax, r8d
movsxd r9, edx
add rax, rcx
movzx eax, BYTE PTR [rax+r9*4]
ret 0
get_by_coordinates1 ENDP
get_by_coordinates1 :
; étendre le signe sur 64-bit des valeurs 32-bit en entrée "a" et "b"
movsx rsi, esi
movsx rdx, edx
lea rax, [rdi+rsi*4]
; RAX=RDI+RSI*4=adresse du tableau+a*4
movzx eax, BYTE PTR [rax+rdx]
; AL=charger l'octet à l'adresse RAX+RDX=adresse du tableau+a*4+b
ret
get_by_coordinates2 :
lea eax, [rdx+rsi*4]
; RAX=RDX+RSI*4=b+a*4
cdqe
movzx eax, BYTE PTR [rdi+rax]
; AL=charger l'octet à l'adresse RDI+RAX=adresse du tableau+b+a*4
ret
get_by_coordinates3 :
sal esi, 2
; ESI=a<<2=a*4
; étendre le signe sur 64-bit des valeurs 32-bit en entrée "a*4" et "b"
movsx rdx, edx
movsx rsi, esi
add rdi, rsi
; RDI=RDI+RSI=adresse du tableau+a*4
movzx eax, BYTE PTR [rdi+rdx]
; AL=charger l'octet à l'adresse RDI+RAX=adresse du tableau+a*4+b
ret
377
Nous allons travailler avec des tableaux de type int : chaque élément nécessite 4
octets en mémoire.
Voyons ceci:
int a[10][20][30];
x86
Rien de particulier. Pour le calcul de l’index, trois arguments en entrée sont utilisés
dans la formule address = 600 ⋅ 4 ⋅ x + 30 ⋅ 4 ⋅ y + 4z, pour représenter le tableau comme
multidimensionnel. N’oubliez pas que le type int est 32-bit (4 octets), donc tous les
coefficients doivent être multipliés par 4.
378
Listing 1.253: GCC 4.4.1
public insert
insert proc near
x = dword ptr 8
y = dword ptr 0Ch
z = dword ptr 10h
value = dword ptr 14h
push ebp
mov ebp, esp
push ebx
mov ebx, [ebp+x]
mov eax, [ebp+y]
mov ecx, [ebp+z]
lea edx, [eax+eax] ; edx=y*2
mov eax, edx ; eax=y*2
shl eax, 4 ; eax=(y*2)<<4 = y*2*16 = y*32
sub eax, edx ; eax=y*32 - y*2=y*30
imul edx, ebx, 600 ; edx=x*600
add eax, edx ; eax=eax+edx=y*30 + x*600
lea edx, [eax+ecx] ; edx=y*30 + x*600 + z
mov eax, [ebp+value]
mov dword ptr ds :a[edx*4], eax ; *(a+edx*4)=valeur
pop ebx
pop ebp
retn
insert endp
value = -0x10
z = -0xC
y = -8
x = -4
379
STR R0, [SP,#0x10+x]
STR R1, [SP,#0x10+y]
STR R2, [SP,#0x10+z]
STR R3, [SP,#0x10+value]
LDR R0, [SP,#0x10+value]
LDR R1, [SP,#0x10+z]
LDR R2, [SP,#0x10+y]
LDR R3, [SP,#0x10+x]
MOV R12, 2400
MUL.W R3, R3, R12
ADD R3, R9
MOV R9, 120
MUL.W R2, R2, R9
ADD R2, R3
LSLS R1, R1, #2 ; R1=R1<<2
ADD R1, R2
STR R0, [R1] ; R1 - adresse de l'élément du tableau
; libérer le chunk sur la pile locale, alloué pour 4 valeurs de type int
ADD SP, SP, #0x10
BX LR
LLVM sans optimisation sauve toutes les variables dans la pile locale, ce qui est
redondant.
L’adresse de l’élément du tableau est calculée par la formule vue précédemment.
380
décalage peut être appliqué au second opérande: (LSL#4).
Mais ce coefficient ne peut être appliqué qu’au second opérande.
C’est bien pour des opérations commutatives comme l’addition ou la multiplication
(les opérandes peuvent être échangés sans changer le résultat).
Mais la soustraction est une opération non commutative, donc RSB existe pour ces
cas.
MIPS
Mon exemple est minuscule, donc le compilateur GCC a décidé de mettre le tableau
a dans la zone de 64KiB adressable par le Global Pointer.
Listing 1.256: GCC 4.4.5 avec optimisation (IDA)
insert :
; $a0=x
; $a1=y
; $a2=z
; $a3=valeur
sll $v0, $a0, 5
; $v0 = $a0<<5 = x*32
sll $a0, 3
; $a0 = $a0<<3 = x*8
addu $a0, $v0
; $a0 = $a0+$v0 = x*8+x*32 = x*40
sll $v1, $a1, 5
; $v1 = $a1<<5 = y*32
sll $v0, $a0, 4
; $v0 = $a0<<4 = x*40*16 = x*640
sll $a1, 1
; $a1 = $a1<<1 = y*2
subu $a1, $v1, $a1
; $a1 = $v1-$a1 = y*32-y*2 = y*30
subu $a0, $v0, $a0
; $a0 = $v0-$a0 = x*640-x*40 = x*600
la $gp, __gnu_local_gp
addu $a0, $a1, $a0
; $a0 = $a1+$a0 = y*30+x*600
addu $a0, $a2
; $a0 = $a0+$a2 = y*30+x*600+z
; charger l'adresse de la table:
lw $v0, (a & 0xFFFF)($gp)
; multiplier l'index par 4 pour avancer d'un élément du tableau:
sll $a0, 2
; ajouter l'index multiplié et l'adresse de la table:
addu $a0, $v0, $a0
; stocker la valeur dans la table et retourner:
jr $ra
sw $a3, 0($a0)
.comm a :0x1770
381
Obtenir la dimension d’un tableau multidimensionnel
int main()
{
int array[10][20];
get_element(array, 4, 5) ;
};
…si compilé (par n’importe quel compilateur) et ensuite décompilé par Hex-Rays:
int get_element(int *array, int x, int y)
{
return array[20 * x + y];
}
int main()
{
int array[10][20][30];
get_element(array, 4, 5, 6) ;
};
Hex-Rays:
int get_element(int *array, int x, int y, int z)
{
return array[600 * x + z + 30 * y];
}
382
Plus d’exemples
L’écran de l’ordinateur est représenté comme un tableau 2D, mais le buffer vidéo
est un tableau linéaire 1D. Nous en parlons ici: 8.15.2 on page 1192.
Un autre exemple dans ce livre est le jeu Minesweeper: son champ est aussi un
tableau à deux dimensions: 8.4 on page 1056.
383
DB 061H
DB 072H
DB 079H
DB 00H
DB 00H
DB 00H
...
get_month2 PROC
; étendre le signe de l'argument en entrée sur 64-bit
movsxd rax, ecx
lea rcx, QWORD PTR [rax+rax*4]
; RCX=mois+mois*4=mois*5
lea rax, OFFSET FLAT :month2
; RAX=pointeur sur la table
lea rax, QWORD PTR [rax+rcx*2]
; RAX=pointeur sur la table + RCX*2=pointeur sur la table +
mois*5*2=pointeur sur la table + mois*10
ret 0
get_month2 ENDP
384
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
movsx rdx, eax
; RDX = valeur entrée avec signe étendu
mov rax, rdx
; RAX = mois
sal rax, 2
; RAX = mois<<2 = mois*4
add rax, rdx
; RAX = RAX+RDX = mois*4+mois = mois*5
add rax, rax
; RAX = RAX*2 = mois*5*2 = mois*10
add rax, OFFSET FLAT :month2
; RAX = mois*10 + pointeur sur la table
pop rbp
ret
Mais une chose est est curieuse: pourquoi ajouter une multiplication par zéro et
ajouter zéro au résultat final?
Ceci ressemble à une bizarrerie du générateur de code du compilateur, qui n’a pas
été détectée par les tests du compilateur (le code résultant fonctionne correctement
après tout). Nous examinons volontairement de tels morceaux de code, afin que le
lecteur prenne conscience qu’il ne doit parfois pas se casser la tête sur des artefacts
de compilateur.
385
32-bit ARM
Keil avec optimisation pour mode ARM utilise des instructions d’addition et de déca-
lage:
Listing 1.262: avec optimisation Keil 6/2013 (Mode ARM)
; R0 = mois
LDR r1,|L0.104|
; R1 = pointeur sur la table
ADD r0,r0,r0,LSL #2
; R0 = R0+R0<<2 = R0+R0*4 = mois*5
ADD r0,r1,r0,LSL #1
; R0 = R1+R0<<2 = pointeur sur la table + mois*5*2 = pointeur sur la table +
mois*10
BX lr
ARM64
SXTW est utilisée pour étendre le signe, convertir l’entrée 32-bit en 64-bit et stocker
le résultat dans X0.
La paire ADRP/ADD est utilisée pour charger l’adresse de la table.
L’instruction ADD a aussi un suffixe LSL, qui aide avec les multiplications.
386
MIPS
Conclusion
C’est une technique surannée de stocker des chaînes de texte. Vous pouvez en trou-
ver beaucoup dans Oracle RDBMS, par exemple. Il est difficile de dire si ça vaut la
peine de le faire sur des ordinateurs modernes. Néanmoins, c’est un bon exemple
de tableaux, donc il a été ajouté à ce livre.
387
1.26.8 Conclusion
Un tableau est un ensemble de données adjacentes en mémoire.
C’est vrai pour tout type d’élément, structures incluses.
Pour accéder à un élément spécifique d’un tableau, il suffit de calculer son adresse.
Donc, un pointeur sur un tableau et l’adresse de son premier élément—sont la même
chose. C’est pourquoi les expressions ptr[0] et *ptr sont équivalentes en C/C++. Il
est intéressant de noter que Hex-Rays remplace souvent la première par la seconde.
Il procède ainsi lorsqu’il n’a aucune idée qu’il travaille avec un pointeur sur le tableau
complet et pense que c’est un pointeur sur une seule variable.
1.26.9 Exercices
• http://challenges.re/62
• http://challenges.re/63
• http://challenges.re/64
• http://challenges.re/65
• http://challenges.re/66
388
From : [email protected] (George Bell)
Subject : [Angband] Multiple artifact copies found (bug ?)
Date : Fri, 23 Jul 1993 15:55:08 GMT
Then, when I found a way into the big vault, I noticed some of the treasure
had already been identified (in fact it looked strangely familiar !). Then ⤦
Ç I
found *two* Short Swords named Sting (1d6) (+7,+8), and I just ran across a
third copy ! I have seen multiple copies of Gurthang on this level as well.
Is there some limit on the number of items per level which I have exceeded ?
This sounds reasonable as all multiple copies I have seen come from this ⤦
Ç level.
-George Bell
( https://groups.google.com/forum/#!original/rec.games.moria/jItmfrdGyL8/
8csctQqA7PQJ )
From : Ceri <[email protected]>
Subject : Re : [Angband] Multiple artifact copies found (bug ?)
Date : Fri, 23 Jul 1993 23:32:20 -0400
welcome to the mush bug. if there are more than 256 items
on the floor, things start duplicating. learn to harness
this power and you will win shortly :>
--Rick
( google groups )
From : [email protected] (Nicholas C. Weaver)
Subject : Re : [Angband] Multiple artifact copies found (bug ?)
Date : 24 Jul 1993 18:18:05 GMT
389
>on the floor, things start duplicating. learn to harness
>this power and you will win shortly :>
>
>--Rick
Oh, for those who like to know about bugs, though, the -n option
(start new character) has the following behavior :
YOu loose all record of artifacts founds and named monsters killed.
YOu loose all items you are carrying (they get turned into error in
objid()s ).
Gaining spells will not work right after this, unless you have a
gain int item (for spellcasters) or gain wis item (for priests/palidans), ⤦
Ç in
which case after performing the above, then take the item back on and off,
you will be able to learn spells normally again.
This can be exploited, if you are a REAL H0ZER (like me), into
getting multiple artifacts early on. Just get to a level where you can
pound wormtongue into the ground, kill him, go up, drop your stuff in your
house, buy a few potions of restore exp and high value spellbooks with your
leftover gold, angband -n yourself back to what you were before, and repeat
the process. Yes, you CAN kill wormtongue multiple times. :)
390
Ç edu
It is a tale, told by an idiot, full of sound and fury, .signifying ⤦
Ç nothing.
Since C evolved out of B, and a C+ is close to a B,
does that mean that C++ is a devolution of the language ?
( https://groups.google.com/forum/#!original/rec.games.moria/jItmfrdGyL8/
FoQeiccewHAJ )
Le fil de discussion complet.
138
J’ai trouvé la version avec le bogue (2.4 fk) , et on peut voir clairement comment
les tableaux globaux sont déclarés:
/* Number of dungeon objects */
#define MAX_DUNGEON_OBJ 423
...
int16 sorted_objects[MAX_DUNGEON_OBJ];
Peut-être que ceci est une raison. La constante MAX_DUNGEON_OBJ est trop petite.
Peut-être que les auteurs devraient utiliser des listes chaînées ou d’autres structures
de données, qui ont une taille illimitée. Mais les tableaux sont plus simples à utiliser.
Un autre exemple de débordement de tampon dans un tableau défini globalement: 3.31
on page 847.
391
fh=CreateFile ("file", GENERIC_WRITE | GENERIC_READ, ⤦
Ç FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL) ;
Ici nous voyons l’instruction TEST, toutefois elle n’utilise pas complètement le second
argument, mais seulement l’octet le plus significatif et le teste avec le flag 0x40 (ce
qui implique le flag GENERIC_WRITE ici).
TEST est essentiellement la même chose que AND, mais sans sauver le résultat (rap-
pelez vous le cas de CMP qui est la même chose que SUB, mais sans sauver le résul-
tat ( 1.12.4 on page 118)).
La logique de ce bout de code est la suivante:
if ((dwDesiredAccess&0x40000000) == 0) goto loc_7C83D417
139. msdn.microsoft.com/en-us/library/aa363858(VS.85).aspx
392
Si l’instruction AND laisse ce bit, le flag ZF sera mis à zéro et le saut conditionnel JZ ne
sera pas effectué. Le saut conditionnel est effectué uniquement su la bit 0x40000000
est absent dans la variable dwDesiredAccess —auquel cas le résultat du AND est 0,
ZF est mis à 1 et le saut conditionnel est effectué.
Essayons avec GCC 4.4.1 et Linux:
#include <stdio.h>
#include <fcntl.h>
void main()
{
int handle ;
Nous obtenons:
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 20h
mov [esp+20h+var_1C], 42h
mov [esp+20h+var_20], offset aFile ; "file"
call _open
mov [esp+20h+var_4], eax
leave
retn
main endp
Donc, le champ de bits pour open() est apparemment testé quelque part dans le
noyau Linux.
393
Bien sûr, il est facile de télécharger le code source de la Glibc et du noyau Linux,
mais nous voulons comprendre ce qui se passe sans cela.
Donc, à partir de Linux 2.6, lorsque l’appel système sys_open est appelé, le contrôle
passe finalement à do_sys_open, et à partir de là—à la fonction do_filp_open()
(elle est située ici fs/namei.c dans l’arborescence des sources du noyau).
N.B. Outre le passage des arguments par la pile, il y a aussi une méthode consistant
à passer certains d’entre eux par des registres. Ceci est aussi appelé fastcall ( 6.1.3
on page 964). Ceci fonctionne plus vite puisque le CPU ne doit pas faire d’accès à la
pile en mémoire pour lire la valeur des arguments. GCC a l’option regparm140 , avec
laquelle il est possible de définir le nombre d’arguments qui peuvent être passés par
des registres.
141 142
Le noyau Linux 2.6 est compilé avec l’option -mregparm=3 .
Cela signifie que les 3 premiers arguments sont passés par les registres EAX, EDX et
ECX, et le reste via la pile. Bien sûr, si le nombre d’arguments est moins que 3, seule
une partie de ces registres seront utilisés.
Donc, téléchargeons le noyau Linux 2.6.31, compilons-le dans Ubuntu: make vmlinux,
ouvrons-le dans IDA, et cherchons la fonction do_filp_open(). Au début, nous voyons
(les commentaires sont les miens) :
GCC sauve les valeurs des 3 premiers arguments dans la pile locale. Si cela n’était
pas fait, le compilateur ne toucherait pas ces registres, et ça serait un environnement
trop étroit pour l’allocateur de registres du compilateur.
Cherchons ce morceau de code:
140. ohse.de/uwe/articles/gcc-attributes.html#func-regparm
141. kernelnewbies.org/Linux_2_6_20#head-042c62f290834eb1fe0a1942bbf5bb9a4accbc8f
142. Voir aussi le fichier arch/x86/include/asm/calling.h dans l’arborescence du noyau
394
loc_C01EF684 : ; CODE XREF: do_filp_open+4F
test bl, 40h ; O_CREAT
jnz loc_C01EF810
mov edi, ebx
shr edi, 11h
xor edi, 1
and edi, 1
test ebx, 10000h
jz short loc_C01EF6D3
or edi, 2
0x40—c’est ce à quoi est égale la macro O_CREAT. Le bit 0x40 de open_flag est
testé, et si il est à 1, le saut de l’instruction JNZ suivante est effectué.
ARM
395
Voici à quoi ressemble le noyau compilé pour le mode ARM dans IDA :
TST est analogue à l’instruction TEST en x86. Nous pouvons «pointer » visuellement
ce morceau de code grâce au fait que la fonction lookup_fast() doit être exécutée
dans un cas et complete_walk() dans l’autre. Ceci correspond au code source de la
fonction do_last(). La macro O_CREAT vaut 0x40 ici aussi.
int f(int a)
{
int rt=a ;
396
SET_BIT (rt, 0x4000) ;
REMOVE_BIT (rt, 0x200) ;
return rt ;
};
int main()
{
f(0x12340678) ;
};
x86
397
OllyDbg
398
OR exécuté:
399
La valeur est encore rechargée (car le compilateur n’est pas en mode avec optimi-
sation) :
400
AND exécuté:
Le 10ème bit a été mis à 0 (ou, en d’autres mots, tous les bits ont été laissés sauf le
10ème) et la valeur finale est maintenant 0x12344478 (0b10010001101000100010001111000).
Si nous le compilons dans MSVC avec l’option d’optimisation (/Ox), le code est même
plus court:
401
var_4 = dword ptr -4
arg_0 = dword ptr 8
push ebp
mov ebp, esp
sub esp, 10h
mov eax, [ebp+arg_0]
mov [ebp+var_4], eax
or [ebp+var_4], 4000h
and [ebp+var_4], 0FFFFFDFFh
mov eax, [ebp+var_4]
leave
retn
f endp
Il y a du code redondant, toutefois, c’est plus court que la version MSVC sans opti-
misation.
Maintenant, essayons GCC avec l’option d’optimisation -O3 :
push ebp
mov ebp, esp
mov eax, [ebp+arg_0]
pop ebp
or ah, 40h
and ah, 0FDh
retn
f endp
C’est plus court. Il est intéressant de noter que le compilateur travaille avec une
partie du registre EAX via le registre AH—qui est la partie du registre EAX située entre
les 8ème et 15ème bits inclus.
Octet d’indice
7 6 5 4 3 2 1 0
RAXx64
EAX
AX
AH AL
N.B. L’accumulateur du CPU 16-bit 8086 était appelé AX et consistait en deux moitiés
de 8-bit—AL (octet bas) et AH (octet haut). Dans le 80386, presque tous les registres
402
ont été étendus à 32-bit, l’accumulateur a été appelé EAX, mais pour des raisons de
compatibilité, ses anciennes parties peuvent toujours être accédées par AX/AH/AL.
Puisque tous les CPUs x86 sont des descendants du CPU 16-bit 8086, ces anciens
opcodes 16-bit sont plus courts que les nouveaux sur 32-bit. C’est pourquoi l’instruc-
tion or ah, 40h occupe seulement 3 octets. Il serait plus logique de générer ici or
eax, 04000h mais ça fait 5 octets, ou même 6 (dans le cas où le registre du premier
opérande n’est pas EAX).
Il serait encore plus court en mettant le flag d’optimisation -O3 et aussi regparm=3.
En effet, le premier argument est déjà chargé dans EAX, donc il est possible de tra-
vailler avec directement. Il est intéressant de noter qu’à la fois le prologue (push ebp
/ mov ebp,esp) et l’épilogue (pop ebp) de la fonction peuvent être facilement omis
ici, mais sans doute que GCC n’est pas assez bon pour effectuer une telle optimisa-
tion de la taille du code. Toutefois, il est préférable que de telles petites fonctions
soient des fonctions inlined ( 3.14 on page 664).
L’instruction BIC (BItwise bit Clear) est une instruction pour mettre à zéro des bits
spécifiques. Ceci est comme l’instruction AND, mais avec un opérande inversé. I.e.,
c’est analogue à la paire d’instructions NOT +AND.
ORR est le «ou logique », analogue à OR en x86.
Jusqu’ici, c’est facile.
403
Listing 1.280: avec optimisation Keil 6/2013 (Mode Thumb)
01 21 89 03 MOVS R1, 0x4000
08 43 ORRS R0, R1
49 11 ASRS R1, R1, #5 ; génère 0x200 et le met dans R1
88 43 BICS R0, R1
70 47 BX LR
Il semble que Keil a décidé que le code en mode Thumb, pour générer 0x200 à partir
de 0x4000, est plus compact que celui pour écrire 0x200 dans un registre arbitraire.
C’est pourquoi, avec l’aide de ASRS (décalage arithmétique vers la droite), cette
valeur est calculée comme 0x4000 ≫ 5.
Le code qui a été généré par LLVM, pourrait être quelque chose comme ça sous la
forme de code source:
REMOVE_BIT (rt, 0x4200) ;
SET_BIT (rt, 0x4000) ;
Et c’est exactement ce dont nous avons besoin. Mais pourquoi 0x4200 ? Peut-être
que c’est un artefact de l’optimiseur de LLVM143 .
Probablement une erreur de l’optimiseur du compilateur, mais le code généré fonc-
tionne malgré tout correctement.
Vous pouvez en savoir plus sur les anomalies de compilateur ici ( 11.4 on page 1303).
avec optimisation Xcode 4.6.3 (LLVM) pour le mode Thumb génère le même code.
return rt ;
};
404
f PROC
BIC r0,r0,#0x1000
BIC r0,r0,#0x234
BX lr
ENDP
Il y a deux instructions BIC, i.e., les bits 0x1234 sont mis à zéro en deux temps.
C’est parce qu’il n’est pas possible d’encoder 0x1234 dans une instruction BIC, mais
il est possible d’encoder 0x1000 et 0x234.
GCC en compilant avec optimisation pour ARM64 peut utiliser l’instruction AND au
lieu de BIC :
GCC sans optimisation génère plus de code redondant, mais fonctionne comme celui
optimisé:
MIPS
405
ori $a0, 0x4000
; $a0=a|0x4000
li $v0, 0xFFFFFDFF
jr $ra
and $v0, $a0, $v0
; à la fin: $v0 = $a0 & $v0 = a|0x4000 & 0xFFFFFDFF
ORI est, bien sûr, l’opération OR. « I » dans l’instruction signifie que la valeur est
intégrée dans le code machine.
Mais après ça, nous avons AND. Il n’y a pas moyen d’utiliser ANDI car il n’est pas
possible d’intégrer le nombre 0xFFFFFDFF dans une seule instruction, donc le com-
pilateur doit d’abord charger 0xFFFFFDFF dans le registre $V0 et ensuite génère AND
qui prend toutes ses valeurs depuis des registres.
1.28.3 Décalages
Les décalages de bit sont implémentés en C/C++ avec les opérateurs ≪ et ≫. Le x86
ISA possède les instructions SHL (SHift Left / décalage à gauche) et SHR (SHift Right
/ décalage à droite) pour ceci. Les instructions de décalage sont souvent utilisées
pour la division et la multiplication par des puissances de deux: 2n (e.g., 1, 2, 4, 8,
etc.) : 1.24.1 on page 277, 1.24.2 on page 283.
Les opérations de décalage sont aussi si importantes car elles sont souvent utilisées
pour isoler des bits spécifiques ou pour construire une valeur à partir de plusieurs
bits épars.
31 30 23 22 0
( S — signe )
Le signe du nombre est dans le MSB144 . Est-ce qu’il est possible de changer le signe
d’un nombre en virgule flottante sans aucune instruction FPU?
#include <stdio.h>
406
{
unsigned int tmp=(*(unsigned int*)&i) | 0x80000000 ;
return *(float*)&tmp ;
};
int main()
{
printf ("my_abs() :\n") ;
printf ("%f\n", my_abs (123.456)) ;
printf ("%f\n", my_abs (-456.123)) ;
printf ("set_sign() :\n") ;
printf ("%f\n", set_sign (123.456)) ;
printf ("%f\n", set_sign (-456.123)) ;
printf ("negate() :\n") ;
printf ("%f\n", negate (123.456)) ;
printf ("%f\n", negate (-456.123)) ;
};
Nous avons besoin de cette ruse en C/C++ pour copier vers/depuis des valeurs
float sans conversion effective. Donc il y a trois fonctions: my_abs() supprime MSB ;
set_sign() met MSB et negate() l’inverse.
XOR peut être utilisé pour inverser un bit: 2.6 on page 596.
x86
_tmp$ = 8
_i$ = 8
_set_sign PROC
or DWORD PTR _i$[esp-4], -2147483648 ; 80000000H
fld DWORD PTR _tmp$[esp-4]
ret 0
_set_sign ENDP
_tmp$ = 8
_i$ = 8
407
_negate PROC
xor DWORD PTR _i$[esp-4], -2147483648 ; 80000000H
fld DWORD PTR _tmp$[esp-4]
ret 0
_negate ENDP
Une valeur en entrée de type float est prise sur la pile, mais traitée comme une
valeur entière.
AND et OR supprime et mette le bit désiré. XOR l’inverse.
Enfin, la valeur modifiée est chargée dans ST0,car les nombres en virgule flottante
sont renvoyés dans ce registre.
Maintenant essayons l’optimisation de MSVC 2012 pour x64:
tmp$ = 8
i$ = 8
set_sign PROC
movss DWORD PTR [rsp+8], xmm0
mov eax, DWORD PTR i$[rsp]
bts eax, 31
mov DWORD PTR tmp$[rsp], eax
movss xmm0, DWORD PTR tmp$[rsp]
ret 0
set_sign ENDP
tmp$ = 8
i$ = 8
negate PROC
movss DWORD PTR [rsp+8], xmm0
mov eax, DWORD PTR i$[rsp]
btc eax, 31
mov DWORD PTR tmp$[rsp], eax
movss xmm0, DWORD PTR tmp$[rsp]
ret 0
negate ENDP
La valeur en entrée est passée dans XMM0, puis elle est copiée sur la pile locale et
nous voyons des nouvelles instructions: BTR, BTS, BTC.
408
Ces instructions sont utilisées pour effacer (BTR), mettre (BTS) et inverser (ou faire
le complément: BTC) de bits spécifiques. Le bit d’index 31 est le MSB, en comptant
depuis 0.
Enfin, le résultat est copié dans XMM0, car les valeurs en virgule flottante sont ren-
voyées dans XMM0 en environnement Win64.
MIPS
set_sign :
; déplacer depuis le coprocesseur 1:
mfc1 $v0, $f12
lui $v1, 0x8000
; $v1=0x80000000
; faire OR:
or $v0, $v1, $v0
; déplacer vers le coprocesseur 1:
mtc1 $v0, $f0
; return
jr $ra
or $at, $zero ; slot de délai de branchement
negate :
; déplacer depuis le coprocesseur 1:
mfc1 $v0, $f12
lui $v1, 0x8000
; $v1=0x80000000
; do XOR:
xor $v0, $v1, $v0
; déplacer vers le coprocesseur 1:
mtc1 $v0, $f0
; sortir
jr $ra
or $at, $zero ; slot de délai de branchement
Une seule instruction LUI est utilisée pour charger 0x80000000 dans un registre, car
409
LUI efface les 16 bits bas et ils sont à zéro dans la constante, donc un LUI sans ORI
ultérieur est suffisant.
ARM
set_sign PROC
; faire OR:
ORR r0,r0,#0x80000000
BX lr
ENDP
negate PROC
; faire XOR:
EOR r0,r0,#0x80000000
BX lr
ENDP
set_sign PROC
MOVS r1,#1
; r1=1
LSLS r1,r1,#31
; r1=1<<31=0x80000000
ORRS r0,r0,r1
; r0=r0 | 0x80000000
410
BX lr
ENDP
negate PROC
MOVS r1,#1
; r1=1
LSLS r1,r1,#31
; r1=1<<31=0x80000000
EORS r0,r0,r1
; r0=r0 ^ 0x80000000
BX lr
ENDP
En ARM, le mode Thumb offre des instructions 16-bit et peu de données peuvent y
être encodées, donc ici une paire d’instructions MOVS/LSLS est utilisée pour former
la constante 0x80000000. Ça fonctionne comme ceci: 1 << 31 = 0x80000000.
Le code de my_abs est bizarre et fonctionne pratiquement comme cette expression:
(i << 1) >> 1. Cette déclaration semble vide de sens. Mais néanmoins, lorsque input << 1
est exécuté, le MSB (bit de signe) est simplement supprimé. Puis lorsque la déclara-
tion suivante result >> 1 est exécutée, tous les bits sont à nouveau à leur place, mais
le MSB vaut zéro, car tous les «nouveaux » bits apparaissant lors d’une opération de
décalage sont toujours zéro. C’est ainsi que la paire d’instructions LSLS/LSRS efface
le MSB.
Listing 1.290: avec optimisation GCC 4.6.3 for Raspberry Pi (Mode ARM)
my_abs
; copier depuis S0 vers R2:
FMRS R2, S0
; effacer le bit:
BIC R3, R2, #0x80000000
; copier depuis R3 vers S0:
FMSR S0, R3
BX LR
set_sign
; copier depuis S0 vers R2:
FMRS R2, S0
; faire OR:
ORR R3, R2, #0x80000000
; copier depuis R3 vers S0:
FMSR S0, R3
BX LR
negate
; copier depuis S0 vers R2:
FMRS R2, S0
; faire ADD:
ADD R3, R2, #0x80000000
411
; copier depuis R3 vers S0:
FMSR S0, R3
BX LR
Lançons Linux pour Raspberry Pi dans QEMU et ça émule un FPU ARM, dons les S-
registres sont utilisés pour les nombres en virgule flottante au lieu des R-registres.
L’instruction FMRS copie des données des GPR vers le FPU et retour.
my_abs() et set_sign() ressemblent a ce que l’on attend, mais negate() ? Pourquoi
est-ce qu’il y a ADD au lieu de XOR ?
C’est dur à croire, mais l’instruction ADD register, 0x80000000 fonctionne tout
comme
XOR register, 0x80000000. Tout d’abord, quel est notre but? Le but est de chan-
ger le MSB, donc oublions l’opération XOR. Des mathématiques niveau scolaire, nous
nous rappelons qu’ajouter une valeur comme 1000 à une autre valeur n’affecte ja-
mais les 3 derniers chiffres. Par exemple: 1234567 + 10000 = 1244567 (les 4 derniers
chiffres ne sont jamais affectés).
Mais ici nous opérons en base décimale et
0x80000000 est 0b100000000000000000000000000000000, i.e., seulement le bit
le plus haut est mis.
Ajouter 0x80000000 à n’importe quelle valeur n’affecte jamais les 31 bits les plus
bas, mais affecte seulement le MSB. Ajouter 1 à 0 donne 1.
Ajouter 1 à 1 donne 0b10 au format binaire, mais le bit d’indice 32 (en comptant à
partir de zéro) est abandonné, car notre registre est large de 32 bit, donc le résultat
est 0. C’est pourquoi XOR peut être remplacé par ADD ici.
Il est difficile de dire pourquoi GCC a décidé de faire ça, mais ça fonctionne correc-
tement.
145. les CPUs x86 modernes (qui supportent SSE4) ont même une instruction POPCNT pour cela
412
return rt ;
};
int main()
{
f(0x12345678) ; // test
};
Dans cette boucle, la variable d’itération i prend les valeurs de 0 à 31, donc la dé-
claration 1 ≪ i prend les valeurs de 1 à 0x80000000. Pour décrire cette opération
en langage naturel, nous dirions décaler 1 par n bits à gauche. En d’autres mots, la
déclaration 1 ≪ i produit consécutivement toutes les positions possible pour un bit
dans un nombre de 32-bit. Le bit libéré à droite est toujours à 0.
Voici une table de tous les 1 ≪ i possible for i = 0 . . . 31 :
413
C/C++ expression Puissance de deux Forme décimale Forme hexadécimale
1≪0 20 1 1
1≪1 21 2 2
1≪2 22 4 4
1≪3 23 8 8
1≪4 24 16 0x10
1≪5 25 32 0x20
1≪6 26 64 0x40
1≪7 27 128 0x80
1≪8 28 256 0x100
1≪9 29 512 0x200
1 ≪ 10 210 1024 0x400
1 ≪ 11 211 2048 0x800
1 ≪ 12 212 4096 0x1000
1 ≪ 13 213 8192 0x2000
1 ≪ 14 214 16384 0x4000
1 ≪ 15 215 32768 0x8000
1 ≪ 16 216 65536 0x10000
1 ≪ 17 217 131072 0x20000
1 ≪ 18 218 262144 0x40000
1 ≪ 19 219 524288 0x80000
1 ≪ 20 220 1048576 0x100000
1 ≪ 21 221 2097152 0x200000
1 ≪ 22 222 4194304 0x400000
1 ≪ 23 223 8388608 0x800000
1 ≪ 24 224 16777216 0x1000000
1 ≪ 25 225 33554432 0x2000000
1 ≪ 26 226 67108864 0x4000000
1 ≪ 27 227 134217728 0x8000000
1 ≪ 28 228 268435456 0x10000000
1 ≪ 29 229 536870912 0x20000000
1 ≪ 30 230 1073741824 0x40000000
1 ≪ 31 231 2147483648 0x80000000
Ces constantes (masques de bit) apparaissent très souvent le code et un rétro-
ingénieur pratiquant doit pouvoir les repérer rapidement.
Les nombres décimaux avant 65536 et les hexadécimaux sont faciles à mémoriser.
Tandis que les nombres décimaux après 65536 ne valent probablement pas la peine
de l’être.
Ces constantes sont utilisées très souvent pour mapper des flags sur des bits spé-
cifiques. Par exemple, voici un extrait de ssl_private.h du code source d’Apache
2.4.6:
/**
* Define the SSL options
*/
#define SSL_OPT_NONE (0)
#define SSL_OPT_RELSET (1<<0)
414
#define SSL_OPT_STDENVVARS (1<<1)
#define SSL_OPT_EXPORTCERTDATA (1<<3)
#define SSL_OPT_FAKEBASICAUTH (1<<4)
#define SSL_OPT_STRICTREQUIRE (1<<5)
#define SSL_OPT_OPTRENEGOTIATE (1<<6)
#define SSL_OPT_LEGACYDNFORMAT (1<<7)
x86
MSVC
415
$LN2@f :
mov eax, DWORD PTR _rt$[ebp]
mov esp, ebp
pop ebp
ret 0
_f ENDP
416
OllyDbg
417
SHL a été exécuté:
418
AND met ZF à 1, ce qui implique que la valeur en entrée (0x12345678) ANDée avec
2 donne 0:
Fig. 1.101: OllyDbg : i = 1, y a-t-il ce bit dans la valeur en entrée? Non. (ZF =1)
419
Avançons un peu plus et i vaut maintenant 4. SHL va être exécuté maintenant:
420
EDX =1 ≪ 4 (ou 0x10 ou 16) :
421
AND est exécuté:
Fig. 1.104: OllyDbg : i = 4, y a-t-il ce bit dans la valeur en entrée? Oui. (ZF =0)
GCC
push ebp
mov ebp, esp
push ebx
sub esp, 10h
mov [ebp+rt], 0
mov [ebp+i], 0
jmp short loc_80483EF
loc_80483D0 :
mov eax, [ebp+i]
422
mov edx, 1
mov ebx, edx
mov ecx, eax
shl ebx, cl
mov eax, ebx
and eax, [ebp+arg_0]
test eax, eax
jz short loc_80483EB
add [ebp+rt], 1
loc_80483EB :
add [ebp+i], 1
loc_80483EF :
cmp [ebp+i], 1Fh
jle short loc_80483D0
mov eax, [ebp+rt]
add esp, 10h
pop ebx
pop ebp
retn
f endp
x64
int f(uint64_t a)
{
uint64_t i ;
int rt=0;
return rt ;
};
423
mov QWORD PTR [rbp-24], rdi ; a
mov DWORD PTR [rbp-12], 0 ; rt=0
mov QWORD PTR [rbp-8], 0 ; i=0
jmp .L2
.L4 :
mov rax, QWORD PTR [rbp-8]
mov rdx, QWORD PTR [rbp-24]
; RAX = i, RDX = a
mov ecx, eax
; ECX = i
shr rdx, cl
; RDX = RDX>>CL = a>>i
mov rax, rdx
; RAX = RDX = a>>i
and eax, 1
; EAX = EAX&1 = (a>>i)&1
test rax, rax
; est-ce que le dernier bit est zéro?
; passer l'instruction ADD suivante, si c'est le cas.
je .L3
add DWORD PTR [rbp-12], 1 ; rt++
.L3 :
add QWORD PTR [rbp-8], 1 ; i++
.L2 :
cmp QWORD PTR [rbp-8], 63 ; i<63?
jbe .L4 ; sauter au début du corps de la
boucle, si oui
mov eax, DWORD PTR [rbp-12] ; renvoyer rt
pop rbp
ret
424
Ce code est plus concis, mais a une particularité.
Dans tous les exemples que nous avons vu jusqu’ici, nous incrémentions la valeur
de «rt » après la comparaison d’un bit spécifique, mais le code ici incrémente «rt »
avant (ligne 6), écrivant la nouvelle valeur dans le registre EDX. Donc, si le dernier
bit est à 1, l’instruction CMOVNE146 (qui est un synonyme pour CMOVNZ147 ) commits la
nouvelle valeur de «rt » en déplaçant EDX («valeur proposée de rt ») dans EAX («rt
courant » qui va être retourné à la fin).
C’est pourquoi l’incrémentation est effectuée à chaque étape de la boucle, i.e., 64
fois, sans relation avec la valeur en entrée.
L’avantage de ce code est qu’il contient seulement un saut conditionnel (à la fin de
la boucle) au lieu de deux sauts (évitant l’incrément de la valeur de «rt » et à la
fin de la boucle). Et cela doit s’exécuter plus vite sur les CPUs modernes avec des
prédicteurs de branchement: 2.10.1 on page 604.
La dernière instruction est REP RET (opcode F3 C3) qui est aussi appelée FATRET par
MSVC. C’est en quelque sorte une version optimisée de RET, qu’AMD recommande de
mettre en fin de fonction, si RET se trouve juste après un saut conditionnel: [Software
Optimization Guide for AMD Family 16h Processors, (2013)p.15] 148 .
Ici l’instruction ROL est utilisée au lieu de SHL, qui est en fait «rotate left / pivoter
à gauche » au lieu de «shift left / décaler à gauche », mais dans cet exemple elle
146. Conditional MOVe if Not Equal
147. Conditional MOVe if Not Zero
148. Lire aussi à ce propos: http://repzret.org/p/repzret/
425
fonctionne tout comme SHL.
Vous pouvez en lire plus sur l’instruction de rotation ici: .1.6 on page 1354.
R8 ici est compté de 64 à 0. C’est tout comme un i inversé.
Voici une table de quelques registres pendant l’exécution:
RDX R8
0x0000000000000001 64
0x0000000000000002 63
0x0000000000000004 62
0x0000000000000008 61
... ...
0x4000000000000000 2
0x8000000000000000 1
À la fin, nous voyons l’instruction FATRET, qui a été expliquée ici: 1.28.5 on the pre-
vious page.
426
MSVC 2012 avec optimisation fait presque le même job que MSVC 2010 avec opti-
misation, mais en quelque sorte, il génère deux corps de boucles identiques et le
nombre de boucles est maintenant 32 au lieu de 64.
Pour être honnête, il n’est pas possible de dire pourquoi. Une ruse d’optimisation?
Peut-être est-il meilleur pour le corps de la boucle d’être légèrement plus long?
De toute façon, ce genre de code est pertinent ici pour montrer que parfois la sortie
du compilateur peut être vraiment bizarre et illogique, mais fonctionner parfaite-
ment.
Presque la même, mais ici il y a deux instructions utilisées, LSL.W/TST, au lieu d’une
seule TST, car en mode Thumb il n’est pas possible de définir le modificateur LSL
directement dans TST.
MOV R1, R0
MOVS R0, #0
MOV.W R9, #1
427
MOVS R3, #0
loc_2F7A
LSL.W R2, R9, R3
TST R2, R1
ADD.W R3, R3, #1
IT NE
ADDNE R0, #1
CMP R3, #32
BNE loc_2F7A
BX LR
Prenons un exemple en 64.bit qui a déjà été utilisé: 1.28.5 on page 423.
Le résultat est très semblable à ce que GCC génère pour x64: 1.294 on page 424.
L’instruction CSEL signifie «Conditional SELect / sélection conditionnelle ». Elle choisi
une des deux variables en fonction des flags mis par TST et copie la valeur dans W2,
qui contient la variable «rt ».
De nouveau, nous travaillons sur un exemple 64-bit qui a déjà été utilisé: 1.28.5 on
page 423. Le code est plus verbeux, comme d’habitude.
428
str wzr, [sp,28] ; i=0
b .L2
.L4 :
ldr w0, [sp,28]
mov x1, 1
lsl x0, x1, x0 ; X0 = X1<<X0 = 1<<i
mov x1, x0
; X1 = 1<<i
ldr x0, [sp,8]
; X0 = a
and x0, x1, x0
; X0 = X1&X0 = (1<<i) & a
; X0 contient zéro? alors sauter en .L3, évitant d'incrémenter "rt"
cmp x0, xzr
beq .L3
; rt++
ldr w0, [sp,24]
add w0, w0, 1
str w0, [sp,24]
.L3 :
; i++
ldr w0, [sp,28]
add w0, w0, 1
str w0, [sp,28]
.L2 :
; i<=63? alors sauter en .L4
ldr w0, [sp,28]
cmp w0, 63
ble .L4
; renvoyer rt
ldr w0, [sp,24]
add sp, sp, 32
ret
MIPS
429
sw $zero, 0x18+rt($fp)
sw $zero, 0x18+i($fp)
; saut aux instructions de test de la boucle
b loc_68
or $at, $zero ; slot de délai de branchement, NOP
loc_20 :
li $v1, 1
lw $v0, 0x18+i($fp)
or $at, $zero ; slot de délai de chargement, NOP
sllv $v0, $v1, $v0
; $v0 = 1<<i
move $v1, $v0
lw $v0, 0x18+a($fp)
or $at, $zero ; slot de délai de chargement, NOP
and $v0, $v1, $v0
; $v0 = a & (1<<i)
; est-ce que a & (1<<i) est égal à zéro? sauter en loc_58 si oui:
beqz $v0, loc_58
or $at, $zero
; il n'y pas eu de saut, cela signifie que a & (1<<i) !=0, il faut donc
incrémenter "rt":
lw $v0, 0x18+rt($fp)
or $at, $zero ; slot de délai de chargement, NOP
addiu $v0, 1
sw $v0, 0x18+rt($fp)
loc_58 :
; incrémenter i:
lw $v0, 0x18+i($fp)
or $at, $zero ; slot de délai de chargement, NOP
addiu $v0, 1
sw $v0, 0x18+i($fp)
loc_68 :
; charger i et le comparer avec 0x20 (32).
; sauter en loc_20 si il vaut moins de 0x20 (32) :
lw $v0, 0x18+i($fp)
or $at, $zero ; slot de délai de chargement, NOP
slti $v0, 0x20 # ' '
bnez $v0, loc_20
or $at, $zero ; slot de délai de branchement, NOP
; épilogue de la fonction. renvoyer rt:
lw $v0, 0x18+rt($fp)
move $sp, $fp ; slot de délai de chargement
lw $fp, 0x18+var_4($sp)
addiu $sp, 0x18 ; slot de délai de chargement
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
C’est très verbeux: toutes les variables locales sont situées dans la pile locale et
rechargées à chaque fois que l’on en a besoin.
L’instruction SLLV est «Shift Word Left Logical Variable », elle diffère de SLL seule-
430
ment de ce que la valeur du décalage est encodée dans l’instruction SLL (et par
conséquent fixée) mais SLLV lit cette valeur depuis un registre.
loc_14 :
and $a1, $a0
; $a1 = a&(1<<i)
; incrémenter i:
addiu $v1, 1
; sauter en loc_28 si a&(1<<i)==0 et incrémenter rt:
beqz $a1, loc_28
addiu $a2, $v0, 1
; si le saut BEQZ n'a pas été suivi, sauver la nouvelle valeur de rt dans
$v0:
move $v0, $a2
loc_28 :
; si i!=32, sauter en loc_14 et préparer la prochaine valeur décalée:
bne $v1, $a3, loc_14
sllv $a1, $t0, $v1
; sortir
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
1.28.6 Conclusion
Semblables aux opérateurs de décalage de C/C++ ≪ et ≫, les instructions de déca-
lage en x86 sont SHR/SHL (pour les valeurs non-signées) et SAR/SHL (pour les valeurs
signées).
431
Les instructions de décalages en ARM sont LSR/LSL (pour les valeurs non-signées)
et ASR/LSL (pour les valeurs signées).
Il est aussi possible d’ajouter un suffixe de décalage à certaines instructions (qui sont
appelées «data processing instructions/instructions de traitement de données »).
Parfois, AND est utilisé au lieu de TEST, mais les flags qui sont mis sont les même.
Ceci est effectué en général par ce bout de code C/C++ (décaler la valeur de n bits
vers la droite, puis couper le plus petit bit) :
432
Ou (décaler 1 bit n fois à gauche, isoler ce bit dans la valeur entrée et tester si ce
n’est pas zéro) :
433
Mettre à 0 un bit spécifique (connu à l’étape de compilation)
Ceci laisse tous les bits qui sont à 1 inchangés excepté un.
ARM en mode ARM a l’instruction BIC, qui fonctionne comme la paire d’instructions:
NOT +AND :
1.28.7 Exercices
• http://challenges.re/67
• http://challenges.re/68
• http://challenges.re/69
• http://challenges.re/70
434
1.29 Générateur congruentiel linéaire comme gé-
nérateur de nombres pseudo-aléatoires
Peut-être que le générateur congruentiel linéaire est le moyen le plus simple possible
de générer des nombres aléatoires.
Ce n’est plus très utilisé aujourd’hui150 , mais il est si simple (juste une multiplication,
une addition et une opération AND) que nous pouvons l’utiliser comme un exemple.
#include <stdint.h>
int my_rand ()
{
rand_state=rand_state*RNG_a ;
rand_state=rand_state+RNG_c ;
return rand_state & 0x7fff ;
}
Il y a deux fonctions: la première est utilisée pour initialiser l’état interne, et la se-
conde est appelée pour générer un nombre pseudo-aléatoire.
Nous voyons que deux constantes sont utilisées dans l’algorithme. Elles proviennent
de [William H. Press and Saul A. Teukolsky and William T. Vetterling and Brian P. Flan-
nery, Numerical Recipes, (2007)].
Définissons-les en utilisant la déclaration C/C++ #define. C’est une macro.
La différence entre une macro C/C++ et une constante est que toutes les macros
sont remplacées par leur valeur par le pré-processeur C/C++, et qu’elles n’utilisent
pas de mémoire, contrairement aux variables.
Par contre, une constante est une variable en lecture seule.
Il est possible de prendre un pointeur (ou une adresse) d’une variable constante,
mais c’est impossible de faire ça avec une macro.
La dernière opération AND est nécessaire car d’après le standard C my_rand() doit
renvoyer une valeur dans l’intervalle 0..32767.
Si vous voulez obtenir des valeurs pseudo-aléatoires 32-bit, il suffit d’omettre la
dernière opération AND.
150. Le twister de Mersenne est meilleur.
435
1.29.1 x86
Listing 1.321: MSVC 2013 avec optimisation
_BSS SEGMENT
_rand_state DD 01H DUP (?)
_BSS ENDS
_init$ = 8
_srand PROC
mov eax, DWORD PTR _init$[esp-4]
mov DWORD PTR _rand_state, eax
ret 0
_srand ENDP
_TEXT SEGMENT
_rand PROC
imul eax, DWORD PTR _rand_state, 1664525
add eax, 1013904223 ; 3c6ef35fH
mov DWORD PTR _rand_state, eax
and eax, 32767 ; 00007fffH
ret 0
_rand ENDP
_TEXT ENDS
Nous les voyons ici: les deux constantes sont intégrées dans le code. Il n’y a pas de
mémoire allouée pour elles.
La fonction my_srand() copie juste sa valeur en entrée dans la variable rand_state
interne.
my_rand() la prend, calcule le rand_state suivant, le coupe et le laisse dans le
registre EAX.
La version non optimisée est plus verbeuse:
_init$ = 8
_srand PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _init$[ebp]
mov DWORD PTR _rand_state, eax
pop ebp
ret 0
_srand ENDP
_TEXT SEGMENT
_rand PROC
436
push ebp
mov ebp, esp
imul eax, DWORD PTR _rand_state, 1664525
mov DWORD PTR _rand_state, eax
mov ecx, DWORD PTR _rand_state
add ecx, 1013904223 ; 3c6ef35fH
mov DWORD PTR _rand_state, ecx
mov eax, DWORD PTR _rand_state
and eax, 32767 ; 00007fffH
pop ebp
ret 0
_rand ENDP
_TEXT ENDS
1.29.2 x64
La version x64 est essentiellement la même et utilise des registres 32-bit au lieu de
64-bit (car nous travaillons avec des valeurs de type int ici).
Mais my_srand() prend son argument en entrée dans le registre ECX plutôt que sur
la pile:
init$ = 8
my_srand PROC
; ECX = argument en entrée
mov DWORD PTR rand_state, ecx
ret 0
my_srand ENDP
_TEXT SEGMENT
my_rand PROC
imul eax, DWORD PTR rand_state, 1664525 ; 0019660dH
add eax, 1013904223 ; 3c6ef35fH
mov DWORD PTR rand_state, eax
and eax, 32767 ; 00007fffH
ret 0
my_rand ENDP
_TEXT ENDS
437
Listing 1.324: avec optimisation Keil 6/2013 (Mode ARM)
my_srand PROC
LDR r1,|L0.52| ; charger un pointeur sur rand_state
STR r0,[r1,#0] ; sauver rand_state
BX lr
ENDP
my_rand PROC
LDR r0,|L0.52| ; charger un pointeur sur rand_state
LDR r2,|L0.56| ; charger RNG_a
LDR r1,[r0,#0] ; charger rand_state
MUL r1,r2,r1
LDR r2,|L0.60| ; charger RNG_c
ADD r1,r1,r2
STR r1,[r0,#0] ; sauver rand_state
; AND avec 0x7FFF:
LSL r0,r1,#17
LSR r0,r0,#17
BX lr
ENDP
|L0.52|
DCD ||.data||
|L0.56|
DCD 0x0019660d
|L0.60|
DCD 0x3c6ef35f
rand_state
DCD 0x00000000
Il n’est pas possible d’intégrer une constante 32-bit dans des instructions ARM, donc
Keil doit les stocker à l’extérieur et en outre les charger. Une chose intéressante est
qu’il n’est pas possible non plus d’intégrer la constante 0x7FFF. Donc ce que fait Keil
est de décaler rand_state vers la gauche de 17 bits et ensuite la décale de 17 bits
vers la droite. Ceci est analogue à la déclaration (rand_state ≪ 17) ≫ 17 en C/C++. Il
semble que ça soit une opération inutile, mais ce qu’elle fait est de mettre à zéro
les 17 bits hauts, laissant les 15 bits bas inchangés, et c’est notre but après tout.
Keil avec optimisation pour le mode Thumb génère essentiellement le même code.
1.29.4 MIPS
Listing 1.325: avec optimisation GCC 4.4.5 (IDA)
my_srand :
; stocker $a0 dans rand_state:
lui $v0, (rand_state >> 16)
jr $ra
sw $a0, rand_state
438
my_rand :
; charger rand_state dans $v0:
lui $v1, (rand_state >> 16)
lw $v0, rand_state
or $at, $zero ; slot de délai de branchement
; multiplier rand_state dans $v0 par 1664525 (RNG_a) :
sll $a1, $v0, 2
sll $a0, $v0, 4
addu $a0, $a1, $a0
sll $a1, $a0, 6
subu $a0, $a1, $a0
addu $a0, $v0
sll $a1, $a0, 5
addu $a0, $a1
sll $a0, 3
addu $v0, $a0, $v0
sll $a0, $v0, 2
addu $v0, $a0
; ajouter 1013904223 (RNG_c)
; l'instruction LI est la fusion par IDA de LUI et ORI
li $a0, 0x3C6EF35F
addu $v0, $a0
; stocker dans rand_state:
sw $v0, (rand_state & 0xFFFF)($v1)
jr $ra
andi $v0, 0x7FFF ; slot de délai de branchement
int f (int a)
{
return a*RNG_a ;
}
439
jr $ra
addu $v0, $a0, $v0 ; branch delay slot
En effet!
Relocations MIPS
Nous allons nous concentrer sur comment les opérations comme charger et stocker
dans la mémoire fonctionnent.
Les listings ici sont produits par IDA, qui cache certains détails.
Nous allons lancer objdump deux fois: pour obtenir le listing désassemblé et aussi
la liste des relogements:
...
00000000 <my_srand> :
0: 3c020000 lui v0,0x0
4: 03e00008 jr ra
8: ac440000 sw a0,0(v0)
0000000c <my_rand> :
c : 3c030000 lui v1,0x0
10: 8c620000 lw v0,0(v1)
14: 00200825 move at,at
18: 00022880 sll a1,v0,0x2
1c : 00022100 sll a0,v0,0x4
20: 00a42021 addu a0,a1,a0
24: 00042980 sll a1,a0,0x6
28: 00a42023 subu a0,a1,a0
2c : 00822021 addu a0,a0,v0
30: 00042940 sll a1,a0,0x5
34: 00852021 addu a0,a0,a1
38: 000420c0 sll a0,a0,0x3
3c : 00821021 addu v0,a0,v0
40: 00022080 sll a0,v0,0x2
44: 00441021 addu v0,v0,a0
48: 3c043c6e lui a0,0x3c6e
4c : 3484f35f ori a0,a0,0xf35f
50: 00441021 addu v0,v0,a0
54: ac620000 sw v0,0(v1)
58: 03e00008 jr ra
5c : 30427fff andi v0,v0,0x7fff
...
# objdump -r rand_O3.o
...
440
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000000 R_MIPS_HI16 .bss
00000008 R_MIPS_LO16 .bss
0000000c R_MIPS_HI16 .bss
00000010 R_MIPS_LO16 .bss
00000054 R_MIPS_LO16 .bss
...
1.30 Structures
Moyennant quelques ajustements, on peut considérer qu’une structure C/C++ n’est
rien d’autre qu’un ensemble de variables, pas toutes nécessairement du même type,
441
151
et toujours stockées en mémoire côte à côte .
void main()
{
SYSTEMTIME t ;
GetSystemTime (&t) ;
return ;
};
442
movzx edx, WORD PTR _t$[ebp+10] ; wMinute
push edx
movzx eax, WORD PTR _t$[ebp+8] ; wHour
push eax
movzx ecx, WORD PTR _t$[ebp+6] ; wDay
push ecx
movzx edx, WORD PTR _t$[ebp+2] ; wMonth
push edx
movzx eax, WORD PTR _t$[ebp] ; wYear
push eax
push OFFSET $SG78811 ; '%04d-%02d-%02d %02d:%02d:%02d', 0aH, 00H
call _printf
add esp, 28
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
16 octets sont réservés sur la pile pour cette structure, ce qui correspond exactement
à sizeof(WORD)*8. La structure comprend effectivement 8 variables d’un WORD
chacun.
Faites attention au fait que le premier membre de la structure est le champ wYear. On
peut donc considérer que la fonction GetSystemTime()153 reçoit comme argument
un pointeur sur la structure SYSTEMTIME, ou bien qu’elle reçoit un pointeur sur le
champ wYear. Et en fait c’est exactement la même chose! GetSystemTime() écrit
l’année courante dans à l’adresse du WORD qu’il a reçu, avance de 2 octets, écrit le
mois courant et ainsi de suite.
443
OllyDbg
Compilons cet exemple avec MSVC 2010 et les options /GS- /MD, puis exécutons le
avec OllyDbg.
Ouvrons la fenêtre des données et celle de la pile à l’adresse du premier argument
fourni à la fonction GetSystemTime(), puis attendons que cette fonction se termine.
Nous constatons :
Sur mon ordinateur, le résultat de l’appel à la fonction est 9 décembre 2014, 22:29:52:
Chaque paire d’octets représente l’un des champs de la structure. Puisque nous
sommes en mode petit-boutien l’octet de poids faible est situé en premier, suivi de
l’octet de poids fort.
Les valeurs effectivement présentes en mémoire sont donc les suivantes:
444
nombre hexadécimal nombre décimal nom du champ
0x07DE 2014 wYear
0x000C 12 wMonth
0x0002 2 wDayOfWeek
0x0009 9 wDay
0x0016 22 wHour
0x001D 29 wMinute
0x0034 52 wSecond
0x03D4 980 wMilliseconds
Les mêmes valeurs apparaissent dans la fenêtre de la pile, mais elle y sont regrou-
pées sous forme de valeurs 32 bits.
La fonction printf() utilise les valeurs qui lui sont nécessaires et les affiche à la
console.
Bien que certaines valeurs telles que (wDayOfWeek et wMilliseconds) ne soient pas
affichées par printf(), elles sont bien présentes en mémoire, prêtes à être utili-
sées.
Le fait que les champs d’une structure ne sont que des variables situées côte-à-côte
peut être aisément démontré de la manière suivante. Tout en conservant à l’esprit
la description de la structure SYSTEMTIME, il est possible de réécrire cet exemple
simple de la manière suivante:
#include <windows.h>
#include <stdio.h>
void main()
{
WORD array[8];
GetSystemTime (array) ;
return ;
};
445
_array$ = -16 ; size = 16
_main PROC
push ebp
mov ebp, esp
sub esp, 16
lea eax, DWORD PTR _array$[ebp]
push eax
call DWORD PTR __imp__GetSystemTime@4
movzx ecx, WORD PTR _array$[ebp+12] ; wSecond
push ecx
movzx edx, WORD PTR _array$[ebp+10] ; wMinute
push edx
movzx eax, WORD PTR _array$[ebp+8] ; wHoure
push eax
movzx ecx, WORD PTR _array$[ebp+6] ; wDay
push ecx
movzx edx, WORD PTR _array$[ebp+2] ; wMonth
push edx
movzx eax, WORD PTR _array$[ebp] ; wYear
push eax
push OFFSET $SG78573 ; '%04d-%02d-%02d %02d:%02d:%02d', 0aH, 00H
call _printf
add esp, 28
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
void main()
{
446
SYSTEMTIME *t ;
GetSystemTime (t) ;
free (t) ;
return ;
};
Compilons cet exemple en utilisant l’option (/Ox) qui facilitera nos observations.
Puisque sizeof(SYSTEMTIME) = 16 c’est aussi le nombre d’octets qui doit être al-
loué par malloc(). Celui-ci renvoie dans le registre EAX un pointeur vers un bloc
mémoire fraîchement alloué. Puis le pointeur est copié dans le registre ESI. La fonc-
tion win32 GetSystemTime() prend soin que la valeur de ESI soit la même à l’issue
447
de la fonction que lors de son appel. C’est pourquoi nous pouvons continuer à l’uti-
liser après sans avoir eu besoin de le sauvegarder.
Tiens, une nouvelle instruction —MOVZX (Move with Zero eXtend). La plupart du
temps, elle peut être utilisée comme MOVSX. La différence est qu’elle positionne sys-
tématiquement les bits supplémentaires à 0. Elle est utilisée ici car printf() attend
une valeur sur 32 bits et que nous ne disposons que d’un WORD dans la structure —
c’est à dire une valeur non signée sur 16 bits. Il nous faut donc forcer à zéro les bits
16 à 31 lorsque le WORD est copié dans un int, sinon nous risquons de récupérer
des bits résiduels de la précédente opération sur le registre.
Dans cet exemple, il reste possible de représenter la structure sous forme d’un ta-
bleau de 8 WORDs:
#include <windows.h>
#include <stdio.h>
void main()
{
WORD *t ;
GetSystemTime (t) ;
free (t) ;
return ;
};
_main PROC
push esi
push 16
call _malloc
add esp, 4
mov esi, eax
push esi
call DWORD PTR __imp__GetSystemTime@4
movzx eax, WORD PTR [esi+12]
movzx ecx, WORD PTR [esi+10]
movzx edx, WORD PTR [esi+8]
push eax
movzx eax, WORD PTR [esi+6]
push ecx
448
movzx ecx, WORD PTR [esi+2]
push edx
movzx edx, WORD PTR [esi]
push eax
push ecx
push edx
push OFFSET $SG78594
call _printf
push esi
call _free
add esp, 32
xor eax, eax
pop esi
ret 0
_main ENDP
Encore une fois nous obtenons un code qu’il n’est pas possible de discerner du pré-
cédent.
Et encore une fois, vous n’avez pas intérêt à faire cela, sauf si vous savez exactement
ce que vous faites.
void main()
{
struct tm t ;
time_t unix_time ;
unix_time=time(NULL) ;
449
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 40h
mov dword ptr [esp], 0 ; premier argument de la fonction time()
call time
mov [esp+3Ch], eax
lea eax, [esp+3Ch] ; récupération de la valeur retournée par
time()
lea edx, [esp+10h] ; la structure tm est à l'adresse ESP+10h
mov [esp+4], edx ; passons le pointeur vers la structure begin
mov [esp], eax ; ... et le pointeur retourné par time()
call localtime_r
mov eax, [esp+24h] ; tm_year
lea edx, [eax+76Ch] ; edx=eax+1900
mov eax, offset format ; "Year: %d\n"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+20h] ; tm_mon
mov eax, offset aMonthD ; "Month: %d\n"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+1Ch] ; tm_mday
mov eax, offset aDayD ; "Day: %d\n"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+18h] ; tm_hour
mov eax, offset aHourD ; "Hour: %d\n"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+14h] ; tm_min
mov eax, offset aMinutesD ; "Minutes: %d\n"
mov [esp+4], edx
mov [esp], eax
call printf
mov edx, [esp+10h]
mov eax, offset aSecondsD ; "Seconds: %d\n"
mov [esp+4], edx ; tm_sec
mov [esp], eax
call printf
leave
retn
main endp
IDA n’a pas utilisé le nom des variables locales pour identifier les éléments de la pile.
Mais comme nous sommes déjà des rétro ingénieurs expérimentés :-) nous pouvons
nous en passer dans cet exemple simple.
Notez l’instruction lea edx, [eax+76Ch] —qui incrémente la valeur de EAX de 0x76C
450
(1900) sans modifier aucun des drapeaux. Référez-vous également à la section au
sujet de LEA ( .1.6 on page 1345).
GDB
154
Tentons de charger l’exemple dans GDB :
Nous retrouvons facilement notre structure dans la pile. Commençons par observer
sa définition dans time.h :
154. Le résultat date est légèrement modifié pour les besoins de la démonstration, car il est bien en-
tendu impossible d’exécuter GDB aussi rapidement.
451
Faites attention au fait qu’ici les champs sont des int sur 32 bits et non des WORD
comme dans SYSTEMTIME.
Voici donc les champs de notre structure tels qu’ils sont présents dans la pile:
0xbffff0dc : 0x080484c3 0x080485c0 0x000007de 0x00000000
0xbffff0ec : 0x08048301 0x538c93ed 0x00000025 sec 0x0000000a ⤦
Ç min
0xbffff0fc : 0x00000012 hour 0x00000002 mday 0x00000005 mon 0x00000072 ⤦
Ç year
0xbffff10c : 0x00000001 wday 0x00000098 yday 0x00000001 isdst 0x00002a30
0xbffff11c : 0x0804b090 0x08048530 0x00000000 0x00000000
ARM
Même exemple:
PUSH {LR}
MOVS R0, #0 ; timer
SUB SP, SP, #0x34
BL time
STR R0, [SP,#0x38+timer]
MOV R1, SP ; tp
ADD R0, SP, #0x38+timer ; timer
BL localtime_r
452
LDR R1, =0x76C
LDR R0, [SP,#0x38+var_24]
ADDS R1, R0, R1
ADR R0, aYearD ; "Year: %d\n"
BL __2printf
LDR R1, [SP,#0x38+var_28]
ADR R0, aMonthD ; "Month: %d\n"
BL __2printf
LDR R1, [SP,#0x38+var_2C]
ADR R0, aDayD ; "Day: %d\n"
BL __2printf
LDR R1, [SP,#0x38+var_30]
ADR R0, aHourD ; "Hour: %d\n"
BL __2printf
LDR R1, [SP,#0x38+var_34]
ADR R0, aMinutesD ; "Minutes: %d\n"
BL __2printf
LDR R1, [SP,#0x38+var_38]
ADR R0, aSecondsD ; "Seconds: %d\n"
BL __2printf
ADD SP, SP, #0x34
POP {PC}
PUSH {R7,LR}
MOV R7, SP
SUB SP, SP, #0x30
MOVS R0, #0 ; time_t *
BLX _time
ADD R1, SP, #0x38+var_34 ; struct tm *
STR R0, [SP,#0x38+var_38]
MOV R0, SP ; time_t *
BLX _localtime_r
LDR R1, [SP,#0x38+var_34.tm_year]
MOV R0, 0xF44 ; "Year: %d\n"
ADD R0, PC ; char *
ADDW R1, R1, #0x76C
BLX _printf
LDR R1, [SP,#0x38+var_34.tm_mon]
MOV R0, 0xF3A ; "Month: %d\n"
ADD R0, PC ; char *
453
BLX _printf
LDR R1, [SP,#0x38+var_34.tm_mday]
MOV R0, 0xF35 ; "Day: %d\n"
ADD R0, PC ; char *
BLX _printf
LDR R1, [SP,#0x38+var_34.tm_hour]
MOV R0, 0xF2E ; "Hour: %d\n"
ADD R0, PC ; char *
BLX _printf
LDR R1, [SP,#0x38+var_34.tm_min]
MOV R0, 0xF28 ; "Minutes: %d\n"
ADD R0, PC ; char *
BLX _printf
LDR R1, [SP,#0x38+var_34]
MOV R0, 0xF25 ; "Seconds: %d\n"
ADD R0, PC ; char *
BLX _printf
ADD SP, SP, #0x30
POP {R7,PC}
...
MIPS
454
13 var_4 = -4
14
15 lui $gp, (__gnu_local_gp >> 16)
16 addiu $sp, -0x50
17 la $gp, (__gnu_local_gp & 0xFFFF)
18 sw $ra, 0x50+var_4($sp)
19 sw $gp, 0x50+var_40($sp)
20 lw $t9, (time & 0xFFFF)($gp)
21 or $at, $zero ; Gaspillage par NOP du délai de branchement
22 jalr $t9
23 move $a0, $zero ; Gaspillage par NOP du délai de branchement
24 lw $gp, 0x50+var_40($sp)
25 addiu $a0, $sp, 0x50+var_38
26 lw $t9, (localtime_r & 0xFFFF)($gp)
27 addiu $a1, $sp, 0x50+seconds
28 jalr $t9
29 sw $v0, 0x50+var_38($sp) ; Utilisation du délai de branchement
30 lw $gp, 0x50+var_40($sp)
31 lw $a1, 0x50+year($sp)
32 lw $t9, (printf & 0xFFFF)($gp)
33 la $a0, $LC0 # "Year: %d\n"
34 jalr $t9
35 addiu $a1, 1900 ; branch delay slot
36 lw $gp, 0x50+var_40($sp)
37 lw $a1, 0x50+month($sp)
38 lw $t9, (printf & 0xFFFF)($gp)
39 lui $a0, ($LC1 >> 16) # "Month: %d\n"
40 jalr $t9
41 la $a0, ($LC1 & 0xFFFF) # "Month: %d\n" ; Utilisation du délai de
branchement
42 lw $gp, 0x50+var_40($sp)
43 lw $a1, 0x50+day($sp)
44 lw $t9, (printf & 0xFFFF)($gp)
45 lui $a0, ($LC2 >> 16) # "Day: %d\n"
46 jalr $t9
47 la $a0, ($LC2 & 0xFFFF) # "Day: %d\n" ; Utilisation du délai de
branchement
48 lw $gp, 0x50+var_40($sp)
49 lw $a1, 0x50+hour($sp)
50 lw $t9, (printf & 0xFFFF)($gp)
51 lui $a0, ($LC3 >> 16) # "Hour: %d\n"
52 jalr $t9
53 la $a0, ($LC3 & 0xFFFF) # "Hour: %d\n" ; Utilisation du délai de
branchement
54 lw $gp, 0x50+var_40($sp)
55 lw $a1, 0x50+minutes($sp)
56 lw $t9, (printf & 0xFFFF)($gp)
57 lui $a0, ($LC4 >> 16) # "Minutes: %d\n"
58 jalr $t9
59 la $a0, ($LC4 & 0xFFFF) # "Minutes: %d\n" ; Utilisation du délai de
branchement
60 lw $gp, 0x50+var_40($sp)
61 lw $a1, 0x50+seconds($sp)
62 lw $t9, (printf & 0xFFFF)($gp)
63 lui $a0, ($LC5 >> 16) # "Seconds: %d\n"
455
64 jalr $t9
65 la $a0, ($LC5 & 0xFFFF) # "Seconds: %d\n" ; Utilisation du délai de
branchement
66 lw $ra, 0x50+var_4($sp)
67 or $at, $zero ; Gaspillage par NOP du délai de branchement
68 jr $ra
69 addiu $sp, 0x50
70
71 $LC0 : .ascii "Year : %d\n"<0>
72 $LC1 : .ascii "Month : %d\n"<0>
73 $LC2 : .ascii "Day : %d\n"<0>
74 $LC3 : .ascii "Hour : %d\n"<0>
75 $LC4 : .ascii "Minutes : %d\n"<0>
76 $LC5 : .ascii "Seconds : %d\n"<0>
Afin d’illustrer le fait qu’une structure n’est qu’une collection de variables située
côte-à-côte, retravaillons notre exemple sur la base de la définition de la structure
tm : listado.1.336.
#include <stdio.h>
#include <time.h>
void main()
{
int tm_sec, tm_min, tm_hour, tm_mday, tm_mon, tm_year, tm_wday, tm_yday⤦
Ç , tm_isdst ;
time_t unix_time ;
unix_time=time(NULL) ;
N.B. Le pointeur vers le champ tm_sec est passé comme argument de la fonction
localtime_r, en tant que premier élément de la «structure ».
Le compilateur nous alerte:
456
Listing 1.340: GCC 4.7.3
GCC_tm2.c : In function 'main' :
GCC_tm2.c :11:5: warning : passing argument 2 of 'localtime_r' from ⤦
Ç incompatible pointer type [enabled by default]
In file included from GCC_tm2.c :2:0:
/usr/include/time.h :59:12: note : expected 'struct tm *' but argument is ⤦
Ç of type 'int *'
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 30h
call __main
mov [esp+30h+var_30], 0 ; arg 0
call time
mov [esp+30h+unix_time], eax
lea eax, [esp+30h+tm_sec]
mov [esp+30h+var_2C], eax
lea eax, [esp+30h+unix_time]
mov [esp+30h+var_30], eax
call localtime_r
mov eax, [esp+30h+tm_year]
add eax, 1900
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aYearD ; "Year: %d\n"
call printf
mov eax, [esp+30h+tm_mon]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aMonthD ; "Month: %d\n"
call printf
mov eax, [esp+30h+tm_mday]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aDayD ; "Day: %d\n"
call printf
mov eax, [esp+30h+tm_hour]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aHourD ; "Hour: %d\n"
457
call printf
mov eax, [esp+30h+tm_min]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aMinutesD ; "Minutes: %d\n"
call printf
mov eax, [esp+30h+tm_sec]
mov [esp+30h+var_2C], eax
mov [esp+30h+var_30], offset aSecondsD ; "Seconds: %d\n"
call printf
leave
retn
main endp
Ce code est similaire à ce que nous avons déjà vue et il n’est pas possible de dire si
le code source original contenait une structure ou un groupe de variables.
Et cela fonctionne. Mais encore une fois ce n’est pas une bonne pratique.
En règle générale les compilateurs en l’absence d’optimisation allouent les variables
sur la pile dans le même ordre que celui dans lequel elles ont été déclarées dans le
code source. Pour autant, ce n’est pas une garantie.
Par ailleurs certains compilateurs peuvent vous avertir que les variables tm_year,
tm_mon, tm_mday, tm_hour, tm_min n’ont pas été initialisées avant leur utilisation,
mais resteront muets au sujet de tm_sec
Le compilateur lui non plus ne sait pas qu’ils sont appelés à être initialisés par la
fonction localtime_r().
Nous avons chois cet exemple car tous les champs de la structure sont de type int.
Tout ceci ne fonctionnerait pas sir les champs de la structure étaient des WORD de
16 bits, tel que dans le cas de la structure SYSTEMTIME structure—GetSystemTime()
les initialiserait de manière erronée (puisque les variables locales sont alignées sur
des frontières de 32bits). Vous en saurez plus à ce sujet dans la prochaine section:
«Organisation des champs dans la structure » ( 1.30.4 on page 462).
Une structure n’est donc qu’un groupe de variables disposées côte-à-côte en mé-
moire. Nous pouvons dire que la structure est une instruction adressée au compi-
lateur et l’obligeant à conserver le groupement des variables. Cela étant dans les
toutes premières versions du langage C (avant 1972), la notion de structure n’exis-
tait pas encore [Dennis M. Ritchie, The development of the C language, (1993)]155 .
Pas d’exemple de débogage ici. Le comportement est toujours le même.
#include <stdio.h>
#include <time.h>
void main()
{
458
struct tm t ;
time_t unix_time ;
int i ;
unix_time=time(NULL) ;
Nous n’avons qu’à utiliser l’opérateur cast pour transformer notre pointeur vers une
structure en un tableau de int’s. Et cela fonctionne ! Nous avons exécuté l’exemple
à 23h51m45s le 26 juillet 2014.
0x0000002D (45)
0x00000033 (51)
0x00000017 (23)
0x0000001A (26)
0x00000006 (6)
0x00000072 (114)
0x00000006 (6)
0x000000CE (206)
0x00000001 (1)
Les variables sont dans le même ordre que celui dans lequel elles apparaissent dans
la définition de la structure: 1.336 on page 451.
Nous avons effectué la compilation avec:
459
loc_80483D8 :
; EBX pointe sur la structure,
; ESI pointe sur la fin de celle-ci.
mov eax, [ebx] ; get 32-bit word from array
add ebx, 4 ; prochain champ de la structure
mov dword ptr [esp+4], offset a0x08xD ; "0x%08X (%d)\n"
mov dword ptr [esp], 1
mov [esp+0Ch], eax ; passage des arguments à printf()
mov [esp+8], eax
call ___printf_chk
cmp ebx, esi ; Avons-nous atteint la fin de la structure ?
jnz short loc_80483D8 ; non - alors passons à la prochaine valeur
lea esp, [ebp-8]
pop ebx
pop esi
pop ebp
retn
main endp
En fait, l’espace dans la pile est tout d’abord traité comme une structure, puis ensuite
comme un tableau.
Le pointeur sur le tableau permet même de modifier les champs de la structure.
Et encore une fois cette manière de procéder est extrêmement douteuse et pas du
tout recommandée pour l’écriture d’un code qui atterrira en production.
Exercice
Nous pouvons aller plus loin. Utilisons l’opérateur cast pour transformer le pointeur
en un tableau d’octets, puis affichons son contenu:
#include <stdio.h>
#include <time.h>
void main()
{
struct tm t ;
time_t unix_time ;
int i, j ;
unix_time=time(NULL) ;
460
for (j=0; j<4; j++)
printf ("0x%02X ", ((unsigned char*)&t)[i*4+j]) ;
printf ("\n") ;
};
};
Cet exemple a été exécuté à 23h51m45s le 26 juillet 2014 156 . Les valeurs sont
identiques à celles du précédent affichage ( 1.30.3 on page 459), et bien entendu
l’octet de poids faible figure en premier puisque nous sommes sur une architecture
de type little-endian ( 2.8 on page 601).
Listing 1.343: avec optimisation GCC 4.8.1
main proc near
push ebp
mov ebp, esp
push edi
push esi
push ebx
and esp, 0FFFFFFF0h
sub esp, 40h
mov dword ptr [esp], 0 ; timer
lea esi, [esp+14h]
call _time
lea edi, [esp+38h] ; struct end
mov [esp+4], esi ; tp
mov [esp+10h], eax
lea eax, [esp+10h]
mov [esp], eax ; timer
call _localtime_r
lea esi, [esi+0] ; NOP
; ESI pointe sur la structure située sur la pile.
; EDI pointe sur la fin de la structure.
loc_8048408 :
xor ebx, ebx ; j=0
loc_804840A :
movzx eax, byte ptr [esi+ebx] ; load byte
add ebx, 1 ; j=j+1
mov dword ptr [esp+4], offset a0x02x ; "0x%02X "
mov dword ptr [esp], 1
156. Les dates et heures sont les mêmes dans tous les exemples. Elles ont été éditées pour la clarté
de la démonstration.
461
mov [esp+8], eax ; Fourniture à printf() des octets qui ont
été chargés
call ___printf_chk
cmp ebx, 4
jnz short loc_804840A
; Imprime un retour chariot (CR)
mov dword ptr [esp], 0Ah ; c
add esi, 4
call _putchar
cmp esi, edi ; Avons nous atteint la fin de la structure ?
jnz short loc_8048408 ; j=0
lea esp, [ebp-0Ch]
pop ebx
pop esi
pop edi
pop ebp
retn
main endp
struct s
{
char a ;
int b ;
char c ;
int d ;
};
void f(struct s s)
{
printf ("a=%d ; b=%d ; c=%d ; d=%d\n", s.a, s.b, s.c, s.d) ;
};
int main()
{
struct s tmp ;
tmp.a=1;
tmp.b=2;
tmp.c=3;
tmp.d=4;
f(tmp) ;
};
Nous avons deux champs de type char (occupant chacun un octet) et deux autres —
de type int (comportant 4 octets chacun).
462
x86
463
Nous passons la structure comme un tout, mais en réalité nous pouvons constater
que la structure est copiée dans un espace temporaire. De l’espace est réservé pour
cela ligne 10 et les 4 champs sont copiées par les lignes de 12 … 19), puis le pointeur
sur l’espace temporaire est passé à la fonction.
La structure est recopiée au cas où la fonction f() viendrait à en modifier le contenu.
Si cela arrive, la copie de la structure qui existe dans main() restera inchangée.
Nous pourrions également utiliser des pointeurs C/C++. Le résulta demeurerait le
même, sans qu’il soit nécessaire de procéder à la copie.
Nous observons que l’adresse de chaque champ est alignée sur un multiple de 4
octets. C’est pourquoi chaque char occupe 4 octets (de même qu’un int). Pourquoi
en est-il ainsi? La réponse se situe au niveau de la CPU. Il est plus facile et performant
pour elle d’accéder la mémoire et de gérer le cache de données en utilisant des
adresses alignées.
En revanche ce n’est pas très économique en terme d’espace.
Tentons maintenant une compilation avec l’option (/Zp1) (/Zp[n] indique qu’il faut
compresser les structures en utilisant des frontières tous les n octets).
464
31 push eax
32 movsx ecx, BYTE PTR _s$[ebp+5]
33 push ecx
34 mov edx, DWORD PTR _s$[ebp+1]
35 push edx
36 movsx eax, BYTE PTR _s$[ebp]
37 push eax
38 push OFFSET $SG3842
39 call _printf
40 add esp, 20
41 pop ebp
42 ret 0
43 ?f@@YAXUs@@@Z ENDP ; f
La structure n’occupe plus que 10 octets et chaque valeur de type char n’occupe
plus qu’un octet. Quelles sont les conséquences ? Nous économisons de la place au
prix d’un accès à ces champs moins rapide que ne pourrait le faire la CPU.
La structure est également copiée dans main(). Cette opération ne s’effectue pas
champ par champ mais par blocs en utilisant trois instructions MOV. Et pourquoi pas
4?
Tout simplement parce que le compilateur a décidé qu’il était préférable d’effectuer
la copie en utilisant 3 paires d’instructions MOV plutôt que de copier deux mots de
32 bits puis 2 fois un octet ce qui aurait nécessité 4 paires d’instructions MOV.
Ce type d’implémentation de la copie qui repose sur les instructions MOV plutôt que
sur l’appel à la fonction memcpy() est très répandu. La raison en est que pour de
petits blocs, cette approche est plus rapide qu’un appel à memcpy() : 3.14.1 on
page 671.
Comme vous pouvez le deviner, si la structure est utilisée dans de nombreux fichiers
sources et objets, ils doivent tous être compilés avec la même convention de com-
pactage de la structure.
Au delà de l’option MSVC /Zp qui permet de définir l’alignement des champs des
structures, il existe également l’option du compilateur #pragma pack qui peut être
utilisée directement dans le code source. Elle est supportée aussi bien par MSVC157 que
pars GCC158 .
Revenons à la structure SYSTEMTIME qui contient des champs de 16 bits. Comment
notre compilateur sait-il les aligner sur des frontières de 1 octet ?
Le fichier WinNT.h contient ces instructions:
et celles-ci:
465
#include "pshpack4.h" // L'alignement sur 4 octets est la
valeur par défaut
466
OllyDbg et les champs alignés par défaut
Examinons dans OllyDbg notre exemple lorsque les champs sont alignés par défaut
sur des frontières de 4 octets:
467
OllyDbg et les champs alignés sur des frontières de 1 octet
Les choses sont beaucoup plus simples ici. Les 4 champs occupent 10 octets et les
valeurs sont stockées côte-à-côte.
ARM
.text :00000280 f
.text :00000280
.text :00000280 var_18 = -0x18
.text :00000280 a = -0x14
.text :00000280 b = -0x10
.text :00000280 c = -0xC
.text :00000280 d = -8
.text :00000280
.text :00000280 0F B5 PUSH {R0-R3,LR}
.text :00000282 81 B0 SUB SP, SP, #4
.text :00000284 04 98 LDR R0, [SP,#16] ; d
.text :00000286 02 9A LDR R2, [SP,#8] ; b
.text :00000288 00 90 STR R0, [SP]
.text :0000028A 68 46 MOV R0, SP
468
.text :0000028C 03 7B LDRB R3, [R0,#12] ; c
.text :0000028E 01 79 LDRB R1, [R0,#4] ; a
.text :00000290 59 A0 ADR R0, aADBDCDDD ; "a=%d; b=%d;
c=%d; d=%d\n"
.text :00000292 05 F0 AD FF BL __2printf
.text :00000296 D2 E6 B exit
Rappelons-nous que c’est une structure qui est passée ici et non pas un pointeur
vers une structure. Comme les 4 premiers arguments d’une fonction sont passés
dans les registres sur les processeurs ARM, les champs de la structure sont passés
dans les registres R0-R3.
LDRB charge un octet présent en mémoire et l’étend sur 32bits en prenant en compte
son signe. Cette opération est similaire à celle effectuée par MOVSX dans les archi-
tectures x86. Elle est utilisée ici pour charger les champs a et c de la structure.
Un autre détail que nous remarquons aisément est que la fonction ne s’achève pas
sur un épilogue qui lui est propre. A la place, il y a un saut vers l’épilogue d’une autre
fonction! Qui plus est celui d’une fonction très différente sans aucun lien avec la
nôtre. Cependant elle possède exactement le même épilogue, probablement parce
qu’elle accepte utilise elle aussi 5 variables locales (5 ∗ 4 = 0x14).
De plus elle est située à une adresse proche.
En réalité, peut importe l’épilogue qui est utilisé du moment que le fonctionnement
est celui attendu.
Il semble donc que le compilateur Keil décide de réutiliser à des fins d’économie un
fragment d’une autre fonction. Notre épilogue aurait nécessité 4 octets. L’instruction
de saut n’en utilise que 2.
PUSH {R7,LR}
MOV R7, SP
SUB SP, SP, #4
MOV R9, R1 ; b
MOV R1, R0 ; a
MOVW R0, #0xF10 ; "a=%d; b=%d; c=%d; d=%d\n"
SXTB R1, R1 ; prepare a
MOVT.W R0, #0
STR R3, [SP,#0xC+var_C] ; place d to stack for printf()
ADD R0, PC ; format-string
SXTB R3, R2 ; prepare c
MOV R2, R9 ; b
BLX _printf
ADD SP, SP, #4
POP {R7,PC}
469
SXTB (Signed Extend Byte) est similaire à MOVSX pour les architectures x86. Pour le
reste—c’est identique.
MIPS
Les champs de la structure sont fournis dans les registres $A0..$A3 puis transfor-
470
mé dans les registres $A1..$A3 pour l’utilisation par printf(), tandis que le 4ème
champ (provenant de $A3) est passé sur la pile en utilisant l’instruction SW.
Mais à quoi servent ces deux instructions SRA («Shift Word Right Arithmetic ») lors
de la préparation des champs char ?
MIPS est une architecture grand-boutien (big-endian) par défaut 2.8 on page 601,
de même que la distribution Debian Linux que nous utilisons.
En conséquence, lorsqu’un octet est stocké dans un emplacement 32bits d’une struc-
ture, ils occupent les bits 31..24 bits.
Quand une variable char doit être étendue en une valeur sur 32 bits, elle doit tout
d’abord être décalée vers la droite de 24 bits.
char étant un type signé, un décalage arithmétique est utilisé ici, à la place d’un
décalage logique.
Un dernier mot
Passer une structure comme argument d’une fonction (plutôt que de passer un poin-
teur sur cette structure) revient à passer chaque champ de la structure individuelle-
ment.
Si les champs de la structure utilisent l’alignement par défaut, la fonction f() peut
être réécrite ainsi:
void f(char a, int b, char c, int d)
{
printf ("a=%d ; b=%d ; c=%d ; d=%d\n", a, b, c, d) ;
};
struct inner_struct
{
int a ;
int b ;
};
struct outer_struct
{
char a ;
int b ;
struct inner_struct c ;
char d ;
int e ;
471
};
int main()
{
struct outer_struct s ;
s.a=1;
s.b=2;
s.c.a=100;
s.c.b=101;
s.d=3;
s.e=4;
f(s) ;
};
…dans ce cas, l’ensemble des champs de inner_struct doivent être situés entre
les champs a,b et d,e de outer_struct.
Compilons (MSVC 2010) :
_TEXT SEGMENT
_s$ = 8
_f PROC
mov eax, DWORD PTR _s$[esp+16]
movsx ecx, BYTE PTR _s$[esp+12]
mov edx, DWORD PTR _s$[esp+8]
push eax
mov eax, DWORD PTR _s$[esp+8]
push ecx
mov ecx, DWORD PTR _s$[esp+8]
push edx
movsx edx, BYTE PTR _s$[esp+8]
push eax
push ecx
push edx
push OFFSET $SG2802 ; 'a=%d; b=%d; c.a=%d; c.b=%d; d=%d; e=%d'
call _printf
add esp, 28
ret 0
_f ENDP
_s$ = -24
_main PROC
sub esp, 24
push ebx
472
push esi
push edi
mov ecx, 2
sub esp, 24
mov eax, esp
; depuis ce moment, EAX est synonyme de ESP:
mov BYTE PTR _s$[esp+60], 1
mov ebx, DWORD PTR _s$[esp+60]
mov DWORD PTR [eax], ebx
mov DWORD PTR [eax+4], ecx
lea edx, DWORD PTR [ecx+98]
lea esi, DWORD PTR [ecx+99]
lea edi, DWORD PTR [ecx+2]
mov DWORD PTR [eax+8], edx
mov BYTE PTR _s$[esp+76], 3
mov ecx, DWORD PTR _s$[esp+76]
mov DWORD PTR [eax+12], esi
mov DWORD PTR [eax+16], ecx
mov DWORD PTR [eax+20], edi
call _f
add esp, 24
pop edi
pop esi
xor eax, eax
pop ebx
add esp, 24
ret 0
_main ENDP
Un point troublant est qu’en observant le code assembleur généré, nous n’avons
aucun indice qui laisse penser qu’il existe une structure imbriquée! Nous pouvons
donc dire que les structures imbriquées sont fusionnées avec leur conteneur pour
former une seule structure linear ou one-dimensional.
Bien entendu, si nous remplaçons la déclaration struct inner_struct c; par struct
inner_struct *c; (en introduisant donc un pointeur) la situation sera totalement
différente.
473
OllyDbg
474
3:0 (4 bits) Stepping
7:4 (4 bits) Modèle
11:8 (4 bits) Famille
13:12 (2 bits) Type de processeur
19:16 (4 bits) Sous-modèle
27:20 (8 bits) Sous-famille
MSVC 2010 fourni une macro CPUID, qui est absente de GCC 4.4.1. Tentons donc de
rédiger nous même cette fonction pour une utilisation dans GCC grâce à l’assem-
bleur160 intégré à ce compilateur.
#include <stdio.h>
#ifdef __GNUC__
static inline void cpuid(int code, int *a, int *b, int *c, int *d) {
asm volatile("cpuid" :"=a"(*a),"=b"(*b),"=c"(*c),"=d"(*d) :"a"(code)) ;
}
#endif
#ifdef _MSC_VER
#include <intrin.h>
#endif
struct CPUID_1_EAX
{
unsigned int stepping :4;
unsigned int model :4;
unsigned int family_id :4;
unsigned int processor_type :2;
unsigned int reserved1 :2;
unsigned int extended_model_id :4;
unsigned int extended_family_id :8;
unsigned int reserved2 :4;
};
int main()
{
struct CPUID_1_EAX *tmp ;
int b[4];
#ifdef _MSC_VER
__cpuid(b,1) ;
#endif
#ifdef __GNUC__
cpuid (1, &b[0], &b[1], &b[2], &b[3]) ;
#endif
475
printf ("family_id=%d\n", tmp->family_id) ;
printf ("processor_type=%d\n", tmp->processor_type) ;
printf ("extended_model_id=%d\n", tmp->extended_model_id) ;
printf ("extended_family_id=%d\n", tmp->extended_family_id) ;
return 0;
};
Après que l’instruction CPUID ait rempli les registres EAX/EBX/ECX/EDX, ceux-ci doivent
être recopiés dans le tableau b[]. Nous affectons dont le pointeur de structure
CPUID_1_EAX pour qu’il contienne l’adresse du tableau b[].
En d’autres termes, nous traitons une valeur int comme une structure, puis nous
lisons des bits spécifiques de la structure.
MSVC
476
push edx
push OFFSET $SG15437 ; 'family_id=%d', 0aH, 00H
call _printf
shr esi, 20
and esi, 255
push esi
push OFFSET $SG15440 ; 'extended_family_id=%d', 0aH, 00H
call _printf
add esp, 48
pop esi
add esp, 16
ret 0
_main ENDP
L’instruction SHR va décaler la valeur du registre EAX d’un certain nombre de bits qui
vont être abandonnées. Nous ignorons donc certains des bits de la partie droite.
L’instruction AND ”efface” les bits inutiles sur la gauche, ou en d’autres termes, ne
laisse dans le registre EAX que les bits qui nous intéressent.
477
MSVC + OllyDbg
Chargeons notre exemple dans OllyDbg et voyons quelles valeurs sont présentes
dans EAX/EBX/ECX/EDX après exécution de l’instruction CPUID:
La valeur de EAX est 0x000206A7 (ma CPU est un Intel Xeon E3-1220).
Cette valeur exprimée en binaire vaut 0b00000000000000100000011010100111.
Voici la manière dont les bits sont répartis sur les différents champs:
champ format binaire format décimal
reserved2 0000 0
extended_family_id 00000000 0
extended_model_id 0010 2
reserved1 00 0
processor_id 00 0
family_id 0110 6
model 1010 10
stepping 0111 7
478
GCC
Essayons maintenant une compilation avec GCC 4.4.1 en utilisant l’option -O3.
479
mov [esp+8], esi
mov dword ptr [esp+4], offset unk_80486D0
mov dword ptr [esp], 1
call ___printf_chk
add esp, 18h
xor eax, eax
pop ebx
pop esi
mov esp, ebp
pop ebp
retn
main endp
Le résultat est quasiment identique. Le seul élément notable est que GCC combine
en quelques sortes le calcul de extended_model_id et extended_family_id en un
seul bloc au lieu de les calculer séparément avant chaque appel à printf().
Comme nous l’avons expliqué dans la section traitant de la FPU ( 1.25 on page 284),
les types float et double sont constitués d’un signe, d’un significande (ou fraction) et
d’un exposant. Mais serions nous capable de travailler avec chacun de ces champs
indépendamment? Essayons avec un float.
31 30 23 22 0
( S — signe )
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <memory.h>
struct float_as_struct
{
unsigned int fraction : 23; // fraction
unsigned int exponent : 8; // exposant + 0x3FF
unsigned int sign : 1; // bit de signe
};
480
memcpy (&f, &t, sizeof (float)) ;
return f ;
};
int main()
{
printf ("%f\n", f(1.234)) ;
};
push 4
lea eax, DWORD PTR _f$[ebp]
push eax
lea ecx, DWORD PTR _t$[ebp]
push ecx
call _memcpy
add esp, 12
481
; ajout de la valeur originale de l'exposant avec le nouvel exposant qui
vient d'être calculé:
or ecx, eax
mov DWORD PTR _t$[ebp], ecx
push 4
lea edx, DWORD PTR _t$[ebp]
push edx
lea eax, DWORD PTR _f$[ebp]
push eax
call _memcpy
add esp, 12
Si nous avions compilé avec le flag /Ox il n’y aurait pas d’appel à la fonction memcpy(),
et la variable f serait utilisée directement. Mais la compréhension est facilitée lorsque
l’on s’intéresse à la version non optimisée.
A quoi cela ressemblerait si nous utilisions l’option -O3 avec le compilateur GCC
4.4.1 ?
push ebp
mov ebp, esp
sub esp, 4
mov eax, [ebp+arg_0]
or eax, 80000000h ; positionnement du signe négatif
mov edx, eax
and eax, 807FFFFFh ; Nous ne conservons que le signe et le
signifiant dans EAX
shr edx, 23 ; Préparation de l'exposant
add edx, 2 ; Ajout de 2
movzx edx, dl ; RAZ de tous les octets dans EDX à
l'exception des bits 7:0
shl edx, 23 ; Décalage du nouvel exposant pour qu'ils
soit à sa place
or eax, edx ; Consolidation du nouvel exposant et de la
valeur originale de l'exposant
mov [ebp+var_4], eax
fld [ebp+var_4]
leave
482
retn
_Z1ff endp
public main
main proc near
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 10h
fld ds :dword_8048614 ; -4.936
fstp qword ptr [esp+8]
mov dword ptr [esp+4], offset asc_8048610 ; "%f\n"
mov dword ptr [esp], 1
call ___printf_chk
xor eax, eax
leave
retn
main endp
La fonction f() est à peu près compréhensible. Par contre ce qui est intéressant
c’est que GCC a été capable de calculer le résultat de f(1.234) durant la compila-
tion malgré tous les triturages des champs de la structure et a directement préparé
l’argument passé à printf() durant la compilation!
1.30.7 Exercices
• http://challenges.re/71
• http://challenges.re/72
483
#include <stdio.h>
Vous compilez votre projet, mais le fichier C où se trouve printer() qui est séparé,
n’est pas recompilé car votre IDE161 ou système de compilation n’a pas d’idée que ce
module dépend d’une définition de structure test. Peut-être car #include <new.h>
est oublié. Ou peut-être que le fichier d’entête new.h est inclus dans printer.c
via un autre fichier d’entête. Le fichier objet n’est pas modifié (l’IDE pense qu’il n’a
pas besoin d’être recompilé), tandis que la fonction setter() est déjà la nouvelle
version. Ces deux fichiers objets (ancien et nouveau) peuvent tôt ou tard être liés
dans un fichier exécutable.
Ensuite, vous le lancez, et le setter() met les 3 champs aux offsets +0, +4 et +8.
Toutefois. printer() connait seulement 2 champs, et les prends aux offsets +0 et
+4 lors de l’affichage.
Ceci conduits à des bogues obscurs et méchants. La raison est que l’IDE ou le sys-
tème de construction ou le Makefile ne savent pas que les deux fichiers C (ou mo-
dules) dépendent de l’entête. Un remède courant est de tout supprimer et de recom-
piler.
Ceci est également vrai pour les classes C++, puisqu’elles fonctionnent tout comme
des structures: 3.21.1 on page 713.
Ceci est une maladie ce C/C++, et une source de critique, oui. De nombreux LPs ont
un meilleur support des modules et interfaces. Mais gardez à l’esprit l’époque de
161. Integrated development environment
484
création du ocmpilateur C: dans les années 70, sur de vieux ordinateurs PDP. Donc
tout a été simplifié à ceci par les créateurs du C.
1.32 Unions
Les unions en C/C++ sont utilisées principalement pour interpréter une variable (ou
un bloc de mémoire) d’un type de données comme une variable d’un autre type de
données.
485
const uint32_t RNG_c=1013904223;
uint32_t RNG_state ; // variable globale
void my_srand(uint32_t i)
{
RNG_state=i ;
};
uint32_t my_rand()
{
RNG_state=RNG_state*RNG_a+RNG_c ;
return RNG_state ;
};
union uint32_t_float
{
uint32_t i ;
float f ;
};
float float_rand()
{
union uint32_t_float tmp ;
tmp.i=my_rand() & 0x007fffff | 0x3F800000 ;
return tmp.f-1;
};
// test
int main()
{
my_srand(time(NULL)) ; // initialisation du PRNG
return 0;
};
x86
__real@3ff0000000000000 DQ 03ff0000000000000r ; 1
tv130 = -4
_tmp$ = -4
?float_rand@@YAMXZ PROC
push ecx
486
call ?my_rand@@YAIXZ
; EAX=valeur pseudo-aléatoire
and eax, 8388607 ; 007fffffH
or eax, 1065353216 ; 3f800000H
; EAX=valeur pseudo-aléatoire & 0x007fffff | 0x3f800000
; la stocker dans la pile locale:
mov DWORD PTR _tmp$[esp+4], eax
; la recharger en tant que nombre à virgule flottante:
fld DWORD PTR _tmp$[esp+4]
; soustraire 1.0:
fsub QWORD PTR __real@3ff0000000000000
; stocker la valeur obtenue dans la pile locale et la recharger:
; ces instructions sont redondantes:
fstp DWORD PTR tv130[esp+4]
fld DWORD PTR tv130[esp+4]
pop ecx
ret 0
?float_rand@@YAMXZ ENDP
_main PROC
push esi
xor eax, eax
call _time
push eax
call ?my_srand@@YAXI@Z
add esp, 4
mov esi, 100
$LL3@main :
call ?float_rand@@YAMXZ
sub esp, 8
fstp QWORD PTR [esp]
push OFFSET $SG4238
call _printf
add esp, 12
dec esi
jne SHORT $LL3@main
xor eax, eax
pop esi
ret 0
_main ENDP
Les noms de fonctions sont étranges ici car cet exemple a été compilé en tant que
C++ et ceci est la modification des noms en C++, nous en parlerons plus loin: 3.21.1
on page 715. Si nous compilons ceci avec MSVC 2012, il utilise des instructions SIMD
pour le FPU, pour en savoir plus: 1.38.5 on page 566.
487
; R0=valeur pseudo-aléatoire
FLDS S0, =1.0
; S0=1.0
BIC R3, R0, #0xFF000000
BIC R3, R3, #0x800000
ORR R3, R3, #0x3F800000
; R3=valeur pseudo-aléatoire & 0x007fffff | 0x3f800000
; copier de R3 vers FPU (registre S15).
; ça se comporte comme une copie bit à bit, pas de conversion faite:
FMSR S15, R3
; soustraire 1.0 et laisser le résultat dans S0:
FSUBS S0, S15, S0
LDMFD SP !, {R3,PC}
main
STMFD SP !, {R4,LR}
MOV R0, #0
BL time
BL my_srand
MOV R4, #0x64 ; 'd'
loc_78
BL float_rand
; S0=valeur pseudo-aléatoire
LDR R0, =aF ; "%f"
; convertir la valeur obtenue en type double (printf() en a besoin) :
FCVTDS D7, S0
; copie bit à bit de D7 dans la paire de registres R2/R3 (pour printf()) :
FMRRD R2, R3, D7
BL printf
SUBS R4, R4, #1
BNE loc_78
MOV R0, R4
LDMFD SP !, {R4,PC}
aF DCB "%f",0xA,0
Nous allons faire un dump avec objdump et nous allons voir que les instructions
FPU ont un nom différent que dans IDA. Apparemment, les développeurs de IDA et
binutils ont utilisés des manuels différents? Peut-être qu’il serait bon de connaître
les deux variantes de noms des instructions.
488
54: ee370ac0 vsub.f32 s0, s15, s0
58: e8bd8008 pop {r3, pc}
5c : 3f800000 svccc 0x00800000
00000000 <main> :
0: e92d4010 push {r4, lr}
4: e3a00000 mov r0, #0
8: ebfffffe bl 0 <time>
c : ebfffffe bl 0 <main>
10: e3a04064 mov r4, #100 ; 0x64
14: ebfffffe bl 38 <main+0x38>
18: e59f0018 ldr r0, [pc, #24] ; 38 <main+0x38>
1c : eeb77ac0 vcvt.f64.f32 d7, s0
20: ec532b17 vmov r2, r3, d7
24: ebfffffe bl 0 <printf>
28: e2544001 subs r4, r4, #1
2c : 1afffff8 bne 14 <main+0x14>
30: e1a00004 mov r0, r4
34: e8bd8010 pop {r4, pc}
38: 00000000 andeq r0, r0, r0
Les instructions en 0x5c dans float_rand() et en 0x38 dans main() sont du bruit
(pseudo-)aléatoire.
union uint_float
{
uint32_t i ;
float f ;
};
void main()
{
printf ("%g\n", calculate_machine_epsilon(1.0)) ;
489
};
Ce que l’on fait ici est simplement de traiter la partie fractionnaire du nombre au
format IEEE 754 comme un entier et de lui ajouter 1. Le nombre flottant en résultant
est égal à starting_value+machine_epsilon, donc il suffit de soustraire starting_value (en
utilisant l’arithmétique flottante) pour mesurer ce que la différence d’un bit repré-
sente dans un nombre flottant simple précision(float). L’ union permet ici d’accéder
au nombre IEEE 754 comme à un entier normal. Lui ajouter 1 ajoute en fait 1 au
significande du nombre, toutefois, inutile de dire, un débordement est possible, qui
ajouterait 1 à l’exposant.
x86
La seconde instruction FST est redondante: il n’est pas nécessaire de stocker la va-
leur en entrée à la même place (le compilateur a décidé d’allouer la variable v à la
même place dans la pile locale que l’argument en entrée). Puis elle est incrémen-
tée avec INC, puisque c’est une variable entière normale. Ensuite elle est chargée
dans le FPU comme un nombre IEEE 754 32-bit, FSUBR fait le reste du travail et la
valeur résultante est stockée dans ST0. La dernière paire d’instructions FSTP/FLD est
redondante, mais le compilateur n’a pas optimisé le code.
ARM64
typedef union
{
uint64_t i ;
double d ;
} uint_double ;
490
double calculate_machine_epsilon(double start)
{
uint_double v ;
v.d=start ;
v.i++;
return v.d-start ;
}
void main()
{
printf ("%g\n", calculate_machine_epsilon(1.0)) ;
};
ARM64 n’a pas d’instruction qui peut ajouter un nombre a un D-registre FPU, donc
la valeur en entrée (qui provient du registre x64 D0) est d’abord copiée dans le GPR,
incrémentée, copiée dans le registre FPU D1, et puis la soustraction est faite.
Voir aussi cet exemple compilé pour x64 avec instructions SIMD: 1.38.4 on page 565.
MIPS
Il y a ici la nouvelle instruction MTC1 («Move To Coprocessor 1 »), elle transfère sim-
plement des données vers les registres du FPU.
Conclusion
Il est difficile de dire si quelqu’un pourrait avoir besoin de cette astuce dans du code
réel, mais comme cela a été mentionné plusieurs fois dans ce livre, cet exemple est
utile pour expliquer le format IEEE 754 et les unions en C/C++.
491
1.32.3 Remplacement de FSCALE
Agner Fog dans son travail163 Optimizing subroutines in assembly language / An op-
timization guide for x86 platforms indique que l’instruction FPU FSCALE (qui calcule
2n ) peut être lente sur de nombreux CPUs, et propose un remplacement plus rapide.
Voici ma conversion de son code assembleur en C/C++ :
#include <stdint.h>
#include <stdio.h>
union uint_float
{
uint32_t i ;
float f ;
};
float flt_2n(int N)
{
union uint_float tmp ;
tmp.i=(N<<23)+0x3f800000 ;
return tmp.f ;
};
struct float_as_struct
{
unsigned int fraction : 23;
unsigned int exponent : 8;
unsigned int sign : 1;
};
float flt_2n_v2(int N)
{
struct float_as_struct tmp ;
tmp.fraction=0;
tmp.sign=0;
tmp.exponent=N+0x7f ;
return *(float*)(&tmp) ;
};
union uint64_double
{
uint64_t i ;
double d ;
};
double dbl_2n(int N)
{
union uint64_double tmp ;
tmp.i=((uint64_t)N<<52)+0x3ff0000000000000UL ;
163. http://www.agner.org/optimize/optimizing_assembly.pdf
492
return tmp.d ;
};
struct double_as_struct
{
uint64_t fraction : 52;
int exponent : 11;
int sign : 1;
};
double dbl_2n_v2(int N)
{
struct double_as_struct tmp ;
tmp.fraction=0;
tmp.sign=0;
tmp.exponent=N+0x3ff ;
return *(double*)(&tmp) ;
};
int main()
{
// 211 = 2048
printf ("%f\n", flt_2n(11)) ;
printf ("%f\n", flt_2n_v2(11)) ;
printf ("%lf\n", dbl_2n(11)) ;
printf ("%lf\n", dbl_2n_v2(11)) ;
};
L’instruction FSCALE peut être plus rapide dans votre environnement, mais néan-
moins, c’est un bon exemple d’union et du fait que l’exposant est stocké sous la
forme 2n , donc une valeur n en entrée est décalée à l’exposant dans le nombre en-
codé en IEEE 754. Ensuite, l’exposant est corrigé avec l’ajout de 0x3f800000 ou de
0x3ff0000000000000.
La même chose peut être faite sans décalage utilisant struct, mais en interne, l’opé-
ration de décalage aura toujours lieu.
493
* To justify the following code, prove that
*
* ((((val_int / 2^m) - b) / 2) + b) * 2^m = ((val_int - 2^m) / 2) + ((⤦
Ç b + 1) / 2) * 2^m)
*
* where
*
* b = exponent bias
* m = number of mantissa bits
*
* .
*/
C’est un algorithme connu de calcul rapide de √1x . L’algorithme devînt connu, sup-
posément, car il a été utilisé dans Quake III Arena.
La description de l’algorithme peut être trouvée sur Wikipédia: http://en.wikipedia.
org/wiki/Fast_inverse_square_root.
494
nées, tant que vous avez une fonction pour comparer deux éléments et que qsort()
est capable de l’appeler.
La fonction de comparaison peut être définie comme:
int (*compare)(const void *, const void *)
1.33.1 MSVC
Compilons le dans MSVC 2010 (certaines parties ont été omises, dans un but de
concision) avec l’option /Ox :
495
mov ecx, DWORD PTR [ecx]
cmp eax, ecx
jne SHORT $LN4@comp
xor eax, eax
ret 0
$LN4@comp :
xor edx, edx
cmp eax, ecx
setge dl
lea eax, DWORD PTR [edx+edx-1]
ret 0
_comp ENDP
...
496
.text :7816CBF0
.text :7816CBF0 lo = dword ptr -104h
.text :7816CBF0 hi = dword ptr -100h
.text :7816CBF0 var_FC = dword ptr -0FCh
.text :7816CBF0 stkptr = dword ptr -0F8h
.text :7816CBF0 lostk = dword ptr -0F4h
.text :7816CBF0 histk = dword ptr -7Ch
.text :7816CBF0 base = dword ptr 4
.text :7816CBF0 num = dword ptr 8
.text :7816CBF0 width = dword ptr 0Ch
.text :7816CBF0 comp = dword ptr 10h
.text :7816CBF0
.text :7816CBF0 sub esp, 100h
....
497
MSVC + OllyDbg
Chargeons notre exemple dans OllyDbg et mettons un point d’arrêt sur comp(). Nous
voyons comment les valeurs sont comparées lors du premier appel de comp() :
OllyDbg montre les valeurs comparées dans la fenêtre sous celle du code, par com-
modité. Nous voyons que SP pointe sur RA, où se trouve la fonction qsort() (dans
MSVCR100.DLL).
498
En traçant (F8) jusqu’à l’instruction RETN et appuyant sur F8 une fois de plus, nous
retournons à la fonction qsort() :
Fig. 1.111: OllyDbg : le code dans qsort() juste après l’appel de comp()
499
Voici aussi une copie d’écran au moment du second appel àcomp()—maintenant les
valeurs qui doivent être comparées sont différentes:
MSVC + tracer
Regardons quelles sont le paires comparées. Ces 10 nombres vont être triés: 1892,
45, 200, -98, 4087, 5, -12345, 1087, 88, -100000.
Nous avons l’adresse de la première instruction CMP dans comp(), c’est 0x0040100C
et nous y avons mis un point d’arrêt:
tracer.exe -l :17_1.exe bpx=17_1.exe !0x0040100C
Maintenant nous avons des informations sur les registres au point d’arrêt:
PID=4336|New process 17_1.exe
(0) 17_1.exe !0x40100c
EAX=0x00000764 EBX=0x0051f7c8 ECX=0x00000005 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=IF
(0) 17_1.exe !0x40100c
EAX=0x00000005 EBX=0x0051f7c8 ECX=0xfffe7960 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=PF ZF IF
(0) 17_1.exe !0x40100c
EAX=0x00000764 EBX=0x0051f7c8 ECX=0x00000005 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=CF PF ZF IF
...
500
Filtrons sur EAX et ECX et nous obtenons:
EAX=0x00000764 ECX=0x00000005
EAX=0x00000005 ECX=0xfffe7960
EAX=0x00000764 ECX=0x00000005
EAX=0x0000002d ECX=0x00000005
EAX=0x00000058 ECX=0x00000005
EAX=0x0000043f ECX=0x00000005
EAX=0xffffcfc7 ECX=0x00000005
EAX=0x000000c8 ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0x00000ff7 ECX=0x00000005
EAX=0x00000ff7 ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0x00000005 ECX=0xffffcfc7
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0xffffff9e ECX=0xffffcfc7
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0x000000c8 ECX=0x00000ff7
EAX=0x0000002d ECX=0x00000ff7
EAX=0x0000043f ECX=0x00000ff7
EAX=0x00000058 ECX=0x00000ff7
EAX=0x00000764 ECX=0x00000ff7
EAX=0x000000c8 ECX=0x00000764
EAX=0x0000002d ECX=0x00000764
EAX=0x0000043f ECX=0x00000764
EAX=0x00000058 ECX=0x00000764
EAX=0x000000c8 ECX=0x00000058
EAX=0x0000002d ECX=0x000000c8
EAX=0x0000043f ECX=0x000000c8
EAX=0x000000c8 ECX=0x00000058
EAX=0x0000002d ECX=0x000000c8
EAX=0x0000002d ECX=0x00000058
501
MSVC + tracer (couverture du code)
Nous pouvons aussi utiliser la capacité du tracer pour collecter tous les registres
possible et les montrer dans IDA.
Exécutons pas à pas toutes les instructions dans comp() :
tracer.exe -l :17_1.exe bpf=17_1.exe !0x00401000,trace :cc
Nous obtenons un scipt .idc pour charger dans IDA et chargeons le:
Fig. 1.113: tracer et IDA. N.B.: certaines valeurs sont coupées à droite
IDA a donné un nom à la fonction (PtFuncCompare)—car IDA voit que le pointeur sur
cette fonction est passé à qsort().
Nous voyons que les pointeurs a et b pointent sur différents emplacements dans le
tableau, mais le pas entre eux est 4, puisque des valeurs 32-bit sont stockées dans
le tableau.
Nous voyons que les instructions en 0x401010 et 0x401012 ne sont jamais exécutées
(donc elles sont laissées en blanc) : en effet, comp() ne renvoie jamais 0, car il n’y
a pas d’éléments égaux dans le tableau.
1.33.2 GCC
Il n’y a pas beaucoup de différence:
502
mov [esp+40h+var_28], 764h
mov [esp+40h+var_24], 2Dh
mov [esp+40h+var_20], 0C8h
mov [esp+40h+var_1C], 0FFFFFF9Eh
mov [esp+40h+var_18], 0FF7h
mov [esp+40h+var_14], 5
mov [esp+40h+var_10], 0FFFFCFC7h
mov [esp+40h+var_C], 43Fh
mov [esp+40h+var_8], 58h
mov [esp+40h+var_4], 0FFFE7960h
mov [esp+40h+var_34], offset comp
mov [esp+40h+var_38], 4
mov [esp+40h+var_3C], 0Ah
call _qsort
Fonction comp() :
public comp
comp proc near
push ebp
mov ebp, esp
mov eax, [ebp+arg_4]
mov ecx, [ebp+arg_0]
mov edx, [eax]
xor eax, eax
cmp [ecx], edx
jnz short loc_8048458
pop ebp
retn
loc_8048458 :
setnl al
movzx eax, al
lea eax, [eax+eax-1]
pop ebp
retn
comp endp
503
.text :0002DDFD mov [esp], edi
.text :0002DE00 mov [esp+8], edx
.text :0002DE04 call [ebp+arg_C]
...
Évidemment, nous avons le code source C de notre exemple ( 1.33 on page 495),
donc nous pouvons mettre un point d’arrêt (b) sur le numéro de ligne (11—la ligne
où la première comparaison se produit. Nous devons aussi compiler l’exemple avec
les informations de débogage incluses (-g), donc la table avec les adresses et les
numéros de ligne correspondants est présente.
Nous pouvons aussi afficher les valeurs en utilisant les noms de variable (p) : les
informations de débogage nous disent aussi quel registre et/ou élément de la pile
locale contient quelle variable.
Nous pouvons aussi voir la pile (bt) et y trouver qu’il y a une fonction intermédiaire
msort_with_tmp() utilisée dans la Glibc.
504
#2 0xb7e4273e in msort_with_tmp (n=2, b=0xbffff0f8, p=0xbffff07c) at msort⤦
Ç .c :45
#3 msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry⤦
Ç =5) at msort.c :53
#4 0xb7e4273e in msort_with_tmp (n=5, b=0xbffff0f8, p=0xbffff07c) at msort⤦
Ç .c :45
#5 msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry⤦
Ç =10) at msort.c :53
#6 0xb7e42cef in msort_with_tmp (n=10, b=0xbffff0f8, p=0xbffff07c) at ⤦
Ç msort.c :45
#7 __GI_qsort_r (b=b@entry=0xbffff0f8, n=n@entry=10, s=s@entry=4, cmp=⤦
Ç cmp@entry=0x804844d <comp>,
arg=arg@entry=0x0) at msort.c :297
#8 0xb7e42dcf in __GI_qsort (b=0xbffff0f8, n=10, s=4, cmp=0x804844d <comp⤦
Ç >) at msort.c :307
#9 0x0804850d in main (argc=1, argv=0xbffff1c4) at 17_1.c :26
(gdb)
Mais souvent, il n’y a pas de code source du tout, donc nous pouvons désassembler
la fonction comp() (disas), trouver la toute première instruction CMP et placer un
point d’arrêt (b) à cette adresse.
À chaque point d’arrêt, nous allons afficher le contenu de tous les registres
(info registers). Le contenu de la pile est aussi disponible (bt),
mais partiellement: il n’y a pas l’information du numéro de ligne pour comp().
505
0x0804846b <+30> : jne 0x8048474 <comp+39>
0x0804846d <+32> : mov eax,0x0
0x08048472 <+37> : jmp 0x804848e <comp+65>
0x08048474 <+39> : mov eax,DWORD PTR [ebp-0x8]
0x08048477 <+42> : mov edx,DWORD PTR [eax]
0x08048479 <+44> : mov eax,DWORD PTR [ebp-0x4]
0x0804847c <+47> : mov eax,DWORD PTR [eax]
0x0804847e <+49> : cmp edx,eax
0x08048480 <+51> : jge 0x8048489 <comp+60>
0x08048482 <+53> : mov eax,0xffffffff
0x08048487 <+58> : jmp 0x804848e <comp+65>
0x08048489 <+60> : mov eax,0x1
0x0804848e <+65> : leave
0x0804848f <+66> : ret
End of assembler dump.
(gdb) b *0x08048469
Breakpoint 1 at 0x8048469
(gdb) run
Starting program : /home/dennis/polygon/./a.out
506
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) c
Continuing.
507
1.33.3 Danger des pointeurs sur des fonctions
Comme nous pouvons le voir, la fonction qsort() attend un pointeur sur une fonction
qui prend deux arguments de type void* et renvoie un entier: Si vous avez plusieurs
fonctions de comparaison dans votre code (une qui compare les chaînes, une autre—
les entiers, etc.), il est très facile de les mélanger les unes avec les autres. Vous
pouvez essayer de trier un tableau de chaîne en utilisant une fonction qui compare
les entiers, et le compilateur ne vous avertira pas de ce bogue.
uint64_t f ()
{
return 0x1234567890ABCDEF ;
};
x86
Dans un environnement 32-bit, les valeurs 64-bit sont renvoyées des fonctions dans
la paire de registres EDX :EAX.
ARM
Une valeur 64-bit est renvoyée dans la paire de registres R0-R1 (R1 est pour la partie
haute et R0 pour la partie basse) :
165. A propos, les valeurs 32-bit sont passées en tant que paire dans les environnements 16-bit de la
même manière: 3.34.4 on page 858
508
ENDP
|L0.12|
DCD 0x90abcdef
|L0.16|
DCD 0x12345678
MIPS
Une valeur 64-bit est renvoyée dans la paire de registres V0-V1 ($2-$3) (V0 ($2) est
pour la partie haute et V1 ($3) pour la partie basse) :
void f_add_test ()
{
#ifdef __GNUC__
printf ("%lld\n", f_add(12345678901234, 23456789012345)) ;
#else
printf ("%I64d\n", f_add(12345678901234, 23456789012345)) ;
#endif
};
509
x86
_f_add_test PROC
push 5461 ; 00001555H
push 1972608889 ; 75939f79H
push 2874 ; 00000b3aH
push 1942892530 ; 73ce2ff2H
call _f_add
push edx
push eax
push OFFSET $SG1436 ; '%I64d', 0aH, 00H
call _printf
add esp, 28
ret 0
_f_add_test ENDP
_f_sub PROC
mov eax, DWORD PTR _a$[esp-4]
sub eax, DWORD PTR _b$[esp-4]
mov edx, DWORD PTR _a$[esp]
sbb edx, DWORD PTR _b$[esp]
ret 0
_f_sub ENDP
Nous voyons dans la fonction f_add_test() que chaque valeur 64-bit est passée en
utilisant deux valeurs 32-bit, partie haute d’abord, puis partie basse.
L’addition et la soustraction se déroulent aussi par paire.
Pour l’addition, la partie basse 32-bit est d’abord additionnée. Si il y a eu une retenue
pendant l’addition, le flag CF est mis.
L’instruction suivante ADC additionne les parties hautes, et ajoute aussi 1 si CF = 1.
La soustraction est aussi effectuée par paire. Le premier SUB peut aussi mettre le
flag CF, qui doit être testé lors de l’instruction SBB suivante: Si le flag de retenue est
mis, alors 1 est soustrait du résultat.
Il est facile de voir comment le résultat de la fonction f_add() est passé à printf().
510
mov edx, DWORD PTR [esp+16]
add eax, DWORD PTR [esp+4]
adc edx, DWORD PTR [esp+8]
ret
_f_add_test :
sub esp, 28
mov DWORD PTR [esp+8], 1972608889 ; 75939f79H
mov DWORD PTR [esp+12], 5461 ; 00001555H
mov DWORD PTR [esp], 1942892530 ; 73ce2ff2H
mov DWORD PTR [esp+4], 2874 ; 00000b3aH
call _f_add
mov DWORD PTR [esp+4], eax
mov DWORD PTR [esp+8], edx
mov DWORD PTR [esp], OFFSET FLAT :LC0 ; "%lld\n"
call _printf
add esp, 28
ret
_f_sub :
mov eax, DWORD PTR [esp+4]
mov edx, DWORD PTR [esp+8]
sub eax, DWORD PTR [esp+12]
sbb edx, DWORD PTR [esp+16]
ret
ARM
f_sub PROC
SUBS r0,r0,r2
SBC r1,r1,r3
BX lr
ENDP
f_add_test PROC
PUSH {r4,lr}
LDR r2,|L0.68| ; 0x75939f79
LDR r3,|L0.72| ; 0x00001555
LDR r0,|L0.76| ; 0x73ce2ff2
LDR r1,|L0.80| ; 0x00000b3a
BL f_add
POP {r4,lr}
MOV r2,r0
511
MOV r3,r1
ADR r0,|L0.84| ; "%I64d\n"
B __2printf
ENDP
|L0.68|
DCD 0x75939f79
|L0.72|
DCD 0x00001555
|L0.76|
DCD 0x73ce2ff2
|L0.80|
DCD 0x00000b3a
|L0.84|
DCB "%I64d\n",0
La première valeur 64-bit est passée par la paire de registres R0 et R1, la seconde
dans la paire de registres R2 et R3. ARM a aussi l’instruction ADC (qui compte le flag de
retenue) et SBC («soustraction avec retenue »). Chose importante: lorsque les parties
basses sont ajoutées/soustraites, les instructions ADDS et SUBS avec le suffixe -S sont
utilisées. Le suffixe -S signifie «mettre les flags », et les flags (en particulier le flag
de retenue) est ce dont les instructions suivantes ADC/SBC ont besoin. Autrement,
les instructions sans le suffixe -S feraient le travail (ADD et SUB).
MIPS
f_sub :
; $a0 - partie
haute de a
; $a1 - partie
basse de a
; $a2 - partie
haute de b
; $a3 - partie
basse de b
subu $v1, $a1, $a3 ; soustraire les parties basses
subu $v0, $a0, $a2 ; soustraire les parties hautes
; est-ce qu'une retenue a été générée lors de la soustraction des parties
basses?
512
; si oui, mettre $a0 à 1
sltu $a1, $v1
jr $ra
; soustraire 1 à la partie haute du résultat si la retenue doit être
générée:
subu $v0, $a1 ; slot de délai de branchement
; $v0 - partie haute du résultat
; $v1 - partie basse du résultat
f_add_test :
var_10 = -0x10
var_4 = -4
MIPS n’a pas de registre de flags, donc il n’y a pas cette information après l’exécution
des opérations arithmétiques. Donc il n’y a pas d’instructions comme ADC et SBB
du x86. Pour savoir si le flag de retenue serait mis, une comparaison est faite (en
utilisant l’instruction SLTU), qui met le registre de destination à 1 ou 0. Ce 1 ou ce 0
est ensuite ajouté ou soustrait au/du résultat final.
513
uint64_t f_div (uint64_t a, uint64_t b)
{
return a/b ;
};
x86
_a$ = 8 ; signe = 8
_b$ = 16 ; signe = 8
_f_div PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _b$[ebp+4]
push eax
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp+4]
push edx
mov eax, DWORD PTR _a$[ebp]
push eax
call __aulldiv ; division long long non signée
pop ebp
ret 0
_f_div ENDP
_a$ = 8 ; signe = 8
_b$ = 16 ; signe = 8
514
_f_rem PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _b$[ebp+4]
push eax
mov ecx, DWORD PTR _b$[ebp]
push ecx
mov edx, DWORD PTR _a$[ebp+4]
push edx
mov eax, DWORD PTR _a$[ebp]
push eax
call __aullrem ; reste long long non signé
pop ebp
ret 0
_f_rem ENDP
_f_div :
sub esp, 28
mov eax, DWORD PTR [esp+40]
mov edx, DWORD PTR [esp+44]
mov DWORD PTR [esp+8], eax
mov eax, DWORD PTR [esp+32]
mov DWORD PTR [esp+12], edx
mov edx, DWORD PTR [esp+36]
mov DWORD PTR [esp], eax
mov DWORD PTR [esp+4], edx
call ___udivdi3 ; division non signé
add esp, 28
ret
_f_rem :
sub esp, 28
mov eax, DWORD PTR [esp+40]
515
mov edx, DWORD PTR [esp+44]
mov DWORD PTR [esp+8], eax
mov eax, DWORD PTR [esp+32]
mov DWORD PTR [esp+12], edx
mov edx, DWORD PTR [esp+36]
mov DWORD PTR [esp], eax
mov DWORD PTR [esp+4], edx
call ___umoddi3 ; modulo non signé
add esp, 28
ret
GCC fait ce que l’on attend, mais le code multiplication est mis en ligne (inlined)
directement dans la fonction, pensant que ça peut être plus efficace. GCC a des
noms de fonctions de bibliothèque différents: .4 on page 1366.
ARM
Keil pour mode Thumb insère des appels à des sous-routines de bibliothèque:
||f_div|| PROC
PUSH {r4,lr}
BL __aeabi_uldivmod
POP {r4,pc}
ENDP
||f_rem|| PROC
PUSH {r4,lr}
BL __aeabi_uldivmod
MOVS r0,r2
MOVS r1,r3
POP {r4,pc}
ENDP
Keil pour mode ARM, d’un autre côté, est capable de produire le code de la multipli-
cation 64-bit:
516
ENDP
||f_div|| PROC
PUSH {r4,lr}
BL __aeabi_uldivmod
POP {r4,pc}
ENDP
||f_rem|| PROC
PUSH {r4,lr}
BL __aeabi_uldivmod
MOV r0,r2
MOV r1,r3
POP {r4,pc}
ENDP
MIPS
GCC avec optimisation pour MIPS peut générer du code pour la multiplication 64-bit,
mais doit appeler une routine de bibliothèque pour la division 64-bit:
f_div :
var_10 = -0x10
var_4 = -4
517
or $at, $zero
jr $ra
addiu $sp, 0x20
f_rem :
var_10 = -0x10
var_4 = -4
Il y a beaucoup de NOPs, sans doute des slots de délai de remplissage après l’ins-
truction de multiplication (c’est plus lent que les autres instructions après tout).
uint64_t f (uint64_t a)
{
return a>>7;
};
x86
518
mov edx, DWORD PTR [esp+8]
mov eax, DWORD PTR [esp+4]
shrd eax, edx, 7
shr edx, 7
ret
Le décalage se produit en deux passes: tout d’abord la partie basse est décalée, puis
la partie haute. Mais la partie basse est décalée avec l’aide de l’instruction SHRD, elle
décale la valeur de EAX de 7 bits, mais tire les nouveaux bits de EDX, i.e., de la partie
haute. En d’autres mots, la valeur 64-bit dans la paire de registres EDX:EAX, dans
son entier, est décalée de 7 bits et les 32 bits bas du résultat sont placés dans EAX.
La partie haute est décalée en utilisant l’instruction plus populaire SHR : en effet, les
bits libérés dans la partie haute doivent être remplis avec des zéros.
ARM
ARM n’a pas une instruction telle que SHRD en x86, donc le compilateur Keil fait cela
en utilisant des simples décalages et des opérations OR :
MIPS
GCC pour MIPS suit le même algorithme que Keil fait pour le mode Thumb:
519
1.34.5 Convertir une valeur 32-bit en 64-bit
#include <stdint.h>
int64_t f (int32_t a)
{
return a ;
};
x86
Ici, nous nous heurtons à la nécessité d’étendre une valeur 32-bit signée en une
64-bit signée. Les valeurs non signées sont converties directement: tous les bits de
la partie haute doivent être mis à 0. Mais ce n’est pas approprié pour les types de
donnée signée: le signe doit être copié dans la partie haute du nombre résultant.
L’instruction CDQ fait cela ici, elle prend sa valeur d’entrée dans EAX, étend le signe
sur 64-bit et laisse le résultat dans la paire de registres EDX :EAX. En d’autres mots,
CDQ prend le signe du nombre dans EAX (en prenant le bit le plus significatif dans
EAX), et suivant sa valeur, met tous les 32 bits de EDX à 0 ou 1. Cette opération est
quelque peu similaire à l’instruction MOVSX.
ARM
Keil pour ARM est différent: il décale simplement arithmétiquement de 31 bits vers
la droite la valeur en entrée. Comme nous le savons, le bit de signe est le MSB, et
le décalage arithmétique copie le bit de signe dans les bits «apparus ». Donc après
«ASR r1,r0,#31 », R1 contient 0xFFFFFFFF si la valeur d’entrée était négative et 0
sinon. R1 contient la partie haute de la valeur 64-bit résultante. En d’autres mots, ce
code copie juste le MSB (bit de signe) de la valeur d’entrée dans R0 dans tous les
bits de la partie haute 32-bit de la valeur 64-bit résultante.
MIPS
GCC pour MIPS fait la même chose que Keil a fait pour le mode ARM:
520
Listing 1.391: GCC 4.4.5 avec optimisation (IDA)
f :
sra $v0, $a0, 31
jr $ra
move $v1, $a0
166. https://docs.microsoft.com/en-us/windows/desktop/api/minwinbase/
ns-minwinbase-filetime
521
Voici ce que fit Microsoft, quelque chose comme ceci167 :
union ULARGE_INTEGER
{
struct backward_compatibility
{
DWORD LowPart ;
DWORD HighPart ;
};
#ifdef NEW_FANCY_COMPILER_SUPPORTING_64_BIT
ULONGLONG QuadPart ;
#endif
};
Ceci est un fragment de 8 octets, qui peut être accédé par l’entier 64-bit QuadPart
(si il est compilé avec un compilateur récent) ou en utilisant deux entiers 32-bit (si
compilé avec un compilateur plus ancien).
Le champ QuadPart est simplement absent lorsque c’est compilé avec un vieux com-
pilateur.
L’ordre est crucial: le premier champ (LowPart) correspond au 4 octets de la valeur
64-bit, le second (HighPart) au 4 octets hauts.
Microsoft a aussi ajouté des fonctions utilitaires pour les différentes opérations arith-
métiques, de la même façon que je l’ai déjà décrit: 1.34 on page 508.
Et ceci provient du code source de Windows 2000 qui avait été divulgué:
522
cPublicProc _RtlLargeIntegerAdd ,4
cPublicFpo 4,0
stdENDP _RtlLargeIntegerAdd
.end RtlLargeIntegerAdd
LEAF_EXIT(RtlLargeIntegerAdd)
.end RtlLargeIntegerAdd
Pas besoin d’utiliser des instructions 32-bit sur Itanium et DEC Alpha—qui soient déjà
prêtes pour le 64-bit.
Et voici ce que l’on peut trouver dans Windows Research Kernel:
523
DECLSPEC_DEPRECATED_DDK // Use native __int64 math
__inline
LARGE_INTEGER
NTAPI
RtlLargeIntegerAdd (
LARGE_INTEGER Addend1,
LARGE_INTEGER Addend2
)
{
LARGE_INTEGER Sum ;
Toutes ces fonctions pourront être supprimées (dans le futur), mais maintenant elles
opèrent sur le champ QuadPart. Si ce morceau de code doit être compilé en utilisant
un compilateur 32-bit moderne (qui supporte les entiers 64-bit), il générera deux
additions 32-bit sous le capot. À partir de ce moment, les champs LowPart/HighPart
pourront être supprimés de l’union/structure LARGE_INTEGER.
Utiliseriez-vous une telle technique aujourd’hui? Probablement pas, mais si quel-
qu’un avait besoin d’un type entier 128-bit, vous pourriez l’implémenter comme
ceci.
Aussi, inutile de dire, ceci fonctionne grâce au petit boutisme ( 2.8 on page 601)
(toutes les architectures pour lesquelles Windows NT a été développé sont petit bou-
tiste. Cette astuce n’est pas possible sur une architecture gros boutiste.
1.36 SIMD
SIMD est un acronyme: Single Instruction, Multiple Data (simple instruction, multiple
données).
Comme son nom le laisse entendre, cela traite des données multiples avec une seule
instruction.
Comme le FPU, ce sous-système du CPU ressemble à un processeur séparé à l’inté-
rieur du x86.
SIMD a commencé avec le MMX en x86. 8 nouveaux registres apparurent: MM0-MM7.
Chaque registre MMX contient 2 valeurs 32-bit, 4 valeurs 16-bit ou 8 octets. Par
exemple, il est possible d’ajouter 8 valeurs 8-bit (octets) simultanément en ajoutant
deux valeurs dans des registres MMX.
Un exemple simple est un éditeur graphique qui représente une image comme un
tableau à deux dimensions. Lorsque l’utilisateur change la luminosité de l’image,
l’éditeur doit ajouter ou soustraire un coefficient à/de chaque valeur du pixel. Dans
un soucis de concision, si l’on dit que l’image est en niveau de gris et que chaque
pixel est défini par un octet de 8-bit, alors il est possible de changer la luminosité de
8 pixels simultanément.
524
À propos, c’est la raison pour laquelle les instructions de saturation sont présentes
en SIMD.
Lorsque l’utilisateur change la luminosité dans l’éditeur graphique, les dépassements
au dessus ou en dessous ne sont pas souhaitables, donc il y a des instructions d’ad-
dition en SIMD qui n’additionnent pas si la valeur maximum est atteinte, etc.
Lorsque le MMX est apparu, ces registres étaient situés dans les registres du FPU. Il
était seulement possible d’utiliser soit le FPU ou soit le MMX. On peut penser qu’Intel
économisait des transistors, mais en fait, la raison d’une telle symbiose était plus
simple —les anciens OSes qui n’étaient pas au courant de ces registres supplémen-
taires et ne les sauvaient pas lors du changement de contexte, mais sauvaient les
registres FPU. Ainsi, CPU avec MMX + ancien OS + processus utilisant les capacités
MMX fonctionnait toujours.
SSE—est une extension des registres SIMD à 128 bits, maintenant séparé du FPU.
AVX—une autre extension, à 256 bits.
Parlons maintenant de l’usage pratique.
Bien sûr, il s’agit de routines de copie en mémoire (memcpy), de comparaison de
mémoire (memcmp) et ainsi de suite.
Un autre exemple: l’algorithme de chiffrement DES prend un bloc de 64-bit et une clef
de 56-bit, chiffre le bloc et produit un résultat de 64-bit. L’algorithme DES peut être
considéré comme un grand circuit électronique, avec des fils et des portes AND/OR/-
NOT.
Le bitslice DES168 —est l’idée de traiter des groupes de blocs et de clés simultané-
ment. Disons, une variable de type unsigned int en x86 peut contenir jusqu’à 32-bit,
donc il est possible d’y stocker des résultats intermédiaires pour 32 paires de blocs-
clé simultanément, en utilisant 64+56 variables de type unsigned int.
Il existe un utilitaire pour brute-forcer les mots de passe/hashes d’Oracle RDBMS
(certains basés sur DES) en utilisant un algorithme bitslice DES légèrement modifié
pour SSE2 et AVX—maintenant il est possible de chiffrer 128 ou 256 paires de blocs-
clé simultanément.
http://conus.info/utils/ops_SIMD/
1.36.1 Vectorisation
La vectorisation169 , c’est lorsque, par exemple, vous avez une boucle qui prend une
paire de tableaux en entrée et produit un tableau. Le corps de la boucle prend les
valeurs dans les tableaux en entrée, fait quelque chose et met le résultat dans le
tableau de sortie. La vectorisation est le fait de traiter plusieurs éléments simulta-
nément.
La vectorisation n’est pas une nouvelle technologie: l’auteur de ce livre l’a vu au
moins sur la série du super-calculateur Cray Y-MP de 1988 lorsqu’il jouait avec sa
168. http://www.darkside.com.au/bitslice/
169. Wikipédia: vectorisation
525
version «lite » le Cray Y-MP EL170 .
Par exemple:
for (i = 0; i < 1024; i++)
{
C[i] = A[i]*B[i];
}
Exemple d’addition
return 0;
};
Intel C++
526
ar1 = dword ptr 8
ar2 = dword ptr 0Ch
ar3 = dword ptr 10h
push edi
push esi
push ebx
push esi
mov edx, [esp+10h+sz]
test edx, edx
jle loc_15B
mov eax, [esp+10h+ar3]
cmp edx, 6
jle loc_143
cmp eax, [esp+10h+ar2]
jbe short loc_36
mov esi, [esp+10h+ar2]
sub esi, eax
lea ecx, ds :0[edx*4]
neg esi
cmp ecx, esi
jbe short loc_55
527
test edi, 3
jnz loc_162
neg edi
add edi, 10h
shr edi, 2
528
mov esi, [esp+10h+ar2]
529
?f@@YAHHPAH00@Z endp
Autrement, la valeur de ar2 est chargée dans XMM0 avec MOVDQU, qui ne nécessite
pas que le pointeur soit aligné, mais peut s’exécuter plus lentement.
movdqu xmm1, xmmword ptr [ebx+edi*4] ; ar1+i*4
movdqu xmm0, xmmword ptr [esi+edi*4] ; ar2+i*4 n'est pas aligné sur
16-octet, donc le charger dans XMM0
paddd xmm1, xmm0
movdqa xmmword ptr [eax+edi*4], xmm1 ; ar3+i*4
GCC
GCC peut aussi vectoriser dans des cas simples172 , si l’option -O3 est utilisée et le
support de SSE2 activé: -msse2.
Ce que nous obtenons (GCC 4.4.1) :
; f(int, int *, int *, int *)
public _Z1fiPiS_S_
_Z1fiPiS_S_ proc near
530
var_18 = dword ptr -18h
var_14 = dword ptr -14h
var_10 = dword ptr -10h
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
arg_8 = dword ptr 10h
arg_C = dword ptr 14h
push ebp
mov ebp, esp
push edi
push esi
push ebx
sub esp, 0Ch
mov ecx, [ebp+arg_0]
mov esi, [ebp+arg_4]
mov edi, [ebp+arg_8]
mov ebx, [ebp+arg_C]
test ecx, ecx
jle short loc_80484D8
cmp ecx, 6
lea eax, [ebx+10h]
ja short loc_80484E8
align 8
531
lea edx, [esi+10h]
cmp ebx, edx
jbe loc_8048578
532
add ebx, 4
cmp ecx, eax
jg short loc_8048558
add esp, 0Ch
xor eax, eax
pop ebx
pop esi
pop edi
pop ebp
retn
void my_memcpy (unsigned char* dst, unsigned char* src, size_t cnt)
{
size_t i ;
for (i=0; i<cnt ; i++)
dst[i]=src[i];
};
533
push rbp
push rbx
neg rcx
and ecx, 15
cmp rcx, rdx
cmova rcx, rdx
xor eax, eax
test rcx, rcx
je .L4
movzx eax, BYTE PTR [rsi]
cmp rcx, 1
mov BYTE PTR [rdi], al
je .L15
movzx eax, BYTE PTR [rsi+1]
cmp rcx, 2
mov BYTE PTR [rdi+1], al
je .L16
movzx eax, BYTE PTR [rsi+2]
cmp rcx, 3
mov BYTE PTR [rdi+2], al
je .L17
movzx eax, BYTE PTR [rsi+3]
cmp rcx, 4
mov BYTE PTR [rdi+3], al
je .L18
movzx eax, BYTE PTR [rsi+4]
cmp rcx, 5
mov BYTE PTR [rdi+4], al
je .L19
movzx eax, BYTE PTR [rsi+5]
cmp rcx, 6
mov BYTE PTR [rdi+5], al
je .L20
movzx eax, BYTE PTR [rsi+6]
cmp rcx, 7
mov BYTE PTR [rdi+6], al
je .L21
movzx eax, BYTE PTR [rsi+7]
cmp rcx, 8
mov BYTE PTR [rdi+7], al
je .L22
movzx eax, BYTE PTR [rsi+8]
cmp rcx, 9
mov BYTE PTR [rdi+8], al
je .L23
movzx eax, BYTE PTR [rsi+9]
cmp rcx, 10
mov BYTE PTR [rdi+9], al
je .L24
movzx eax, BYTE PTR [rsi+10]
cmp rcx, 11
mov BYTE PTR [rdi+10], al
je .L25
534
movzx eax, BYTE PTR [rsi+11]
cmp rcx, 12
mov BYTE PTR [rdi+11], al
je .L26
movzx eax, BYTE PTR [rsi+12]
cmp rcx, 13
mov BYTE PTR [rdi+12], al
je .L27
movzx eax, BYTE PTR [rsi+13]
cmp rcx, 15
mov BYTE PTR [rdi+13], al
jne .L28
movzx eax, BYTE PTR [rsi+14]
mov BYTE PTR [rdi+14], al
mov eax, 15
.L4 :
mov r10, rdx
lea r9, [rdx-1]
sub r10, rcx
lea r8, [r10-16]
sub r9, rcx
shr r8, 4
add r8, 1
mov r11, r8
sal r11, 4
cmp r9, 14
jbe .L6
lea rbp, [rsi+rcx]
xor r9d, r9d
add rcx, rdi
xor ebx, ebx
.L7 :
movdqa xmm0, XMMWORD PTR [rbp+0+r9]
add rbx, 1
movups XMMWORD PTR [rcx+r9], xmm0
add r9, 16
cmp rbx, r8
jb .L7
add rax, r11
cmp r10, r11
je .L1
.L6 :
movzx ecx, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
lea rcx, [rax+1]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+1+rax]
mov BYTE PTR [rdi+1+rax], cl
lea rcx, [rax+2]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+2+rax]
535
mov BYTE PTR [rdi+2+rax], cl
lea rcx, [rax+3]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+3+rax]
mov BYTE PTR [rdi+3+rax], cl
lea rcx, [rax+4]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+4+rax]
mov BYTE PTR [rdi+4+rax], cl
lea rcx, [rax+5]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+5+rax]
mov BYTE PTR [rdi+5+rax], cl
lea rcx, [rax+6]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+6+rax]
mov BYTE PTR [rdi+6+rax], cl
lea rcx, [rax+7]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+7+rax]
mov BYTE PTR [rdi+7+rax], cl
lea rcx, [rax+8]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+8+rax]
mov BYTE PTR [rdi+8+rax], cl
lea rcx, [rax+9]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+9+rax]
mov BYTE PTR [rdi+9+rax], cl
lea rcx, [rax+10]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+10+rax]
mov BYTE PTR [rdi+10+rax], cl
lea rcx, [rax+11]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+11+rax]
mov BYTE PTR [rdi+11+rax], cl
lea rcx, [rax+12]
cmp rdx, rcx
jbe .L1
movzx ecx, BYTE PTR [rsi+12+rax]
mov BYTE PTR [rdi+12+rax], cl
lea rcx, [rax+13]
cmp rdx, rcx
536
jbe .L1
movzx ecx, BYTE PTR [rsi+13+rax]
mov BYTE PTR [rdi+13+rax], cl
lea rcx, [rax+14]
cmp rdx, rcx
jbe .L1
movzx edx, BYTE PTR [rsi+14+rax]
mov BYTE PTR [rdi+14+rax], dl
.L1 :
pop rbx
pop rbp
.L41 :
rep ret
.L13 :
xor eax, eax
.L3 :
movzx ecx, BYTE PTR [rsi+rax]
mov BYTE PTR [rdi+rax], cl
add rax, 1
cmp rax, rdx
jne .L3
rep ret
.L28 :
mov eax, 14
jmp .L4
.L15 :
mov eax, 1
jmp .L4
.L16 :
mov eax, 2
jmp .L4
.L17 :
mov eax, 3
jmp .L4
.L18 :
mov eax, 4
jmp .L4
.L19 :
mov eax, 5
jmp .L4
.L20 :
mov eax, 6
jmp .L4
.L21 :
mov eax, 7
jmp .L4
.L22 :
mov eax, 8
jmp .L4
.L23 :
mov eax, 9
jmp .L4
.L24 :
537
mov eax, 10
jmp .L4
.L25 :
mov eax, 11
jmp .L4
.L26 :
mov eax, 12
jmp .L4
.L27 :
mov eax, 13
jmp .L4
if (str_is_aligned==false)
return strlen (str) ;
for (;;)
{
xmm1 = _mm_load_si128((__m128i *)s) ;
xmm1 = _mm_cmpeq_epi8(xmm1, xmm0) ;
if ((mask = _mm_movemask_epi8(xmm1)) != 0)
{
unsigned long pos ;
_BitScanForward(&pos, mask) ;
len += (size_t)pos ;
break ;
}
538
s += sizeof(__m128i) ;
len += sizeof(__m128i) ;
};
return len ;
}
push ebp
mov ebp, esp
and esp, -16 ; fffffff0H
mov eax, DWORD PTR _str$[ebp]
sub esp, 12 ; 0000000cH
push esi
mov esi, eax
and esi, -16 ; fffffff0H
xor edx, edx
mov ecx, eax
cmp esi, eax
je SHORT $LN4@strlen_sse
lea edx, DWORD PTR [eax+1]
npad 3 ; aligner le prochain label
$LL11@strlen_sse :
mov cl, BYTE PTR [eax]
inc eax
test cl, cl
jne SHORT $LL11@strlen_sse
sub eax, edx
pop esi
mov esp, ebp
pop ebp
ret 0
$LN4@strlen_sse :
movdqa xmm1, XMMWORD PTR [eax]
pxor xmm0, xmm0
pcmpeqb xmm1, xmm0
pmovmskb eax, xmm1
test eax, eax
jne SHORT $LN9@strlen_sse
$LL3@strlen_sse :
movdqa xmm1, XMMWORD PTR [ecx+16]
add ecx, 16 ; 00000010H
pcmpeqb xmm1, xmm0
add edx, 16 ; 00000010H
pmovmskb eax, xmm1
test eax, eax
je SHORT $LL3@strlen_sse
539
$LN9@strlen_sse :
bsf eax, eax
mov ecx, eax
mov DWORD PTR _pos$75552[esp+16], eax
lea eax, DWORD PTR [ecx+edx]
pop esi
mov esp, ebp
pop ebp
ret 0
?strlen_sse2@@YAIPBD@Z ENDP ; strlen_sse2
540
0x008c1ff8 ’h’
0x008c1ff9 ’e’
0x008c1ffa ’l’
0x008c1ffb ’l’
0x008c1ffc ’o’
0x008c1ffd ’\x00’
0x008c1ffe random noise
0x008c1fff random noise
Donc, dans des conditions normales, le programme appelle strlen(), en lui passant
un pointeur sur la chaîne 'hello' se trouvant en mémoire à l’adresse 0x008c1ff8.
strlen() lit un octet à la fois jusqu’à 0x008c1ffd, où se trouve un octet à zéro, et
puis s’arrête.
Maintenant, si nous implémentons notre propre strlen() lisant 16 octets à la fois,
à partir de n’importe quelle adresse, alignée ou pas, MOVDQU pourrait essayer de
charger 16 octets à la fois depuis l’adresse 0x008c1ff8 jusqu’à 0x008c2008, et ainsi
déclencherait une exception. Cette situation peut être évitée, bien sûr.
Nous allons donc ne travailler qu’avec des adresses alignées sur une limite de 16
octets, ce qui en combinaison avec la connaissance que les pages de l’OS sont en
général alignées sur une limite de 16 octets nous donne quelques garanties que
notre fonction ne va pas lire de la mémoire non allouée.
Retournons à notre fonction.
_mm_setzero_si128()—est une macro générant pxor xmm0, xmm0 —elle efface juste
le registre XMM0.
_mm_load_si128()—est une macro pour MOVDQA, elle charge 16 octets depuis l’adresse
dans le registre XMM1.
_mm_cmpeq_epi8()—est une macro pour PCMPEQB, une instruction qui compare deux
registres XMM par octet.
Et si l’un des octets est égal à celui dans l’autre registre, il y aura 0xff à ce point
dans le résultat ou 0 sinon.
Par exemple:
XMM1: 0x11223344556677880000000000000000
XMM0: 0x11ab3444007877881111111111111111
Après l’exécution de pcmpeqb xmm1, xmm0, le registre XMM1 contient:
XMM1: 0xff0000ff0000ffff0000000000000000
Dans notre cas, cette instruction compare chacun des 16 octets avec un bloc de 16
octets à zéro, qui ont été mis dans le registre XMM0 par pxor xmm0, xmm0.
La macro suivante est _mm_movemask_epi8() —qui est l’instruction PMOVMSKB.
Elle est très utile avec PCMPEQB.
pmovmskb eax, xmm1
541
Cette instruction met d’abord le premier bit d’EAX à 1 si le bit le plus significatif du
premier octet dans XMM1 est 1. En d’autres mots, si le premier octet du registre XMM1
est 0xff, alors le premier bit de EAX sera 1 aussi.
Si le second octet du registre XMM1 est 0xff, alors le second bit de EAX sera mis à 1
aussi. En d’autres mots, cette instruction répond à la question «quels octets de XMM1
ont le bit le plus significatif à 1 ou sont plus grand que 0x7f ? » et renvoie 16 bits
dans le registre EAX. Les autres bits du registre EAX sont mis à zéro.
À propos, ne pas oublier cette bizarrerie dans notre algorithme. Il pourrait y avoir 16
octets dans l’entrée, comme:
15 14 13 12 11 10 9 3 2 1 0
542
1.37 64 bits
1.37.1 x86-64
Il s’agit d’une extension à 64 bits de l’architecture x86.
Pour l’ingénierie inverse, les changements les plus significatifs sont:
• La plupart des registres (à l’exception des registres FPU et SIMD) ont été éten-
dus à 64 bits et leur nom préfixé de la lettre R. 8 registres ont également été
ajoutés. Les GPR sont donc désormais: RAX, RBX, RCX, RDX, RBP, RSP, RSI, RDI,
R8, R9, R10, R11, R12, R13, R14, R15.
Les anciens registres restent accessibles de la manière habituelle. Ainsi, l’utili-
sation de EAX donne accès aux 32 bits de poids faible du registre RAX :
Octet d’indice
7 6 5 4 3 2 1 0
RAXx64
EAX
AX
AH AL
Les nouveaux registres R8-R15 possèdent eux aussi des sous-parties : R8D-R15D
(pour les 32 bits de poids faible), R8W-R15W (16 bits de poids faible), R8L-R15L
(8 bits de poids faible).
Octet d’indice
7 6 5 4 3 2 1 0
R8
R8D
R8W
R8L
Les registres SIMD ont vu leur nombre passé de 8 à 16: XMM0-XMM15.
• En environnement Win64, la convention d’appel de fonctions est légèrement
différente et ressemble à la convention fastcall ( 6.1.3 on page 964). Les 4
premiers arguments sont stockés dans les registres RCX, RDX, R8 et R9. Les ar-
guments suivants —sur la pile. La fonction appelante doit également allouer
32 octets pour utilisation par la fonction appelée qui pourra y sauvegarder les
registres contenant les 4 premiers arguments. Les fonctions les plus simples
peuvent utiliser les arguments directement depuis les registres. En revanche,
les fonctions plus complexes peuvent sauvegarder ces registres sur la pile.
L’ABI System V AMD64 (Linux, *BSD, Mac OS X)[Michael Matz, Jan Hubicka, An-
dreas Jaeger, Mark Mitchell, System V Application Binary Interface. AMD64 Ar-
chitecture Processor Supplement, (2013)] 179 ressemble elle aussi à la conven-
tion fastcall. Elle utilise 6 registres RDI, RSI, RDX, RCX, R8, R9 pour les 6 premiers
arguments. Tous les suivants sont passés sur la pile.
179. Aussi disponible en https://software.intel.com/sites/default/files/article/402129/
mpx-linux64-abi.pdf
543
Référez-vous également à la section sur les conventions d’appel ( 6.1 on page 962).
• Pour des raisons de compatibilité, le type C/C++ int conserve sa taille de 32bits.
• Tous les pointeurs sont désormais sur 64 bits.
Dans la mesure où le nombre de registres a doublé, les compilateurs disposent de
plus de marge de manœuvre en matière d’allocation des registres. Pour nous, il en
résulte que le code généré contient moins de variables locales.
Par exemple, la fonction qui calcule la première S-box de l’algorithme de chiffrage
DES traite en une fois au moyen de la méthode DES bitslice des valeurs de 32/64/128/256
bits (en fonction du type DES_type (uint32, uint64, SSE2 or AVX)). Pour en savoir plus
sur cette technique, voyez ( 1.36 on page 525) :
/*
* Generated S-box files.
*
* This software may be modified, redistributed, and used for any purpose,
* so long as its origin is acknowledged.
*
* Produced by Matthew Kwan - March 1998
*/
#ifdef _WIN64
#define DES_type unsigned __int64
#else
#define DES_type unsigned int
#endif
void
s1 (
DES_type a1,
DES_type a2,
DES_type a3,
DES_type a4,
DES_type a5,
DES_type a6,
DES_type *out1,
DES_type *out2,
DES_type *out3,
DES_type *out4
) {
DES_type x1, x2, x3, x4, x5, x6, x7, x8 ;
DES_type x9, x10, x11, x12, x13, x14, x15, x16 ;
DES_type x17, x18, x19, x20, x21, x22, x23, x24 ;
DES_type x25, x26, x27, x28, x29, x30, x31, x32 ;
DES_type x33, x34, x35, x36, x37, x38, x39, x40 ;
DES_type x41, x42, x43, x44, x45, x46, x47, x48 ;
DES_type x49, x50, x51, x52, x53, x54, x55, x56 ;
x1 = a3 & ~a5 ;
x2 = x1 ^ a4 ;
x3 = a3 & ~a4 ;
x4 = x3 | a5 ;
544
x5 = a6 & x4 ;
x6 = x2 ^ x5 ;
x7 = a4 & ~a5 ;
x8 = a3 ^ a4 ;
x9 = a6 & ~x8 ;
x10 = x7 ^ x9 ;
x11 = a2 | x10 ;
x12 = x6 ^ x11 ;
x13 = a5 ^ x5 ;
x14 = x13 & x8 ;
x15 = a5 & ~a4 ;
x16 = x3 ^ x14 ;
x17 = a6 | x16 ;
x18 = x15 ^ x17 ;
x19 = a2 | x18 ;
x20 = x14 ^ x19 ;
x21 = a1 & x20 ;
x22 = x12 ^ ~x21 ;
*out2 ^= x22 ;
x23 = x1 | x5 ;
x24 = x23 ^ x8 ;
x25 = x18 & ~x2 ;
x26 = a2 & ~x25 ;
x27 = x24 ^ x26 ;
x28 = x6 | x7 ;
x29 = x28 ^ x25 ;
x30 = x9 ^ x24 ;
x31 = x18 & ~x30 ;
x32 = a2 & x31 ;
x33 = x29 ^ x32 ;
x34 = a1 & x33 ;
x35 = x27 ^ x34 ;
*out4 ^= x35 ;
x36 = a3 & x28 ;
x37 = x18 & ~x36 ;
x38 = a2 | x3 ;
x39 = x37 ^ x38 ;
x40 = a3 | x31 ;
x41 = x24 & ~x37 ;
x42 = x41 | x3 ;
x43 = x42 & ~a2 ;
x44 = x40 ^ x43 ;
x45 = a1 & ~x44 ;
x46 = x39 ^ ~x45 ;
*out1 ^= x46 ;
x47 = x33 & ~x9 ;
x48 = x47 ^ x39 ;
x49 = x4 ^ x36 ;
x50 = x49 & ~x5 ;
x51 = x42 | x18 ;
x52 = x51 ^ a5 ;
x53 = a2 & ~x52 ;
x54 = x50 ^ x53 ;
545
x55 = a1 | x54 ;
x56 = x48 ^ ~x55 ;
*out3 ^= x56 ;
}
546
xor eax, ebx
mov esi, ebp
or esi, edx
mov DWORD PTR _x4$[esp+36], esi
and esi, DWORD PTR _a6$[esp+32]
mov DWORD PTR _x7$[esp+32], ecx
mov edx, esi
xor edx, eax
mov DWORD PTR _x6$[esp+36], edx
mov edx, DWORD PTR _a3$[esp+32]
xor edx, ebx
mov ebx, esi
xor ebx, DWORD PTR _a5$[esp+32]
mov DWORD PTR _x8$[esp+36], edx
and ebx, edx
mov ecx, edx
mov edx, ebx
xor edx, ebp
or edx, DWORD PTR _a6$[esp+32]
not ecx
and ecx, DWORD PTR _a6$[esp+32]
xor edx, edi
mov edi, edx
or edi, DWORD PTR _a2$[esp+32]
mov DWORD PTR _x3$[esp+36], ebp
mov ebp, DWORD PTR _a2$[esp+32]
xor edi, ebx
and edi, DWORD PTR _a1$[esp+32]
mov ebx, ecx
xor ebx, DWORD PTR _x7$[esp+32]
not edi
or ebx, ebp
xor edi, ebx
mov ebx, edi
mov edi, DWORD PTR _out2$[esp+32]
xor ebx, DWORD PTR [edi]
not eax
xor ebx, DWORD PTR _x6$[esp+36]
and eax, edx
mov DWORD PTR [edi], ebx
mov ebx, DWORD PTR _x7$[esp+32]
or ebx, DWORD PTR _x6$[esp+36]
mov edi, esi
or edi, DWORD PTR _x1$[esp+36]
mov DWORD PTR _x28$[esp+32], ebx
xor edi, DWORD PTR _x8$[esp+36]
mov DWORD PTR _x24$[esp+32], edi
xor edi, ecx
not edi
and edi, edx
mov ebx, edi
and ebx, ebp
xor ebx, DWORD PTR _x28$[esp+32]
547
xor ebx, eax
not eax
mov DWORD PTR _x33$[esp+32], ebx
and ebx, DWORD PTR _a1$[esp+32]
and eax, ebp
xor eax, ebx
mov ebx, DWORD PTR _out4$[esp+32]
xor eax, DWORD PTR [ebx]
xor eax, DWORD PTR _x24$[esp+32]
mov DWORD PTR [ebx], eax
mov eax, DWORD PTR _x28$[esp+32]
and eax, DWORD PTR _a3$[esp+32]
mov ebx, DWORD PTR _x3$[esp+36]
or edi, DWORD PTR _a3$[esp+32]
mov DWORD PTR _x36$[esp+32], eax
not eax
and eax, edx
or ebx, ebp
xor ebx, eax
not eax
and eax, DWORD PTR _x24$[esp+32]
not ebp
or eax, DWORD PTR _x3$[esp+36]
not esi
and ebp, eax
or eax, edx
xor eax, DWORD PTR _a5$[esp+32]
mov edx, DWORD PTR _x36$[esp+32]
xor edx, DWORD PTR _x4$[esp+36]
xor ebp, edi
mov edi, DWORD PTR _out1$[esp+32]
not eax
and eax, DWORD PTR _a2$[esp+32]
not ebp
and ebp, DWORD PTR _a1$[esp+32]
and edx, esi
xor eax, edx
or eax, DWORD PTR _a1$[esp+32]
not ebp
xor ebp, DWORD PTR [edi]
not ecx
and ecx, DWORD PTR _x33$[esp+32]
xor ebp, ebx
not eax
mov DWORD PTR [edi], ebp
xor eax, ecx
mov ecx, DWORD PTR _out3$[esp+32]
xor eax, DWORD PTR [ecx]
pop edi
pop esi
xor eax, ebx
pop ebp
mov DWORD PTR [ecx], eax
548
pop ebx
add esp, 20
ret 0
_s1 ENDP
549
mov r12, rsi
or r12, r15
not r13
and r13, rcx
mov r14, r12
and r14, rcx
mov rax, r14
mov r8, r14
xor r8, rbx
xor rax, r15
not rbx
and rax, rdx
mov rdi, rax
xor rdi, rsi
or rdi, rcx
xor rdi, r10
and rbx, rdi
mov rcx, rdi
or rcx, r9
xor rcx, rax
mov rax, r13
xor rax, QWORD PTR x36$1$[rsp]
and rcx, QWORD PTR a1$[rsp]
or rax, r9
not rcx
xor rcx, rax
mov rax, QWORD PTR out2$[rsp]
xor rcx, QWORD PTR [rax]
xor rcx, r8
mov QWORD PTR [rax], rcx
mov rax, QWORD PTR x36$1$[rsp]
mov rcx, r14
or rax, r8
or rcx, r11
mov r11, r9
xor rcx, rdx
mov QWORD PTR x36$1$[rsp], rax
mov r8, rsi
mov rdx, rcx
xor rdx, r13
not rdx
and rdx, rdi
mov r10, rdx
and r10, r9
xor r10, rax
xor r10, rbx
not rbx
and rbx, r9
mov rax, r10
and rax, QWORD PTR a1$[rsp]
xor rbx, rax
mov rax, QWORD PTR out4$[rsp]
xor rbx, QWORD PTR [rax]
550
xor rbx, rcx
mov QWORD PTR [rax], rbx
mov rbx, QWORD PTR x36$1$[rsp]
and rbx, rbp
mov r9, rbx
not r9
and r9, rdi
or r8, r11
mov rax, QWORD PTR out1$[rsp]
xor r8, r9
not r9
and r9, rcx
or rdx, rbp
mov rbp, QWORD PTR [rsp+80]
or r9, rsi
xor rbx, r12
mov rcx, r11
not rcx
not r14
not r13
and rcx, r9
or r9, rdi
and rbx, r14
xor r9, r15
xor rcx, rdx
mov rdx, QWORD PTR a1$[rsp]
not r9
not rcx
and r13, r10
and r9, r11
and rcx, rdx
xor r9, rbx
mov rbx, QWORD PTR [rsp+72]
not rcx
xor rcx, QWORD PTR [rax]
or r9, rdx
not r9
xor rcx, r8
mov QWORD PTR [rax], rcx
mov rax, QWORD PTR out3$[rsp]
xor r9, r13
xor r9, QWORD PTR [rax]
xor r9, r8
mov QWORD PTR [rax], r9
pop r15
pop r14
pop r13
pop r12
pop rdi
pop rsi
ret 0
s1 ENDP
551
Le compilateur n’a pas eu besoin d’allouer de l’espace sur la pile. x36 est synonyme
de a5.
Il existe cependant des CPUs qui possèdent beaucoup plus de GPR. Itanium possède
ainsi 128 registres.
1.37.2 ARM
Les instructions 64 bits sont apparues avec ARMv8.
Les pointeurs ont perdu mes faveurs au point que j’en viens à les
injurier. Si je cherche vraiment à utiliser au mieux les capacités de mon
ordinateur 64 bits, j’en conclus que je ferais mieux de ne pas utiliser
de pointeurs. Les registres de mon ordinateur sont sur 64 bits, mais
je n’ai que 2Go de RAM. Les pointeurs n’ont donc jamais plus de 32
bits significatifs. Et pourtant, chaque fois que j’utilise un pointeur, il
me coûte 64 bits ce qui double la taille de ma structure de données.
Pire, ils atterrissent dans mon cache et en gaspillent la moitié et cela
me coûte car le cache est cher.
Donc, si je cherche à grappiller, j’en viens à utiliser des tableaux
au lieu de pointeurs. Je rédige des macros compliquées qui peuvent
laisser l’impression à tort que j’utilise des pointeurs.
552
1.38 Travailler avec des nombres à virgule flottante
en utilisant SIMD
Bien sûr. le FPU est resté dans les processeurs compatible x86 lorsque les extensions
SIMD ont été ajoutées.
L’extension SIMD (SSE2) offre un moyen facile de travailler avec des nombres à
virgule flottante.
Le format des nombres reste le même (IEEE 754).
Donc, les compilateurs modernes (incluant ceux générant pour x86-64) utilisent les
instructions SIMD au lieu de celles pour FPU.
On peut dire que c’est une bonne nouvelle, car il est plus facile de travailler avec
elles.
Nous allons ré-utiliser les exemples de la section FPU ici: 1.25 on page 284.
int main()
{
printf ("%f\n", f(1.2, 3.4)) ;
};
x64
a$ = 8
b$ = 16
f PROC
divsd xmm0, QWORD PTR __real@40091eb851eb851f
mulsd xmm1, QWORD PTR __real@4010666666666666
addsd xmm0, xmm1
ret 0
f ENDP
Les valeurs en virgule flottante entrées sont passées dans les registres XMM0-XMM3,
tout le reste—via la pile 181 .
181. MSDN: Parameter Passing
553
a est passé dans XMM0, b—via XMM1.
Les registres XMM font 128-bit (comme nous le savons depuis la section à propos
de SIMD : 1.36 on page 524), mais les valeurs double font 64-bit, donc seulement la
moitié basse du registre est utilisée.
DIVSD est une instruction SSE qui signifie «Divide Scalar Double-Precision Floating-
Point Values » (Diviser des nombres flottants double-précision), elle divise une valeur
de type double par une autre, stockées dans la moitié basse des opérandes.
Les constantes sont encodées par le compilateur au format IEEE 754.
MULSD et ADDSD fonctionnent de même, mais font la multiplication et l’addition.
Le résultat de l’exécution de la fonction de type double est laissé dans le registre
XMM0.
a$ = 8
b$ = 16
f PROC
movsdx QWORD PTR [rsp+16], xmm1
movsdx QWORD PTR [rsp+8], xmm0
movsdx xmm0, QWORD PTR a$[rsp]
divsd xmm0, QWORD PTR __real@40091eb851eb851f
movsdx xmm1, QWORD PTR b$[rsp]
mulsd xmm1, QWORD PTR __real@4010666666666666
addsd xmm0, xmm1
ret 0
f ENDP
x86
Compilons cet exemple pour x86. Bien qu’il compile pour x86, MSVC 2012 utilise des
instructions SSE2:
554
movsd xmm0, QWORD PTR _a$[ebp]
divsd xmm0, QWORD PTR __real@40091eb851eb851f
movsd xmm1, QWORD PTR _b$[ebp]
mulsd xmm1, QWORD PTR __real@4010666666666666
addsd xmm0, xmm1
movsd QWORD PTR tv70[ebp], xmm0
fld QWORD PTR tv70[ebp]
mov esp, ebp
pop ebp
ret 0
_f ENDP
555
Essayons l’exemple optimisé dans OllyDbg :
556
Fig. 1.115: OllyDbg : DIVSD a calculé le quotient et l’a stocké dans XMM1
557
Fig. 1.116: OllyDbg : MULSD a calculé le produit et l’a stocké dans XMM0
558
Fig. 1.117: OllyDbg : ADDSD ajoute la valeur dans XMM0 à celle dans XMM1
559
Fig. 1.118: OllyDbg : FLD laisse le résultat de la fonction dans ST(0)
Nous voyons qu’OllyDbg montre les registres XMM comme des paires de nombres
double, mais seule la partie basse est utilisée.
Apparemment, OllyDbg les montre dans ce format car les instructions SSE2 (suf-
fixées avec -SD) sont exécutées actuellement.
Mais bien sûr, il est possible de changer le format du registre et de voir le contenu
comme 4 nombres float ou juste comme 16 octets.
560
1.38.2 Passer des nombres à virgule flottante via les argu-
ments
#include <math.h>
#include <stdio.h>
int main ()
{
printf ("32.01 ^ 1.54 = %lf\n", pow (32.01,1.54)) ;
return 0;
}
main PROC
sub rsp, 40 ; 00000028H
movsdx xmm1, QWORD PTR __real@3ff8a3d70a3d70a4
movsdx xmm0, QWORD PTR __real@40400147ae147ae1
call pow
lea rcx, OFFSET FLAT :$SG1354
movaps xmm1, xmm0
movd rdx, xmm1
call printf
xor eax, eax
add rsp, 40 ; 00000028H
ret 0
main ENDP
Il n’y a pas d’instruction MOVSDX dans les manuels Intel et AMD ( 12.1.4 on page 1327),
elle y est appelée MOVSD. Donc il y a deux instructions qui partagent le même nom
en x86 (à propos de l’autre lire: .1.6 on page 1346). Apparemment, les développeurs
de Microsoft voulaient arrêter cette pagaille, donc ils l’ont renommée MOVSDX. Elle
charge simplement une valeur dans la moitié inférieure d’un registre XMM.
pow() prends ses arguments de XMM0 et XMM1, et renvoie le résultat dans XMM0. Il est
ensuite déplacé dans RDX pour printf(). Pourquoi? Peut-être parce que printf()—
est une fonction avec un nombre variable d’arguments?
561
call pow
; le résultat est maintenant dans XMM0
mov edi, OFFSET FLAT :.LC2
mov eax, 1 ; nombre de registres vecteur passé
call printf
xor eax, eax
add rsp, 8
ret
.LC0 :
.long 171798692
.long 1073259479
.LC1 :
.long 2920577761
.long 1077936455
GCC génère une sortie plus claire. La valeur pour printf() est passée dans XMM0. À
propos, il y a un cas lorsque 1 est écrit dans EAX pour printf()—ceci implique qu’un
argument sera passé dans des registres vectoriels, comme le requiert le standard
[Michael Matz, Jan Hubicka, Andreas Jaeger, Mark Mitchell, System V Application
Binary Interface. AMD64 Architecture Processor Supplement, (2013)] 182 .
return b ;
};
int main()
{
printf ("%f\n", d_max (1.2, 3.4)) ;
printf ("%f\n", d_max (5.6, -4)) ;
};
x64
562
movaps xmm0, xmm1
$LN2@d_max :
fatret 0
d_max ENDP
MSVC sans optimisation génère plus de code redondant, mais il n’est toujours pas
très difficile à comprendre:
Toutefois, GCC 4.4.6 effectue plus d’optimisations et utilise l’instruction MAXSD («Re-
turn Maximum Scalar Double-Precision Floating-Point Value ») qui choisit la valeur
maximum!
563
x86
Presque la même chose, mais les valeurs de a et b sont prises depuis la pile et le
résultat de la fonction est laissé dans ST(0).
Si nous chargeons cet exemple dans OllyDbg, nous pouvons voir comment l’instruc-
tion COMISD compare les valeurs et met/efface les flags CF et PF :
564
Fig. 1.119: OllyDbg : COMISD a changé les flags CF et PF
Il n’y a pas moyen d’ajouter 1 à une valeur dans un registre XMM 128-bit, donc il
doit être placé en mémoire.
Il y a toutefois l’instruction ADDSD (Add Scalar Double-Precision Floating-Point Values
565
ajouter des valeurs scalaires à virgule flottante double-précision), qui peut ajouter
une valeur dans la moitié 64-bit basse d’un registre XMM en ignorant celle du haut,
mais MSVC 2012 n’est probablement pas encore assez bon 183 .
Néanmoins, la valeur est ensuite rechargée dans un registre XMM et la soustraction
est effectuée. SUBSD est «Subtract Scalar Double-Precision Floating-Point Values »
(soustraire des valeurs en virgule flottante double-précision), i.e., elle opère sur la
partie 64-bit basse d’un registre XMM 128-bit. Le résultat est renvoyé dans le registre
XMM0.
tv128 = -4
_tmp$ = -4
?float_rand@@YAMXZ PROC
push ecx
call ?my_rand@@YAIXZ
; EAX=valeur pseudo-aléatoire
and eax, 8388607 ; 007fffffH
or eax, 1065353216 ; 3f800000H
; EAX=valeur pseudo-aléatoire & 0x007fffff | 0x3f800000
; la stocker dans la pile locale:
mov DWORD PTR _tmp$[esp+4], eax
; la recharger comme un nombre à virgule flottante:
movss xmm0, DWORD PTR _tmp$[esp+4]
; soustraire 1.0:
subss xmm0, DWORD PTR __real@3f800000
; mettre la valeur dans ST0 en la plaçant dans une variable temporaire...
movss DWORD PTR tv128[esp+4], xmm0
; ... et en la rechargeant dans ST0:
fld DWORD PTR tv128[esp+4]
pop ecx
ret 0
?float_rand@@YAMXZ ENDP
Toutes les instructions ont le suffixe -SS, qui signifie «Scalar Single » (scalaire simple).
«Scalar » (scalaire) implique qu’une seule valeur est stockée dans le registre.
«Single » (simple184 ) signifie un type de donnée float.
183. À titre d’exercice, vous pouvez retravailler ce code pour éliminer l’usage de la pile locale
184. pour simple précision
566
1.38.6 Résumé
Seule la moitié basse des registres XMM est utilisée dans tous les exemples ici, pour
stocker un nombre au format IEEE 754.
Pratiquement, toutes les instructions préfixées par -SD («Scalar Double-Precision »)—
sont des instructions travaillant avec des nombres à virgule flottante au format IEEE
754, stockés dans la moitié 64-bit basse d’un registre XMM.
Et c’est plus facile que dans le FPU, sans doute parce que les extensions SIMD ont
évolué dans un chemin moins chaotique que celles FPU dans le passé. Le modèle de
pile de registre n’est pas utilisé.
Si vous voulez, essayez de remplacer double avec float
dans ces exemples, la même instruction sera utilisée, mais préfixée avec -SS («Sca-
lar Single-Precision » scalaire simple-précision), par exemple, MOVSS, COMISS, ADDSS,
etc.
«Scalaire » implique que le registre SIMD contienne seulement une valeur au lieu de
plusieurs.
Les instructions travaillant avec plusieurs valeurs dans un registre simultanément
ont «Packed » dans leur nom.
Inutile de dire que les instructions SSE2 travaillent avec des nombres 64-bit au for-
mat IEEE 754 (double), alors que la représentation interne des nombres à virgule
flottante dans le FPU est sur 80-bit.
C’est pourquoi la FPU produit moins d’erreur d’arrondi et par conséquent, le FPU
peut donner des résultats de calcul plus précis.
567
Ceci signifie ajouter 24 à la valeur dans X29 et charger la valeur à cette adresse.
Notez s’il vous plaît que 24 est à l’intérieur des parenthèses. La signification est
différente si le nombre est à l’extérieur des parenthèses:
ldr w4, [x1],28
Ceci signifie charger la valeur à l’adresse dans X1, puis ajouter 28 à X1.
ARM permet d’ajouter ou de soustraire une constante à/de l’adresse utilisée pour
charger.
Et il est possible de faire cela à la fois avant et après le chargement.
Il n’y a pas de tels modes d’adressage en x86, mais ils sont présents dans d’autres
processeurs, même sur le PDP-11.
Il y a une légende disant que les modes pré-incrémentation, post-incrémentation,
pré-décrémentation et post-décrémentation du PDP-11, sont «responsables » de l’ap-
parition du genre de constructions en langage C (qui a été développé sur PDP-11)
comme *ptr++, *++ptr, *ptr--, *--ptr.
À propos, ce sont des caractéristiques de C difficiles à mémoriser. Voici comment ça
se passe:
568
1.39.3 Charger une constante dans un registre
ARM 32-bit
Comme nous le savons déjà, toutes les instructions ont une taille de 4 octets en
mode ARM et de 2 octets en mode Thumb.
Mais comment peut-on charger une valeur 32-bit dans un registre, s’il n’est pas
possible de l’encoder dans une instruction?
Essayons:
unsigned int f()
{
return 0x12345678 ;
};
Nous voyons que la valeur est chargée dans le registre par parties, la partie basse
en premier (en utilisant MOVW), puis la partie haute (en utilisant MOVT).
Ceci implique que 2 instructions sont nécessaires en mode ARM pour charger une
valeur 32-bit dans un registre.
Ce n’est pas un problème, car en fait il n’y pas beaucoup de constantes dans du
code réel (excepté pour 0 et 1).
Est-ce que ça signifie que la version à deux instructions est plus lente que celle à
une instruction?
C’est discutable. Le plus souvent, les processeurs ARM modernes sont capable de
détecter de telle séquences et les exécutent rapidement.
D’un autre côté, IDA est capable de détecter ce genre de patterns dans le code et
désassemble cette fonction comme:
MOV R0, 0x12345678
BX LR
569
ARM64
uint64_t f()
{
return 0x12345678ABCDEF01 ;
};
MOVK signifie «MOV Keep » (déplacer garder), i.e., elle écrit une valeur 16-bit dans
le registre, sans affecter le reste des bits. Le suffixe LSL signifie décaler la valeur à
gauche de 16, 32 et 48 bits à chaque étape. Le décalage est fait avant le chargement.
Ceci implique que 4 instructions sont nécessaires pour charger une valeur de 64-bit
dans un registre.
Le nombre 1.5 a en effet été encodé dans une instruction 32-bit. Mais comment?
En ARM64, il y a 8 bits dans l’instruction FMOV pour encoder certains nombres à
virgule flottante.
L’algorithme est appelé VFPExpandImm() en [ARM Architecture Reference Manual,
ARMv8, for ARMv8-A architecture profile, (2013)]187 . Ceci est aussi appelé mini-
float 188 (mini flottant).
187. Aussi disponible en http://yurichev.com/mirrors/ARMv8-A_Architecture_Reference_
Manual_(Issue_A.a).pdf
188. Wikipédia
570
Nous pouvons essayer différentes valeurs. Le compilateur est capable d’encoder 30.0
et 31.0, mais il ne peut pas encoder 32.0, car 8 octets doivent être alloués pour ce
nombre au format IEEE 754:
double a()
{
return 32;
};
...>aarch64-linux-gnu-objdump.exe -d hw.o
...
0000000000000000 <main> :
0: a9bf7bfd stp x29, x30, [sp,#-16]!
4: 910003fd mov x29, sp
8: 90000000 adrp x0, 0 <main>
c : 91000000 add x0, x0, #0x0
10: 94000000 bl 0 <printf>
14: 52800000 mov w0, #0x0 // #0
18: a8c17bfd ldp x29, x30, [sp],#16
1c : d65f03c0 ret
...>aarch64-linux-gnu-objdump.exe -r hw.o
...
571
OFFSET TYPE VALUE
0000000000000008 R_AARCH64_ADR_PREL_PG_HI21 .rodata
000000000000000c R_AARCH64_ADD_ABS_LO12_NC .rodata
0000000000000010 R_AARCH64_CALL26 printf
...
572
(0x5F+1=0x60). Donc le nombre sous sa forme signée est -0x60. Multiplions -0x60
par 4 (car l’adresse est stockée dans l’opcode est divisée par 4) : ça fait -0x180. Main-
tenant calculons l’adresse de destination: 0x4005a0 + (-0x180) = 0x400420 (noter
s’il vous plaît: nous considérons l’adresse de l’instruction BL, pas la valeur courante
du PC, qui peut être différente!). Donc l’adresse de destination est 0x400420.
Plus d’informations relatives aux relogements en ARM64: [ELF for the ARM 64-bit
Architecture (AArch64), (2013)]189 .
Toutes les instructions MIPS, tout comme en ARM, ont une taille de 32-bit, donc il
n’est pas possible d’inclure une constante 32-bit dans une instruction.
Donc il faut utiliser au moins deux instructions: la première charge la partie haute
du nombre de 32-bit et la seconde effectue une opération OR, qui met effectivement
la partie 16-bit basse du registre de destination:
IDA reconnaît ce pattern de code, qui se rencontre fréquemment, donc, par commo-
dité, il montre la dernière instruction ORI comme la pseudo-instruction LI qui charge
soit disant un nombre entier de 32-bit dans le registre $V0.
La sortie de l’assembleur GCC a la pseudo instruction LI, mais il s’agit en fait ici de
LUI («Load Upper Immediate » charger la valeur immédiate en partie haute), qui
stocke une valeur 16-bit dans la partie haute du registre.
Regardons la sortie de objdump :
573
0: 3c021234 lui v0,0x1234
4: 03e00008 jr ra
8: 34425678 ori v0,v0,0x5678
Ceci est légèrement différent: LUI charge les 16-bit haut de global_var dans $2 (ou
$V0) et ensuite LW charge les 16-bit bas en l’ajoutant au contenu de $2:
...
global_var :
.word 305419896
...
.data
.globl global_var
global_var : .word 0x12345678 # DATA XREF: _f2
La sortie d’objdump est la même que la sortie assembleur de GCC: Affichons le code
de relogement du fichier objet:
...
574
0000000c <f2> :
c : 3c020000 lui v0,0x0
10: 8c420000 lw v0,0(v0)
14: 03e00008 jr ra
18: 00200825 move at,at ; slot de délai de branchement
1c : 00200825 move at,at
00000000 <global_var> :
0: 12345678 beq s1,s4,159e4 <f2+0x159d8>
...
objdump -r filename.o
...
...
Nous voyons que l’adresse de global_var est écrite dans les instructions LUI et LW
lors du chargement de l’exécutable: la partie haute de global_var se trouve dans la
première (LUI), la partie basse dans la seconde (LW).
575
Chapitre 2
Fondamentaux importants
2.1.1 Bit
Les valeurs booléennes sont une utilisation évidente des bits: 0 pour faux et 1 pour
vrai.
Plusieurs valeurs booléennes peuvent être regroupées en un mot : Un mot de 32 bits
contiendra 32 valeur booléennes, etc. On appelle bitmap ou bitfield un tel assem-
blage.
Cette approche engendre un surcoût de traitement: décalages, extraction, etc. A
l’inverse l’utilisation d’un mot (ou d’un type int) pour chaque booléen gaspille de
l’espace, au profit des performances.
Dans les environnements C/C++, la valeur 0 représente faux et toutes les autres
valeurs vrai. Par exemple:
if (1234)
printf ("toujours exécuté\n") ;
else
printf ("jamais exécuté\n") ;
576
input++;
};
2.1.2 Nibble
AKA demi-octet, tétrade. Représente 4 bits.
Toutes ces expressions sont toujours en usage.
Les demi-octets ont été utilisés par des CPU 4-bits tel que le Intel 4004 (utilisé dans
les calculatrices).
On notera que la représentation binary-coded decimal (BCD) a été utilisée pour repré-
senter les nombres sur 4 bits. L’entier 0 est représenté par la valeur 0b0000, l’entier
9 par 0b1001 tandis que les valeurs supérieures ne sont pas utilisées. La valeur déci-
male 1234 est ainsi représentée par 0x1234. Il est évident que cette représentation
n’est pas la plus efficace en matière d’espace.
Elle possède en revanche un avantage: la conversion des nombres depuis et vers
le format BCD est extrêmement simple. Les nombres au format BCD peuvent être
additionnés, soustraits, etc., au prix d’une opération supplémentaire de gestion des
demi-retenues. Les CPUs x86 proposent pour cela quelques instructions assez rares:
AAA/DAA (gestion de la demi-retenue après addition), AAS/DAS (gestion de la demi-
retenue après soustraction), AAM (après multiplication), AAD (après division).
Le support par les CPUs des nombres au format BCD est la raison d’être des half-carry
flag (sur 8080/Z80) et auxiliary flag (AF sur x86). Ils représentent la retenue générée
après traitement des 4 bits de poids faible (d’un octet). Le drapeau est utilisé par
les instructions de gestion de retenue ci-dessus.
Le livre [Peter Abel, IBM PC assembly language and programming (1987)] doit sa
popularité à la facilité de ces conversions. Hormis ce livre, l’auteur de ces notes n’a
jamais rencontré en pratique de nombres au format BCD, sauf dans certains nombres
magiques ( 5.6.1 on page 934), tels que lorsque la date de naissance d’un individu
est encodé sous la forme 0x19791011—qui n’est autre qu’un nombre au format BCD.
Étonnement, j’ai trouvé que des nombres encodés BCD sont utilisés dans le logiciel
SAP: https://yurichev.com/blog/SAP/. Certains nombres, prix inclus, sont enco-
dés en format BCD dans la base de données. Peut-être ont-ils utilisé ce format pour
être compatible avec d’anciens logiciels ou matériel?
Les instructions x86 destinées au traitement des nombres BCD ont parfois été utili-
sées à d’autres fins, le plus souvent non documentées, par exemple:
cmp al,10
sbb al,69h
das
1. Binary-Coded Decimal
577
Ce fragment de code abscons converties les nombres de 0 à 15 en caratères ASCII
’0’..’9’, ’A’..’F’.
Z80
Le processeur Z80 était un clone de la CPU 8 bits 8080 d’Intel. Par manque de place,
il utilisait une UAL de 4 bits. Chaque opération impliquant deux nombres de 8 bits
devait être traitée en deux étapes. Il en a découlé une utilisation naturelle des half-
carry flag.
2.1.3 Caractère
A l’heure actuelle, l’utilisation de 8 bits par caractère est pratique courante. Il n’en a
pas toujours été ainsi. Les cartes perforées utilisées pour les télétypes ne pouvaient
comporter que 5 ou 6 perforations par caractères, et donc autant de bits.
Le terme octet met l’accent sur l’utilisation de 8 bits.: fetchmail est un de ceux qui
utilise cette terminologie.
Sur les architectures à 36 bits, l’utilisation de 9 bits par caractère a été utilisée: un
mot pouvait contenir 4 caractères. Ceci explique peut-être que le standard C/C++
indique que le type char doit supporter au moins 8 bits, mais que l’utilisation d’un
nombre plus importants de bits est autorisé.
Par exemple, dans l’un des premiers ouvrage sur le langage C 2 , nous trouvons :
char one byte character (PDP-11, IBM360 : 8 bits ; H6070 : 9 bits)
H6070 signifie probablement Honeywell 6070, qui comprenait des mots de 36 bits.
La représentation ASCII des caractères sur 7 bits constitue un standard, qui sup-
porte donc 128 caractères différents. Les premiers logiciels de transport de mails
fonctionnaient avec des codes ASCII sur 7 bits. Le standard MIME3 nécessitait donc
l’encodage des messages rédigés avec des alphabets non latins. Le code ASCII sur
7 bits a ensuite été augmenté d’un bit de parité qui a aboutit à la représentation sur
8 bits.
Les clefs de chiffrage utilisées par Data Encryption Standard (DES4 ) comportent 56
bits, soit 8 groupes de 7 bits ce qui laisse un espace pour un bit de parité dans
chaque groupe.
La mémorisation de la table ASCII est inutile. Il suffit de se souvenir de certains in-
tervalles. [0..0x1F] sont les caractères de contrôle (non imprimables). [0x20..0x7E]
sont les caractères imprimables. Les codes à partir de la valeur 0x80 sont géné-
ralement utilisés pour les caractères non latins et pour certains caractères pseudo
graphiques.
2. https://yurichev.com/mirrors/C/bwk-tutor.html
3. Multipurpose Internet Mail Extensions
4. Data Encryption Standard
578
Quelques valeurs typiques à mémoriser sont : 0 (terminateur d’une chaîne de carac-
tères en C, '\0' et C/C++) ; 0xA ou 10 (fin de ligne, '\n' en C/C++) ; 0xD ou 13
(retour chariot, '\r' en C/C++).
0x20 (espace).
CPUs 8 bits
Les processeurs x86 - descendants des CPUs 8080 8 bits - supportent la manipu-
lation d’octet(s) au sein des registres. Les CPUs d’architecture RISC telles que les
processeurs ARM et MIPS n’offrent pas cette possibilité.
2.1.6 Mot
mot Le terme de ’mot’ est quelque peu ambigu et dénote en général un type de
données dont la taille correspond à celle d’un GPR. L’utilisation d’octets est pratique
pour le stockage des caractères, mais souvent inadapté aux calculs arithmétiques.
C’est pourquoi, nombre de CPUs possèdent des GPRs dont la taille est de 16, 32 ou
64 bits. Les CPUs 8 bits tels que le 8080 et le Z80 proposent quant à eux de travailler
sur des paires de registres 8 bits, dont chacune constitue un pseudo-registre de 16
579
bits. (BC, DE, HL, etc.). Les capacités des paires de registres du Z80 en font, en
quelque sorte, un émulateur d’une CPU 16 bits.
En règle générale, un CPU présenté comme ”CPU n-bits” possède des GPRs dont la
taille est de n bits.
À une certaine époque, les disques durs et les barettes de RAM étaient caractérisés
comme ayant n kilo-mots et non pas b kilooctets/megaoctets.
Par exemple, Apollo Guidance Computer possède 2048 mots de RAM. S’agissant d’un
ordinateur 16 bits, il y avait donc 4096 octets de RAM.
La mémoire magnétique du TX-0 était de 64K mots de 18 bits, i.e., 64 kilo-mots.
DECSYSTEM-2060 pouvait supporter jusqu’à 4096 kilo mots de solid state memory
(i.e., hard disks, tapes, etc). S’agissant d’un ordinateur 36 bits, cela représentait
18432 kilo octets ou 18 mega octets.
En fait, pourquoi auriez-vous besoin d’octets si vous avez des mots? Surtout pour le
traitement des chaînes de texte. Les mots peuvent être utilisés dans presque toutes
les autres situations.
L’accès le plus rapide à une variable s’effectue lorsqu’elle est contenue dans un
GPR, plus même qu’un ensemble de bits, et parfois même plus rapide qu’un octet
(puisqu’il n’est pas besoin d’isoler un bit ou un octet au sein d’un GPR). Ceci reste
vrai même lorsque le registre est utilisé comme compteur d’itération d’une boucle
de 0 à 99.
En langage assembleur x86, un mot représente 16 bits, car il en était ainsi sur les
processeurs 8086 16 bits. Un Double word représente 32 bits, et un quad word 64
bits. C’est pourquoi, les mots de 16 bits sont déclarés par DW en assembleur x86,
ceux de 32 bits par DD et ceux de 64 bits par DQ.
Dans les architectures ARM, MIPS, etc... un mot représente 32 bits, on parlera alors
de demi-mot pour les types sur 16 bits. En conséquence, un double word sur une
architecture RISC 32 bits est un type de données qui représente 64 bits.
5. http://yurichev.com/blog/typeless/
580
GDB utilise la terminologie suivante : demi-mot pour 16 bits, mot pour 32 bits et mot
géant pour 64 bits.
Les environnements C/C++ 16 bits sur PDP-11 et MS-DOS définissent le type long
comme ayant une taille de 32 bits, ce qui serait sans doute une abréviation de long
word ou de long int.
Les environnements C/C++ 32 bits définissent le type long long dont la taille est de
64 bits.
L’ambiguïté du terme mot est donc désormais évidente.
Certains affirment que le type int ne doit jamais être utilisé, l’ambiguïté de sa défini-
tion pouvant être génératrice de bugs. A une certaine époque, la bibliothèque bien
connue lzhuf utilisais le type int et fonctionnait parfaitement sur les architectures
16 bits. Portée sur une architecture pour laquelle le type int représentait 32 bits, elle
pouvait alors crasher: http://yurichev.com/blog/lzhuf/.
Des types de données moins ambigus sont définis dans le fichier stdint.h : uint8_t,
uint16_t, uint32_t, uint64_t, etc.
Donald E. Knuth fut l’un de ceux qui proposa6 d’utiliser pour ces différents types des
dénominations aux consonances distinctes: octet/wyde/tetrabyte/octabyte. Cette
pratique est cependant moins courante que celle consistant à inclure directement
dans le nom du type les termes u (unsigned) ainsi que le nombre de bits.
En dépit de l’ambiguïté du terme mot, les ordinateurs modernes restent conçus sur
ce concept: la RAM ainsi que tous les niveaux de mémoire cache demeurent or-
ganisés en mots et non pas en octets. La notion d’octet reste prépondérante en
marketing.
Les accès aux adresses mémoire et cache alignées sur des frontières de mots est
souvent plus performante que lorsque l’adresse n’est pas alignée.
Afin de rendre performante l’utilisation des structures de données, il convient tou-
jours de de prendre en compte la longueur du mot du CPU sur lequel sera exécuté
le programme lors de la définition des structures de données. Certains compilateurs
- mais pas tous - prennent en charge cet alignement.
581
Le processeur 8 bits Z80 peut adresser 216 octets, en utilisant une paire de registres 8
bits ou certains registres spécialisés (IX, IY). En outre sur ce processeur les registres
SP et PC contiennent 16 bits.
Le super calculateur Cray-1 possèdent des registres généraux de 64-bit, et des re-
gistres d’adressage de 24 bits. Il peut donc adresser 224 octets, soit (16 mega mots
ou 128 mega octets). La RAM coûtait très cher, et un Cray typique avait 1048576
(0x100000) mots de RAM, soit 8MB. Dans les années 70, la RAM était très coûteuse.
Il paraissait alors inconcevable qu’un tel calculateur atteigne les 128 Mo. Dès lors
pourquoi aurait-on utilisé des registres 64 bits pour l’adressage?
Les processeurs 8086/8088 utilisent un schéma d’adressage particulièrement bi-
zarre: Les valeurs de deux registres de 16 bits sont additionnées de manière étrange
afin d’obtenir une adresse sur 20 bits. S’agirait-il d’une sorte de virtualisation gad-
get ( 11.6 on page 1309) ? Les processeurs 8086 pouvaient en effet faire fonctionner
plusieurs programmes côte à côte (mais pas simultanément bien sûr).
Les premiers processeurs ARM1 implémentent un artefact intéressant:
( http://www.righto.com/2015/12/reverse-engineering-arm1-ancestor-of.html
)
En conséquence, il n’est pas possible d’affecter au registre PC une valeur dont l’un
des deux bits de poids faible est différent de 0, pas plus qu’il n’est possible de posi-
tionner à 1 l’un des 6 bits de poids fort.
L’architecture x86-64 utilise des pointeurs et des adresses sur 64 bits, cependant en
interne la largeur du bus d’adresse est de 48 bits, (ce qui est suffisant pour adresser
256 Tera octets de RAM).
2.1.8 Nombres
A quoi sont utilisés les nombres ?
Lorsque vous constatez que la valeur d’un registre de la CPU est modifié selon un
certain motif, vous pouvez chercher à comprendre à quoi correspond ce motif. La ca-
pacité à déterminer le type de données qui découle de ce motif est une compétence
précieuse pour le reverse engineer .
Booléen
582
Compteur de boucle, index dans un tableau
Nombres signés
Si vous constatez qu’une variable contient parfois des nombres très petits et d’autre
fois des nombres très grands, tels que 0, 1, 2, 3, et 0xFFFFFFFF, 0xFFFFFFFE, 0xFFFFFFFD,
il est probable qu’il s’agisse d’un entier signé sous forme de two’s complement ( 2.2
on page 585) auquel cas les 3 dernières valeurs représentent en réalité -1, -2, -3.
Il existe des nombres tellement grands, qu’il existe une notation spéciale pour les re-
présenter (Notation exponentielle de Knuth’s) De tels nombres sont tellement grands
qu’ils s’avèrent peu pratiques pour l’ingénierie, les sciences ou les mathématiques.
La plupart des ingénieurs et des scientifiques sont donc ravis d’utiliser la notation
IEEE 754 pour les nombres flottants à double précision, laquelle peut représenter des
valeurs allant jusqu’à 1.8 ⋅ 10308 . (En comparaison, le nombre d’atomes dans l’univers
observable est estimé être entre 4 ⋅ 1079 et 4 ⋅ 1081 .)
De fait, la limite supérieure des nombres utilisés dans les opérations concrètes est
très très inférieure.
Pareil à l’époque de MS-DOS: les int 16 bits étaient utilisés pratiquement pour tout
(indice de tableau, compteur de boucle), tandis que le type long sur 32 bits ne l’était
que rarement.
Durant l’avènement de l’architecture x86-64, il fut décidé que le type int conserve-
rait une taille de 32 bits, probablement parce que l’utilisation d’un type int de 64
bits est encore plus rare.
Je dirais que les nombre sur 16 bits qui couvrent l’intervalle 0..65535 sont probable-
ment les nombres les plus utilisés en informatique.
Ceci étant, si vous rencontrez des nombres sur 32 bits particulièrement élevé tels
que 0x87654321, il existe une bonne chance qu’il s’agisse :
• Il peut toujours s’agir d’un entier sur 16 bits, mais signé lorsque la valeur est
entre 0xFFFF8000 (-32768) et 0xFFFFFFFF (-1).
• une adresse mémoire (ce qui peut être vérifié en utilisant les fonctionnalités de
gestion mémoire du débogueur).
• des octets compactés (ce qui peut être vérifié visuellement).
• un ensemble de drapeaux binaires.
• de la cryptographie (amateur).
• un nombre magique ( 5.6.1 on page 934).
• un nombre flottant utilisant la représentation IEEE 754 (également vérifiable).
583
Il en va à peu près de même pour les valeurs sur 64 bits.
…donc un int sur 16 bits est suffisant pour à peu près n’importe quoi?
Addresse
584
Il convient de savoir que win32 n’utilise pas les adresses inférieures à 0x10000, donc
si vous observez un nombre inférieur à cette valeur, ce ne peut être une adresse (voir
aussi https://msdn.microsoft.com/en-us/library/ms810627.aspx).
De toute manière, beaucoup de débogueurs savent vous indiquer si la valeur conte-
nue dans un registre peut représenter l’adresse d’un élément. OllyDbg peut éga-
lement vous afficher le contenu d’une chaîne de caractères ASCII si la valeur du
registre est l’adresse d’une telle chaîne.
Drapeaux
Si vous observez une valeur pour laquelle un ou plusieurs bits changent de valeur
de temps en temps tel que 0xABCD1234 → 0xABCD1434 et retour, il s’agit probable-
ment d’un ensemble de drapeaux ou bitmap.
Compactage de caractères
585
binaire hexadécimal non-signé signé
01111111 0x7f 127 127
01111110 0x7e 126 126
...
00000110 0x6 6 6
00000101 0x5 5 5
00000100 0x4 4 4
00000011 0x3 3 3
00000010 0x2 2 2
00000001 0x1 1 1
00000000 0x0 0 0
11111111 0xff 255 -1
11111110 0xfe 254 -2
11111101 0xfd 253 -3
11111100 0xfc 252 -4
11111011 0xfb 251 -5
11111010 0xfa 250 -6
...
10000010 0x82 130 -126
10000001 0x81 129 -127
10000000 0x80 128 -128
La différence entre nombres signé et non-signé est que si l’on représente 0xFFFFFFFE
et 0x00000002 comme non signées, alors le premier nombre (4294967294) est plus
grand que le second (2). Si nous les représentons comme signés, le premier devient
−2, et il est plus petit que le second. C’est la raison pour laquelle les sauts condition-
nels ( 1.18 on page 163) existent à la fois pour des opérations signées (p. ex. JG, JL)
et non-signées (JA, JB).
Par souci de simplicité, voici ce qu’il faut retenir:
• Les nombres peuvent être signés ou non-signés.
• Types C/C++ signés:
– int64_t (-9,223,372,036,854,775,808 .. 9,223,372,036,854,775,807) (- 9.2.. 9.2
quintillions) ou
0x8000000000000000..0x7FFFFFFFFFFFFFFF),
– int (-2,147,483,648..2,147,483,647 (- 2.15.. 2.15Gb) ou 0x80000000..0x7FFFFFFF),
– char (-128..127 ou 0x80..0x7F),
– ssize_t.
Non-signés:
– uint64_t (0..18,446,744,073,709,551,615 ( 18 quintillions) ou 0..0xFFFFFFFFFFFFFFFF),
– unsigned int (0..4,294,967,295 ( 4.3Gb) ou 0..0xFFFFFFFF),
– unsigned char (0..255 ou 0..0xFF),
– size_t.
586
• Les types signés ont le signe dans le MSB : 1 signifie «moins », 0 signifie «plus ».
• Étendre à un type de données plus large est facile: 1.34.5 on page 520.
• La négation est simple: il suffit d’inverser tous les bits et d’ajouter 1.
Nous pouvons garder à l’esprit qu’un nombre de signe opposé se trouve de
l’autre côté, à la même distance de zéro. L’addition d’un est nécessaire car
zéro se trouve au milieu.
• Les opérations d’addition et de soustraction fonctionnent bien pour les valeurs
signées et non-signées. Mais pour la multiplication et la division, le x86 possède
des instructions différentes: IDIV/IMUL pour les signés et DIV/MUL pour les non-
signés.
• Voici d’autres instructions qui fonctionnent avec des nombres signés:
CBW/CWD/CWDE/CDQ/CDQE ( .1.6 on page 1349), MOVSX ( 1.23.1 on page 263),
SAR ( .1.6 on page 1354).
Une table avec quelques valeurs négatives et positives ( ?? on page ??) ressemble
à un thermomètre avec une échelle Celsius. C’est pourquoi l’addition et la soustrac-
tion fonctionnent bien pour les nombres signés et non-signés: si le premier opérande
est représenté par une marque sur un thermomètre, et que l’on doit ajouter un se-
cond opérande, et qu’il est positif, nous devons juste augmenter la marque sur le
thermomètre de la valeur du second opérande. Si le second opérande est négatif,
alors nous baissons la marque de la valeur absolue du second opérande.
L’addition de deux nombres négatifs fonctionne comme suit. Par exemple, nous de-
vons ajouter -2 et -3 en utilisant des registres 16-bit. -2 et -3 sont respectivement
0xfffe et 0xfffd. si nous les ajoutons comme nombres non-signés, nous obtenons
0xfffe+0xfffd=0x1fffb. Mais nous travaillons avec des registres 16-bit, le résultat
est tronqué, le premier 1 est perdu, et il reste 0xfffb et c’est -5. Ceci fonctionne
car -2 (ou 0xfffe) peut être représenté en utilisant des mots simples comme suit: “il
manque 2 à la valeur maximale d’un registre 16-bit + 1”. -3 peut être représenté
comme “…il manque 3 à la valeur maximale jusqu’à …”. La valeur maximale d’un
registre 16-bit + 1 est 0x10000. Pendant l’addition de deux nombres et en tronquant
modulo 216 , il manquera 2 + 3 = 5.
587
Donc, le compilateur C/C++ peut utiliser indifféremment ces deux instructions.
Mais IMUL est plus flexible que MUL, car elle prend n’importe quel(s) registre(s) comme
source, alors que MUL nécessite que l’un des multiplicandes soit stocké dans le re-
gistre AX/EAX/RAX Et même plus que ça: MUL stocke son résultat dans la paire EDX:EAX
en environnement 32-bit, ou RDX:RAX dans un 64-bit, donc elle calcule toujours le
résultat complet. Au contraire, il est possible de ne mettre qu’un seul registre de
destination lorsque l’on utilise IMUL, au lieu d’une paire, et alors le CPU calculera
seulement la partie basse, ce qui fonctionne plus rapidement [voir Torborn Granlund,
Instruction latencies and throughput for AMD and Intel x86 processors8 ].
Cela étant considéré, les compilateurs C/C++ peuvent générer l’instruction IMUL
plus souvent que MUL.
Néanmoins, en utilisant les fonctions intrinsèques du compilateur, il est toujours pos-
sible d’effectuer une multiplication non signée et d’obtenir le résultat complet. Ceci
est parfois appelé multiplication étendue. MSVC a une fonction intrinsèque pour ceci,
appelée __emul9 et une autre: _umul12810 . GCC offre le type de données __int128,
et dans le cas de multiplicandes 64-bit, ils sont déjà promus en 128-bit, puis le pro-
duit est stocké dans une autre valeur __int128, puis le résultat est décalé de 64 bits
à droite, et vous obtenez la moitié haute du résultat11 .
Le maximum d’un nombre non signé est simplement un nombre où tous les bits sont
mis: 0xFF....FF (ceci est -1 si le mot est traité comme un entier signé). Donc, vous
prenez un mot, vous mettez tous les bits et vous obtenez la valeur:
8. http://yurichev.com/mirrors/x86-timing.pdf
9. https://msdn.microsoft.com/en-us/library/d2s81xt0(v=vs.80).aspx
10. https://msdn.microsoft.com/library/3dayytw9%28v=vs.100%29.aspx
11. Exemple: http://stackoverflow.com/a/13187798
12. https://msdn.microsoft.com/en-us/library/windows/desktop/aa383718(v=vs.85).aspx
588
#include <stdio.h>
int main()
{
unsigned int val=~0; // changer à "unsigned char" pour obtenir la
valeur maximale pour un octet 8-bit non-signé
// 0-1 fonctionnera aussi, ou juste -1
printf ("%u\n", val) ; // ;
Le nombre signé minimum est encodé en 0x80....00, i.e., le bit le plus significatif est
mis, tandis que tous les autres sont à zéro. Le nombre maximum signé est encodé
de la même manière, mais tous les bits sont inversés: 0x7F....FF.
Déplaçons un seul bit jusqu’à ce qu’il disparaisse:
#include <stdio.h>
int main()
{
signed int val=1; // changer à "signed char" pour trouver les valeurs
pour un octet signé
while (val !=0)
{
printf ("%d %d\n", val, ~val) ;
val=val<<1;
};
};
La sortie est:
...
536870912 -536870913
1073741824 -1073741825
-2147483648 2147483647
2.2.3 -1
Vous savez maintenant que −1 est lorsque tous les bits sont mis à 1. Souvent, vous
pouvez trouver la constante −1 dans toute sorte de code qui nécessite une constante
avec tous les bits à 1, par exemple, un masque.
Par exemple: 3.18.1 on page 692.
589
2.3 Dépassement d’entier
J’ai intentionnellement mis cette section après celle sur la représentation des nombres
signés.
Tout d’abord, regardons l’implémentation de la fonction itoa() dans [Brian W. Kerni-
ghan, Dennis M. Ritchie, The C Programming Language, 2ed, (1988)] :
void itoa(int n, char s[])
{
int i, sign ;
if ((sign = n) < 0) /* record sign */
n = -n ; /* make n positive */
i = 0;
do { /* generate digits in reverse order */
s[i++] = n % 10 + '0' ; /* get next digit */
} while ((n /= 10) > 0) ; /* delete it */
if (sign < 0)
s[i++] = '-' ;
s[i] = '\0' ;
strrev(s) ;
}
590
De [Brian W. Kernighan, Dennis M. Ritchie, The C Programming Language, 2ed, (1988)] :
La réponse est: la fonction ne peut pas traiter le plus grand nombre négatif (INT_MIN
ou 0x80000000 ou -2147483648) correctement.
Comment changer le signe? Inverser tous les bits et ajouter 1. Si vous inversez tous
les bits de la valeur INT_MIN (0x80000000), ça donne 0x7fffffff. Ajouter 1 et vous
obtenez à nouveau 0x80000000. C’est un artefact important du système de complé-
ment à deux.
Lectures complémentaires:
• blexim – Basic Integer Overflows13
• Yannick Moy, Nikolaj Bjørner, et David Sielaff – Modular Bug-finding for Integer
Overflows in the Large: Sound, Efficient, Bit-precise Static Analysis14
2.4 AND
Autrement dit, ce code vérifie si il y a un bit mis parmi les 12 bits inférieurs. Un effet
de bord, les 12 bits inférieurs sont toujours le reste de la division d’une valeur par
4096 (car une division par 2n est simplement un décalage à droite, et les bits décalés
(et perdus) sont les bits du reste).
Même principe si vous voulez tester si un nombre est pair ou impair:
13. http://phrack.org/issues/60/10.html
14. https://yurichev.com/mirrors/SMT/z3prefix.pdf
591
if (value&1)
// odd
else
// even
Ceci est la même chose que de diviser par 2 et de prendre le reste de 1-bit.
On peut remarquer que les caractères cyrilliques sont alloués presque dans la même
séquence que les caractères Latin. Ceci conduit à une propriété importante: si tout
les 8ème bits d’un texte encodé en Cyrillique sont mis à zéro, le texte est transformé
en un texte translittéré avec des caractères latin à la place de cyrillique. Par exemple,
une phrase en Russe:
…s’il est encodé en KOI-8R et que le 8ème bit est supprimé, il est transformé en:
592
…ceci n’est peut-être très esthétiquement attrayant, mais ce texte est toujours li-
sible pour les gens de langue maternelle russe.
De ce fait, un texte cyrillique encodé en KOI-8R, passé à travers un vieux service
7-bit survivra à la translittération, et sera toujours un texte lisible.
Supprimer le 8ème bit transpose automatiquement un caractère de la seconde moi-
tié de n’importe quelle table ASCII 8-bit dans la première, à la même place (regardez
la flèche rouge à droite de la table). Si le caractère était déjà dans la première moitié
(i.e., il était déjà dans la table ASCII 7-bit standard), il n’est pas transposé.
Peut-être qu’un texte translittéré est toujours récupérable, si vous ajoutez le 8ème
bit aux caractères qui ont l’air d’avoir été translittérés.
L’inconvénient est évident: les caractères cyrilliques alloués dans la table KOI-8R
ne sont pas dans le même ordre dans l’alphabet Russe/Bulgare/Ukrainien/etc., et ce
n’est pas utilisable pour le tri, par exemple.
593
L048C : DEFM "MERGE erro" ; Report 'a'.
DEFB 'r'+$80
L0497 : DEFM "Wrong file typ" ; Report 'b'.
DEFB 'e'+$80
L04A6 : DEFM "CODE erro" ; Report 'c'.
DEFB 'r'+$80
L04B0 : DEFM "Too many bracket" ; Report 'd'.
DEFB 's'+$80
L04C1 : DEFM "File already exist" ; Report 'e'.
DEFB 's'+$80
( http://www.matthew-wilson.net/spectrum/rom/128_ROM0.html )
Le dernier caractère a le bit le plus significatif mis, ce qui marque la fin de la chaîne.
Vraisemblablement que ça a été fait pour économiser de l’espace. Les vieux ordina-
teurs 8-bit avaient une mémoire très restreinte.
Les caractères de tous les messages sont toujours dans la table ASCII 7-bit standard,
donc il est garanti que le 7ème bit n’est jamais utilisé pour les caractères.
Pour afficher une telle chaîne, nous devons tester le MSB de chaque octet, et s’il est
mis, nous devons l’effacer, puis afficher le caractère et arrêter. Voici un exemple en
C:
unsigned char hw[]=
{
'H',
'e',
'l',
'l',
'o'|0x80
};
void print_string()
{
for (int i=0; ;i++)
{
if (hw[i]&0x80) // check MSB
{
// clear MSB
// (en d'autres mots, les effacer tous, mais laisser
les 7 bits inférieurs intacts)
printf ("%c", hw[i] & 0x7F) ;
// stop
break ;
};
printf ("%c", hw[i]) ;
};
};
Maintenant ce qui est intéressant, puisque le 7ème bit est le bit le plus significa-
tif (dans un octet), c’est que nous pouvons le tester, le mettre et le supprimer en
utilisant des opérations arithmétiques au lieu de logiques:
594
Je peux récrire mon exemple en C:
unsigned char hw[]=
{
'H',
'e',
'l',
'l',
'o'+0x80
};
void print()
{
for (int i=0; ;i++)
{
// hw[] doit avoir le type 'unsigned char'
if (hw[i] >= 0x80) // tester le MSB
{
printf ("%c", hw[i]-0x80) ; // clear MSB
// stop
break ;
};
printf ("%c", hw[i]) ;
};
};
Par défaut, char est un type signé en C/C++, donc pour le comparer avec une va-
riable comme 0x80 (qui est négative (−128) si elle est traitée comme signée), nous
devons traiter chaque caractère dans le texte du message comme non signé.
Maintenant si le 7ème bit est mis, le nombre est toujours supérieur ou égal à 0x80.
Si le 7ème est à zéro, le nombre est toujours plus petit que 0x80.
Et même plus que ça: si le 7ème bit est mis, il peut être effacé en soustrayant 0x80,
rien d’autre. Si il n’est pas mis avant, toutefois, la soustraction va détruire d’autres
bits.
De même, si le 7ème est à zéro, il est possible de le mettre en ajoutant 0x80. Mais
s’il est déjà mis, l’opération d’addition va détruire d’autres bits.
En fait, ceci est valide pour n’importe quel bit. Si le 4ème bit est à zéro, vous pouvez
le mettre juste en ajoutant 0x10: 0x100+0x10 = 0x110. Si le 4ème bit est mis, vous
pouvez l’effacer en soustrayant 0x10: 0x1234-0x10 = 0x1224.
Ça fonctionne, car il n’y a pas de retenue générée pendant l’addition/soustraction.
Elle le serait, toutefois, si le bit est déjà à 1 avant l’addition, ou à 0 avant la sous-
traction.
De même, addition/soustraction peuvent être remplacées en utilisant une opération
OR/AND si deux conditions sont réunies: 1) vous voulez ajouter/soustraire un nombre
de la forme 2n ; 2) la valeur du bit d’indice n dans la valeur source est 0/1.
Par exemple, l’addition de 0x20 est la même chose que OR-er la valeur avec 0x20
sous la condition que ce bit est à zéro avant: 0x1204|0x20 = 0x1204+0x20 = 0x1224.
595
La soustraction de 0x20 est la même chose que AND-er la valeur avec 0x20 (0x....FFDF),
mais si ce bit est mis avant: 0x1234&(~0x20) = 0x1234&0xFFDF = 0x1234-0x20 =
0x1214.
À nouveau, ceci fonctionne parce qu’il n’y a pas de retenue générée lorsque vous
ajoutez le nombre 2n et que ce bit n’est pas à 1 avant.
Cette propriété de l’algèbre booléenne est importante, elle vaut la peine d’être com-
prise et gardée à l’esprit.
Un autre exemple dans ce livre: 3.19.3 on page 706.
596
2.6.3 Chiffrement
XOR est beaucoup utilisé à la fois par le chiffrement amateur ( 9.1 on page 1213) et
réel (au moins dans le réseau de Feistel).
XOR est trés pratique ici car: cipher_text = plain_text ⊕ key et alors: (plain_text ⊕ key) ⊕
key = plain_text.
2.6.4 RAID4
RAID4 offre une méthode très simple pour protéger les disques dur. Par exemple, il
y a quelques disques (D1 , D2 , D3 , etc.) et un disque de parité (P ). Chaque bit/octet
écrit sur le disque de parité est calculé et écrit au vol:
P = D1 ⊕ D2 ⊕ D3 (2.1)
Si n’importe lequel des disques est défaillant, par exemple, D2 , il est restauré en
utilisant la même méthode:
D2 = D1 ⊕ P ⊕ D3 (2.2)
Qu’est ce que X et Y valent à chaque étape? Gardez à l’esprit cette règle simple:
(X ⊕ Y ) ⊕ Y = X pour toutes valeurs de X et Y.
597
Regardons, après la 1ère étape X vaut X⊕Y ; après la 2ème étape Y vaut Y ⊕(X⊕Y ) =
X ; après la 3ème étape X vaut (X ⊕ Y ) ⊕ X = Y .
Difficile de dire si on doit utiliser cette astuce, mais elle est un bon exemple de
démonstration des propriétés de XOR.
L’article de Wikipédia (https://en.wikipedia.org/wiki/XOR_swap_algorithm) donne
d’autres explication: l’addition et la soustraction peuvent être utilisées à la place de
XOR:
X = X + Y
Y = X - Y
X = X - Y
598
À nouveau, il est difficile de dire si quelqu’un doit utiliser ce truc rusé, mais c’est une
bonne démonstration des propriétés de XOR. Avec l’algorithme d’échange avec XOR,
l’article de Wikipédia montre des méthodes pour utiliser l’addition et la soustraction
au lieu de XOR: https://en.wikipedia.org/wiki/XOR_linked_list.
int main()
{
int a=123;
#define C 123^456
a=a^C ;
printf ("%d\n", a) ;
a=a^C ;
printf ("%d\n", a) ;
a=a^C ;
printf ("%d\n", a) ;
};
Ça fonctionne car 123 ⊕ 123 ⊕ 456 = 0 ⊕ 456 = 456 et 456 ⊕ 123 ⊕ 456 = 456 ⊕ 456 ⊕ 123 =
0 ⊕ 123 = 123.
On pourrait discuter si ça vaut la peine ou non, particulièrement si on a à l’esprit la
lisibilité du code. Mais ceci est une autre démonstration des propriétés de XOR.
599
Voici un moyen de compresser une position d’échecs dans une valeur 64-bit, appelée
le hachage de Zobrist:
// nous avons un échiquier de 8*8 et 12 pièces (6 pour le côté blanc et 6
pour le noir)
uint64_t hash ;
if (piece !=0)
hash=hash^table[piece][row][col];
};
return hash ;
2.6.9 À propos
Le OR usuel est parfois appelé OU inclusif (ou même IOR), par opposition au OU
exclusif. C’est ainsi dans la bibliothèque Python operator : il y est appelé operator.ior.
600
XOR reg, reg, peu importe ce qui se trouvait précédemment dans le registre, efface
tous les bits et se comporte donc comme MOV reg, 0.
Cette instruction est aussi connue en tant qu’« instruction NSA18 » à cause de ru-
meurs:
2.8 Endianness
L’endianness (boutisme) est la façon de représenter les valeurs en mémoire.
17. https://github.com/DennisYurichev/base64scanner
18. National Security Agency (Agence Nationale de la Sécurité)
19. NDT: traduit en français par Laurent Viennot
20. La traduction de la citation est extraite de ce livre.
601
2.8.1 Big-endian
La valeur 0x12345678 est représentée en mémoire comme:
adresse en mémoire valeur de l’octet
+0 0x12
+1 0x34
+2 0x56
+3 0x78
Les CPUs big-endian comprennent les Motorola 68k, IBM POWER.
2.8.2 Little-endian
La valeur 0x12345678 est représentée en mémoire comme:
adresse en mémoire valeur de l’octet
+0 0x78
+1 0x56
+2 0x34
+3 0x12
Les CPUs little-endian comprennent les Intel x86. Un exemple important d’utilisation
de little-endian dans ce livre est: 1.35 on page 521.
2.8.3 Exemple
21
Prenons une système Linux MIPS big-endian déjà installé et prêt dans QEMU .
Et compilons cet exemple simple:
#include <stdio.h>
int main()
{
int v ;
v=123;
602
C’est ça. 0x7B est 123 en décimal. En architecture little-endian, 7B est le premier
octet (vous pouvez vérifier en x86 ou en x86-64, mais ici c’est le dernier, car l’octet
le plus significatif vient en premier.
C’est pourquoi il y a des distributions Liux séparées pour MIPS («mips » (big-endian)
et «mipsel » (little-endian)). Il est impossible pour un binaire compilé pour une archi-
tecture de fonctionner sur un OS avec une architecture différente.
Il y a un exemple de MIPS big-endian dans ce livre: 1.30.4 on page 470.
2.8.4 Bi-endian
Les CPUs qui peuvent changer d’endianness sont les ARM, PowerPC, SPARC, MIPS,
IA6422 , etc.
2.9 Mémoire
Il y a 3 grands types de mémoire:
• Mémoire globale AKA « allocation statique de mémoire ». Pas besoin de l’al-
louer explicitement, l’allocation est effectuée juste en déclarant des variables/-
tableaux globalement. Ce sont des variables globales, se trouvant dans le seg-
ment de données ou de constantes. Elles sont accessibles globalement (ce qui
est considéré comme un anti-pattern). Ce n’est pas pratique pour les buffers/-
tableaux, car ils doivent avoir une taille fixée. Les débordements de tampons
se produisant ici le sont en général en récrivant les variables ou les buffers se
trouvant à côté d’eux en mémoire. Il y a un exemple dans ce livre: 1.12.3 on
page 104.
• Stack (pile) AKA «allocation sur la pile ». L’allocation est effectuée simplement
en déclarant des variables/ tableaux localement dans la fonction. Ce sont en
général des variables locales de la fonction. Parfois ces variables locales sont
aussi visibles depuis les fonctions appelées, si l’appelant passe un pointeur sur
une variable à la fonction appelée qui va être exécutée). L’allocation et la dé-
allocation sont très rapide, il suffit de décaler SP.
22. Intel Architecture 64 (Itanium)
603
Mais elles ne conviennent pas non plus pour les tampons/tableaux, car la taille
du tampon doit être fixée, à moins qu’alloca() ( 1.9.2 on page 49) (ou un
tableau de longueur variable) ne soit utilisé. Les débordements de tampons
écrasent en général les structures de pile importantes: 1.26.2 on page 349.
• Heap (tas) AKA «allocation dynamique de mémoire ». L’allocation/dé-allocation
est effectuée en appelant malloc()/free() ou new/delete en C++. Ceci est
la méthode la plus pratique: la taille du bloc peut être définie lors de l’exécution.
2.10 CPU
2.10.1 Prédicteurs de branchement
Certains des derniers compilateurs essayent d’éliminer les instructions de saut. Il y
a des exemples dans ce livre: 1.18.1 on page 177, 1.18.3 on page 187, 1.28.5 on
page 423.
C’est parce que le prédicteur de branchement n’est pas toujours parfait, donc les
compilateurs essayent de faire sans les sauts conditionnels, si possible.
Les instructions conditionnelles en ARM (comme ADRcc) sont une manière, une autre
est l’instruction x86 CMOVcc.
604
2.11 Fonctions de hachage
Un exemple très simple est CRC32, un algorithme qui fournit des checksum plus
«fort » à des fins de vérifications d’intégrité. Il est impossible de restaurer le texte
d’origine depuis la valeur du hash, il a beaucoup moins d’informations: Mais CRC32
n’est pas cryptographiquement sûr: on sait comment modifier un texte afin que son
hash CRC32 résultant soit celui que l’on veut. Les fonctions cryptographiques sont
protégées contre cela.
MD5, SHA1, etc. sont de telles fonctions et elles sont largement utilisées pour ha-
cher les mots de passe des utilisateurs afin de les stocker dans une base de données.
En effet: la base de données d’un forum Internet ne doit pas contenir les mots de
passe des utilisateurs (une base de données volée compromettrait tous les mots
de passe des utilisateurs) mais seulement les hachages (donc un cracker ne pour-
rait pas révéler les mots de passe). En outre, un forum Internet n’a pas besoin de
connaître votre mot de passe exactement, il a seulement besoin de vérifier si son
hachage est le même que celui dans la base de données, et vous donne accès s’ils
correspondent. Une des méthodes de cracking la plus simple est simplement d’es-
sayer de hacher tous les mots de passe possible pour voir celui qui correspond à la
valeur recherchée. D’autres méthodes sont beaucoup plus complexes.
L’algorithme pour une fonction à sens unique la plus simple possible est:
• prendre le nombre à l’indice zéro (4 dans notre cas) ;
• prendre le nombre à l’indice 1 (6 dans notre cas) ;
• échanger les nombres aux positions 4 et 6.
Marquons les nombres aux positions 4 et 6:
4 6 0 1 3 5 7 8 9 2
^ ^
605
En regardant le résultat, et même si nous connaissons l’algorithme, nous ne pou-
vons pas connaître l’état initial de façon certaine, car les deux premiers nombres
pourraient être 0 et/ou 1, et pourraient donc participer à la procédure d’échange.
Ceci est un exemple extrêmement simplifié pour la démonstration. Les fonctions à
sens unique réelles sont bien plus complexes.
606
Chapitre 3
607
.text :010281BC mov esi, [ebp+arg_0]
.text :010281BF xor ebx, ebx ; *
.text :010281C1 mov [ebp+var_10], ebx ; *
.text :010281C4 cmp [esi], ebx ; *
.text :010281C6 jbe short loc_10281E8
.text :010281C8
.text :010281C8 loc_10281C8 : ; CODE XREF: sub_10281AE+38j
.text :010281C8 mov eax, [esi+0Ch]
.text :010281CB mov ecx, [ebp+var_10]
.text :010281CE push dword ptr [eax+ecx*4]
.text :010281D1 call sub_10506C9
.text :010281D6 mov eax, [ebp+var_10]
.text :010281D9 pop ecx
.text :010281DA mov ecx, [esi+0Ch]
.text :010281DD mov [ecx+eax*4], ebx ; *
.text :010281E0 inc eax
.text :010281E1 mov [ebp+var_10], eax
.text :010281E4 cmp eax, [esi]
.text :010281E6 jb short loc_10281C8
.text :010281E8
.text :010281E8 loc_10281E8 : ; CODE XREF: sub_10281AE+18j
.text :010281E8 mov [esi], ebx ; *
.text :010281EA mov [edi+14h], ebx ; *
.text :010281ED mov [ebp+var_34], ebx ; *
.text :010281F0 mov [ebp+var_30], ebx ; *
.text :010281F3 mov [ebp+var_2C], 10h
.text :010281FA mov [ebp+var_28], ebx ; *
.text :010281FD mov [ebp+var_4], ebx ; *
.text :01028200 mov [ebp+arg_0], ebx ; *
.text :01028203 cmp [edi+0B0h], ebx ; *
.text :01028209 jbe loc_10282C3
.text :0102820F
.text :0102820F loc_102820F : ; CODE XREF: sub_10281AE+10Fj
.text :0102820F mov eax, [edi+0BCh]
.text :01028215 mov ecx, [ebp+arg_0]
.text :01028218 mov eax, [eax+ecx*4]
.text :0102821B mov [ebp+var_14], eax
.text :0102821E cmp eax, ebx ; *
.text :01028220 jz loc_10282A6
.text :01028226 push ebx ; *
.text :01028227 push eax
.text :01028228 mov ecx, edi
.text :0102822A call sub_1026B3D
.text :0102822F test al, al
.text :01028231 jz short loc_10282A6
.text :01028233 mov [ebp+var_24], ebx ; *
.text :01028236 mov [ebp+var_20], ebx ; *
.text :01028239 mov [ebp+var_1C], 10h
.text :01028240 mov [ebp+var_18], ebx ; *
.text :01028243 lea eax, [ebp+var_34]
.text :01028246 push eax
.text :01028247 lea eax, [ebp+var_24]
.text :0102824A push eax
608
.text :0102824B push [ebp+var_14]
.text :0102824E mov ecx, edi
.text :01028250 mov byte ptr [ebp+var_4], 1
.text :01028254 call sub_1026E4F
.text :01028259 mov [ebp+var_10], ebx ; *
.text :0102825C cmp [ebp+var_24], ebx ; *
.text :0102825F jbe short loc_102829B
.text :01028261
.text :01028261 loc_1028261 : ; CODE XREF: sub_10281AE+EBj
.text :01028261 push 0Ch ; Size
.text :01028263 call sub_102E741
.text :01028268 pop ecx
.text :01028269 cmp eax, ebx ; *
.text :0102826B jz short loc_1028286
.text :0102826D mov edx, [ebp+var_10]
.text :01028270 mov ecx, [ebp+var_18]
.text :01028273 mov ecx, [ecx+edx*4]
.text :01028276 mov edx, [ebp+var_14]
.text :01028279 mov edx, [edx+4]
.text :0102827C mov [eax], edx
.text :0102827E mov [eax+4], ecx
.text :01028281 mov [eax+8], ebx ; *
.text :01028284 jmp short loc_1028288
.text :01028286 ;
---------------------------------------------------------------------------
.text :01028286
.text :01028286 loc_1028286 : ; CODE XREF: sub_10281AE+BDj
.text :01028286 xor eax, eax
.text :01028288
.text :01028288 loc_1028288 : ; CODE XREF: sub_10281AE+D6j
.text :01028288 push eax
.text :01028289 mov ecx, esi
.text :0102828B call sub_104922B
.text :01028290 inc [ebp+var_10]
.text :01028293 mov eax, [ebp+var_10]
.text :01028296 cmp eax, [ebp+var_24]
.text :01028299 jb short loc_1028261
.text :0102829B
.text :0102829B loc_102829B : ; CODE XREF: sub_10281AE+B1j
.text :0102829B lea ecx, [ebp+var_24]
.text :0102829E mov byte ptr [ebp+var_4], bl
.text :010282A1 call sub_10349DB
.text :010282A6
.text :010282A6 loc_10282A6 : ; CODE XREF: sub_10281AE+72j
.text :010282A6 ; sub_10281AE+83j
.text :010282A6 push [ebp+arg_0]
.text :010282A9 lea ecx, [ebp+var_34]
.text :010282AC call sub_104922B
.text :010282B1 inc [ebp+arg_0]
.text :010282B4 mov eax, [ebp+arg_0]
.text :010282B7 cmp eax, [edi+0B0h]
.text :010282BD jb loc_102820F
.text :010282C3
.text :010282C3 loc_10282C3 : ; CODE XREF: sub_10281AE+5Bj
609
.text :010282C3 cmp [ebp+arg_4], bl
.text :010282C6 jz short loc_1028337
.text :010282C8 mov eax, dword_1088AD8
.text :010282CD mov esi, ds :EnableMenuItem
.text :010282D3 mov edi, 40002
.text :010282D8 cmp [eax+8], ebx ; *
.text :010282DB jnz short loc_10282EC
.text :010282DD push 3 ; uEnable
.text :010282DF push edi ; uIDEnableItem
.text :010282E0 push hMenu ; hMenu
.text :010282E6 call esi ; EnableMenuItem
.text :010282E8 push 3
.text :010282EA jmp short loc_10282F7
.text :010282EC ;
---------------------------------------------------------------------------
.text :010282EC
.text :010282EC loc_10282EC : ; CODE XREF:
sub_10281AE+12Dj
.text :010282EC push ebx ; *
.text :010282ED push edi ; uIDEnableItem
.text :010282EE push hMenu ; hMenu
.text :010282F4 call esi ; EnableMenuItem
.text :010282F6 push ebx ; *
.text :010282F7
.text :010282F7 loc_10282F7 : ; CODE XREF:
sub_10281AE+13Cj
.text :010282F7 push edi ; uIDEnableItem
.text :010282F8 push hmenu ; hMenu
.text :010282FE call esi ; EnableMenuItem
.text :01028300 mov ecx, dword_1088AD8
.text :01028306 call sub_1020402
.text :0102830B mov edi, 40001
.text :01028310 test al, al
.text :01028312 jz short loc_1028321
.text :01028314 push ebx ; *
.text :01028315 push edi ; uIDEnableItem
.text :01028316 push hMenu ; hMenu
.text :0102831C call esi ; EnableMenuItem
.text :0102831E push ebx ; *
.text :0102831F jmp short loc_102832E
.text :01028321 ;
---------------------------------------------------------------------------
.text :01028321
.text :01028321 loc_1028321 : ; CODE XREF:
sub_10281AE+164j
.text :01028321 push 3 ; uEnable
.text :01028323 push edi ; uIDEnableItem
.text :01028324 push hMenu ; hMenu
.text :0102832A call esi ; EnableMenuItem
.text :0102832C push 3 ; uEnable
.text :0102832E
.text :0102832E loc_102832E : ; CODE XREF:
sub_10281AE+171j
.text :0102832E push edi ; uIDEnableItem
.text :0102832F push hmenu ; hMenu
.text :01028335 call esi ; EnableMenuItem
610
.text :01028337
.text :01028337 loc_1028337 : ; CODE XREF:
sub_10281AE+118j
.text :01028337 lea ecx, [ebp+var_34]
.text :0102833A call sub_10349DB
.text :0102833F call __EH_epilog3
.text :01028344 retn 8
.text :01028344 sub_10281AE endp
XOR efface toujours la valeur de retour dans EAX, même si SETNE n’est pas déclenché.
I.e., XOR met la valeur de retour par défaut à zéro.
Si la valeur en entrée n’est pas égale à zéro (le suffixe -NE dans l’instruction SET), 1
est mis dans AL, autrement AL n’est pas modifié.
Pourquoi est-ce que SETNE opère sur la partie 8-bit basse du registre EAX ? Parce que
ce qui compte c’est juste le dernier bit (0 or 1), puisque les autres bits sont mis à
zéro par XOR.
Ainsi, ce code C/C++ peut être récrit comme ceci:
int convert_to_bool(int a)
{
if (a !=0)
return 1;
else
return 0;
};
…ou même:
1. C’est sujet à controverse, car ça conduit à du code difficile à lire
611
int convert_to_bool(int a)
{
if (a)
return 1;
else
return 0;
};
Les compilateurs visant des CPUs n’ayant pas d’instructions similaires à SET, gé-
nèrent dans ce cas des instructions de branchement, etc.
int main()
{
f(&a) ;
return a ;
};
Les chaînes C anonymes (non liées à un nom de variable) ont aussi un type const char*.
Vous ne pouvez pas les modifier:
#include <string.h>
#include <stdio.h>
int main()
612
{
alter_string ("Hello, world !\n") ;
};
Ce code va planter sur Linux (“segmentation fault”) et sur Windows si il compilé par
MinGW.
GCC pour Linux met toutes les chaînes de texte dans le segment de données .rodata,
qui est explicitement en lecture seule (“read only data”) :
$ objdump -s 1
...
...
...
C :\...>objdump -x 1.exe
...
Sections :
Idx Name Size VMA LMA File off Algn
0 .text 00006d2a 00401000 00401000 00000400 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rdata 00002262 00408000 00408000 00007200 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .data 00000e00 0040b000 0040b000 00009600 2**2
CONTENTS, ALLOC, LOAD, DATA
3 .reloc 00000b98 0040e000 0040e000 0000a400 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
Toutefois, MinGW n’a pas cette erreur et alloue les chaînes de texte dans le segment
.rdata.
613
3.3.1 Chaînes const se chevauchant
Le fait est qu’une chaîne C anonyme a un type const ( 1.5.1 on page 13), et que les
chaînes C allouées dans le segment des constantes sont garanties d’être immuables,
a cette conséquence intéressante: Le compilateur peut utiliser une partie spécifique
de la chaîne.
Voyons cela avec un exemple:
#include <stdio.h>
int f1()
{
printf ("world\n") ;
}
int f2()
{
printf ("hello world\n") ;
}
int main()
{
f1() ;
f2() ;
}
La plupart des compilateurs C/C++ (MSVC inclus) allouent deux chaînes, mais voyons
ce que fait GCC 4.8.1:
Listing 3.2: GCC 4.8.1 + IDA
f1 proc near
f2 proc near
614
s db 'world',0xa,0
Effectivement: lorsque nous affichons la chaîne «hello world » ses deux mots sont
positionnés consécutivement en mémoire et l’appel à puts() depuis la fonction f2()
n’est pas au courant que la chaîne est divisée. En fait, elle n’est pas divisée; elle l’est
virtuellement, dans ce listing.
Lorsque puts() est appelé depuis f1(), il utilise la chaîne «world » ainsi qu’un octet
à zéro. puts() ne sait pas qu’il y a quelque chose avant cette chaîne!
Cette astuce est souvent utilisée, au moins par GCC, et permet d’économiser de la
mémoire. C’est proche du string interning.
Un autre exemple concernant ceci se trouve là: 3.4.
int main()
{
char *s="Hello, world !" ;
char *w=strstr(s, "world") ;
La sortie est:
0x8048530, [Hello, world !]
0x8048537, [world !]
615
Maintenant que vous êtes déjà familier avec la fonction qsort() ( 1.33 on page 494),
voici un bel exemple où l’opération de comparaison (CMP) peut être remplacée par
l’opération de soustraction (SUB).
/* fonction de comparaison qsort int */
int int_cmp(const void *a, const void *b)
{
const int *ia = (const int *)a ; // casting de types de pointeur
const int *ib = (const int *)b ;
return *ia - *ib ;
/* comparaison d'entier : renvoie négatif si if b > a
et positif si a > b */
}
( http://www.anyexample.com/programming/c/qsort__sorting_array_of_strings_
_integers_and_structs.xml http://archive.is/Hh3jz )
Aussi, une implémentation typique de strcmp() (tiré d’OpenBSD) :
int
strcmp(const char *s1, const char *s2)
{
while (*s1 == *s2++)
if (*s1++ == 0)
return (0) ;
return (*(unsigned char *)s1 - *(unsigned char *)--s2) ;
}
5 ⋅ (F − 32)
C=
9
Nous pouvons aussi ajouter une gestion des erreurs simples: 1) nous devons vérifier
si l’utilisateur a entré un nombre correct; 2) nous devons tester si la température en
Celsius n’est pas en dessous de −273 (qui est en dessous du zéro absolu, comme vu
pendant les cours de physique à l’école)
La fonction exit() termine le programme instantanément, sans retourner à la fonc-
tion appelante.
int main()
616
{
int celsius, fahr ;
printf ("Enter temperature in Fahrenheit :\n") ;
if (scanf ("%d", &fahr) !=1)
{
printf ("Error while parsing your input\n") ;
exit(0) ;
};
celsius = 5 * (fahr-32) / 9;
if (celsius<-273)
{
printf ("Error : incorrect temperature !\n") ;
exit(0) ;
};
printf ("Celsius : %d\n", celsius) ;
};
_fahr$ = -4 ; taille = 4
_main PROC
push ecx
push esi
mov esi, DWORD PTR __imp__printf
push OFFSET $SG4228 ; 'Enter temperature in Fahrenheit:'
call esi ; appeler printf()
lea eax, DWORD PTR _fahr$[esp+12]
push eax
push OFFSET $SG4230 ; '%d'
call DWORD PTR __imp__scanf
add esp, 12
cmp eax, 1
je SHORT $LN2@main
push OFFSET $SG4231 ; 'Error while parsing your input'
call esi ; appeler printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN9@main :
$LN2@main :
mov eax, DWORD PTR _fahr$[esp+8]
add eax, -32 ; ffffffe0H
lea ecx, DWORD PTR [eax+eax*4]
617
mov eax, 954437177 ; 38e38e39H
imul ecx
sar edx, 1
mov eax, edx
shr eax, 31 ; 0000001fH
add eax, edx
cmp eax, -273 ; fffffeefH
jge SHORT $LN1@main
push OFFSET $SG4233 ; 'Error: incorrect temperature!'
call esi ; appeler printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN10@main :
$LN1@main :
push eax
push OFFSET $SG4234 ; 'Celsius: %d'
call esi ; appeler printf()
add esp, 8
; renvoyer 0 - d'après le standard C99
xor eax, eax
pop esi
pop ecx
ret 0
$LN8@main :
_main ENDP
618
Cependant, MSVC ne supporte pas officiellement C99, mais peut-être qu’il le
supporte partiellement ?
Le code est quasiment le même, mais nous trouvons une instruction INT 3 après
chaque appel à exit().
xor ecx, ecx
call QWORD PTR __imp_exit
int 3
int main()
{
double celsius, fahr ;
printf ("Enter temperature in Fahrenheit :\n") ;
if (scanf ("%lf", &fahr) !=1)
{
printf ("Error while parsing your input\n") ;
exit(0) ;
};
celsius = 5 * (fahr-32) / 9;
if (celsius<-273)
{
printf ("Error : incorrect temperature !\n") ;
exit(0) ;
};
printf ("Celsius : %lf\n", celsius) ;
};
619
$SG4044 DB 'Celsius : %lf', 0aH, 00H
_fahr$ = -8 ; taille = 8
_main PROC
sub esp, 8
push esi
mov esi, DWORD PTR __imp__printf
push OFFSET $SG4038 ; 'Enter temperature in Fahrenheit:'
call esi ; appeler printf()
lea eax, DWORD PTR _fahr$[esp+16]
push eax
push OFFSET $SG4040 ; '%lf'
call DWORD PTR __imp__scanf
add esp, 12
cmp eax, 1
je SHORT $LN2@main
push OFFSET $SG4041 ; 'Error while parsing your input'
call esi ; appeler printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN2@main :
fld QWORD PTR _fahr$[esp+12]
fsub QWORD PTR __real@4040000000000000 ; 32
fmul QWORD PTR __real@4014000000000000 ; 5
fdiv QWORD PTR __real@4022000000000000 ; 9
fld QWORD PTR __real@c071100000000000 ; -273
fcomp ST(1)
fnstsw ax
test ah, 65 ; 00000041H
jne SHORT $LN1@main
push OFFSET $SG4043 ; 'Error: incorrect temperature!'
fstp ST(0)
call esi ; appeler printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN1@main :
sub esp, 8
fstp QWORD PTR [esp]
push OFFSET $SG4044 ; 'Celsius: %lf'
call esi
add esp, 12
; renvoyer 0 - d'après le standard C99
xor eax, eax
pop esi
add esp, 8
ret 0
620
$LN10@main :
_main ENDP
_fahr$ = -8 ; taile = 8
_main PROC
sub esp, 8
push esi
mov esi, DWORD PTR __imp__printf
push OFFSET $SG4228 ; 'Enter temperature in Fahrenheit:'
call esi ; appeler printf()
lea eax, DWORD PTR _fahr$[esp+16]
push eax
push OFFSET $SG4230 ; '%lf'
call DWORD PTR __imp__scanf
add esp, 12
cmp eax, 1
je SHORT $LN2@main
push OFFSET $SG4231 ; 'Error while parsing your input'
call esi ; appeler printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN9@main :
$LN2@main :
movsd xmm1, QWORD PTR _fahr$[esp+12]
subsd xmm1, QWORD PTR __real@4040000000000000 ; 32
movsd xmm0, QWORD PTR __real@c071100000000000 ; -273
mulsd xmm1, QWORD PTR __real@4014000000000000 ; 5
divsd xmm1, QWORD PTR __real@4022000000000000 ; 9
comisd xmm0, xmm1
jbe SHORT $LN1@main
push OFFSET $SG4233 ; 'Error: incorrect temperature!'
call esi ; appeler printf()
add esp, 4
push 0
call DWORD PTR __imp__exit
$LN10@main :
$LN1@main :
sub esp, 8
movsd QWORD PTR [esp], xmm1
621
push OFFSET $SG4234 ; 'Celsius: %lf'
call esi ; appeler printf()
add esp, 12
; renvoyer 0 - d'après le standard C99
xor eax, eax
pop esi
add esp, 8
ret 0
$LN8@main :
_main ENDP
Bien sûr, les instructions SIMD sont disponibles dans le mode x86, incluant celles qui
fonctionnent avec les nombres à virgule flottante.
C’est un peu plus simple de les utiliser pour les calculs, donc le nouveau compilateur
de Microsoft les utilise.
Nous pouvons aussi voir que la valeur −273 est chargée dans le registre XMM0 trop
tôt. Et c’est OK, parce que le compilateur peut mettre des instructions dans un ordre
différent de celui du code source.
3.7.1 Exemple #1
L’implémentation est simple. Ce programme génère la suite jusqu’à 21.
#include <stdio.h>
int main()
{
printf ("0\n1\n1\n") ;
fib (1, 1, 20) ;
3. http://oeis.org/A000045
622
};
_main PROC
push ebp
mov ebp, esp
push OFFSET $SG2647 ; "0\n1\n1\n"
call DWORD PTR __imp__printf
add esp, 4
push 20
push 1
push 1
call _fib
add esp, 12
xor eax, eax
pop ebp
ret 0
_main ENDP
623
Chargeons cet exemple dans OllyDbg et traçons jusqu’au dernier appel de f() :
624
Examinons plus attentivement la pile. Les commentaires ont été ajoutés par l’auteur
de ce livre 4 :
0035F940 00FD1039 RETURN to fib.00FD1039 from fib.00FD1000
0035F944 00000008 1er argument : a
0035F948 0000000D 2nd argument b
0035F94C 00000014 3ème argument : limit
0035F950 /0035F964 registre EBP sauvé
0035F954 |00FD1039 RETURN to fib.00FD1039 from fib.00FD1000
0035F958 |00000005 1er argument : a
0035F95C |00000008 2nd argument : b
0035F960 |00000014 3ème argument : limit
0035F964 ]0035F978 registre EBP sauvé
0035F968 |00FD1039 RETURN to fib.00FD1039 from fib.00FD1000
0035F96C |00000003 1er argument : a
0035F970 |00000005 2nd argument : b
0035F974 |00000014 3ème argument : limit
0035F978 ]0035F98C registre EBP sauvé
0035F97C |00FD1039 RETURN to fib.00FD1039 from fib.00FD1000
0035F980 |00000002 1er argument : a
0035F984 |00000003 2nd argument : b
0035F988 |00000014 3ème argument : limit
0035F98C ]0035F9A0 registre EBP sauvé
0035F990 |00FD1039 RETURN to fib.00FD1039 from fib.00FD1000
0035F994 |00000001 1er argument : a
0035F998 |00000002 2nd argument : b
0035F99C |00000014 3ème argument : limit
0035F9A0 ]0035F9B4 registre EBP sauvé
0035F9A4 |00FD105C RETURN to fib.00FD105C from fib.00FD1000
0035F9A8 |00000001 1er argument : a \
0035F9AC |00000001 2nd argument : b | préparé dans main() pour f1()
0035F9B0 |00000014 3ème argument : limit /
0035F9B4 ]0035F9F8 registre EBP sauvé
0035F9B8 |00FD11D0 RETURN to fib.00FD11D0 from fib.00FD1040
0035F9BC |00000001 main() 1er argument : argc \
0035F9C0 |006812C8 main() 2nd argument : argv | préparé dans CRT pour ⤦
Ç main()
0035F9C4 |00682940 main() 3ème argument : envp /
625
fonction), bien que cela soit techniquement possible.
C’est généralement vrai, à moins que la fonction n’ait des bugs.
Chaque valeur sauvée de EBP est l’adresse de la structure de pile locale précédente:
c’est la raison pour laquelle certains débogueurs peuvent facilement diviser la pile
en blocs et afficher chaque argument de la fonction.
Comme nous le voyons ici, chaque exécution de fonction prépare les arguments pour
l’appel de fonction suivant.
À la fin, nous voyons les 3 arguments de main(). argc vaut 1 (oui, en effet, nous
avons lancé le programme sans argument sur la ligne de commande).
Ceci peut conduire facilement à un débordement de pile: il suffit de supprimer (ou
commenter) le test de la limite et ça va planter avec l’exception 0xC00000FD (stack
overflow).
3.7.2 Exemple #2
Ma fonction a quelques redondances, donc ajoutons une nouvelle variable locale
next et remplaçons tout les «a+b » avec elle:
#include <stdio.h>
int main()
{
printf ("0\n1\n1\n") ;
fib (1, 1, 20) ;
};
C’est la sortie de MSVC sans optimisation, donc la variable next est allouée sur la
pile locale:
626
mov DWORD PTR _next$[ebp], eax
mov ecx, DWORD PTR _next$[ebp]
push ecx
push OFFSET $SG2751 ; '%d'
call DWORD PTR __imp__printf
add esp, 8
mov edx, DWORD PTR _next$[ebp]
cmp edx, DWORD PTR _limit$[ebp]
jle SHORT $LN1@fib
jmp SHORT $LN2@fib
$LN1@fib :
mov eax, DWORD PTR _limit$[ebp]
push eax
mov ecx, DWORD PTR _next$[ebp]
push ecx
mov edx, DWORD PTR _b$[ebp]
push edx
call _fib
add esp, 12
$LN2@fib :
mov esp, ebp
pop ebp
ret 0
_fib ENDP
_main PROC
push ebp
mov ebp, esp
push OFFSET $SG2753 ; "0\n1\n1\n"
call DWORD PTR __imp__printf
add esp, 4
push 20
push 1
push 1
call _fib
add esp, 12
xor eax, eax
pop ebp
ret 0
_main ENDP
627
Chargeons-le à nouveau dans OllyDbg :
628
Examinons plus attentivement la pile. L’auteur a de nouveau ajouté ses commen-
taires:
0029FC14 00E0103A RETURN to fib2.00E0103A from fib2.00E01000
0029FC18 00000008 1er argument : a
0029FC1C 0000000D 2nd argument : b
0029FC20 00000014 3ème argument : limit
0029FC24 0000000D variable "next"
0029FC28 /0029FC40 registre EBP sauvé
0029FC2C |00E0103A RETURN to fib2.00E0103A from fib2.00E01000
0029FC30 |00000005 1er argument : a
0029FC34 |00000008 2nd argument : b
0029FC38 |00000014 3ème argument : limit
0029FC3C |00000008 "next" variable
0029FC40 ]0029FC58 registre EBP sauvé
0029FC44 |00E0103A RETURN to fib2.00E0103A from fib2.00E01000
0029FC48 |00000003 1er argument : a
0029FC4C |00000005 2nd argument : b
0029FC50 |00000014 3ème argument : limit
0029FC54 |00000005 variable "next"
0029FC58 ]0029FC70 registre EBP sauvé
0029FC5C |00E0103A RETURN to fib2.00E0103A from fib2.00E01000
0029FC60 |00000002 1er argument : a
0029FC64 |00000003 2nd argument : b
0029FC68 |00000014 3ème argument : limit
0029FC6C |00000003 variable "next"
0029FC70 ]0029FC88 registre EBP sauvé
0029FC74 |00E0103A RETURN to fib2.00E0103A from fib2.00E01000
0029FC78 |00000001 1er argument : a \
0029FC7C |00000002 2nd argument : b | préparé dans f1() pour le ⤦
Ç prochain appel à f1()
0029FC80 |00000014 3ème argument : limit /
0029FC84 |00000002 variable "next"
0029FC88 ]0029FC9C registre EBP sauvé
0029FC8C |00E0106C RETURN to fib2.00E0106C from fib2.00E01000
0029FC90 |00000001 1er argument : a \
0029FC94 |00000001 2nd argument : b | préparé dans main() pour f1()
0029FC98 |00000014 3ème argument : limit /
0029FC9C ]0029FCE0 registre EBP sauvé
0029FCA0 |00E011E0 RETURN to fib2.00E011E0 from fib2.00E01050
0029FCA4 |00000001 main() 1er argument : argc \
0029FCA8 |000812C8 main() 2nd argument : argv | préparé dans CRT pour ⤦
Ç main()
0029FCAC |00082940 main() 3ème argument : envp /
Voici ce que l’on voit: la valeur next est calculée dans chaque appel de la fonction,
puis passée comme argument b au prochain appel.
3.7.3 Résumé
Les fonctions récursives sont esthétiquement jolies, mais techniquement elles peuvent
dégrader les performances à cause de leur usage intensif de la pile. Quiconque qui
629
écrit du code dont la perfomance est critique devrait probablement éviter la récur-
sion.
Par exemple, j’ai écrit une fois une fonction pour chercher un nœud particulier dans
un arbre binaire. Bien que la fonction récursive avait l’air élégante, il y avait du temps
passé à chaque appel de fonction pour le prologue et l’épilogue, elle fonctionnait
deux ou trois fois plus lentement que l’implémentation itérative (sans récursion).
À propos, c’est la raison pour laquelle certains compilateurs fonctionnels LP6 (où
la récursion est très utilisée) utilisent les appels terminaux. Nous parlons d’appel
terminal lorsqu’une fonction a un seul appel à elle-même, situé à sa fin, comme:
Les appels terminaux sont importants car le compilateur peut retravailler facilement
ce code en un code itératif, pour supprimer la récursion.
#include <stdio.h>
#include <stddef.h>
#include <string.h>
630
0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190,
0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e,
0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed,
0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3,
0xfbd44c65, 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5,
0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010,
0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17,
0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6,
0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615,
0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344,
0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a,
0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1,
0xa6bc5767, 0x3fb506dd, 0x48b2364b, 0xd80d2bda, 0xaf0a1b4c,
0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe,
0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31,
0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c,
0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b,
0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1,
0x18b74777, 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278,
0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7,
0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66,
0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8,
0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b,
0x2d02ef8d
};
631
j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0) ;
j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0) ;
j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0) ;
j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0) ;
printf("0x%.8lx, ", j) ;
if (i%6 == 5) printf("\n") ;
}
}
Nous sommes seulement intéressés par la fonction crc(). À propos, faîtes attention
aux deux déclarations d’initialisation dans la boucle for() : hash=len, i=0. Le stan-
dard C/C++ permet ceci, bien sûr. Le code généré contiendra deux opérations dans
la partie d’initialisation de la boucle, au lieu d’une.
Compilons-le dans MSVC avec l’optimisation (/Ox). Dans un soucis de concision,
seule la fonction crc() est listée ici, avec mes commentaires.
_key$ = 8 ; size = 4
_len$ = 12 ; size = 4
_hash$ = 16 ; size = 4
_crc PROC
mov edx, DWORD PTR _len$[esp-4]
xor ecx, ecx ; i est stocké dans ECX
mov eax, edx
test edx, edx
jbe SHORT $LN1@crc
push ebx
push esi
mov esi, DWORD PTR _key$[esp+4] ; ESI = key
push edi
$LL3@crc :
632
movzx edi, BYTE PTR [ecx+esi]
mov ebx, eax ; EBX = (hash = len)
and ebx, 255 ; EBX = hash & 0xff
push ebp
xor edx, edx
mov ebp, esp
push esi
mov esi, [ebp+key]
push ebx
mov ebx, [ebp+hash]
test ebx, ebx
mov eax, ebx
jz short loc_80484D3
nop ; remplissage
lea esi, [esi+0] ; remplissage; fonctionne comme NOP
; (ESI ne change pas ici)
loc_80484B8 :
mov ecx, eax ; sauve l'état pérécédent du hash dans
633
ECX
xor al, [esi+edx] ; AL=*(key+i)
add edx, 1 ; i++
shr ecx, 8 ; ECX=hash>>8
movzx eax, al ; EAX=*(key+i)
mov eax, dword ptr ds :crctab[eax*4] ; EAX=crctab[EAX]
xor eax, ecx ; hash=EAX^ECX
cmp ebx, edx
ja short loc_80484B8
loc_80484D3 :
pop ebx
pop esi
pop ebp
retn
crc endp
GCC a aligné le début de la boucle sur une limite de 8-octet en ajoutant NOP et lea
esi, [esi+0] (qui est aussi une opération sans effet).
Vous pouvez en lire plus à ce sujet dans la section npad ( .1.7 on page 1359).
634
Masque Hôte Utilisable Masque de réseau Masque hexadécimal
/30 4 2 255.255.255.252 0xfffffffc
/29 8 6 255.255.255.248 0xfffffff8
/28 16 14 255.255.255.240 0xfffffff0
/27 32 30 255.255.255.224 0xffffffe0
/26 64 62 255.255.255.192 0xffffffc0
/24 256 254 255.255.255.0 0xffffff00 réseau de classe C
/23 512 510 255.255.254.0 0xfffffe00
/22 1024 1022 255.255.252.0 0xfffffc00
/21 2048 2046 255.255.248.0 0xfffff800
/20 4096 4094 255.255.240.0 0xfffff000
/19 8192 8190 255.255.224.0 0xffffe000
/18 16384 16382 255.255.192.0 0xffffc000
/17 32768 32766 255.255.128.0 0xffff8000
/16 65536 65534 255.255.0.0 0xffff0000 réseau de classe B
/8 16777216 16777214 255.0.0.0 0xff000000 réseau de classe A
uint32_t form_IP (uint8_t ip1, uint8_t ip2, uint8_t ip3, uint8_t ip4)
{
return (ip1<<24) | (ip2<<16) | (ip3<<8) | ip4 ;
};
// bit=31..0
uint32_t set_bit (uint32_t input, int bit)
{
return input=input|(1<<bit) ;
};
return netmask ;
};
635
void calc_network_address (uint8_t ip1, uint8_t ip2, uint8_t ip3, uint8_t ⤦
Ç ip4, uint8_t netmask_bits)
{
uint32_t netmask=form_netmask(netmask_bits) ;
uint32_t ip=form_IP(ip1, ip2, ip3, ip4) ;
uint32_t netw_adr ;
printf ("netmask=") ;
print_as_IP (netmask) ;
netw_adr=ip&netmask ;
int main()
{
calc_network_address (10, 1, 2, 4, 24) ; // 10.1.2.4, /24
calc_network_address (10, 1, 2, 4, 8) ; // 10.1.2.4, /8
calc_network_address (10, 1, 2, 4, 25) ; // 10.1.2.4, /25
calc_network_address (10, 1, 2, 64, 26) ; // 10.1.2.4, /26
};
3.9.1 calc_network_address()
La fonction calc_network_address() est la plus simple: elle effectue simplement
un AND entre l’adresse de l’hôte et le masque de réseau, dont le résultat est l’adresse
du réseau.
636
22 and eax, edi ; network address = host address & netmask
23 push eax
24 call _print_as_IP
25 add esp, 36
26 pop edi
27 ret 0
28 _calc_network_address ENDP
À la ligne 22, nous voyons le plus important AND—ici l’adresse du réseau est calculée.
3.9.2 form_IP()
La fonction form_IP() met juste les 4 octets dans une valeur 32-bit.
Voici comment cela est fait habituellement:
• Allouer une variable pour la valeur de retour. La mettre à 0.
• Prendre le 4ème octet (de poids le plus faible), appliquer l’opération OR à cet
octet et renvoyer la valeur.
• Prendre le troisième octet, le décaler à gauche de 8 bits. Vous obtenez une
valeur comme 0x0000bb00 où bb est votre troisième octet. Appliquer l’opération
OR à la valeur résultante. La valeur de retour contenait 0x000000aa jusqu’à
présent, donc effectuer un OU logique des valeurs produira une valeur comme
0x0000bbaa.
• Prendre le second octet, le décaler à gauche de 16 bits. Vous obtenez une va-
leur comme 0x00cc0000 où cc est votre deuxième octet. Appliquer l’opération
OR à la valeur résultante. La valeur de retour contenait 0x0000bbaa jusqu’à
présent, donc effectuer un OU logique des valeurs produira une valeur comme
0x00ccbbaa.
• Prendre le premier octet, le décaler à gauche de 24 bits. Vous obtenez une
valeur comme 0xdd000000 où dd est votre premier octet. Appliquer l’opération
OR à la valeur résultante. La valeur de retour contenait 0x00ccbbaa jusqu’à
présent, donc effectuer un OU logique des valeurs produira une valeur comme
0xddccbbaa.
Voici comment c’est fait par MSVC 2012 sans optimisation:
637
; EAX=dd000000
movzx ecx, BYTE PTR _ip2$[ebp]
; ECX=000000cc
shl ecx, 16
; ECX=00cc0000
or eax, ecx
; EAX=ddcc0000
movzx edx, BYTE PTR _ip3$[ebp]
; EDX=000000bb
shl edx, 8
; EDX=0000bb00
or eax, edx
; EAX=ddccbb00
movzx ecx, BYTE PTR _ip4$[ebp]
; ECX=000000aa
or eax, ecx
; EAX=ddccbbaa
pop ebp
ret 0
_form_IP ENDP
Certes, l’ordre est différent, mais, bien sûr, l’ordre des opérations n’a pas d’impor-
tance.
MSVC 2012 avec optimisation produit en fait la même chose, mais d’une façon dif-
férente:
638
; EAX=ddccbbaa
ret 0
_form_IP ENDP
Nous pourrions dire que chaque octet est écrit dans les 8 bits inférieurs de la valeur
de retour, et qu’elle est ensuite décalée à gauche d’un octet à chaque étape.
Répéter 4 fois pour chaque octet en entrée.
C’est tout! Malheureusement, il n’y sans doute pas d’autre moyen de le faite.
Il n’y a pas de CPUs ou d’ISAs répandues qui possède une instruction pour composer
une valeur à partir de bits ou d’octets.
C’est d’habitude fait par décalage de bit et OU logique.
3.9.3 print_as_IP()
La fonction print_as_IP() effectue l’inverse: séparer une valeur 32-bit en 4 octets.
Le découpage fonctionne un peu plus simplement: il suffit de décaler la valeur en
entrée de 24, 16, 8 ou 0 bits, prendre les 8 bits d’indice 0 à 7 (octet de poids faible),
et c’est fait:
639
push eax
push OFFSET $SG2973 ; '%d.%d.%d.%d'
call DWORD PTR __imp__printf
add esp, 20
pop ebp
ret 0
_print_as_IP ENDP
MSVC 2012 avec optimisation fait presque la même chose, mais sans recharger in-
utilement la valeur en entrée:
640
Nous allons aussi écrire une fonction séparées set_bit(). Ce n’est pas une très
bonne idée de créer un fonction pour une telle opération primitive, mais cela facilite
la compréhension du fonctionnement.
_netmask_bits$ = 8 ; size = 1
_form_netmask PROC
push ebx
push esi
movzx esi, BYTE PTR _netmask_bits$[esp+4]
xor ecx, ecx
xor bl, bl
test esi, esi
jle SHORT $LN9@form_netma
xor edx, edx
$LL3@form_netma :
mov eax, 31
sub eax, edx
push eax
push ecx
call _set_bit
inc bl
movzx edx, bl
add esp, 8
mov ecx, eax
cmp edx, esi
jl SHORT $LL3@form_netma
$LN9@form_netma :
pop esi
mov eax, ecx
pop ebx
ret 0
_form_netmask ENDP
set_bit() est primitive: elle décale juste 1 à gauche du nombre de bits dont nous
avons besoin et puis effectue un OU logique avec la valeur «input ». form_netmask()
a une boucle: elle met autant de bits (en partant du MSB) que demandé dans l’argu-
ment netmask_bits.
3.9.5 Résumé
C’est tout! Nous le lançons et obtenons:
641
netmask=255.255.255.0
network address=10.1.2.0
netmask=255.0.0.0
network address=10.0.0.0
netmask=255.255.255.128
network address=10.1.2.0
netmask=255.255.255.192
network address=10.1.2.64
642
jne SHORT $LL3@f
$LN1@f :
ret 0
f ENDP
Donc, au prix de la mise à jour de 3 itérateurs à chaque itération au lieu d’un, nous
pouvons supprimer deux opérations de multiplication.
643
cmp rsi, rdx
jne .L3
.L1 :
rep ret
Il n’y a plus de variable counter : GCC en a conclu qu’elle n’étais pas nécessaire.
Le dernier élément du tableau a2 est calculé avant le début de la boucle (ce qui est
facile: cnt ∗ 7) et c’est ainsi que la boucle est arrêtée: itérer jusqu’à ce que le second
index atteignent cette valeur pré-calculée.
Vous trouverez plus d’informations sur la multiplication en utilisant des décalages/ad-
ditions/soustractions ici: 1.24.1 on page 278.
Ce code peut être récrit en C/C++ comme ceci:
#include <stdio.h>
GCC (Linaro) 4.9 pour ARM64 fait la même chose, mais il pré-calcule le dernier index
de a1 au lieu de a2, ce qui a bien sûr le même effet:
644
cmp x3, x2 ; fini?
bne .L3
.L1 :
ret
loc_8 :
; charger le mot 32-bit en $a1
lw $a3, 0($a1)
; incrémenter le compteur (i) :
addiu $v0, 1
; vérifier si terminé (comparer "i" dans $v0 et "cnt" dans $a2) :
sltu $v1, $v0, $a2
; stocker le mot 32-bit en $a0:
sw $a3, 0($a0)
; ajouter 0x1C (28) à $a1 à chaque itération:
addiu $a1, 0x1C
; sauter au corps de la boulce si i<cnt:
bnez $v1, loc_8
; ajouter 0xC (12) à $a0 à chaque itération:
addiu $a0, 0xC ; slot de délai de branchement
locret_24 :
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
645
.B1.2::
cmp r8, 6
jbe just_copy
.B1.3::
cmp rcx, rdx
jbe .B1.5
.B1.4::
mov r10, r8
mov r9, rcx
shl r10, 5
lea rax, QWORD PTR [r8*4]
sub r9, rdx
sub r10, rax
cmp r9, r10
jge just_copy2
.B1.5::
cmp rdx, rcx
jbe just_copy
.B1.6::
mov r9, rdx
lea rax, QWORD PTR [r8*8]
sub r9, rcx
lea r10, QWORD PTR [rax+r8*4]
cmp r9, r10
jl just_copy
just_copy2 ::
; R8 = cnt
; RDX = a2
; RCX = a1
xor r10d, r10d
xor r9d, r9d
xor eax, eax
.B1.8::
mov r11d, DWORD PTR [rax+rdx]
inc r10
mov DWORD PTR [r9+rcx], r11d
add r9, 12
add rax, 28
cmp r10, r8
jb .B1.8
jmp exit
just_copy ::
; R8 = cnt
; RDX = a2
; RCX = a1
xor r10d, r10d
646
xor r9d, r9d
xor eax, eax
.B1.11::
mov r11d, DWORD PTR [rax+rdx]
inc r10
mov DWORD PTR [r9+rcx], r11d
add r9, 12
add rax, 28
cmp r10, r8
jb .B1.11
exit ::
ret
Tout d’abord, quelques décisions sont prises, puis une des routines est exécutée.
Il semble qu’il teste si les tableaux se recoupent.
C’est une façons très connue d’optimiser les routines de copie de blocs de mémoire.
Mais les routines de copie sont les même!
ça doit être une erreur de l’optimiseur Intel C++, qui produit néanmoins un code
fonctionnel.
Nous prenons volontairement en compte de tels exemples dans ce livre, afin que
lecteur comprenne que le ce que génère un compilateur est parfois bizarre mais
toujours correct, car lorsque le compilateur a été testé, il a réussi les tests.
647
#include <stdint.h>
#include <stdio.h>
if (count&(~7))
// traiter les blocs de 8 octets
for (i=0; i<count>>3; i++)
{
*(uint64_t*)dst=0;
dst=dst+8;
};
// traiter le rset
switch(count & 7)
{
case 7: *dst++ = 0;
case 6: *dst++ = 0;
case 5: *dst++ = 0;
case 4: *dst++ = 0;
case 3: *dst++ = 0;
case 2: *dst++ = 0;
case 1: *dst++ = 0;
case 0: // ne rien faire
break ;
}
}
Tout d’abord, comprenons comment le calcul est effectué. La taille de la zone mé-
moire est passée comme une valeur 64-bit. Et cette valeur peut être divisée en deux
parties:
7 6 5 4 3 2 1 0
… B B B B B S S S
648
8-octets et écrire des valeurs 64-bits zéro en mémoire La seconde partie est une
boucle déroulée implémentée avec une déclaration switch() sans arrêt.
Premièrement, exprimons en français ce que nous faisons ici.
Nous devons «écrire autant d’octets à zéro en mémoire, que la valeur count&7 nous
l’indique ». Si c’est 0, sauter à la fin, et il n’y a rien à faire. Si c’est 1, sauter à l’en-
droit à l’intérieur de la déclaration switch() où une seule opération de stockage sera
exécutée. Si c’est 2, sauter à un autre endroit, où deux opérations de stockage se-
ront exécutées, etc. Une valeur d’entrée de 7 conduit à l’exécution de toutes les 7
opérations. Il n’y a pas de 8, car une zone mémoire de 8 octets serait traitée par
la première partie de notre fonction. Donc, nous avons écrit une boucle déroulée.
C’était assurément plus rapide sur les anciens ordinateurs que les boucles normales
(et au contraire, les CPUs récents travaillent mieux avec des boucles courtes qu’avec
des boucles déroulées). Peut-être est-ce encore utile sur les MCU10 s embarqués mo-
derne à bas coût.
Voyons ce que MSVC 2012 avec optimisation fait:
dst$ = 8
count$ = 16
bzero PROC
test rdx, -8
je SHORT $LN11@bzero
; traiter les blocs de 8 octets
xor r10d, r10d
mov r9, rdx
shr r9, 3
mov r8d, r10d
test r9, r9
je SHORT $LN11@bzero
npad 5
$LL19@bzero :
inc r8d
mov QWORD PTR [rcx], r10
add rcx, 8
movsxd rax, r8d
cmp rax, r9
jb SHORT $LL19@bzero
$LN11@bzero :
; traiter le reste
and edx, 7
dec rdx
cmp rdx, 6
ja SHORT $LN9@bzero
lea r8, OFFSET FLAT :__ImageBase
mov eax, DWORD PTR $LN22@bzero[r8+rdx*4]
add rax, r8
jmp rax
$LN8@bzero :
mov BYTE PTR [rcx], 0
inc rcx
649
$LN7@bzero :
mov BYTE PTR [rcx], 0
inc rcx
$LN6@bzero :
mov BYTE PTR [rcx], 0
inc rcx
$LN5@bzero :
mov BYTE PTR [rcx], 0
inc rcx
$LN4@bzero :
mov BYTE PTR [rcx], 0
inc rcx
$LN3@bzero :
mov BYTE PTR [rcx], 0
inc rcx
$LN2@bzero :
mov BYTE PTR [rcx], 0
$LN9@bzero :
fatret 0
npad 1
$LN22@bzero :
DD $LN2@bzero
DD $LN3@bzero
DD $LN4@bzero
DD $LN5@bzero
DD $LN6@bzero
DD $LN7@bzero
DD $LN8@bzero
bzero ENDP
La premières partie de la fonction est prévisible. La seconde partie est juste une
boucle déroulée et un saut y passant le contrôle du flux à la bonne instruction. Il
n’y a pas d’autre code entre la paire d’instructions MOV/INC, donc l’exécution va
continuer jusqu’à la fin, exécutant autant de paires d’instructions que nécessaire. Á
propos, nous pouvons observer que la paire d’instructions MOV/INC utilise un nombre
fixe d’octets (3+3). Donc la paire utilise 6 octets. Sachant cela, mous pouvons nous
passer de la table des sauts de switch(), nous pouvons simplement multiplier la
valeur en entrée par 6 et sauter en current_RIP + input_value ∗ 6.
Ceci peut aussi être plus rapide car nous ne devons pas aller chercher une valeur
dans la table des sauts.
Il est possible que 6 ne soit pas une très bonne constante pour une multiplication
rapide et peut-être que ça n’en vaut pas la peine, mais vous voyez l’idée11 .
C’est ce que les démomakers old-school faisaient dans le passé avec les boucles
déroulées.
11. Comme exercice, vous pouvez essayer de retravailler le code pour se passer de la table des sauts.
La paire d’instructions peut être récrite de façon à ce qu’elle utilise 4 octets ou peut-être 8. 1 octet est
aussi possible (en utilisant l’instruction STOSB).
650
3.11.1 Faut-il utiliser des boucles déroulées?
Les boucles déroulées peuvent être bénéfiques si il n’y a pas de cache mémoire
rapide entre la RAM et le CPU, et que le CPU, afin d’avoir le code de l’instruction
suivante, doit le charger depuis la mémoire à chaque fois. C’est le cas des MCU
low-cost moderne et des anciens CPUs.
Les boucles déroulées sont plus lentes que les boucles courtes si il y a un cache
rapide entre la RAM et le CPU, et que le corps de la boucle tient dans le cache, et
que le CPU va charger le code depuis ce dernier sans toucher à la RAM. Les boucles
rapides sont les boucles dont le corps tient dans le cache L1, mais des boucles encore
plus rapide sont ces petites qui tiennent dans le cache des micro-opérations.
3.12.1 x86
…est compilée de manière très prédictive:
IDIV divise le nombre 64-bit stocké dans la paire de registres EDX:EAX par la valeur
dans ECX. Comme résultat, EAX contiendra le quotient, et EDX— le reste. Le résultat
de la fonction f() est renvoyé dans le registre EAX, donc la valeur n’est pas déplacée
après la division, elle est déjà à la bonne place.
Puisque IDIV utilise la valeur dans la paire de registres EDX:EAX, l’instruction CDQ
(avant IDIV) étend la valeur dans EAX en une valeur 64-bit, en tenant compte du
signe, tout comme MOVSX le fait.
Si nous mettons l’optimisation (/Ox), nous obtenons:
651
Listing 3.21: MSVC avec optimisation
_a$ = 8 ; size = 4
_f PROC
Ceci est la division par la multiplication. L’opération de multiplication est bien plus
rapide. Et il possible d’utiliser cette astuce 12 pour produire du code effectivement
équivalent et plus rapide.
Ceci est aussi appelé «strength reduction » dans les optimisations du compilateur.
GCC 4.4.1 génère presque le même code, même sans flag d’optimisation, tout comme
MSVC avec l’optimisation:
push ebp
mov ebp, esp
mov ecx, [ebp+arg_0]
mov edx, 954437177 ; 38E38E39h
mov eax, ecx
imul edx
sar edx, 1
mov eax, ecx
sar eax, 1Fh
mov ecx, edx
sub ecx, eax
mov eax, ecx
pop ebp
retn
f endp
652
font cela pour l’arithmétique en virgule flottante, par exemple, l’instruction FDIV en
code x86 peut être remplacée par FMUL. Au moins MSVC 6.0 va remplacer la division
par 9 par un multiplication par 0.111111... et parfois il est difficile d’être sûr de quelle
opération il s’agissait dans le code source.
Mais lorsque nous opérons avec des valeurs entières et des registres CPU entier,
nous ne pouvons pas utiliser de fractions. Toutefois, nous pouvons retravailler la
fraction comme ceci:
result = x
9
=x⋅ 1
9
=x⋅ 1⋅M agicN umber
9⋅M agicN umber
Avec le fait que la division par 2n est très rapide (en utilisant des décalages), nous
devons maintenant trouver quels M agicN umber, pour lesquels l’équation suivante
sera vraie: 2n = 9 ⋅ M agicN umber.
La division par 232 est quelque peu cachée: la partie basse 32-bit du produit dans
EAX n’est pas utilisée (ignorée), seule la partie haute 32-bit du produit (dans EDX)
est utilisée et ensuite décalée de 1 bit additionnel.
954437177
Autrement dit, le code assembleur que nous venons de voir multiplie par 232+1 ,
32+1
2
ou divise par 954437177 . Pour trouver le diviseur, nous avons juste à diviser le numéra-
teur par le dénominateur. En utilisant Wolfram Alpha, nous obtenons 8.99999999....
comme résultat (qui est proche de 9).
En lire plus à ce sujet dans [Henry S. Warren, Hacker’s Delight, (2002)10-3].
Beaucoup de gens manquent la division “cachée” par 232 or 264 , lorsque la partie
basse 32-bit (ou la partie 64-bit) du produit n’est pas utilisée. C’est pourquoi la divi-
sion par la multiplication est difficile à comprendre au début.
Mathematics for Programmers13 a une autre explication.
3.12.3 ARM
Le processeur ARM, tout comme un autre processeur «pur » RISC n’a pas d’instruc-
tion pour la division. Il manque aussi une simple instruction pour la multiplication
avec une constante 32-bit (rappelez-vous qu’une constante 32-bit ne tient pas dans
un opcode 32-bit).
En utilisant de cette astuce intelligente (ou hack), il est possible d’effectuer la divi-
sion en utilisant seulement trois instructions: addition, soustraction et décalages de
bit ( 1.28 on page 391).
Voici un exemple qui divise un nombre 32-bit par 10, tiré de [Advanced RISC Ma-
chines Ltd, The ARM Cookbook, (1994)3.3 Division by a Constant]. La sortie est
constituée du quotient et du reste.
; prend l'argument dans a1
; renvoie le quotient dans a1, le reste dans a2
; on peut utiliser moins de cycles si seul le quotient ou le reste est
requis
SUB a2, a1, #10 ; garde (x-10) pour plus tard
13. https://yurichev.com/writings/Math-for-programmers.pdf
653
SUB a1, a1, a1, lsr #2
ADD a1, a1, a1, lsr #4
ADD a1, a1, a1, lsr #8
ADD a1, a1, a1, lsr #16
MOV a1, a1, lsr #3
ADD a3, a1, a1, asl #2
SUBS a2, a2, a3, asl #1 ; calcule (x-10) - (x/10)*10
ADDPL a1, a1, #1 ; fix-up quotient
ADDMI a2, a2, #10 ; fix-up reste
MOV pc, lr
Ce code est presque le même que celui généré par MSVC avec optimisation et GCC.
Il semble que LLVM utilise le même algorithme pour générer des constantes.
Le lecteur attentif pourrait se demander comment MOV écrit une valeur 32-bit dans
un registre, alors que ceci n’est pas possible en mode ARM.
C’est impossible, en effet, mais on voit qu’il y a 8 octets par instruction, au lieu des
4 standards, en fait, ce sont deux instructions.
La première instruction charge 0x8E39 dans les 16 bits bas du registre et la seconde
instruction est MOVT, qui charge 0x383E dans les 16 bits hauts du registre. IDA re-
connaît de telles séquences, et par concision, il les réduit a une seule « pseudo-
instruction ».
L’instruction SMMUL (Signed Most Significant Word Multiply mot le plus significatif
d’une multiplication signée), multiplie deux nombres, les traitant comme des nombres
signés et laisse la partie 32-bit haute dans le registre R0, en ignorant la partie 32-bit
basse du résultat.
L’instruction «MOV R1, R0,ASR#1 » est le décalage arithmétique à droite d’un bit.
«ADD R0, R1, R0,LSR#31 » est R0 = R1 + R0 >> 31
Il n’y a pas d’instruction de décalage séparée en mode ARM. A la place, des instruc-
tions comme (MOV, ADD, SUB, RSB)14 peuvent avoir un suffixe, indiquant si le second
argument doit être décalé, et si oui, de quelle valeur et comment. ASR signifie Arith-
metic Shift Right, LSR—Logical Shift Right.
654
MOV R1, 0x38E38E39
SMMUL.W R0, R0, R1
ASRS R1, R0, #1
ADD.W R0, R1, R0,LSR#31
BX LR
LLVM sans optimisation ne génère pas le code que nous avons vu avant dans cette
section, mais insère à la place un appel à la fonction de bibliothèque ___divsi3.
À propos de Keil: il insère un appel à la fonction de bibliothèque __aeabi_idivmod
dans tous les cas.
3.12.4 MIPS
Pour une raison quelconque, GCC 4.4.5 avec optimisation génère seulement une
instruction de division:
loc_10 :
mflo $v0
jr $ra
or $at, $zero ; slot de délai de branchement, NOP
Ici, nous voyons une nouvelle instruction: BREAK. Elle lève simplement une excep-
tion.
Dans ce cas, une exception est levée si le diviseur est zéro (il n’est pas possible de
diviser par zéro dans les mathématiques conventionnelles).
Mais GCC n’a probablement pas fait correctement le travail d’optimisation et n’a pas
vu que $V0 ne vaut jamais zéro.
Donc le test est laissé ici. Donc, si $V0 est zéro, BREAK est exécuté, signalant l’ex-
ception à l’OS.
Autrement, MFLO s’exécute, qui prend le résultat de la division depuis le registre LO
et le copie dans $V0.
À propos, comme on devrait le savoir, l’instruction MUL laisse les 32bits hauts du
résultat dans le registre HI et les 32 bits bas dans le registre LO.
655
DIV laisse le résultat dans le registre LO, et le reste dans le registre HI.
Si nous modifions la déclaration en «a % 9 », l’instruction MFHI est utilisée au lieu
de MFLO.
3.12.5 Exercice
• http://challenges.re/27
while (*s)
{
rt=rt*10 + (*s-'0') ;
s++;
};
return rt ;
};
int main()
{
printf ("%d\n", my_atoi ("1234")) ;
printf ("%d\n", my_atoi ("1234567890")) ;
};
Donc, tout ce que fait l’algorithme, c’est de lire les chiffres de gauche à droite.
Le caractère ASCII zéro est soustrait de chaque chiffre.
Les chiffres de «0 » à «9 » sont consécutifs dans la table ASCII, donc nous n’avons
même pas besoin de connaître la valeur exacte du caractère «0 ».
Tout ce que nous avons besoin de savoir, c’est que «0 » moins «0 » vaut 0, «9 »
moins «0 » vaut 9, et ainsi de suite.
Soustraire «0 » de chaque caractère résulte en un nombre de 0 à 9 inclus.
656
Tout autre caractère conduit à un résultat incorrect, bien sûr!
Chaque chiffre doit être ajouté au résultat final (dans la variable « rt »), mais le
résultat final est aussi multiplié par 10 à chaque chiffre.
Autrement dit, le résultat est décalé à gauche d’une position au format décimal à
chaque itération.
Le dernier chiffre est ajouté, mais il n’y a pas de décalage.
Un caractère peut-être chargé à deux endroits: le premier caractère et tous les carac-
tères subséquents. Ceci est arrangé de cette manière afin de regrouper les boucles.
Il n’y a pas d’instructions pour multiplier par 10, à la place, deux instructions LEA le
font.
Parfois, MSVC utilise l’instruction ADD avec une constante négative à la place d’un
SUB. C’est le cas.
657
C’est très difficile de dire pourquoi c’est meilleur que SUB. Mais MSVC fait souvent
ceci.
GCC 4.9.1 avec optimisation est plus concis, mais il y a une instruction RET redon-
dante à la fin. Une suffit.
658
|L0.28|
; charger le caractère entré dans R2
LDRB r2,[r1,#0]
; est-ce que c'est l'octet nul? si non, sauter au corps de la boucle.
CMP r2,#0
BNE |L0.12|
; sortir si octet nul.
; la variable "rt" est encore dans le registre R0, prête à être utilisée
dans
; la fonction appelante
BX lr
ENDP
659
GCC 4.9.1 ARM64 avec optimisation
660
int rt=0;
if (*s=='-')
{
negative=1;
s++;
};
while (*s)
{
if (*s<'0' || *s>'9')
{
printf ("Error ! Unexpected char : '%c'\n", *s) ;
exit(0) ;
};
rt=rt*10 + (*s-'0') ;
s++;
};
if (negative)
return -rt ;
return rt ;
};
int main()
{
printf ("%d\n", my_atoi ("1234")) ;
printf ("%d\n", my_atoi ("1234567890")) ;
printf ("%d\n", my_atoi ("-1234")) ;
printf ("%d\n", my_atoi ("-1234567890")) ;
printf ("%d\n", my_atoi ("-a1234567890")) ; // error
};
my_atoi :
sub rsp, 8
movsx edx, BYTE PTR [rdi]
; tester si c'est le signe moins
cmp dl, 45 ; '-'
je .L22
xor esi, esi
test dl, dl
je .L20
.L10 :
; ESI=0 ici si il n'y avait pas de signe moins et 1 si il en avait un
lea eax, [rdx-48]
; tout caractère autre qu'un chiffre résultera en
661
; un nombre non signé plus grand que 9 après
; soustraction donc s'il ne s'agit pas d'un chiffre,
; sauter en L4, où l'erreur doit être rapportée
cmp al, 9
ja .L4
xor eax, eax
jmp .L6
.L7 :
lea ecx, [rdx-48]
cmp cl, 9
ja .L4
.L6 :
lea eax, [rax+rax*4]
add rdi, 1
lea eax, [rdx-48+rax*2]
movsx edx, BYTE PTR [rdi]
test dl, dl
jne .L7
; s'il n'y avait pas de signe moins, sauter l'instruction NEG
; s'il y en avait un, l'exécuter.
test esi, esi
je .L18
neg eax
.L18 :
add rsp, 8
ret
.L22 :
movsx edx, BYTE PTR [rdi+1]
lea rax, [rdi+1]
test dl, dl
je .L20
mov rdi, rax
mov esi, 1
jmp .L10
.L20 :
xor eax, eax
jmp .L18
.L4 :
; signale une erreur. le caractère est dans EDX
mov edi, 1
mov esi, OFFSET FLAT :.LC0 ; "Error! Unexpected char: '%c'\n"
xor eax, eax
call __printf_chk
xor edi, edi
call exit
662
if (*s<'0' || *s>'9')
...
663
28 BNE |L0.36|
29 CMP r6,#0
30 ; negate result
31 RSBNE r0,r5,#0
32 MOVEQ r0,r5
33 POP {r4-r6,pc}
34 ENDP
35
36 |L0.220|
37 DCB "Error ! Unexpected char : '%c'\n",0
Il n’y a pas d’instruction NEG en ARM 32-bit, donc l’opération «Reverse Subtraction »
(ligne 31) est utilisée ici.
Elle est déclenchée si le résultat de l’instruction CMP (à la ligne 29) était «Not Equal »
(non égal) (d’où le suffixe -NE).
Donc ce que fait RSBNE, c’est soustraire la valeur résultante de 0.
Cela fonctionne comme l’opération de soustraction normale, mais échange les opé-
randes
Soustraire n’importe quel nombre de 0 donne sa négation: 0 − x = −x.
Le code en mode Thumb est en gros le même.
GCC 4.9 pour ARM64 peut utiliser l’instruction NEG, qui est disponible en ARM64.
3.13.3 Exercice
Oh, à propos, les chercheurs en sécurité sont souvent confrontés à un comportement
imprévisible de programme lorsqu’il traite des données incorrectes.
Par exemple, lors du fuzzing. À titre d’exercice, vous pouvez essayer d’entrer des
caractères qui ne soient pas des chiffres et de voir ce qui se passe.
Essayez d’expliquer ce qui s’est passé, et pourquoi.
664
int celsius=atol(argv[1]) ;
printf ("%d\n", celsius_to_fahrenheit (celsius)) ;
};
…est compilée de façon très prédictive, toutefois, si nous utilisons l’option d’optimi-
sation de GCC (-O3), nous voyons:
(Ici la division est effectuée avec une multiplication( 3.12 on page 651).)
Oui, notre petite fonction celsius_to_fahrenheit() a été placée juste avant l’appel
à printf().
Pourquoi? C’est plus rapide que d’exécuter la code de cette fonction plus le surcoût
de l’appel/retour.
Les optimiseurs des compilateurs modernes choisissent de mettre en ligne les pe-
tites fonctions automatiquement. Mais il est possible de forcer le compilateur à
mettre en ligne automatiquement certaines fonctions, en les marquants avec le mot
clef «inline » dans sa déclaration.
665
Ce sont des patterns très fréquents et il est hautement recommandé aux rétro-
ingénieurs d’apprendre à les détecter automatiquement.
strcmp()
assert(0) ;
};
666
.L3 :
add esp, 20
mov eax, 1
pop esi
pop edi
ret
_s$ = 8 ; taille = 4
?is_bool@@YA_NPAD@Z PROC ; is_bool
push esi
mov esi, DWORD PTR _s$[esp]
mov ecx, OFFSET $SG3454 ; 'true'
mov eax, esi
npad 4 ; aligner le label suivant
$LL6@is_bool :
mov dl, BYTE PTR [eax]
cmp dl, BYTE PTR [ecx]
jne SHORT $LN7@is_bool
test dl, dl
je SHORT $LN8@is_bool
mov dl, BYTE PTR [eax+1]
cmp dl, BYTE PTR [ecx+1]
jne SHORT $LN7@is_bool
add eax, 2
add ecx, 2
test dl, dl
jne SHORT $LL6@is_bool
$LN8@is_bool :
xor eax, eax
jmp SHORT $LN9@is_bool
$LN7@is_bool :
sbb eax, eax
sbb eax, -1
$LN9@is_bool :
test eax, eax
jne SHORT $LN2@is_bool
mov al, 1
pop esi
ret 0
$LN2@is_bool :
667
test dl, dl
je SHORT $LN12@is_bool
mov dl, BYTE PTR [eax+1]
cmp dl, BYTE PTR [ecx+1]
jne SHORT $LN11@is_bool
add eax, 2
add ecx, 2
test dl, dl
jne SHORT $LL10@is_bool
$LN12@is_bool :
xor eax, eax
jmp SHORT $LN13@is_bool
$LN11@is_bool :
sbb eax, eax
sbb eax, -1
$LN13@is_bool :
test eax, eax
jne SHORT $LN1@is_bool
xor al, al
pop esi
ret 0
$LN1@is_bool :
push 11
push OFFSET $SG3458
push OFFSET $SG3459
call DWORD PTR __imp___wassert
add esp, 12
pop esi
ret 0
?is_bool@@YA_NPAD@Z ENDP ; is_bool
strlen()
668
test cl, cl
jne SHORT $LL3@strlen_tes
sub eax, edx
ret 0
_strlen_test ENDP
strcpy()
memset()
Exemple#1
669
Listing 3.41: GCC 4.9.1 x64 avec optimisation
f :
mov QWORD PTR [rdi], 0
mov QWORD PTR [rdi+8], 0
mov QWORD PTR [rdi+16], 0
mov QWORD PTR [rdi+24], 0
ret
Exemple#2
…tandis que GCC utilise REP STOSQ, en concluant que cela sera plus petit qu’un
paquet de MOVs:
670
mov rcx, rdi
lea rdi, [rdi+8]
xor eax, eax
and rdi, -8
sub rcx, rdi
add ecx, 67
shr ecx, 3
rep stosq
ret
memcpy()
Petits blocs
La routine pour copier des blocs courts est souvent implémentée comme une sé-
quence d’instructions MOV.
671
mov BYTE PTR [edx+6], al
pop ebx
ret
C’est effectué en général ainsi: des blocs de 4-octets sont d’abord copiés, puis un
mot de 16-bit (si nécessaire) et enfin un dernier octet (si nécessaire).
Les structures sont aussi copiées en utilisant MOV : 1.30.4 on page 465.
Longs blocs
Pour copier 128 octets, MSVC utilise une seule instruction MOVSD (car 128 est divisible
par 4) :
Lors de la copie de 123 octets, 30 mots de 32-bit sont tout d’abord copiés en utilisant
MOVSD (ce qui fait 120 octets), puis 2 octets sont copiés en utilisant MOVSW, puis un
autre octet en utilisant MOVSB.
672
_memcpy_123 PROC
push esi
mov esi, DWORD PTR _inbuf$[esp]
push edi
mov edi, DWORD PTR _outbuf$[esp+4]
add edi, 10
mov ecx, 30
rep movsd
movsw
movsb
pop edi
pop esi
ret 0
_memcpy_123 ENDP
GCC utilise une grosse fonction universelle, qui fonctionne pour n’importe quelle
taille de bloc:
673
add esi, 1
test edi, 2
mov BYTE PTR [edx+10], al
mov eax, 122
je .L7
.L25 :
movzx edx, WORD PTR [esi]
add edi, 2
add esi, 2
sub eax, 2
mov WORD PTR [edi-2], dx
jmp .L7
.LFE3 :
memcmp()
Pour n’importe quelle taille de bloc, MSVC 2013 insère la même fonction universelle:
674
cmp al, BYTE PTR [edx]
jne SHORT $LN6@memcmp_123
mov al, BYTE PTR [ecx+1]
cmp al, BYTE PTR [edx+1]
jne SHORT $LN6@memcmp_123
mov al, BYTE PTR [ecx+2]
cmp al, BYTE PTR [edx+2]
jne SHORT $LN6@memcmp_123
cmp esi, -1
je SHORT $LN3@memcmp_123
mov al, BYTE PTR [ecx+3]
cmp al, BYTE PTR [edx+3]
jne SHORT $LN6@memcmp_123
$LN3@memcmp_123 :
xor eax, eax
pop esi
ret 0
$LN6@memcmp_123 :
sbb eax, eax
or eax, 1
pop esi
ret 0
_memcmp_1235 ENDP
strcat()
Ceci est un strcat() inline tel qu’il a été généré par MSVC 6.0. Il y a 3 parties visibles:
1) obtenir la longueur de la chaîne source (premier scasb) ; 2) obtenir la longueur
de la chaîne destination (second scasb) ; 3) copier la chaîne source dans la fin de la
chaîne de destination (paire movsd/movsb).
675
Script IDA
Il y a aussi un petit script IDA pour chercher et suivre de tels morceaux de code inline,
que l’on rencontre fréquemment:
GitHub.
C’est un exemple très simple, qui contient une spécificité: le pointeur sur le tableau
update_me peut-être un pointeur sur le tableau sum, le tableau product ou même le
tableau sum_product—rien ne l’interdit, n’est-ce pas?
Le compilateur est parfaitement conscient de ceci, donc il génère du code avec
quatre étapes dans le corps de la boucle:
• calcule le sum[i] suivant
• calcule le product[i] suivant
• calcule le update_me[i] suivant
• calcule le sum_product[i] suivant—à cette étape, nous devons charger depuis
la mémoire les valeurs sum[i] et product[i] déjà calculées
Et-il possible d’optimiser la dernière étape? Puisque nous avons déjà calculé sum[i]
et product[i], il n’est pas nécessaire de les charger à nouveau depuis la mémoire.
Oui, mais le compilateurs n’est pas sûr que rien n’a été récris à la 3ème étape! Ceci
est appelé «pointer aliasing », une situation dans laquelle le compilateur ne peut
pas être sûr que la mémoire sur laquelle le pointeur pointe n’a pas été modifiée.
restrict dans le standard C99 [ISO/IEC 9899:TC3 (C C99 standard), (2007) 6.7.3/1]
est une promesse faite par le programmeur au compilateur que les arguments de
la fonction marqués par ce mot-clef vont toujours pointer vers des case mémoire
différentes et ne vont jamais se recouper.
Pour être plus précis et décrire ceci formellement, restrict indique que seul ce poin-
teur est utilisé pour accéder un objet, et qu’aucun autre pointeur ne sera utilisé pour
ceci.
676
On peut même dire que l’objet ne sera accéder que par un seul pointeur, si il est
marqué comme restrict.
Ajoutons ce mot-clef à chaque argument pointeur:
void f2 (int* restrict x, int* restrict y, int* restrict sum, int* restrict⤦
Ç product, int* restrict sum_product,
int* restrict update_me, size_t s)
{
for (int i=0; i<s ; i++)
{
sum[i]=x[i]+y[i];
product[i]=x[i]*y[i];
update_me[i]=i*123; // some dummy value
sum_product[i]=sum[i]+product[i];
};
};
Regardons le résultat:
Listing 3.55: GCC x64: f1()
f1 :
push r15 r14 r13 r12 rbp rdi rsi rbx
mov r13, QWORD PTR 120[rsp]
mov rbp, QWORD PTR 104[rsp]
mov r12, QWORD PTR 112[rsp]
test r13, r13
je .L1
add r13, 1
xor ebx, ebx
mov edi, 1
xor r11d, r11d
jmp .L4
.L6 :
mov r11, rdi
mov rdi, rax
.L4 :
lea rax, 0[0+r11*4]
lea r10, [rcx+rax]
lea r14, [rdx+rax]
lea rsi, [r8+rax]
add rax, r9
mov r15d, DWORD PTR [r10]
add r15d, DWORD PTR [r14]
mov DWORD PTR [rsi], r15d ; stocker dans sum[]
mov r10d, DWORD PTR [r10]
imul r10d, DWORD PTR [r14]
mov DWORD PTR [rax], r10d ; stocker dans product[]
mov DWORD PTR [r12+r11*4], ebx ; stocker dans update_me[]
add ebx, 123
mov r10d, DWORD PTR [rsi] ; recharger sum[i]
add r10d, DWORD PTR [rax] ; recharger product[i]
lea rax, 1[rdi]
cmp rax, r13
677
mov DWORD PTR 0[rbp+r11*4], r10d ; stocker dans sum_product[]
jne .L6
.L1 :
pop rbx rsi rdi rbp r12 r13 r14 r15
ret
La différence entre les fonctions f1() et f2() compilées est la suivante: dans f1(),
sum[i] et product[i] sont rechargés au milieu de la boucle, et il n’y a rien de tel
dans f2(), les valeurs déjà calculées sont utilisées, puisque nous avons «promis »
au compilateur que rien ni personne ne changera les valeurs pendant l’exécution du
corps de la boucle, donc il est «certain » qu’il n’y a pas besoin de recharger la valeur
depuis la mémoire.
Étonnamment, le second exemple est plus rapide.
Mais que se passe-t-il si les pointeurs dans les arguments de la fonction se modifient
d’une manière ou d’une autre?
Ceci est du ressort de la conscience du programmeur, et le résultat sera incorrect.
678
Retournons au Fortran.
Les compilateurs de ce langage traitent tous les pointeurs de cette façon, donc lors-
qu’il n’est pas possible d’utiliser restrict en C, Fortran peut générer du code plus
rapide dans ces cas.
À quel point est-ce pratique?
Dans les cas où le fonction travaille avec des gros blocs en mémoire.
C’est le cas en algèbre linéaire, par exemple.
Les superordinateurs/HPC15 utilisent beaucoup d’algèbre linéaire, c’est probable-
ment pourquoi,
traditionnellement, Fortran y est encore utilisé [Eugene Loh, The Ideal HPC Program-
ming Language, (2010)].
Mais lorsque le nombre d’itérations n’est pas très important, certainement, le gain
en vitesse ne doit pas être significatif.
679
sub eax, edx
ret
680
3.17 Fonctions variadiques
Les fonctions comme printf() et scanf() peuvent avoir un nombre variable d’ar-
guments. Comment sont-ils accédés?
Il y a le fichier d’entête standard stdarg.h qui défini des macros pour prendre en
compte de tels arguments.
Les fonctions printf() et scanf() l’utilisent aussi.
#include <stdio.h>
#include <stdarg.h>
while(1)
{
i=va_arg(args, int) ;
if (i==-1) // terminateur
break ;
sum=sum+i ;
count++;
}
va_end(args) ;
return sum/count ;
};
int main()
{
printf ("%d\n", arith_mean (1, 2, 7, 10, 15, -1 /* terminateur */))⤦
Ç ;
};
681
Qu’y a-t-il à l’intérieur?
cdq
idiv esi
pop esi
ret 0
_arith_mean ENDP
682
La fonction arith_mean() prend la valeur du premier argument et le stocke dans la
variable sum.
Puis, elle met dans le registre EDX l’adresse du second argument, prend sa valeur,
l’ajoute à sum, et fait cela dans une boucle infinie, jusqu’à ce que −1 soit trouvé.
Lorsqu’il est rencontré, la somme est divisée par le nombre de valeurs (en excluant
−1) et le quotient est renvoyé.
Donc, autrement dit, la fonction traite le morceau de pile comme un tableau de
valeurs entières d’une longueur infinie.
Maintenant nous pouvons comprendre pourquoi la convention d’appel cdecl nous
force à pousser le premier argument au moins sur la pile.
Car sinon, il ne serait pas possible de trouver le premier argument, ou, pour les
fonctions du genre de printf, il ne serait pas possible de trouver l’adresse de la chaîne
de format.
v$ = 8
arith_mean PROC
mov DWORD PTR [rsp+8], ecx ; 1er argument
mov QWORD PTR [rsp+16], rdx ; 2nd argument
mov QWORD PTR [rsp+24], r8 ; 3ème argument
mov eax, ecx ; sum = 1er argument
lea rcx, QWORD PTR v$[rsp+8] ; pointeur sur le 2nd argument
mov QWORD PTR [rsp+32], r9 ; 4ème argument
mov edx, DWORD PTR [rcx] ; charger le 2nd argument
mov r8d, 1 ; count=1
cmp edx, -1 ; est-ce que le 2nd argument est
-1?
je SHORT $LN8@arith_mean ; sortir si oui
$LL3@arith_mean :
add eax, edx ; sum = sum + argument chargé
mov edx, DWORD PTR [rcx+8] ; charger l'argument suivant
lea rcx, QWORD PTR [rcx+8] ; décaler le pointeur pour pointer
; sur l'argument après le suivant
inc r8d ; count++
cmp edx, -1 ; est-ce que l'argument chargé est
-1?
jne SHORT $LL3@arith_mean ; aller au début de la boucle si
non
$LN8@arith_mean :
; calculer le quotient
cdq
idiv r8d
683
ret 0
arith_mean ENDP
main PROC
sub rsp, 56
mov edx, 2
mov DWORD PTR [rsp+40], -1
mov DWORD PTR [rsp+32], 15
lea r9d, QWORD PTR [rdx+8]
lea r8d, QWORD PTR [rdx+5]
lea ecx, QWORD PTR [rdx-1]
call arith_mean
lea rcx, OFFSET FLAT :$SG3013
mov edx, eax
call printf
xor eax, eax
add rsp, 56
ret 0
main ENDP
Nous voyons que les 4 premiers arguments sont passés dans des registres, et les
deux autres—par la pile.
La fonction arith_mean() place d’abord ces 4 arguments dans le Shadow Space puis
traite le Shadow Space et la pile derrière comme s’il s’agissait d’un tableau continu!
Qu’en est-il de GCC? Les choses sont légèrement plus maladroites ici, car maintenant
la fonction est divisée en deux parties: la première partie sauve les registres dans
la «zone rouge », traite cet espace, et la seconde partie traite la pile:
684
add rcx, rax
mov ecx, DWORD PTR [rcx]
cmp ecx, -1
je .L4
.L8 :
add edi, ecx
add r8d, 1
.L5 :
; décider, quelle partie traiter maintenant.
; est-ce que le nombre d'arguments actuel est inférieur ou égal à 6?
cmp esi, 47
jbe .L7 ; non, traiter les arguments sauvegardés;
; traiter les arguments de la pile
mov rcx, rdx
add rdx, 8
mov ecx, DWORD PTR [rcx]
cmp ecx, -1
jne .L8
.L4 :
mov eax, edi
cdq
idiv r8d
ret
.LC1 :
.string "%d\n"
main :
sub rsp, 8
mov edx, 7
mov esi, 2
mov edi, 1
mov r9d, -1
mov r8d, 15
mov ecx, 10
xor eax, eax
call arith_mean
mov esi, OFFSET FLAT :.LC1
mov edx, eax
mov edi, 1
xor eax, eax
add rsp, 8
jmp __printf_chk
À propos, un usage similaire du Shadow Space est aussi considéré ici: 6.1.8 on
page 971.
685
{
int *i=&v ;
int sum=*i, count=1;
i++;
while(1)
{
if ((*i)==-1) // terminator
break ;
sum=sum+(*i) ;
count++;
i++;
}
return sum/count ;
};
int main()
{
printf ("%d\n", arith_mean (1, 2, 7, 10, 15, -1 /* terminator */)) ;
// test: https://www.wolframalpha.com/input/?i=mean(1,2,7,10,15)
};
Autrement dit, si l’argument mis est un tableau de mots (32-bit ou 64-bit), nous
devons juste énumérer les éléments du tableau en commençant par le premier.
686
En examinant plus précisément, nous voyons que va_list est un pointeur sur un
tableau. Compilons:
Nous voyons que tout ce que fait notre fonction est de prendre un pointeur sur les
arguments et le passe à la fonction vprintf(), et que cette fonction le traite comme
un tableau infini d’arguments!
687
INS_InsertPredicatedCall(
ins, IPOINT_BEFORE, (AFUNPTR)RecordMemRead,
IARG_INST_PTR,
IARG_MEMORYOP_EA, memOp,
IARG_END) ;
( pinatrace.cpp )
Et voici comment la fonction INS_InsertPredicatedCall() est déclarée:
extern VOID INS_InsertPredicatedCall(INS ins, IPOINT ipoint, AFUNPTR funptr⤦
Ç , ...) ;
( pin_client.PH )
Ainsi, les constantes avec un nom débutant par IARG_ sont des sortes d’arguments
pour la fonction, qui sont manipulés à l’intérieur de INS_InsertPredicatedCall().
Vous pouvez passer autant d’arguments que vous en avez besoin. Certaines com-
mandes ont des arguments additionnels, d’autres non. Liste complète des argu-
ments: https://software.intel.com/sites/landingpage/pintool/docs/58423/
Pin/html/group__INST__ARGS.html. Et il faut un moyen pour détecter la fin de la
liste des arguments, donc la liste doit être terminée par la constante IARG_END, sans
laquelle, la fonction essayerait de traiter les données indéterminées dans la pile lo-
cale comme des arguments additionnels.
Aussi, dans [Brian W. Kernighan, Rob Pike, Practice of Programming, (1999)] nous
pouvons trouver un bel exemple de routines C/C++ très similaires à pack/unpack 17
en Python.
int main()
{
char *s1="hello" ;
char *s2="world" ;
char buf[128];
printf ("%s") ;
17. https://docs.python.org/3/library/struct.html
688
};
Veuillez noter que printf() n’a pas d’argument supplémentaire autre que la chaîne
de format.
Maintenant, imaginons que c’est l’attaquant qui a mis la chaîne %s dans le premier
argument du dernier printf(). Je compile cet exemple en utilisant GCC 5.4.0 sous
Ubuntu x86, et l’exécutable résultant affiche la chaîne «world » s’il est exécuté!
Si je compile avec l’optimisation, printf() affiche n’importe quoi, aussi—probablement,
l’appel à strcpy() a été optimisé et/ou les variables locales également. De même, le
résultat pour du code x64 sera différent, pour différents compilateurs, OS, etc.
Maintenant, disons que l’attaquant peut passer la chaîne suivante à l’appel de printf() :
%x %x %x %x %x. Dans mon cas, la sortie est: «80485c6 b7751b48 1 0 80485c0 »
(ce sont simplement des valeurs de la pile locale). Vous voyez, il y a les valeurs 1 et
0, et des pointeurs (le premier est probablement un pointeur sur la chaîne «world »).
Donc si l’attaquant passe la chaîne %s %s %s %s %s, le processus va se planter, car
printf() traite 1 et/ou 0 comme des pointeurs sur une chaîne, essaye de lire des
caractères et échoue.
Encore pire, il pourrait y avoir sprintf (buf, string) dans le code, où buf est un
buffer dans la pile locale avec un taille de 1024 octets ou autre, l’attaquant pourrait
préparer une chaîne de telle sorte que buf serait débordé, peut-être même de façon
à conduire à l’exécution de code.
De nombreux logiciels bien connus et très utilisés étaient (ou sont encore) vulné-
rables:
QuakeWorld went up, got to around 4000 users, then the master
server exploded.
(QuakeWorld est arrivé, monté à environ 4000 utilisateurs, puis le ser-
veur master a explosé.)
Disrupter and cohorts are working on more robust code now.
(Les perturbateurs et cohortes travaillent maintenant sur un code plus
robuste.)
If anyone did it on purpose, how about letting us know... (It wasn’t
all the people that tried %s as a name)
(Si quelqu’un l’a fait exprès, pourquoi ne pas nous le faire savoir... (Ce
n’est pas tout le monde qui a essayé %s comme nom))
689
zapper les variables locales en passant plusieurs commandes %n dans la chaîne de
format.
int main()
{
// test
690
printf ("[%s]\n", str_trim (strdup("test3\n\r\n\r"))) ;
printf ("[%s]\n", str_trim (strdup("test4\n"))) ;
printf ("[%s]\n", str_trim (strdup("test5\r"))) ;
printf ("[%s]\n", str_trim (strdup("test6\r\r\r"))) ;
};
L’argument en entrée est toujours renvoyé en sortie, ceci est pratique lorsque vous
voulez chaîner les fonctions de traitement de chaîne, comme c’est fait ici dans la
fonction main().
La seconde partie de for() (str_len>0 && (c=s[str_len-1])) est appelé le «short-
circuit » en C/C++ et est très pratique [Dennis Yurichev, C/C++ programming lan-
guage notes1.3.8].
Les compilateurs C/C++ garantissent une séquence d’évaluation de gauche à droite.
Donc, si la première clause est fausse après l’évaluation, la seconde n’est pas éva-
luée.
691
cmp al, 10
jne SHORT $LN15@str_trim
$LN2@str_trim :
; le dernier caractère a un code de 13 ou 10
; écrire zéro à cet endroit:
mov BYTE PTR [rcx], 0
; décrémenter l'adresse du dernier caractère,
; donc il pointera sur le caractère précédent celui qui vient d'être effacé:
dec rcx
lea rax, QWORD PTR [r8+rcx]
; RAX = 1 - s + adresse du dernier caractère courant
; ainsi nous pouvons déterminer si nous avons atteint le premier caractère
et
; nous devons arrêter, si c'est le cas
test rax, rax
jne SHORT $LL6@str_trim
$LN15@str_trim :
mov rax, rdx
ret 0
str_trim ENDP
Tout d’abord, MSVC a inliné le code la fonction strlen(), car il en a conclus que ceci
était plus rapide que le strlen() habituel + le coût de l’appel et du retour. Ceci est
appelé de l’inlining: 3.14 on page 664.
La première instruction de strlen() mis en ligne est
OR RAX, 0xFFFFFFFFFFFFFFFF.
MSVC utilise souvent OR au lieu de MOV RAX, 0xFFFFFFFFFFFFFFFF, car l’opcode
résultant est plus court.
Et bien sûr, c’est équivalent: tous les bits sont mis à 1, et un nombre avec tous les
bits mis vaut −1 en complément à 2: 2.2 on page 585.
On peut se demander pourquoi le nombre −1 est utilisé dans strlen(). À des fins
d’optimisation, bien sûr. Voici le code que MSVC a généré:
Essayez d’écrite plus court si vous voulez initialiser le compteur à 0! OK, essayons:
692
cmp byte ptr [rcx+rax], 0
jz exit
inc rax
jmp label
exit :
; RAX = longueur de la chaîne
Nous avons échoué. Nous devons utilisé une instruction JMP additionnelle!
Donc, ce que le compilateur de MSVC 2013 a fait, c’est de déplacer l’instruction INC
avant le chargement du caractère courant.
Si le premier caractère est 0, c’est OK, RAX contient 0 à ce moment, donc la longueur
de la chaîne est 0.
Le reste de cette fonction semble facile à comprendre.
693
lea rdx, [rax-1] ; RDX=str_len-1
mov rax, QWORD PTR [rbp-24] ; RAX=s
add rax, rdx ; RAX=s+str_len-1
movzx eax, BYTE PTR [rax] ; AL=s[str_len-1]
mov BYTE PTR [rbp-9], al ; stocker l caractère chargé dans
"c"
cmp BYTE PTR [rbp-9], 0 ; est-ce zéro?
jne .L5 ; oui? alors sortir
; la deuxième partie de for() se termine ici
.L4 :
; renvoyer "s"
mov rax, QWORD PTR [rbp-24]
leave
ret
694
je .L9 ; sortir si c'est zéro
cmp cl, 10
je .L4
cmp cl, 13 ; sortir si ce n'est ni '\n' ni '\r'
jne .L9
.L4 :
; ceci est une instruction bizarre, nous voulons RSI=s-1 ici.
; c'est possible de l'obtenir avec MOV RSI, EBX / DEC RSI
; mais ce sont deux instructions au lieu d'une
sub rsi, rax
; RSI = s+str_len-1-str_len = s-1
; la boucle principale commence
.L12 :
test rdx, rdx
; stocker zéro à l'adresse s-1+str_len-1+1 = s-1+str_len = s+str_len-1
mov BYTE PTR [rsi+1+rdx], 0
; tester si str_len-1==0. sortir si oui.
je .L9
sub rdx, 1 ; équivalent à str_len--
; charger le caractère suivant à l'adresse s+str_len-1
movzx ecx, BYTE PTR [rbx+rdx]
test cl, cl ; est-ce zéro? sortir si oui
je .L9
cmp cl, 10 ; est-ce '\n'?
je .L12
cmp cl, 13 ; est-ce '\r'?
je .L12
.L9 :
; renvoyer "s"
mov rax, rbx
pop rbx
ret
695
Maintenant la boucle principale est très courte, ce qui est bon pour les derniers CPUs.
Le code n’utilise pas la variable str_len, mais str_len-1. Donc c’est plus comme un
index dans un buffer.
Apparemment, GCC a remarqué que l’expression str_len-1 est utilisée deux fois.
Donc, c’est mieux d’allouer une variable qui contient toujours une valeur qui est plus
petite que la longueur actuelle de la chaîne de un, et la décrémente (ceci a le même
effet que de décrémenter la variable str_len).
696
; str_len==0?
cmp x0, xzr
; sauter alors à la sortie
beq .L4
ldr x0, [x29,40]
; X0=str_len
sub x0, x0, #1
; X0=str_len-1
ldr x1, [x29,24]
; X1=s
add x0, x1, x0
; X0=s+str_len-1
; charger l'octet à l'adresse s+str_len-1 dans W0
ldrb w0, [x0]
strb w0, [x29,39] ; stocker l'octet chargé dans "c"
ldrb w0, [x29,39] ; le recharger
; est-ce l'octet zéro?
cmp w0, wzr
; sauter à la sortie, si c'est zéro ou en L5 sinon
bne .L5
.L4 :
; renvoyer s
ldr x0, [x29,24]
ldp x29, x30, [sp], 48
ret
697
; W2=octet chargé
cbz w2, .L9 ; est-ce zéro? sauter alors à la sortie
cmp w2, 10 ; est-ce '\n'?
bne .L15
.L12 :
; corps de la boucle principale. Le caractère chargé est toujours 10 ou 13 à
ce moment!
sub x2, x1, x0
; X2=X1-X0=str_len-1-str_len=-1
add x2, x3, x2
; X2=X3+X2=s+str_len-1+(-1)=s+str_len-2
strb wzr, [x2,1] ; stocker l'octet zéro à l'adresse
s+str_len-2+1=s+str_len-1
cbz x1, .L9 ; str_len-1==0? sauter à la sortie si oui
sub x1, x1, #1 ; str_len--
ldrb w2, [x19,x1] ; charger le caractère suivant à l'adresse
X19+X1=s+str_len-1
cmp w2, 10 ; est-ce '\n'?
cbz w2, .L9 ; sauter à la sortie, si c'est zéro
beq .L12 ;
sauter au début du corps de la boucle, si c'est '\n'
.L15 :
cmp w2, 13 ; est-ce '\r'?
beq .L12 ; oui, sauter au début du corps de la boucle
.L9 :
; renvoyer "s"
mov x0, x19
ldr x19, [sp,16]
ldp x29, x30, [sp], 32
ret
698
BEQ |L0.56| ; sauter à la sortie si str_len==0 ou si
l'octet chargé est 0
CMP r1,#0xd ; est-ce que l'octet chargé est '\r'?
CMPNE r1,#0xa ;
(si l'octet chargé n'est pas '\r') est-ce '\r'?
SUBEQ r0,r0,#1 ;
(si l'octet chargé est '\r' ou '\n') R0-- ou str_len--
STRBEQ r3,[r2,#-1] ; (si l'octet chargé est '\r' ou '\n') stocker
R3 (zéero) à l'adresse R2-1=s+str_len-1
BEQ |L0.16| ;
sauter au début de a boucle si l'octet chargé était '\r' ou '\n'
|L0.56|
; renvoyer "s"
MOV r0,r4
POP {r4,pc}
ENDP
699
23 LDRB r1,[r2,#0x1f] ; charger l'octet à l'adresse
R2+0x1F=s+str_len-0x20+0x1F=s+str_len-1 dans R1
24 CMP r1,#0 ; est-ce que l'octet chargé est 0?
25 BNE |L0.12| ; sauter au début de la boucle, si ce n'est
pas 0
26 |L0.38|
27 ; renvoyer "s"
28 MOVS r0,r4
29 POP {r4,pc}
30 ENDP
3.18.8 MIPS
Listing 3.71: GCC 4.4.5 avec optimisation (IDA)
str_trim :
; IDA n'a pas connaissance des noms des variables locales, nous les entrons
manuellement
saved_GP = -0x10
saved_S0 = -8
saved_RA = -4
700
; $v1 = $s0+$v1 = s+str_len-2
li $a2, 0xD
; sauter le corps de boucle:
b loc_6C
li $a3, 0xA ; slot de délai de branchement
loc_5C :
; charger l'octet suivant de la mémoire dans $a0:
lb $a0, 0($v1)
move $a1, $v1
; $a1=s+str_len-2
; sauter à la sortie si l'octet chargé est zéro:
beqz $a0, exit
; décrémenter str_len:
addiu $v1, -1 ; slot de délai de branchement
loc_6C :
; à ce moment, $a0=octet chargé, $a2=0xD (symbole CR) et $a3=0xA (symbole
LF)
; l'octet chargé est CR? sauter alors en loc_7C:
beq $a0, $a2, loc_7C
addiu $v0, -1 ; slot de délai de branchement
; l'octet chargé est LF? sauter à la sortie si ce n'est pas LF:
bne $a0, $a3, exit
or $at, $zero ; slot de délai de branchement, NOP
loc_7C :
; l'octet chargé est CR à ce moment
; sauter en loc_5c (début du corps de la boucle) si str_len (dans $v0) n'est
pas zéro:
bnez $v0, loc_5C
; simultanément, stocker zéro à cet endroit en mémoire:
sb $zero, 0($a1) ; slot de délai de branchement
; le label "exit" à été renseigné manuellemnt:
exit :
lw $ra, 0x20+saved_RA($sp)
move $v0, $s0
lw $s0, 0x20+saved_S0($sp)
jr $ra
addiu $sp, 0x20 ; slot de délai de branchement
Les registres préfixés avec S- sont aussi appelés «saved temporaries » (sauvé tem-
porairement), donc la valeur de $S0 est sauvée dans la pile locale et restaurée à la
fin.
701
return c ;
}
L’expression 'a'+'A' est laissée dans le code source pour améliorer la lisibilité, elle
sera optimisée par le compilateur, bien sûr. 21 .
Le code ASCII de «a » est 97 (ou 0x61), et celui de «A », 65 (ou 0x41).
La différence (ou distance) entre les deux dans la table ASCII est 32 (ou 0x20).
Pour une meilleure compréhension, le lecteur peut regarder la table ASCII 7-bit stan-
dard:
3.19.1 x64
Deux opérations de comparaison
MSVC sans optimisation est direct: le code vérifie si le symbole en entrée est dans
l’intervalle [97..122] (ou dans l’intervalle [‘a’..‘z’]) et soustrait 32 si c’est le cas.
Il y a quelques artefacts du compilateur:
21. Toutefois, pour être méticuleux, il y a toujours des compilateurs qui ne peuvent pas optimiser de
telles expressions et les laissent telles quelles dans le code.
702
15 movzx eax, BYTE PTR c$[rsp] ; casting inutile
16 $LN1@toupper :
17 $LN3@toupper : ; artefact du compilateur
18 ret 0
19 toupper ENDP
Il est important de remarquer que l’octet en entrée est chargé dans un slot 64-bit
de la pile locale à la ligne 3.
Tous les bits restants ([8..e3]) ne sont pas touchés, i.e., contiennent du bruit indéter-
miné (vous le verrez dans le débogueur).
Toutes les instructions opèrent seulement au niveau de l’octet, donc c’est bon.
La dernière instruction MOVZX à la ligne 15 prend un octet de la pile locale et l’étend
avec des zéro à un type de donnée int 32-bit.
GCC sans optimisation fait essentiellement la même chose:
MSVC avec optimisation fait un meilleur travail, il ne génère qu’une seule opération
de comparaison:
703
movzx eax, cl
ret 0
toupper ENDP
Il a déjà été expliqué comment remplacer les deux opérations de comparaison par
une seule: 3.13.2 on page 662.
Nous allons maintenant récrire ceci en C/C++ :
int tmp=c-97;
if (tmp>25)
return c ;
else
return c-32;
3.19.2 ARM
Keil avec optimisation pour le mode ARM génère aussi une seule comparaison:
704
SUBLS r0,r0,#0x20
ANDLS r0,r0,#0xff
BX lr
ENDP
Les instructions SUBLS et ANDLS ne sont exécutées que si la valeur dans R1 est
inférieure à 0x19 (ou égale).
Keil avec optimisation pour le mode Thumb génère lui aussi une seule opération de
comparaison:
Listing 3.77: avec optimisation Keil 6/2013 (Mode Thumb)
toupper PROC
MOVS r1,r0
SUBS r1,r1,#0x61
CMP r1,#0x19
BHI |L0.14|
SUBS r0,r0,#0x20
LSLS r0,r0,#24
LSRS r0,r0,#24
|L0.14|
BX lr
ENDP
Les deux dernières instructions LSLS et LSRS fonctionnent comme AND reg, 0xFF :
elles sont équivalentes à l’expression C/C++ (i << 24) >> 24.
Il semble que Keil pour le mode Thumb déduit que ces deux instructions de 2-octets
sont plus courtes que le code qui charge la constante 0xFF dans un registre plus une
instruction AND.
705
Listing 3.79: GCC 4.9 (ARM64) avec optimisation
toupper :
uxtb w0, w0
sub w1, w0, #97
uxtb w1, w1
cmp w1, 25
bhi .L2
sub w0, w0, #32
uxtb w0, w0
.L2 :
ret
Le code est proche de ce GCC avec optimisation a produit pour l’exemple précédent
( 3.75 on page 704) :
706
Very old keyboards used to do Shift just by toggling the 32 or 16
bit, depending on the key; this is why the relationship between small
and capital letters in ASCII is so regular, and the relationship between
numbers and symbols, and some pairs of symbols, is sort of regular if
you squint at it.
int main()
{
// affichera "hELLO, WORLD!"
for (char *s="Hello, world !" ; *s ; s++)
printf ("%c", flip(*s)) ;
};
3.19.4 Summary
Toutes ces optimisations de compilateurs sont aujourd’hui courantes et un rétro-
ingénieur pratiquant voit souvent ce genre de patterns de code.
3.20 Obfuscation
L’obfuscation est une tentative de cacher le code (ou sa signification) aux rétro-
ingénieurs.
707
mov byte ptr [ebx], 'h'
mov byte ptr [ebx+1], 'e'
mov byte ptr [ebx+2], 'l'
mov byte ptr [ebx+3], 'l'
mov byte ptr [ebx+4], 'o'
mov byte ptr [ebx+5], ' '
mov byte ptr [ebx+6], 'w'
mov byte ptr [ebx+7], 'o'
mov byte ptr [ebx+8], 'r'
mov byte ptr [ebx+9], 'l'
mov byte ptr [ebx+10], 'd'
La chaîne peut aussi être comparée avec une autre comme ceci:
mov ebx, offset username
cmp byte ptr [ebx], 'j'
jnz fail
cmp byte ptr [ebx+1], 'o'
jnz fail
cmp byte ptr [ebx+2], 'h'
jnz fail
cmp byte ptr [ebx+3], 'n'
jnz fail
jz it_is_john
Dans les deux cas, il est impossible de trouver ces chaînes directement dans un
éditeur hexadécimal.
À propos, ceci est un moyen de travailler avec des chaînes lorsqu’il est impossible
d’allouer de l’espace pour elles dans le segment de données, par exemple dans un
PIC22 ou un shellcode.
Une autre méthode est d’utiliser sprintf() pour la construction:
sprintf(buf, "%s%c%s%c%s", "hel",'l',"o w",'o',"rld") ;
Le code semble bizarre, mais peut être utile comme simple mesure anti-reversing.
Les chaînes de texte peuvent aussi être présentes dans une forme chiffrée, donc
chaque utilisation d’une chaîne est précédée par une routine de déchiffrement. Par
exemple: 8.8.2 on page 1100.
708
Listing 3.81: code original
add eax, ebx
mul ecx
Ici, le code inutile utilise des registres qui ne sont pas utilisés dans le code réel (ESI
et EDX). Toutefois, les résultats intermédiaires produit par le code réel peuvent être
utilisés par les instructions inutiles pour brouiller les pistes—pourquoi pas?
• MOV op1, op2 peut être remplacé par la paire PUSH op2 / POP op1.
• JMP label peut être remplacé par la paire PUSH label / RET. IDA ne montrera
pas la référence au label.
• CALL label peut être remplacé par le triplet d’instructions suivant:
PUSH label_after_CALL_instruction / PUSH label / RET.
• PUSH op peut aussi être remplacé par la paire d’instructions suivante:
SUB ESP, 4 (or 8) / MOV [ESP], op.
709
mul ecx ; code réel
add eax, esi ; prédicat opaque.
; XOR, AND ou SHL, etc, peuvent être ici au lieu de ADD.
instruction 1
instruction 2
instruction 3
ins2_label : instruction 2
jmp ins3_label
ins3_label : instruction 3
jmp exit :
ins1_label : instruction 1
jmp ins2_label
exit :
func proc
...
mov eax, offset dummy_data1 ; PE or ELF reloc here
add eax, 100h
push eax
call dump_string
...
mov eax, offset dummy_data2 ; PE or ELF reloc here
add eax, 200h
push eax
call dump_string
...
func endp
710
Maintenant, quelque chose de légèrement plus avancé.
Franchement, je ne connais pas con nom exact, mais je vais l’appeler pointeur déca-
lés. Cette technique est assez commune, au moins dans les systèmes de protection
contre la copie.
En bref: lorsque vous écrivez une valeur dans la mémoire globale, vous utilisez une
adresse, mais lorsque vous lisez, vous utilisez la somme d’une (autre) adresse, ou
peut-être une différence. Le but est de cacher l’adresse réelle au rétro-ingénieur qui
débogue le code ou l’explore dans IDA (ou un autre désassembleur).
Ceci peut être pénible.
#include <stdio.h>
void check_lic_key()
{
// pretend licence check has been failed
secret_array[0x6123]=1; // 1 mean failed
void check_again()
{
if (get_byte_at_0x6000(secret_array+0x123)==1)
{
// do something mean (add watermark maybe) or report error:
printf ("check failed\n") ;
}
else
{
// proceed further
};
};
int main()
{
// at start:
check_lic_key() ;
// do something
711
// ... and while in some very critical part:
check_again() ;
};
a = dword ptr 8
push ebp
mov ebp, esp
mov eax, [ebp+a]
mov al, [eax+6000h]
pop ebp
retn
_get_byte_at_0x6000 endp
loc_406735 :
pop ebp
retn
_check_again endp
Vous voyez, IDA obtient seulement deux adresses: secret_array[] (début du ta-
712
bleau) et point_passed_to_get_byte_at_0x6000.
Comment s’y prendre: vous pouvez utiliser les point d’arrêt matériel sur les opéra-
tions d’accès en mémoire, tracer possède l’option BPMx) ou des moteurs d’exécution
symbolique ou peut-être écrire un module pour IDA …
C’est sûr, un tableau peut être utiliser pour des nombreuses valeurs, non limité aux
booléennes…
N.B.: MSVC 2015 avec optimisation est assez malin pour optimiser la fonction
get_byte_at_0x6000().
3.20.5 Exercice
• http://challenges.re/29
3.21 C++
3.21.1 Classes
Un exemple simple
En interne, la représentation des classes C++ est presque la même que les struc-
tures.
Essayons un exemple avec deux variables, deux constructeurs et une méthode:
#include <stdio.h>
class c
{
private :
713
int v1 ;
int v2 ;
public :
c() // ctor par défaut
{
v1=667;
v2=999;
};
void dump()
{
printf ("%d ; %d\n", v1, v2) ;
};
};
int main()
{
class c c1 ;
class c c2(5,6) ;
c1.dump() ;
c2.dump() ;
return 0;
};
MSVC: x86
714
lea ecx, DWORD PTR _c2$[ebp]
call ?dump@c@@QAEXXZ ; c::dump
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
Voici ce qui se passe. Pour chaque objet (instance de la classe c) 8 octets sont alloués,
exactement la taille requise pour stocker les deux variables.
Pour c1 un constructeur par défaut sans argument ??0c@@QAE@XZ est appelé. Pour
c2 un autre constructeur ??0c@@QAE@HH@Z est appelé et deux nombres sont passés
comme arguments.
Un pointeur sur l’objet (this en terminologie C++) est passé dans le registre ECX.
Ceci est appelé thiscall ( 3.21.1)—la méthode pour passer un pointeur à l’objet.
MSVC le fait en utilisant le regsitre ECX. Inutile de le dire, ce n’est pas une méthode
standardisée, d’autres compilateurs peuvent le faire différemment, e.g., par le pre-
mier argument de la fonction (comme GCC).
Pourquoi est-ce que ces fonctions ont un nom aussi étrange? C’est le name mangling.
Une classe C++ peut contenir plusieurs méthodes partageant le même nom mais
ayant des arguments différents—c’est le polymorphisme. Et bien sûr, différentes
classes peuvent avoir leurs propres méthodes avec le même nom.
Le Name mangling nous permet d’encoder le nom de la classe + le nom de la mé-
thode + tous les types des arguments de la méthode dans une chaîne ASCII, qui est
ensuite utilisée comme le nom interne de la fonction. C’est ainsi car ni le linker, ni
le chargeur de DLL de l’OS (les mangled names peuvent aussi se trouver parmi les
exports de DLL) n’ont conscience de C++ ou de l’POO24 .
La fonction dump() est appelée deux fois.
Maintenant, regardons le code du constructeur:
715
ret 0
??0c@@QAE@XZ ENDP ; c::c
_this$ = -4 ; size = 4
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
??0c@@QAE@HH@Z PROC ; c::c, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR _a$[ebp]
mov DWORD PTR [eax], ecx
mov edx, DWORD PTR _this$[ebp]
mov eax, DWORD PTR _b$[ebp]
mov DWORD PTR [edx+4], eax
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 8
??0c@@QAE@HH@Z ENDP ; c::c
Les constructeurs sont juste des fonctions, ils utilisent un pointeur sur la structure
dans ECX, en copiant le pointeur dans leur propre variable locale, toutefois, ce n’est
pas nécessaire.
D’après le standard (C++11 12.1) nous savons que les constructeurs n’ont pas l’obli-
gation de renvoyer une valeur.
En fait, en interne, les constructeurs renvoient un pointeur sur l’objet nouvellement
créé, i.e., this.
Maintenant, la méthode dump() :
716
pop ebp
ret 0
?dump@c@@QAEXXZ ENDP ; c::dump
Assez simple: dump() prend un pointeur sur la structure qui contient les deux int
dans ECX, prend les deux valeurs et les passe à printf().
Le code est bien plus court s’il est compilé avec les optimisations (/Ox) :
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
??0c@@QAE@HH@Z PROC ; c::c, COMDAT
; _this$ = ecx
mov edx, DWORD PTR _b$[esp-4]
mov eax, ecx
mov ecx, DWORD PTR _a$[esp-4]
mov DWORD PTR [eax], ecx
mov DWORD PTR [eax+4], edx
ret 8
??0c@@QAE@HH@Z ENDP ; c::c
C’est tout. L’autre chose que nous devons noter est que le pointeur de pile n’a pas été
corrigé avec add esp, X après l’appel du constructeur. En même temps, le construc-
teur a ret 8 au lieu de RET à la fin.
C’est parce que la convention d’appel thiscall ( 3.21.1 on page 715) est utilisée
ici, qui, comme avec la méthode stdcall ( 6.1.2 on page 962), offre à l’appelée la
possibilité de corriger la pile au lieu de l’appelante. L’instruction ret x ajoute X à la
valeur de ESP, puis passe le contrôle à la fonction appelante.
Regardez la section parlant des conventions d’appel ( 6.1 on page 962).
717
Il faut également noter que le compilateur décide quand appeler le constructeur et
le destructeur—mais nous le savons déjà des bases du langage C++.
MSVC: x86-64
Comme nous le savons déjà, les 4 premiers arguments de fonction en x86-64 sont
passés dans les registres RCX, RDX, R8 et R9, tous les autres—par la pile.
Néanmoins, le pointeur this sur l’objet est passé dans RCX, le premier argument de
la méthode dans RDX, etc. Nous pouvons le voir dans les entrailles de la méthode
c(int a, int b) :
; c(int a, int b)
; default ctor
25
Le type de donnée int est toujours 32-bit en x64 , c’est donc pourquoi les parties
32-bit des registres sont utilisées ici.
Nous voyons également JMP printf au lieu de RET dans la méthode dump(), astuce
que nous avons déjà vu plus tôt: 1.21.1 on page 204.
GCC: x86
718
C’est presque la même chose avec GCC 4.4.1, avec quelques exceptions.
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 20h
lea eax, [esp+20h+var_8]
mov [esp+20h+var_20], eax
call _ZN1cC1Ev
mov [esp+20h+var_18], 6
mov [esp+20h+var_1C], 5
lea eax, [esp+20h+var_10]
mov [esp+20h+var_20], eax
call _ZN1cC1Eii
lea eax, [esp+20h+var_8]
mov [esp+20h+var_20], eax
call _ZN1c4dumpEv
lea eax, [esp+20h+var_10]
mov [esp+20h+var_20], eax
call _ZN1c4dumpEv
mov eax, 0
leave
retn
main endp
Ici, nous voyons un autre style de name mangling, spécifique à GNU 26 . Il peut aus-
si être noté que le pointeur sur l’objet est passé comme premier argument de la
fonction—invisible au programmeur, bien sûr.
Premier constructeur:
public _ZN1cC1Ev ; weak
_ZN1cC1Ev proc near ; CODE XREF: main+10
push ebp
mov ebp, esp
mov eax, [ebp+arg_0]
mov dword ptr [eax], 667
26. Il y a un bon document à propos des différentes conventions de name mangling dans différent
compilateurs:
[Agner Fog, Calling conventions (2015)].
719
mov eax, [ebp+arg_0]
mov dword ptr [eax+4], 999
pop ebp
retn
_ZN1cC1Ev endp
Il écrit juste les deux nombres en utilisant le pointeur passé comme premier (et seul)
argument.
Second constructeur:
public _ZN1cC1Eii
_ZN1cC1Eii proc near
push ebp
mov ebp, esp
mov eax, [ebp+arg_0]
mov edx, [ebp+arg_4]
mov [eax], edx
mov eax, [ebp+arg_0]
mov edx, [ebp+arg_8]
mov [eax+4], edx
pop ebp
retn
_ZN1cC1Eii endp
Ceci est une fonction, l’analogue d’une qui pourrait ressembler à ceci:
void ZN1cC1Eii (int *obj, int a, int b)
{
*obj=a ;
*(obj+1)=b ;
};
push ebp
mov ebp, esp
sub esp, 18h
mov eax, [ebp+arg_0]
720
mov edx, [eax+4]
mov eax, [ebp+arg_0]
mov eax, [eax]
mov [esp+18h+var_10], edx
mov [esp+18h+var_14], eax
mov [esp+18h+var_18], offset aDD ; "%d; %d\n"
call _printf
leave
retn
_ZN1c4dumpEv endp
Ainsi, si nous basons notre jugement sur ces simples exemples, la différence entre
MSVC et GCC est le style d’encodage des noms de fonctions (name mangling) et la
méthode pour passer un pointeur sur l’objet (via le registre ECX ou via le premier
argument).
GCC: x86-64
Les 6 premiers arguments, comme nous le savons déjà, sont passés par les registres
RDI, RSI, RDX, RCX, R8 et R9 ([Michael Matz, Jan Hubicka, Andreas Jaeger, Mark Mit-
chell, System V Application Binary Interface. AMD64 Architecture Processor Supple-
ment, (2013)] 27 ), et le pointeur sur this via le premier (RDI) et c’est ce que l’on voit
ici. Le type de donnée int est aussi 32-bit ici.
L’astuce du JMP au lieu de RET est aussi utilisée ici.
_ZN1cC2Ev :
mov DWORD PTR [rdi], 667
mov DWORD PTR [rdi+4], 999
ret
; c(int a, int b)
_ZN1cC2Eii :
mov DWORD PTR [rdi], esi
mov DWORD PTR [rdi+4], edx
721
ret
; dump()
_ZN1c4dumpEv :
mov edx, DWORD PTR [rdi+4]
mov esi, DWORD PTR [rdi]
xor eax, eax
mov edi, OFFSET FLAT :.LC0 ; "%d; %d\n"
jmp printf
Héritage de classe
Les classes héritées sont similaires aux simples structures dont nous avons déjà
discuté, mais étendues aux classes héritables.
Prenons ce simple exemple:
#include <stdio.h>
class object
{
public :
int color ;
object() { } ;
object (int color) { this->color=color ; } ;
void print_color() { printf ("color=%d\n", color) ; } ;
};
722
sphere(int color, int radius)
{
this->color=color ;
this->radius=radius ;
};
void dump()
{
printf ("this is sphere. color=%d, radius=%d\n", color, radius) ;
};
};
int main()
{
box b(1, 10, 20, 30) ;
sphere s(2, 40) ;
b.print_color() ;
s.print_color() ;
b.dump() ;
s.dump() ;
return 0;
};
28. L’option /Ob0 signifie la désactivation de l’expension inline, puisque la mise en ligne de fonctions
peut rendre notre expérience plus difficile.
723
mov eax, DWORD PTR [ecx+12]
mov edx, DWORD PTR [ecx+8]
push eax
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push edx
push eax
push ecx
;
'this is a box. color=%d, width=%d, height=%d, depth=%d', 0aH, 00H ; `string'
push OFFSET ??_C@_0DG@NCNGAADL@this ?5is ?5box ?4?5color ?$DN ?$CFd ?0?5⤦
Ç width ?$DN ?$CFd ?0@
call _printf
add esp, 20
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump
724
offset description
+0x0 int color
+0x4 int radius
Regardons le corps de la fonction main() :
Les classes héritées doivent toujours ajouter leurs champs après les champs de la
classe de base, afin que les méthodes de la classe de base puissent travailler avec
ses propres champs.
Lorsque la méthode object::print_color() est appelée, un pointeur sur les deux
objets box et sphere est passé en this, et il peut travailler facilement avec ces
objets puisque le champ color dans ces objets est toujours à l’adresse épinglée (à
l’offset +0x0).
On peut dire que la méthode object::print_color() est agnostique en relation
avec le type d’objet en entrée tant que les champs sont épinglés à la même adresse
et cette condition est toujours vraie.
Et si vous créez une classe héritée de la classe box, le compilateur ajoutera les
nouveaux champs après le champ depth, laissant les champs de la classe box à
l’adresse épinglée.
725
Ainsi, la méthode box::dump() fonctionnera correctement pour accéder aux champs
color, width, height et depth, qui sont toujours positionnés à l’adresse connue.
Le code généré par GCC est presque le même, avec la seule exception du passage
du pointeur this (comme il a déjà été expliqué plus haut, il est passé en premier
argument au lieu d’utiliser le registre ECX).
Encapsulation
L’encapsulation consiste à cacher les données dans des sections private de la classe,
e.g. de n’autoriser leurs accès que depuis les méthodes de la classe.
Toutefois, y a-t-il des repères dans le code à propos du fait que certains champs sont
privé et d’autres—non?
Non, il n’y a pas de tels repères.
Essayons avec ce simple exemple:
#include <stdio.h>
class box
{
private :
int color, width, height, depth ;
public :
box(int color, int width, int height, int depth)
{
this->color=color ;
this->width=width ;
this->height=height ;
this->depth=depth ;
};
void dump()
{
printf ("this is a box. color=%d, width=%d, height=%d, depth=%d⤦
Ç \n", color, width, height, depth) ;
};
};
Compilons-le à nouveau dans MSVC 2008 avec les options /Ox et /Ob0 puis regar-
dons le code de la méthode box::dump() :
?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+12]
mov edx, DWORD PTR [ecx+8]
push eax
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push edx
push eax
push ecx
; 'this is a box. color=%d, width=%d, height=%d, depth=%d', 0aH, 00H
726
push OFFSET ??_C@_0DG@NCNGAADL@this ?5is ?5box ?4?5color ?$DN ?$CFd ?0?5⤦
Ç width ?$DN ?$CFd ?0@
call _printf
add esp, 20
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump
Néanmoins, si nous castons le type box sur un pointeur sur un tableau de int, que
nous modifions le tableau de int-s que nous avons, nous pourrions réussir.
void hack_oop_encapsulation(class box * o)
{
unsigned int *ptr_to_object=reinterpret_cast<unsigned int*>(o) ;
ptr_to_object[1]=123;
};
Le code de cette fonction est très simple—on peut dire que la fonction prend un
pointeur sur un tableau de int-s en entrée et écrit 123 dans le second int :
?hack_oop_encapsulation@@YAXPAVbox@@@Z PROC ; hack_oop_encapsulation
mov eax, DWORD PTR _o$[esp-4]
mov DWORD PTR [eax+4], 123
ret 0
?hack_oop_encapsulation@@YAXPAVbox@@@Z ENDP ; hack_oop_encapsulation
b.dump() ;
727
hack_oop_encapsulation(&b) ;
b.dump() ;
return 0;
};
Lançons-le:
this is a box. color=1, width=10, height=20, depth=30
this is a box. color=1, width=123, height=20, depth=30
Nous voyons que l’encapsulation est juste une protection des champs de la classe
lors de l’étape de compilation.
Le compilateur C++ n’autorise pas la génération de code qui modifie directement
les champs protégés, néanmoins, il est possible de le faire avec l’aide de dirty hacks.
Héritage multiple
L’héritage multiple est la création d’une classe qui hérite des champs et méthodes
de deux classes ou plus.
Écrivons à nouveau un exemple simple:
#include <stdio.h>
class box
{
public :
int width, height, depth ;
box() { } ;
box(int width, int height, int depth)
{
this->width=width ;
this->height=height ;
this->depth=depth ;
};
void dump()
{
printf ("this is a box. width=%d, height=%d, depth=%d\n", width⤦
Ç , height, depth) ;
};
int get_volume()
{
return width * height * depth ;
};
};
class solid_object
{
public :
728
int density ;
solid_object() { } ;
solid_object(int density)
{
this->density=density ;
};
int get_density()
{
return density ;
};
void dump()
{
printf ("this is a solid_object. density=%d\n", density) ;
};
};
int main()
{
box b(10, 20, 30) ;
solid_object so(100) ;
solid_box sb(10, 20, 30, 3) ;
b.dump() ;
so.dump() ;
sb.dump() ;
printf ("%d\n", sb.get_weight()) ;
return 0;
};
Compilons-le avec MSVC 2008 avec les options /Ox et /Ob0 et regardons le code de
box::dump(),
solid_object::dump() et solid_box::dump() :
Listing 3.94: MSVC 2008 avec optimisation /Ob0
729
?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+8]
mov edx, DWORD PTR [ecx+4]
push eax
mov eax, DWORD PTR [ecx]
push edx
push eax
; 'this is a box. width=%d, height=%d, depth=%d', 0aH, 00H
push OFFSET ??_C@_0CM@DIKPHDFI@this ?5is ?5box ?4?5width ?$DN ?$CFd ?0?5⤦
Ç height ?$DN ?$CFd@
call _printf
add esp, 16
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump
730
offset description
+0x0 width
+0x4 height
+0x8 depth
Classe solid_object :
offset description
+0x0 density
On peut dire que la disposition de la mémoire de la classe solid_box est unifiée :
Classe solid_box :
offset description
+0x0 width
+0x4 height
+0x8 depth
+0xC density
Le code des méthodes box::get_volume() et solid_object::get_density() est
trivial:
731
imul eax, edi
pop edi
pop esi
ret 0
?get_weight@solid_box@@QAEHXZ ENDP ; solid_box::get_weight
get_weight() appelle juste deux méthodes, mais pour get_volume() il passe sim-
plement un pointeur sur this, et pour get_density() il passe un pointeur sur this
incrémenté de 12 (ou 0xC) octets, et ici, dans la disposition de la mémoire de la
classe solid_box, les champs de la classe solid_object commencent.
Ainsi, la méthode solid_object::get_density() croira qu’elle traite une classe
solid_object usuelle, et la méthode box::get_volume() fonctionnera avec ses
trois champs, croyant que c’est juste un objet usuel de la classe box.
Ainsi, on peut dire, un objet d’une classe, qui hérite de plusieurs autres classes, est
représenté en mémoire comme une classe unifiée, qui contient tous les champs
hérités. Et chaque méthode héritée est appelée avec un pointeur sur la partie cor-
respondante de la structure.
Méthodes virtuelles
class object
{
public :
int color ;
object() { } ;
object (int color) { this->color=color ; } ;
virtual void dump()
{
printf ("color=%d\n", color) ;
};
};
732
printf ("this is a box. color=%d, width=%d, height=%d, depth=%d⤦
Ç \n", color, width, height, depth) ;
};
};
int main()
{
box b(1, 10, 20, 30) ;
sphere s(2, 40) ;
object *o1=&b ;
object *o2=&s ;
o1->dump() ;
o2->dump() ;
return 0;
};
La classe object a une méthode virtuelle dump() qui est remplacée par celle de la
classe héritant box et sphere.
Si nous sommes dans un environnement où le type de l’objet n’est pas connu, comme
dans la fonction main() de l’exemple, où la méthode virtuelle dump() est appelée,
l’information à propos de son type doit être stockée quelque part, afin d’être capable
d’appeler la bonne méthode virtuelle.
Compilons-le dans MSVC 2008 avec les options /Ox et /Ob0, puis regardons le code
de main() :
_s$ = -32 ; size = 12
_b$ = -20 ; size = 20
_main PROC
sub esp, 32
push 30
push 20
push 10
push 1
733
lea ecx, DWORD PTR _b$[esp+48]
call ??0box@@QAE@HHHH@Z ; box::box
push 40
push 2
lea ecx, DWORD PTR _s$[esp+40]
call ??0sphere@@QAE@HH@Z ; sphere::sphere
mov eax, DWORD PTR _b$[esp+32]
mov edx, DWORD PTR [eax]
lea ecx, DWORD PTR _b$[esp+32]
call edx
mov eax, DWORD PTR _s$[esp+32]
mov edx, DWORD PTR [eax]
lea ecx, DWORD PTR _s$[esp+32]
call edx
xor eax, eax
add esp, 32
ret 0
_main ENDP
Un pointeur sur la fonction dump() est pris quelque part dans l’objet. Où pourrions-
nous stocker l’adresse de la nouvelle méthode? Seulement quelque part dans le
constructeur: il n’y a pas d’autre endroit puisque rien d’autre n’est appelé dans la
fonction main(). 29
Regardons le code du constructeur de la classe box :
??_R0 ?AVbox@@@8 DD FLAT :??_7type_info@@6B@ ; box `RTTI Type Descriptor'
DD 00H
DB '.?AVbox@@', 00H
29. Vous pouvez en lire plus sur les pointeurs sur les fonctions dans la section afférente:( 1.33 on
page 494).
734
DD FLAT :??_R3box@@8
_color$ = 8 ; size = 4
_width$ = 12 ; size = 4
_height$ = 16 ; size = 4
_depth$ = 20 ; size = 4
??0box@@QAE@HHHH@Z PROC ; box::box, COMDAT
; _this$ = ecx
push esi
mov esi, ecx
call ??0object@@QAE@XZ ; object::object
mov eax, DWORD PTR _color$[esp]
mov ecx, DWORD PTR _width$[esp]
mov edx, DWORD PTR _height$[esp]
mov DWORD PTR [esi+4], eax
mov eax, DWORD PTR _depth$[esp]
mov DWORD PTR [esi+16], eax
mov DWORD PTR [esi], OFFSET ??_7box@@6B@
mov DWORD PTR [esi+8], ecx
mov DWORD PTR [esi+12], edx
mov eax, esi
pop esi
ret 16
??0box@@QAE@HHHH@Z ENDP ; box::box
735
3.21.2 ostream
Recommençons avec l’exemple «hello world », mais cette fois nous allons utiliser
ostream :
#include <iostream>
int main()
{
std ::cout << "Hello, world !\n" ;
}
Presque tous les livres sur C++ nous disent que l’opérateur << peut-être défini (sur-
chargé) pour tous les types. C’est ce qui est fait dans ostream. Nous voyons que
operator<< est appelé pour ostream :
Listing 3.100: MSVC 2012 (listing réduit)
$SG37112 DB 'Hello, world !', 0aH, 00H
_main PROC
push OFFSET $SG37112
push OFFSET ?cout@std@@3V ?$basic_ostream@DU ?$char_traits@D@std@@@1@A ;
std::cout
call ??$ ?6U ?$char_traits@D@std@@@std@@YAAAV ?$basic_ostream@DU ?⤦
Ç $char_traits@D@std@@@0@AAV10@PBD@Z ;
std::operator<<<std::char_traits<char> >
add esp, 8
xor eax, eax
ret 0
_main ENDP
Modifions l’exemple:
#include <iostream>
int main()
{
std ::cout << "Hello, " << "world !\n" ;
}
À nouveau, dans chaque livre sur C++ nous lisons que le résultat de chaque operator<<
dans ostream est transmis au suivant. En effet:
Listing 3.101: MSVC 2012
$SG37112 DB 'world !', 0aH, 00H
$SG37113 DB 'Hello, ', 00H
_main PROC
push OFFSET $SG37113 ; 'Hello, '
push OFFSET ?cout@std@@3V ?$basic_ostream@DU ?$char_traits@D@std@@@1@A ;
std::cout
call ??$ ?6U ?$char_traits@D@std@@@std@@YAAAV ?$basic_ostream@DU ?⤦
Ç $char_traits@D@std@@@0@AAV10@PBD@Z ;
std::operator<<<std::char_traits<char> >
736
add esp, 8
3.21.3 Références
En C++, les références sont aussi des pointeurs ( 3.23 on page 789), mais elles sont
dites sûre, car il est plus difficile de faire une erreur en les utilisant (C++11 8.3.2).
Par exemple, les références doivent toujours pointer sur un objet de type correspon-
dant et ne peuvent pas être NULL [Marshall Cline, C++ FAQ8.6].
Encore mieux que ça, les références ne peuvent être changées, il est impossible de
les faire pointer sur un autre objet (réassigner) [Marshall Cline, C++ FAQ8.5].
Si nous essayons de modifier l’exemple avec des pointeurs ( 3.23 on page 789) pour
utiliser des références à la place …
void f2 (int x, int y, int & sum, int & product)
{
sum=x+y ;
product=x*y ;
};
…alors nous pouvons voir que le code compilé est simplement le même que dans
l’exemple avec les pointeurs ( 3.23 on page 789) :
737
push esi
mov esi, DWORD PTR _sum$[esp]
mov DWORD PTR [esi], edx
mov DWORD PTR [ecx], eax
pop esi
ret 0
?f2@@YAXHHAAH0@Z ENDP ; f2
(La raison pour laquelle les fonctions C++ ont des noms aussi étranges est expliquée
ici: 3.21.1 on page 715.)
De ce fait, les références C++ sont bien plus efficientes que les pointeurs usuels.
3.21.4 STL
N.B.: tous les exemples ici ont été testés uniquement en environnement 32-bit. x64
non testé.
std::string
Internals
MSVC
738
Listing 3.103: exemple pour MSVC
#include <string>
#include <stdio.h>
struct std_string
{
union
{
char buf[16];
char* ptr ;
} u;
size_t size ; // AKA 'Mysize' dans MSVC
size_t capacity ; // AKA 'Myres' dans MSVC
};
int main()
{
std ::string s1="a short string" ;
std ::string s2="a string longer than 16 bytes" ;
dump_std_string(s1) ;
dump_std_string(s2) ;
739
par un
0, donc ça fonctionne.
L’affichage de la seconde chaîne (de plus de 16 caractères) est encore plus dan-
gereux: c’est une erreur typique de programmeur (ou une typo) d’oublier d’écrire
c_str().
Ceci fonctionne car pour le moment un pointeur sur le buffer est situé au début de
la structure.
Ceci peut passer inaperçu pendant un long moment, jusqu’à ce qu’une chaîne plus
longue apparaisse à un moment, alors le processus plantera.
GCC
struct std_string
{
size_t length ;
size_t capacity ;
size_t refcount ;
};
int main()
740
{
std ::string s1="a short string" ;
std ::string s2="a string longer than 16 bytes" ;
dump_std_string(s1) ;
dump_std_string(s2) ;
Il faut utiliser une astuce pour imiter l’erreur que nous avons vue avant car GCC a
une vérification de type plus forte, cependant, printf() fonctionne ici également sans
c_str().
#include <string>
#include <stdio.h>
int main()
{
std ::string s1="Hello, " ;
std ::string s2="world !\n" ;
std ::string s3=s1+s2 ;
push 7
push OFFSET $SG39512
lea ecx, DWORD PTR _s1$[esp+80]
mov DWORD PTR _s1$[esp+100], 15
mov DWORD PTR _s1$[esp+96], 0
mov BYTE PTR _s1$[esp+80], 0
call ?assign@ ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@QAEAAV12@PBDI@Z ;
std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
741
push 7
push OFFSET $SG39514
lea ecx, DWORD PTR _s2$[esp+80]
mov DWORD PTR _s2$[esp+100], 15
mov DWORD PTR _s2$[esp+96], 0
mov BYTE PTR _s2$[esp+80], 0
call ?assign@ ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@QAEAAV12@PBDI@Z ;
std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
push eax
push OFFSET $SG39581
call _printf
add esp, 20
742
xor eax, eax
add esp, 72
ret 0
_main ENDP
call _ZNSsC1EPKcRKSaIcE
743
call _ZNSsC1EPKcRKSaIcE
call _ZNSsC1ERKSs
call _ZNSs6appendERKSs
call puts
On voit que ce n’est pas un pointeur sur l’objet qui est passé aux destructeurs, mais
plutôt une adresse 12 octets (ou 3 mots) avant, i.e., un pointeur sur le début réel de
la structure.
Les programmeurs C++ expérimentés savent que les des variables globales des
744
types STL31 peuvent être définis sans problème.
Oui, en effet:
#include <stdio.h>
#include <string>
int main()
{
printf ("%s\n", s.c_str()) ;
};
Listing 3.107: MSVC 2012: voici comment une variable globale est construite et aussi
comment sont destructeur est déclaré
??__Es@@YAXXZ PROC
push 8
push OFFSET $SG39512 ; 'a string'
mov ecx, OFFSET ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A ;
s
call ?assign@ ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@QAEAAV12@PBDI@Z ;
std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
push OFFSET ??__Fs@@YAXXZ ; `dynamic atexit destructor for 's''
call _atexit
pop ecx
ret 0
??__Es@@YAXXZ ENDP
Listing 3.108: MSVC 2012: ici une variable globale est utilisée dans main()
$SG39512 DB 'a string', 00H
$SG39519 DB '%s', 0aH, 00H
_main PROC
cmp DWORD PTR ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A+20, 16
mov eax, OFFSET ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A ;
s
cmovae eax, DWORD PTR ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A
push eax
push OFFSET $SG39519 ; '%s'
call _printf
add esp, 8
xor eax, eax
745
ret 0
_main ENDP
Listing 3.109: MSVC 2012: cette fonction destructeur est appelée avant exit
??__Fs@@YAXXZ PROC
push ecx
cmp DWORD PTR ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A+20, 16
jb SHORT $LN23@dynamic
push esi
mov esi, DWORD PTR ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A
lea ecx, DWORD PTR $T2[esp+8]
call ??0?$_Wrap_alloc@V ?$allocator@D@std@@@std@@QAE@XZ
push OFFSET ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A ;
s
lea ecx, DWORD PTR $T2[esp+12]
call ??$destroy@PAD@ ?$_Wrap_alloc@V ?⤦
Ç $allocator@D@std@@@std@@QAEXPAPAD@Z
lea ecx, DWORD PTR $T1[esp+8]
call ??0?$_Wrap_alloc@V ?$allocator@D@std@@@std@@QAE@XZ
push esi
call ??3@YAXPAX@Z ; operator delete
add esp, 4
pop esi
$LN23@dynamic :
mov DWORD PTR ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A+20, 15
mov DWORD PTR ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A+16, 0
mov BYTE PTR ?s@@3V ?$basic_string@DU ?$char_traits@D@std@@V ?⤦
Ç $allocator@D@2@@std@@A, 0
pop ecx
ret 0
??__Fs@@YAXXZ ENDP
En fait, une fonction spéciale avec tous les constructeurs des variables globales est
appelée depuis CRT, avant main().
Mieux que ça: avec l’aide de atexit() une autre fonction est enregistrée, qui contient
des appels aux destructeurs de telles variables globales.
GCC fonctionne comme ceci:
746
call puts
xor eax, eax
leave
ret
.LC0 :
.string "a string"
_GLOBAL__sub_I_s :
sub esp, 44
lea eax, [esp+31]
mov DWORD PTR [esp+8], eax
mov DWORD PTR [esp+4], OFFSET FLAT :.LC0
mov DWORD PTR [esp], OFFSET FLAT :s
call _ZNSsC1EPKcRKSaIcE
mov DWORD PTR [esp+8], OFFSET FLAT :__dso_handle
mov DWORD PTR [esp+4], OFFSET FLAT :s
mov DWORD PTR [esp], OFFSET FLAT :_ZNSsD1Ev
call __cxa_atexit
add esp, 44
ret
.LFE645 :
.size _GLOBAL__sub_I_s, .-_GLOBAL__sub_I_s
.section .init_array,"aw"
.align 4
.long _GLOBAL__sub_I_s
.globl s
.bss
.align 4
.type s, @object
.size s, 4
s :
.zero 4
.hidden __dso_handle
Mais il ne crée pas une fonction séparée pour cela, chaque destructeur est passé à
atexit(), un par un.
std::list
Ceci est la célèbre liste doublement chaînée: chaque élément a deux pointeurs, un
sur l’élément précédent et un sur le suivant.
Ceci implique que l’empreinte mémoire est augmentés de 2 mots pour chaque élé-
ment (8 octets dans un environnement 32-bit ou 16v octets en 64-bit).
C++ STL ajoute juste les pointeurs «next » et «previous » à la structure existante
du type que vous voulez unir dans une liste.
Travaillons sur un exemple avec une simple structure de deux variables que nous
voulons stocker dans une liste.
Bien que le standard C++ ne dise pas comme l’implémenter, GCC et MSVC l’implé-
mentent de manière directe et similaire, donc il n’y a qu’un seul code source pour
les deux:
747
#include <stdio.h>
#include <list>
#include <iostream>
struct a
{
int x ;
int y ;
};
struct List_node
{
struct List_node* _Next ;
struct List_node* _Prev ;
int x ;
int y ;
};
for (;;)
{
dump_List_node (current) ;
current=current->_Next ;
if (current==n) // end
break ;
};
};
int main()
{
std ::list<struct a> l ;
748
struct a t1 ;
t1.x=1;
t1.y=2;
l.push_front (t1) ;
t1.x=3;
t1.y=4;
l.push_front (t1) ;
t1.x=5;
t1.y=6;
l.push_back (t1) ;
GCC
749
morceaux.
* empty list :
ptr=0x0028fe90 _Next=0x0028fe90 _Prev=0x0028fe90 x=3 y=0
Next
Prev
X=déchets
Y=déchets
Le dernier élément est encore en 0x0028fe90, il ne sera pas déplacé avant l’élimi-
nation de la liste.
Il contient toujours des valeurs aléatoires dans x et y (5 et 6). Par coïncidence, ces
valeurs sont les même que dans le dernier élément, mais ça ne signifie pas que ça
soit significatif.
Voici comment ces 3 éléments sont stockés en mémoire:
750
Variable
std::list
list.begin() list.end()
Le fait que le dernier élément ait un pointeur sur le premier et le premier élément
ait un pointeur sur le dernier nous rappelle les listes circulaires.
Ceci est très pratique ici: en ayant un pointeur sur le premier élément de la liste, i.e.,
ce qui est dans la variable l, il est très facile d’obtenir rapidement un pointeur sur le
dernier, sans devoir traverser toute la liste.
Insérer un élément à la fin de la liste est également rapide, grâce à cette caractéris-
tique.
operator-- et operator++ mettent la valeur courante de l’itérateur à la valeur
current_node->prev ou current_node->next.
Les itérateurs inverses (.rbegin, .rend) fonctionne de la même façon, mais à l’envers.
operator* renvoie un pointeur sur le point dans la structure du nœud, où la structure
débute, i.e., un pointeur sur le premier élément de la structure (x).
751
L’insertion et la suppression dans la liste sont triviaux: simplement allouer un nou-
veau nœud (ou le désallouer) et mettre à jour les pointeurs afin qu’ils soient valides.
C’est pourquoi un itérateur peut devenir invalide après la suppression d’un élément:
il peut toujours pointer sur le nœud qui a été déjà désalloué. Ceci est aussi appelé
un dangling pointer.
Et bien sûr, l’information sur le nœud libéré (sur lequel pointe toujours l’itérateur)
ne peut plus être utilisée.
L’implémentation de GCC (à partir de 4.8.1) ne stocke plus la taille courante de la
liste: ceci implique une méthode .size() lente: il doit traverser toute la liste pour
compter les éléments, car il n’a pas d’autre moyen d’obtenir l’information.
Ceci signifie que cette opération est en O(n), i.e., elle devient constamment plus
lente lorsque la liste grandit.
Listing 3.111: GCC 4.8.1 -fno-inline-small-functions avec optimisation
main proc near
push ebp
mov ebp, esp
push esi
push ebx
and esp, 0FFFFFFF0h
sub esp, 20h
lea ebx, [esp+10h]
mov dword ptr [esp], offset s ; "* empty list:"
mov [esp+10h], ebx
mov [esp+14h], ebx
call puts
mov [esp], ebx
call _Z13dump_List_valPj ; dump_List_val(uint *)
lea esi, [esp+18h]
mov [esp+4], esi
mov [esp], ebx
mov dword ptr [esp+18h], 1 ; X pour le nouvel élément
mov dword ptr [esp+1Ch], 2 ; Y pour le nouvel élément
call _ZNSt4listI1aSaIS0_EE10push_frontERKS0_ ;
std::list<a,std::allocator<a>>::push_front(a const&)
mov [esp+4], esi
mov [esp], ebx
mov dword ptr [esp+18h], 3 ; X pour le nouvel élément
mov dword ptr [esp+1Ch], 4 ; Y pour le nouvel élément
call _ZNSt4listI1aSaIS0_EE10push_frontERKS0_ ;
std::list<a,std::allocator<a>>::push_front(a const&)
mov dword ptr [esp], 10h
mov dword ptr [esp+18h], 5 ; X pour le nouvel élément
mov dword ptr [esp+1Ch], 6 ; Y pour le nouvel élément
call _Znwj ; opérateur new(uint)
cmp eax, 0FFFFFFF8h
jz short loc_80002A6
mov ecx, [esp+1Ch]
mov edx, [esp+18h]
mov [eax+0Ch], ecx
mov [eax+8], edx
752
loc_80002A6 : ; CODE XREF: main+86
mov [esp+4], ebx
mov [esp], eax
call _ZNSt8__detail15_List_node_base7_M_hookEPS0_ ;
std::__detail::_List_node_base::_M_hook(std::__detail::_List_node_base*)
mov dword ptr [esp], offset a3ElementsList ; "* 3-elements list:"
call puts
mov [esp], ebx
call _Z13dump_List_valPj ; dump_List_val(uint *)
mov dword ptr [esp], offset aNodeAt_begin ; "node at .begin:"
call puts
mov eax, [esp+10h]
mov [esp], eax
call _Z14dump_List_nodeP9List_node ; dump_List_node(List_node *)
mov dword ptr [esp], offset aNodeAt_end ; "node at .end:"
call puts
mov [esp], ebx
call _Z14dump_List_nodeP9List_node ; dump_List_node(List_node *)
mov dword ptr [esp], offset aLetSCountFromT ; "* let's count from the
beginning:"
call puts
mov esi, [esp+10h]
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a1stElementDD ; "1st element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov esi, [esi] ; operator++: get ->next pointer
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a2ndElementDD ; "2nd element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov esi, [esi] ; operator++: get ->next pointer
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a3rdElementDD ; "3rd element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov eax, [esi] ; operator++: get ->next pointer
mov edx, [eax+0Ch]
mov [esp+0Ch], edx
mov eax, [eax+8]
mov dword ptr [esp+4], offset aElementAt_endD ;
"element at .end() : %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
753
call __printf_chk
mov dword ptr [esp], offset aLetSCountFro_0 ; "* let's count from the
end:"
call puts
mov eax, [esp+1Ch]
mov dword ptr [esp+4], offset aElementAt_endD ;
"element at .end() : %d %d\n"
mov dword ptr [esp], 1
mov [esp+0Ch], eax
mov eax, [esp+18h]
mov [esp+8], eax
call __printf_chk
mov esi, [esp+14h]
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a3rdElementDD ; "3rd element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov esi, [esi+4] ; operator--: get ->prev pointer
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a2ndElementDD ; "2nd element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov eax, [esi+4] ; operator--: get ->prev pointer
mov edx, [eax+0Ch]
mov [esp+0Ch], edx
mov eax, [eax+8]
mov dword ptr [esp+4], offset a1stElementDD ; "1st element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov dword ptr [esp], offset aRemovingLastEl ; "removing last
element..."
call puts
mov esi, [esp+14h]
mov [esp], esi
call _ZNSt8__detail15_List_node_base9_M_unhookEv ;
std::__detail::_List_node_base::_M_unhook(void)
mov [esp], esi ; void *
call _ZdlPv ; operator delete(void *)
mov [esp], ebx
call _Z13dump_List_valPj ; dump_List_val(uint *)
mov [esp], ebx
call _ZNSt10_List_baseI1aSaIS0_EE8_M_clearEv ;
std::_List_base<a,std::allocator<a>>::_M_clear(void)
lea esp, [ebp-8]
xor eax, eax
pop ebx
pop esi
pop ebp
754
retn
main endp
MSVC
L’implémentation de MSVC (2012) est la même, mais elle stocke aussi la taille cou-
rante de la liste.
Ceci implique que la méthode .size() est très rapide (O(1)) : elle doit juste lire une
valeur depuis la mémoire.
D’un autre côté, la variable size doit être mise à jour à chaque insertion/suppression.
L’implémentation de MSVC est aussi légèrement différente dans la façon dont elle
arrange les nœuds:
755
Variable
std::list
list.end() list.begin()
GCC a son élément fictif à la fin de la liste, tandis que MSVC l’a au début.
756
mov DWORD PTR _t1$[esp+40], 1 ; data for a new node
mov DWORD PTR _t1$[esp+44], 2 ; data for a new node
; allocate new node
call ??$_Buynode@ABUa@@@ ?$_List_buy@Ua@@V ?⤦
Ç $allocator@Ua@@@std@@@std@@QAEPAU ?⤦
Ç $_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ;
std::_List_buy<a,std::allocator<a> >::_Buynode<a const &>
mov DWORD PTR [esi+4], eax
mov ecx, DWORD PTR [eax+4]
mov DWORD PTR _t1$[esp+28], 3 ; data for a new node
mov DWORD PTR [ecx], eax
mov esi, DWORD PTR [ebx]
lea eax, DWORD PTR _t1$[esp+28]
push eax
push DWORD PTR [esi+4]
lea ecx, DWORD PTR _l$[esp+36]
push esi
mov DWORD PTR _t1$[esp+44], 4 ; data for a new node
; allocate new node
call ??$_Buynode@ABUa@@@ ?$_List_buy@Ua@@V ?⤦
Ç $allocator@Ua@@@std@@@std@@QAEPAU ?⤦
Ç $_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ;
std::_List_buy<a,std::allocator<a> >::_Buynode<a const &>
mov DWORD PTR [esi+4], eax
mov ecx, DWORD PTR [eax+4]
mov DWORD PTR _t1$[esp+28], 5 ; data for a new node
mov DWORD PTR [ecx], eax
lea eax, DWORD PTR _t1$[esp+28]
push eax
push DWORD PTR [ebx+4]
lea ecx, DWORD PTR _l$[esp+36]
push ebx
mov DWORD PTR _t1$[esp+44], 6 ; data for a new node
; allocate new node
call ??$_Buynode@ABUa@@@ ?$_List_buy@Ua@@V ?⤦
Ç $allocator@Ua@@@std@@@std@@QAEPAU ?⤦
Ç $_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ;
std::_List_buy<a,std::allocator<a> >::_Buynode<a const &>
mov DWORD PTR [ebx+4], eax
mov ecx, DWORD PTR [eax+4]
push OFFSET $SG40689 ; '* 3-elements list:'
mov DWORD PTR _l$[esp+36], 3
mov DWORD PTR [ecx], eax
call edi ; printf
lea eax, DWORD PTR _l$[esp+32]
push eax
call ?dump_List_val@@YAXPAI@Z ; dump_List_val
push OFFSET $SG40831 ; 'node at .begin:'
call edi ; printf
push DWORD PTR [ebx] ; get next field of node "l" variable points to
call ?dump_List_node@@YAXPAUList_node@@@Z ; dump_List_node
push OFFSET $SG40835 ; 'node at .end:'
call edi ; printf
push ebx ; pointer to the node "l" variable points to!
757
call ?dump_List_node@@YAXPAUList_node@@@Z ; dump_List_node
push OFFSET $SG40839 ; '* let''s count from the begin:'
call edi ; printf
mov esi, DWORD PTR [ebx] ; operator++: get ->next pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40846 ; '1st element: %d %d'
call edi ; printf
mov esi, DWORD PTR [esi] ; operator++: get ->next pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40848 ; '2nd element: %d %d'
call edi ; printf
mov esi, DWORD PTR [esi] ; operator++: get ->next pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40850 ; '3rd element: %d %d'
call edi ; printf
mov eax, DWORD PTR [esi] ; operator++: get ->next pointer
add esp, 64
push DWORD PTR [eax+12]
push DWORD PTR [eax+8]
push OFFSET $SG40852 ; 'element at .end() : %d %d'
call edi ; printf
push OFFSET $SG40853 ; '* let''s count from the end:'
call edi ; printf
push DWORD PTR [ebx+12] ; use x and y fields from the node "l" variable
points to
push DWORD PTR [ebx+8]
push OFFSET $SG40860 ; 'element at .end() : %d %d'
call edi ; printf
mov esi, DWORD PTR [ebx+4] ; operator--: get ->prev pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40862 ; '3rd element: %d %d'
call edi ; printf
mov esi, DWORD PTR [esi+4] ; operator--: get ->prev pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40864 ; '2nd element: %d %d'
call edi ; printf
mov eax, DWORD PTR [esi+4] ; operator--: get ->prev pointer
push DWORD PTR [eax+12]
push DWORD PTR [eax+8]
push OFFSET $SG40866 ; '1st element: %d %d'
call edi ; printf
add esp, 64
push OFFSET $SG40867 ; 'removing last element...'
call edi ; printf
mov edx, DWORD PTR [ebx+4]
add esp, 4
; prev=next?
; it is the only element, garbage one ?
758
; if yes, do not delete it!
cmp edx, ebx
je SHORT $LN349@main
mov ecx, DWORD PTR [edx+4]
mov eax, DWORD PTR [edx]
mov DWORD PTR [ecx], eax
mov ecx, DWORD PTR [edx]
mov eax, DWORD PTR [edx+4]
push edx
mov DWORD PTR [ecx+4], eax
call ??3@YAXPAX@Z ; operator delete
add esp, 4
mov DWORD PTR _l$[esp+32], 2
$LN349@main :
lea eax, DWORD PTR _l$[esp+28]
push eax
call ?dump_List_val@@YAXPAI@Z ; dump_List_val
mov eax, DWORD PTR [ebx]
add esp, 4
mov DWORD PTR [ebx], ebx
mov DWORD PTR [ebx+4], ebx
cmp eax, ebx
je SHORT $LN412@main
$LL414@main :
mov esi, DWORD PTR [eax]
push eax
call ??3@YAXPAX@Z ; operator delete
add esp, 4
mov eax, esi
cmp esi, ebx
jne SHORT $LL414@main
$LN412@main :
push ebx
call ??3@YAXPAX@Z ; operator delete
add esp, 4
xor eax, eax
pop edi
pop esi
pop ebx
add esp, 16
ret 0
_main ENDP
759
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC2A0 x=6226002 y=4522072
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
ptr=0x003CC270 _Next=0x003CC2A0 _Prev=0x003CC288 x=1 y=2
ptr=0x003CC2A0 _Next=0x003CC258 _Prev=0x003CC270 x=5 y=6
node at .begin :
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
node at .end :
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC2A0 x=6226002 y=4522072
* let's count from the beginning :
1st element : 3 4
2nd element : 1 2
3rd element : 5 6
element at .end() : 6226002 4522072
* let's count from the end :
element at .end() : 6226002 4522072
3rd element : 5 6
2nd element : 1 2
1st element : 3 4
removing last element...
_Myhead=0x003CC258, _Mysize=2
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC270 x=6226002 y=4522072
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
ptr=0x003CC270 _Next=0x003CC258 _Prev=0x003CC288 x=1 y=2
C++11 std::forward_list
La même chose que std::list, mais simplement chaîné, i.e., ayant seulement le champ
«next » dans chaque nœud.
Il a une empreinte mémoire plus faible, mais dons n’offre pas la possibilité de tra-
verser la liste en arrière.
std::vector
760
pour les deux compilateurs. Voici encore le code pseudo-C pour afficher la structure
de std::vector :
#include <stdio.h>
#include <vector>
#include <algorithm>
#include <functional>
struct vector_of_ints
{
// noms MSVC:
int *Myfirst ;
int *Mylast ;
int *Myend ;
int main()
{
std ::vector<int> c ;
dump ((struct vector_of_ints*)(void*)&c) ;
c.push_back(1) ;
dump ((struct vector_of_ints*)(void*)&c) ;
c.push_back(2) ;
dump ((struct vector_of_ints*)(void*)&c) ;
c.push_back(3) ;
dump ((struct vector_of_ints*)(void*)&c) ;
c.push_back(4) ;
dump ((struct vector_of_ints*)(void*)&c) ;
c.reserve (6) ;
dump ((struct vector_of_ints*)(void*)&c) ;
c.push_back(5) ;
dump ((struct vector_of_ints*)(void*)&c) ;
c.push_back(6) ;
dump ((struct vector_of_ints*)(void*)&c) ;
printf ("%d\n", c.at(5)) ; // avec vérifications de limites
printf ("%d\n", c[8]) ; // operator[], sans vérifications de limites
};
761
_Myfirst=00000000, _Mylast=00000000, _Myend=00000000
size=0, capacity=0
_Myfirst=0051CF48, _Mylast=0051CF4C, _Myend=0051CF4C
size=1, capacity=1
element 0: 1
_Myfirst=0051CF58, _Mylast=0051CF60, _Myend=0051CF60
size=2, capacity=2
element 0: 1
element 1: 2
_Myfirst=0051C278, _Mylast=0051C284, _Myend=0051C284
size=3, capacity=3
element 0: 1
element 1: 2
element 2: 3
_Myfirst=0051C290, _Mylast=0051C2A0, _Myend=0051C2A0
size=4, capacity=4
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0051B180, _Mylast=0051B190, _Myend=0051B198
size=4, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0051B180, _Mylast=0051B194, _Myend=0051B198
size=5, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
_Myfirst=0051B180, _Mylast=0051B198, _Myend=0051B198
size=6, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
element 5: 6
6
6619158
On voit qu’il n’y a pas de buffer alloué lorsque main() débute. Après le premier appel
à push_back(), un buffer est alloué. Et puis, après chaque appel à push_back(), la
taille du tableau et la taille du buffer (capacity) sont augmentées. Mais l’adresse
du buffer change aussi, car push_back() ré-alloue le buffer dans le tas à chaque
fois. C’est une opération coûteuse, c’est pourquoi il est très important de prévoir
la taille du tableau dans le futur et de lui réserver assez d’espace avec la méthode
.reserve().
762
Le dernier nombre est du déchet: il n’y a pas d’élément du tableau à cet endroit, donc
un nombre aléatoire est affiché. Ceci illustre le fait que operator[] de std::vector
ne vérifie pas si l’index est dans le limites du tableau. La méthode plus lente .at(),
toutefois, fait cette vérification et envoie une exception std::out_of_range en cas
d’erreur.
Regardons le code:
_this$ = -4 ; size = 4
__Pos$ = 8 ; size = 4
?at@ ?$vector@HV ?$allocator@H@std@@@std@@QAEAAHI@Z PROC ;
std::vector<int,std::allocator<int> >::at, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR _this$[ebp]
mov edx, DWORD PTR [eax+4]
sub edx, DWORD PTR [ecx]
sar edx, 2
cmp edx, DWORD PTR __Pos$[ebp]
ja SHORT $LN1@at
push OFFSET ??_C@_0BM@NMJKDPPO@invalid ?5vector ?$DMT ?$DO ?5subscript ?⤦
Ç $AA@
call DWORD PTR __imp_ ?_Xout_of_range@std@@YAXPBD@Z
$LN1@at :
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR [eax]
mov edx, DWORD PTR __Pos$[ebp]
lea eax, DWORD PTR [ecx+edx*4]
$LN3@at :
mov esp, ebp
pop ebp
ret 4
?at@ ?$vector@HV ?$allocator@H@std@@@std@@QAEAAHI@Z ENDP ;
std::vector<int,std::allocator<int> >::at
763
mov DWORD PTR _c$[ebp], 0 ; Myfirst
mov DWORD PTR _c$[ebp+4], 0 ; Mylast
mov DWORD PTR _c$[ebp+8], 0 ; Myend
lea eax, DWORD PTR _c$[ebp]
push eax
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T6[ebp], 1
lea ecx, DWORD PTR $T6[ebp]
push ecx
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@ ?$vector@HV ?$allocator@H@std@@@std@@QAEX$$QAH@Z ;
std::vector<int,std::allocator<int> >::push_back
lea edx, DWORD PTR _c$[ebp]
push edx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T5[ebp], 2
lea eax, DWORD PTR $T5[ebp]
push eax
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@ ?$vector@HV ?$allocator@H@std@@@std@@QAEX$$QAH@Z ;
std::vector<int,std::allocator<int> >::push_back
lea ecx, DWORD PTR _c$[ebp]
push ecx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T4[ebp], 3
lea edx, DWORD PTR $T4[ebp]
push edx
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@ ?$vector@HV ?$allocator@H@std@@@std@@QAEX$$QAH@Z ;
std::vector<int,std::allocator<int> >::push_back
lea eax, DWORD PTR _c$[ebp]
push eax
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T3[ebp], 4
lea ecx, DWORD PTR $T3[ebp]
push ecx
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@ ?$vector@HV ?$allocator@H@std@@@std@@QAEX$$QAH@Z ;
std::vector<int,std::allocator<int> >::push_back
lea edx, DWORD PTR _c$[ebp]
push edx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
push 6
lea ecx, DWORD PTR _c$[ebp]
call ?reserve@ ?$vector@HV ?$allocator@H@std@@@std@@QAEXI@Z ;
std::vector<int,std::allocator<int> >::reserve
lea eax, DWORD PTR _c$[ebp]
push eax
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
764
add esp, 4
mov DWORD PTR $T2[ebp], 5
lea ecx, DWORD PTR $T2[ebp]
push ecx
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@ ?$vector@HV ?$allocator@H@std@@@std@@QAEX$$QAH@Z ;
std::vector<int,std::allocator<int> >::push_back
lea edx, DWORD PTR _c$[ebp]
push edx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T1[ebp], 6
lea eax, DWORD PTR $T1[ebp]
push eax
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@ ?$vector@HV ?$allocator@H@std@@@std@@QAEX$$QAH@Z ;
std::vector<int,std::allocator<int> >::push_back
lea ecx, DWORD PTR _c$[ebp]
push ecx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
push 5
lea ecx, DWORD PTR _c$[ebp]
call ?at@ ?$vector@HV ?$allocator@H@std@@@std@@QAEAAHI@Z ;
std::vector<int,std::allocator<int> >::at
mov edx, DWORD PTR [eax]
push edx
push OFFSET $SG52650 ; '%d'
call DWORD PTR __imp__printf
add esp, 8
mov eax, 8
shl eax, 2
mov ecx, DWORD PTR _c$[ebp]
mov edx, DWORD PTR [ecx+eax]
push edx
push OFFSET $SG52651 ; '%d'
call DWORD PTR __imp__printf
add esp, 8
lea ecx, DWORD PTR _c$[ebp]
call ?_Tidy@ ?$vector@HV ?$allocator@H@std@@@std@@IAEXXZ ;
std::vector<int,std::allocator<int> >::_Tidy
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP
Nous voyons que la méthode .at() vérifie les limites et envoie une exception en cas
d’erreur. Le nombre que le dernier appel à printf() affiche est pris de la mémoire,
sans aucune vérification.
On peut se demander pourquoi ne pas utiliser des variables comme «size » et «ca-
pacity », comme c’est fait dans std::string. Probablement que c’est fait comme
cela pour avoir une vérification des limites plus rapide.
765
Le code que GCC génère est en général presque le même, mais la méthode .at()
est mise en ligne:
766
std::vector<int,std::allocator<int>>::push_back(int const&)
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov ebx, [esp+14h]
mov eax, [esp+1Ch]
sub eax, ebx
cmp eax, 17h
ja short loc_80001CF
mov edi, [esp+18h]
sub edi, ebx
sar edi, 2
mov dword ptr [esp], 18h
call _Znwj ; operator new(uint)
mov esi, eax
test edi, edi
jz short loc_80001AD
lea eax, ds :0[edi*4]
mov [esp+8], eax ; n
mov [esp+4], ebx ; src
mov [esp], esi ; dest
call memmove
767
lea eax, [esp+14h]
mov [esp], eax
call _ZNSt6vectorIiSaIiEE9push_backERKi ;
std::vector<int,std::allocator<int>>::push_back(int const&)
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov eax, [esp+14h]
mov edx, [esp+18h]
sub edx, eax
cmp edx, 17h
ja short loc_8000246
mov dword ptr [esp], offset aVector_m_range ; "vector::_M_range_check"
call _ZSt20__throw_out_of_rangePKc ;
std::__throw_out_of_range(char const*)
768
pop ebp
.reserve() est aussi mise en ligne. Elle appelle new() si le buffer est trop petit pour
la nouvelle taille, appelle memmove() pour copier le contenu du buffer et appelle
delete() pour libérer l’ancien buffer.
Regardons aussi ce que le programme affiche s’il est compilé avec GCC:
_Myfirst=0x(nil), _Mylast=0x(nil), _Myend=0x(nil)
size=0, capacity=0
_Myfirst=0x8257008, _Mylast=0x825700c, _Myend=0x825700c
size=1, capacity=1
element 0: 1
_Myfirst=0x8257018, _Mylast=0x8257020, _Myend=0x8257020
size=2, capacity=2
element 0: 1
element 1: 2
_Myfirst=0x8257028, _Mylast=0x8257034, _Myend=0x8257038
size=3, capacity=4
element 0: 1
element 1: 2
element 2: 3
_Myfirst=0x8257028, _Mylast=0x8257038, _Myend=0x8257038
size=4, capacity=4
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x8257040, _Mylast=0x8257050, _Myend=0x8257058
size=4, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x8257040, _Mylast=0x8257054, _Myend=0x8257058
size=5, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
_Myfirst=0x8257040, _Mylast=0x8257058, _Myend=0x8257058
size=6, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
element 5: 6
769
6
0
Nous repérons que la taille du buffer grossit d’une manière différente qu’avec MSVC.
Une simple expérimentation montre que l’implémentation de MSVC augmente le
buffer de ~50% à chaque fois qu’il a besoin d’être augmenté, tandis que le code de
GCC l’augmente de 100% à chaque fois, i.e., le double.
10
1 100
0 5 20 107
3 6 12 99 101 1001
2 9 11 1010
Toutes les clefs qui sont plus petites que la valeur de la clef du nœud sont stockées
du côté gauche.
770
Toutes les clefs qui sont plus grandes que la valeur de la clef du nœud sont stockées
du côté droit.
Ainsi, l’algorithme de recherche est simple: si la valeur que vous cherchez est plus
petite que la valeur de la clef du nœud courant: déplacez à gauche, si elle plus
grande: déplacez à droite, stoppez si la valeur cherchée est égale à la valeur de la
clef du nœud.
C’est pourquoi l’algorithme de recherche peut chercher des nombres, des chaînes
de texte, etc., tant que la fonction de comparaison de clef est fourni.
Toutes les clefs ont des valeurs uniques.
En ayant cela, il faut ≈ log2 n itérations pour trouver une clef dans un arbre binaire
équilibré avec n clef. Ceci implique que ≈ 10 itérations sont pour ≈ 1000 keys, ou ≈ 13
itérations pour ≈ 10000 clefs.
Pas mal, mais l’arbre doit toujours être équilibré pour cela: i.e., les clefs doivent
être distribuées uniformément à chaque niveaux. Les opérations d’insertion et de
suppression font un peut de maintenance pour garder l’arbre dans un état équilibré.
Il y a plusieurs algorithme d’équilibrage disponible, incluant l’arbre AVL et l’arbre
red-black.
La dernière étend chaque nœud avec une valeur de « couleur » opur simplifier le
processus de rééquilibrage, de ce fait, chaque nœud peut être rouge ou noir.
À la fois les implémentations des templates std::map et std::set de GCC et MSVC
utilisent les arbres red-black.
std::set a seulement des clefs. std::map est la version étendue de std::set : il a
aussi une valeur à chaque nœud.
MSVC
#include <map>
#include <set>
#include <string>
#include <iostream>
struct tree_struct
771
{
struct tree_node *Myhead ;
size_t Mysize ;
};
if (traverse)
{
if (n->Isnil==1)
dump_tree_node (n->Parent, is_set, true) ;
else
{
if (n->Left->Isnil==0)
dump_tree_node (n->Left, is_set, true) ;
if (n->Right->Isnil==0)
dump_tree_node (n->Right, is_set, true) ;
};
};
};
772
{
printf ("ptr=0x%p, Myhead=0x%p, Mysize=%d\n", m, m->Myhead, m->Mysize) ;
dump_tree_node (m->Myhead, is_set, true) ;
printf ("As a tree :\n") ;
printf ("root----") ;
dump_as_tree (1, m->Myhead->Parent, is_set) ;
};
int main()
{
// map
m[10]="ten" ;
m[20]="twenty" ;
m[3]="three" ;
m[101]="one hundred one" ;
m[100]="one hundred" ;
m[12]="twelve" ;
m[107]="one hundred seven" ;
m[0]="zero" ;
m[1]="one" ;
m[6]="six" ;
m[99]="ninety-nine" ;
m[5]="five" ;
m[11]="eleven" ;
m[1001]="one thousand one" ;
m[1010]="one thousand ten" ;
m[2]="two" ;
m[9]="nine" ;
printf ("dumping m as map :\n") ;
dump_map_and_set ((struct tree_struct *)(void*)&m, false) ;
// set
std ::set<int> s ;
s.insert(123) ;
s.insert(456) ;
s.insert(11) ;
s.insert(12) ;
s.insert(100) ;
s.insert(1001) ;
printf ("dumping s as set :\n") ;
dump_map_and_set ((struct tree_struct *)(void*)&s, true) ;
std ::set<int> ::iterator it2=s.begin() ;
773
printf ("s.begin() :\n") ;
dump_tree_node ((struct tree_node *)*(void**)&it2, true, false) ;
it2=s.end() ;
printf ("s.end() :\n") ;
dump_tree_node ((struct tree_node *)*(void**)&it2, true, false) ;
};
774
first=107 second=[one hundred seven]
ptr=0x005BB420 Left=0x005BB3A0 Parent=0x005BB480 Right=0x005BB3A0 Color=1 ⤦
Ç Isnil=0
first=101 second=[one hundred one]
ptr=0x005BB560 Left=0x005BB3A0 Parent=0x005BB480 Right=0x005BB580 Color=1 ⤦
Ç Isnil=0
first=1001 second=[one thousand one]
ptr=0x005BB580 Left=0x005BB3A0 Parent=0x005BB560 Right=0x005BB3A0 Color=0 ⤦
Ç Isnil=0
first=1010 second=[one thousand ten]
As a tree :
root----10 [ten]
L-------1 [one]
L-------0 [zero]
R-------5 [five]
L-------3 [three]
L-------2 [two]
R-------6 [six]
R-------9 [nine]
R-------100 [one hundred]
L-------20 [twenty]
L-------12 [twelve]
L-------11 [eleven]
R-------99 [ninety-nine]
R-------107 [one hundred seven]
L-------101 [one hundred one]
R-------1001 [one thousand one]
R-------1010 [one thousand ten]
m.begin() :
ptr=0x005BB4A0 Left=0x005BB3A0 Parent=0x005BB4C0 Right=0x005BB3A0 Color=1 ⤦
Ç Isnil=0
first=0 second=[zero]
m.end() :
ptr=0x005BB3A0 Left=0x005BB4A0 Parent=0x005BB3C0 Right=0x005BB580 Color=1 ⤦
Ç Isnil=1
dumping s as set :
ptr=0x0020FDFC, Myhead=0x005BB5E0, Mysize=6
ptr=0x005BB5E0 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB6A0 Color=1 ⤦
Ç Isnil=1
ptr=0x005BB600 Left=0x005BB660 Parent=0x005BB5E0 Right=0x005BB620 Color=1 ⤦
Ç Isnil=0
first=123
ptr=0x005BB660 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB680 Color=1 ⤦
Ç Isnil=0
first=12
ptr=0x005BB640 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 ⤦
Ç Isnil=0
first=11
ptr=0x005BB680 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 ⤦
Ç Isnil=0
first=100
ptr=0x005BB620 Left=0x005BB5E0 Parent=0x005BB600 Right=0x005BB6A0 Color=1 ⤦
775
Ç Isnil=0
first=456
ptr=0x005BB6A0 Left=0x005BB5E0 Parent=0x005BB620 Right=0x005BB5E0 Color=0 ⤦
Ç Isnil=0
first=1001
As a tree :
root----123
L-------12
L-------11
R-------100
R-------456
R-------1001
s.begin() :
ptr=0x005BB640 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 ⤦
Ç Isnil=0
first=11
s.end() :
ptr=0x005BB5E0 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB6A0 Color=1 ⤦
Ç Isnil=1
La structure n’est pas paquée, donc chaque valeur char occupe 4 octets.
std::set
[Cormen, Thomas H. and Leiserson, Charles E. and Rivest, Ronald L. and Stein, Clif-
ford, Introduction to Algorithms, Third Edition, (2009)].
34
.
GCC
#include <stdio.h>
#include <map>
#include <set>
#include <string>
#include <iostream>
struct map_pair
{
int key ;
const char *value ;
};
struct tree_node
{
int M_color ; // 0 - Red, 1 - Black
struct tree_node *M_parent ;
struct tree_node *M_left ;
struct tree_node *M_right ;
};
34. http://www.ethoberon.ethz.ch/WirthPubl/AD.pdf
776
struct tree_struct
{
int M_key_compare ;
struct tree_node M_header ;
size_t M_node_count ;
};
void dump_tree_node (struct tree_node *n, bool is_set, bool traverse, bool ⤦
Ç dump_keys_and_values)
{
printf ("ptr=0x%p M_left=0x%p M_parent=0x%p M_right=0x%p M_color=%d\n",
n, n->M_left, n->M_parent, n->M_right, n->M_color) ;
if (dump_keys_and_values)
{
if (is_set)
printf ("key=%d\n", *(int*)point_after_struct) ;
else
{
struct map_pair *p=(struct map_pair *)point_after_struct ;
printf ("key=%d value=[%s]\n", p->key, p->value) ;
};
};
if (traverse==false)
return ;
if (n->M_left)
dump_tree_node (n->M_left, is_set, traverse, dump_keys_and_values) ;
if (n->M_right)
dump_tree_node (n->M_right, is_set, traverse, dump_keys_and_values)⤦
Ç ;
};
if (is_set)
printf ("%d\n", *(int*)point_after_struct) ;
else
{
struct map_pair *p=(struct map_pair *)point_after_struct ;
printf ("%d [%s]\n", p->key, p->value) ;
}
if (n->M_left)
{
printf ("%.*sL-------", tabs, ALOT_OF_TABS) ;
777
dump_as_tree (tabs+1, n->M_left, is_set) ;
};
if (n->M_right)
{
printf ("%.*sR-------", tabs, ALOT_OF_TABS) ;
dump_as_tree (tabs+1, n->M_right, is_set) ;
};
};
int main()
{
// map
m[10]="ten" ;
m[20]="twenty" ;
m[3]="three" ;
m[101]="one hundred one" ;
m[100]="one hundred" ;
m[12]="twelve" ;
m[107]="one hundred seven" ;
m[0]="zero" ;
m[1]="one" ;
m[6]="six" ;
m[99]="ninety-nine" ;
m[5]="five" ;
m[11]="eleven" ;
m[1001]="one thousand one" ;
m[1010]="one thousand ten" ;
m[2]="two" ;
m[9]="nine" ;
778
Ç ;
// set
std ::set<int> s ;
s.insert(123) ;
s.insert(456) ;
s.insert(11) ;
s.insert(12) ;
s.insert(100) ;
s.insert(1001) ;
printf ("dumping s as set :\n") ;
dump_map_and_set ((struct tree_struct *)(void*)&s, true) ;
std ::set<int> ::iterator it2=s.begin() ;
printf ("s.begin() :\n") ;
dump_tree_node ((struct tree_node *)*(void**)&it2, true, false, true) ;
it2=s.end() ;
printf ("s.end() :\n") ;
dump_tree_node ((struct tree_node *)*(void**)&it2, true, false, false) ;
};
779
ptr=0x007A49A8 M_left=0x007A4BA0 M_parent=0x007A4B80 M_right=0x007A4C40 ⤦
Ç M_color=0
key=20 value=[twenty]
ptr=0x007A4BA0 M_left=0x007A4C80 M_parent=0x007A49A8 M_right=0x00000000 ⤦
Ç M_color=1
key=12 value=[twelve]
ptr=0x007A4C80 M_left=0x00000000 M_parent=0x007A4BA0 M_right=0x00000000 ⤦
Ç M_color=0
key=11 value=[eleven]
ptr=0x007A4C40 M_left=0x00000000 M_parent=0x007A49A8 M_right=0x00000000 ⤦
Ç M_color=1
key=99 value=[ninety-nine]
ptr=0x007A4BC0 M_left=0x007A4B60 M_parent=0x007A4B80 M_right=0x007A4CA0 ⤦
Ç M_color=0
key=107 value=[one hundred seven]
ptr=0x007A4B60 M_left=0x00000000 M_parent=0x007A4BC0 M_right=0x00000000 ⤦
Ç M_color=1
key=101 value=[one hundred one]
ptr=0x007A4CA0 M_left=0x00000000 M_parent=0x007A4BC0 M_right=0x007A4CC0 ⤦
Ç M_color=1
key=1001 value=[one thousand one]
ptr=0x007A4CC0 M_left=0x00000000 M_parent=0x007A4CA0 M_right=0x00000000 ⤦
Ç M_color=0
key=1010 value=[one thousand ten]
As a tree :
root----10 [ten]
L-------1 [one]
L-------0 [zero]
R-------5 [five]
L-------3 [three]
L-------2 [two]
R-------6 [six]
R-------9 [nine]
R-------100 [one hundred]
L-------20 [twenty]
L-------12 [twelve]
L-------11 [eleven]
R-------99 [ninety-nine]
R-------107 [one hundred seven]
L-------101 [one hundred one]
R-------1001 [one thousand one]
R-------1010 [one thousand ten]
m.begin() :
ptr=0x007A4BE0 M_left=0x00000000 M_parent=0x007A4C00 M_right=0x00000000 ⤦
Ç M_color=1
key=0 value=[zero]
m.end() :
ptr=0x0028FE40 M_left=0x007A4BE0 M_parent=0x007A4988 M_right=0x007A4CC0 ⤦
Ç M_color=0
dumping s as set :
ptr=0x0028FE20, M_key_compare=0x8, M_header=0x0028FE24, M_node_count=6
ptr=0x007A1E80 M_left=0x01D5D890 M_parent=0x0028FE24 M_right=0x01D5D850 ⤦
780
Ç M_color=1
key=123
ptr=0x01D5D890 M_left=0x01D5D870 M_parent=0x007A1E80 M_right=0x01D5D8B0 ⤦
Ç M_color=1
key=12
ptr=0x01D5D870 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 ⤦
Ç M_color=0
key=11
ptr=0x01D5D8B0 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 ⤦
Ç M_color=0
key=100
ptr=0x01D5D850 M_left=0x00000000 M_parent=0x007A1E80 M_right=0x01D5D8D0 ⤦
Ç M_color=1
key=456
ptr=0x01D5D8D0 M_left=0x00000000 M_parent=0x01D5D850 M_right=0x00000000 ⤦
Ç M_color=0
key=1001
As a tree :
root----123
L-------12
L-------11
R-------100
R-------456
R-------1001
s.begin() :
ptr=0x01D5D870 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 ⤦
Ç M_color=0
key=11
s.end() :
ptr=0x0028FE24 M_left=0x01D5D870 M_parent=0x007A1E80 M_right=0x01D5D8D0 ⤦
Ç M_color=0
35
.
Il y a aussi une démo nous montrant comment un arbre est rééquilibré après quelques
insertions.
struct map_pair
{
int key ;
35. http://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.1/stl__tree_
8h-source.html
781
const char *value ;
};
struct tree_node
{
int M_color ; // 0 - Red, 1 - Black
struct tree_node *M_parent ;
struct tree_node *M_left ;
struct tree_node *M_right ;
};
struct tree_struct
{
int M_key_compare ;
struct tree_node M_header ;
size_t M_node_count ;
};
if (n->M_left)
{
printf ("%.*sL-------", tabs, ALOT_OF_TABS) ;
dump_as_tree (tabs+1, n->M_left) ;
};
if (n->M_right)
{
printf ("%.*sR-------", tabs, ALOT_OF_TABS) ;
dump_as_tree (tabs+1, n->M_right) ;
};
};
int main()
{
std ::set<int> s ;
s.insert(123) ;
s.insert(456) ;
printf ("123, 456 has been inserted\n") ;
dump_map_and_set ((struct tree_struct *)(void*)&s) ;
s.insert(11) ;
s.insert(12) ;
782
printf ("\n") ;
printf ("11, 12 has been inserted\n") ;
dump_map_and_set ((struct tree_struct *)(void*)&s) ;
s.insert(100) ;
s.insert(1001) ;
printf ("\n") ;
printf ("100, 1001 has been inserted\n") ;
dump_map_and_set ((struct tree_struct *)(void*)&s) ;
s.insert(667) ;
s.insert(1) ;
s.insert(4) ;
s.insert(7) ;
printf ("\n") ;
printf ("667, 1, 4, 7 has been inserted\n") ;
dump_map_and_set ((struct tree_struct *)(void*)&s) ;
printf ("\n") ;
};
783
3.21.5 Mémoire
Vous pouvez parfois entendre de la part de programmeurs C++ «allouer la mémoire
sur la pile » et/ou «allouer la mémoire sur le tas ».
Allouer un objet sur la pile :
void f()
{
...
Class o=Class(...) ;
...
};
...
};
void f2()
{
...
delete o ;
...
};
Ceci est la même chose que d’allouer de la mémoire pour une structure en utilisant
un appel à malloc(). En fait, new en C++ est un wrapper pour malloc(), et delete
est un wrapper pour free(). Puisque le bloc de mémoire a été allouée sur le tas, il
doit être désalloué explicitement, en utilisant delete. Le destructeur de classe sera
appelé automatiquement juste avant ce moment.
Quelle méthode est la meilleure? L’allocation isur la pile est très rapide, et bon pour
les petits, à durée de vie courte objets, qui seront utilisés seulement dans la fonction
courante.
L’allocation sur le heap est plus lente, et meilleure pour des objets à longue durée
de vie, qui seront utilisés dans plusieurs fonctions. Aussi, les objets alloués sur le tas
784
sont sujets à la fuite de mémoire, car ils doivent être libérés explicitement, mais on
peut oublier de le faire.
De toutes façons, ceci est une affaire de goût.
int main()
{
char *s="Hello, world !" ;
char *s_end=s+strlen(s) ;
Ça fonctionne, mais s_end doit toujours avoir l’adresse du zéro en fin de la chaîne s.
Si la taille de la chaîne s est modifiée, s_end doit être modifié aussi.
L’astuce est douteuse, mais, encore une fois, ceci est une démonstration d’indices
négatifs.
785
Malheureusement, l’index −0 ne fonctionnera pas, puisque la représentation des
nombres négatifs en complément à deux ( 2.2 on page 585) ne permet pas de zéro
négatif. donc il ne peut pas être distingué d’un zéro positif.
Ceci est aussi mentionné dans “Transaction processing”, Jim Gray, 1993, chapitre
“The Tuple-Oriented File System”, p. 755.
int main()
{
int random_value=0x11223344 ;
unsigned char array[10];
int i ;
unsigned char *fakearray=&array[-1];
786
9 _array$ = -16 ; taille = 10
10 _i$ = -4 ; taille = 4
11 _main PROC
12 push ebp
13 mov ebp, esp
14 sub esp, 24
15 mov DWORD PTR _random_value$[ebp], 287454020 ; 11223344H
16 ; définir fakearray[] un octet avant array[]
17 lea eax, DWORD PTR _array$[ebp]
18 add eax, -1 ; eax=eax-1
19 mov DWORD PTR _fakearray$[ebp], eax
20 mov DWORD PTR _i$[ebp], 0
21 jmp SHORT $LN3@main
22 ; remplir array[] avec 0..9
23 $LN2@main :
24 mov ecx, DWORD PTR _i$[ebp]
25 add ecx, 1
26 mov DWORD PTR _i$[ebp], ecx
27 $LN3@main :
28 cmp DWORD PTR _i$[ebp], 10
29 jge SHORT $LN1@main
30 mov edx, DWORD PTR _i$[ebp]
31 mov al, BYTE PTR _i$[ebp]
32 mov BYTE PTR _array$[ebp+edx], al
33 jmp SHORT $LN2@main
34 $LN1@main :
35 mov ecx, DWORD PTR _fakearray$[ebp]
36 ; ecx=adresse de fakearray[0], ecx+1 est fakearray[1] ou array[0]
37 movzx edx, BYTE PTR [ecx+1]
38 push edx
39 push OFFSET $SG2751 ; 'first element %d'
40 call _printf
41 add esp, 8
42 mov eax, DWORD PTR _fakearray$[ebp]
43 ; eax=adresse de fakearray[0], eax+2 est fakearray[2] ou array[1]
44 movzx ecx, BYTE PTR [eax+2]
45 push ecx
46 push OFFSET $SG2752 ; 'second element %d'
47 call _printf
48 add esp, 8
49 mov edx, DWORD PTR _fakearray$[ebp]
50 ; edx=adresse de fakearray[0], edx+10 est fakearray[10] ou array[9]
51 movzx eax, BYTE PTR [edx+10]
52 push eax
53 push OFFSET $SG2753 ; 'last element %d'
54 call _printf
55 add esp, 8
56 ; soustrait 4, 3, 2 et 1 du pointeur sur array[0] afin de trouver
les valeurs avant array[]
57 lea ecx, DWORD PTR _array$[ebp]
58 movzx edx, BYTE PTR [ecx-4]
59 push edx
60 lea eax, DWORD PTR _array$[ebp]
61 movzx ecx, BYTE PTR [eax-3]
787
62 push ecx
63 lea edx, DWORD PTR _array$[ebp]
64 movzx eax, BYTE PTR [edx-2]
65 push eax
66 lea ecx, DWORD PTR _array$[ebp]
67 movzx edx, BYTE PTR [ecx-1]
68 push edx
69 push OFFSET $SG2754 ;
'array[-1]=%02X, array[-2]=%02X, array[-3]=%02X, array[-4]=%02X'
70 call _printf
71 add esp, 20
72 xor eax, eax
73 mov esp, ebp
74 pop ebp
75 ret 0
76 _main ENDP
Donc nous avons le tableau array[] de dix éléments, rempli avec les octets 0 . . . 9.
Puis nous avons le pointeur fakearray[], qui pointe un octet avant array[].
fakearray[1] pointe exactement sur array[0]. Mais nous sommes toujours cu-
rieux, qu’y a-t-il avant array[] ? Nous avons ajouté random_value avant array[] et
l’avons défini à 0x11223344. Le compilateur sans optimisation a alloué les variables
dans l’ordre dans lequel elles sont déclarées, donc oui, la valeur 32-bit random_value
est juste avant le tableau.
Nous le lançons, et:
first element 0
second element 1
last element 9
array[-1]=11, array[-2]=22, array[-3]=33, array[-4]=44
Voici le fragment de pile que nous avons copier/coller depuis la fenêtre de pile d’Ol-
lyDbg (avec les commentaires ajoutés par l’auteur) :
Listing 3.122: MSVC 2010 sans optimisation
Pile du CPU
Address Value
001DFBCC /001DFBD3 ; pointeur fakearray
001DFBD0 |11223344 ; random_value
001DFBD4 |03020100 ; 4 octets de array[]
001DFBD8 |07060504 ; 4 octets de array[]
001DFBDC |00CB0908 ; reste aléatoire + 2 dernier octets de array[]
001DFBE0 |0000000A ; dernière valeur de i après la fin de la boucle
001DFBE4 |001DFC2C ; valeur de EBP sauvée
001DFBE8 \00CB129D ; Adresse de Retour
788
3.23 Plus loin avec les pointeurs
Pour ceux qui veulent se casser la tête à comprendre les pointeurs C/C++, voici
plus d’exemples. Certains d’entre eux sont bizarres et ne servent qu’à des fins de
démonstration: utilisez-les en production uniquement si vous savez vraiment ce que
vous faites.
int main()
{
char *s="Hello, world !" ;
print_string (s) ;
};
789
#include <stdio.h>
#include <stdint.h>
int main()
{
char *s="Hello, world !" ;
print_string ((uint64_t)s) ;
};
J’utilise uint64_t car j’ai effectué cet exemple sur Linux x64. int fonctionnerait pour
des OS-s 32-bit. D’abord, un pointeur sur un caractère (la toute première chaîne de
bienvenu) est casté en uint64_t, puis passé plus loin. La fonction print_string()
re-caste la valeur uint64_t en un pointeur sur un caractère.
Ce qui est intéressant, c’est que GCC 4.8.4 produit une sortie assembleur identique
pour les deux versions:
gcc 1.c -S -masm=intel -O3 -fno-inline
.LC0 :
.string "(address : 0x%llx)\n"
print_string :
push rbx
mov rdx, rdi
mov rbx, rdi
mov esi, OFFSET FLAT :.LC0
mov edi, 1
xor eax, eax
call __printf_chk
mov rdi, rbx
pop rbx
jmp puts
.LC1 :
.string "Hello, world !"
main :
sub rsp, 8
mov edi, OFFSET FLAT :.LC1
call print_string
add rsp, 8
ret
790
Continuons à abuser massivement des traditions de programmation de C/C++. On
pourrait écrire ceci:
#include <stdio.h>
#include <stdint.h>
int main()
{
char *s="Hello, world !" ;
print_string (s) ;
};
791
int main()
{
char *s="Hello, world !" ;
print_string ((uint64_t)s) ;
};
load_byte_at_address :
movzx eax, BYTE PTR [rdi]
ret
print_string :
.LFB15 :
push rbx
mov rbx, rdi
jmp .L4
.L7 :
movsx edi, al
add rbx, 1
call putchar
.L4 :
mov rdi, rbx
call load_byte_at_address
test al, al
jne .L7
pop rbx
ret
.LC0 :
.string "Hello, world !"
main :
sub rsp, 8
mov edi, OFFSET FLAT :.LC0
call print_string
add rsp, 8
ret
792
uint64_t multiply1 (uint64_t a, uint64_t b)
{
return a*b ;
};
int main()
{
printf ("%d\n", multiply1(123, 456)) ;
printf ("%d\n", (uint64_t)multiply2((uint64_t*)123, (uint64_t*)456)⤦
Ç );
};
Il fonctionne sans problème et GCC 4.8.4 compile les fonctions multiply1() et multi-
ply2() de manière identique!
multiply1 :
mov rax, rdi
imul rax, rsi
ret
multiply2 :
mov rax, rdi
imul rax, rsi
ret
Tant que vous ne déréférencez pas le pointeur (autrement dit, que vous ne lisez
aucune donnée depuis l’adresse stockée dans le pointeur), tout se passera bien.
Un pointeur est une variable qui peut stocker n’importe quoi, comme une variable
usuelle.
L’instruction de multiplication signée (IMUL) est utilisée ici au lieu de la non-signée
(MUL), lisez-en plus à ce sujet ici: 2.2.1 on page 587.
À propos, il y a une astuce très connue pour abuser des pointeurs appelée tagged
pointers. En gros, si tous vos pointeurs pointent sur des blocs de mémoire de taille,
disons, 16 octets (ou qu’ils sont toujours alignés sur une limite de 16-octet), les 4
bits les plus bas du pointeur sont toujours zéro et cet espace peut être utilisé d’une
certaine façon. C’est très répandu dans les compilateurs et interpréteurs LISP. Ils
stockent le type de cell/objet dans ces bits inutilisés, ceci peut économiser un peu
de mémoire. Encore mieux, vous pouvez évaluer le type de cell/objet en utilisant
seulement le pointeur, sans accès supplémentaire à la mémoire. En lire plus à ce
sujet: [Dennis Yurichev, C/C++ programming language notes 1.3].
793
3.23.3 Abus de pointeurs dans le noyau Windows
La section ressource d’un exécutable PE dans les OS Windows est une section conte-
nant des images, des icônes, des chaînes, etc. Les premières versions de Windows
permettaient seulement d’adresser les ressources par ID, mais Microsoft a ajouté un
moyen de les adresser en utilisant des chaînes.
Donc, il doit être alors possible de passer un ID ou une chaîne a la fonction Elle est
déclarée comme ceci: FindResource().
HRSRC WINAPI FindResource(
_In_opt_ HMODULE hModule,
_In_ LPCTSTR lpName,
_In_ LPCTSTR lpType
);
lpName et lpType ont un type char* ou wchar*, et lorsque quelqu’un veut encore
passer un ID, il doit utiliser la macro MAKEINTRESOURCE, comme ceci:
result = FindResource(..., MAKEINTRESOURCE(1234), ...) ;
C’est un fait intéressant que MAKEINTRESOURCE est juste un casting d’entier vers
un pointeur. Dans MSVC 2013, dans le fichier
Microsoft SDKs\Windows\v7.1A\Include\Ks.h nous pouvons voir ceci:
...
...
Ça semble fou. Regardons dans l’ancien code source de Windows NT4 qui avait fuité.
Dans private/windows/base/client/module.c nous pouvons trouver le source code de
FindResource() :
HRSRC
FindResourceA(
HMODULE hModule,
LPCSTR lpName,
LPCSTR lpType
)
...
{
NTSTATUS Status ;
ULONG IdPath[ 3 ];
PVOID p ;
IdPath[ 0 ] = 0;
IdPath[ 1 ] = 0;
794
try {
if ((IdPath[ 0 ] = BaseDllMapResourceIdA( lpType )) == -1) {
Status = STATUS_INVALID_PARAMETER ;
}
else
if ((IdPath[ 1 ] = BaseDllMapResourceIdA( lpName )) == -1) {
Status = STATUS_INVALID_PARAMETER ;
...
try {
if ((ULONG)lpId & LDR_RESOURCE_ID_NAME_MASK) {
if (*lpId == '#') {
Status = RtlCharToInteger( lpId+1, 10, &Id ) ;
if (!NT_SUCCESS( Status ) || Id & LDR_RESOURCE_ID_NAME_MASK⤦
Ç ) {
if (NT_SUCCESS( Status )) {
Status = STATUS_INVALID_PARAMETER ;
}
BaseSetLastNTError( Status ) ;
Id = (ULONG)-1;
}
}
else {
RtlInitAnsiString( &AnsiString, lpId ) ;
Status = RtlAnsiStringToUnicodeString( &UnicodeString,
&AnsiString,
TRUE
);
if (!NT_SUCCESS( Status )){
BaseSetLastNTError( Status ) ;
Id = (ULONG)-1;
}
else {
s = UnicodeString.Buffer ;
while (*s != UNICODE_NULL) {
*s = RtlUpcaseUnicodeChar( *s ) ;
s++;
}
Id = (ULONG)UnicodeString.Buffer ;
}
795
}
}
else {
Id = (ULONG)lpId ;
}
}
except (EXCEPTION_EXECUTE_HANDLER) {
BaseSetLastNTError( GetExceptionCode() ) ;
Id = (ULONG)-1;
}
return Id ;
}
...
Donc lpId est ANDé avec 0xFFFF0000 et si des bits sont encore présents dans la par-
tie 16-bit basse, la première partie de la fonction est exécutée (lpId est traité comme
une adresse de chaîne). Autrement—la seconde moitié (lpId est traitée comme une
valeur 16-bit).
Encore, ce code peut être trouvé dans le fichier kernel32.dll de Windows 7:
....
.text :0000000078D24510 ;
__int64 __fastcall BaseDllMapResourceIdA(PCSZ SourceString)
.text :0000000078D24510 BaseDllMapResourceIdA proc near ; CODE XREF:
FindResourceExA+34
.text :0000000078D24510 ;
FindResourceExA+4B
.text :0000000078D24510
.text :0000000078D24510 var_38 = qword ptr -38h
.text :0000000078D24510 var_30 = qword ptr -30h
.text :0000000078D24510 var_28 = _UNICODE_STRING ptr -28h
.text :0000000078D24510 DestinationString= _STRING ptr -18h
.text :0000000078D24510 arg_8 = dword ptr 10h
.text :0000000078D24510
.text :0000000078D24510 ; FUNCTION CHUNK AT .text:0000000078D42FB4 SIZE
000000D5 BYTES
.text :0000000078D24510
.text :0000000078D24510 push rbx
.text :0000000078D24512 sub rsp, 50h
.text :0000000078D24516 cmp rcx, 10000h
.text :0000000078D2451D jnb loc_78D42FB4
.text :0000000078D24523 mov [rsp+58h+var_38], rcx
.text :0000000078D24528 jmp short $+2
.text :0000000078D2452A ;
---------------------------------------------------------------------------
796
.text :0000000078D2452A
.text :0000000078D2452A loc_78D2452A : ; CODE XREF:
BaseDllMapResourceIdA+18
.text :0000000078D2452A ;
BaseDllMapResourceIdA+1EAD0
.text :0000000078D2452A jmp short $+2
.text :0000000078D2452C ;
---------------------------------------------------------------------------
.text :0000000078D2452C
.text :0000000078D2452C loc_78D2452C : ;
CODE XREF: BaseDllMapResourceIdA:loc_78D2452A
.text :0000000078D2452C ;
BaseDllMapResourceIdA+1EB74
.text :0000000078D2452C mov rax, rcx
.text :0000000078D2452F add rsp, 50h
.text :0000000078D24533 pop rbx
.text :0000000078D24534 retn
.text :0000000078D24534 ;
---------------------------------------------------------------------------
.text :0000000078D24535 align 20h
.text :0000000078D24535 BaseDllMapResourceIdA endp
....
....
Si la valeur du pointeur en entrée est plus grande que 0x10000, un saut au traite-
ment de chaînes se produit. Autrement, la valeur en entrée du lpId est renvoyée
telle quelle. Le masque 0xFFFF0000 n’est plus utilisé ici, car ceci est du code 64-bit,
mais encore, 0xFFFFFFFFFFFF0000 pourrait fonctionner ici.
Le lecteur attentif pourrait demander ce qui se passe si l’adresse de la chaîne en
entrée est plus petite que 0x10000? Ce code se base sur le fait que dans Windows,
il n’y a rien aux adresses en dessous de 0x10000, au moins en Win32 realm.
Raymond Chen écrit à propos de ceci:
797
on the convention that the first 64KB of address space is never mapped
to valid memory, a convention that is enforced starting in Windows 7.
En quelques mots, ceci est un sale hack et probablement qu’il ne devrait être utilisé
qu’en cas de réelle nécessité. Peut-être que la fonction FindResource() avait un type
SHORT pour ses arguments, et puis Microsoft a ajouté un moyen de passer des
chaînes ici, mais que le code ancien devait toujours être supporté.
Maintenant, voici un exemple distillé:
#include <stdio.h>
#include <stdint.h>
void f(char* a)
{
if (((uint64_t)a)>0x10000)
printf ("Pointer to string has been passed : %s\n", a) ;
else
printf ("16-bit value has been passed : %d\n", (uint64_t)a)⤦
Ç ;
};
int main()
{
f("Hello !") ; // pass string
f((char*)1234) ; // pass 16-bit value
};
Ça fonctionne!
Comme ça a déjà été pointé dans des commentaires sur Hacker News, le noyau
Linux comporte aussi des choses comme ça.
Par exemple, cette fonction peut renvoyer un code erreur ou un pointeur:
struct kernfs_node *kernfs_create_link(struct kernfs_node *parent,
const char *name,
struct kernfs_node *target)
{
struct kernfs_node *kn ;
int error ;
if (kernfs_ns_enabled(parent))
kn->ns = target->ns ;
kn->symlink.target_kn = target ;
kernfs_get(target) ; /* ref owned by symlink */
798
error = kernfs_add_one(kn) ;
if (!error)
return kn ;
kernfs_put(kn) ;
return ERR_PTR(error) ;
}
( https://github.com/torvalds/linux/blob/fceef393a538134f03b778c5d2519e670269342f/
fs/kernfs/symlink.c#L25 )
ERR_PTR est une macro pour caster un entier en un pointeur:
static inline void * __must_check ERR_PTR(long error)
{
return (void *) error ;
}
( https://github.com/torvalds/linux/blob/61d0b5a4b2777dcf5daef245e212b3c1fa8091ca/
tools/virtio/linux/err.h )
Ce fichier d’en-tête contient aussi une macro d’aide pour distinguer un code d’erreur
d’un pointeur:
#define IS_ERR_VALUE(x) unlikely((x) >= (unsigned long)-MAX_ERRNO)
Ceci signifie que les codes erreurs sont les “pointeurs” qui sont très proche de -1,
et, heureusement, il n’y a rien dans la mémoire du noyau à des adresses comme
0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFE, 0xFFFFFFFFFFFFFFFD, etc.
Une méthode bien plus répandue est de renvoyer NULL en cas d’erreur et de passer
le code d’erreur par un argument supplémentaire. Les auteurs du noyau Linux ne font
pas ça, mais quiconque veut utiliser ces fonctions doit toujours garder en mémoire
que le pointeur renvoyé doit toujours être testé avec IS_ERR_VALUE avant d’être
déréférencé.
Par exemple:
fman->cam_offset = fman_muram_alloc(fman->muram, fman->cam_size) ;
if (IS_ERR_VALUE(fman->cam_offset)) {
dev_err(fman->dev, "%s : MURAM alloc for DMA CAM failed\n",
__func__) ;
return -ENOMEM ;
}
( https://github.com/torvalds/linux/blob/aa00edc1287a693eadc7bc67a3d73555d969b35d/
drivers/net/ethernet/freescale/fman/fman.c#L826 )
La fonction mmap() renvoie -1 en cas d’erreur (ou MAP_FAILED, qui vaut -1). Cer-
taines personnes disent que mmap() peut mapper une zone mémoire à l’adresse
799
zéro dans de rares situations, donc elle ne peut pas utiliser 0 ou NULL comme code
d’erreur.
Des anciens peuvent se souvenir d’un message d’erreur bizarre du temps de MS-
DOS: “Null pointer assignment”. Qu’est-ce que ça signifie?
Il n’est pas possible d’écrire à l’adresse mémoire zéro avec les OSs *NIX et Windows,
mais il est possible de le faire avec MS-DOS, à cause de l’absence de protection de
la mémoire.
Donc, j’ai sorti mon ancien Turbo C++ 3.0 (qui fût renommer plus tard en Borland
C++) d’avant les années 1990s et essayé de compiler ceci:
#include <stdio.h>
int main()
{
int *ptr=NULL ;
*ptr=1234;
printf ("Now let's read at NULL\n") ;
printf ("%d\n", *ptr) ;
};
C’est difficile à croire, mais ça fonctionne, sans erreur jusqu’à la sortie, toutefois:
C :\TC30\BIN>_
Plongeons un peu plus profondément dans le code du CRT de Borland C++ 3.1, fi-
chier c0.asm :
; _checknull() check for null pointer zapping copyright message
...
IF LDATA EQ false
IFNDEF __TINY__
push si
push di
800
mov es, cs :DGROUP@@
xor ax, ax
mov si, ax
mov cx, lgth_CopyRight
ComputeChecksum label near
add al, es :[si]
adc ah, 0
inc si
loop ComputeChecksum
sub ax, CheckSum
jz @@SumOK
mov cx, lgth_NullCheck
mov dx, offset DGROUP : NullCheck
call ErrorDisplay
@@SumOK : pop di
pop si
ENDIF
ENDIF
_DATA SEGMENT
; Magic symbol used by the debug info to locate the data segment
public DATASEG@
DATASEG@ label byte
CopyRight db 4 dup(0)
db 'Borland C++ - Copyright 1991 Borland Intl.',0
lgth_CopyRight equ $ - CopyRight
IF LDATA EQ false
IFNDEF __TINY__
CheckSum equ 00D5Ch
NullCheck db 'Null pointer assignment', 13, 10
lgth_NullCheck equ $ - NullCheck
ENDIF
ENDIF
...
801
Mais pourquoi? Écrire à l’adresse zéro est une erreur courante en C/C++, et si vous
faites cela sur *NIX ou Windows, votre application va planter. MS-DOS n’a pas de
protection de la mémoire, donc le CRT doit vérifier ceci post-factum et le signaler à
la sortie. Si vous voyez ce message, ceci signifie que votre programme à un certain
point, a écrit à l’adresse 0.
Notre programme le fait. Et ceci est pourquoi le nombre 1234 a été lu correctement:
car il a été écrit à la place des 4 premiers octets à zéro. La somme de contrôle est
incorrecte à la sortie (car le nombre y a été laissé), donc le message a été affiché.
Ai-je raison? J’ai récrit le programme pour vérifier mes suppositions:
#include <stdio.h>
int main()
{
int *ptr=NULL ;
*ptr=1234;
printf ("Now let's read at NULL\n") ;
printf ("%d\n", *ptr) ;
*ptr=0; // psst, cover our tracks!
};
Mais pourquoi un programmeur sain d’esprit écrirait du code écrivant quelque chose
à l’adresse 0? Ça peut être fait accidentellement: par exemple, un pointeur doit
être initialisé au bloc de mémoire nouvellement alloué et ensuite passé à quelque
fonction qui renvoie des données à travers un pointeur.
int *ptr=NULL ;
strcpy (ptr, buf) ; // strcpy() termine silencieusement car MS-DOS n'a pas de
protection de la mémoire
Encore pire:
int *ptr=malloc(1000) ;
802
strcpy (ptr, buf) ; // strcpy() termine silencieusement car MS-DOS n'a pas ⤦
Ç de protection de la mémoire
Voici un exemple tiré de dmalloc38 , une façon portable de générer un core dump, si
les autres moyens ne sont pas disponibles:
3.4 Generating a Core File on Errors
====================================
If the `error-abort' debug token has been enabled, when the library
detects any problems with the heap memory, it will immediately attempt
to dump a core file. *Note Debug Tokens ::. Core files are a complete
copy of the program and it's state and can be used by a debugger to see
specifically what is going on when the error occurred. *Note Using
With a Debugger ::. By default, the low, medium, and high arguments to
the library utility enable the `error-abort' token. You can disable
this feature by entering `dmalloc -m error-abort' (-m for minus) to
remove the `error-abort' token and your program will just log errors
and continue. You can also use the `error-dump' token which tries to
dump core when it sees an error but still continue running. *Note
Debug Tokens ::.
When a program dumps core, the system writes the program and all of
its memory to a file on disk usually named `core'. If your program is
called `foo' then your system may dump core as `foo.core'. If you are
not getting a `core' file, make sure that your program has not changed
to a new directory meaning that it may have written the core file in a
different location. Also insure that your program has write privileges
over the directory that it is in otherwise it will not be able to dump
a core file. Core dumps are often security problems since they contain
all program memory so systems often block their being produced. You
will want to check your user and system's core dump size ulimit
settings.
The library by default uses the `abort' function to dump core which
may or may not work depending on your operating system. If the
following program does not dump core then this may be the problem. See
`KILL_PROCESS' definition in `settings.dist'.
main()
{
abort() ;
}
If `abort' does work then you may want to try the following setting
in `settings.dist'. This code tries to generate a segmentation fault
by dereferencing a `NULL' pointer.
38. http://dmalloc.com/
803
#define KILL_PROCESS { int *_int_p = 0L ; *_int_p = 1; }
NULL en C/C++
NULL en C/C++ est juste une macro qui est souvent définie comme ceci:
#define NULL ((void*)0)
( fichier libio.h )
void* est un type de données reflétant le fait que c’est un pointeur, mais sur une
valeur d’un type de données inconnu (void).
NULL est usuellement utilisé pour montrer l’absence d’un objet. Par exemple, vous
avez une liste simplement chaînée, et chaque nœud a une valeur (ou un pointeur sur
une valeur) et un pointeur next. Pour montrer qu’il n’y a pas de nœud suivant, 0 est
stocké dans le champ next. (D’autres solutions sont pires.) Peut-être pourriez-vous
avoir un environnement fou où vous devriez allouer un bloc de mémoire à l’adresse
zéro. Comment indiqueriez-vous l’absence de nœud suivant? Avec une sorte de ma-
gic number ? Peut-être -1? Ou peut-être avec un bit additionnel?
Nous trouvons ceci dans Wikipédia:
( https://en.wikipedia.org/wiki/Zero_page )
Il est possible d’appeler une fonction avec son adresse. Par exemple, je compile ceci
avec MSVC 2010 et le lance dans Windows 7:
#include <windows.h>
#include <stdio.h>
int main()
{
printf ("0x%x\n", &MessageBoxA) ;
};
Le résultat est 0x7578feae et ne change pas après plusieurs lancements, car user32.dll
(où la fonction MessageBoxA se trouve) est toujours chargée à la même adresse. Et
aussi car ASLR39 n’est pas activé (le résultat serait différent à chaque exécution dans
ce cas).
Appelons MessageBoxA() par son adresse:
39. Address Space Layout Randomization
804
#include <windows.h>
#include <stdio.h>
int main()
{
msgboxtype msgboxaddr=0x7578feae ;
Qui voudrait appeler une fonction à l’adresse 0? Ceci est un moyen portable de
sauter à l’adresse zéro. De nombreux micro-contrôleurs à bas coût n’ont pas de
protection mémoire ou de MMU et après un reset, ils commencent à exécuter le
code à l’adresse 0, où une sorte de code d’initialisation est stocké. Donc sauter à
l’adresse 0 est un moyen de se réinitialiser. On pourrait utiliser de l’assembleur inline,
mais si ce n’est pas possible, cette méthode portable est utilisable.
Ça compile même correctement avec mon GCC 4.8.4 sur Linux x64:
reset :
sub rsp, 8
xor eax, eax
call rax
add rsp, 8
ret
Le fait que le pointeur de pile soit décalé n’est pas un problème: le code d’initialisa-
tion dans les micro-contrôleurs ignorent en général les registres et l’état de la RAM
et démarrent from scratch.
805
Et bien sûr, ce code planterait sur *NIX ou Windows à cause de la protection mémoire,
et même sans la protection mémoire, il n’y a pas de code à l’adresse 0.
GCC possède même des extensions non-standard, permettant de sauter à une adresse
spécifique plutôt qu’un appel à une fonction ici: http://gcc.gnu.org/onlinedocs/
gcc/Labels-as-Values.html.
int f()
{
int a[16];
write_something1(a) ;
write_something2(a) ;
};
write_something2 :
mov DWORD PTR [rdi+20], 0
ret
Mais vous pouvez toujours déclarer un tableau au lieu d’un pointeur à des fins d’auto-
documentation, si la taille du tableau est toujours fixée. Et peut-être, des outils d’ana-
lyse statique seraient capable de vous avertir d’un possible débordement de tampon.
Ou est-ce possible avec des outils aujourd’hui?
Certaines personnes, incluant Linux Torvalds, critiquent cette possibilité de C/C++ :
https://lkml.org/lkml/2015/9/3/428.
Le standard C99 a aussi le mot-clef static [ISO/IEC 9899:TC3 (C C99 standard), (2007)
6.7.5.3] :
806
If the keyword static also appears within the [ and ] of the array
type derivation, then for each call to the function, the value of the cor-
responding actual argument shall provide access to the first element
of an array with at least as many elements as specified by the size
expression.
void print_something ()
{
printf ("we are in %s()\n", __FUNCTION__) ;
};
int main()
{
print_something() ;
printf ("first 3 bytes : %x %x %x...\n",
*(unsigned char*)print_something,
*((unsigned char*)print_something+1),
*((unsigned char*)print_something+2)) ;
};
Ça dit que les 3 premiers octets de la fonction sont 55 89 e5. En effet, ce sont les
opcodes des instructions PUSH EBP et MOV EBP, ESP (se sont des opcodes x86). Mais
alors notre programme plante, car la section text est en lecture seule.
40
Nous pouvons recompiler notre exemple et rendre la section text modifiable :
gcc --static -g -Wl,--omagic -o example example.c
Ça fonctionne!
40. http://stackoverflow.com/questions/27581279/make-text-segment-writable-elf
807
we are in print_something()
first 3 bytes : 55 89 e5...
going to call patched print_something() :
it must exit at this point
int check_protection()
{
// do something
return 0;
// or return 1;
};
int main()
{
if (check_protection()==0)
{
printf ("no protection installed\n") ;
exit(0) ;
};
0000054d <check_protection> :
808
54d : 55 push %ebp
54e : 89 e5 mov %esp,%ebp
550: e8 b7 00 00 00 call 60c <__x86.get_pc_thunk.ax>
555: 05 7f 1a 00 00 add $0x1a7f,%eax
55a : b8 00 00 00 00 mov $0x0,%eax
55f : 5d pop %ebp
560: c3 ret
int main()
{
if (expired) // must be expired() here
{
print ("expired\n") ;
exit(0) ;
}
else
{
// do something
};
};
Puisque le nom de la fonction seul est interprété comme un pointeur sur une fonction,
ou une adresse, la déclaration if(function_name) est comme if(true).
Malheureusement, un compilateur C/C++ ne va pas générer d’avertissement.
809
Puis, vous passez ce “handle” à une autre fonction comme GetProcAddress(). Mais
en fait, LoadLibrary() renvoie un pointeur sur le fichier DLL mappé en mémoire. 43 .
Vous pouvez lire deux octets de l’adresse renvoyée par LoadLibrary(), et ça sera
“MZ” (deux premiers octets de n’importe quel fichier .EXE/.DLL sur Windows).
Il semble que Microsoft “cache” cela, afin de fournir une meilleure compatibilité as-
cendante. Donc, le type de données de HMODULE et HINSTANCE a une autre signifi-
cation dan Windows 16-bits.
Probablement que ceci est la raison pour laquelle printf() possède le modificateur
“%p”, qui est utilisé pour afficher des pointeurs (entier 32-bits sur une architecture
32-bits et 64-bit sur une 64-bits, etc.) au format hexadécimal. L’adresse d’une struc-
ture écrite dans des logs de debug peut aider à la retrouver dans d’autres logs.
Voici un exemple tiré du code source de SQLite:
...
struct Pager {
sqlite3_vfs *pVfs ; /* OS functions to use for IO */
u8 exclusiveMode ; /* Boolean. True if locking_mode==EXCLUSIVE ⤦
Ç */
u8 journalMode ; /* One of the PAGER_JOURNALMODE_* values */
u8 useJournal ; /* Use a rollback journal on this file */
u8 noSync ; /* Do not sync the journal if true */
....
...
PAGER_INCR(sqlite3_pager_readdb_count) ;
PAGER_INCR(pPager->nRead) ;
IOTRACE(("PGIN %p %d\n", pPager, pgno)) ;
PAGERTRACE(("FETCH %d page %d hash(%08x)\n",
PAGERID(pPager), pgno, pager_pagehash(pPg))) ;
43. https://blogs.msdn.microsoft.com/oldnewthing/20041025-00/?p=37483
810
...
44. Plus d’information sur les commentaires dans les blocs alloués: Dennis Yurichev, C/C++ program-
ming language notes http://yurichev.com/C-book.html
811
Images plus grosses: 1, 2.
Ceci est assez impressionnant, compte tenu du fait que je n’ai aucune information à
propos des types de données de toutes ces structures. Mais je peux en obtenir des
informations.
Si vous utilisez un bloc alloué en mémoire, son adresse doit être présente quelque
part, comme un pointeur dans une structure ou un tableau dans un autre bloc alloué,
ou dans une structure allouée globale, ou dans une variable locale sur la pile. S’il n’y
a plus de pointeur sur un bloc, vous pouvez l’appeler ”orphelin”, et il est une cause
des fuites de mémoire.
Et c’est ce qu’un GC45 fait. Il balaye tous les blocs (car il garde un œil sur tous
les blocs alloués) à la recherche de pointeurs. Il est important de comprendre qu’il
n’a aucune idée du type de données de tous les champs de ces structures dans le
blocs—ceci est important, le GC n’a aucune information sur les types. Il balayage
juste les blocs à la recherche de mots 32-bit ou 64-bit, et regarde s’ils peuvent être
des pointeurs sur d’autres bloc(s). Il balaye aussi la pile. Il traite les blocs alloués
et la pile comme des tableaux de mots, dont certains pourraient être des pointeurs.
Et s’il trouve un bloc alloué, qui est ”orphelin”, i.e., sur lequel aucun autre pointeur
sur lui depuis un autre bloc ou la pile, ce bloc est considéré comme inutile, devant
être libéré. Le processus de balayage prend du temps, et c’est pourquoi les GCs sont
critiqués.
Ainsi, un GC comme celui de Boehm46 (pour du C pur) possède une fonction comme
GC_malloc_atomic()—en l’utilisant, vous déclarez que le bloc alloué avec cette
fonction ne contiendra jamais de pointeur vers un autre bloc. Ça peut être une chaîne
de texte, ou un autre type de donnée. (En effet, GC_strdup() appelle GC_malloc_atomic().)
Le GC ne va pas le balayer.
Au moins MSVC 6.0 de la fin des années 1990 jusqu’à MSVC 2013 peuvent produire
du code vraiment étrange (ce listing est généré par MSVC 2013 x86) :
45. Garbage Collector
46. https://www.hboehm.info/gc/
812
_dst$ = 8 ; taille = 4
_src$ = 12 ; taille = 4
_cnt$ = 16 ; taille = 4
_memcpy PROC
mov edx, DWORD PTR _cnt$[esp-4]
test edx, edx
je SHORT $LN1@f
mov eax, DWORD PTR _dst$[esp-4]
push esi
mov esi, DWORD PTR _src$[esp]
sub esi, eax
; ESI=src-dst, i.e., différence des pointeurs
$LL8@f :
mov cl, BYTE PTR [esi+eax] ; charger l'octet em "esi+dst" ou en
"src-dst+dst" au début ou juste en "src"
lea eax, DWORD PTR [eax+1] ; dst++
mov BYTE PTR [eax-1], cl ; stocker l'octet en "(dst++)--" ou
juste en "dst" au début
dec edx ; décrémenter le compteur jusqu'à ce
que nous ayons fini
jne SHORT $LL8@f
pop esi
$LN1@f :
ret 0
_memcpy ENDP
Ceci est étrange, car comment travaille les humains avec deux pointeurs? Ils stockent
les deux adresses dans deux registres ou deux emplacements mémoire. Dans ce cas,
le compilateur MSVC stocke les deux pointeurs comme un pointeur (dst glissant dans
EAX) et la différence entre les pointeurs src et dst (qui reste inchangée lors de l’exé-
cution du corps de la boucle) dans ESI. (À propos, ceci est un des rare cas où le type
de donnée ptrdiff_t peut être utilisé.) Lorsqu’il doit charger un octet depuis src, il le
charge en diff + dst glissant et stocke l’octet juste en dst glissant.
Ceci est plus une astuce d’optimisation. Mais j’ai récrit cette fonction en:
_f2 PROC
mov edx, DWORD PTR _cnt$[esp-4]
test edx, edx
je SHORT $LN1@f
mov eax, DWORD PTR _dst$[esp-4]
push esi
mov esi, DWORD PTR _src$[esp]
; eax=dst; esi=src
$LL8@f :
mov cl, BYTE PTR [esi+edx]
mov BYTE PTR [eax+edx], cl
dec edx
jne SHORT $LL8@f
pop esi
$LN1@f :
ret 0
_f2 ENDP
813
…et ça fonctionne aussi efficacement que la version optimisée sur mon Intel Xeon
E31220 @ 3.10GHz. Peut-être que cette optimisation ciblait des vieux CPUs x86 des
années 1990, puisque ce truc est utilisé au moins par l’ancien MS VC 6.0?
Une idée?
Hex-Rays 2.2 a du mal à reconnaître des schémas comme ça (avec de la chance,
temporairement?) :
void __cdecl f1(char *dst, char *src, size_t size)
{
size_t counter ; // edx@1
char *sliding_dst ; // eax@2
char tmp ; // cl@3
counter = size ;
if ( size )
{
sliding_dst = dst ;
do
{
tmp = (sliding_dst++)[src - dst]; // difference (src-dst) is
calculated once, at the beginnning
*(sliding_dst - 1) = tmp ;
--counter ;
}
while ( counter ) ;
}
}
Néanmoins, cette astuce d’optimisation est souvent utilisée par MSVC (pas unique-
ment dans des routines memcpy() DIY47 maison, mais dans de nombreuses boucles
qui utilisent deux tableaux ou plus). Donc, ça vaut le coup pour les rétro-ingénieurs
de la garder à l’esprit.
int a[128];
int sum_of_a()
{
int rt=0;
47. Do It Yourself
814
return rt ;
};
int main()
{
// initialize
for (int i=0; i<128; i++)
a[i]=i ;
...
815
comme ceci:
int sum_of_a_v2()
{
int *tmp=a ;
int rt=0;
do
{
rt=rt+(*tmp) ;
tmp++;
}
while (tmp<(a+128)) ;
return rt ;
};
Inutile de dire que cette optimisation n’est possible que si le compilateur peut calcu-
ler l’adresse de la fin du tableau pendant la compilation. Ceci se produit si le tableau
est global et sa taille fixée.
Toutefois, si l’adresse du tableau est inconnue lors de la compilation, mais que la
taille est fixée, l’adresse du label juste après la fin du tableau peut être calculée.
816
3.25 Plus sur les structures
3.25.1 Parfois une structure C peut être utilisée au lieu d’un
tableau
Moyenne arithmétique
#include <stdio.h>
struct five_ints
{
int a0 ;
int a1 ;
int a2 ;
int a3 ;
int a4 ;
};
int main()
{
struct five_ints a ;
a.a0=123;
a.a1=456;
a.a2=789;
a.a3=10;
a.a4=100;
printf ("%d\n", mean(&a, 5)) ;
// test:
https://www.wolframalpha.com/input/?i=mean(123,456,789,10,100)
};
#include <stdio.h>
struct five_chars
{
char a0 ;
char a1 ;
char a2 ;
char a3 ;
817
char a4 ;
} __attribute__ ((aligned (1),packed)) ;
int main()
{
struct five_chars a ;
a.a0='h' ;
a.a1='i' ;
a.a2=' !' ;
a.a3='\n' ;
a.a4=0;
printf (&a) ; // prints "hi!"
};
L’attribut ((aligned (1),packed)) doit être utilisé, car sinon, chaque champ de la struc-
ture sera aligné sur une limite de 4 ou 8 octets.
Résumé
Ceci est simplement un autre exemple de la façon dont les structures et les tableaux
sont stockés en mémoire. Peut-être qu’aucun programmeur sain ne ferait quelque
chose comme dans cet exemple, excepté dans le cas d’astuces spécifiques. Ou peut-
être dans le cas d’obfuscation de code source?
...
ULONG MaxNameLen ;
TCHAR Name[1];
} SYMBOL_INFO, *PSYMBOL_INFO ;
( https://msdn.microsoft.com/en-us/library/windows/desktop/ms680686(v=
vs.85).aspx )
Ceci est une astuce, signifiant que le dernier champ est un tableau de taille inconnue,
qui doit être calculée lors de l’allocation de la structure.
Pourquoi: le champ Name peut-être court, donc pourquoi le définir avec une sorte
de constante MAX_NAME qui peut être 128, 256 et même plus ?
Pourquoi ne pas utiliser un pointeur à la place ? Alors vous devez allouer deux blocs:
un pour la structure et pour la chaîne. Ceci peut-être plus lent et peut nécessiter un
plus large surplus de mémoire. Donc, vous devez déréférencer le pointeur (i.e., lire
818
l’adresse de la chaîne dans la structure)—ce n’est pas un problème, mais certains
disent que c’est un coût supplémentaire.
Ceci est connu comme l’astuce struct : http://c-faq.com/struct/structhack.
html.
Exemple:
#include <stdio.h>
struct st
{
int a ;
int b ;
char s[];
};
int main()
{
#define STRING "Hello !"
struct st *s=malloc(sizeof(struct st)+strlen(STRING)+1) ; // incl.
terminating zero
s->a=1;
s->b=2;
strcpy (s->s, STRING) ;
f(s) ;
};
En quelques mots, ça fonctionne car le C n’a pas de vérification des bornes d’un
tableau. Tout tableau est traité comme ayant une taille infinie.
Problème: après l’allocation, la taille entière du bloc alloué pour la structure est incon-
nue (excepté pour le gestionnaire de mémoire), donc vous ne pouvez pas remplacer
une chaîne par un chaîne plus large. Vous pourriez faire quelque chose comme ça si
le champ était déclaré comme quelque chose comme s[MAX_NAME].
Autrement dit, vous avez une structure plus un tableau (ou une chaîne) fusionnés
ensemble dans un bloc de mémoire alloué unique. Un autre problème est que vous
ne pouvez évidemment pas déclarer deux tableaux comme ceci dans une structure
unique, ou déclarer un autre champ après un tel tableau.
Les cieux compilateurs nécessitent de déclarer le tableau avec au moins un élé-
ment: s[1], les plus récents permettent de le déclarer comme un tableau de taille
variable:s[]. Ceci est aussi appelé membre tableau flexible.
En lire plus à ce sujet dans GCC documentation48 , MSDN documentation49 .
48. https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html
49. https://msdn.microsoft.com/en-us/library/b6fae073.aspx
819
Dennis Ritchie (un des créateurs du C) a appelé ce truc «amabilité non voulue avec
l’implémentation du C » (peut-être pour reconnaître la nature astucieuse de cette
ruse).
Aimez le ou non, utilisez le ou non: il est encore une autre démonstration de la façon
dont les structures sont stockées dans la mémoire, c’est pourquoi j’en ai parlé.
( https://msdn.microsoft.com/en-us/library/windows/desktop/ms680686(v=
vs.85).aspx )
En effet, certaines structures comme SYMBOL_INFO commencent en effet avec ce
champ. Pourquoi ? C’est une sorte de version de structure.
Imaginez que vous avez une fonction qui dessine un cercle. Elle prend un unique
argument—un pointeur sur une structure avec seulement trois champs: X, Y et radius.
Et puis, les affichages couleurs ont inondé le marché durant les années 1980. Et vous
voulez ajouter un argument color à la fonction. Mais, disons que vous ne pouvez pas
lui ajouter un argument (de nombreux logiciels utilisent votre API50 et ne peuvent pas
être recompilés). Et si un vieux logiciels utilise votre API avec un affichage couleur,
faites que votre fonction dessine un cercle avec par défaut les couleurs noire et
blanche.
Un autre jour, vous ajoutez une autre possibilité: le cercle peut maintenant être
rempli, et le type de brosse peut être passé.
Voici une solution à ce problème:
#include <stdio.h>
struct ver1
{
size_t SizeOfStruct ;
int coord_X ;
int coord_Y ;
int radius ;
};
struct ver2
{
size_t SizeOfStruct ;
int coord_X ;
int coord_Y ;
int radius ;
int color ;
820
};
struct ver3
{
size_t SizeOfStruct ;
int coord_X ;
int coord_Y ;
int radius ;
int color ;
int fill_brush_type ; // 0 - do not fill circle
};
if (s->SizeOfStruct>=sizeof(int)*5)
{
// this is at least ver2, color field is present
printf ("We are going to set color %d\n", s->color) ;
}
if (s->SizeOfStruct>=sizeof(int)*6)
{
// this is at least ver3, fill_brush_type field is present
printf ("We are going to fill it using brush type %d\n", s⤦
Ç ->fill_brush_type) ;
}
};
821
printf ("** %s()\n", __FUNCTION__) ;
draw_circle(&s) ;
};
int main()
{
call_as_ver1() ;
call_as_ver2() ;
call_as_ver3() ;
};
Autrement dit, le champ SizeOfStruct prend le rôle d’un champ version of structure.
Il pourrait être un type énuméré (1, 2, 3, etc.), mais mettre le champ SizeOfStruct à si-
zeof(struct...) est moins sujet à l’erreur: nous écrivons simplement s.SizeOfStruct=sizeof(...)
dans le code de l’appelant.
En C++, ce problème est résolu en utilisant l’hérutage ( 3.21.1 on page 722). Vous
avez seulement à étendre la classe de base (appelons la Circle), puis vous aurez
une classe ColoredCircle, et ensuite FilledColoredCircle, et ainsi de suite. La version
courante d’un objet (ou, plus précisemment, le type courant) sera déterminé en
utilisant la RTTI de C++.
Donc lorsque vous voyez SizeOfStruct quelque part dans MSDN—peut-être que cette
structure a été étendue au moins une fois par le passé.
822
Fig. 3.4: Table des meilleurs scores
Maintenant, nous pouvons voir que le fichier qui a changé après que nous ayons
ajouté notre nom est BLSCORE.DAT.
% xxd -g 1 BLSCORE.DAT
00000000: 0a 00 58 65 6e 69 61 2e 2e 2e 2e 2e 00 df 01 00 ..Xenia.........
00000010: 00 30 33 2d 32 37 2d 32 30 31 38 00 50 61 75 6c .03-27-2018.Paul
00000020: 2e 2e 2e 2e 2e 2e 00 61 01 00 00 30 33 2d 32 37 .......a...03-27
00000030: 2d 32 30 31 38 00 4a 6f 68 6e 2e 2e 2e 2e 2e 2e -2018.John......
00000040: 00 46 01 00 00 30 33 2d 32 37 2d 32 30 31 38 00 .F...03-27-2018.
00000050: 4a 61 6d 65 73 2e 2e 2e 2e 2e 00 44 01 00 00 30 James......D...0
00000060: 33 2d 32 37 2d 32 30 31 38 00 43 68 61 72 6c 69 3-27-2018.Charli
00000070: 65 2e 2e 2e 00 ea 00 00 00 30 33 2d 32 37 2d 32 e........03-27-2
00000080: 30 31 38 00 4d 69 6b 65 2e 2e 2e 2e 2e 2e 00 b5 018.Mike........
00000090: 00 00 00 30 33 2d 32 37 2d 32 30 31 38 00 50 68 ...03-27-2018.Ph
000000a0 : 69 6c 2e 2e 2e 2e 2e 2e 00 ac 00 00 00 30 33 2d il⤦
Ç ...........03-
000000b0 : 32 37 2d 32 30 31 38 00 4d 61 72 79 2e 2e 2e 2e 27-2018.Mary⤦
Ç ....
000000c0 : 2e 2e 00 7b 00 00 00 30 33 2d 32 37 2d 32 30 31 ⤦
Ç ...{...03-27-201
000000d0 : 38 00 54 6f 6d 2e 2e 2e 2e 2e 2e 2e 00 77 00 00 8.Tom........w⤦
Ç ..
000000e0 : 00 30 33 2d 32 37 2d 32 30 31 38 00 42 6f 62 2e .03-27-2018.Bob⤦
Ç .
000000f0 : 2e 2e 2e 2e 2e 2e 00 77 00 00 00 30 33 2d 32 37 .......w⤦
Ç ...03-27
00000100: 2d 32 30 31 38 00 -2018.
Toutes les entrées sont clairement visibles. Le premier octet est probablement le
nombre d’entrées. Le second est zéro, en fait, le nombre d’entrées peut-être une
823
valeurs 16-bit couvrant les deux premiers octets.
Ensuite, après le nom «Xenia », nous voyons les octets 0xDF et 0x01. Xenia a un
score de 479, et ceci est exactement 0x1DF en hexadécimal. Donc une valeur de
score est probablement un entier 16-bit, ou un entier 32-bit: il y a deux octets à zéro
de plus après.
Maintenant, pensons au fait qu’à la fois les éléments des tableaux et des structures
sont toujours placés en mémoire de manière adjacente les uns aux autres. Cela
nous permet d’écrire le tableau/la structure entièrement dans le fichier en utilisant
une fonction unique write() ou fwrite(), et de le restaurer en utilisant read() ou fread(),
aussi simplement que ça. Ceci est ce qui est appelé sérialisation de nos jours.
Lire
struct entry
{
char name[11]; // incl. terminating zero
uint32_t score ;
char date[11]; // incl. terminating zero
} __attribute__ ((aligned (1),packed)) ;
struct highscore_file
{
uint8_t count ;
uint8_t unknown ;
struct entry entries[10];
} __attribute__ ((aligned (1), packed)) ;
824
};
Nous avons besoin de l’attribut ((aligned (1),packed)) de GCC afin que tous les
champs de la structure soient alignés sur une limite de 1-octet.
Bien sûr, il fonctionne:
name=Xenia..... score=479 date=03-27-2018
name=Paul...... score=353 date=03-27-2018
name=John...... score=326 date=03-27-2018
name=James..... score=324 date=03-27-2018
name=Charlie... score=234 date=03-27-2018
name=Mike...... score=181 date=03-27-2018
name=Phil...... score=172 date=03-27-2018
name=Mary...... score=123 date=03-27-2018
name=Tom....... score=119 date=03-27-2018
name=Bob....... score=119 date=03-27-2018
(Inutile de dire que chaque nom est complété avec des points, à la fois à l’écran et
dans le fichier, peut-être pour des raisons esthétique.)
Écrire
f=fopen(argv[1], "wb") ;
assert (f !=NULL) ;
got=fwrite(&file, 1, sizeof(struct highscore_file), f) ;
assert (got==sizeof(struct highscore_file)) ;
fclose(f) ;
};
Lançons Blockout:
825
Fig. 3.5: Table des meilleurs scores
Les deux premiers chiffres (1 et 2) ne sont pas affichés: 12345678 devient 345678.
Peut-être est-ce un problème de formatage... mais le nombre est presque correct.
Maintenant, je le change en 999999 et relance le jeu:
Est-ce de la sérialisation?
…presque. Ce genre de sérialisation est très populaire dans les logiciels scientifiques
et d’ingénierie, où l’efficacité et la rapidité sont bien plus importantes que de conver-
tir de et vers XML52 ou JSON53 .
52. Extensible Markup Language
53. JavaScript Object Notation
826
Une chose importante est que vous ne pouvez évidemment pas sérialiser des poin-
teurs, car à chaque fois que vous chargez le programme en mémoire, toutes les
structures peuvent être allouées à des endroits différents.
Mais, si vous travaillez sur des sortes de MCU à bas coût avec un simple OS dessus
et que vous avez vos structures toujours allouées à la même place en mémoire,
peut-être pouvez-vous sauver et restaurer de la sorte.
Bruit aléatoire
Lorsque je préparais cet exemple, j’ai dû lancer «Block out » de nombreuses fois
et jouer un peu avec pour remplir la table des meilleurs scores avec des noms au
hasard.
Et lorsqu’il y avait seulement 3 entrées dans le fichier, j’ai vu ceci:
00000000: 03 00 54 6f 6d 61 73 2e 2e 2e 2e 2e 00 da 2a 00 ..Tomas.......*.
00000010: 00 30 38 2d 31 32 2d 32 30 31 36 00 43 68 61 72 .08-12-2016.Char
00000020: 6c 69 65 2e 2e 2e 00 8b 1e 00 00 30 38 2d 31 32 lie........08-12
00000030: 2d 32 30 31 36 00 4a 6f 68 6e 2e 2e 2e 2e 2e 2e -2016.John......
00000040: 00 80 00 00 00 30 38 2d 31 32 2d 32 30 31 36 00 .....08-12-2016.
00000050: 00 00 57 c8 a2 01 06 01 ba f9 47 c7 05 00 f8 4f ..W.......G....O
00000060: 06 01 06 01 a6 32 00 00 00 00 00 00 00 00 00 00 .....2..........
00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000a0 : 00 00 00 00 00 00 00 00 00 00 93 c6 a2 01 46 72 ..............⤦
Ç Fr
000000b0 : 8c f9 f6 c5 05 00 f8 4f 00 02 06 01 a6 32 06 01 .......O⤦
Ç .....2..
000000c0 : 00 00 98 f9 f2 c0 05 00 f8 4f 00 02 a6 32 a2 f9 .........O⤦
Ç ...2..
000000d0 : 80 c1 a6 32 a6 32 f4 4f aa f9 39 c1 a6 32 06 01 ...2.2.O⤦
Ç ..9..2..
000000e0 : b4 f9 2b c5 a6 32 e1 4f c7 c8 a2 01 82 72 c6 f9 ..+..2.O.....r⤦
Ç ..
000000f0 : 30 c0 05 00 00 00 00 00 00 00 a6 32 d4 f9 76 2d 0..........2..v⤦
Ç -
00000100: a6 32 00 00 00 00 .2....
827
Ceci est un problème courant. Pas un problème au sens strict: ce n’est pas un bogue,
mais de l’information peut fuiter à l’extérieur.
Les versions de Microsoft Word des années 1990 laissaient souvent des morceaux
de texte précédemment édité dans les fichiers *.doc*. C’était alors une sorte de dis-
traction d’obtenir un fichier .doc de quelqu’un d’autre, de l’ouvrir dans un éditeur
hexadécimal et de lire d’autres choses, qui avaient été éditées avant sur cet ordina-
teur.
Le problème peut être beaucoup plus sérieux: le bogue Heartbleed dans OpenSSL.
Devoir
memcpy() qui copie des mots de 32-bit ou de 64-bit à la fois, ou même SIMD, va
manifestement échouer ici, une routine de copie octet par octet doit être utilisée à
la place.
Maintenant un exemple encore plus avancé, insérer deux octets au début d’une
chaîne:
`|h|e|l|l|o|...` -> `|.|.|h|e|l|l|o|...`
Maintenant, même une copie octet par octet va échouer, car vous devez copier en
partant de la fin.
C’est un cas rare où le flag x86 DF doit être mis avant l’instruction REP MOVSB : DF
défini la direction, et maintenant, nous devons déplacer en arrière.
La routine memmove() typique fonctionne comme ceci: 1) si la source est avant la
destination, copier en avant; 2) si la source est après la destination, copier en arrière.
Ceci est la fonction memmove() de uClibc:
void *memmove(void *dest, const void *src, size_t n)
{
828
int eax, ecx, esi, edi ;
__asm__ __volatile__(
" movl %%eax, %%edi\n"
" cmpl %%esi, %%eax\n"
" je 2f\n" /* (optional) src == dest -> NOP */
" jb 1f\n" /* src > dest -> simple copy */
" leal -1(%%esi,%%ecx), %%esi\n"
" leal -1(%%eax,%%ecx), %%edi\n"
" std\n"
"1: rep ; movsb\n"
" cld\n"
"2:\n"
: "=&c" (ecx), "=&S" (esi), "=&a" (eax), "=&D" (edi)
: "0" (n), "1" (src), "2" (dest)
: "memory"
);
return (void*)eax ;
}
Dans le premier cas, REP MOVSB est appelée avec le flag DF à zéro. Dans le second,
DF est mis, puis remis à zéro.
Un algorithme plus complexe contient la logique suivante:
«Si la différence entre la source et la destination est plus grande que la largeur d’un
mot, copier en utilisant des mots plutôt que des octets, et utiliser une copie octet
par octet pour copier les parties non alignées. »
Voici comment ça se passe dans la partie C non optimisée de la Glibc 2.24.
Compte tenu de cela, memmove() peut être plus lente que memcpy(). Mais certains,
Linus Torvalds inclus, argumentent54 que memcpy() devrait être un alias (ou syno-
nyme) de memmove(), et cette dernière fonction devrait juste tester au début, si les
buffers se recouvrent ou non, et ensuite se comporter comme memcpy() ou mem-
move(). De nos jours, le test de recouvrement de buffers est peu coûteux, après
tout.
829
3.27 setjmp/longjmp
Il s’agit d’un mécanisme en C qui est très similaire au mecanisme throw/catch en
C++ ou d’autres LPs de haut niveau. Voici un exemple tiré de la zlib:
...
...
...
if (s->left == 0) {
s->left = s->infun(s->inhow, &(s->in)) ;
if (s->left == 0) longjmp(s->env, 1) ; /* out of input */
( zlib/contrib/blast/blast.c )
L’appel à setjmp() sauve les valeurs courantes de PC, SP et autres registres dans
une structure env, puis renvoie 0.
En cas d’erreur, longjmp() vous téléporte au point juste après l’appel à setjmp(),
comme si l’appel à setjmp() avait renvoyé une valeur non nulle (qui avait été passée
à longjmp()). Ceci nous rappel l’appel système fork() sous UNIX.
Maintenant, regardons un exemple épuré:
#include <stdio.h>
#include <setjmp.h>
jmp_buf env ;
void f2()
{
printf ("%s() begin\n", __FUNCTION__) ;
// something odd happened here
longjmp (env, 1234) ;
printf ("%s() end\n", __FUNCTION__) ;
};
830
void f1()
{
printf ("%s() begin\n", __FUNCTION__) ;
f2() ;
printf ("%s() end\n", __FUNCTION__) ;
};
int main()
{
int err=setjmp(env) ;
if (err==0)
{
f1() ;
}
else
{
printf ("Error %d\n", err) ;
};
};
...
831
movdqa xmmword ptr [rcx+90h], xmm9
movdqa xmmword ptr [rcx+0A0h], xmm10
movdqa xmmword ptr [rcx+0B0h], xmm11
movdqa xmmword ptr [rcx+0C0h], xmm12
movdqa xmmword ptr [rcx+0D0h], xmm13
movdqa xmmword ptr [rcx+0E0h], xmm14
movdqa xmmword ptr [rcx+0F0h], xmm15
retn
Cela remplit juste la structure jmp_buf avec la valeur courante de presque tous les
registres. Aussi, la valeur courante de RA est prise de la pile et sauvée dans jmp_buf:
elle sera utilisée comme nouvelle valeur de PC dans le futur.
Maintenant longjmp() :
...
...
Cela restaure (presque) tous les registres, prend RA dans la structure et y saute.
Ceci fonctionne en effet comme si setjmp() retournait à l’appelant. Aussi, RAX est
mis pour être égal au second argument de longjmp(). Ceci fonctionne comme si
setjmp() renvoyait une valeur non-zéro en première place.
832
Comme effet de bord de la restauration de SP, toutes les valeurs dans la pile qui ont
été définies et utilisées entre les appels à setjmp() et longjmp() sont laissées tomber.
Elles ne seront plus utilisées du tout. Ainsi, longjmp() saute usuellement en arrière
55
.
Ceci implique que, contrairement au mécanisme throw/catch en C++, aucune mé-
moire ne sera libérée, aucun destructeur ne sera appelé, etc. Ainsi, cette technique
peut parfois être dangereuse. Néanmoins, elle est assez populaire. C’est toujours
utilisé dans Oracle RDBMS.
Cela a aussi un effet de bord inattendu: si un buffer a été dépassé dans une des
fonctions (peut-être à cause d’une attaque distante), et qu’une fonction veut signaler
une erreur, et ça appelle longjmp(), la partie de la pile récrite ne sera pas utilisée.
À titre d’exercice, vous pouvez essayer de comprendre pourquoi tous les registres
ne sont pas sauvegardés. Pourquoi XMM0-XMM5 et d’autres registres sont évités?
int main()
{
printf ("address of main()=0x%x\n", &main) ;
55. Toutefois, il y a des gens qui l’utilisent pour des choses bien plus compliquées, imitation des corou-
tines, etc.: https://www.embeddedrelated.com/showarticle/455.php, http://fanf.livejournal.
com/105413.html
833
printf ("address of draw_text()=0x%x\n", &draw_text) ;
draw_text(100, 200, "Hello !") ;
};
834
{
printf ("found\n") ;
*(tmp+1)=210; // change 200 to 210
break ;
};
tmp++;
};
};
Hé mais, ça fonctionne:
found
We are going to draw [Hello !] at 100:210
Résumé
C’est vraiment un sale hack, dont le but est de montrer l’intérieur de la pile. Je n’ai
jamais vu ni entendu dire que quelqu’un ai utilisé ceci dans du code réel. Mais encore,
ceci est un bon exemple.
Exercice
L’exemple a été compilé sans optimisation sur Ubuntu 32-bit avec GCC 5.4.0 et il
fonctionne. Mais lorsque j’active l’optimisation maximum -O3, ça plante. Essayez de
trouver pourquoi.
Utilisez votre compilateur et OS favori, essayez différents niveaux d’optimisation,
trouvez si ça fonctionne et si ça ne fonctionne pas, trouvez pourquoi.
return buf ;
};
int main()
{
printf ("%s\n", amsg (1234, "something wrong !")) ;
};
835
Il va planter. Tout d’abord essayons de comprendre pourquoi.
Ceci est l’état de la pile avant le retour de amsg() :
(lower addresses)
...
...
(upper addresses)
Ensuite amsg() rend le contrôle du flux à main(), jusqu’ici, tout va bien. Mais printf()
est appelée depuis main(), qui, en fait, utilise la pile pour ses propres besoin, zap-
pant le buffer de 100-octet. Au mieux, du contenu indéterminé sera affiché.
Difficile à croire, mais je sais comment résoudre ce problème:
#include <stdio.h>
return buf ;
};
int main()
{
printf ("%s\n", interim (1234, "something wrong !")) ;
};
Cela va fonctionner si il est compilé avec MSVC 2013 sans optimisation et avec
l’option /GS- option56 . MSVC avertira: “warning C4172: returning address of local
variable or temporary”, mais le code s’exécutera et le message sera affiché. Regar-
dons l’état de la pile au moment où amsg() renvoie le contrôle à interim() :
56. Supprimer la vérification de sécurité du buffer
836
(lower addresses)
...
...
(upper addresses)
...
...
(upper addresses)
Donc lorsque main() appelle printf(), elle utilise l’espace de pile où le buffer d’in-
terim() était alloué, et ne zappe pas les 100 octets contenant le message d’erreur,
car 8000 octets (ou peut-être bien moins) sont suffisants pour tout ce que printf()
et les autres fonctions font!
Ça pourrait aussi fonctionner si il y a plusieurs fonctions entre, comme: main() →
f1() → f2() → f3() ... → amsg(), et alors le résultat de amsg() est utilisé dans main().
La distance entre SP dans main() et l’adresse de buf[] doit être assez grande.
C’est pourquoi les bugs de ce genre sont dangereux: parfois votre code fonctionne
(et le bug ne se produit pas), parfois non. Ces genres de bug sont par humour
appelés heisenbugs ou schrödinbugs.
3.29 OpenMP
OpenMP est l’un des moyens les plus simple de paralléliser des algorithmes simples.
À titre d’exemple, essayons de construire un programme pour calculer une nonce
cryptographique.
837
Dans mon exemple simpliste, le nonce est un nombre ajouté au texte non chiffré
afin de produire un hash avec quelques caractéristiques spécifiques.
Par exemple, à certaines étapes, le protocole Bitcoin nécessite de trouver de tels
nonce dont le hash résultant contient un nombre spécifique de zéros consécutifs.
Ceci est aussi appelé «preuve de travail » 57 (i.e., le système prouve qu’il a fait des
calculs intensifs et y a passé du temps).
Mon exemple n’est en aucun cas lié au Bitcoin, il va essayer d’ajouter des nombres
à la chaîne afin de trouver un nombre tel que le hash de «hello, world!_<number> »
avec l’algorithme SHA512, contiendra au moins 3 octets à zéro.
Limitons notre recherche brute-force dans l’intervalle 0..INT32_MAX-1 (i.e., 0x7FFFFFFE
ou 2147483646).
L’algorithme est assez direct:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "sha512.h"
int found=0;
int32_t checked=0;
int32_t* __min ;
int32_t* __max ;
time_t start ;
#ifdef __GNUC__
#define min(X,Y) ((X) < (Y) ? (X) : (Y))
#define max(X,Y) ((X) > (Y) ? (X) : (Y))
#endif
// update statistics
int t=omp_get_thread_num() ;
if (__min[t]==-1)
__min[t]=nonce ;
if (__max[t]==-1)
__max[t]=nonce ;
__min[t]=min(__min[t], nonce) ;
__max[t]=max(__max[t], nonce) ;
57. Wikipédia
838
// idle if valid nonce found
if (found)
return ;
sha512_init_ctx (&ctx) ;
sha512_process_bytes (buf, strlen(buf), &ctx) ;
sha512_finish_ctx (&ctx, &res) ;
if (res[0]==0 && res[1]==0 && res[2]==0)
{
printf ("found (thread %d) : [%s]. seconds spent=%d\n", t, ⤦
Ç buf, time(NULL)-start) ;
found=1;
};
#pragma omp atomic
checked++;
int main()
{
int32_t i ;
int threads=omp_get_max_threads() ;
printf ("threads=%d\n", threads) ;
__min=(int32_t*)malloc(threads*sizeof(int32_t)) ;
__max=(int32_t*)malloc(threads*sizeof(int32_t)) ;
for (i=0; i<threads ; i++)
__min[i]=__max[i]=-1;
start=time(NULL) ;
free(__min) ; free(__max) ;
};
839
for (i=0; i<INT32_MAX ; i++)
check_nonce (i) ;
Oui, c’est simple, sans le #pragma nous appelons check_nonce() pour chaque nombre
de 0 à INT32_MAX (0x7fffffff ou 2147483647). Avec le #pragma, le compilateur
ajoute du code particulier qui découpe l’intervalle de la boucle en des plus petits,
afin de les lancer sur tous les cœurs de CPU disponible58 .
L’exemple peut être compilé59 dans MSVC 2012:
cl openmp_example.c sha512.obj /openmp /O1 /Zi /Faopenmp_example.asm
Ou dans GCC:
gcc -fopenmp 2.c sha512.c -S -masm=intel
3.29.1 MSVC
Maintenant voici comment MSVC 2012 génère la boucle principale:
Toutes les fonctions préfixées par vcomp sont relatives à OpenMP et sont stockées
dans le fichier vcomp*.dll. Donc, ici un groupe de threads est démarré.
Regardons _main$omp$1 :
58. N.B.: Ceci est intentionnellement l’exemple le plus simple possible, mais en pratique, l’utilisation
de OpenMP peut être plus difficile et plus complexe.
59. Les fichiers sha512.(c|h) et u64.h peuvent être pris de la bibliothèque OpenSSL: http://www.
openssl.org/source/
840
push 2147483646 ; 7ffffffeH
push 0
call __vcomp_for_static_simple_init
mov esi, DWORD PTR $T1[ebp]
add esp, 24
jmp SHORT $LN6@main$omp$1
$LL2@main$omp$1 :
push esi
call _check_nonce
pop ecx
inc esi
$LN6@main$omp$1 :
cmp esi, DWORD PTR $T2[ebp]
jle SHORT $LL2@main$omp$1
call __vcomp_for_static_end
pop esi
leave
ret 0
_main$omp$1 ENDP
Oui, le résultat est correct, les 3 premiers octets sont des zéros:
841
C :\...\sha512sum test
000000f4a8fac5a4ed38794da4c1e39f54279ad5d9bb3c5465cdf57adaf60403
df6e3fe6019f5764fc9975e505a7395fed780fee50eb38dd4c0279cb114672e2 *test
Le temps de traitement est ≈ 2..3 secondes sur un Intel Xeon E3-1220 3.10 GHz 4-
core. Dans le gestionnaire de tâches nous voyons 5 threads: 1 thread principal + 4
autres. Il n’y a pas d’optimisations faites afin de garder cet exemple aussi petit et
clair que possible. Mais probablement qu’on pourrait le rendre plus rapide. Mon CPU
a 4 cœurs, c’est pourquoi OpenMP a démarré exactement 4 threads.
En regardant la table des statistiques, nous voyons clairement comment la boucle
a été découpée en 4 parties égales. Oui bon, presque égales, si nous ne tenons pas
compte du dernier bit.
Il y a aussi des pragmas pour les operations atomiques..
Voyons comment ce code est compilé:
#pragma omp atomic
checked++;
842
Il semble que la fonction vcomp_atomic_add_i4() dans vcomp*.dll soit juste une
minuscule fonction avec l’instruction LOCK XADD60 dedans.
vcomp_enter_critsect() appelle finalement la fonction de l’API win32
EnterCriticalSection() 61 .
3.29.2 GCC
GCC 4.8.1 produit un programme qui montre exactement la même table de statis-
tique,
donc, l’implémentation de GCC divise la boucle en parties de la même manière.
843
add eax, edx
lea ebx, [rax+rcx]
cmp eax, ebx
jge .L14
mov DWORD PTR [rbp-20], eax
.L17 :
mov eax, DWORD PTR [rbp-20]
mov edi, eax
call check_nonce
add DWORD PTR [rbp-20], 1
cmp DWORD PTR [rbp-20], ebx
jl .L17
jmp .L14
.L15 :
mov eax, 0
add ecx, 1
jmp .L18
.L14 :
add rsp, 40
pop rbx
pop rbp
ret
844
call printf
.L7 :
call GOMP_critical_end
Les fonctions préfixées par GOMP sont de la bibliothèque GNU OpenMP. Contraire-
ment à vcomp*.dll, son code source est librement disponible: GitHub.
Si vous divisez par 4, il faut ajouter 3 à la valeur en entrée si elle est négative. Donc
ceci est ce que GCC 4.8 génère pour x/4 :
845
lea eax, [rdi+3] ; préparer la valeur x+3 en avance
test edi, edi
La division par 8 dans MSVC 2013 est similaire, mais 3 bits de EDX sont pris au lieu
de 2, produisant une correction de 7 au lieu de 3.
Parfois, Hex-Rays 6.8 ne gère pas correctement un tel code, et il peut produire
quelque chose comme ceci:
int v0 ;
...
__int64 v14
846
...
v14 = ...;
v0 = ((signed int)v14 - HIDWORD(v14)) >> 1;
En outre, une telle correction est souvent utilisé lorsque la division est remplacée par
la multiplication par des nombres magiques : lire Mathematics for Programmers62 à
propos de la multiplication inverse. Et parfois, un décalage additionnel est utilisé
x
après la multiplication. Par exemple, lorsque GCC optimise 10 , il ne peut pas trouver
la multiplication inverse pour 10, car l’équation diophantienne n’a pas de solution.
Donc il génère du code pour x5 et puis ajoute une opération de décalage arithmétique
à droite de 1 bit, pour diviser le résultat par 2. Bien sûr, ceci est seulement vrai pour
les entiers signés.
Donc, voici la division par 10 de GCC 4.8:
mov eax, edi
mov edx, 1717986919 ; nombre magique
sar edi, 31 ; isoler le bit le plus à gauche (qui
reflète le signe)
imul edx ; multiplication par le nombre magique (calculer
x/5)
sar edx, 2 ; mainenant calculer (x/5)/2
847
#include <stdio.h>
int array1[128];
int important_var1 ;
int important_var2 ;
int important_var3 ;
int important_var4 ;
int important_var5 ;
int main()
{
important_var1=1;
important_var2=2;
important_var3=3;
important_var4=4;
important_var5=5;
array1[0]=123;
array1[128]=456; // BUG
Ceci est ce que ce programme a affiché dans mon cas (GCC 5.4 x86 sans optimisation
sur Linux) :
important_var1=1
important_var2=456
important_var3=3
important_var4=4
important_var5=5
Lorsque ça se produit, important_var2 avait été mise par le compilateur juste après
array1[] :
D’autres compilateurs peuvent arranger les variables dans un autre ordre, et une
autre variable sera écrasée. Ceci est aussi un heisenbug ( 3.28.2 on page 837)—
848
bug qui peut se produire ou passer inaperçu suivant la version du compilateur et les
options d’optimisation.
Si toutes les variables et tableaux sont allouées sur la pile locale, la protection de la
pile peut être déclenchée, ou pas. Toutefois, Valgrind peut trouver ce genre de bugs.
Un exemple connexe dans le livre (jeu Angband) : 1.27 on page 388.
struct color
{
int R ;
int G ;
int B ;
};
rt->R=R ;
rt->G=G ;
rt->B=B ;
// must be "return rt;" here
};
int main()
{
struct color* a=create_color(1,2,3) ;
printf ("%d %d %d\n", a->R, a->G, a->B) ;
};
849
sub rsp, 32
mov DWORD PTR [rbp-20], edi
mov DWORD PTR [rbp-24], esi
mov DWORD PTR [rbp-28], edx
mov edi, 12
call malloc
; RAX = pointer to newly allocated buffer
; now fill it with R/G/B:
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rbp-20]
mov DWORD PTR [rax], edx
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rbp-24]
mov DWORD PTR [rax+4], edx
mov rax, QWORD PTR [rbp-8]
mov edx, DWORD PTR [rbp-28]
mov DWORD PTR [rax+8], edx
nop
leave
; RAX hasn't been modified till that point!
ret
850
Des bogues de ce type sont très dangereux, parfois ils apparaissent, parfois ils res-
tent invisibles.
Maintenant, j’essaye GCC avec l’optimisation:
main :
xor eax, eax
; as if create_color() was called and returned 0
sub rsp, 8
mov r8d, DWORD PTR ds :8
mov ecx, DWORD PTR [rax+4]
mov edx, DWORD PTR [rax]
mov esi, OFFSET FLAT :.LC1
mov edi, 1
call __printf_chk
xor eax, eax
add rsp, 8
ret
851
mov eax, DWORD PTR _G$[ebp]
; EAX is set to G argument:
mov DWORD PTR [edx+4], eax
mov ecx, DWORD PTR _rt$[ebp]
mov edx, DWORD PTR _B$[ebp]
mov DWORD PTR [ecx+8], edx
mov esp, ebp
pop ebp
; EAX = G at this point:
ret 0
_create_color ENDP
Maintenant MSVC 2015 x86 avec optimisation, qui génère du code qui plante aussi
mais pour une raison différente.
_R$ = 8
_G$ = 12
_B$ = 16
_create_color PROC
push 12
call _malloc
mov ecx, DWORD PTR _R$[esp]
add esp, 4
mov DWORD PTR [eax], ecx
mov ecx, DWORD PTR _G$[esp-4]
mov DWORD PTR [eax+4], ecx
mov ecx, DWORD PTR _B$[esp-4]
mov DWORD PTR [eax+8], ecx
852
; EAX points to allocated buffer, OK
ret 0
_create_color ENDP
Toutefois, MSVC 2015 x64 sans optimisation génère du code qui fonctionne:
MSVC 2015 x64 avec optimisation met le fonction en ligne, comme dans le cas du
x86, et le code résultant plante.
La morale de l’histoire: les warnings sont très importants, utilisez -Wall, etc, etc...
Lorsque la déclaration return est absente, le compilateur peut simplement silen-
cieusement ne rien faire à ce point.
853
Un tel bug passé inaperçu peut gâcher une journée.
Aussi, le débogage shotgun est mauvais, car encore une fois, un tel bogue peut
passer inaperçu (“tout fonctionne maintenant, qu’il en soit ainsi”).
typedef union {
int i ;
unsigned int ui ;
float f ;
const void *v ;
} Arg ;
...
typedef struct {
unsigned int mod ;
KeySym keysym ;
void (*func)(const Arg *) ;
const Arg arg ;
} Key ;
...
63. https://dwm.suckless.org/
854
{ MODKEY, XK_t, setlayout, {.v = &layouts[0]} },
{ MODKEY, XK_f, setlayout, {.v = &layouts[1]} },
{ MODKEY, XK_m, setlayout, {.v = &layouts[2]} },
...
void
spawn(const Arg *arg)
{
...
void
focusstack(const Arg *arg)
{
...
Pour chaque touche frappée (ou raccourci), une fonction est définie. Encore mieux:
un paramètre (ou argument) peut être passé à une fonction dans chaque cas. Mais
les paramètres peuvent avoir des types variés. Donc une union est utilisée ici. Une
valeur du type requis est mise dans la table. Chaque fonction prend ce dont elle a
besoin.
À titre d’exercice, essayez d’écrire un code comme cela, ou plongez-vous dans dwm
et voyez comment l’union est passée aux fonctions et gérée.
3.34.1 Exemple#1
#include <windows.h>
855
LPSTR lpCmdLine,
int nCmdShow )
{
MessageBeep(MB_ICONEXCLAMATION) ;
return 0;
};
3.34.2 Exemple #2
#include <windows.h>
856
dseg02 :0010 aCaption db 'caption',0
dseg02 :0018 aHelloWorld db 'hello, world',0
3.34.3 Exemple #3
#include <windows.h>
if (result==IDCANCEL)
MessageBox (NULL, "you pressed cancel", "caption", MB_OK) ;
else if (result==IDYES)
MessageBox (NULL, "you pressed yes", "caption", MB_OK) ;
else if (result==IDNO)
MessageBox (NULL, "you pressed no", "caption", MB_OK) ;
return 0;
};
857
jnz short loc_2F
xor ax, ax
push ax
push ds
mov ax, offset aYouPressedCanc ; "you pressed cancel"
jmp short loc_49
loc_2F :
cmp ax, 6 ; IDYES
jnz short loc_3D
xor ax, ax
push ax
push ds
mov ax, offset aYouPressedYes ; "you pressed yes"
jmp short loc_49
loc_3D :
cmp ax, 7 ; IDNO
jnz short loc_57
xor ax, ax
push ax
push ds
mov ax, offset aYouPressedNo ; "you pressed no"
loc_49 :
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
xor ax, ax
push ax
call MESSAGEBOX
loc_57 :
xor ax, ax
pop bp
retn 0Ah
WinMain endp
3.34.4 Exemple #4
#include <windows.h>
858
{
return a*b+c-d ;
};
c = word ptr 4
b = word ptr 6
a = word ptr 8
push bp
mov bp, sp
mov ax, [bp+a]
imul [bp+b]
add ax, [bp+c]
pop bp
retn 6
func1 endp
push bp
mov bp, sp
mov ax, [bp+arg_8]
mov dx, [bp+arg_A]
mov bx, [bp+arg_4]
mov cx, [bp+arg_6]
call sub_B2 ; long 32-bit multiplication
add ax, [bp+arg_0]
adc dx, [bp+arg_2]
pop bp
retn 12
func2 endp
859
arg_0 = word ptr 4
arg_2 = word ptr 6
arg_4 = word ptr 8
arg_6 = word ptr 0Ah
arg_8 = word ptr 0Ch
arg_A = word ptr 0Eh
arg_C = word ptr 10h
push bp
mov bp, sp
mov ax, [bp+arg_A]
mov dx, [bp+arg_C]
mov bx, [bp+arg_6]
mov cx, [bp+arg_8]
call sub_B2 ; long 32-bit multiplication
mov cx, [bp+arg_2]
add cx, ax
mov bx, [bp+arg_4]
adc bx, dx ; BX=high part, CX=low part
mov ax, [bp+arg_0]
cwd ; AX=low part d, DX=high part d
sub cx, ax
mov ax, cx
sbb bx, dx
mov dx, bx
pop bp
retn 14
func3 endp
860
mov ax, 9 ; high part of 600000
push ax
mov ax, 27C0h ; low part of 600000
push ax
mov ax, 0Ah ; high part of 700000
push ax
mov ax, 0AE60h ; low part of 700000
push ax
mov ax, 0Ch ; high part of 800000
push ax
mov ax, 3500h ; low part of 800000
push ax
mov ax, 7Bh ; 123
push ax
call func3
xor ax, ax ; return 0
pop bp
retn 0Ah
WinMain endp
Les valeurs 32-bit (le type de donnée long implique 32 bits, tandis que int est 16-bit
en code 16-bit (à la fois pour MS-DOS et Win16) sont passées par paires. C’est tout
comme lorsqu’une valeur 64-bit est utilisée dans un environnement 32-bit ( 1.34 on
page 508).
sub_B2 voici une fonction de bibliothèques écrite par les développeurs du compi-
lateurs qui fait la « multiplication des long » (i.e., multiplie deux valeurs 32-bits).
D’autres fonctions de compilateur qui font la même chose sont listées ici: .5 on
page 1366, .4 on page 1366.
La paire d’instructions ADD/ADC est utilisée pour l’addition de valeurs composées: ADD
peut mettre le flag CF à 0/1, et ADC l’utilise après.
La paire d’instructions SUB/SBB est utilisée pour la soustraction: SUB peut mettre la
flag CF à 0/1, et SBB l’utilise après.
Les valeurs 32-bit sont renvoyées de la fonction dans la paire de registres DX:AX.
Les constantes sont aussi passées par paires dans WinMain() ici.
La constante 123 typée int est d’abord converti suivant le signe de la valeur 32-bit
en utilisant l’instruction CWD.
3.34.5 Exemple #5
#include <windows.h>
861
return 1; // end of string
s1++;
s2++;
};
};
};
push bp
mov bp, sp
push si
862
mov si, [bp+arg_0]
mov bx, [bp+arg_2]
push bp
mov bp, sp
push si
mov si, [bp+arg_0]
mov bx, [bp+arg_4]
863
xor ax, ax
jmp short loc_67
push bp
mov bp, sp
mov bx, [bp+arg_0]
864
loc_86 : ; CODE XREF: remove_digits+Aj
pop bp
retn 2
remove_digits endp
Nous voyons ici une différence entre les pointeurs appelés «near » et «far » : un
autre effet bizarre de la mémoire segmentée en 16-bit 8086.
Vous pouvez en lire plus à ce sujet ici: ?? on page ??.
Les pointeurs «near » sont ceux qui pointent dans le segment de données courant.
C’est pourquoi la fonction string_compare() prend seulement deux pointeurs 16-
bit, et accède des données dans le segment sur lequel DS pointe (L’instruction mov
al, [bx] fonctionne en fait comme mov al, ds:[bx]—DS est implicite ici).
Les pointeurs «far » sont ceux qui pointent sur des données dans un autre segment
de mémoire. C’est pourquoi string_compare_far() prend la paire de 16-bit comme
un pointeur, charge la partie haute dans le registre de segment ES et accède aux
données à travers lui (mov al, es:[bx]). Les pointeurs « far » sont aussi utilisés
865
dans mon exemple win16 MessageBox() : 3.34.2 on page 856. En effet, le noyau
de Windows n’est pas au courant du segment de données qui doit être utilisé pour
accéder aux chaînes de texte, donc il a besoin de l’information complète. La raison
de cette distinction est qu’un programme compact peut n’utiliser qu’un segment de
données de 64kb, donc il n’a pas besoin de passer la partie haute de l’adresse, qui
est toujours la même. Un programme plus gros peut utiliser plusieurs segments de
données de 64kb, donc il doit spécifier le segment de données à chaque fois.
C’est la même histoire avec les segments de code. Un programme compact peut
avoir tout son code exécutable dans un seul segment de 64kb, donc toutes les fonc-
tions y seront appelées en utilisant l’instruction CALL NEAR, et le contrôle du flux sera
renvoyé en utilisant RETN. Mais si il y a plusieurs segments de code, alors l’adresse
d’une fonction devra être spécifiée par une paire, et sera appelée en utilisant l’ins-
truction CALL FAR, et le contrôle du flux renvoyé en utilisant RETF.
Ceci est ce qui est mis dans le compilateur en spécifiant le «modèle de mémoire ».
Les compilateurs qui ciblent MS-DOS et Win16 ont des bibliothèques spécifiques pour
chaque modèle de mémoire: elles diffèrent par le type de pointeurs pour le code et
les données.
3.34.6 Exemple #6
#include <windows.h>
#include <time.h>
#include <stdio.h>
char strbuf[256];
struct tm *t ;
time_t unix_time ;
unix_time=time(NULL) ;
t=localtime (&unix_time) ;
866
var_4 = word ptr -4
var_2 = word ptr -2
push bp
mov bp, sp
push ax
push ax
xor ax, ax
call time_
mov [bp+var_4], ax ; low part of UNIX time
mov [bp+var_2], dx ; high part of UNIX time
lea ax, [bp+var_4] ; take a pointer of high part
call localtime_
mov bx, ax ; t
push word ptr [bx] ; second
push word ptr [bx+2] ; minute
push word ptr [bx+4] ; hour
push word ptr [bx+6] ; day
push word ptr [bx+8] ; month
mov ax, [bx+0Ah] ; year
add ax, 1900
push ax
mov ax, offset a04d02d02d02d02 ;
"%04d-%02d-%02d %02d:%02d:%02d"
push ax
mov ax, offset strbuf
push ax
call sprintf_
add sp, 10h
xor ax, ax ; NULL
push ax
push ds
mov ax, offset strbuf
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
xor ax, ax ; MB_OK
push ax
call MESSAGEBOX
xor ax, ax
mov sp, bp
pop bp
retn 0Ah
WinMain endp
Le temps UNIX est une valeur 32-bit, donc il est renvoyé dans la paire de registres
DX:AX et est stocké dans deux variables locales 16-bit. Puis, un pointeur sur la
paire est passé à la fonction localtime(). La fonction localtime() a une struc-
ture struct tm allouée quelque part dans les entrailles de la bibliothèque C, donc
seul un pointeur est renvoyé.
À propos, ceci implique aussi que la fonction ne peut pas être appelée tant que le
867
résultat n’a pas été utilisé.
Pour les fonctions time() et localtime(), une convention d’appel Watcom est uti-
lisée ici: les quatre premiers arguments sont passés dans les registres AX, DX, BX et
CX, et le reste des arguments par la pile.
Les fonctions utilisant cette convention sont aussi marquées par un souligné à la fin
de leur nom.
sprintf() n’utilise pas la convention d’appel PASCAL, ni la Watcom,
donc les arguments sont passés de la manière cdecl normale ( 6.1.1 on page 962).
Variables globales
Ceci est le même exemple, mais cette fois les variables sont globales:
#include <windows.h>
#include <time.h>
#include <stdio.h>
char strbuf[256];
struct tm *t ;
time_t unix_time ;
unix_time=time(NULL) ;
t=localtime (&unix_time) ;
unix_time_low dw 0
unix_time_high dw 0
t dw 0
868
call localtime_
mov bx, ax
mov t, ax ; will not be used in future...
push word ptr [bx] ; seconds
push word ptr [bx+2] ; minutes
push word ptr [bx+4] ; hour
push word ptr [bx+6] ; day
push word ptr [bx+8] ; month
mov ax, [bx+0Ah] ; year
add ax, 1900
push ax
mov ax, offset a04d02d02d02d02 ;
"%04d-%02d-%02d %02d:%02d:%02d"
push ax
mov ax, offset strbuf
push ax
call sprintf_
add sp, 10h
xor ax, ax ; NULL
push ax
push ds
mov ax, offset strbuf
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
xor ax, ax ; MB_OK
push ax
call MESSAGEBOX
xor ax, ax ; return 0
pop bp
retn 0Ah
WinMain endp
t ne va pas être utilisée, mais le compilateur a généré le code qui stocke la valeur.
Car il n’est pas sûr, peut-être que la valeur sera utilisée dans un autre module.
869
Chapitre 4
Java
4.1 Java
4.1.1 Introduction
Il existe des décompilateurs très connus pour Java (ou pour du bytecode JVM en
général) 1 .
La raison est que la décompilation du bytecode JVM est un peu plus facile que du
code x86 de plus bas niveau:
• Il y a bien plus d’informations sur les types de données.
• Le modèle de la mémoire JVM est beaucoup plus rigoureux et décrit.
• Le compilateur Java ne fait pas d’optimisation (JVM JIT2 le fait à l’exécution),
donc le bytecode dans les fichiers de classe est généralement assez lisible.
Quand est-ce que la connaissance de la JVM est utile ?
• Créer des patchs ”Quick-and-dirty” des fichiers de classe sans avoir besoin de
recompiler les résultats du décompilateur.
• Analyse de code obfusqué.
• Analyser du code généré par les nouveaux compilateurs Java, pour lesquels il
n’existe pas encore de décompilateur mis à jour.
• Construire votre propre obfuscateur.
• Construire un compilateur générateur de code (back-end) ciblant la JVM (comme
Scala, Clojure, etc. 3 ).
Commençons avec quelques bouts de code. Le JDK 1.7 est ici utilisé partout, sauf
mention contraire.
1. Par exemple, JAD: http://varaneckas.com/jad/
2. Just-In-Time compilation
3. Liste complète: http://en.wikipedia.org/wiki/List_of_JVM_languages
870
Voici la commande utilisée partout pour décompiler les fichiers de classe :
javap -c -verbose.
Voici le livre que j’ai utilisé pour préparer tous les exemples : [Tim Lindholm, Frank
Yellin, Gilad Bracha, Alex Buckley, The Java(R) Virtual Machine Specification / Java
SE 7 Edition] 4 .
Compilons le :
javac ret.java
Nous obtenons :
Les développeurs Java ont décidé que comme 0 est l’une des constantes les plus
utilisées en programmation, alors il existe une courte instruction séparée d’un octet,
iconst_0 qui pousse 0 5 .
Il y a aussi iconst_1 (qui pousse 1), iconst_2, etc., jusqu’à iconst_5.
4. Aussi disponible en https://docs.oracle.com/javase/specs/jvms/se7/jvms7.pdf ; http://
docs.oracle.com/javase/specs/jvms/se7/html/
5. Comme en MIPS où un registre séparé existe pour la constante zéro : 1.5.4 on page 36.
871
Il existe également l’instruction iconst_m1 qui pousse -1.
La pile est utilisée en JVM pour passer des données à une fonction appelée et éga-
lement pour renvoyer des valeurs. Donc iconst_0 pousse 0 sur la pile. ireturn
renvoie une valeur entière (i dans le nom signifie integer) depuis le TOS6 .
Réécrivons légèrement notre exemple, pour qu’il renvoie 1234 maintenant :
public class ret
{
public static int main(String[] args)
{
return 1234;
}
}
…nous avons :
sipush (short integer) pousse 1234 sur la pile. short dans le nom implique qu’une
valeur de 16-bit va être poussée. Le nombre 1234 tiens bien, en effet, dans une
valeur de 16-bit.
Qu’en est-il des valeurs plus grandes ?
public class ret
{
public static int main(String[] args)
{
return 12345678;
}
}
6. Top of Stack
872
Ce n’est pas possible d’encoder un nombre 32-bit dans une instruction opcode JVM,
les développeurs n’ont pas laissé une telle possibilité.
Donc le nombre 32-bit 12345678 est enregistré dans ce qu’on appelle le «constant
pool » qui est, disons, la bibliothèque des constantes les plus utilisées (incluant les
strings, objects, etc.).
Cette façon de passer des constantes n’est pas propre à JVM.
MIPS, ARM et les autres CPUs RISC ne peuvent pas non plus encoder un nombre
32-bit dans un opcode de 32-bit, donc le code CPU RISC (incluant MIPS et ARM)
doit construire la valeur en plusieurs étapes, ou le garder dans le segment des don-
nées : 1.39.3 on page 569, 1.40.1 on page 573.
Le code MIPS a aussi traditionnellement un pool des constantes, nommé « literal
pool », les segments sont nommés «.lit4 » (pour des nombres flottants constants de
simples précisions sur 32-bit) et «.lit8 » (pour des nombres flottants constants de
double précision sur 64-bit).
Essayons quelques autres types de données !
Boolean:
public class ret
{
public static boolean main(String[] args)
{
return true ;
}
}
873
public static short main(java.lang.String[]) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=1, locals=1, args_size=1
0: sipush 1234
3: ireturn
…et char !
public class ret
{
public static char main(String[] args)
{
return 'A' ;
}
}
bipush signifie «push byte ». Inutile de préciser qu’un char en Java est un caractère
UTF-16 16-bit, ce qui équivaut à un short, mais le code ASCII du caractère «A » est
65, et c’est possible d’utiliser cette instruction pour pousser un octet dans la pile.
Essayons aussi un byte :
public class retc
{
public static byte main(String[] args)
{
return 123;
}
}
874
Un char peut être essentiellement le même qu’un short, mais nous saisissons rapi-
dement que c’est un substitut pour un caractère 16-bit, et non pour une autre valeur
entière.
Quand on utilise short, nous montrons à tout le monde que la plage de la variable
est limitée à 16 bits.
C’est une très bonne idée d’utiliser le type boolean où c’est nécessaire, plutôt que
le int de style C.
Il y a aussi un type de donnée entier sur 64-bits en Java :
public class ret3
{
public static long main(String[] args)
{
return 1234567890123456789L ;
}
}
Le nombre 64-bit est aussi stocké dans le pool des constantes, ldc2_w le charge et
lreturn (long return) le retourne.
L’instruction ldc2_w est aussi utilisée pour charger des nombres flottants double
précision (qui occupent aussi 64 bits) depuis le pool des constantes :
public class ret
{
public static double main(String[] args)
{
return 123.456d ;
}
}
875
public static double main(java.lang.String[]) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=2, locals=1, args_size=1
0: ldc2_w #2 // double 123.456d
3: dreturn
L’instruction ldc utilisée ici est la même que celle pour charger des nombres entiers
de 32-bit depuis le pool des constantes.
freturn signifie «return float ».
Maintenant, qu’en est-il de la fonction qui ne retourne rien ?
public class ret
{
public static void main(String[] args)
{
return ;
}
}
876
Cela signifie que l’instruction return est utilisée pour retourner le contrôle sans
renvoyer une vraie valeur.
En sachant cela, il est très facile de déduire le type renvoyé par des fonctions (ou
des méthodes) depuis la dernière instruction.
idiv prend juste les deux valeurs depuis le TOS, divise l’un par l’autre et laisse le
résultat au TOS :
+--------+
TOS ->| result |
+--------+
877
{
return a/2.0;
}
}
C’est pareil, mais l’instruction ldc2_w est utilisée pour charger la constante 2.0 de-
puis le pool des constantes.
Aussi, les trois autres instructions sont préfixées par d, ce qui signifie qu’elles tra-
vaillent avec des valeurs de type double.
Utilisons maintenant une fonction avec deux arguments :
public class calc
{
public static int sum(int a, int b)
{
return a+b ;
}
}
878
iadd ajoute les deux valeurs et laisse le résultat au TOS :
+----------+
TOS ->| resultat |
+----------+
…nous avons :
public static long lsum(long, long) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=4, locals=4, args_size=2
0: lload_0
1: lload_2
2: ladd
3: lreturn
879
iload_2 charge le troisième argument (c) dans la pile:
+---------+
TOS ->| c |
+---------+
| produit |
+---------+
880
return Math.random()/2;
}
}
881
{
System.out.println("Hello, World") ;
}
}
ldc à l’offset 3 prend un pointeur sur la chaîne «Hello, World » dans le pool constant
et le pousse sur la pile.
C’est appelé une référence dans le monde Java, mais c’est plutôt un pointeur, ou
une adresse8 .
L’instruction connue invokevirtual prend les informations concernant la fonction
println (ou méthode) depuis le pool constant et l’appelle.
Comme on peut le savoir, il y a plusieurs méthodes println, une pour chaque type
de données.
8. À propos de la différence entre pointeurs et références en C++ voir: 3.21.3 on page 737.
882
Dans notre cas, c’est la version de println destinée au type de données String.
Mais qu’en est-il de la première instruction getstatic ?
Cette instruction prend une référence (ou l’adresse de) un champ de l’objet System.out
et le pousse sur la pile.
Cette valeur se comporte comme le pointeur this pour la méthode println.
Ainsi, en interne, la méthode println prend deux paramètres en entrée: 1)this, i.e.,
un pointeur sur un objet; 2) l’adresse de la chaîne «Hello, World ».
En effet, println() est appelé comme une méthode dans un objet System.out ini-
tialisé.
Par commodité, l’utilitaire javap écrit toutes ces informations dans les commen-
taires.
883
}
iload_1 prend la valeur en entrée et la pousse sur la pile. Mais pourquoi pas iload_0 ?
C’est parce que cette fonction peut utiliser les champs de la classe, et donc this est
aussi passé à la fonction comme paramètre d’indice zéro.
Le champ rand_state occupe le 2ème slot dans la classe, donc putstatic copie la
valeur depuis le TOS dans le 2ème slot.
Maintenant my_rand() :
public int my_rand() ;
flags : ACC_PUBLIC
884
Code :
stack=2, locals=1, args_size=1
0: getstatic #2 // Field rand_state :I
3: getstatic #3 // Field RNG_a :I
6: imul
7: putstatic #2 // Field rand_state :I
10: getstatic #2 // Field rand_state :I
13: getstatic #4 // Field RNG_c :I
16: iadd
17: putstatic #2 // Field rand_state :I
20: getstatic #2 // Field rand_state :I
23: sipush 32767
26: iand
27: ireturn
Ça charge toutes les valeurs depuis les champs de l’objet, effectue l’opération et
met à jour les valeurs de rand_state en utilisant l’instruction putstatic.
À l’offset 20, rand_state est rechargé à nouveau (car il a été supprimé de la pile
avant, par putstatic).
Ceci semble non-efficient, mais soyez assuré que la JVM est en général assez bonne
pour vraiment bien optimiser de telles choses.
885
N’oubliez pas que chaque instruction ifXX supprime la valeur (qui doit être compa-
rée) de la pile.
ineg inverse le signe de la valeur du TOS.
Un autre exemple:
public static int min (int a, int b)
{
if (a>b)
return b ;
return a ;
}
Nous obtenons:
public static int min(int, int) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: if_icmple 7
5: iload_1
6: ireturn
7: iload_0
8: ireturn
if_icmple prend deux valeurs et les compare. Si la seconde est plus petite ou égale
à la première, un saut à l’offset 7 est effectué.
Quand nous définissons la fonction max() …
public static int max (int a, int b)
{
if (a>b)
return a ;
return b ;
}
…le code résultant est le même, mais les deux dernières instructions iload (aux
offsets 5 et 7) sont échangées:
public static int max(int, int) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: if_icmple 7
5: iload_0
6: ireturn
7: iload_1
8: ireturn
886
Un exemple plus avancé:
public class cond
{
public static void f(int i)
{
if (i<100)
System.out.print("<100") ;
if (i==100)
System.out.print("==100") ;
if (i>100)
System.out.print(">100") ;
if (i==0)
System.out.print("==0") ;
}
}
887
if_icmpge prend deux valeurs et les compare. Si le seconde est plus grande que la
première, un saut à l’offset 14 est effectué.
if_icmpne et if_icmple fonctionnent de la même façon, mais implémentent des
conditions différentes.
Il y a aussi une instruction ifne à l’offset 43.
Le nom est un terme inapproprié, il aurait été meilleur de l’appeler ifnz (saut si la
valeur du TOS n’est pas zéro).
Et c’est ce qu’elle fait: elle saute à l’offset 54 si la valeur en entrée n’est pas zéro.
Si c’est zéro, le flux d’exécution continue à l’offset 46, où la chaîne « ==0 » est
affichée.
N.B.: la JVM n’a pas de type de données non signée, donc les instructions de compa-
raison opèrent seulement sur des valeurs entières signées.
888
0: bipush 123
2: istore_1
3: sipush 456
6: istore_2
7: iload_1
8: iload_2
9: invokestatic #2 // Method max :(II)I
12: istore_3
13: iload_1
14: iload_2
15: invokestatic #3 // Method min :(II)I
18: istore 4
20: getstatic #4 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
23: iload 4
25: invokevirtual #5 // Method java/io/PrintStream.println :(⤦
Ç I)V
28: getstatic #4 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
31: iload_3
32: invokevirtual #5 // Method java/io/PrintStream.println :(⤦
Ç I)V
35: return
Les paramètres sont passés à l’autre fonction dans la pile, et la valeur renvoyée est
laissée sur le TOS.
889
public static int clear(int, int) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=3, locals=2, args_size=2
0: iload_0
1: iconst_1
2: iload_1
3: ishl
4: iconst_m1
5: ixor
6: iand
7: ireturn
iconst_m1 charge −1 sur la pile, c’est la même chose que le nombre 0xFFFFFFFF.
XORé avec 0xFFFFFFFF a le même effet qu’inverser tous les bits ( 2.6 on page 596).
Étendons tous les types de données à 64-bit long :
public static long lset (long a, int b)
{
return a | 1<<b ;
}
890
Le code est le même, mais des instructions avec le préfixe l sont utilisées, qui opèrent
avec des valeurs 64-bit.
Ainsi, le second paramètre de la fonction est toujours du type int, et lorsque la va-
leur 32-bit qu’il contient doit être étendues à une valeur 64-bit, l’instruction i2l est
utilisée,
4.1.11 Boucles
public class Loop
{
public static void main(String[] args)
{
for (int i = 1; i <= 10; i++)
{
System.out.println(i) ;
}
}
}
891
À propos, nous appelons la méthode println pour un entier, et nous voyons this
dans le commentaire: «(I)V » ((I signifie integer et V signifie que le type de retour
est void).
Lorsque println termine, i est incrémenté à l’offset 15.
Le premier opérande de l’instruction est le numéro d’un slot (1), le second est le
nombre a ajouté à la variable.
Procédons avec un exemple plus complexe:
public class Fibonacci
{
public static void main(String[] args)
{
int limit = 20, f = 0, g = 1;
892
31: iinc 4, 1
34: goto 10
37: return
4.1.12 switch()
La déclaration switch() est implémentée avec l’instruction tableswitch :
public static void f(int a)
{
switch (a)
{
case 0: System.out.println("zero") ; break ;
case 1: System.out.println("one\n") ; break ;
case 2: System.out.println("two\n") ; break ;
case 3: System.out.println("three\n") ; break ;
case 4: System.out.println("four\n") ; break ;
default : System.out.println("something unknown\n") ; break ;
};
}
893
0: iload_0
1: tableswitch { // 0 to 4
0: 36
1: 47
2: 58
3: 69
4: 80
default : 91
}
36: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
39: ldc #3 // String zero
41: invokevirtual #4 // Method java/io/PrintStream.println :(⤦
Ç Ljava/lang/String ;)V
44: goto 99
47: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
50: ldc #5 // String one\n
52: invokevirtual #4 // Method java/io/PrintStream.println :(⤦
Ç Ljava/lang/String ;)V
55: goto 99
58: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
61: ldc #6 // String two\n
63: invokevirtual #4 // Method java/io/PrintStream.println :(⤦
Ç Ljava/lang/String ;)V
66: goto 99
69: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
72: ldc #7 // String three\n
74: invokevirtual #4 // Method java/io/PrintStream.println :(⤦
Ç Ljava/lang/String ;)V
77: goto 99
80: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
83: ldc #8 // String four\n
85: invokevirtual #4 // Method java/io/PrintStream.println :(⤦
Ç Ljava/lang/String ;)V
88: goto 99
91: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
94: ldc #9 // String something unknown\n
96: invokevirtual #4 // Method java/io/PrintStream.println :(⤦
Ç Ljava/lang/String ;)V
99: return
4.1.13 Tableaux
Exemple simple
894
{
int a[]=new int[10];
for (int i=0; i<10; i++)
a[i]=i ;
dump (a) ;
}
895
for (int i=0; i<a.length ; i++)
System.out.println(a[i]) ;
}
Un autre exemple:
public class ArraySum
{
public static int f (int[] a)
{
int sum=0;
for (int i=0; i<a.length ; i++)
sum=sum+a[i];
return sum ;
}
896
}
Nous allons utiliser le seul argument de la fonction main(), qui est un tableau de
chaînes:
public class UseArgument
{
public static void main(String[] args)
{
System.out.print("Hi, ") ;
System.out.print(args[1]) ;
System.out.println(". How are you ?") ;
}
}
L’argument d’indice zéro est le nom du programme (comme en C/C++, etc.), donc
le 1er argument fourni par l’utilisateur est à l’indice 1.
public static void main(java.lang.String[]) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
897
3: ldc #3 // String Hi,
5: invokevirtual #4 // Method java/io/PrintStream.print :(⤦
Ç Ljava/lang/String ;)V
8: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
11: aload_0
12: iconst_1
13: aaload
14: invokevirtual #4 // Method java/io/PrintStream.print :(⤦
Ç Ljava/lang/String ;)V
17: getstatic #2 // Field java/lang/System.out :Ljava/io/⤦
Ç PrintStream ;
20: ldc #5 // String . How are you ?
22: invokevirtual #6 // Method java/io/PrintStream.println :(⤦
Ç Ljava/lang/String ;)V
25: return
aload_0 en 11 charge une référence sur le slot zéro du LVA (1er et unique argument
de main()).
iconst_1 et aaload en 12 et 13 prend une référence sur l’élément 1 du tableau (en
comptant depuis 0).
La référence sur l’objet chaîne est sur le TOS à l’offset 14, et elle est prise d’ici par
la méthode println.
898
public java.lang.String get_month(int) ;
flags : ACC_PUBLIC
Code :
stack=2, locals=2, args_size=2
0: getstatic #2 // Field months :[Ljava/lang/String ;
3: iload_1
4: aaload
5: areturn
899
44: ldc #11 // String August
46: aastore
47: dup
48: bipush 8
50: ldc #12 // String September
52: aastore
53: dup
54: bipush 9
56: ldc #13 // String October
58: aastore
59: dup
60: bipush 10
62: ldc #14 // String November
64: aastore
65: dup
66: bipush 11
68: ldc #15 // String December
70: aastore
71: putstatic #2 // Field months :[Ljava/lang/String ;
74: return
Fonctions variadiques
900
for (int i=0; i<values.length ; i++)
System.out.println(values[i]) ;
}
901
17: iconst_4
18: iastore
19: dup
20: iconst_4
21: iconst_5
22: iastore
23: invokestatic #4 // Method f :([I)V
26: return
Le tableau est construit dans main() en utilisant l’instruction newarray, puis il est
rempli, et f() est appelée.
Oh, à propos, l’objet tableau n’est pas détruit à la fin de main().
Il n’y a pas du tout de destructeurs en Java, car la JVM a un ramasse miette qui fait
ceci automatiquement, lorsqu’il sent qu’il doit.
Que dire de la méthode format() ?
Elle prend deux arguments en entrée: une chaîne et un tableau d’objets:
public PrintStream format(String format, Object... args)
( http://docs.oracle.com/javase/tutorial/java/data/numberformat.html )
Voyons:
public static void main(String[] args)
{
int i=123;
double d=123.456;
System.out.format("int : %d double : %f.%n", i, d) ;
}
902
25: dload_2
26: invokestatic #8 // Method java/lang/Double.valueOf :(D⤦
Ç )Ljava/lang/Double ;
29: aastore
30: invokevirtual #9 // Method java/io/PrintStream.format⤦
Ç :(Ljava/lang/String ;[Ljava/lang/Object ;)Ljava/io/PrintStream ;
33: pop
34: return
Donc, les valeurs des types int et double sont d’abord convertis en objets Integer
et Double en utilisant les méthodes valueOf.
La méthode format() nécessite un objet de type Object en entrée, et comme
Integer et Double sont dérivées de la classe racine Object, ils conviennent comme
éléments du tableau en entrée.
D’un autre côté, un tableau est toujours homogène, i.e., il ne peut pas contenir
d’éléments de types différents, ce qui rend impossible de pousser des valeurs int
et double dedans.
Un tableau d’objets Object est créé à l’offset 13, un objet Integer est ajouté au
tableau à l’offset 22, et un objet Double est ajouté au tableau à l’offset 29.
La pénultième instruction pop supprime l’élément du TOS, donc lorsque return est
exécuté, la pile se retrouve vide (ou balancée).
Tableaux bi-dimensionnels
903
Il est créé en utilisant l’instruction multianewarray : le type de l’objet et ses dimen-
sions sont passés comme opérandes.
La taille du tableau (10*5) est laissée dans la pile (en utilisant les instructions iconst_5
et bipush).
Une référence à la line #1 est chargée à l’offset 10 (iconst_1 et aaload).
La colonne est choisie en utilisant iconst_2 à l’offset 11.
La valeur à écrire est mise à l’offset 12.
iastore en 13 écrit l’élément du tableau.
Comment un élément est-il accédé?
public static int get12 (int[][] in)
{
return in[1][2];
}
Un référence sur la ligne du tableau est chargée à l’offset 2, la colonne est mise à
l’offset 3, puis iaload charge l’élément du tableau.
Tableaux tri-dimensionnels
a[1][2][3]=4;
get_elem(a) ;
}
904
0: iconst_5
1: bipush 10
3: bipush 15
5: multianewarray #2, 3 // class "[[[I"
9: astore_1
10: aload_1
11: iconst_1
12: aaload
13: iconst_2
14: aaload
15: iconst_3
16: iconst_4
17: iastore
18: aload_1
19: invokestatic #3 // Method get_elem :([[[I)I
22: pop
23: return
Résumé
905
4.1.14 Chaînes
Premier exemple
Les chaînes sont des objets et sont construites de la même manière que les autres
objets (et tableaux).
public static void main(String[] args)
{
System.out.println("What is your name ?") ;
String input = System.console().readLine() ;
System.out.println("Hello, "+input) ;
}
La méthode readLine() est appelée à l’offset 11, une référence sur la chaîne (qui
est fournie par l’utilisateur) est stockée sur le TOS.
À l’offset 14, la référence sur la chaîne est stockée dans le slot 1 du LVA.
La chaîne que l’utilisateur a entré est rechargée à l’offset 30 et concaténée avec la
chaîne «Hello, » en utilisant la classe StringBuilder.
La chaîne construite est ensuite affichée en utilisant println à l’offset 37.
906
Second exemple
Un autre exemple:
public class strings
{
public static char test (String a)
{
return a.charAt(3) ;
};
Un autre exemple:
public static void main(String[] args)
{
String s="Hello !" ;
int n=123;
System.out.println("s=" + s + " n=" + n) ;
907
}
4.1.15 Exceptions
Retravaillons un peu notre exemple Month ( 4.1.13 on page 898) :
Listing 4.10: IncorrectMonthException.java
public class IncorrectMonthException extends Exception
{
private int index ;
908
return index ;
}
}
909
puis il met la valeur entière en entrée dans l’unique champ de la classe IncorrectMonthException :
public IncorrectMonthException(int) ;
flags : ACC_PUBLIC
Code :
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Exception."<init⤦
Ç >":()V
4: aload_0
5: iload_1
6: putfield #2 // Field index :I
9: return
910
Le type de l’objet est passé comme un opérande à l’instruction (qui est IncorrectMonthException).
Ensuite, son constructeur est appelé et l’index est passé via le TOS (offset 15).
Lorsque le contrôle du flux se trouve à l’offset 18, l’objet est déjà construit, donc
maintenant l’instruction athrow prend une référence sur l’objet nouvellement construit
et indique à la JVM de trouver le gestionnaire d’exception approprié.
L’instruction athrow ne renvoie pas le contrôle du flus ici, donc à l’offset 19 il y a un
autre bloc de base, non relatif aux exceptions, où nous pouvons aller depuis l’offset
7.
Comment fonctionnent les gestionnaires?
main() in Month2.class :
Listing 4.13: Month2.class
public static void main(java.lang.String[]) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=3, locals=2, args_size=1
0: getstatic #5 // Field java/lang/System.out :Ljava/io⤦
Ç /PrintStream ;
3: bipush 100
5: invokestatic #6 // Method get_month :(I)Ljava/lang/⤦
Ç String ;
8: invokevirtual #7 // Method java/io/PrintStream.println⤦
Ç :(Ljava/lang/String ;)V
11: goto 47
14: astore_1
15: getstatic #5 // Field java/lang/System.out :Ljava/io⤦
Ç /PrintStream ;
18: new #8 // class java/lang/StringBuilder
21: dup
22: invokespecial #9 // Method java/lang/StringBuilder."<⤦
Ç init>":()V
25: ldc #10 // String incorrect month index :
27: invokevirtual #11 // Method java/lang/StringBuilder.⤦
Ç append :(Ljava/lang/String ;)Ljava/lang/StringBuilder ;
30: aload_1
31: invokevirtual #12 // Method IncorrectMonthException.⤦
Ç getIndex :()I
34: invokevirtual #13 // Method java/lang/StringBuilder.⤦
Ç append :(I)Ljava/lang/StringBuilder ;
37: invokevirtual #14 // Method java/lang/StringBuilder.⤦
Ç toString :()Ljava/lang/String ;
40: invokevirtual #7 // Method java/io/PrintStream.println⤦
Ç :(Ljava/lang/String ;)V
43: aload_1
44: invokevirtual #15 // Method IncorrectMonthException.⤦
Ç printStackTrace :()V
47: return
Exception table :
from to target type
0 11 14 Class IncorrectMonthException
911
Ici se trouve la table Exception, qui définit que de l’offset 0 à 11 (inclus), une
exception
IncorrectMonthException peut se produire, et si cela se produit, le contrôle du flux
sera passé à l’offset 14.
En effet, le programme principal se termine à l’offset 11.
À l’offset 14, le gestionnaire commence. Il n’est pas possible d’arriver ici, il n’y a pas
de saut conditionnel/inconditionnel à cet endroit.
Mais la JVM transférera le flux d’exécution ici en cas d’exception.
Le tout premier astore_1 (en 14) prend la référence en entrée sur l’objet exception
et la stocke dans le slot 1 du LVA.
Plus tard, la méthode getIndex() (de cet objet exception) sera appelée à l’offset
31.
La référence sur l’objet exception courant est passée juste avant cela (offset 30).
Le reste du code effectue juste de la manipulation de chaîne: d’abord. la valeur en-
tière renvoyée par getIndex() est convertie en chaîne par la méthode toString(),
puis est concaténée avec la chaîne de texte «incorrect month index: » (comme nous
l’avons vu avant), enfin println() et printStackTrace() sont appelées.
Après la fin de printStackTrace(), l’exception est gérer et nous pouvons continuer
avec l’exécution normale.
À l’offset 47 il y a un return qui termine la fonction main(), mais il pourrait y avoir
n’importe quel autre code qui serait exécuté comme si aucune exception n’avait été
déclenchée.
Voici un exemple de la façon dont IDA montre les intervalles d’exceptions:
Listing 4.14: tiré d’un fichier .class quelconque trouvé sur mon ordinateur
.catch java/io/FileNotFoundException from met001_335 to met001_360\
using met001_360
.catch java/io/FileNotFoundException from met001_185 to met001_214\
using met001_214
.catch java/io/FileNotFoundException from met001_181 to met001_192\
using met001_195
.catch java/io/FileNotFoundException from met001_155 to met001_176\
using met001_176
.catch java/io/FileNotFoundException from met001_83 to met001_129 using⤦
Ç \
met001_129
.catch java/io/FileNotFoundException from met001_42 to met001_66 using ⤦
Ç \
met001_69
.catch java/io/FileNotFoundException from met001_begin to met001_37\
using met001_37
4.1.16 Classes
Classe simple:
912
Listing 4.15: test.java
public class test
{
public static int a ;
private static int b ;
public test()
{
a=0;
b=0;
}
public static void set_a (int input)
{
a=input ;
}
public static int get_a ()
{
return a ;
}
public static void set_b (int input)
{
b=input ;
}
public static int get_b ()
{
return b ;
}
}
Setter de a :
public static void set_a(int) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=1, locals=1, args_size=1
0: iload_0
1: putstatic #2 // Field a :I
4: return
913
Getter de a :
public static int get_a() ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=1, locals=0, args_size=0
0: getstatic #2 // Field a :I
3: ireturn
Setter de b :
public static void set_b(int) ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=1, locals=1, args_size=1
0: iload_0
1: putstatic #3 // Field b :I
4: return
Getter de b :
public static int get_b() ;
flags : ACC_PUBLIC, ACC_STATIC
Code :
stack=1, locals=0, args_size=0
0: getstatic #3 // Field b :I
3: ireturn
Il n’y a aucune différence dans le code qui fonctionne avec des champs publics ou
privés.
Mais ce type d’information est présent dans le fichier .class et il n’est pas possible
d’accéder aux champs privés depuis n’importe où.
Créons un objet et appelons sa méthode:
914
4: invokespecial #3 // Method test."<init>":()V
7: astore_1
8: aload_1
9: pop
10: sipush 1234
13: invokestatic #4 // Method test.set_a :(I)V
16: getstatic #5 // Field java/lang/System.out :Ljava/io⤦
Ç /PrintStream ;
19: aload_1
20: pop
21: getstatic #6 // Field test.a :I
24: invokevirtual #7 // Method java/io/PrintStream.println⤦
Ç :(I)V
27: return
L’instruction new crée un objet, mais n’appelle pas le constructeur (il est appelé à
l’offset 4).
La méthode set_a() est appelée à l’offset 16.
Le champ a est accédé en utilisant l’instruction getstatic à l’offset 21.
915
Chapitre 5
916
Par exemple, si un programme utilise des fichiers XML, la premières étape peut-être
de déterminer quelle bibliothèque XML est utilisée pour le traitement, puisque les
bibliothèques standards (ou bien connues) sont en général utilisées au lieu de code
fait maison.
Par exemple, j’ai essayé une fois de comprendre comment la compression/décom-
pression des paquets réseau fonctionne dans SAP 6.0. C’est un logiciel gigantesque,
mais un .PDB détaillé avec des informations de débogage est présent, et c’est pra-
tique. J’en suis finalement arrivé à l’idée que l’une des fonctions, qui était appelée
par CsDecomprLZC, effectuait la décompression des paquets réseau. Immédiate-
ment, j’ai essayé de googler le nom et rapidement trouvé que la fonction était utili-
sée dans MaxDB (c’est un projet open-source de SAP) 3 .
http://www.google.com/search?q=CsDecomprLZC
Étonnement, les logiciels MaxDB et SAP 6.0 partagent du code comme ceci pour la
compression/ décompression des paquets réseau.
Marketing ver. Internal ver. CL.EXE ver. DLLs imported Release date
6 6.0 12.00 msvcrt.dll June 1998
msvcp60.dll
.NET (2002) 7.0 13.00 msvcr70.dll February 13, 2002
msvcp70.dll
.NET 2003 7.1 13.10 msvcr71.dll April 24, 2003
msvcp71.dll
2005 8.0 14.00 msvcr80.dll November 7, 2005
msvcp80.dll
2008 9.0 15.00 msvcr90.dll November 19, 2007
msvcp90.dll
2010 10.0 16.00 msvcr100.dll April 12, 2010
msvcp100.dll
2012 11.0 17.00 msvcr110.dll September 12, 2012
msvcp110.dll
2013 12.0 18.00 msvcr120.dll October 17, 2013
msvcp120.dll
msvcp*.dll contient des fonctions relatives à C++, donc si elle est importées, il s’agit
probablement d’un programme C++.
Mangling de nom
917
Vous trouverez plus d’informations le mangling de nom de MSVC ici: 3.21.1 on page 715.
5.1.2 GCC
À part les cibles *NIX, GCC est aussi présent dans l’environnement win32, sous la
forme de Cygwin et MinGW.
Mangling de nom
Les noms commencent en général par le symbole _Z. Vous trouverez plus d’informa-
tions le mangling de nom de GCC ici: 3.21.1 on page 715.
Cygwin
MinGW
5.1.5 Borland
Voici un exemple de mangling de nom de Delphi de Borland et de C++Builder:
@TApplication@IdleAction$qv
@TApplication@ProcessMDIAccels$qp6tagMSG
@TModule@$bctr$qpcpvt1
@TModule@$bdtr$qv
@TModule@ValidWindow$qp14TWindowsObject
@TrueColorTo8BitN$qpviiiiiit1iiiiii
@TrueColorTo16BitN$qpviiiiiit1iiiiii
918
@DIB24BitTo8BitBitmap$qpviiiiiit1iiiii
@TrueBitmap@$bctr$qpcl
@TrueBitmap@$bctr$qpvl
@TrueBitmap@$bctr$qiilll
Les noms commencent toujours avec le symbole @, puis nous avons le nom de la
classe, de la méthode et les types des arguments de méthode encodés.
Ces noms peuvent être dans des imports .exe, des exports .dll, des données de
débogage, etc.
Les Borland Visual Component Libraries (VCL) sont stockées dans des fichiers .bpl
au lieu de .dll, par exemple, vcl50.dll, rtl60.dll.
Une autre DLL qui peut être importée: BORLNDMM.DLL.
Delphi
Presque tous les exécutables Delpi ont la chaîne de texte «Boolean » au début de
leur segment de code, ainsi que d’autres noms de type.
Ceci est le début très typique du segment CODE d’un programme Delphi, ce bloc
vient juste après l’entête de fichier win32 PE:
04 10 40 00 03 07 42 6f 6f 6c 65 61 6e 01 00 00 |[email protected]...|
00 00 01 00 00 00 00 10 40 00 05 46 61 6c 73 65 |[email protected]|
04 54 72 75 65 8d 40 00 2c 10 40 00 09 08 57 69 |.True.@.,[email protected]|
64 65 43 68 61 72 03 00 00 00 00 ff ff 00 00 90 |deChar..........|
44 10 40 00 02 04 43 68 61 72 01 00 00 00 00 ff |[email protected]......|
00 00 00 90 58 10 40 00 01 08 53 6d 61 6c 6c 69 |[email protected]|
6e 74 02 00 80 ff ff ff 7f 00 00 90 70 10 40 00 |nt..........p.@.|
01 07 49 6e 74 65 67 65 72 04 00 00 00 80 ff ff |..Integer.......|
ff 7f 8b c0 88 10 40 00 01 04 42 79 74 65 01 00 |[email protected]..|
00 00 00 ff 00 00 00 90 9c 10 40 00 01 04 57 6f |[email protected]|
72 64 03 00 00 00 00 ff ff 00 00 90 b0 10 40 00 |rd............@.|
01 08 43 61 72 64 69 6e 61 6c 05 00 00 00 00 ff |..Cardinal......|
ff ff ff 90 c8 10 40 00 10 05 49 6e 74 36 34 00 |[email protected].|
00 00 00 00 00 00 80 ff ff ff ff ff ff ff 7f 90 |................|
e4 10 40 00 04 08 45 78 74 65 6e 64 65 64 02 90 |[email protected]..|
f4 10 40 00 04 06 44 6f 75 62 6c 65 01 8d 40 00 |[email protected]..@.|
04 11 40 00 04 08 43 75 72 72 65 6e 63 79 04 90 |[email protected]..|
14 11 40 00 0a 06 73 74 72 69 6e 67 20 11 40 00 |[email protected] .@.|
0b 0a 57 69 64 65 53 74 72 69 6e 67 30 11 40 00 |..WideString0.@.|
0c 07 56 61 72 69 61 6e 74 8d 40 00 40 11 40 00 |..Variant.@.@.@.|
0c 0a 4f 6c 65 56 61 72 69 61 6e 74 98 11 40 00 |..OleVariant..@.|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00 00 00 00 00 00 00 00 00 00 00 00 98 11 40 00 |..............@.|
04 00 00 00 00 00 00 00 18 4d 40 00 24 4d 40 00 |.........M@.$M@.|
28 4d 40 00 2c 4d 40 00 20 4d 40 00 68 4a 40 00 |(M@.,M@. [email protected]@.|
84 4a 40 00 c0 4a 40 00 07 54 4f 62 6a 65 63 74 |[email protected]@..TObject|
a4 11 40 00 07 07 54 4f 62 6a 65 63 74 98 11 40 |[email protected]..@|
00 00 00 00 00 00 00 06 53 79 73 74 65 6d 00 00 |........System..|
c4 11 40 00 0f 0a 49 49 6e 74 65 72 66 61 63 65 |[email protected]|
00 00 00 00 01 00 00 00 00 00 00 00 00 c0 00 00 |................|
919
00 00 00 00 46 06 53 79 73 74 65 6d 03 00 ff ff |....F.System....|
f4 11 40 00 0f 09 49 44 69 73 70 61 74 63 68 c0 |[email protected].|
11 40 00 01 00 04 02 00 00 00 00 00 c0 00 00 00 |.@..............|
00 00 00 46 06 53 79 73 74 65 6d 04 00 ff ff 90 |...F.System.....|
cc 83 44 24 04 f8 e9 51 6c 00 00 83 44 24 04 f8 |..D$...Ql...D$..|
e9 6f 6c 00 00 83 44 24 04 f8 e9 79 6c 00 00 cc |.ol...D$...yl...|
cc 21 12 40 00 2b 12 40 00 35 12 40 00 01 00 00 |.!.@[email protected].@....|
00 00 00 00 00 00 00 00 00 c0 00 00 00 00 00 00 |................|
46 41 12 40 00 08 00 00 00 00 00 00 00 8d 40 00 |FA.@..........@.|
bc 12 40 00 4d 12 40 00 00 00 00 00 00 00 00 00 |[email protected].@.........|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
bc 12 40 00 0c 00 00 00 4c 11 40 00 18 4d 40 00 |[email protected][email protected]@.|
50 7e 40 00 5c 7e 40 00 2c 4d 40 00 20 4d 40 00 |P~@.\~@.,M@. M@.|
6c 7e 40 00 84 4a 40 00 c0 4a 40 00 11 54 49 6e |[email protected]@[email protected]|
74 65 72 66 61 63 65 64 4f 62 6a 65 63 74 8b c0 |terfacedObject..|
d4 12 40 00 07 11 54 49 6e 74 65 72 66 61 63 65 |[email protected]|
64 4f 62 6a 65 63 74 bc 12 40 00 a0 11 40 00 00 |dObject..@...@..|
00 06 53 79 73 74 65 6d 00 00 8b c0 00 13 40 00 |..System......@.|
11 0b 54 42 6f 75 6e 64 41 72 72 61 79 04 00 00 |..TBoundArray...|
00 00 00 00 00 03 00 00 00 6c 10 40 00 06 53 79 |[email protected]|
73 74 65 6d 28 13 40 00 04 09 54 44 61 74 65 54 |stem([email protected]|
69 6d 65 01 ff 25 48 e0 c4 00 8b c0 ff 25 44 e0 |ime..%H......%D.|
920
5.3 Communication avec le monde extérieur (win32)
Parfois, il est suffisant d’observer les entrées/sorties d’une fonction pour comprendre
ce qu’elle fait. Ainsi, vous pouvez gagner du temps.
Accès aux fichiers et au registre: pour les analyses très basiques, l’utilitaire, Process
Monitor5 de SysInternals peut aider.
Pour l’analyse basique des accès au réseau, Wireshark6 peut être utile.
Mais vous devrez de toutes façons regarder à l’intérieur,
Les premières choses à chercher sont les fonctions des APIs de l’OS et des biblio-
thèques standards qui sont utilisées.
Si le programme est divisé en un fichier exécutable et un groupe de fichiers DLL,
parfois le nom des fonctions dans ces DLLs peut aider.
Si nous sommes intéressés par exactement ce qui peut conduire à appeler MessageBox()
avec un texte spécifique, nous pouvons essayer de trouver ce texte dans le segment
de données, trouver sa référence et trouver les points depuis lesquels le contrôle
peut être passé à l’appel à MessageBox() qui nous intéresse.
Si nous parlons d’un jeu vidéo et que nous sommes intéressés par les évènements
qui y sont plus ou moins aléatoires, nous pouvons essayer de trouver la fonction
rand() ou sa remplaçante (comme l’algorithme du twister de Mersenne) et trouver
les points depuis lesquels ces fonctions sont appelées, et plus important, comment
les résultats sont utilisés. Un exemple: 8.3 on page 1053.
Mais si ce n’est pas un jeu, et que rand() est toujours utilisé, il est intéressant de
savoir pourquoi. Il a y des cas d’utilisation inattendu de rand() dans des algorithmes
de compression de données (pour une imitation du chiffrement) : blog.yurichev.com.
921
• Réseau TCP/IP (ws2_32.dll) : WSARecv, WSASend.
• Accès fichier (kernel32.dll) : CreateFile, ReadFile, ReadFileEx, WriteFile, WriteFi-
leEx.
• Accès haut niveau à Internet (wininet.dll) : WinHttpOpen.
• Vérifier la signature digitale d’uin fichier exécutable (wintrust.dll) : WinVerify-
Trust.
• La bibliothèque MSVC standard (si elle est liée dynamiquement) (msvcr*.dll) :
assert, itoa, ltoa, open, printf, read, strcmp, atol, atoi, fopen, fread, fwrite, memcmp,
rand, strlen, strstr, strchr.
Ou, mettons un point d’arrêt INT3 sur toutes les fonctions avec le préfixe xml dans
leur nom:
--one-time-INT3-bp :somedll.dll !xml.*
Le revers de la médaille est que de tels points d’arrêt ne sont déclenchés qu’une
fois. Tracer montrera l’appel à une fonction, s’il se produit, mais seulement une fois.
Un autre inconvénient—il est impossible de voir les arguments de la fonction.
Néanmoins, cette fonctionnalité est très utile lorsque vous avez qu’un programme
utilise une DLL, mais que vous ne savez pas quelles fonctions sont effectivement
utilisées. Et il y a beaucoup de fonctions.
Par exemple, regardons ce qu’utilise l’utilitaire uptime de Cygwin:
922
tracer -l :uptime.exe --one-time-INT3-bp :cygwin1.dll !.*
Ainsi nous pouvons voir quelles sont les fonctions de la bibliothèque cygwin1.dll qui
sont appelées au moins une fois, et depuis où:
One-time INT3 breakpoint : cygwin1.dll !__main (called from uptime.exe !OEP+0⤦
Ç x6d (0x40106d))
One-time INT3 breakpoint : cygwin1.dll !_geteuid32 (called from uptime.exe !⤦
Ç OEP+0xba3 (0x401ba3))
One-time INT3 breakpoint : cygwin1.dll !_getuid32 (called from uptime.exe !⤦
Ç OEP+0xbaa (0x401baa))
One-time INT3 breakpoint : cygwin1.dll !_getegid32 (called from uptime.exe !⤦
Ç OEP+0xcb7 (0x401cb7))
One-time INT3 breakpoint : cygwin1.dll !_getgid32 (called from uptime.exe !⤦
Ç OEP+0xcbe (0x401cbe))
One-time INT3 breakpoint : cygwin1.dll !sysconf (called from uptime.exe !OEP⤦
Ç +0x735 (0x401735))
One-time INT3 breakpoint : cygwin1.dll !setlocale (called from uptime.exe !⤦
Ç OEP+0x7b2 (0x4017b2))
One-time INT3 breakpoint : cygwin1.dll !_open64 (called from uptime.exe !OEP⤦
Ç +0x994 (0x401994))
One-time INT3 breakpoint : cygwin1.dll !_lseek64 (called from uptime.exe !OEP⤦
Ç +0x7ea (0x4017ea))
One-time INT3 breakpoint : cygwin1.dll !read (called from uptime.exe !OEP+0⤦
Ç x809 (0x401809))
One-time INT3 breakpoint : cygwin1.dll !sscanf (called from uptime.exe !OEP+0⤦
Ç x839 (0x401839))
One-time INT3 breakpoint : cygwin1.dll !uname (called from uptime.exe !OEP+0⤦
Ç x139 (0x401139))
One-time INT3 breakpoint : cygwin1.dll !time (called from uptime.exe !OEP+0⤦
Ç x22e (0x40122e))
One-time INT3 breakpoint : cygwin1.dll !localtime (called from uptime.exe !⤦
Ç OEP+0x236 (0x401236))
One-time INT3 breakpoint : cygwin1.dll !sprintf (called from uptime.exe !OEP⤦
Ç +0x25a (0x40125a))
One-time INT3 breakpoint : cygwin1.dll !setutent (called from uptime.exe !OEP⤦
Ç +0x3b1 (0x4013b1))
One-time INT3 breakpoint : cygwin1.dll !getutent (called from uptime.exe !OEP⤦
Ç +0x3c5 (0x4013c5))
One-time INT3 breakpoint : cygwin1.dll !endutent (called from uptime.exe !OEP⤦
Ç +0x3e6 (0x4013e6))
One-time INT3 breakpoint : cygwin1.dll !puts (called from uptime.exe !OEP+0⤦
Ç x4c3 (0x4014c3))
5.4 Chaînes
5.4.1 Chaînes de texte
C/C++
923
La raison pour laquelle le format des chaînes C est ce qu’il est (terminé par zéro) est
apparemment historique: Dans [Dennis M. Ritchie, The Evolution of the Unix Time-
sharing System, (1979)] nous lisons:
A minor difference was that the unit of I/O was the word, not the
byte, because the PDP-7 was a word-addressed machine. In practice
this meant merely that all programs dealing with character streams
ignored null characters, because null was used to pad a file to an even
number of characters.
Une différence mineure était que l’unité d’E/S était le mot, pas l’octet, car le PDP-7
était une machine adressée par mot. En pratique, cela signifiait que tous les pro-
grammes ayant à faire avec des flux de caractères ignoraient le caractère nul, car
nul était utilisé pour compléter un fichier ayant un nombre impair de caractères.
Dans Hiew ou FAR Manager ces chaînes ressemblent à ceci:
int main()
{
printf ("Hello, world !\n") ;
};
Borland Delphi
Une chaîne en Pascal et en Delphi de Borland est précédée par sa longueur sur 8-bit
ou 32-bit.
Par exemple:
924
...
Unicode
Souvent, ce qui est appelé Unicode est la méthode pour encoder des chaînes où
chaque caractère occupe 2 octets ou 16 bits. Ceci est une erreur de terminologie
répandue. Unicode est un standard pour assigner un nombre à chaque caractère
dans un des nombreux systèmes d’écriture dans le monde, mais ne décrit pas la
méthode d’encodage.
Les méthodes d’encodage les plus répandues sont: UTF-8 (est répandue sur Internet
et les systèmes *NIX) et UTF-16LE (est utilisé dans Windows).
UTF-8
UTF-8 est l’une des méthodes les plus efficace pour l’encodage des caractères. Tous
les symboles Latin sont encodés comme en ASCII, et les symboles après la table ASCII
sont encodés en utilisant quelques octets. 0 est encodé comme avant, donc toutes
les fonctions C de chaîne standard fonctionnent avec des chaînes UTF-8 comme avec
tout autre chaîne.
Voyons comment les symboles de divers langages sont encodés en UTF-8 et de quoi
ils ont l’air en FAR, en utilisant la page de code 4377 :
925
Fig. 5.2: FAR: UTF-8
UTF-16LE
926
Fig. 5.3: Hiew
Les chaînes avec des caractères qui occupent exactement 2 octets sont appelées
«Unicode » dans IDA :
.data :0040E000 aHelloWorld :
.data :0040E000 unicode 0, <Hello, world !>
.data :0040E000 dw 0Ah, 0
927
Fig. 5.5: Hiew: UTF-16LE
Ce que nous remarquons facilement, c’est que les symboles sont intercalés par le
caractère diamant (qui a le code ASCII 4). En effet, les symboles cyrilliques sont
situés dans le quatrième plan Unicode. Ainsi, tous les symboles cyrillique en UTF-
16LE sont situés dans l’intervalle 0x400-0x4FF.
Retournons à l’exemple avec la chaîne écrite dans de multiple langages. Voici à quoi
elle ressemble en UTF-16LE.
Ici nous pouvons aussi voir le BOM au début. Tous les caractères Latin sont intercalés
928
avec un octet à zéro.
Certains caractères avec signe diacritique (hongrois et islandais) sont aussi souli-
gnés en rouge.
Base64
L’encodage base64 est très répandu dans les cas où vous devez transférer des don-
nées binaires sous forme de chaîne de texte.
Pour l’essentiel, cet algorithme encode 3 octets binaires en 4 caractères imprimables:
toutes les 26 lettres Latin (à la fois minuscule et majuscule), chiffres, signe plus («+ »)
et signe slash («/ »), 64 caractères en tout.
Une particularité des chaînes base64 est qu’elles se terminent souvent (mais pas
toujours) par 1 ou 2 symbole égal («= ») pour l’alignement, par exemple:
AVjbbVSVfcUMu1xvjaMgjNtueRwBbxnyJw8dpGnLW8ZW8aKG3v4Y0icuQT+qEJAp9lAOuWs=
WVjbbVSVfcUMu1xvjaMgjNtueRwBbxnyJw8dpGnLW8ZW8aKG3v4Y0icuQT+qEJAp9lAOuQ==
Mettons ces 4 octets au forme binaire, puis regroupons les dans des groupes de
6-bit:
| 00 || 11 || 22 || 33 || || |
00000000000100010010001000110011????????????????
| A || B || E || i || M || w || = || = |
Les trois premiers octets (0x00, 0x11, 0x22) peuvent être encodés dans 4 caractères
base64 (“ABEi”), mais le dernier (0x33) — ne le peut pas, donc il est encodé en
utilisant deux caractères (“Mw”) et de symbole (“=”) de padding est ajouté deux fois
pour compléter le dernier groupe à 4 caractères. De ce fait, la longueur de toutes
les chaînes en base64 correctes est toujours divisible par 4.
Base64 est souvent utilisé lorsque des données binaires doivent être stockées dans
du XML. Les clefs PGP “Armored” (i.e., au format texte) et les signatures sont enco-
dées en utilisant base64.
Certains essayent d’utiliser base64 pour masquer des chaînes: http://blog.sec-consult.
com/2016/01/deliberately-hidden-backdoor-account-in.html9 .
Il existe des utilitaires pour rechercher des chaînes base64 dans des fichiers binaires
arbitraires. L’un d’entre eux est base64scanner10 .
9. http://archive.is/nDCas
10. https://github.com/DennisYurichev/base64scanner
929
Un autre système d’encodage qui était très répandu sur UseNet et FidoNet est l’Uuen-
coding. Les fichiers binaires sont toujours encodés au format Uuencode dans le ma-
gazine Phrack. Il offre à peu près la même fonctionnalité, mais il est différent de
base64 dans le sens où le nom de fichier est aussi stocké dans l’entête.
À propos: base64 à un petit frère: base32, alphabet qui a 10 chiffres et 26 caractères
Latin. Un usage répandu est les adresses onion11 , comme:
http://3g2upl4pq6kufc4m.onion/. URL ne peut pas avoir de mélange de casse
de caractères Latin, donc, c’est apparemment pourquoi les développeurs de Tor ont
utilisé base32.
11. https://trac.torproject.org/projects/tor/wiki/doc/HiddenServiceNames
930
Bind to port %s on %s.
Bind to port %s on %s failed : %.200s.
/bin/login
/bin/sh
/bin/sh /etc/ssh/sshrc
...
D$4PQWR1
D$4PUj
D$4PV
D$4PVj
D$4PW
D$4PWj
D$4X
D$4XZj
D$4Y
...
diffie-hellman-group-exchange-sha1
diffie-hellman-group-exchange-sha256
digests
D$iPV
direct-streamlocal
[email protected]
...
FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A6...
...
Il y a des options, des messages d’erreur, des chemins de fichier, des modules et des
fonctions importés dynamiquement, ainsi que d’autres chaînes étranges (clefs?). Il y
a aussi du bruit illisible—le code x86 à parfois des fragments constitués de caractères
ASCII imprimables, jusqu’à 8 caractères.
Bien sûr, OpenSSH est un programme open-source. Mais regarder les chaînes lisibles
dans un binaire inconnu est souvent une première étape d’analyse.
grep peut aussi être utilisé.
Hiew a la même capacité (Alt-F6), ainsi que ProcessMonitor de Sysinternals.
931
utilisée. Des cas drôles arrivent parfois12 .
Le message d’erreur peut aussi nous aider. Dans Oracle RDBMS, les erreurs sont
rapportées en utilisant un groupe de fonctions.
Vous pouvez en lire plus ici: blog.yurichev.com.
Il est possible de trouver rapidement quelle fonction signale une erreur et dans
quelles conditions.
À propos, ceci est souvent la raison pour laquelle les systèmes de protection contre
la copie utilisent des messages d’erreur inintelligibles ou juste des numéros d’erreur.
Personne n’est content lorsque le copieur de logiciel comprend comment fonctionne
la protection contre la copie seulement en lisant les messages d’erreur.
Un exemple de messages d’erreur chiffrés se trouve ici: 8.8.2 on page 1100.
Plus précisément, cette méthode de cacher des accès non documentés est appelée
«sécurité par l’obscurité ».
12. blog.yurichev.com
13. http://sekurak.pl/tp-link-httptftp-backdoor/
14. Request for Comments
932
5.5 Appels à assert()
Parfois, la présence de la macro assert() est aussi utile: En général, cette macro
laisse le nom du fichier source, le numéro de ligne et une condition dans le code.
L’information la plus utile est contenue dans la condition d’assert, nous pouvons en
déduire les noms de variables ou les noms de champ de la structure. Les autres
informations utiles sont les noms de fichier—nous pouvons essayer d’en déduire le
type de code dont il s’agit ici. Il est aussi possible de reconnaître les bibliothèques
open-source connues d’après les noms de fichier.
...
...
Il est recommandé de «googler » à la fois les conditions et les noms de fichier, qui
peuvent nous conduire à une bibliothèque open-source. Par exemple, si nous «goo-
glons » «sp->lzw_nbits <= BITS_MAX », cela va comme prévu nous donner du code
open-source relatif à la compression LZW.
5.6 Constantes
Les humains, programmeurs inclus, utilisent souvent des nombres ronds, comme 10,
100, 1000, dans la vie courante comme dans le code.
933
Le rétro ingénieur pratiquant connaît en général bien leur représentation décimale:
10=0xA, 100=0x64, 1000=0x3E8, 10000=0x2710.
Les constantes 0xAAAAAAAA (0b10101010101010101010101010101010) et
0x55555555 (0b01010101010101010101010101010101) sont aussi répandues—elles
sont composées d’alternance de bits.
Cela peut aider à distinguer un signal d’un signal dans lequel tous les bits sont à
1 (0b1111 …) ou à 0 (0b0000 …). Par exemple, la constante 0x55AA est utilisée
au moins dans le secteur de boot, MBR15 , et dans la ROM de cartes d’extention de
compatible IBM.
Certains algorithmes, particulièrement ceux de chiffrement, utilisent des constantes
distinctes, qui sont faciles à trouver dans le code en utilisant IDA.
Par exemple, l’algorithme MD5 initialise ses propres variables internes comme ceci:
var int h0 := 0x67452301
var int h1 := 0xEFCDAB89
var int h2 := 0x98BADCFE
var int h3 := 0x10325476
Si vous trouvez ces quatre constantes utilisées à la suite dans du code, il est très
probable que cette fonction soit relatives à MD5.
Un autre exemple sont les algorithmes CRC16/CRC32, ces algorithmes de calcul uti-
lisent souvent des tables pré-calculées comme celle-ci:
934
Ça peut être fait comme ceci: (buf pointe sur le début du fichier chargé en mémoire)
cmp [buf], 0x6468544D ; "MThd"
jnz _error_not_a_MIDI_file
…ou en appelant une fonction pour comparer des blocs de mémoire comme memcmp()
ou tout autre code équivalent jusqu’à une instruction CMPSB ( .1.6 on page 1350).
Lorsque vous trouvez un tel point, vous pouvez déjà dire que le chargement du
fichier MIDI commence, ainsi, vous pouvez voir l’endroit où se trouve le buffer avec
le contenu du fichier MIDI, ce qui est utilisé dans le buffer et comment.
Dates
Souvent, on peut rencontrer des nombres comme 0x19870116, qui ressemble clai-
rement à une date (année 1987, 1er mois (janvier), 16ème jour). Ça peut être la
date de naissance de quelqu’un (un programmeur, une de ses relations, un enfant),
ou une autre date importante. La date peut aussi être écrite dans l’ordre inverse,
comme 0x16011987. Les dates au format américain sont aussi courante, comme
0x01161987.
Un exemple célèbre est 0x19540119 (nombre magique utilisé dans la structure du
super-bloc UFS2), qui est la date de naissance de Marshall Kirk McKusick, éminent
contributeur FreeBSD.
Stuxnet utilise le nombre “19790509” (pas comme un nombre 32-bit, mais comme
une chaîne, toutefois), et ça a conduit à spéculer que le malware était relié à Israël17 .
Aussi, des nombres comme ceux-ci sont très répandus dans dans le chiffrement
niveau amateur, par exemple, extrait de la fonction secrète des entrailles du dongle
HASP318 :
void xor_pwd(void)
{
int i ;
pwd^=0x09071966 ;
for(i=0;i<8;i++)
{
al_buf[i]= pwd & 7; pwd = pwd >> 3;
}
};
935
for(j=0;j<8;j++)
{
seed *= 0x1989 ;
seed += 5;
ch[i] |= (tab[(seed>>9)&0x3f]) << (7-j) ;
}
}
}
DHCP
Ceci s’applique aussi aux protocoles réseaux. Par exemple, les paquets réseau du
protocole DHCP contiennent un soi-disant nombre magique : 0x63538263. Tout code
qui génère des paquets DHCP doit contenir quelque part cette constante à insérer
dans les paquets. Si nous la trouvons dans du code, nous pouvons trouver ce qui
s’y passe, et pas seulement ça. Tout programme qui peut recevoir des paquet DHCP
doit vérifier le cookie magique, et le comparer à cette constante.
Par exemple, prenons le fichier dhcpcore.dll de Windows 7 x64 et cherchons cette
constante. Et nous la trouvons, deux fois: Il semble que la constante soit utilisée
dans deux fonctions avec des noms parlants
DhcpExtractOptionsForValidation() et DhcpExtractFullOptions() :
Et:
936
s’est avéré que ce code prenait des fichiers audio de 12 canaux en entrée et les
traitait.
Et vice versa: par exemple, si un programme fonctionne avec des champs de texte
qui ont une longueur de 120 octets, il doit y avoir une constante 120 ou 119 quelque
part dans le code. Si UTF-16 est utilisé, alors 2 ⋅ 120. Si le code fonctionne avec des
paquets réseau de taille fixe, c’est une bonne idée de chercher cette constante dans
le code.
C’est aussi vrai pour le chiffrement amateur (clefs de licence, etc.). Si le bloc chif-
fré a une taille de n octets, vous pouvez essayer de trouver des occurrences de ce
nombre à travers le code. Aussi, si vous voyez un morceau de code qui est répété n
fois dans une boucle durant l’exécution, ceci peut être une routine de chiffrement/-
déchiffrement.
19. GitHub
937
EIP=0x2F64E919
FLAGS=PF IF
FPU ControlWord=IC RC=NEAR PC=64bits PM UM OM ZM DM IM
FPU StatusWord=
FPU ST(0) : 1.000000
Excel affiche 666 dans la cellule, achevant de nous convaincre que nous avons trouvé
le bon endroit.
938
Fig. 5.7: La blague a fonctionné
Si nous essayons la même version d’Excel, mais en x64, nous allons y trouver seule-
ment 12 instructions FDIV, et celle que nous cherchons est la troisième.
tracer.exe -l :excel.exe bpx=excel.exe !BASE+0x1B7FCC,set(st0,666)
Une exception à cette observation, qu’il est utile de noter, est le «canari » ( 1.26.3
on page 357). Sa génération et sa vérification sont souvent effectuées en utilisant
des instructions XOR.
939
Ce script awk peut être utilisé pour traité les fichiers listing (.lst) d’IDA :
gawk -e '$2=="xor" { tmp=substr($3, 0, length($3)-1) ; if (tmp !=$4) if($4⤦
Ç !="esp") if ($4 !="ebp") { print $1, $2, tmp, ",", $4 } }' filename.⤦
Ç lst
Il est aussi utile de noter que ce type de script peut aussi rapporter du code mal
désassemblé ( 5.11.1 on page 953).
940
En effet, si nous regardons dans le code source de WRK20 v1.2, ce code peut être
trouvé facilement dans le fichier
WRK-v1.2\base\ntos\ke\i386\cpu.asm.
D’après l’instruction RCL que j’ai pu trouver dans le fichier ntoskrnl.exe de Windows
2003 x86 (compilé avec MS Visual C compiler). Elle apparaît seulement une fois ici,
dans la fonction RtlExtendedLargeIntegerDivide() et ça pourrait être un cas de
code assembleur en ligne.
Cela peut aussi être utilisé pour des paquets réseau. Il est important que le nombre
magique soit unique et ne soit pas présent dans le code du programme.
À part tracer, DosBox (émulateur MS-DOS) en mode heavydebug est capable d’écrire
de l’information à propos de l’état de tous les registres pour chaque instruction du
programme exécutée dans un fichier texte21 , donc cette technique peut être utile
également pour des programmes DOS.
20. Windows Research Kernel
21. Voir aussi mon article de blog sur cette fonctionnalité de DosBox: blog.yurichev.com
941
5.10 Boucles
À chaque fois que votre programme travaille avec des sortes de fichier, ou un buffer
d’une certaine taille, il doit s’agir d’un sorte de boucle de déchiffrement/traitement
à l’intérieur du code.
Ceci est un exemple réel de sortie de l’outil tracer. Il y avait un code qui chargeait
une sorte de fichier chiffré de 258 octets. Je l’ai lancé dans l’intention d’obtenir le
nombre d’exécution de chaque instruction (l’outil DBI irait beaucoup mieux de nos
jours). Et j’ai rapidement trouvé un morceau de code qui était exécuté 259/258 fois:
...
942
Ç 0xef, 0xf7, 0xfc
0x45a725 e= 258 [JMP 45A6E4h]
0x45a727 e= 1 [PUSH 5]
0x45a729 e= 1 [MOV ECX, [EBP-254h]] [EBP-254h]=0x218fbd8
0x45a72f e= 1 [CALL 45B500h]
0x45a734 e= 1 [MOV ECX, EAX] EAX=0x218fbd8
0x45a736 e= 1 [CALL 45B710h]
0x45a73b e= 1 [CMP EAX, 5] EAX=5
...
943
Tableaux
944
Et voici un exemple de code MIPS très typique.
Comme nous pouvons nous en souvenir, chaque instruction MIPS (et aussi ARM en
mode ARM ou ARM64) a une taille de 32 bits (ou 4 octets), donc un tel code est un
tableau de valeurs 32-bit.
En regardant cette copie d’écran, nous voyons des sortes de schémas.
Les lignes rouge verticales ont été ajoutées pour la clarté:
Il y a un autre exemple de tel schéma ici dans le livre: 9.5 on page 1269.
945
Fichiers clairsemés
Ceci est un fichier clairsemé avec des données éparpillées dans un fichier presque
vide. Chaque caractère espace est en fait l’octet zéro (qui rend comme un espace).
Ceci est un fichier pour programmer des FPGA (Altera Stratix GX device). Bien sûr,
de tels fichiers peuvent être compressés facilement, mais des formats comme celui-
ci sont très populaire dans les logiciels scientifiques et d’ingénierie, où l’efficience
des accès est importante, tandis que la compacité ne l’est pas.
946
Fichiers compressés
Ce fichier est juste une archive compressée. Il a une entropie relativement haute et
visuellement, il à l’air chaotique. Ceci est ce à quoi ressemble les fichiers compressés
et/ou chiffrés.
947
CDFS23
Les fichiers d’installation d’un OS sont en général distribués sous forme de fichiers
ISO, qui sont des copies de disques CD/DVD. Le système de fichiers utilisé est appe-
lé CDFS, ce que vous voyez ici sont des noms de fichiers mixés avec des données
additionnelles. Ceci peut-être la taille des fichiers, des pointeurs sur d’autres réper-
toires, des attributs de fichier, etc. C’est l’aspect typique de ce à quoi ressemble un
système de fichiers en interne.
948
Code exécutable x86 32-bit
Voici l’allure de code exécutable x86 32-bit. Il n’a pas une grande entropie, car cer-
tains octets reviennent plus souvent que d’autres.
949
Fichiers graphique BMP
Les fichiers BMP ne sont pas compressés, donc chaque octet (ou groupe d’octet)
représente chaque pixel. J’ai trouvé cette image quelque part dans mon installation
de Windows 8.1:
Vous voyez que cette image a des pixels qui ne doivent pas pouvoir être compressés
beaucoup (autour du centre), mais il y a de longues lignes monochromes au haut
et en bas. En effet, de telles lignes ressemblent à des lignes lorsque l’on regarde le
fichier:
950
Fig. 5.15: Fragment de fichier BMP
951
savaient à quelles adresses se trouvaient les octets qui devaient être modifiés (en
utilisant l’instruction BASIC POKE) pour le bidouiller. Ceci à conduit à des listes de
«cheat » qui contenaient les instructions POKE publiées dans des magazines relatifs
aux jeux 8-bit.
De même, il est facile de modifier le fichier des «meilleurs scores », ceci ne fonctionne
pas seulement avec des jeux 8-bit. Notez votre score et sauvez le fichier quelque
part. Lorsque le décompte des «meilleurs scores » devient différent, comparez juste
les deux fichiers, ça peut même être fait avec l’utilitaire DOS FC25 (les fichiers des
«meilleurs scores » sont souvent au format binaire).
Il y aura un endroit où quelques octets seront différents et il est facile de voir lesquels
contiennent le score. Toutefois, les développeurs de jeux étaient conscient de ces
trucs et pouvaient protéger le programme contre ça.
Exemple quelque peu similaire dans ce livre: 9.3 on page 1255.
C’était un temps de l’engouement pour la messagerie ICQ, au moins dans les pays
de l’ex-URSS. Cette messagerie avait une particularité — certains utilisateurs ne vou-
laient pas partager leur état en ligne avec tout le monde. Et vous deviez demander
une autorisation à cet utilisateur. Il pouvait vous autoriser à voir son état, ou pas.
Voici ce que j’ai fait:
• Ajouté un utilisateur.
• Un utiliseur est apparu dans la liste de contact, dans la section “attente d’au-
torisation”.
• Fermé ICQ.
• Sauvegardé la base de données ICQ.
• Ouvert à nouveau ICQ.
• L’utilisateur m’a autorisé.
• Refermé ICQ et comparé les deux base de données.
Il s’est avéré que: les deux bases de données ne différaient que d’un octet. Dans
la première version: RESU\x03, dans la seconde: RESU\x02. (“RESU”, signifie proba-
blement “USER”, i.e., un entête d’une structure où toutes les informations à propos
d’un utilisateur étaient stockées.) Cela signifie que l’information sur l’autorisation
n’était pas stockée sur le serveur, mais sur le client. Vraisemblablement, la valeur
2/3 reflétait l’état de l’«autorisation ».
Registres de Windows
Il est aussi possible de comparer les registres de Windows avant et après l’installation
d’un programme.
25. Utilitaire MS-DOS pour comparer des fichiers binaires.
952
C’est une méthode courante que de trouver quels sont les éléments des registres
utilisés par le programme. Peut-être que ceci est la raison pour laquelle le shareware
de «nettoyage des registres windows » est si apprécié.
À propos, voici comment sauver les registres de Windows dans des fichiers texte:
reg export HKLM HKLM.reg
reg export HKCU HKCU.reg
reg export HKCR HKCR.reg
reg export HKU HKU.reg
reg export HKCC HKCC.reg
Si un logiciel utilise des fichiers propriétaires, vous pouvez aussi les examiner. Sau-
vez un fichier. Puis, ajouter un point ou une ligne ou une autre primitive. Sauvez le
fichier, comparez. Ou déplacez un point, sauvez le fichier, comparez.
Comparateur à clignotement
953
Désassemblage depuis une adresse de début incorrecte (x86)
954
jnz short loc_402D7B
955
db 36h
pusha
stosb
test [ebx], ebx
sub al, 0D3h ; 'L'
pop eax
stosb
pop edx
out 0B0h, al
lodsb
push ebx
cdq
out dx, al
sub al, 0Ah
sti
outsd
add dword ptr [edx], 96FCBE4Bh
and eax, 0E537EE4Fh
inc esp
stosd
cdq
push ecx
in al, 0CBh
mov ds :0D114C45Ch, al
mov esi, 659D1985h
db 2Fh ; /
pop rsp
db 64h
retf 0E993h
956
cmp ah, [rax+4Ah]
movzx rsi, dword ptr [rbp-25h]
push 4Ah
movzx rdi, dword ptr [rdi+rdx*8]
db 9Ah
db 16h
957
LDCCS p9, c13, [R6,#0x1BC]
LDRGE R8, [R9,#0x66E]
STRNEB R5, [R8],#-0x8C3
STCCSL p15, c9, [R7,#-0x84]
RSBLS LR, R2, R11,ASR LR
SVCGT 0x9B0362
SVCGT 0xA73173
STMNEDB R11 !, {R0,R1,R4-R6,R8,R10,R11,SP}
STR R0, [R3],#-0xCE4
LDCGT p15, c8, [R1,#0x2CC]
LDRCCB R1, [R11],-R7,ROR#30
BLLT 0xFED9D58C
BL 0x13E60F4
LDMVSIB R3 !, {R1,R4-R7}^
USATNE R10, #7, SP,LSL#11
LDRGEB LR, [R1],#0xE56
STRPLT R9, [LR],#0x567
LDRLT R11, [R1],#-0x29B
SVCNV 0x12DB29
MVNNVS R5, SP,LSL#25
LDCL p8, c14, [R12,#-0x288]
STCNEL p2, c6, [R6,#-0xBC]!
SVCNV 0x2E5A2F
BLX 0x1A8C97E
TEQGE R3, #0x1100000
STMLSIA R6, {R3,R6,R10,R11,SP}
BICPLS R12, R2, #0x5800
BNE 0x7CC408
TEQGE R2, R4,LSL#20
SUBS R1, R11, #0x28C
BICVS R3, R12, R7,ASR R0
LDRMI R7, [LR],R3,LSL#21
BLMI 0x1A79234
STMVCDB R6, {R0-R3,R6,R7,R10,R11}
EORMI R12, R6, #0xC5
MCRRCS p1, 0xF, R1,R3,c2
958
DCB 0x17
DCB 0xED
.byte 0x17
.byte 0xED
.byte 0x4B # K
.byte 0x54 # T
959
sltiu $t6, $a3, -0x66AD
lb $t7, -0x4F6($t3)
sd $fp, 0x4B02($a1)
960
Donc j’ai essayé toutes les fonctions qui suivaient Twofish::Base::UncheckedSetKey()—
comme elles arrivaient,
une a été Twofish::Enc::ProcessAndXorBlock(),
une autre—Twofish::Dec::ProcessAndXorBlock().
5.12.4 C++
Les données RTTI ( 3.21.1 on page 735)- peuvent être utiles pour l’identification des
classes C++.
961
Chapitre 6
Spécifique aux OS
6.1.2 stdcall
Similaire à cdecl, sauf que c’est l’appelé qui doit réinitialise ESP à sa valeur d’origine
en utilisant l’instruction RET x et non pas RET,
avec x = nb arguments * sizeof(int)1 . Après le retour du callee, l’appelant ne
modifie pas le pointeur de pile. Il ny’ a donc pas d’instruction add esp, x.
1. La taille d’une variable de type int est de 4 octets sur les systèmes x86 et de 8 octets sur les systèmes
x64
962
function :
;... do something ...
ret 12
Cette méthode est omniprésente dans les librairies standard win32, mais pas dans
celles win64 (voir ci-dessous pour win64).
Prenons par exemple la fonction 1.89 on page 131 et modifions la légèrement en
utilisant la convention __stdcall :
int __stdcall f2 (int a, int b, int c)
{
return a*b+c ;
};
; ...
push 3
push 2
push 1
call _f2@12
push eax
push OFFSET $SG81369
call _printf
add esp, 8
Les fonctions du style printf() sont un des rares cas de fonctions à nombre d’ar-
guments variables en C/C++. Elles permettent d’illustrer une différence importante
963
entre les conventions cdecl et stdcall. Partons du principe que le compilateur connait
le nombre d’arguments utilisés à chaque fois que la fonction printf() est appelée.
En revanche, la fonction printf() est déjà compilée dans MSVCRT.DLL (si l’on parle
de Windows) et ne possède aucune information sur le nombre d’arguments qu’elle
va recevoir. Elle peut cependant le deviner à partir du contenu du paramètre format.
Si la convention stdcall était utilisé pour la fonction printf(), elle devrait réajuster
le pointeur de pile à sa valeur initiale en comptant le nombre d’arguments dans la
chaîne de format. Cette situation serait dangereuse et pourrait provoquer un crash
du programme à la moindre faute de frappe du programmeur. La convention stdcall
n’est donc pas adaptée à ce type de fonction. La convention cdecl est préférable.
6.1.3 fastcall
Il s’agit d’un nom générique pour désigner les conventions qui passent une partie des
paramètres dans des registres et le reste sur la pile. Historiquement, cette méthode
était plus performante que les conventions cdecl/stdcall - car elle met moins de
pression sur la pile. Ce n’est cependant probablement plus le cas sur les processeurs
actuels qui sont beaucoup plus complexes.
Cette convention n’est pas standardisée. Les compilateurs peuvent donc l’implé-
menter à leur guise. Prenez une DLL qui en utilise une autre compilée avec une
interprétation différente de la convention fastcall. Vous avez un cas d’école et des
problèmes en perspective.
Les compilateurs MSVC et GCC passent les deux premiers arguments dans ECX et
EDX, et le reste des arguments sur la pile.
Le pointeur de pile doit être restauré à sa valeur initiale par l’appelé (comme pour
la convention stdcall).
function :
.. do something ..
ret 4
964
Listing 6.5: MSVC 2010 optimisé/Ob0
_c$ = 8 ; size = 4
@f3@12 PROC
; _a$ = ecx
; _b$ = edx
mov eax, ecx
imul eax, edx
add eax, DWORD PTR _c$[esp-4]
ret 4
@f3@12 ENDP
; ...
mov edx, 2
push 3
lea ecx, DWORD PTR [edx-1]
call @f3@12
push eax
push OFFSET $SG81390
call _printf
add esp, 8
Nous voyons que l’appelé ajuste SP en utilisant l’instruction RETN suivie d’un opé-
rande.
Le nombre d’arguments peut, encore une fois, être facilement déduit.
GCC regparm
Watcom/OpenWatcom
965
6.1.4 thiscall
Cette convention passe le pointeur d’objet this à une méthode en C++.
Le compilateur MSVC, passe généralement le pointeur this dans le registre ECX.
Le compilateur GCC passe le pointeur this comme le premier argument de la fonc-
tion. Thus it will be very visible that internally: all function-methods have an extra
argument.
Pour un example, voir ( 3.21.1 on page 715).
6.1.5 x86-64
Windows x64
int main()
{
f1(1,2,3,4,5,6,7) ;
};
main PROC
sub rsp, 72
966
mov r9d, 4
mov r8d, 3
mov edx, 2
mov ecx, 1
call f1
a$ = 80
b$ = 88
c$ = 96
d$ = 104
e$ = 112
f$ = 120
g$ = 128
f1 PROC
$LN3 :
mov DWORD PTR [rsp+32], r9d
mov DWORD PTR [rsp+24], r8d
mov DWORD PTR [rsp+16], edx
mov DWORD PTR [rsp+8], ecx
sub rsp, 72
add rsp, 72
ret 0
f1 ENDP
Nous vyons ici clairement que des 7 arguments, 4 sont passés dans les registres et
les 3 suivants sur la pile.
Le prologue du code de la fonction f1() sauvegarde les arguments dans le «scratch
space »—un espace sur la pile précisément prévu à cet effet.
Le compilateur agit ainsi car il n’est pas certain par avance qu’il disposera de suf-
fisamment de registres pour toute la fonction en l’absence des 4 utilisés par les
paramètres.
967
L’appelant est responsable de l’allocation du «scratch space » sur la pile.
a$ = 80
b$ = 88
c$ = 96
d$ = 104
e$ = 112
f$ = 120
g$ = 128
f1 PROC
$LN3 :
sub rsp, 72
add rsp, 72
ret 0
f1 ENDP
main PROC
sub rsp, 72
mov edx, 2
mov DWORD PTR [rsp+48], 7
mov DWORD PTR [rsp+40], 6
lea r9d, QWORD PTR [rdx+2]
lea r8d, QWORD PTR [rdx+1]
lea ecx, QWORD PTR [rdx-1]
mov DWORD PTR [rsp+32], 5
call f1
968
Notez également la manière dont MSVC 2012 optimise le chargement de certaines
valeurs litérales dans les registres en utilisant LEA ( .1.6 on page 1345). L’instruction
MOV utiliserait 1 octet de plus (5 au lieu de 4).
8.2.1 on page 1051 est un autre exemple de cette pratique.
Le pointeur this est passé dans le registre RCX, le premier argument de la méthode
dans RDX, etc. Pour un exemple, voir : 3.21.1 on page 718.
Linux x64
Le passage d’arguments par Linux pour x86-64 est quasiment identique à celui de
Windows, si ce n’est que 6 registres sont utilisés au lieu de 4 (RDI, RSI, RDX, RCX,
R8, R9) et qu’il n’existe pas de «scratch space ». L’glslinkcalleeappelé conserve la
possibilité de sauvegarder les registres sur la pile s’il le souhaite ou en a besoin.
969
N.B.: Les valeurs sont enregistrées dans la partie basse des registres (e.g., EAX) et
non pas dans la totalité du registre 64 bits (RAX). Ceci s’explique par le fait que
l’écriture des 32 bits de poids faible du registre remet automatiquement à zéro les
32 bits de poids fort. On suppose qu’AMD a pris cette décision afin de simplifier le
portage du code 32 bits vers l’architecture x86-64.
970
Donc, oui, la valeur des arguments peut être modifiée sans problème. Sous réserver
que l’argument ne soit pas une references en C++ ( 3.21.3 on page 737), et que
vous ne modifiez pas la valeur qui est référencée par un pointeur, l’effet de votre
modification ne se propagera pas au dehors de la fonction.
En théorie, après le retour de l’appelé, l’appelant pourrait récupérer l’argument mo-
difié et l’utiliser à sa guise. Ceci pourrait peut être se faire dans un programme rédigé
en assembleur.
Par exemple, un compilateur C/C++ génèrera un code comme celui-ci :
push 456 ; will be b
push 123 ; will be a
call f ; f() modifies its first argument
add esp, 2*4
Il est difficile d’imaginer pourquoi quelqu’un aurait besoin d’agir ainsi, mais en pra-
tique c’est possible. Toujours est-il que les langages C/C++ ne permettent pas de
faire ainsi.
void f (int a)
{
modify_a (&a) ;
printf ("%d\n", a) ;
};
_a$ = 8
_f PROC
971
lea eax, DWORD PTR _a$[esp-4] ; just get the address of value in
local stack
push eax ; and pass it to modify_a()
call _modify_a
mov ecx, DWORD PTR _a$[esp] ; reload it from the local stack
push ecx ; and pass it to printf()
push OFFSET $SG2796 ; '%d'
call _printf
add esp, 12
ret 0
_f ENDP
L’argument a est placé sur la pile et l’adresse de cet emplacement de pile est passé
à une autre fonction. Celle-ci modifie la valeur à l’adresse référencée par le pointeur,
puis printf() affiche la valeur après modification.
Le lecteur attentif se demandera peut-être ce qu’il en est avec les conventions d’ap-
pel qui utilisent les registres pour passer les arguments.
C’est justement une des utilisations du Shadow Space.
La valeur en entrée est copiée du registre vers le Shadow Space dans la pile locale,
puis l’adresse de la pile locale est passée à la fonction appelée:
a$ = 48
f PROC
mov DWORD PTR [rsp+8], ecx ; save input value in Shadow Space
sub rsp, 40
lea rcx, QWORD PTR a$[rsp] ; get address of value and pass it
to modify_a()
call modify_a
mov edx, DWORD PTR a$[rsp] ; reload value from Shadow Space
and pass it to printf()
lea rcx, OFFSET FLAT :$SG2994 ; '%d'
call printf
add rsp, 40
ret 0
f ENDP
972
mov edx, DWORD PTR [rsp+12] ; reload value from the local stack
and pass it to printf()
mov esi, OFFSET FLAT :.LC0 ; '%d'
mov edi, 1
xor eax, eax
call __printf_chk
add rsp, 24
ret
Enfin, nous constatons un usage similaire du Shadow Space ici: 3.17.1 on page 683.
( https://docs.python.org/3/library/ctypes.html )3
3. NDT:Projet de traduction en français: https://docs.python.org/fr/3/library/ctypes.html
973
En fait, nous pouvons modifier le module ctypes (ou tout autre module d’appel), afin
qu’il appelle avec succès des fonctions externes cdecl ou stdcall, sans connaître, ce
qui se trouve où. (Le nombre d’arguments, toutefois, doit être spécifié).
Il est possible de le résoudre en utilisant environ 5 à 10 instructions assembleur x86
dans l’appelant. Essayez de trouver ça.
Je voulais voir quelles fonctions étaient appelées lors de l’exécution, et quand. Tou-
tefois, j’étais pressé et n’avais pas le temps de déduire le nombre d’arguments pour
chaque fonction, encore moins pour les types des données. Donc chaque fonction
dans ma DLL de remplacement n’avait aucun argument que ce soit. Mais tout fonc-
tionnait, car toutes les fonctions utilisaient la convention d’appel cdecl. (Ça ne fonc-
tionnerait pas si les fonctions avaient la convention d’appel stdcall.) Ça a aussi fonc-
tionné pour la version x64.
Et puis j’ai fait une étape de plus: j’ai déduit les types des arguments pour certaines
fonctions. Mais j’ai fait quelques erreurs, par exemple, la fonction originale prend 3
arguments, mais je n’en ai découvert que 2, etc.
Cependant, ça fonctionnait. Au début, ma DLL de remplacement ignorait simplement
tous les arguments. Puis, elle ignorait le 3ème argument.
974
indique que chaque thread possède sa propre copie de la variable, qui peut-être
initialisée et est alors conservée dans l’espace TLS 4 :
int main()
{
std ::cout << tmp << std ::endl ;
};
Compilé avec MinGW GCC 4.8.1, mais pas avec MSVC 2012.
Dans le contexte des fichiers au format PE, la variable tmp sera allouée dans la
section dédiée au TLS du fichier exécutable résultant de la compilation.
Win32
975
17 {
18 rand_state=rand_state*RNG_a ;
19 rand_state=rand_state+RNG_c ;
20 return rand_state & 0x7fff ;
21 }
22
23 int main()
24 {
25 my_srand(0x12345678) ;
26 printf ("%d\n", my_rand()) ;
27 };
Hiew nous montre alors qu’il existe une nouvelle section nommée .tls dans le fichier
PE.
Listing 6.15: avec optimisation MSVC 2013 x86
_TLS SEGMENT
_rand_state DD 01H DUP (?)
_TLS ENDS
_DATA SEGMENT
$SG84851 DB '%d', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
_init$ = 8 ; size = 4
_my_srand PROC
; FS:0=address of TIB
mov eax, DWORD PTR fs :__tls_array ; displayed in IDA as FS:2Ch
; EAX=address of TLS of process
mov ecx, DWORD PTR __tls_index
mov ecx, DWORD PTR [eax+ecx*4]
; ECX=current TLS segment
mov eax, DWORD PTR _init$[esp-4]
mov DWORD PTR _rand_state[ecx], eax
ret 0
_my_srand ENDP
_my_rand PROC
; FS:0=address of TIB
mov eax, DWORD PTR fs :__tls_array ; displayed in IDA as FS:2Ch
; EAX=address of TLS of process
mov ecx, DWORD PTR __tls_index
mov ecx, DWORD PTR [eax+ecx*4]
; ECX=current TLS segment
imul eax, DWORD PTR _rand_state[ecx], 1664525
add eax, 1013904223 ; 3c6ef35fH
mov DWORD PTR _rand_state[ecx], eax
and eax, 32767 ; 00007fffH
ret 0
_my_rand ENDP
_TEXT ENDS
976
La variable rand_state se trouve donc maintenant dans le segment TLS et chaque
thread en possède sa propre version de cette variable.
Voici comment elle est accédée. L’adresse du TIB est chargée depuis FS:2Ch, un
index est ajouté (si nécessaire), puis l’adresse du segment TLS est calculée.
Il est ainsi possible d’accéder la valeur de la variable rand_state au travers du
registre ECX qui contient une adresse propre à chaque thread.
Le sélecteur FS: est connu de tous les rétro-ingénieur. Il est spécifiquement utilisé
pour toujours contenir l’adresse du TIB du thread en cours d’exécution. L’accès aux
données propres à chaque thread est donc une opération performante.
En environnement Win64, c’est le sélecteur GS: qui est utilisé pour ce faire. L’adresse
de l’espace TLS y est conservé à l’offset 0x58 :
_DATA SEGMENT
$SG85451 DB '%d', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT
init$ = 8
my_srand PROC
mov edx, DWORD PTR _tls_index
mov rax, QWORD PTR gs :88 ; 58h
mov r8d, OFFSET FLAT :rand_state
mov rax, QWORD PTR [rax+rdx*8]
mov DWORD PTR [r8+rax], ecx
ret 0
my_srand ENDP
my_rand PROC
mov rax, QWORD PTR gs :88 ; 58h
mov ecx, DWORD PTR _tls_index
mov edx, OFFSET FLAT :rand_state
mov rcx, QWORD PTR [rax+rcx*8]
imul eax, DWORD PTR [rcx+rdx], 1664525 ; 0019660dH
add eax, 1013904223 ; 3c6ef35fH
mov DWORD PTR [rcx+rdx], eax
and eax, 32767 ; 00007fffH
ret 0
my_rand ENDP
_TEXT ENDS
977
Imaginons maintenant que nous voulons nous prémunir des erreurs de program-
mation en initialisant systématiquement la variable rand_state avec une valeur
constante (ligne 9) :
1 #include <stdint.h>
2 #include <windows.h>
3 #include <winnt.h>
4
5 // from the Numerical Recipes book:
6 #define RNG_a 1664525
7 #define RNG_c 1013904223
8
9 __declspec( thread ) uint32_t rand_state=1234;
10
11 void my_srand (uint32_t init)
12 {
13 rand_state=init ;
14 }
15
16 int my_rand ()
17 {
18 rand_state=rand_state*RNG_a ;
19 rand_state=rand_state+RNG_c ;
20 return rand_state & 0x7fff ;
21 }
22
23 int main()
24 {
25 printf ("%d\n", my_rand()) ;
26 };
Le code ne semble pas différent de celui que nous avons étudié. Pourtant dans IDA
nous constatons:
.tls :00404000 ; Segment type: Pure data
.tls :00404000 ; Segment permissions: Read/Write
.tls :00404000 _tls segment para public 'DATA' use32
.tls :00404000 assume cs :_tls
.tls :00404000 ;org 404000h
.tls :00404000 TlsStart db 0 ; DATA XREF:
.rdata:TlsDirectory
.tls :00404001 db 0
.tls :00404002 db 0
.tls :00404003 db 0
.tls :00404004 dd 1234
.tls :00404008 TlsEnd db 0 ; DATA XREF:
.rdata:TlsEnd_ptr
...
La valeur 1234 est bien présente. Chaque fois qu’un nouveau thread est créé, un
nouvel espace TLS est alloué pour ses besoins et toutes les données - y compris
1234 - y sont copiées.
Considérons le scénario hypothétique suivant:
978
• Le thread A démarre. Un espace TLS est créé pour ses besoins et la valeur 1234
est copiée dans rand_state.
• La fonction my_rand() est invoquée plusieurs fois par le thread A.
La valeur de la variable rand_state est maintenant différente de 1234.
• Le thread B démarre. Un espace TLS est créé pour ses besoins et la valeur
1234 est copiée dans rand_state. Dans le thread A, la valeur de rand_state
demeure différente de 1234.
#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback ;
#pragma data_seg()
int my_rand ()
{
979
rand_state=rand_state*RNG_a ;
rand_state=rand_state+RNG_c ;
return rand_state & 0x7fff ;
}
int main()
{
// rand_state is already initialized at the moment (using
GetTickCount())
printf ("%d\n", my_rand()) ;
};
...
...
Les fonctions de rappel TLS sont parfois utilisées par les mécanismes de décompres-
sion d’exécutable afin d’en rendre le fonctionnement plus obscure.
Cette pratique peut en laisser certains dans le noir parce qu’ils auront omis de consi-
dérer qu’un fragment de code a pu s’exécuter avant l’OEP5 .
Linux
Voyons maintenant comment une variable globale conservée dans l’espace de sto-
ckage propre au thread est déclarée avec GCC:
__thread uint32_t rand_state=1234;
980
Il ne s’agit pas du modificateur standard C/C++ modifier, mais bien d’un modifica-
teur spécifique à GCC 6 .
Le sélecteur GS: est utilisé lui aussi pour accéder au TLS, mais d’une manière un
peu différente:
Pour en savoir plus: [Ulrich Drepper, ELF Handling For Thread-Local Storage, (2013)]7 .
981
Les appels systèmes (syscall-s) sont un point où deux espaces sont connectés.
On peut dire que c’est la principale API fournie aux applications.
Comme dans Windows NT, la table des appels système se trouve dans la SSDT9 .
L’utilisation des appels système est très répandue parmi les auteurs de shellcode et
de virus, car il est difficile de déterminer l’adresse des fonctions nécessaires dans
les bibliothèques système, mais il est simple d’utiliser les appels système. Toutefois,
il est nécessaire d’écrire plus de code à cause du niveau d’abstraction plus bas de
l’API.
Il faut aussi noter que les numéros des appels systèmes peuvent être différent dans
différentes versions d’un OS.
6.3.1 Linux
Sous Linux, un appel système s’effectue d’habitude par int 0x80. Le numéro de
l’appel est passé dans le registre EAX, et tout autre paramètre —dans les autres
registres.
_start :
mov edx,len ; buffer len
mov ecx,msg ; buffer
mov ebx,1 ; file descriptor. 1 is for stdout
mov eax,4 ; syscall number. 4 is for sys_write
int 0x80
section .data
Compilation:
nasm -f elf32 1.s
ld 1.o
982
6.3.2 Windows
Ici, ils sont appelés via int 0x2e ou en utilisant l’instruction x86 spéciale SYSENTER.
La liste complète des appels systèmes sous Windows: http://j00ru.vexillium.
org/ntapi/.
Pour aller plus loin:
«Windows Syscall Shellcode » par Piotr Bania: http://www.symantec.com/connect/
articles/windows-syscall-shellcode.
6.4 Linux
6.4.1 Code indépendant de la position
Lorsque l’on analyse des bibliothèques partagées sous Linux (.so), on rencontre sou-
vent ce genre de code:
...
...
...
983
.text :00057A15 call __assert_fail
Tous les pointeurs sur des chaînes sont corrigés avec une certaine constante et la
valeur de EBX, qui est calculée au début de chaque fonction.
C’est ce que l’on appelle du code PIC, qui peut être exécuté à n’importe quelle
adresse mémoire, c’est pourquoi il ne doit contenir aucune adresse absolue.
PIC était crucial dans les premiers ordinateurs, et l’est encore aujourd’hui dans les
systèmes embarqués sans support de la mémoire virtuelle (où tous les processus se
trouvent dans un seul bloc continu de mémoire).
C’est encore utilisé sur les systèmes *NIX pour les bibliothèques partagées, car elles
sont utilisées par de nombreux processus mais ne sont chargées qu’une seule fois en
mémoire. Mais tous ces processus peuvent mapper la même bibliothèque partagée
à des adresses différentes, c’est pourquoi elles doivent fonctionner correctement
sans aucune adresse absolue.
Faisons un petit essai:
#include <stdio.h>
int global_variable=123;
Compilons le avec GCC 4.7.3 et examinons le fichier .so généré dans IDA :
gcc -fPIC -shared -O3 -o 1.so 1.c
984
.text :00000570 arg_0 = dword ptr 4
.text :00000570
.text :00000570 sub esp, 1Ch
.text :00000573 mov [esp+1Ch+var_8], ebx
.text :00000577 call __x86_get_pc_thunk_bx
.text :0000057C add ebx, 1A84h
.text :00000582 mov [esp+1Ch+var_4], esi
.text :00000586 mov eax, ds :(global_variable_ptr - ⤦
Ç 2000h)[ebx]
.text :0000058C mov esi, [eax]
.text :0000058E lea eax, (aReturningD - 2000h)[ebx] ;
"returning %d\n"
.text :00000594 add esi, [esp+1Ch+arg_0]
.text :00000598 mov [esp+1Ch+var_18], eax
.text :0000059C mov [esp+1Ch+var_1C], 1
.text :000005A3 mov [esp+1Ch+var_14], esi
.text :000005A7 call ___printf_chk
.text :000005AC mov eax, esi
.text :000005AE mov ebx, [esp+1Ch+var_8]
.text :000005B2 mov esi, [esp+1Ch+var_4]
.text :000005B6 add esp, 1Ch
.text :000005B9 retn
.text :000005B9 f1 endp
C’est ça: les pointeurs sur «returning %d\n» et global_variable sont corrigés à chaque
exécution de la fonction.
La fonction __x86_get_pc_thunk_bx() renvoie dans EBX l’adresse de l’instruction
après son appel (0x57C ici).
C’est un moyen simple d’obtenir la valeur du compteur de programme (EIP) à un
endroit quelconque. La constante 0x1A84 est relative à la différence entre le début
de cette fonction et ce que l’on appelle Global Offset Table Procedure Linkage Table
(GOT PLT), la section juste après la Global Offset Table (GOT), où se trouve le pointeur
sur global_variable. IDA montre ces offsets sous leur forme calculée pour rendre les
choses plus facile à comprendre, mais en fait, le code est:
.text :00000577 call __x86_get_pc_thunk_bx
.text :0000057C add ebx, 1A84h
.text :00000582 mov [esp+1Ch+var_4], esi
.text :00000586 mov eax, [ebx-0Ch]
.text :0000058C mov esi, [eax]
.text :0000058E lea eax, [ebx-1A30h]
Ici, EBX pointe sur la section GOT PLT et pour calculer le pointeur sur global_variable
( qui est stocké dans la GOT), il faut soustraire 0xC.
Pour calculer la valeur du pointeur sur la chaîne «returning %d\n», il faut soustraire
0x1A30.
A propos, c’est la raison pour laquelle le jeu d’instructions AMD64 ajoute le support
d’adressage relatif de RIP10 — pour simplifier le code PIC.
10. compteur de programme sur AMD64
985
Compilons le même code C en utilisant la même version de GCC, mais pour x64.
IDA simplifierait le code en supprimant les détails de l’adressage relatif à RIP, donc
utilisons objdump à la place d’IDA pour tout voir:
0000000000000720 <f1> :
720: 48 8b 05 b9 08 20 00 mov rax,QWORD PTR [rip+0x2008b9] ⤦
Ç ;
200fe0 <_DYNAMIC+0x1d0>
727: 53 push rbx
728: 89 fb mov ebx,edi
72a : 48 8d 35 20 00 00 00 lea rsi,[rip+0x20] ;
751 <_fini+0x9>
731: bf 01 00 00 00 mov edi,0x1
736: 03 18 add ebx,DWORD PTR [rax]
738: 31 c0 xor eax,eax
73a : 89 da mov edx,ebx
73c : e8 df fe ff ff call 620 <__printf_chk@plt>
741: 89 d8 mov eax,ebx
743: 5b pop rbx
744: c3 ret
Windows
Le mécanisme PIC n’est pas utilisé dans les DLLs de Windows. Si le chargeur de
Windows doit charger une DLL à une autre adresse, il «patche » la DLL en mémoire
(aux places FIXUP) afin de corriger toutes les adresses.
Cela implique que plusieurs processus Windows ne peuvent pas partager une DLL
déjà chargée à une adresse différente dans des blocs mémoire de différents proces-
sus — puisque chaque instance qui est chargée en mémoire est fixé pour fonctionner
uniquement à ces adresses...
Regardons si l’on peut tromper l’utilitaire uptime. Comme chacun le sait, il indique
986
depuis combien de temps l’ordinateur est démarré. Avec l’aide de strace( 7.2.3 on
page 1041), on voit que ces informations proviennent du fichier /proc/uptime :
$ strace uptime
...
open("/proc/uptime", O_RDONLY) = 3
lseek(3, 0, SEEK_SET) = 0
read(3, "416166.86 414629.38\n", 2047) = 20
...
Ce n’est pas un fichier réel sur le disque, il est virtuel et généré au vol par le noyau
Linux. Il y a seulement deux nombres:
$ cat /proc/uptime
416690.91 415152.03
11
Ce que l’on peut apprendre depuis Wikipédia :
Essayons d’écrire notre propre bibliothèque dynamique, avec les fonctions open(),
read(), close() fonctionnant comme nous en avons besoin.
Tout d’abord, notre fonction open() va comparer le nom du fichier à ouvrir avec celui
que l’on veut modifier, et si c’est le cas, sauver le descripteur du fichier ouvert.
Ensuite, si read() est appelé avec ce descripteur de fichier, nous allons substituer
la sortie, et dans les autres cas, nous appellerons la fonction read() originale de
libc.so.6. Et enfin close() fermera le fichier que nous suivons.
Nous allons utiliser les fonctions dlopen() et dlsym() pour déterminer les adresses
des fonctions originales dans libc.so.6.
Nous en avons besoin pour appeler les fonctions «réelles ».
D’un autre côté, si nous interceptons strcmp() et surveillons chaque comparaison de
chaînes dans le programme, nous pouvons alors implémenter notre propre fonction
strcmp(), et ne pas utiliser la fonction originale du tout.
12
.
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
11. Wikipédia
12. Par exemple, voilà comment implémenter une interception simple de strcmp() dans cet article 13
987
void *libc_handle = NULL ;
int (*open_ptr)(const char *, int) = NULL ;
int (*close_ptr)(int) = NULL ;
ssize_t (*read_ptr)(int, void*, size_t) = NULL ;
inited = true ;
}
988
};
if (fd==opened_fd)
opened_fd=0; // the file is not opened anymore
return (*close_ptr)(fd) ;
};
( Code source )
Compilons le comme une bibliothèque dynamique standard:
gcc -fpic -shared -Wall -o fool_uptime.so fool_uptime.c -ldl
Et nous voyons:
01:23:02 up 24855 days, 3:14, 3 users, load average : 0.00, 0.01, 0.05
Plus d’exemples:
• Interception simple de strcmp() (Yong Huang) https://yurichev.com/mirrors/
LD_PRELOAD/Yong%20Huang%20LD_PRELOAD.txt
• Kevin Pulo—Jouons avec LD_PRELOAD. De nombreux exemples et idées. yuri-
chev.com
• Interception des fonctions de fichier pour compresser/décompresser les fichiers
au vol (zlibc) ftp://metalab.unc.edu/pub/Linux/libs/compression
989
6.5 Windows NT
6.5.1 CRT (win32)
L’exécution du programme débute donc avec la fonction main() ? Non, pas du tout.
Ouvrez un exécutable dans IDA ou HIEW, et vous constaterez que l’OEP pointe sur
un bloc de code qui se situe ailleurs.
Ce code prépare l’environnement avant de passer le contrôle à notre propre code.
Ce fragment de code initial est dénommé CRT (pour C RunTime).
990
11 mov eax, 5A4Dh
12 cmp ds :400000h, ax
13 jnz short loc_401096
14 mov eax, ds :40003Ch
15 cmp dword ptr [eax+400000h], 4550h
16 jnz short loc_401096
17 mov ecx, 10Bh
18 cmp [eax+400018h], cx
19 jnz short loc_401096
20 cmp dword ptr [eax+400074h], 0Eh
21 jbe short loc_401096
22 xor ecx, ecx
23 cmp [eax+4000E8h], ecx
24 setnz cl
25 mov [ebp+var_1C], ecx
26 jmp short loc_40109A
27
28
29 loc_401096 : ; CODE XREF: ___tmainCRTStartup+18
30 ; ___tmainCRTStartup+29 ...
31 and [ebp+var_1C], 0
32
33 loc_40109A : ; CODE XREF: ___tmainCRTStartup+50
34 push 1
35 call __heap_init
36 pop ecx
37 test eax, eax
38 jnz short loc_4010AE
39 push 1Ch
40 call _fast_error_exit
41 pop ecx
42
43 loc_4010AE : ; CODE XREF: ___tmainCRTStartup+60
44 call __mtinit
45 test eax, eax
46 jnz short loc_4010BF
47 push 10h
48 call _fast_error_exit
49 pop ecx
50
51 loc_4010BF : ; CODE XREF: ___tmainCRTStartup+71
52 call sub_401F2B
53 and [ebp+ms_exc.disabled], 0
54 call __ioinit
55 test eax, eax
56 jge short loc_4010D9
57 push 1Bh
58 call __amsg_exit
59 pop ecx
60
61 loc_4010D9 : ; CODE XREF: ___tmainCRTStartup+8B
62 call ds :GetCommandLineA
63 mov dword_40B7F8, eax
991
64 call ___crtGetEnvironmentStringsA
65 mov dword_40AC60, eax
66 call __setargv
67 test eax, eax
68 jge short loc_4010FF
69 push 8
70 call __amsg_exit
71 pop ecx
72
73 loc_4010FF : ; CODE XREF: ___tmainCRTStartup+B1
74 call __setenvp
75 test eax, eax
76 jge short loc_401110
77 push 9
78 call __amsg_exit
79 pop ecx
80
81 loc_401110 : ; CODE XREF: ___tmainCRTStartup+C2
82 push 1
83 call __cinit
84 pop ecx
85 test eax, eax
86 jz short loc_401123
87 push eax
88 call __amsg_exit
89 pop ecx
90
91 loc_401123 : ; CODE XREF: ___tmainCRTStartup+D6
92 mov eax, envp
93 mov dword_40AC80, eax
94 push eax ; envp
95 push argv ; argv
96 push argc ; argc
97 call _main
98 add esp, 0Ch
99 mov [ebp+var_20], eax
100 cmp [ebp+var_1C], 0
101 jnz short $LN28
102 push eax ; uExitCode
103 call $LN32
104
105 $LN28 : ; CODE XREF: ___tmainCRTStartup+105
106 call __cexit
107 jmp short loc_401186
108
109
110 $LN27 : ; DATA XREF: .rdata:stru_4092D0
111 mov eax, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function
401044
112 mov ecx, [eax]
113 mov ecx, [ecx]
114 mov [ebp+var_24], ecx
115 push eax
116 push ecx
992
117 call __XcptFilter
118 pop ecx
119 pop ecx
120
121 $LN24 :
122 retn
123
124
125 $LN14 : ; DATA XREF: .rdata:stru_4092D0
126 mov esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function
401044
127 mov eax, [ebp+var_24]
128 mov [ebp+var_20], eax
129 cmp [ebp+var_1C], 0
130 jnz short $LN29
131 push eax ; int
132 call __exit
133
134
135 $LN29 : ; CODE XREF: ___tmainCRTStartup+135
136 call __c_exit
137
138 loc_401186 : ; CODE XREF: ___tmainCRTStartup+112
139 mov [ebp+ms_exc.disabled], 0FFFFFFFEh
140 mov eax, [ebp+var_20]
141 call __SEH_epilog4
142 retn
Nous y trouvons des appels à GetCommandLineA() (line 62), puis setargv() (line
66) et setenvp() (line 74), qui semblent être utilisés pour initialiser les variables
globales argc, argv, envp.
Enfin, main() est invoqué avec ces arguments (line 97).
Nous observons également des appels à des fonctions au nom évocateur telles que
heap_init() (line 35), ioinit() (line 54).
Le tas heap est lui aussi initialisé par le CRT. Si vous tentez d’utiliser la fonction
malloc() dans un programme ou le CRT n’a pas été ajouté, le programme va s’ache-
ver de manière anormale avec l’erreur suivante:
runtime error R6030
- CRT not initialized
993
#include <windows.h>
int main()
{
MessageBox (NULL, "hello, world", "caption", MB_OK) ;
};
Nous obtenons un programme exécutable .exe de 2560 octets qui contient en tout
et pour tout l’en-tête PE, les instructions pour invoquer MessageBox et deux chaînes
de caractères dans le segment de données: le nom de la fonction MessageBox et
celui de sa DLL d’origine user32.dll.
Cela fonctionne, mais vous ne pouvez pas déclarer WinMain et ses 4 arguments à la
place de main().
Pour être précis, vous pourriez, mais les arguments ne seraient pas préparés au
moment de l’exécution du programme.
Cela étant, il est possible de réduire encore la taille de l’exécutable en utilisant une
valeur pour l’alignement des sections du fichier au format PE une valeur inférieur à
celle par défaut de 4096 octets.
cl no_crt.c user32.lib /link /entry :main /align :16
Nous pouvons ainsi obtenir un exécutable de 720 octets. Son exécution est possible
sur Windows 7 x86, mais pas en environnement x64 (un message d’erreur apparaîtra
alors si vous tentez de l’exécuter).
Avec des efforts accrus il est possible de réduire encore la taille, mais avec des
problèmes de compatibilité comme vous le voyez.
6.5.2 Win32 PE
PE est un format de fichier exécutable utilisé sur Windows. La différence entre .exe,
.dll et .sys est que les .exe et .sys n’ont en général pas d’exports, uniquement des
imports.
Une DLL15 , tout comme n’importe quel fichier PE, possède un point d’entrée (OEP)
(la fonction DllMain() se trouve là) mais cette fonction ne fait généralement rien. .sys
est généralement un pilote de périphérique. Pour les pilotes, Windows exige que la
somme de contrôle soit présente dans le fichier PE et qu’elle soit correcte 16 .
15. Dynamic-Link Library
16. Par exemple, Hiew( 7.1 on page 1038) peut la calculer
994
A partir de Windows Vista, un fichier de pilote doit aussi être signé avec une signature
numérique. Le chargement échouera sinon.
Chaque fichier PE commence avec un petit programme DOS qui affiche un message
comme «Ce programme ne peut pas être lancé en mode DOS. »—si vous lancez sous
DOS ou Windows 3.1 (OS-es qui ne supportent pas le format PE), ce message sera
affiché.
Terminologie
Adresse de base
Le problème est que plusieurs auteurs de module peuvent préparer des fichiers DLL
pour que d’autres les utilisent et qu’il n’est pas possible de s’accorder pour assigner
une adresse à ces modules.
C’est pourquoi si deux DLLs nécessaires pour un processus ont la même adresse
de base, une des deux sera chargée à cette adresse de base, et l’autre—à un autre
espace libre de la mémoire du processus et chaque adresse virtuelle de la seconde
DLL sera corrigée.
17. Virtual Address
18. Relative Virtual Address
19. Import Address Table
20. Matt Pietrek, An In-Depth Look into the Win32 Portable Executable File Format, (2002)]
21. Import Name Table
22. Matt Pietrek, An In-Depth Look into the Win32 Portable Executable File Format, (2002)]
995
Avec MSVC l’éditeur de lien génère souvent les fichiers .exe avec une adresse de
base en 0x40000023 , et avec une section de code débutant en 0x401000. Cela signifie
que la RVA du début de la section de code est 0x1000.
Les DLLs sont souvent générées par l’éditeur de lien de MSVC avec une adresse de
base de 0x1000000024 .
Il y a une autre raison de charger les modules a des adresses de base variées, aléa-
toires dans ce cas. C’est l’ASLR25 .
Un shellcode essayant d’être exécuté sur une système compromis doit appeler des
fonctions systèmes, par conséquent, connaître leurs adresses.
Dans les anciens OS (dans la série Windows NT : avant Windows Vista), les DLLs
système (comme kernel32.dll, user32.dll) étaient toujours chargées à une adresse
connue, et si nous nous rappelons que leur version changeait rarement, l’adresse
des fonctions était fixe et le shellcode pouvait les appeler directement.
Pour éviter ceci, la méthode ASLR charge votre programme et tous les modules dont
il a besoin à des adresses de base aléatoires, différentes à chaque fois.
Le support de l’ASLR est indiqué dans un fichier PE en mettant le flag
IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE [voir Mark Russinovich, Microsoft Win-
dows Internals].
Sous-système
Version d’OS
Un fichier PE indique aussi la version de Windows minimale requise pour pouvoir être
chargé.
La table des numéros de version stockés dans un fichier PE et les codes de Windows
correspondants est ici27 .
Par exemple, MSVC 2005 compile les fichiers .exe pour tourner sur Windows NT4
(version 4.00), mais MSVC 2008 ne le fait pas (les fichiers générés ont une version
de 5.00, il faut au moins Windows 2000 pour les utiliser).
MSVC 2012 génère des fichiers .exe avec la version 6.00 par défaut, visant au moins
Windows Vista. Toutefois, en changeant l’option du compilateur28 , il est possible de
23. L’origine de ce choix d’adresse est décrit ici: MSDN
24. Cela peut être changé avec l’option /BASE de l’éditeur de liens
25. Wikipédia
26. Signifiant que le module utilise l’API Native au lieu de Win32
27. Wikipédia
28. MSDN
996
le forcer à compiler pour Windows XP.
Sections
Il semble que la division en section soit présente dans tous les formats de fichier
exécutable.
C’est divisé afin de séparer le code des données, et les données—des données
constantes.
• Si l’un des flags IMAGE_SCN_CNT_CODE ou IMAGE_SCN_MEM_EXECUTE est mis
dans la section de code—c’est de code exécutable.
• Dans la section de données—les flags IMAGE_SCN_CNT_INITIALIZED_DATA,
IMAGE_SCN_MEM_READ et IMAGE_SCN_MEM_WRITE.
• Dans une section vide avec des données non initialisées—
IMAGE_SCN_CNT_UNINITIALIZED_DATA, IMAGE_SCN_MEM_READ et
IMAGE_SCN_MEM_WRITE.
• Dans une section de données constantes (une qui est protégée contre l’écri-
ture), les flags IMAGE_SCN_CNT_INITIALIZED_DATA et IMAGE_SCN_MEM_READ
peuvent être mis, mais pas IMAGE_SCN_MEM_WRITE. Un processus plantera s’il
essaye d’écrire dans cette section.
Chaque section dans un fichier PE peut avoir un nom, toutefois, ce n’est pas très im-
portant. (peut-être que .rdata signifie read-only-data). Souvent (mais pas toujours)
la section de code est appelée .text, la section de données—.data, la section de
données constante — .rdata (readable data). D’autres noms de section courants
sont:
• .idata—section d’imports. IDA peut créer une pseudo-section appelée ainsi: 6.5.2
on page 995.
• .edata—section d’exports section (rare)
• .pdata—section contenant toutes les informations sur les exceptions dans Win-
dows NT pout MIPS, IA64 et x64: 6.5.3 on page 1030
• .reloc—section de relocalisation
• .bss—données non initialisées (BSS)
• .tls—stockage local d’un thread (TLS)
• .rsrc—ressources
• .CRT—peut être présente dans les fichiers binaire compilés avec des anciennes
versions de MSVC
Les packeurs/chiffreurs rendent souvent les noms de sections inintelligibles ou les
remplacent avec des noms qui leurs sont propres.
29
MSVC permet de déclarer des données dans une section de n’importe quel nom .
29. MSDN
997
Certains compilateurs et éditeurs de liens peuvent ajouter une section avec des sym-
boles et d’autres informations de débogage (MinGW par exemple). Toutefois ce n’est
pas comme cela dans les dernières versions de MSVC (des fichiers PDB séparés sont
utilisés dans ce but).
30
Les chaînes sont généralement situées ici (car elles ont le type const char*), ainsi
que d’autres variables marquées comme const, les noms des fonctions importées.
Voir aussi: 3.3 on page 612.
Relocs (relocalisation)
998
C’est pourquoi une table des relocalisations est présente. Elle contient les adresses
des points qui doivent être corrigés en cas de chargement à une adresse de base
différente.
Par exemple, il y a une variable globale à l’adresse 0x410000 et voici comment elle
est accédée:
A1 00 00 41 00 mov eax,[000410000]
L’adresse de base du module est 0x400000, le RVA de la variable globale est 0x10000.
Si le module est chargé à l’adresse de base 0x500000, l’adresse réelle de la variable
globale doit être 0x510000.
Comme nous pouvons le voir, l’adresse de la variable est encodée dans l’instruction
MOV, après l’octet 0xA1.
C’est pourquoi l’adresse des 4 octets aprés 0xA1 est écrite dans la table de relocali-
sations.
Si le module est chargé à une adresse de base différente, le chargeur de l’OS parcourt
toutes les adresses dans la table, trouve chaque mot de 32-bit vers lequel elles
pointent, soustrait l’adresse de base d’origine (nous obtenons le RVA ici), et ajoute
la nouvelle adresse de base.
Si un module est chargé à son adresse de base originale, rien ne se passe.
Toutes les variables globales sont traitées ainsi.
Relocs peut avoir différent types, toutefois, dans Windows pour processeurs x86, le
type est généralement
IMAGE_REL_BASED_HIGHLOW.
Á propos, les relocs sont plus foncés dans Hiew, par exemple: fig.1.22. (vous devez
éviter ces octets lors d’un patch.)
OllyDbg souligne les endroits en mémoire où sont appliqués les relocs, par exemple:
fig.1.53.
Exports et imports
Comme nous le savons, tout programme exécutable doit utiliser les services de l’OS
et autres bibliothèques d’une manière ou d’une autre.
On peut dire que les fonctions d’un module (en général DLL) doivent être reliées aux
points de leur appel dans les autres modules (fichier .exe ou autre DLL).
Pour cela, chaque DLL a une table d’«exports », qui consiste en les fonctions plus
leur adresse dans le module.
Et chaque fichier .exe ou DLL possède des «imports », une table des fonctions re-
quises pour l’exécution incluant une liste des noms de DLL.
Après le chargement du fichier .exe principal, le chargeur de l’OS traite la table des
imports: il charge les fichiers DLL additionnels, trouve les noms des fonctions parmi
les exports des DLL et écrit leur adresse dans le module IAT de l’.exe principal.
999
Comme on le voit, pendant le chargement, le chargeur doit comparer de nombreux
noms de fonctions, mais la comparaison des chaînes n’est pas une procédure très
rapide, donc il y a un support pour «ordinaux » ou «hints », qui sont les numéros de
fonctions stockés dans une table au lieu de leurs noms.
C’est ainsi qu’elles peuvent être localisées plus rapidement lors du chargement
d’une DLL. Les ordinaux sont toujours présents dans la table d’«export ».
Par exemple, un programme utilisant la bibliothèque MFC32 charge en général mfc*.dll
par ordinaux, et dans un programme n’ayant aucun nom de fonction MFC dans INT.
Lors du chargement d’un tel programme dans IDA, il va demander le chemin vers
les fichiers mfc*.dll, afin de déterminer le nom des fonctions.
Si vous ne donnez pas le chemin vers ces DLLs à IDA, il y aura mfc80_123 à la place
des noms de fonctions.
Section d’imports
Souvent, une section séparée est allouée pour la table d’imports et tout ce qui y est
relatif (avec un nom comme .idata), toutefois, ce n’est pas une règle stricte.
Les imports sont un sujet prêtant à confusion à cause de la terminologie confuse.
Essayons de regrouper toutes les informations au même endroit.
32. Microsoft Foundation Classes
1000
Fig. 6.1: Un schéma qui regroupe toutes les structures concernant les imports d’un
fichier PE
1001
paraison de chaînes n’a pas lieu. Le tableau est terminé par zéro.
Il y a aussi un pointeur sur la table IAT appelé FirstThunk, qui est juste l’adresse RVA
de l’endroit où le chargeur écrit l’adresse résolue de la fonction.
Les endroits où le chargeur écrit les adresses sont marqués par IDA comme ceci:
__imp_CreateFileA, etc.
Il y a au moins deux façons d’utiliser les adresses écrites par le chargeur.
• Le code aura des instructions comme call __imp_CreateFileA, et puisque le
champ avec l’adresse de la fonction importée est en un certain sens une va-
riable globale, l’adresse de l’instruction call (plus 1 ou 2) doit être ajoutée à
la table de relocs, pour le cas où le module est chargé à une adresse de base
différente.
Mais, évidemment, ceci augmente significativement la table de relocs.
Car il peut y avoir un grand nombre d’appels aux fonctions importées dans le
module.
En outre, une grosse table de relocs ralenti le processus de chargement des
modules.
• Pour chaque fonction importée, il y a seulement un saut alloué, en utilisant l’ins-
truction JMP ainsi qu’un reloc sur lui. De tel point sont aussi appelés «thunks ».
Tous les appels aux fonction importées sont juste des instructions CALL au
«thunk » correspondant. Dans ce cas, des relocs supplémentaires ne sont pas
nécessaire car cex CALL-s ont des adresses relatives et ne doivent pas être
corrigés.
Ces deux méthodes peuvent être combinées.
Il est possible que l’éditeur de liens créé des «thunk »s individuels si il n’y a pas trop
d’appels à la fonction, mais ce n’est pas fait par défaut
Á propos, le tableau d’adresses de fonction sur lequel pointe FirstThunk ne doit pas
nécessairement se trouver dans la section IAT. Par exemple, j’ai écrit une fois l’utili-
taire PE_add_import33 pour ajouter des imports à un fichier .exe existant.
Quelques temps plus tôt, dans une version précédente de cet utilitaire, à la place de
la fonction que vous vouliez substituer par un appel à une autre DLL, mon utilitaire
écrivait le code suivant:
MOV EAX, [yourdll.dll !function]
JMP EAX
1002
On pourrait demander: pourrais-je fournir à un programme un ensemble de fichiers
DLL qui n’est pas supposé changer (incluant les adresses des fonctions DLL), est-il
possible d’accélérer le processus de chargement?
Oui, il est possible d’écrire les adresses des fonctions qui doivent être importées en
avance dans le tableau FirstThunk.
Le champ Timestamp est présent dans la structure IMAGE_IMPORT_DESCRIPTOR.
Si une valeur est présente ici, alors le loader compare cette valeur avec la date et
l’heure du fichier DLL.
Si les valeurs sont égales, alors le loader ne fait rien, et le processus de chargement
peut être plus rapide. Ceci est appelé «old-style binding » 34 .
L’utilitaire BIND.EXE dans le SDK est fait pour ça. Pour accélérer le chargement de
votre programme, Matt Pietrek dans Matt Pietrek, An In-Depth Look into the Win32
Portable Executable File Format, (2002)]35 , suggère de faire le binding juste après
l’installation de votre programme sur l’ordinateur de l’utilisateur final.
Par conséquent, le packeur/chiffreur fait cela lui même, avec l’aide des fonctions
LoadLibrary() et GetProcAddress().
C’est pourquoi ces deux fonctions sont souvent présentes dans l’IAT des fichiers pa-
ckés.
Dans les DLLs standards de l’installation de Windows, IAT es souvent situé juste
après le début du fichier PE. Supposément, c’est fait par soucis d’optimisation.
Pendant le chargement, le fichier .exe n’est pas chargé dans la mémoire d’un seul
coup (rappelez-vous ces fichiers d’installation énormes qui sont démarrés de façon
douteusement rapide), ils sont «mappés », et chargés en mémoire par parties lors-
qu’elles sont accédées.
Les développeurs de Microsoft ont sûrement décidé que ça serait plus rapide.
Ressources
Par effet de bord, elles peuvent être éditées facilement et sauvées dans le fichier
34. MSDN. Il y a aussi le «new-style binding ».
35. Aussi disponible en http://msdn.microsoft.com/en-us/magazine/bb985992.aspx
1003
exécutable, même si on n’a pas de connaissance particulière, en utilisant l’éditeur
ResHack, par exemple ( 6.5.2).
.NET
Les programmes .NET ne sont pas compilés en code machine, mais dans un bytecode
spécial. Strictement parlant, il y a du bytecode à la place du code x86 usuel dans le
fichier .eexe, toutefois, le point d’entrée (OEP) point sur un petit fragment de code
x86:
jmp mscoree.dll !_CorExeMain
TLS
Cette section contient les données initialisées pour le TLS( 6.2 on page 974) (si né-
cessaire). Lorsque qu’une nouvelle thread démarre, ses données TLS sont initialisées
en utilisant les données de cette section.
Á part ça, la spécification des fichiers PE fourni aussi l’initialisation de la section TLS,
aussi appelée TLS callbacks.
Si elles sont présentes, elles doivent être appelées avant que le contrôle ne soit
passé au point d’entrée principal (OEP).
Ceci est largement utilisé dans les packeurs/chiffreurs de fichiers PE.
Outils
• objdump (présent dans cygwin) pour afficher toutes les structures d’un fichier
PE.
• Hiew( 7.1 on page 1038) comme éditeur.
37
• pefile—bibliothèque-Python pour le traitement des fichiers PE .
38
• ResHack AKA Resource Hacker—éditeur de ressources .
• PE_add_import39 —outil simple pour ajouter des symboles à la table d’imports
d’un exécutable au format PE.
• PE_patcher40 —outil simple pour patcher les exécutables PE.
36. MSDN
37. https://code.google.com/p/pefile/
38. https://code.google.com/p/pefile/
39. http://yurichev.com/PE_add_imports.html
40. yurichev.com
1004
• PE_search_str_refs41 —outil simple pour chercher une fonction dans un exécu-
table PE qui utilise une certaine chaîne de texte.
Autres lectures
42
• Daniel Pistelli—The .NET File Format
1005
Fig. 6.2: Windows XP
1006
Fig. 6.4: Windows 7
1007
ling, (1997)]44 . Pour le compiler, il faut utiliser l’option SAFESEH : cl seh1.cpp
/link /safeseh:no. Vous trouverez plus d’informations concernant SAFESEH à: MSDN.
#include <windows.h>
#include <stdio.h>
DWORD new_value=1234;
if (ExceptionRecord->ExceptionCode==0xE1223344)
{
printf ("That's for us\n") ;
// yes, we "handled" the exception
return ExceptionContinueExecution ;
}
else if (ExceptionRecord->ExceptionCode==EXCEPTION_ACCESS_VIOLATION⤦
Ç )
{
printf ("ContextRecord->Eax=0x%08X\n", ContextRecord->Eax) ;
// will it be possible to 'fix' it?
printf ("Trying to fix wrong pointer address\n") ;
ContextRecord->Eax=(DWORD)&new_value ;
// yes, we "handled" the exception
return ExceptionContinueExecution ;
}
else
{
printf ("We do not handle this\n") ;
// someone else's problem
return ExceptionContinueSearch ;
};
}
int main()
{
DWORD handler = (DWORD)except_handler ; // take a pointer to our
handler
1008
// install exception handler
__asm
{ // make EXCEPTION_REGISTRATION
record:
push handler // address of handler function
push FS :[0] // address of previous handler
mov FS :[0],ESP // add new EXECEPTION_REGISTRATION
}
return 0;
}
1009
TIB
+4: … Prev=0xFFFFFFFF
Gestionnaire d’ex-
+8: … Handle
ception
Prev
Gestionnaire d’ex-
Handle
ception
Prev
Gestionnaire d’ex-
Handle
ception
45. MSDN
1010
as defined in WinBase.h as defined in ntstatus.h value
EXCEPTION_ACCESS_VIOLATION STATUS_ACCESS_VIOLATION 0xC0000005
EXCEPTION_DATATYPE_MISALIGNMENT STATUS_DATATYPE_MISALIGNMENT 0x80000002
EXCEPTION_BREAKPOINT STATUS_BREAKPOINT 0x80000003
EXCEPTION_SINGLE_STEP STATUS_SINGLE_STEP 0x80000004
EXCEPTION_ARRAY_BOUNDS_EXCEEDED STATUS_ARRAY_BOUNDS_EXCEEDED 0xC000008C
EXCEPTION_FLT_DENORMAL_OPERAND STATUS_FLOAT_DENORMAL_OPERAND 0xC000008D
EXCEPTION_FLT_DIVIDE_BY_ZERO STATUS_FLOAT_DIVIDE_BY_ZERO 0xC000008E
EXCEPTION_FLT_INEXACT_RESULT STATUS_FLOAT_INEXACT_RESULT 0xC000008F
EXCEPTION_FLT_INVALID_OPERATION STATUS_FLOAT_INVALID_OPERATION 0xC0000090
EXCEPTION_FLT_OVERFLOW STATUS_FLOAT_OVERFLOW 0xC0000091
EXCEPTION_FLT_STACK_CHECK STATUS_FLOAT_STACK_CHECK 0xC0000092
EXCEPTION_FLT_UNDERFLOW STATUS_FLOAT_UNDERFLOW 0xC0000093
EXCEPTION_INT_DIVIDE_BY_ZERO STATUS_INTEGER_DIVIDE_BY_ZERO 0xC0000094
EXCEPTION_INT_OVERFLOW STATUS_INTEGER_OVERFLOW 0xC0000095
EXCEPTION_PRIV_INSTRUCTION STATUS_PRIVILEGED_INSTRUCTION 0xC0000096
EXCEPTION_IN_PAGE_ERROR STATUS_IN_PAGE_ERROR 0xC0000006
EXCEPTION_ILLEGAL_INSTRUCTION STATUS_ILLEGAL_INSTRUCTION 0xC000001D
EXCEPTION_NONCONTINUABLE_EXCEPTION STATUS_NONCONTINUABLE_EXCEPTION 0xC0000025
EXCEPTION_STACK_OVERFLOW STATUS_STACK_OVERFLOW 0xC00000FD
EXCEPTION_INVALID_DISPOSITION STATUS_INVALID_DISPOSITION 0xC0000026
EXCEPTION_GUARD_PAGE STATUS_GUARD_PAGE_VIOLATION 0x80000001
EXCEPTION_INVALID_HANDLE STATUS_INVALID_HANDLE 0xC0000008
EXCEPTION_POSSIBLE_DEADLOCK STATUS_POSSIBLE_DEADLOCK 0xC0000194
CONTROL_C_EXIT STATUS_CONTROL_C_EXIT 0xC000013A
1011
call _printf
add esp, 8
...
Serait-il possible de corriger cette erreur «au vol » afin de continuer l’exécution du
programme?
Notre gestionnaire d’exception peut modifier la valeur du registre EAX puis laisser
l’OS exécuter de nouveau l’instruction fautive. C’est ce que nous faisons et la raison
pour laquelle printf() affiche 1234. Lorsque notre gestionnaire a fini son travail, la
valeur de EAX n’est plus 0 mais l’adresse de la variable globale new_value. L’exécu-
tion du programme se poursuit donc.
Voici ce qui se passe: le gestionnaire mémoire de la CPU signale une erreur, la CPU
suspend le thread, trouve le gestionnaire d’exception dans le noyau Windows, lequel
à son tour appelle les gestionnaires de la chaîne SEH un par un.
Nous utilisons ici le compilateur MSVC 2010. Bien entendu, il n’y a aucune garantie
que celui-ci décide d’utiliser le registre EAX pour conserver la valeur du pointeur.
Le truc du remplacement du contenu du registre n’est qu’une illustration de ce que
peut être le fonctionnement interne des SEH. En pratique, il est très rare qu’il soit
utilisé pour corriger «on-the-fly » une erreur.
Pourquoi les enregistrements SEH sont-ils conservés directement sur la pile et non
pas à un autre endroit?
L’explication la plus plausible est que l’OS n’a ainsi pas besoin de libérer l’espace
qu’ils utilisent. Ces enregistrements sont automatiquement supprimés lorsque la
fonction se termine. C’est un peu comme la fonction alloca() : ( 1.9.2 on page 49).
Retour à MSVC
46. MSDN
1012
__finally
{
...
}
KeReleaseMutant( (PKMUTANT)SignalObject,
MUTANT_INCREMENT,
FALSE,
TRUE ) ;
} except((GetExceptionCode () == STATUS_ABANDONED ||
GetExceptionCode () == STATUS_MUTANT_NOT_OWNED) ?
EXCEPTION_EXECUTE_HANDLER :
EXCEPTION_CONTINUE_SEARCH) {
Status = GetExceptionCode() ;
goto WaitExit ;
}
1013
/*++
Routine Description :
This routine serves as an exception filter and has the special job of
extracting the "real" I/O error when Mm raises STATUS_IN_PAGE_ERROR
beneath us.
Arguments :
Return Value :
EXCEPTION_EXECUTE_HANDLER
--*/
{
*ExceptionCode = ExceptionPointer->ExceptionRecord->ExceptionCode ;
ASSERT( !NT_SUCCESS(*ExceptionCode) ) ;
return EXCEPTION_EXECUTE_HANDLER ;
}
En interne, SEH est une extension du mécanisme de gestion des exceptions implé-
menté par l’OS. La fonction de gestion d’exceptions est _except_handler3 (pour
SEH3) ou _except_handler4 (pour SEH4).
Le code de ce gestionnaire est propre à MSVC et est situé dans ses librairies, ou dans
msvcr*.dll. Il est essentiel de comprendre que SEH est purement lié à MSVC.
D’autres compilateurs win32 peuvent choisir un modèle totalement différent.
SEH3
1014
buffer.
La structure scope table est un ensemble de pointeurs vers les blocs de code du
filtre et du gestionnaire de chaque niveau try/except imbriqué.
TIB Stack
+4: … Prev=0xFFFFFFFF
Gestionnaire d’ex-
+8: … Handle
ception
Gestionnaire d’ex-
0xFFFFFFFF (-1) Handle
ception
fonction de filtrage …
handler/finaliseur Prev
Handle _except_handler3
0
scope table
fonction de filtrage
handler/finaliseur
EBP
1
…
fonction de filtrage
handler/finaliseur
… en savoir plus …
Il est essentiel de comprendre que l’OS ne se préoccupe que des champs prev/handle
et de rien d’autre.
Les autres champs sont exploités par la fonction _except_handler3, de même que
le contenu de la structure scope table afin de décider quel gestionnaire exécuter et
quand.
1015
ReactOS49 .
Lorsque le champ filter est un pointeur NULL, le champ handler est un pointeur
vers un bloc de code finally.
#include <stdio.h>
#include <windows.h>
#include <excpt.h>
int main()
{
int* p = NULL ;
__try
{
printf("hello #1!\n") ;
*p = 13; // causes an access violation exception;
printf("hello #2!\n") ;
}
__except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("access violation, can't recover\n") ;
}
}
; scope table:
CONST SEGMENT
$T74622 DD 0ffffffffH ; previous try level
DD FLAT :$L74617 ; filter
DD FLAT :$L74618 ; handler
CONST ENDS
_TEXT SEGMENT
$T74621 = -32 ; size = 4
_p$ = -28 ; size = 4
__$SEHRec$ = -24 ; size = 24
_main PROC NEAR
49. http://doxygen.reactos.org/d4/df2/lib_2sdk_2crt_2except_2except_8c_source.html
1016
push ebp
mov ebp, esp
push -1 ; previous try level
push OFFSET FLAT :$T74622 ; scope table
push OFFSET FLAT :__except_handler3 ; handler
mov eax, DWORD PTR fs :__except_list
push eax ; prev
mov DWORD PTR fs :__except_list, esp
add esp, -16
; 3 registers to be saved:
push ebx
push esi
push edi
mov DWORD PTR __$SEHRec$[ebp], esp
mov DWORD PTR _p$[ebp], 0
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
push OFFSET FLAT :$SG74605 ; 'hello #1!'
call _printf
add esp, 4
mov eax, DWORD PTR _p$[ebp]
mov DWORD PTR [eax], 13
push OFFSET FLAT :$SG74606 ; 'hello #2!'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -1 ; previous try level
jmp SHORT $L74616
; filter code:
$L74617 :
$L74627 :
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR [edx]
mov DWORD PTR $T74621[ebp], eax
mov eax, DWORD PTR $T74621[ebp]
sub eax, -1073741819 ; c0000005H
neg eax
sbb eax, eax
inc eax
$L74619 :
$L74626 :
ret 0
; handler code:
$L74618 :
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET FLAT :$SG74608 ; 'access violation, can''t recover'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -1 ; setting previous try level
back to -1
$L74616 :
xor eax, eax
mov ecx, DWORD PTR __$SEHRec$[ebp+8]
1017
mov DWORD PTR fs :__except_list, ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
_TEXT ENDS
END
Nous voyons ici la manière dont le bloc SEH est construit sur la pile. La structure
scope table est présente dans le segment CONST du programme— ce qui est normal
puisque son contenu n’a jamais besoin d’être changé. Un point intéressant est la
manière dont la valeur de la variable previous try level évolue. Sa valeur initiale est
0xFFFFFFFF (−1). L’entrée dans le bloc try débute par l’écriture de la valeur 0 dans
la variable. La sortie du bloc try est marquée par la restauration de la valeur −1.
Nous voyons également l’adresse du bloc de filtrage et de celui du gestionnaire.
Nous pouvons donc observer facilement la présence de blocs try/except dans la fonc-
tion.
Le code d’initialisation des structures SEH dans le prologue de la fonction peut être
partagé par de nombreuses fonctions. Le compilateur choisi donc parfois d’insérer
dans le prologue d’une fonction un appel à la fonction SEH_prolog() qui assure cette
initialisation.
Le code de nettoyage des structures SEH se trouve quant à lui dans la fonction
SEH_epilog().
1018
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll !⤦
Ç __except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header : GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll !⤦
Ç ___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll !⤦
Ç _TppTerminateProcess@4+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll !⤦
Ç _FinalExceptionHandler@16)
Le deux premiers sont situés dans le code de notre exemple. Deux? Mais nous
n’en avons défini qu’un! Effectivement, mais un second a été initialisé dans la fonc-
tion _mainCRTStartup() du CRT. Il semble que celui-ci gère au moins les exceptions
FPU. Son code source figure dans le fichier crt/src/winxfltr.c fournit avec l’ins-
tallation de MSVC.
Le troisième est le gestionnaire SEH4 dans ntdll.dll. Le quatrième n’est pas lié à
MSVC et se situe dans ntdll.dll. Son nom suffit à en décrire l’utilité.
Comme vous le constatez, nous avons 3 types de gestionnaire dans la même chaîne:
L’un n’a rien à voir avec MSVC (le dernier) et deux autres sont liés à MSVC: SEH3 et
SEH4.
#include <stdio.h>
#include <windows.h>
#include <excpt.h>
1019
__try
{
__try
{
printf ("hello !\n") ;
RaiseException (0x112233, 0, 0, NULL) ;
printf ("0x112233 raised. now let's crash\n") ;
*p = 13; // causes an access violation exception;
}
__except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
printf("access violation, can't recover\n") ;
}
}
__except(filter_user_exceptions(GetExceptionCode(), ⤦
Ç GetExceptionInformation()))
{
// the filter_user_exceptions() function answering to the question
// "is this exception belongs to this block?"
// if yes, do the follow:
printf("user exception caught\n") ;
}
}
Nous avons maintenant deux blocs try. La structure scope table possède donc deux
entrées, une pour chaque bloc. La valeur de Previous try level change selon que
l’exécution entre ou sort des blocs try.
_code$ = 8 ; size = 4
_ep$ = 12 ; size = 4
_filter_user_exceptions PROC NEAR
push ebp
mov ebp, esp
mov eax, DWORD PTR _code$[ebp]
push eax
push OFFSET FLAT :$SG74606 ; 'in filter. code=0x%08X'
call _printf
add esp, 8
cmp DWORD PTR _code$[ebp], 1122867 ; 00112233H
jne SHORT $L74607
push OFFSET FLAT :$SG74608 ; 'yes, that is our exception'
call _printf
add esp, 4
1020
mov eax, 1
jmp SHORT $L74605
$L74607 :
push OFFSET FLAT :$SG74610 ; 'not our exception'
call _printf
add esp, 4
xor eax, eax
$L74605 :
pop ebp
ret 0
_filter_user_exceptions ENDP
; scope table:
CONST SEGMENT
$T74644 DD 0ffffffffH ; previous try level for outer block
DD FLAT :$L74634 ; outer block filter
DD FLAT :$L74635 ; outer block handler
DD 00H ; previous try level for inner block
DD FLAT :$L74638 ; inner block filter
DD FLAT :$L74639 ; inner block handler
CONST ENDS
1021
call _printf
add esp, 4
mov eax, DWORD PTR _p$[ebp]
mov DWORD PTR [eax], 13
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set
previous try level back to 0
jmp SHORT $L74615
$L74615 :
mov DWORD PTR __$SEHRec$[ebp+20], -1 ; outer try block exited, set
previous try level back to -1
jmp SHORT $L74633
1022
; outer block handler:
$L74635 :
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET FLAT :$SG74623 ; 'user exception caught'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -1 ; both try blocks exited. set
previous try level back to -1
$L74633 :
xor eax, eax
mov ecx, DWORD PTR __$SEHRec$[ebp+8]
mov DWORD PTR fs :__except_list, ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
Si nous positionnons un point d’arrêt sur la fonction printf() qui est appelée par
le gestionnaire, nous pouvons constater comment un nouveau gestionnaire SEH est
ajouté.
Il s’agit peut-être d’un autre mécanisme interne de la gestion SEH. Nous constatons
aussi que notre structure scope table contient 2 entrées.
tracer.exe -l :3.exe bpx=3.exe !printf --dump-seh
1023
SEH4 header : GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll !⤦
Ç ___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll !⤦
Ç _TppTerminateProcess@4+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll !⤦
Ç _FinalExceptionHandler@16)
SEH4
Lors d’une attaque par dépassement de buffer ( 1.26.2 on page 349), l’adresse de
la scope table peut être modifiée. C’est pourquoi à partir de MSVC 2005 SEH3 a été
amélioré vers SEH4 pour ajouter une protection contre ce type d’attaque. Le pointeur
vers la structure scope table est désormais xored avec la valeur d’un security cookie.
Par ailleurs, la structure scope table a été étendue avec une en-tête contenant deux
pointeurs vers des security cookies.
Chaque élément contient un offset dans la pile d’une valeur correspondant à: adresse
du stack frame (EBP) xored avec la valeur du security_cookie lui aussi situé sur la
pile.
Durant la gestion d’exception, l’intégrité de cette valeur est vérifiée. La valeur de
chaque security cookie situé sur la pile est aléatoire. Une attaque à distance ne pour-
ra donc pas la deviner.
Avec SEH4, la valeur initiale de previous try level est de −2 et non de −1.
1024
TIB Stack
+4: … Prev=0xFFFFFFFF
Gestionnaire d’ex-
+8: … Handle
ception
Gestionnaire d’ex-
Handle
ception
Prev
Handle _except_handler4
scope table
0xFFFFFFFF (-1) ⊕security_cookie
fonction de filtrage
EBP
handler/finaliseur
…
0
EBP⊕security_cookie
fonction de filtrage
…
handler/finaliseur
fonction de filtrage
handler/finaliseur
… en savoir plus …
; scope table:
xdata$x SEGMENT
__sehtable$_main DD 0fffffffeH ; GS Cookie Offset
DD 00H ; GS Cookie XOR Offset
1025
DD 0ffffffccH ; EH Cookie Offset
DD 00H ; EH Cookie XOR Offset
DD 0fffffffeH ; previous try level
DD FLAT :$LN12@main ; filter
DD FLAT :$LN8@main ; handler
xdata$x ENDS
; filter:
$LN7@main :
$LN12@main :
mov ecx, DWORD PTR __$SEHRec$[ebp+4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR [edx]
mov DWORD PTR $T2[ebp], eax
cmp DWORD PTR $T2[ebp], -1073741819 ; c0000005H
jne SHORT $LN4@main
mov DWORD PTR tv68[ebp], 1
1026
jmp SHORT $LN5@main
$LN4@main :
mov DWORD PTR tv68[ebp], 0
$LN5@main :
mov eax, DWORD PTR tv68[ebp]
$LN9@main :
$LN11@main :
ret 0
; handler:
$LN8@main :
mov esp, DWORD PTR __$SEHRec$[ebp]
push OFFSET $SG85488 ; 'access violation, can''t recover'
call _printf
add esp, 4
mov DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
$LN6@main :
xor eax, eax
mov ecx, DWORD PTR __$SEHRec$[ebp+8]
mov DWORD PTR fs :0, ecx
pop ecx
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
xdata$x SEGMENT
__sehtable$_main DD 0fffffffeH ; GS Cookie Offset
DD 00H ; GS Cookie XOR Offset
DD 0ffffffc8H ; EH Cookie Offset
DD 00H ; EH Cookie Offset
DD 0fffffffeH ; previous try level for outer block
DD FLAT :$LN19@main ; outer block filter
DD FLAT :$LN9@main ; outer block handler
DD 00H ; previous try level for inner block
DD FLAT :$LN18@main ; inner block filter
DD FLAT :$LN13@main ; inner block handler
xdata$x ENDS
1027
_p$ = -32 ; size = 4
tv72 = -28 ; size = 4
__$SEHRec$ = -24 ; size = 24
_main PROC
push ebp
mov ebp, esp
push -2 ; initial previous try level
push OFFSET __sehtable$_main
push OFFSET __except_handler4
mov eax, DWORD PTR fs :0
push eax ; prev
add esp, -24
push ebx
push esi
push edi
mov eax, DWORD PTR ___security_cookie
xor DWORD PTR __$SEHRec$[ebp+16], eax ; xored pointer to scope
table
xor eax, ebp ; ebp ^ security_cookie
push eax
lea eax, DWORD PTR __$SEHRec$[ebp+8] ;
pointer to VC_EXCEPTION_REGISTRATION_RECORD
mov DWORD PTR fs :0, eax
mov DWORD PTR __$SEHRec$[ebp], esp
mov DWORD PTR _p$[ebp], 0
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; entering outer try block,
setting previous try level=0
mov DWORD PTR __$SEHRec$[ebp+20], 1 ; entering inner try block,
setting previous try level=1
push OFFSET $SG85497 ; 'hello!'
call _printf
add esp, 4
push 0
push 0
push 0
push 1122867 ; 00112233H
call DWORD PTR __imp__RaiseException@16
push OFFSET $SG85499 ; '0x112233 raised. now let''s crash'
call _printf
add esp, 4
mov eax, DWORD PTR _p$[ebp]
mov DWORD PTR [eax], 13
mov DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, set
previous try level back to 0
jmp SHORT $LN2@main
1028
mov DWORD PTR tv72[ebp], 1
jmp SHORT $LN6@main
$LN5@main :
mov DWORD PTR tv72[ebp], 0
$LN6@main :
mov eax, DWORD PTR tv72[ebp]
$LN14@main :
$LN16@main :
ret 0
1029
pop esi
pop ebx
mov esp, ebp
pop ebp
ret 0
_main ENDP
_code$ = 8 ; size = 4
_ep$ = 12 ; size = 4
_filter_user_exceptions PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _code$[ebp]
push eax
push OFFSET $SG85486 ; 'in filter. code=0x%08X'
call _printf
add esp, 8
cmp DWORD PTR _code$[ebp], 1122867 ; 00112233H
jne SHORT $LN2@filter_use
push OFFSET $SG85488 ; 'yes, that is our exception'
call _printf
add esp, 4
mov eax, 1
jmp SHORT $LN3@filter_use
jmp SHORT $LN3@filter_use
$LN2@filter_use :
push OFFSET $SG85490 ; 'not our exception'
call _printf
add esp, 4
xor eax, eax
$LN3@filter_use :
pop ebp
ret 0
_filter_user_exceptions ENDP
Windows x64
Vous imaginez bien qu’il n’est pas très performant de construire le contexte SEH
dans le prologue de chaque fonction. S’y ajoute les nombreux changements de la
valeur de previous try level durant l’exécution de la fonction.
1030
C’est pourquoi avec x64, la manière de faire a complètement changé. Tous les poin-
teurs vers les blocs try, les filtres et les gestionnaires sont désormais stockés dans
une nouveau segment PE: .pdata à partir duquel les gestionnaires d’exception de
l’OS récupéreront les informations.
Voici deux exemples tirés de la section précédente et compilés pour x64:
pdata SEGMENT
$pdata$main DD imagerel $LN9
DD imagerel $LN9+61
DD imagerel $unwind$main
pdata ENDS
pdata SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
DD imagerel main$filt$0+32
DD imagerel $unwind$main$filt$0
pdata ENDS
xdata SEGMENT
$unwind$main DD 020609H
DD 030023206H
DD imagerel __C_specific_handler
DD 01H
DD imagerel $LN9+8
DD imagerel $LN9+40
DD imagerel main$filt$0
DD imagerel $LN9+40
$unwind$main$filt$0 DD 020601H
DD 050023206H
xdata ENDS
_TEXT SEGMENT
main PROC
$LN9 :
push rbx
sub rsp, 32
xor ebx, ebx
lea rcx, OFFSET FLAT :$SG86276 ; 'hello #1!'
call printf
mov DWORD PTR [rbx], 13
lea rcx, OFFSET FLAT :$SG86277 ; 'hello #2!'
call printf
jmp SHORT $LN8@main
$LN6@main :
lea rcx, OFFSET FLAT :$SG86279 ; 'access violation, can''t
recover'
call printf
npad 1 ; align next label
$LN8@main :
xor eax, eax
1031
add rsp, 32
pop rbx
ret 0
main ENDP
_TEXT ENDS
text$x SEGMENT
main$filt$0 PROC
push rbp
sub rsp, 32
mov rbp, rdx
$LN5@main$filt$ :
mov rax, QWORD PTR [rcx]
xor ecx, ecx
cmp DWORD PTR [rax], -1073741819 ; c0000005H
sete cl
mov eax, ecx
$LN7@main$filt$ :
add rsp, 32
pop rbp
ret 0
int 3
main$filt$0 ENDP
text$x ENDS
pdata SEGMENT
$pdata$filter_user_exceptions DD imagerel $LN6
DD imagerel $LN6+73
DD imagerel $unwind$filter_user_exceptions
$pdata$main DD imagerel $LN14
DD imagerel $LN14+95
DD imagerel $unwind$main
pdata ENDS
pdata SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
DD imagerel main$filt$0+32
DD imagerel $unwind$main$filt$0
$pdata$main$filt$1 DD imagerel main$filt$1
DD imagerel main$filt$1+30
DD imagerel $unwind$main$filt$1
pdata ENDS
xdata SEGMENT
$unwind$filter_user_exceptions DD 020601H
1032
DD 030023206H
$unwind$main DD 020609H
DD 030023206H
DD imagerel __C_specific_handler
DD 02H
DD imagerel $LN14+8
DD imagerel $LN14+59
DD imagerel main$filt$0
DD imagerel $LN14+59
DD imagerel $LN14+8
DD imagerel $LN14+74
DD imagerel main$filt$1
DD imagerel $LN14+74
$unwind$main$filt$0 DD 020601H
DD 050023206H
$unwind$main$filt$1 DD 020601H
DD 050023206H
xdata ENDS
_TEXT SEGMENT
main PROC
$LN14 :
push rbx
sub rsp, 32
xor ebx, ebx
lea rcx, OFFSET FLAT :$SG86288 ; 'hello!'
call printf
xor r9d, r9d
xor r8d, r8d
xor edx, edx
mov ecx, 1122867 ; 00112233H
call QWORD PTR __imp_RaiseException
lea rcx, OFFSET FLAT :$SG86290 ; '0x112233 raised. now let''s
crash'
call printf
mov DWORD PTR [rbx], 13
jmp SHORT $LN13@main
$LN11@main :
lea rcx, OFFSET FLAT :$SG86292 ; 'access violation, can''t
recover'
call printf
npad 1 ; align next label
$LN13@main :
jmp SHORT $LN9@main
$LN7@main :
lea rcx, OFFSET FLAT :$SG86294 ; 'user exception caught'
call printf
npad 1 ; align next label
$LN9@main :
xor eax, eax
add rsp, 32
pop rbx
ret 0
main ENDP
1033
text$x SEGMENT
main$filt$0 PROC
push rbp
sub rsp, 32
mov rbp, rdx
$LN10@main$filt$ :
mov rax, QWORD PTR [rcx]
xor ecx, ecx
cmp DWORD PTR [rax], -1073741819 ; c0000005H
sete cl
mov eax, ecx
$LN12@main$filt$ :
add rsp, 32
pop rbp
ret 0
int 3
main$filt$0 ENDP
main$filt$1 PROC
push rbp
sub rsp, 32
mov rbp, rdx
$LN6@main$filt$ :
mov rax, QWORD PTR [rcx]
mov rdx, rcx
mov ecx, DWORD PTR [rax]
call filter_user_exceptions
npad 1 ; align next label
$LN8@main$filt$ :
add rsp, 32
pop rbp
ret 0
int 3
main$filt$1 ENDP
text$x ENDS
_TEXT SEGMENT
code$ = 48
ep$ = 56
filter_user_exceptions PROC
$LN6 :
push rbx
sub rsp, 32
mov ebx, ecx
mov edx, ecx
lea rcx, OFFSET FLAT :$SG86277 ; 'in filter. code=0x%08X'
call printf
cmp ebx, 1122867 ; 00112233H
jne SHORT $LN2@filter_use
lea rcx, OFFSET FLAT :$SG86279 ; 'yes, that is our exception'
call printf
mov eax, 1
1034
add rsp, 32
pop rbx
ret 0
$LN2@filter_use :
lea rcx, OFFSET FLAT :$SG86281 ; 'not our exception'
call printf
xor eax, eax
add rsp, 32
pop rbx
ret 0
filter_user_exceptions ENDP
_TEXT ENDS
Pour plus d’informations sur le sujet, lisez [Igor Skochinsky, Compiler Internals: Ex-
ceptions and RTTI, (2012)] 50 .
Hormis les informations d’exception, .pdata est aussi une section qui contient les
adresses de début et de fin de toutes les fonctions. Elle revêt donc un intérêt parti-
culier dans le cadre d’une analyse automatique d’un programme.
[Matt Pietrek, A Crash Course on the Depths of Win32™ Structured Exception Hand-
ling, (1997)]51 , [Igor Skochinsky, Compiler Internals: Exceptions and RTTI, (2012)]
52
.
//
// The following three fields control entering and exiting the critical
// section for the resource
//
LONG LockCount ;
1035
LONG RecursionCount ;
HANDLE OwningThread ; // from the thread's ClientId->UniqueThread
HANDLE LockSemaphore ;
ULONG_PTR SpinCount ; // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION ;
loc_7DE922DD :
mov eax, large fs :18h
mov ecx, [eax+24h]
mov [edi+0Ch], ecx
mov dword ptr [edi+8], 1
pop edi
xor eax, eax
pop esi
mov esp, ebp
pop ebp
retn 4
... skipped
L’instruction la plus importante dans ce morceau de code est BTR (préfixée avec
LOCK) :
Le bit d’index zéro est stocké dans le flag CF et est effacé en mémoire. Ceci est une
opération atomique, bloquant tous les autres accès du CPU à cette zone de mémoire
(regardez le préfixe LOCK se trouvant avant l’instruction BTR). Si le bit en LockCount
est 1, bien, remise à zéro et retour de la fonction: nous sommes dans une section
critique.
Si non—la section critique est déjà occupée par un autre thread, donc attendre. L’at-
tente est effectuée en utilisant WaitForSingleObject().
1036
Et voici comment la fonction LeaveCriticalSection() fonctionne:
loc_7DE922B0 :
pop edi
pop ebx
loc_7DE922B2 :
xor eax, eax
pop esi
pop ebp
retn 4
... skipped
1037
Chapitre 7
Outils
Richard M. Stallman
1038
• (Libre, open-source) binary grep : un petit utilitaire pour rechercher une sé-
quence d’octets dans un paquet de fichiers, incluant ceux non exécutables :
GitHub. Il y a aussi rafind2 dans rada.re pour le même usage.
7.1.1 Désassembleurs
• IDA. Une ancienne version Freeware est disponible via téléchargement 6 . Anti-
sèche des touches de raccourci: .6.1 on page 1366
• (Gratuit, open-source) Ghidra7 — une alternative libre et open-source de IDA
développée par la NSA.
• Binary Ninja8
• (Gratuit, open-source) zynamics BinNavi9
• (Gratuit, open-source) objdump : simple utilitaire en ligne de commandes pour
désassembler et réaliser des dumps.
• (Gratuit, open-source) readelf 10 : réaliser des dumps d’informations sur des
fichiers ELF.
7.1.2 Décompilateurs
Il n’existe qu’un seul décompilateur connu en C, d’excellente qualité et disponible
au public Hex-Rays :
hex-rays.com/products/decompiler/
Pour en savoir plus: 11.8 on page 1313.
Il y a une alternative libre développée par la NSA : Ghidra11 .
1039
7.2 Analyse dynamique
Outils à utiliser lorsque que le système est en cours d’exploitation ou lorsqu’un pro-
cessus est en cours d’exécution.
7.2.1 Débogueurs
14
• (Gratuit) OllyDbg. Débogueur Win32 très populaire . Anti-sèche des touches
de raccourci: .6.2 on page 1367
• (Gratuit, open-source) GDB. Débogueur peu populaire parmi les ingénieurs en
rétro-ingénierie, car il est principalement destiné aux programmeurs. Quelques
commandes : .6.5 on page 1368. Il y a une interface graphique pour GDB, “GDB
dashboard”15 .
• (Gratuit, open-source) LLDB16 .
• WinDbg17 : débogueur pour le noyau Windows.
• (Gratuit, open-source) Radare AKA rada.re AKA r218 . Une interface graphique
existe aussi : ragui19 .
20
• (Gratuit, open-source) tracer. L’auteur utilise souvent tracer au lieu d’un dé-
bogueur.
L’auteur de ces lignes a finalement arrêté d’utiliser un débogueur, depuis que
tout ce dont il a besoin est de repérer les arguments d’une fonction lorsque
cette dernière est exécutée, ou l’état des registres à un instant donné. Le temps
de chargement d’un débogueur étant trop long, un petit utilitaire sous le nom
de tracer a été conçu. Il fonctionne depuis la ligne de commandes, permettant
d’intercepter l’exécution d’une fonction, en plaçant des breakpoints à des en-
droits définis, en lisant et en changeant l’état des registres, etc...
N.B.: tracer n’évolue pas, parce qu’il a été développé en tant qu’outil de dé-
monstration pour ce livre, et non pas comme un outil dont on se servirait au
quotidien.
1040
7.2.3 Tracer les appels système
strace / dtruss
Montre les appels système (syscalls( 6.3 on page 981)) effectués dans l’immédiat.
Par exemple:
# strace df -h
...
7.2.5 Sysinternals
(Gratuit) Sysinternals (développé par Mark Russinovich) 25 . Ces outils sont impor-
tants et valent la peine d’être étudiés : Process Explorer, Handle, VMMap, TCPView,
Process Monitor.
7.2.6 Valgrind
(Gratuit, open-source) un puissant outil pour détecter les fuites mémoire : http:
//valgrind.org/. Grâce à ses puissants mécanismes JIT (”Just In Time”), Valgrind
est utilisé comme un framework pour d’autres outils.
22. https://www.wireshark.org/
23. https://wiki.wireshark.org/CaptureSetup/USB
24. http://www.tcpdump.org/
25. https://technet.microsoft.com/en-us/sysinternals/bb842062
1041
7.2.7 Emulateurs
• (Gratuit, open-source) QEMU26 : émulateur pour différents CPUs et architec-
tures.
• (Gratuit, open-source) DosBox27 : émulateur MS-DOS, principalement utilisé
pour le rétro-gaming.
• (Gratuit, open-source) SimH28 : émulateur d’anciens ordinateurs, unités cen-
trales, etc...
7.3.2 Calculatrices
Une bonne calculatrice pour les besoins des rétro-ingénieurs doit au moins supporter
les bases décimale, hexadécimale et binaire, ainsi que plusieurs opérations impor-
tantes comme XOR et les décalages.
• IDA possède une calculatrice intégrée (“?”).
• rada.re a rax2.
• https://yurichev.com/progcalc/
• En dernier recours, la calculatrice standard de Windows dispose d’un mode
programmeur.
26. http://qemu.org
27. https://www.dosbox.com/
28. http://simh.trailing-edge.com/
29. visualstudio.com/en-US/products/visual-studio-express-vs
1042
7.4 Un outil manquant ?
Si vous connaissez un bon outil non listé précédemment, n’hésitez pas à m’en faire
la remarque :
<first_name @ last_name . com> / <first_name . last_name @ gmail . com>.
1043
Chapitre 8
Études de cas
1044
brillant; c’était seulement parce qu’ils avaient utilisé une programma-
tion non structurée et optimisé le code manuellement.
C’était simplement la façon de résoudre une énigme inconnue-faire
des tableaux et des graphiques et y obtenir un peu plus d’informations
et faire une hypothèse. En général lorsque je lis un papier technique,
c’est le même défi. J’essaye de me mettre dans l’esprit de l’auteur,
pour essayer de comprendre ce qu’est le concept. Plus vous apprenez
à lire les trucs des autres, plus vous serez capable d’inventer les votre
dans le futur, il me semble.
1. NDT: ouvrage non traduit en français, la traduction, et les fautes, sont miennes.
1045
MENUITEM "&Statistics\tF4", 40003
MENUITEM "&Options\tF5", 40004
MENUITEM "Change &Appearance\tF7", 40005
MENUITEM SEPARATOR
MENUITEM "E&xit", 40006
}
POPUP "&Help"
{
MENUITEM "&View Help\tF1", 40015
MENUITEM "&About Mahjong Titans", 40016
MENUITEM SEPARATOR
MENUITEM "Get &More Games Online", 40020
}
}
...
1046
.text :0102065C 57 push edi ; uIDEnableItem
.text :0102065D FF 35 C8 97 08 01 push hmenu ; hMenu
.text :01020663 FF D6 call esi ; EnableMenuItem
1047
Ce qui est toujours bien avec les exécutables Microsoft, c’est que IDA peut téléchar-
ger le fichier PDB correspondant à cet exécutable et afficher les noms de toutes les
fonctions.
Il est visible que le gestionnaire des tâches est écrit en C++ et certains noms de
fonction et classes sont vraiment parlants. Il y a des classes CAdapter, CNetPage,
CPerfPage, CProcInfo, CProcPage, CSvcPage, CTaskPage, CUserPage.
Il semble que chaque onglet du gestionnaire de tâches ait une classe correspon-
dante.
Regardons chaque appel et ajoutons un commentaire avec la valeur qui est passée
comme premier argument de la fonction. Nous allons écrire «not zero » à certains
endroits, car la valeur n’est clairement pas zéro, mais quelque chose de vraiment
différent (plus à ce propos dans la seconde partie de ce chapitre).
Et nous cherchons les zéros passés comme argument après tout.
1048
.text :10000B4C4 call cs :__imp_NtQuerySystemInformation ; 0
.text :10000B4CA xor ebx, ebx
.text :10000B4CC cmp eax, ebx
.text :10000B4CE jge short loc_10000B4D7
.text :10000B4D0
.text :10000B4D0 loc_10000B4D0 : ; CODE XREF:
InitPerfInfo(void)+97
.text :10000B4D0 ;
InitPerfInfo(void)+AF
.text :10000B4D0 xor al, al
.text :10000B4D2 jmp loc_10000B5EA
.text :10000B4D7 ;
---------------------------------------------------------------------------
.text :10000B4D7
.text :10000B4D7 loc_10000B4D7 : ; CODE XREF:
InitPerfInfo(void)+36
.text :10000B4D7 mov eax, [rsp+0C78h+var_C50]
.text :10000B4DB mov esi, ebx
.text :10000B4DD mov r12d, 3E80h
.text :10000B4E3 mov cs :?g_PageSize@@3KA, eax ;
ulong g_PageSize
.text :10000B4E9 shr eax, 0Ah
.text :10000B4EC lea r13, __ImageBase
.text :10000B4F3 imul eax, [rsp+0C78h+var_C4C]
.text :10000B4F8 cmp [rsp+0C78h+var_C20], bpl
.text :10000B4FD mov cs :?g_MEMMax@@3_JA, rax ;
__int64 g_MEMMax
.text :10000B504 movzx eax, [rsp+0C78h+var_C20] ; number of CPUs
.text :10000B509 cmova eax, ebp
.text :10000B50C cmp al, bl
.text :10000B50E mov cs :?g_cProcessors@@3EA, al ;
uchar g_cProcessors
g_cProcessors est une variable globale, et ce nom a été assigné par IDA suivant le
symbole PDB chargé depuis le serveur de Microsoft.
L’octet est pris de var_C20. Et var_C58 est passée à
NtQuerySystemInformation() comme un pointeur sur le buffer de réception. La
différence entre 0xC20 et 0xC58 est 0x38 (56).
Regardons le format de la structure renvoyée, que nous pouvons trouver dans MSDN:
typedef struct _SYSTEM_BASIC_INFORMATION {
BYTE Reserved1[24];
PVOID Reserved2[4];
CCHAR NumberOfProcessors ;
} SYSTEM_BASIC_INFORMATION ;
1049
un autre répertoire (ainsi le Windows Resource Protection ne va pas essayer de res-
taurer l’ancienne version du taskmgr.exe modifié).
Ouvrons-le dans Hiew et trouvons l’endroit:
Et ça fonctionne! Bien sûr, les données dans les graphes ne sont pas correctes.
À certains moments, le gestionnaire de tâches montre même une charge globale du
CPU de plus de 100%.
1050
Fig. 8.4: Gestionnaire de tâches Windows fou
Le plus grand nombre avec lequel le gestionnaire de tâches ne plante pas est 64.
Il semble que le gestionnaire de tâche de Windows Vista n’a pas été testé sur des
ordinateurs avec un grand nombre de cœurs.
Il doit y avoir une sorte de structure de données dedans. limitée à 64 cœurs (ou
plusieurs).
1051
; ECX=SystemPerformanceInformation
call cs :__imp_NtQuerySystemInformation ; 2
...
...
Peut-être que MSVC fit ainsi car le code machine de LEA est plus court que celui de
MOV REG, 5 (il serait de 5 au lieu de 4).
LEA avec un offset dans l’intervalle −128..127 (l’offset occupe 1 octet dans l’opcode)
avec des registres 32-bit est encore plus court (faute de préfixe REX )—3 octets.
Un autre exemple d’une telle chose: 6.1.5 on page 968.
1052
8.3 Blague avec le jeu Color Lines
Ceci est un jeu très répandu dont il existe plusieurs implémentations. Nous utili-
sons l’une d’entre elles, appelée BallTriX, de 1997, disponible librement ici https:
//archive.org/details/BallTriX_1020 4 . Voici à quoi il ressemble:
4. Ou ici https://web.archive.org/web/20141110053442/http://www.download-central.ws/
Win32/Games/B/BallTriX/ ou http://www.benya.com/balltrix/.
1053
Dons regardons s’il est possible de trouver le générateur d’aléas et de jouer des tours
avec. IDA reconnaît rapidement la fonction standard _rand dans balltrix.exe en
0x00403DA0. IDA montre aussi qu’elle n’est appelée que d’un seul endroit:
.text :00402C9C sub_402C9C proc near ;
CODE XREF: sub_402ACA+52
.text :00402C9C ; sub_402ACA+64 ...
.text :00402C9C
.text :00402C9C arg_0 = dword ptr 8
.text :00402C9C
.text :00402C9C push ebp
.text :00402C9D mov ebp, esp
.text :00402C9F push ebx
.text :00402CA0 push esi
.text :00402CA1 push edi
.text :00402CA2 mov eax, dword_40D430
.text :00402CA7 imul eax, dword_40D440
.text :00402CAE add eax, dword_40D5C8
.text :00402CB4 mov ecx, 32000
.text :00402CB9 cdq
.text :00402CBA idiv ecx
.text :00402CBC mov dword_40D440, edx
.text :00402CC2 call _rand
.text :00402CC7 cdq
.text :00402CC8 idiv [ebp+arg_0]
.text :00402CCB mov dword_40D430, edx
.text :00402CD1 mov eax, dword_40D430
.text :00402CD6 jmp $+5
.text :00402CDB pop edi
.text :00402CDC pop esi
.text :00402CDD pop ebx
.text :00402CDE leave
.text :00402CDF retn
.text :00402CDF sub_402C9C endp
Voici le troisième:
1054
.text :00402BBB mov eax, dword_40C058 ; 5 here
.text :00402BC0 push eax
.text :00402BC1 call random
.text :00402BC6 add esp, 4
.text :00402BC9 inc eax
Nous avons remplacé l’appel à la fonction random() par du code qui renvoie toujours
zéro.
1055
Lançons-le maintenant:
Hé oui, ça fonctionne5 .
Mais pourquoi est-ce que les arguments de la fonction random() sont des variables
globales? C’est seulement parce qu’il est possible de changer la taille du plateau
dans les préférences du jeu, donc ces valeurs ne sont pas codées en dur. Le valeurs
10 et 5 sont celles par défaut.
1056
.text :01003940 ; StartGame()+61
.text :01003940
.text :01003940 arg_0 = dword ptr 4
.text :01003940
.text :01003940 call ds :__imp__rand
.text :01003946 cdq
.text :01003947 idiv [esp+arg_0]
.text :0100394B mov eax, edx
.text :0100394D retn 4
.text :0100394D _Rnd@4 endp
IDA l’a appelé ainsi, et c’est le nom que lui ont donné les développeurs du démineur.
La fonction est très simple:
int Rnd(int limit)
{
return rand() % limit ;
};
(Il n’y a pas de nom «limit » dans le fichier PDB ; nous avons nommé manuellement
les arguments comme ceci.)
Donc elle renvoie une valeur aléatoire entre 0 et la limite spécifiée.
Rnd() est appelée depuis un seul endroit, la fonction appelée StartGame(), et il
semble bien que ce soit exactement le code qui place les mines:
.text :010036C7 push _xBoxMac
.text :010036CD call _Rnd@4 ; Rnd(x)
.text :010036D2 push _yBoxMac
.text :010036D8 mov esi, eax
.text :010036DA inc esi
.text :010036DB call _Rnd@4 ; Rnd(x)
.text :010036E0 inc eax
.text :010036E1 mov ecx, eax
.text :010036E3 shl ecx, 5 ; ECX=ECX*32
.text :010036E6 test _rgBlk[ecx+esi], 80h
.text :010036EE jnz short loc_10036C7
.text :010036F0 shl eax, 5 ; EAX=EAX*32
.text :010036F3 lea eax, _rgBlk[eax+esi]
.text :010036FA or byte ptr [eax], 80h
.text :010036FD dec _cBombStart
.text :01003703 jnz short loc_10036C7
Le démineur vous permet de définir la taille du plateau, donc les dimensions X (xBox-
Mac) et Y (yBoxMac) du plateau sont des variables globales. Elles sont passées à
Rnd() et des coordonnées aléatoires sont générées. Une mine est placée par l’ins-
truction OR en 0x010036FA. Et si une mine y a déjà été placée avant (il est possible
que la fonction Rnd() génère une paire de coordonnées qui a déjà été générée), alors
les instructions TEST et JNZ en 0x010036E6 bouclent sur la routine de génération.
cBombStart est la variable globale contenant le nombre total de mines. Donc ceci
est une boucle.
1057
La largeur du tableau est 32 (nous pouvons conclure ceci en regardant l’instruction
SHL, qui multiplie l’une des coordonnées par 32).
La taille du tableau global rgBlk peut facilement être déduite par la différence entre
le label rgBlk dans le segment de données et le label suivant. Il s’agit de 0x360
(864) :
.data :01005340 _rgBlk db 360h dup(?) ; DATA XREF:
MainWndProc(x,x,x,x)+574
.data :01005340 ; DisplayBlk(x,x)+23
.data :010056A0 _Preferences dd ? ; DATA XREF:
FixMenus()+2
...
864/32 = 27.
Donc, la taille du tableau est-elle 27∗32 ? C’est proche de ce que nous savons: lorsque
nous essayons de définir la taille du plateau à 100 ∗ 100 dans les préférences du
démineur, il corrige à une taille de plateau de 24 ∗ 30. Donc ceci est la taille maximale
du plateau. Et le tableau a une taille fixe, pour toutes les tailles de plateau.
REgardons tout ceci dans OllyDbg. Nous allons lancer le démineur, lui attacher Ol-
lyDbg et nous allons pouvoir voir le contenu de la mémoire à l’adresse du tableau
rgBlk (0x01005340)6 . Donc nous avons ceci à l’emplacement mémoire du tableau:
Address Hex dump
01005340 10 10 10 10|10 10 10 10|10 10 10 0F|0F 0F 0F 0F|
01005350 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005360 10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005370 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005380 10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005390 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053A0 10 0F 0F 0F|0F 0F 0F 0F|8F 0F 10 0F|0F 0F 0F 0F|
010053B0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053C0 10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
010053D0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053E0 10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
010053F0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005400 10 0F 0F 8F|0F 0F 8F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005410 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005420 10 8F 0F 0F|8F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005430 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005440 10 8F 0F 0F|0F 0F 8F 0F|0F 8F 10 0F|0F 0F 0F 0F|
01005450 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005460 10 0F 0F 0F|0F 8F 0F 0F|0F 8F 10 0F|0F 0F 0F 0F|
01005470 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005480 10 10 10 10|10 10 10 10|10 10 10 0F|0F 0F 0F 0F|
01005490 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054A0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054B0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054C0 0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
6. Toutes les adresses ici sont pour le démineur de Windows XP SP3 English. Elles peuvent être diffé-
rentes pour d’autres services packs.
1058
OllyDbg, comme tout autre éditeur hexadécimal, affiche 16 octets par ligne. Donc
chaque ligne de tableau de 32-octet occupe exactement 2 lignes ici.
Ceci est le niveau débutant (plateau de 9*9).
Il y a une sorte de structure carré que l’on voit ici (octets 0x10).
Nous cliquons «Run » dans OllyDbg pour débloquer le processus du démineur, puis
nous cliquons au hasard dans la fenêtre du démineur et nous tombons sur une mine,
mais maintenant, toutes les mines sont visibles:
1059
010053E0 10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
010053F0 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #6:
01005400 10 0F 0F[8F]0F 0F[8F]0F 0F 0F 10 0F 0F 0F 0F 0F
01005410 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #7:
01005420 10[8F]0F 0F[8F]0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
01005430 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #8:
01005440 10[8F]0F 0F 0F 0F[8F]0F 0F[8F]10 0F 0F 0F 0F 0F
01005450 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #9:
01005460 10 0F 0F 0F 0F[8F]0F 0F 0F[8F]10 0F 0F 0F 0F 0F
01005470 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
border :
01005480 10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F
01005490 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
Maintenant nous allons supprimer tous les octet de bord (0x10) et ce qu’il y a après:
0F 0F 0F 0F 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F[8F]0F
0F 0F 0F 0F 0F 0F 0F 0F 0F
0F 0F 0F 0F 0F 0F 0F 0F 0F
0F 0F[8F]0F 0F[8F]0F 0F 0F
[8F]0F 0F[8F]0F 0F 0F 0F 0F
[8F]0F 0F 0F 0F[8F]0F 0F[8F]
0F 0F 0F 0F[8F]0F 0F 0F[8F]
Oui, ce sont des mines, maintenant ça peut être vu clairement et comparé avec la
copie d’écran.
1060
Ce qui est intéressant, c’est que nous pouvons modifier le tableau directement dans
OllyDbg. Nous pouvons supprimer toutes les mines en changeant les octets à 0x8F
par 0x0F, et voici ce que nous obtenons dans le démineur:
Bon, le débogueur n’est pas très pratique pour espionner (ce qui est notre but), donc
nous allons écrire un petit utilitaire pour afficher le contenu du plateau:
// Windows XP MineSweeper cheater
// written by dennis(a)yurichev.com for http://beginners.re/ book
#include <windows.h>
1061
#include <assert.h>
#include <stdio.h>
if (argc !=3)
{
printf ("Usage : %s <PID> <address>\n", argv[0]) ;
return 0;
};
assert (argv[1]!=NULL) ;
assert (argv[2]!=NULL) ;
if (h==NULL)
{
DWORD e=GetLastError() ;
printf ("OpenProcess error : %08X\n", e) ;
return 0;
};
};
1062
printf ("\n") ;
};
CloseHandle (h) ;
};
7. ID d’un processus
8. Le PID peut être vu dans le Task Manager (l’activer avec «View → Select Columns »)
9. L’exécutable compilé est ici: beginners.re
1063
printf ("Found frame and grid at 0x%x\n", address) ;
};
8.4.2 Exercices
• Pourquoi est-ce que les octets de bord (ou valeurs sentinelles) (0x10) existent
dans le tableau?
À quoi servent-elles si elles ne sont pas visibles dans l’interface du démineur?
Comment est-ce qu’il pourrait fonctionner sans elles?
• Comme on s’en doute, il y a plus de valeurs possible (pour les blocs ouverts,
ceux flagués par l’utilisateur, etc.). Essayez de trouver la signification de cha-
cune d’elles.
• Modifiez mon utilitaire afin qu’il puisse supprimer toutes les mines ou qu’il les
place suivant un schéma fixé de votre choix dans le démineur.
1064
Fig. 8.10: Resource Hacker
Ok, que savons-nous? Comment afficher une aiguille? Elles commencent au milieu
du cercle, s’arrêtent sur son bord. De ce fait, nous devons calculer les coordon-
nées d’un point sur le bord d’un cercle. Des mathématiques scolaires, nous pou-
vons nous rappeler que nous devons utiliser les fonctions sinus/cosinus pour des-
siner un cercle, ou au moins la racine carré. Il n’y a pas de telles choses dans TI-
MEDATE.CPL, au moins à première vue. Mais grâce au fichier PDB de débogage de
Microsoft, je peux trouver une fonction appelée CAnalogClock::DrawHand(), qui ap-
pelle Gdiplus::Graphics::DrawLine() au moins deux fois.
Voici le code:
.text :6EB9DBC7 ; private: enum Gdiplus::Status __thiscall
CAnalogClock::_DrawHand(class Gdiplus::Graphics *, int, struct ClockHand
const &, class Gdiplus::Pen *)
.text :6EB9DBC7 ?_DrawHand@CAnalogClock@@AAE ?⤦
Ç AW4Status@Gdiplus@@PAVGraphics@3@HABUClockHand@@PAVPen@3@@Z proc near
.text :6EB9DBC7 ; CODE XREF: CAnalogClock::_ClockPaint(HDC__ *)+163
.text :6EB9DBC7 ; CAnalogClock::_ClockPaint(HDC__ *)+18B
.text :6EB9DBC7
.text :6EB9DBC7 var_10 = dword ptr -10h
.text :6EB9DBC7 var_C = dword ptr -0Ch
.text :6EB9DBC7 var_8 = dword ptr -8
.text :6EB9DBC7 var_4 = dword ptr -4
1065
.text :6EB9DBC7 arg_0 = dword ptr 8
.text :6EB9DBC7 arg_4 = dword ptr 0Ch
.text :6EB9DBC7 arg_8 = dword ptr 10h
.text :6EB9DBC7 arg_C = dword ptr 14h
.text :6EB9DBC7
.text :6EB9DBC7 mov edi, edi
.text :6EB9DBC9 push ebp
.text :6EB9DBCA mov ebp, esp
.text :6EB9DBCC sub esp, 10h
.text :6EB9DBCF mov eax, [ebp+arg_4]
.text :6EB9DBD2 push ebx
.text :6EB9DBD3 push esi
.text :6EB9DBD4 push edi
.text :6EB9DBD5 cdq
.text :6EB9DBD6 push 3Ch
.text :6EB9DBD8 mov esi, ecx
.text :6EB9DBDA pop ecx
.text :6EB9DBDB idiv ecx
.text :6EB9DBDD push 2
.text :6EB9DBDF lea ebx, table[edx*8]
.text :6EB9DBE6 lea eax, [edx+1Eh]
.text :6EB9DBE9 cdq
.text :6EB9DBEA idiv ecx
.text :6EB9DBEC mov ecx, [ebp+arg_0]
.text :6EB9DBEF mov [ebp+var_4], ebx
.text :6EB9DBF2 lea eax, table[edx*8]
.text :6EB9DBF9 mov [ebp+arg_4], eax
.text :6EB9DBFC call ?⤦
Ç SetInterpolationMode@Graphics@Gdiplus@@QAE ?⤦
Ç AW4Status@2@W4InterpolationMode@2@@Z ;
Gdiplus::Graphics::SetInterpolationMode(Gdiplus::InterpolationMode)
.text :6EB9DC01 mov eax, [esi+70h]
.text :6EB9DC04 mov edi, [ebp+arg_8]
.text :6EB9DC07 mov [ebp+var_10], eax
.text :6EB9DC0A mov eax, [esi+74h]
.text :6EB9DC0D mov [ebp+var_C], eax
.text :6EB9DC10 mov eax, [edi]
.text :6EB9DC12 sub eax, [edi+8]
.text :6EB9DC15 push 8000 ; nDenominator
.text :6EB9DC1A push eax ; nNumerator
.text :6EB9DC1B push dword ptr [ebx+4] ; nNumber
.text :6EB9DC1E mov ebx, ds :__imp__MulDiv@12 ;
MulDiv(x,x,x)
.text :6EB9DC24 call ebx ; MulDiv(x,x,x) ; MulDiv(x,x,x)
.text :6EB9DC26 add eax, [esi+74h]
.text :6EB9DC29 push 8000 ; nDenominator
.text :6EB9DC2E mov [ebp+arg_8], eax
.text :6EB9DC31 mov eax, [edi]
.text :6EB9DC33 sub eax, [edi+8]
.text :6EB9DC36 push eax ; nNumerator
.text :6EB9DC37 mov eax, [ebp+var_4]
.text :6EB9DC3A push dword ptr [eax] ; nNumber
.text :6EB9DC3C call ebx ; MulDiv(x,x,x) ; MulDiv(x,x,x)
.text :6EB9DC3E add eax, [esi+70h]
1066
.text :6EB9DC41 mov ecx, [ebp+arg_0]
.text :6EB9DC44 mov [ebp+var_8], eax
.text :6EB9DC47 mov eax, [ebp+arg_8]
.text :6EB9DC4A mov [ebp+var_4], eax
.text :6EB9DC4D lea eax, [ebp+var_8]
.text :6EB9DC50 push eax
.text :6EB9DC51 lea eax, [ebp+var_10]
.text :6EB9DC54 push eax
.text :6EB9DC55 push [ebp+arg_C]
.text :6EB9DC58 call ?DrawLine@Graphics@Gdiplus@@QAE ?⤦
Ç AW4Status@2@PBVPen@2@ABVPoint@2@1@Z ;
Gdiplus::Graphics::DrawLine(Gdiplus::Pen const *,Gdiplus::Point const
&,Gdiplus::Point const &)
.text :6EB9DC5D mov ecx, [edi+8]
.text :6EB9DC60 test ecx, ecx
.text :6EB9DC62 jbe short loc_6EB9DCAA
.text :6EB9DC64 test eax, eax
.text :6EB9DC66 jnz short loc_6EB9DCAA
.text :6EB9DC68 mov eax, [ebp+arg_4]
.text :6EB9DC6B push 8000 ; nDenominator
.text :6EB9DC70 push ecx ; nNumerator
.text :6EB9DC71 push dword ptr [eax+4] ; nNumber
.text :6EB9DC74 call ebx ; MulDiv(x,x,x) ; MulDiv(x,x,x)
.text :6EB9DC76 add eax, [esi+74h]
.text :6EB9DC79 push 8000 ; nDenominator
.text :6EB9DC7E push dword ptr [edi+8] ; nNumerator
.text :6EB9DC81 mov [ebp+arg_8], eax
.text :6EB9DC84 mov eax, [ebp+arg_4]
.text :6EB9DC87 push dword ptr [eax] ; nNumber
.text :6EB9DC89 call ebx ; MulDiv(x,x,x) ; MulDiv(x,x,x)
.text :6EB9DC8B add eax, [esi+70h]
.text :6EB9DC8E mov ecx, [ebp+arg_0]
.text :6EB9DC91 mov [ebp+var_8], eax
.text :6EB9DC94 mov eax, [ebp+arg_8]
.text :6EB9DC97 mov [ebp+var_4], eax
.text :6EB9DC9A lea eax, [ebp+var_8]
.text :6EB9DC9D push eax
.text :6EB9DC9E lea eax, [ebp+var_10]
.text :6EB9DCA1 push eax
.text :6EB9DCA2 push [ebp+arg_C]
.text :6EB9DCA5 call ?DrawLine@Graphics@Gdiplus@@QAE ?⤦
Ç AW4Status@2@PBVPen@2@ABVPoint@2@1@Z ;
Gdiplus::Graphics::DrawLine(Gdiplus::Pen const *,Gdiplus::Point const
&,Gdiplus::Point const &)
.text :6EB9DCAA
.text :6EB9DCAA loc_6EB9DCAA : ; CODE XREF:
CAnalogClock::_DrawHand(Gdiplus::Graphics *,int,ClockHand const
&,Gdiplus::Pen *)+9B
.text :6EB9DCAA ; CAnalogClock::_DrawHand(Gdiplus::Graphics
*,int,ClockHand const &,Gdiplus::Pen *)+9F
.text :6EB9DCAA pop edi
.text :6EB9DCAB pop esi
.text :6EB9DCAC pop ebx
.text :6EB9DCAD leave
.text :6EB9DCAE retn 10h
1067
.text :6EB9DCAE ?_DrawHand@CAnalogClock@@AAE ?⤦
Ç AW4Status@Gdiplus@@PAVGraphics@3@HABUClockHand@@PAVPen@3@@Z endp
.text :6EB9DCAE
...
Elle n’est référencée que depuis la fonction DrawHand(). Elle a 120 mots de 32-bit ou
60 paires 32-bit... attendez, 60? Regardons ces valeurs de plus près. Tout d’abord, je
vais remplacer 6 paires ou 12 mots de 32-bit par des zéros, et je vais mettre le fichier
TIMEDATE.CPL modifié dans C:\WINDOWS\SYSTEM32. (Vous pourriez devoir changer
le propriétaire du fichier *TIMEDATE.CPL* pour votre compte utilisateur primaire (au
lieu de TrustedInstaller), et donc, démarrer en mode sans échec avec la ligne de
commande afin de pouvoir copier le fichier, qui est en général bloqué.)
1068
Fig. 8.11: Tentative d’exécution
Maintenant lorsqu’une aiguilles est située dans 0..5 secondes/minutes, elle est in-
visible! Toutefois, la partie opposée (plus courte) de la seconde aiguille est visible
et bouge. Lorsqu’une aiguille est en dehors de cette partie, elle est visible comme
d’habitude.
Regardons d’ encore plus près la table dans Mathematica. J’ai copié/collé la table
de TIMEDATE.CPL dans un fichier tbl (480 octets). Nous tenons pour acquis le fait
que ce sont des valeurs signées, car la moitié des éléments sont inférieurs à zéro
(0FFFFE0C1h, etc.). Si ces valeurs étaient non signées, elles seraient étrangement
grandes.
In[]:= tbl = BinaryReadList["~/.../tbl", "Integer32"]
Out[]= {0, -7999, 836, -7956, 1663, -7825, 2472, -7608, 3253, -7308, 3999, ⤦
Ç \
-6928, 4702, -6472, 5353, -5945, 5945, -5353, 6472, -4702, 6928, \
-4000, 7308, -3253, 7608, -2472, 7825, -1663, 7956, -836, 8000, 0, \
7956, 836, 7825, 1663, 7608, 2472, 7308, 3253, 6928, 4000, 6472, \
4702, 5945, 5353, 5353, 5945, 4702, 6472, 3999, 6928, 3253, 7308, \
2472, 7608, 1663, 7825, 836, 7956, 0, 7999, -836, 7956, -1663, 7825, \
-2472, 7608, -3253, 7308, -4000, 6928, -4702, 6472, -5353, 5945, \
-5945, 5353, -6472, 4702, -6928, 3999, -7308, 3253, -7608, 2472, \
-7825, 1663, -7956, 836, -7999, 0, -7956, -836, -7825, -1663, -7608, \
-2472, -7308, -3253, -6928, -4000, -6472, -4702, -5945, -5353, -5353, \
-5945, -4702, -6472, -3999, -6928, -3253, -7308, -2472, -7608, -1663, \
-7825, -836, -7956}
In[]:= Length[tbl]
Out[]= 120
1069
Out[]= {{0, -7999}, {836, -7956}, {1663, -7825}, {2472, -7608}, \
{3253, -7308}, {3999, -6928}, {4702, -6472}, {5353, -5945}, {5945, \
-5353}, {6472, -4702}, {6928, -4000}, {7308, -3253}, {7608, -2472}, \
{7825, -1663}, {7956, -836}, {8000, 0}, {7956, 836}, {7825,
1663}, {7608, 2472}, {7308, 3253}, {6928, 4000}, {6472,
4702}, {5945, 5353}, {5353, 5945}, {4702, 6472}, {3999,
6928}, {3253, 7308}, {2472, 7608}, {1663, 7825}, {836, 7956}, {0,
7999}, {-836, 7956}, {-1663, 7825}, {-2472, 7608}, {-3253,
7308}, {-4000, 6928}, {-4702, 6472}, {-5353, 5945}, {-5945,
5353}, {-6472, 4702}, {-6928, 3999}, {-7308, 3253}, {-7608,
2472}, {-7825, 1663}, {-7956, 836}, {-7999,
0}, {-7956, -836}, {-7825, -1663}, {-7608, -2472}, {-7308, -3253}, \
{-6928, -4000}, {-6472, -4702}, {-5945, -5353}, {-5353, -5945}, \
{-4702, -6472}, {-3999, -6928}, {-3253, -7308}, {-2472, -7608}, \
{-1663, -7825}, {-836, -7956}}
In[]:= Length[pairs]
Out[]= 60
Essayons de traiter chaque paire comme des coordonnées X/Y et dessinons les 60
paires, et aussi les 15 premières paires:
1070
Fig. 8.12: Mathematica
Ça donne quelque chose! Chaque paire est juste une coordonnée. Les 15 premières
paires sont les coordonnées pour 14 de cercle.
Peut-être que les développeurs de Microsoft ont pré-calculé toutes les coordonnées
et les ont mises dans une table. myindexMemoization Ceci est une pratique très
répandue, quoique désuète – l’accès à une table précalculée est plus rapide que
d’appeler les fonctions sinus/cosinus relativement lente10 . Les opérations sinus/co-
sinus ne sont plus aussi couteuses...
Maintenant, je comprends pourquoi lorsque j’ai effacé les 6 premières paires, les
10. Aujourd’hui ceci est appelé la memoïsation
1071
aiguilles étaient invisibles dans cette zone: en fait, les aiguilles étaient dessinées,
elles avaient juste une longueur de zéro, car elles commençaient et finissaient en
(0,0).
La blague
Sachant tout cela, comment serait-il possible de forcer les aiguilles à tourner à l’en-
vers? En fait, ceci est simple, nous devons seulement tourner la table, afin que
chaque aiguille, au lieu d’être dessinée à l’index 0, le soit à l’index 59.
J’ai créé le modificateur il y a longtemps, au tout début des années 2000, pour Win-
dows 2000. Difficile à croire, il fonctionne toujours pour Windows 7, peut-être que la
table n’a pas changé depuis lors!
Code source du modificateur: https://beginners.re/current-tree/examples/timedate/
time_pt.c.
Maintenant, je peux voir les aiguilles tourner à l’envers:
Bon, il n’y a pas d’animation dans ce livre, mais si vous y regardez de plus près,
vous pouvez voir que les aiguilles affichent en fait l’heure correcte, mais que la
surface entière de l’horloge est tournée verticalement, comme si nous la voyons
depuis l’intérieur de l’horloge.
Donc, j’ai écrit le modificateur et ensuite le code source de Windows 2000 a fuité
(je ne peux toutefois pas vous obligez à me croire). Jettons un coup d’œil au code
source de cette fonction et à la table.
Le fichier est win2k/private/shell/cpls/utc/clock.c :
//
1072
// Array containing the sine and cosine values for hand positions.
//
POINT rCircleTable[] =
{
{ 0, -7999},
{ 836, -7956},
{ 1663, -7825},
{ 2472, -7608},
{ 3253, -7308},
...
{ -4702, -6472},
{ -3999, -6928},
{ -3253, -7308},
{ -2472, -7608},
{ -1663, -7825},
{ -836 , -7956},
};
////////////////////////////////////////////////////////////////////////////
//
// DrawHand
//
// Draws the hands of the clock.
//
////////////////////////////////////////////////////////////////////////////
void DrawHand(
HDC hDC,
int pos,
HPEN hPen,
int scale,
int patMode,
PCLOCKSTR np)
{
LPPOINT lppt ;
int radius ;
LineTo( hDC,
np->clockCenter.x + MulDiv(lppt->x, radius, 8000),
np->clockCenter.y + MulDiv(lppt->y, radius, 8000) ) ;
}
1073
La structure POINT11 est une structure de deux valeurs 32-bit, la première est x, la
seconde y.
...
La seconde est la fonction avec un nom significatif (nom tiré du PDB par IDA) :
InitialDeal() :
.text :00000001000365F8 ; void __fastcall SolitaireGame ::InitialDeal(⤦
Ç SolitaireGame *__hidden this)
.text :00000001000365F8 ?InitialDeal@SolitaireGame@@QEAAXXZ proc near
.text :00000001000365F8
.text :00000001000365F8 var_58 = byte ptr -58h
.text :00000001000365F8 var_48 = qword ptr -48h
.text :00000001000365F8 var_40 = dword ptr -40h
.text :00000001000365F8 var_3C = dword ptr -3Ch
.text :00000001000365F8 var_38 = dword ptr -38h
.text :00000001000365F8 var_30 = qword ptr -30h
.text :00000001000365F8 var_28 = xmmword ptr -28h
.text :00000001000365F8 var_18 = byte ptr -18h
.text :00000001000365F8
.text :00000001000365F8 ; FUNCTION CHUNK AT .text :00000001000A55C2 SIZE ⤦
Ç 00000018 BYTES
.text :00000001000365F8
.text :00000001000365F8 ; __unwind { // __CxxFrameHandler3
.text :00000001000365F8 mov rax, rsp
.text :00000001000365FB push rdi
.text :00000001000365FC push r12
.text :00000001000365FE push r13
11. https://msdn.microsoft.com/en-us/library/windows/desktop/dd162805(v=vs.85).aspx
1074
.text :0000000100036600 sub rsp, 60h
.text :0000000100036604 mov [rsp+78h+var_48], 0⤦
Ç FFFFFFFFFFFFFFFEh
.text :000000010003660D mov [rax+8], rbx
.text :0000000100036611 mov [rax+10h], rbp
.text :0000000100036615 mov [rax+18h], rsi
.text :0000000100036619 movaps xmmword ptr [rax-28h], xmm6
.text :000000010003661D mov rsi, rcx
.text :0000000100036620 xor edx, edx ; struct ⤦
Ç Card *
.text :0000000100036622 call ?⤦
Ç SetSelectedCard@SolitaireGame@@QEAAXPEAVCard@@@Z ; SolitaireGame ::⤦
Ç SetSelectedCard(Card *)
.text :0000000100036627 and qword ptr [rsi+0F0h], 0
.text :000000010003662F mov rax, cs :?⤦
Ç g_pSolitaireGame@@3PEAVSolitaireGame@@EA ; SolitaireGame * ⤦
Ç g_pSolitaireGame
.text :0000000100036636 mov rdx, [rax+48h]
.text :000000010003663A cmp byte ptr [rdx+51h], 0
.text :000000010003663E jz short loc_10003664E
.text :0000000100036640 xor r8d, r8d ; bool
.text :0000000100036643 mov dl, 1 ; int
.text :0000000100036645 lea ecx, [r8+3] ; this
.text :0000000100036649 call ?⤦
Ç PlaySoundProto@GameAudio@@YA_NH_NPEAI@Z ; GameAudio ::PlaySoundProto(⤦
Ç int,bool,uint *)
.text :000000010003664E
.text :000000010003664E loc_10003664E : ; CODE ⤦
Ç XREF : SolitaireGame ::InitialDeal(void)+46
.text :000000010003664E mov rbx, [rsi+88h]
.text :0000000100036655 mov r8d, 4
.text :000000010003665B lea rdx, aCardstackCreat ; "⤦
Ç CardStack ::CreateDeck() ::uiNumSuits == "...
.text :0000000100036662 mov ebp, 10000h
.text :0000000100036667 mov ecx, ebp ; unsigned ⤦
Ç int
.text :0000000100036669 call ?Log@@YAXIPEBGZZ ; Log(uint⤦
Ç ,ushort const *,...)
.text :000000010003666E mov r8d, 52 ; ---
.text :0000000100036674 lea rdx, aCardstackCreat_0 ; "⤦
Ç CardStack ::CreateDeck() ::uiNumCards == "...
.text :000000010003667B mov ecx, ebp ; unsigned ⤦
Ç int
.text :000000010003667D call ?Log@@YAXIPEBGZZ ; Log(uint⤦
Ç ,ushort const *,...)
.text :0000000100036682 xor edi, edi
1075
Ç int
.text :0000000100036692 mov eax, r8d
.text :0000000100036695 imul eax, 52 ; ---
.text :0000000100036698 mov edx, edi
.text :000000010003669A sub edx, eax ; unsigned ⤦
Ç int
.text :000000010003669C mov rcx, [rbx+128h] ; this
.text :00000001000366A3 call ?⤦
Ç CreateCard@CardTable@@IEAAPEAVCard@@II@Z ; CardTable ::CreateCard(⤦
Ç uint,uint)
.text :00000001000366A8 mov rdx, rax ; struct ⤦
Ç Card *
.text :00000001000366AB mov rcx, rbx ; this
.text :00000001000366AE call ?⤦
Ç Push@CardStack@@QEAAXPEAVCard@@@Z ; CardStack ::Push(Card *)
.text :00000001000366B3 inc edi
.text :00000001000366B5 cmp edi, 52 ; ---
.text :00000001000366B8 jb short loc_100036684
...
De toutes façons, nous voyons clairement une boucle avec 52 itérations. Le corps de
la boucle possède des appels à CardTable() ::CreateCard() et CardStack::Push().
La fonction CardTable::CreateCard() appelle finalement Card::Init() avec des
valeurs dans l’intervalle 0..51, dans l’un de ses arguments. Ceci peut être vérifié
facilement dans un débogueur.
Donc j’ai essayé de simplement changer le nombre 52 (0x34) en 51 (0x33) dans
l’instruction cmp edi, 52 en 0x1000366B5 et de le lancer. À première vue, rien ne
s’est passé, mais j’ai remarqué qu’il était maintenant difficile de résoudre le jeu. J’ai
passé presque une heure pour atteindre cette position :
1076
Il manque l’as de cœur. Peut-être qu’en interne, cette carte a l’indice 51 (si les indices
partent de zéro).
À un autre endroit, j’ai trouvé tous les noms des cartes. Peut-être que les noms sont
utilisés pour aller chercher l’image de la carte dans les ressources?
.data :00000001000B6970 ?CARD_NAME@Card@@2PAPEBGA dq offset aTwoofclubs
.data :00000001000B6970 ; "⤦
Ç TwoOfClubs"
.data :00000001000B6978 dq offset aThreeofclubs ; "⤦
Ç ThreeOfClubs"
.data :00000001000B6980 dq offset aFourofclubs ; "⤦
Ç FourOfClubs"
.data :00000001000B6988 dq offset aFiveofclubs ; "⤦
Ç FiveOfClubs"
.data :00000001000B6990 dq offset aSixofclubs ; "⤦
Ç SixOfClubs"
.data :00000001000B6998 dq offset aSevenofclubs ; "⤦
Ç SevenOfClubs"
.data :00000001000B69A0 dq offset aEightofclubs ; "⤦
Ç EightOfClubs"
.data :00000001000B69A8 dq offset aNineofclubs ; "⤦
Ç NineOfClubs"
.data :00000001000B69B0 dq offset aTenofclubs ; "⤦
Ç TenOfClubs"
1077
.data :00000001000B69B8 dq offset aJackofclubs ; "⤦
Ç JackOfClubs"
.data :00000001000B69C0 dq offset aQueenofclubs ; "⤦
Ç QueenOfClubs"
.data :00000001000B69C8 dq offset aKingofclubs ; "⤦
Ç KingOfClubs"
.data :00000001000B69D0 dq offset aAceofclubs ; "⤦
Ç AceOfClubs"
.data :00000001000B69D8 dq offset aTwoofdiamonds ; "⤦
Ç TwoOfDiamonds"
.data :00000001000B69E0 dq offset aThreeofdiamond ; "⤦
Ç ThreeOfDiamonds"
.data :00000001000B69E8 dq offset aFourofdiamonds ; "⤦
Ç FourOfDiamonds"
.data :00000001000B69F0 dq offset aFiveofdiamonds ; "⤦
Ç FiveOfDiamonds"
.data :00000001000B69F8 dq offset aSixofdiamonds ; "⤦
Ç SixOfDiamonds"
.data :00000001000B6A00 dq offset aSevenofdiamond ; "⤦
Ç SevenOfDiamonds"
.data :00000001000B6A08 dq offset aEightofdiamond ; "⤦
Ç EightOfDiamonds"
.data :00000001000B6A10 dq offset aNineofdiamonds ; "⤦
Ç NineOfDiamonds"
.data :00000001000B6A18 dq offset aTenofdiamonds ; "⤦
Ç TenOfDiamonds"
.data :00000001000B6A20 dq offset aJackofdiamonds ; "⤦
Ç JackOfDiamonds"
.data :00000001000B6A28 dq offset aQueenofdiamond ; "⤦
Ç QueenOfDiamonds"
.data :00000001000B6A30 dq offset aKingofdiamonds ; "⤦
Ç KingOfDiamonds"
.data :00000001000B6A38 dq offset aAceofdiamonds ; "⤦
Ç AceOfDiamonds"
.data :00000001000B6A40 dq offset aTwoofspades ; "⤦
Ç TwoOfSpades"
.data :00000001000B6A48 dq offset aThreeofspades ; "⤦
Ç ThreeOfSpades"
.data :00000001000B6A50 dq offset aFourofspades ; "⤦
Ç FourOfSpades"
.data :00000001000B6A58 dq offset aFiveofspades ; "⤦
Ç FiveOfSpades"
.data :00000001000B6A60 dq offset aSixofspades ; "⤦
Ç SixOfSpades"
.data :00000001000B6A68 dq offset aSevenofspades ; "⤦
Ç SevenOfSpades"
.data :00000001000B6A70 dq offset aEightofspades ; "⤦
Ç EightOfSpades"
.data :00000001000B6A78 dq offset aNineofspades ; "⤦
Ç NineOfSpades"
.data :00000001000B6A80 dq offset aTenofspades ; "⤦
Ç TenOfSpades"
.data :00000001000B6A88 dq offset aJackofspades ; "⤦
1078
Ç JackOfSpades"
.data :00000001000B6A90 dq offset aQueenofspades ; "⤦
Ç QueenOfSpades"
.data :00000001000B6A98 dq offset aKingofspades ; "⤦
Ç KingOfSpades"
.data :00000001000B6AA0 dq offset aAceofspades ; "⤦
Ç AceOfSpades"
.data :00000001000B6AA8 dq offset aTwoofhearts ; "⤦
Ç TwoOfHearts"
.data :00000001000B6AB0 dq offset aThreeofhearts ; "⤦
Ç ThreeOfHearts"
.data :00000001000B6AB8 dq offset aFourofhearts ; "⤦
Ç FourOfHearts"
.data :00000001000B6AC0 dq offset aFiveofhearts ; "⤦
Ç FiveOfHearts"
.data :00000001000B6AC8 dq offset aSixofhearts ; "⤦
Ç SixOfHearts"
.data :00000001000B6AD0 dq offset aSevenofhearts ; "⤦
Ç SevenOfHearts"
.data :00000001000B6AD8 dq offset aEightofhearts ; "⤦
Ç EightOfHearts"
.data :00000001000B6AE0 dq offset aNineofhearts ; "⤦
Ç NineOfHearts"
.data :00000001000B6AE8 dq offset aTenofhearts ; "⤦
Ç TenOfHearts"
.data :00000001000B6AF0 dq offset aJackofhearts ; "⤦
Ç JackOfHearts"
.data :00000001000B6AF8 dq offset aQueenofhearts ; "⤦
Ç QueenOfHearts"
.data :00000001000B6B00 dq offset aKingofhearts ; "⤦
Ç KingOfHearts"
.data :00000001000B6B08 dq offset aAceofhearts ; "⤦
Ç AceOfHearts"
1079
Ç "|44682|CardNames|Nine Of Clubs"
.data :00000001000B6B50 dq offset a51853Cardnames ; ⤦
Ç "|51853|CardNames|Ten Of Clubs"
.data :00000001000B6B58 dq offset a46368Cardnames ; ⤦
Ç "|46368|CardNames|Jack Of Clubs"
.data :00000001000B6B60 dq offset a61344Cardnames ; ⤦
Ç "|61344|CardNames|Queen Of Clubs"
.data :00000001000B6B68 dq offset a65017Cardnames ; ⤦
Ç "|65017|CardNames|King Of Clubs"
.data :00000001000B6B70 dq offset a57807Cardnames ; ⤦
Ç "|57807|CardNames|Ace Of Clubs"
.data :00000001000B6B78 dq offset a48455Cardnames ; ⤦
Ç "|48455|CardNames|Two Of Diamonds"
.data :00000001000B6B80 dq offset a44156Cardnames ; ⤦
Ç "|44156|CardNames|Three Of Diamonds"
.data :00000001000B6B88 dq offset a51672Cardnames ; ⤦
Ç "|51672|CardNames|Four Of Diamonds"
.data :00000001000B6B90 dq offset a45972Cardnames ; ⤦
Ç "|45972|CardNames|Five Of Diamonds"
.data :00000001000B6B98 dq offset a47206Cardnames ; ⤦
Ç "|47206|CardNames|Six Of Diamonds"
.data :00000001000B6BA0 dq offset a48399Cardnames ; ⤦
Ç "|48399|CardNames|Seven Of Diamonds"
.data :00000001000B6BA8 dq offset a47847Cardnames ; ⤦
Ç "|47847|CardNames|Eight Of Diamonds"
.data :00000001000B6BB0 dq offset a48606Cardnames ; ⤦
Ç "|48606|CardNames|Nine Of Diamonds"
.data :00000001000B6BB8 dq offset a61278Cardnames ; ⤦
Ç "|61278|CardNames|Ten Of Diamonds"
.data :00000001000B6BC0 dq offset a52038Cardnames ; ⤦
Ç "|52038|CardNames|Jack Of Diamonds"
.data :00000001000B6BC8 dq offset a54643Cardnames ; ⤦
Ç "|54643|CardNames|Queen Of Diamonds"
.data :00000001000B6BD0 dq offset a48902Cardnames ; ⤦
Ç "|48902|CardNames|King Of Diamonds"
.data :00000001000B6BD8 dq offset a46672Cardnames ; ⤦
Ç "|46672|CardNames|Ace Of Diamonds"
.data :00000001000B6BE0 dq offset a41049Cardnames ; ⤦
Ç "|41049|CardNames|Two Of Spades"
.data :00000001000B6BE8 dq offset a49327Cardnames ; ⤦
Ç "|49327|CardNames|Three Of Spades"
.data :00000001000B6BF0 dq offset a51933Cardnames ; ⤦
Ç "|51933|CardNames|Four Of Spades"
.data :00000001000B6BF8 dq offset a42651Cardnames ; ⤦
Ç "|42651|CardNames|Five Of Spades"
.data :00000001000B6C00 dq offset a65342Cardnames ; ⤦
Ç "|65342|CardNames|Six Of Spades"
.data :00000001000B6C08 dq offset a53644Cardnames ; ⤦
Ç "|53644|CardNames|Seven Of Spades"
.data :00000001000B6C10 dq offset a54466Cardnames ; ⤦
Ç "|54466|CardNames|Eight Of Spades"
.data :00000001000B6C18 dq offset a56874Cardnames ; ⤦
Ç "|56874|CardNames|Nine Of Spades"
1080
.data :00000001000B6C20 dq offset a46756Cardnames ; ⤦
Ç "|46756|CardNames|Ten Of Spades"
.data :00000001000B6C28 dq offset a62876Cardnames ; ⤦
Ç "|62876|CardNames|Jack Of Spades"
.data :00000001000B6C30 dq offset a64633Cardnames ; ⤦
Ç "|64633|CardNames|Queen Of Spades"
.data :00000001000B6C38 dq offset a46215Cardnames ; ⤦
Ç "|46215|CardNames|King Of Spades"
.data :00000001000B6C40 dq offset a60450Cardnames ; ⤦
Ç "|60450|CardNames|Ace Of Spades"
.data :00000001000B6C48 dq offset a51010Cardnames ; ⤦
Ç "|51010|CardNames|Two Of Hearts"
.data :00000001000B6C50 dq offset a64948Cardnames ; ⤦
Ç "|64948|CardNames|Three Of Hearts"
.data :00000001000B6C58 dq offset a43079Cardnames ; ⤦
Ç "|43079|CardNames|Four Of Hearts"
.data :00000001000B6C60 dq offset a57131Cardnames ; ⤦
Ç "|57131|CardNames|Five Of Hearts"
.data :00000001000B6C68 dq offset a58953Cardnames ; ⤦
Ç "|58953|CardNames|Six Of Hearts"
.data :00000001000B6C70 dq offset a45105Cardnames ; ⤦
Ç "|45105|CardNames|Seven Of Hearts"
.data :00000001000B6C78 dq offset a47775Cardnames ; ⤦
Ç "|47775|CardNames|Eight Of Hearts"
.data :00000001000B6C80 dq offset a41825Cardnames ; ⤦
Ç "|41825|CardNames|Nine Of Hearts"
.data :00000001000B6C88 dq offset a41501Cardnames ; ⤦
Ç "|41501|CardNames|Ten Of Hearts"
.data :00000001000B6C90 dq offset a47108Cardnames ; ⤦
Ç "|47108|CardNames|Jack Of Hearts"
.data :00000001000B6C98 dq offset a55659Cardnames ; ⤦
Ç "|55659|CardNames|Queen Of Hearts"
.data :00000001000B6CA0 dq offset a44572Cardnames ; ⤦
Ç "|44572|CardNames|King Of Hearts"
.data :00000001000B6CA8 dq offset a44183Cardnames ; ⤦
Ç "|44183|CardNames|Ace Of Hearts"
Si vous voulez faire ceci à quelqu’un, assurez-vous que sa santé mentale est stable.
À part les noms de fonction dans le fichier PDB, il y a de nombreux appels à la fonction
Log() qui peuvent grandement aider, car le jeu Solitaire signale ce qu’il est en train
de faire en ce moment.
Devoir: essayer de supprimer quelques cartes ou le deux de trèfle. Et que se passe-
t-il si nous échangeons les noms des cartes dans les tableaux de chaînes?
J’ai aussi essayé de passer des nombres comme 0, 0..50 à Card:Init() (pour avoir
2 zéro dans une liste de 52 nombres). Ainsi, j’ai vu deux cartes deux de trèfle à un
moment, mais le Solitaire avait un comportement erratique.
Ceci est le Solitaire de Windows 7 modifié: Solitaire51.exe.
1081
8.6.2 53 cartes
Maintenant, regardons la première partie de la boucle:
.text :0000000100036684 loc_100036684 : ; CODE ⤦
Ç XREF : SolitaireGame ::InitialDeal(void)+↓C0j
.text :0000000100036684 mov eax, 4EC4EC4Fh
.text :0000000100036689 mul edi
.text :000000010003668B mov r8d, edx
.text :000000010003668E shr r8d, 4 ; unsigned ⤦
Ç int
.text :0000000100036692 mov eax, r8d
.text :0000000100036695 imul eax, 52
.text :0000000100036698 mov edx, edi
.text :000000010003669A sub edx, eax ; unsigned ⤦
Ç int
.text :000000010003669C mov rcx, [rbx+128h] ; this
.text :00000001000366A3 call ?⤦
Ç CreateCard@CardTable@@IEAAPEAVCard@@II@Z ; CardTable ::CreateCard(⤦
Ç uint,uint)
.text :00000001000366A8 mov rdx, rax ; struct ⤦
Ç Card *
.text :00000001000366AB mov rcx, rbx ; this
.text :00000001000366AE call ?⤦
Ç Push@CardStack@@QEAAXPEAVCard@@@Z ; CardStack ::Push(Card *)
.text :00000001000366B3 inc edi
.text :00000001000366B5 cmp edi, 52
.text :00000001000366B8 jb short loc_100036684
1082
Ceci est le Solitaire de Windows 7 modifié: Solitaire53.exe.
8.7.1 Partie I
Donc, j’ai chargé FreeCell.exe dans IDA et trouvé qu’à la fois rand(), srand() et time()
sont importées depuis msvcrt.dll. time() est en effet utilisée comme valeur d’initiali-
sation pour srand() :
.text :01029612 sub_1029612 proc near ;
CODE XREF: sub_102615C+149
1083
.text :01029612 ;
sub_1029DA6+67
.text :01029612 8B FF mov edi, edi
.text :01029614 56 push esi
.text :01029615 57 push edi
.text :01029616 6A 00 push 0 ;
Time
.text :01029618 8B F9 mov edi, ecx
.text :0102961A FF 15 80 16 00+ call ds :time
.text :01029620 50 push eax ;
Seed
.text :01029621 FF 15 84 16 00+ call ds :srand
.text :01029627 8B 35 AC 16 00+ mov esi, ds :rand
.text :0102962D 59 pop ecx
.text :0102962E 59 pop ecx
.text :0102962F FF D6 call esi ; rand
.text :01029631 FF D6 call esi ; rand
.text :01029633
.text :01029633 loc_1029633 : ;
CODE XREF: sub_1029612+26
.text :01029633 ;
sub_1029612+2D
.text :01029633 FF D6 call esi ; rand
.text :01029635 83 F8 01 cmp eax, 1
.text :01029638 7C F9 jl short loc_1029633
.text :0102963A 3D 40 42 0F 00 cmp eax, 1000000
.text :0102963F 7F F2 jg short loc_1029633
.text :01029641 6A 01 push 1
.text :01029643 50 push eax
.text :01029644 8B CF mov ecx, edi
.text :01029646 E8 2D F8 FF FF call sub_1028E78
.text :0102964B 5F pop edi
.text :0102964C 5E pop esi
.text :0102964D C3 retn
.text :0102964D sub_1029612 endp
“In the morning you will send for a hansom, desiring your man to take neither the
first nor the second which may present itself.” ( The Memoirs of Sherlock Holmes,
par Arthur Conan Doyle12 )
Il y a un autre appel a la parie time() et srand(), mais mon tracer a montré que ceci
est notre point d’intérêt:
tracer.exe -l :FreeCell.exe bpf=msvcrt.dll !time bpf=msvcrt.dll !srand,args⤦
Ç :1
...
12. http://www.gutenberg.org/files/834/834-0.txt
1084
TID=5340|(1) msvcrt.dll !srand() -> 0x5507e0
TID=5340|(1) msvcrt.dll !srand(0x399f) (called from FreeCell.exe !BASE+0⤦
Ç x27d3a (0x207d3a))
TID=5340|(1) msvcrt.dll !srand() -> 0x5507e0
Vous voyez, la fonction time() a renvoyé 0x5ddb68aa et la même valeur est utilisée
comme un argument pour srand().
Essayons de forcer time() a toujours renvoyé 0:
tracer.exe -l :FreeCell.exe bpf=msvcrt.dll !time,rt :0 bpf=msvcrt.dll !srand,⤦
Ç args :1
...
Maintenant, je vois toujours le même jeu à chaque fois que je lance FreeCell en
utilisant tracer :
1085
Maintenant, comment modifier l’exécutable?
Nous voulons passer 0 comme argument à srand() en 0x01029620. Mais il y a une
instruction sur un octet: PUSH EAX. Or PUSH 0 est une instruction sur deux octets.
Comment la faire tenir?
Qui a-t-il dans les autres registres à ce moment? En utilisant tracer je les affiche tout:
tracer.exe -l :FreeCell.exe bpx=FreeCell.exe !0x01029620
...
1086
ESI=0x7740c460 EDI=0x054732d0 EBP=0x0020da78 ESP=0x0020d9d4
EIP=0x00899620
FLAGS=PF ZF IF
...
Peu importe le nombre de fois que je redémaare le jeu, ECX et EDX semblent toujours
contenir 0. Donc, j’ai modifié PUSH EAX à l’adress 0x01029620 en PUSH EDX (aussi
une instruction sur 1 octet), et maintenant FreeCell montre toujours le même jeu au
joueur.
Toutefois, d’autres options pourraient exister. En fait, nous n’avons pas besoin de
passer 0 à srand(). Plutôt, nous voulons passer une constante à srand() pour que
le jeu soit le même à chaque fois. Comme on peut le voir, la valeur d’EDI n’a pas
changé. Peut-être que nous pourrions l’essayer aussi.
Maintenant une modification un peu plus difficile. Ouvrons FreeCell.exe dans Hiew:
Nous n’avons pas des place pour remplacer l’instruction d’un octet PUSH EAX avec
celle sur deux octets PUSH 0. Et nous ne pouvons pas juste remplir CALL ds:time
avec des NOPs, car il y a un FIXUP (adresse de la fonction time() dans msvcrt.dll).
(Hiew a marqué ces 4 octets en gris.) Donc, voici ce que je fais: modifier les 2 pre-
miers octets en EB 04. Ceci est un JMP pour contourner les 4 octets FIXUP.
1087
Puis, je remplace PUSH EAX avec NOP. Ainsi, srand() aura son argument du PUSH 0
au-dessus. Aussi, je modifie une des POP ECX en NOP, car j’ai supprimé un PUSH.
...
1088
TID=4936|(0) msvcrt.dll !srand(0x1) (called from FreeCell.exe !BASE+0x27d3a ⤦
Ç (0xb47d3a))
TID=4936|(0) msvcrt.dll !srand() -> 0x5907e0
TID=4936|(0) msvcrt.dll !srand(0x2) (called from FreeCell.exe !BASE+0x27d3a ⤦
Ç (0xb47d3a))
TID=4936|(0) msvcrt.dll !srand() -> 0x5907e0
TID=4936|(0) msvcrt.dll !srand(0x3) (called from FreeCell.exe !BASE+0x27d3a ⤦
Ç (0xb47d3a))
TID=4936|(0) msvcrt.dll !srand() -> 0x5907e0
...
Je n’ai pas pu modifier PUSH EDI d’un octet en PUSH 0 de deux octets. Mais je vois
qu’il y seulement un unique saut à loc_1027D33 dans ce qui précède.
Je modifie CMP EDI, ... en XOR EDI, EDI, en complètant le 3ème octet avec NOP.
Je modifie aussi JNZ en JMP, afin que le saut se produise toujours.
Maintenant FreeCell ignore le nombre entré par l’utilisateur, mais soudain, il y a le
même jeu au début:
1089
Il semble que le code que nous avons modifié dans la partie I est relié d’une certaine
façon à du code après 0x01027CBD, qui s’exécute si EDI==0xFFFFFFFC. De toutes
façons, notre but est atteint — le jeu est toujours le même au début, et l’utilisateur
ne peut pas en choisir un autre avec le menu.
8.8 Dongles
J’ai occasionnellement effectué des remplacements logiciel de dongle de protection
de copie, ou «émulateur de dongle » et voici quelques exemples de comment ça
s’est produit.
À propos du cas avec Rocket et Z3, qui n’est pas présent ici, vous pouvez le lire là:
http://yurichev.com/tmp/SAT_SMT_DRAFT.pdf.
1090
Lorsqu’on le lançait sans le dongle connecté, une boite de dialogue avec le message
”Invalid Security Device” apparaissait.
Par chance, cette chaîne de texte pût facilement être trouvée dans le fichier exécu-
table binaire.
Prétendons que nous ne sommes pas très familier, à la fois avec le Mac OS Classic
et le PowerPC, mais essayons tout de même.
IDA ouvre le fichier exécutable sans problème, indique son type comme ”PEF (Mac OS
or Be OS executable)” (en effet, c’est un format de fichier Mac OS Classic standard).
En cherchant la chaîne de texte avec le message d’erreur, nous trouvons ce morceau
de code:
...
...
1091
seg000 :00101B40 check1 : # CODE XREF: seg000:00063E7Cp
seg000 :00101B40 # sub_64070+160p ...
seg000 :00101B40
seg000 :00101B40 .set arg_8, 8
seg000 :00101B40
seg000 :00101B40 7C 08 02 A6 mflr %r0
seg000 :00101B44 90 01 00 08 stw %r0, arg_8(%sp)
seg000 :00101B48 94 21 FF C0 stwu %sp, -0x40(%sp)
seg000 :00101B4C 48 01 6B 39 bl check2
seg000 :00101B50 60 00 00 00 nop
seg000 :00101B54 80 01 00 48 lwz %r0, 0x40+arg_8(%sp)
seg000 :00101B58 38 21 00 40 addi %sp, %sp, 0x40
seg000 :00101B5C 7C 08 03 A6 mtlr %r0
seg000 :00101B60 4E 80 00 20 blr
seg000 :00101B60 # End of function check1
Comme on peut le voir dans IDA, cette fonction est appelée depuis de nombreux
points du programme, mais seule la valeur du registre r3 est testée après chaque
appel.
Tout ce que fait cette fonction est d’appeler une autre fonction, donc c’est une fonc-
tion thunk : il y a un prologue et un épilogue de fonction, mais le registre r3 n’est
pas touché, donc checkl() renvoie ce que check2() renvoie.
BLR16 semble être le retour de la fonction, mais vu comment IDA dispose la fonction,
nous ne devons probablement pas nous en occuper.
Puisque c’est un RISC typique, il semble que les sous-programmes soient appelés
en utilisant un registre de lien, tout comme en ARM.
La fonction check2() est plus complexe:
seg000 :00118684 check2 : # CODE XREF: check1+Cp
seg000 :00118684
seg000 :00118684 .set var_18, -0x18
seg000 :00118684 .set var_C, -0xC
seg000 :00118684 .set var_8, -8
seg000 :00118684 .set var_4, -4
seg000 :00118684 .set arg_8, 8
seg000 :00118684
seg000 :00118684 93 E1 FF FC stw %r31, var_4(%sp)
seg000 :00118688 7C 08 02 A6 mflr %r0
seg000 :0011868C 83 E2 95 A8 lwz %r31, off_1485E8 # dword_24B704
seg000 :00118690 .using dword_24B704, %r31
seg000 :00118690 93 C1 FF F8 stw %r30, var_8(%sp)
seg000 :00118694 93 A1 FF F4 stw %r29, var_C(%sp)
seg000 :00118698 7C 7D 1B 78 mr %r29, %r3
seg000 :0011869C 90 01 00 08 stw %r0, arg_8(%sp)
seg000 :001186A0 54 60 06 3E clrlwi %r0, %r3, 24
seg000 :001186A4 28 00 00 01 cmplwi %r0, 1
seg000 :001186A8 94 21 FF B0 stwu %sp, -0x50(%sp)
seg000 :001186AC 40 82 00 0C bne loc_1186B8
1092
seg000 :001186B0 38 60 00 01 li %r3, 1
seg000 :001186B4 48 00 00 6C b exit
seg000 :001186B8
seg000 :001186B8 loc_1186B8 : # CODE XREF: check2+28j
seg000 :001186B8 48 00 03 D5 bl sub_118A8C
seg000 :001186BC 60 00 00 00 nop
seg000 :001186C0 3B C0 00 00 li %r30, 0
seg000 :001186C4
seg000 :001186C4 skip : # CODE XREF: check2+94j
seg000 :001186C4 57 C0 06 3F clrlwi. %r0, %r30, 24
seg000 :001186C8 41 82 00 18 beq loc_1186E0
seg000 :001186CC 38 61 00 38 addi %r3, %sp, 0x50+var_18
seg000 :001186D0 80 9F 00 00 lwz %r4, dword_24B704
seg000 :001186D4 48 00 C0 55 bl .RBEFINDNEXT
seg000 :001186D8 60 00 00 00 nop
seg000 :001186DC 48 00 00 1C b loc_1186F8
seg000 :001186E0
seg000 :001186E0 loc_1186E0 : # CODE XREF: check2+44j
seg000 :001186E0 80 BF 00 00 lwz %r5, dword_24B704
seg000 :001186E4 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :001186E8 38 60 08 C2 li %r3, 0x1234
seg000 :001186EC 48 00 BF 99 bl .RBEFINDFIRST
seg000 :001186F0 60 00 00 00 nop
seg000 :001186F4 3B C0 00 01 li %r30, 1
seg000 :001186F8
seg000 :001186F8 loc_1186F8 : # CODE XREF: check2+58j
seg000 :001186F8 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :001186FC 41 82 00 0C beq must_jump
seg000 :00118700 38 60 00 00 li %r3, 0 # error
seg000 :00118704 48 00 00 1C b exit
seg000 :00118708
seg000 :00118708 must_jump : # CODE XREF: check2+78j
seg000 :00118708 7F A3 EB 78 mr %r3, %r29
seg000 :0011870C 48 00 00 31 bl check3
seg000 :00118710 60 00 00 00 nop
seg000 :00118714 54 60 06 3F clrlwi. %r0, %r3, 24
seg000 :00118718 41 82 FF AC beq skip
seg000 :0011871C 38 60 00 01 li %r3, 1
seg000 :00118720
seg000 :00118720 exit : # CODE XREF: check2+30j
seg000 :00118720 # check2+80j
seg000 :00118720 80 01 00 58 lwz %r0, 0x50+arg_8(%sp)
seg000 :00118724 38 21 00 50 addi %sp, %sp, 0x50
seg000 :00118728 83 E1 FF FC lwz %r31, var_4(%sp)
seg000 :0011872C 7C 08 03 A6 mtlr %r0
seg000 :00118730 83 C1 FF F8 lwz %r30, var_8(%sp)
seg000 :00118734 83 A1 FF F4 lwz %r29, var_C(%sp)
seg000 :00118738 4E 80 00 20 blr
seg000 :00118738 # End of function check2
Nous sommes encore chanceux: quelques noms de foncions ont été laissés dans
l’exécutable (section de symboles de débogage?
difficile à dire puisque nous ne sommes pas très familier de ce format de fichier,
1093
peut-être est-ce une sorte d’export PE? (6.5.2)),
comme .RBEFINDNEXT() et .RBEFINDFIRST().
Enfin ces fonctions appellent d’autres fonctions avec des noms comme .GetNextDeviceViaUSB(),
.USBSendPKT(), donc elles travaillent clairement avec un dispositif USB.
Il y a même une fonction appelée .GetNextEve3Device()—qui semble familière, il
y avait un dongle Sentinel Eve3 pour le port ADB (présent sue les MACs) dans les
1990s.
Regardons d’abord comment le registre r3 est mis avant le retour, en ignorant tout
le reste.
Nous savons qu’une «bonne » valeur de r3 doit être non-nulle, r3 à zéro conduit à
l’exécution de la partie avec un message d’erreur dans une boite de dialogue.
Il y a deux instructions li %r3, 1 présentes dans la fonction et une li %r3, 0 (Load
Immediate, i.e., charger une valeur dans un registre). La première instruction est en
0x001186B0—et franchement, il est difficile de dire ce qu’elle signifie.
Ce que l’on voit ensuite, toutefois, est plus facile à comprendre: .RBEFINDFIRST()
est appelé: si elle échoue, 0 est écrit dans r3 et nous sautons à exit, autrement
une autre fonction est appelée(check3())—si elle échoue aussi, .RBEFINDNEXT()
est appelée, probablement afin de chercher un autre dispositif USB.
N.B.: clrlwi. %r0, %r3, 16 est analogue à ce que nous avons déjà vu, mais elle
éfface 16 bits, i.e.,
.RBEFINDFIRST() renvoie probablement une valeur 16-bit.
B (signifie branch) saut inconditionnel.
BEQ est l’instruction inverse de BNE.
Regardons check3() :
seg000 :0011873C check3 : # CODE XREF: check2+88p
seg000 :0011873C
seg000 :0011873C .set var_18, -0x18
seg000 :0011873C .set var_C, -0xC
seg000 :0011873C .set var_8, -8
seg000 :0011873C .set var_4, -4
seg000 :0011873C .set arg_8, 8
seg000 :0011873C
seg000 :0011873C 93 E1 FF FC stw %r31, var_4(%sp)
seg000 :00118740 7C 08 02 A6 mflr %r0
seg000 :00118744 38 A0 00 00 li %r5, 0
seg000 :00118748 93 C1 FF F8 stw %r30, var_8(%sp)
seg000 :0011874C 83 C2 95 A8 lwz %r30, off_1485E8 # dword_24B704
seg000 :00118750 .using dword_24B704, %r30
seg000 :00118750 93 A1 FF F4 stw %r29, var_C(%sp)
seg000 :00118754 3B A3 00 00 addi %r29, %r3, 0
seg000 :00118758 38 60 00 00 li %r3, 0
seg000 :0011875C 90 01 00 08 stw %r0, arg_8(%sp)
seg000 :00118760 94 21 FF B0 stwu %sp, -0x50(%sp)
seg000 :00118764 80 DE 00 00 lwz %r6, dword_24B704
seg000 :00118768 38 81 00 38 addi %r4, %sp, 0x50+var_18
1094
seg000 :0011876C 48 00 C0 5D bl .RBEREAD
seg000 :00118770 60 00 00 00 nop
seg000 :00118774 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :00118778 41 82 00 0C beq loc_118784
seg000 :0011877C 38 60 00 00 li %r3, 0
seg000 :00118780 48 00 02 F0 b exit
seg000 :00118784
seg000 :00118784 loc_118784 : # CODE XREF: check3+3Cj
seg000 :00118784 A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :00118788 28 00 04 B2 cmplwi %r0, 0x1100
seg000 :0011878C 41 82 00 0C beq loc_118798
seg000 :00118790 38 60 00 00 li %r3, 0
seg000 :00118794 48 00 02 DC b exit
seg000 :00118798
seg000 :00118798 loc_118798 : # CODE XREF: check3+50j
seg000 :00118798 80 DE 00 00 lwz %r6, dword_24B704
seg000 :0011879C 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :001187A0 38 60 00 01 li %r3, 1
seg000 :001187A4 38 A0 00 00 li %r5, 0
seg000 :001187A8 48 00 C0 21 bl .RBEREAD
seg000 :001187AC 60 00 00 00 nop
seg000 :001187B0 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :001187B4 41 82 00 0C beq loc_1187C0
seg000 :001187B8 38 60 00 00 li %r3, 0
seg000 :001187BC 48 00 02 B4 b exit
seg000 :001187C0
seg000 :001187C0 loc_1187C0 : # CODE XREF: check3+78j
seg000 :001187C0 A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :001187C4 28 00 06 4B cmplwi %r0, 0x09AB
seg000 :001187C8 41 82 00 0C beq loc_1187D4
seg000 :001187CC 38 60 00 00 li %r3, 0
seg000 :001187D0 48 00 02 A0 b exit
seg000 :001187D4
seg000 :001187D4 loc_1187D4 : # CODE XREF: check3+8Cj
seg000 :001187D4 4B F9 F3 D9 bl sub_B7BAC
seg000 :001187D8 60 00 00 00 nop
seg000 :001187DC 54 60 06 3E clrlwi %r0, %r3, 24
seg000 :001187E0 2C 00 00 05 cmpwi %r0, 5
seg000 :001187E4 41 82 01 00 beq loc_1188E4
seg000 :001187E8 40 80 00 10 bge loc_1187F8
seg000 :001187EC 2C 00 00 04 cmpwi %r0, 4
seg000 :001187F0 40 80 00 58 bge loc_118848
seg000 :001187F4 48 00 01 8C b loc_118980
seg000 :001187F8
seg000 :001187F8 loc_1187F8 : # CODE XREF: check3+ACj
seg000 :001187F8 2C 00 00 0B cmpwi %r0, 0xB
seg000 :001187FC 41 82 00 08 beq loc_118804
seg000 :00118800 48 00 01 80 b loc_118980
seg000 :00118804
seg000 :00118804 loc_118804 : # CODE XREF: check3+C0j
seg000 :00118804 80 DE 00 00 lwz %r6, dword_24B704
seg000 :00118808 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :0011880C 38 60 00 08 li %r3, 8
1095
seg000 :00118810 38 A0 00 00 li %r5, 0
seg000 :00118814 48 00 BF B5 bl .RBEREAD
seg000 :00118818 60 00 00 00 nop
seg000 :0011881C 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :00118820 41 82 00 0C beq loc_11882C
seg000 :00118824 38 60 00 00 li %r3, 0
seg000 :00118828 48 00 02 48 b exit
seg000 :0011882C
seg000 :0011882C loc_11882C : # CODE XREF: check3+E4j
seg000 :0011882C A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :00118830 28 00 11 30 cmplwi %r0, 0xFEA0
seg000 :00118834 41 82 00 0C beq loc_118840
seg000 :00118838 38 60 00 00 li %r3, 0
seg000 :0011883C 48 00 02 34 b exit
seg000 :00118840
seg000 :00118840 loc_118840 : # CODE XREF: check3+F8j
seg000 :00118840 38 60 00 01 li %r3, 1
seg000 :00118844 48 00 02 2C b exit
seg000 :00118848
seg000 :00118848 loc_118848 : # CODE XREF: check3+B4j
seg000 :00118848 80 DE 00 00 lwz %r6, dword_24B704
seg000 :0011884C 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :00118850 38 60 00 0A li %r3, 0xA
seg000 :00118854 38 A0 00 00 li %r5, 0
seg000 :00118858 48 00 BF 71 bl .RBEREAD
seg000 :0011885C 60 00 00 00 nop
seg000 :00118860 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :00118864 41 82 00 0C beq loc_118870
seg000 :00118868 38 60 00 00 li %r3, 0
seg000 :0011886C 48 00 02 04 b exit
seg000 :00118870
seg000 :00118870 loc_118870 : # CODE XREF: check3+128j
seg000 :00118870 A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :00118874 28 00 03 F3 cmplwi %r0, 0xA6E1
seg000 :00118878 41 82 00 0C beq loc_118884
seg000 :0011887C 38 60 00 00 li %r3, 0
seg000 :00118880 48 00 01 F0 b exit
seg000 :00118884
seg000 :00118884 loc_118884 : # CODE XREF: check3+13Cj
seg000 :00118884 57 BF 06 3E clrlwi %r31, %r29, 24
seg000 :00118888 28 1F 00 02 cmplwi %r31, 2
seg000 :0011888C 40 82 00 0C bne loc_118898
seg000 :00118890 38 60 00 01 li %r3, 1
seg000 :00118894 48 00 01 DC b exit
seg000 :00118898
seg000 :00118898 loc_118898 : # CODE XREF: check3+150j
seg000 :00118898 80 DE 00 00 lwz %r6, dword_24B704
seg000 :0011889C 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :001188A0 38 60 00 0B li %r3, 0xB
seg000 :001188A4 38 A0 00 00 li %r5, 0
seg000 :001188A8 48 00 BF 21 bl .RBEREAD
seg000 :001188AC 60 00 00 00 nop
seg000 :001188B0 54 60 04 3F clrlwi. %r0, %r3, 16
1096
seg000 :001188B4 41 82 00 0C beq loc_1188C0
seg000 :001188B8 38 60 00 00 li %r3, 0
seg000 :001188BC 48 00 01 B4 b exit
seg000 :001188C0
seg000 :001188C0 loc_1188C0 : # CODE XREF: check3+178j
seg000 :001188C0 A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :001188C4 28 00 23 1C cmplwi %r0, 0x1C20
seg000 :001188C8 41 82 00 0C beq loc_1188D4
seg000 :001188CC 38 60 00 00 li %r3, 0
seg000 :001188D0 48 00 01 A0 b exit
seg000 :001188D4
seg000 :001188D4 loc_1188D4 : # CODE XREF: check3+18Cj
seg000 :001188D4 28 1F 00 03 cmplwi %r31, 3
seg000 :001188D8 40 82 01 94 bne error
seg000 :001188DC 38 60 00 01 li %r3, 1
seg000 :001188E0 48 00 01 90 b exit
seg000 :001188E4
seg000 :001188E4 loc_1188E4 : # CODE XREF: check3+A8j
seg000 :001188E4 80 DE 00 00 lwz %r6, dword_24B704
seg000 :001188E8 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :001188EC 38 60 00 0C li %r3, 0xC
seg000 :001188F0 38 A0 00 00 li %r5, 0
seg000 :001188F4 48 00 BE D5 bl .RBEREAD
seg000 :001188F8 60 00 00 00 nop
seg000 :001188FC 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :00118900 41 82 00 0C beq loc_11890C
seg000 :00118904 38 60 00 00 li %r3, 0
seg000 :00118908 48 00 01 68 b exit
seg000 :0011890C
seg000 :0011890C loc_11890C : # CODE XREF: check3+1C4j
seg000 :0011890C A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :00118910 28 00 1F 40 cmplwi %r0, 0x40FF
seg000 :00118914 41 82 00 0C beq loc_118920
seg000 :00118918 38 60 00 00 li %r3, 0
seg000 :0011891C 48 00 01 54 b exit
seg000 :00118920
seg000 :00118920 loc_118920 : # CODE XREF: check3+1D8j
seg000 :00118920 57 BF 06 3E clrlwi %r31, %r29, 24
seg000 :00118924 28 1F 00 02 cmplwi %r31, 2
seg000 :00118928 40 82 00 0C bne loc_118934
seg000 :0011892C 38 60 00 01 li %r3, 1
seg000 :00118930 48 00 01 40 b exit
seg000 :00118934
seg000 :00118934 loc_118934 : # CODE XREF: check3+1ECj
seg000 :00118934 80 DE 00 00 lwz %r6, dword_24B704
seg000 :00118938 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :0011893C 38 60 00 0D li %r3, 0xD
seg000 :00118940 38 A0 00 00 li %r5, 0
seg000 :00118944 48 00 BE 85 bl .RBEREAD
seg000 :00118948 60 00 00 00 nop
seg000 :0011894C 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :00118950 41 82 00 0C beq loc_11895C
seg000 :00118954 38 60 00 00 li %r3, 0
1097
seg000 :00118958 48 00 01 18 b exit
seg000 :0011895C
seg000 :0011895C loc_11895C : # CODE XREF: check3+214j
seg000 :0011895C A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :00118960 28 00 07 CF cmplwi %r0, 0xFC7
seg000 :00118964 41 82 00 0C beq loc_118970
seg000 :00118968 38 60 00 00 li %r3, 0
seg000 :0011896C 48 00 01 04 b exit
seg000 :00118970
seg000 :00118970 loc_118970 : # CODE XREF: check3+228j
seg000 :00118970 28 1F 00 03 cmplwi %r31, 3
seg000 :00118974 40 82 00 F8 bne error
seg000 :00118978 38 60 00 01 li %r3, 1
seg000 :0011897C 48 00 00 F4 b exit
seg000 :00118980
seg000 :00118980 loc_118980 : # CODE XREF: check3+B8j
seg000 :00118980 # check3+C4j
seg000 :00118980 80 DE 00 00 lwz %r6, dword_24B704
seg000 :00118984 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :00118988 3B E0 00 00 li %r31, 0
seg000 :0011898C 38 60 00 04 li %r3, 4
seg000 :00118990 38 A0 00 00 li %r5, 0
seg000 :00118994 48 00 BE 35 bl .RBEREAD
seg000 :00118998 60 00 00 00 nop
seg000 :0011899C 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :001189A0 41 82 00 0C beq loc_1189AC
seg000 :001189A4 38 60 00 00 li %r3, 0
seg000 :001189A8 48 00 00 C8 b exit
seg000 :001189AC
seg000 :001189AC loc_1189AC : # CODE XREF: check3+264j
seg000 :001189AC A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :001189B0 28 00 1D 6A cmplwi %r0, 0xAED0
seg000 :001189B4 40 82 00 0C bne loc_1189C0
seg000 :001189B8 3B E0 00 01 li %r31, 1
seg000 :001189BC 48 00 00 14 b loc_1189D0
seg000 :001189C0
seg000 :001189C0 loc_1189C0 : # CODE XREF: check3+278j
seg000 :001189C0 28 00 18 28 cmplwi %r0, 0x2818
seg000 :001189C4 41 82 00 0C beq loc_1189D0
seg000 :001189C8 38 60 00 00 li %r3, 0
seg000 :001189CC 48 00 00 A4 b exit
seg000 :001189D0
seg000 :001189D0 loc_1189D0 : # CODE XREF: check3+280j
seg000 :001189D0 # check3+288j
seg000 :001189D0 57 A0 06 3E clrlwi %r0, %r29, 24
seg000 :001189D4 28 00 00 02 cmplwi %r0, 2
seg000 :001189D8 40 82 00 20 bne loc_1189F8
seg000 :001189DC 57 E0 06 3F clrlwi. %r0, %r31, 24
seg000 :001189E0 41 82 00 10 beq good2
seg000 :001189E4 48 00 4C 69 bl sub_11D64C
seg000 :001189E8 60 00 00 00 nop
seg000 :001189EC 48 00 00 84 b exit
seg000 :001189F0
1098
seg000 :001189F0 good2 : # CODE XREF: check3+2A4j
seg000 :001189F0 38 60 00 01 li %r3, 1
seg000 :001189F4 48 00 00 7C b exit
seg000 :001189F8
seg000 :001189F8 loc_1189F8 : # CODE XREF: check3+29Cj
seg000 :001189F8 80 DE 00 00 lwz %r6, dword_24B704
seg000 :001189FC 38 81 00 38 addi %r4, %sp, 0x50+var_18
seg000 :00118A00 38 60 00 05 li %r3, 5
seg000 :00118A04 38 A0 00 00 li %r5, 0
seg000 :00118A08 48 00 BD C1 bl .RBEREAD
seg000 :00118A0C 60 00 00 00 nop
seg000 :00118A10 54 60 04 3F clrlwi. %r0, %r3, 16
seg000 :00118A14 41 82 00 0C beq loc_118A20
seg000 :00118A18 38 60 00 00 li %r3, 0
seg000 :00118A1C 48 00 00 54 b exit
seg000 :00118A20
seg000 :00118A20 loc_118A20 : # CODE XREF: check3+2D8j
seg000 :00118A20 A0 01 00 38 lhz %r0, 0x50+var_18(%sp)
seg000 :00118A24 28 00 11 D3 cmplwi %r0, 0xD300
seg000 :00118A28 40 82 00 0C bne loc_118A34
seg000 :00118A2C 3B E0 00 01 li %r31, 1
seg000 :00118A30 48 00 00 14 b good1
seg000 :00118A34
seg000 :00118A34 loc_118A34 : # CODE XREF: check3+2ECj
seg000 :00118A34 28 00 1A EB cmplwi %r0, 0xEBA1
seg000 :00118A38 41 82 00 0C beq good1
seg000 :00118A3C 38 60 00 00 li %r3, 0
seg000 :00118A40 48 00 00 30 b exit
seg000 :00118A44
seg000 :00118A44 good1 : # CODE XREF: check3+2F4j
seg000 :00118A44 # check3+2FCj
seg000 :00118A44 57 A0 06 3E clrlwi %r0, %r29, 24
seg000 :00118A48 28 00 00 03 cmplwi %r0, 3
seg000 :00118A4C 40 82 00 20 bne error
seg000 :00118A50 57 E0 06 3F clrlwi. %r0, %r31, 24
seg000 :00118A54 41 82 00 10 beq good
seg000 :00118A58 48 00 4B F5 bl sub_11D64C
seg000 :00118A5C 60 00 00 00 nop
seg000 :00118A60 48 00 00 10 b exit
seg000 :00118A64
seg000 :00118A64 good : # CODE XREF: check3+318j
seg000 :00118A64 38 60 00 01 li %r3, 1
seg000 :00118A68 48 00 00 08 b exit
seg000 :00118A6C
seg000 :00118A6C error : # CODE XREF: check3+19Cj
seg000 :00118A6C # check3+238j ...
seg000 :00118A6C 38 60 00 00 li %r3, 0
seg000 :00118A70
seg000 :00118A70 exit : # CODE XREF: check3+44j
seg000 :00118A70 # check3+58j ...
seg000 :00118A70 80 01 00 58 lwz %r0, 0x50+arg_8(%sp)
seg000 :00118A74 38 21 00 50 addi %sp, %sp, 0x50
seg000 :00118A78 83 E1 FF FC lwz %r31, var_4(%sp)
1099
seg000 :00118A7C 7C 08 03 A6 mtlr %r0
seg000 :00118A80 83 C1 FF F8 lwz %r30, var_8(%sp)
seg000 :00118A84 83 A1 FF F4 lwz %r29, var_C(%sp)
seg000 :00118A88 4E 80 00 20 blr
seg000 :00118A88 # End of function check3
1100
/dev/rbsl8
/dev/rbsl9
/dev/rbsl10
Le programme renvoie une erreur lorsque le dongle n’est pas connecté, mais le
message d’erreur n’est pas trouvé dans les exécutables.
Grâce à IDA, il est facile de charger l’exécutable COFF utilisé dans SCO OpenServer.
Essayons de trouver la chaîne «rbsl » et en effet, elle se trouve dans ce morceau de
code:
.text :00022AB8 public SSQC
.text :00022AB8 SSQC proc near ; CODE XREF: SSQ+7p
.text :00022AB8
.text :00022AB8 var_44 = byte ptr -44h
.text :00022AB8 var_29 = byte ptr -29h
.text :00022AB8 arg_0 = dword ptr 8
.text :00022AB8
.text :00022AB8 push ebp
.text :00022AB9 mov ebp, esp
.text :00022ABB sub esp, 44h
.text :00022ABE push edi
.text :00022ABF mov edi, offset unk_4035D0
.text :00022AC4 push esi
.text :00022AC5 mov esi, [ebp+arg_0]
.text :00022AC8 push ebx
.text :00022AC9 push esi
.text :00022ACA call strlen
.text :00022ACF add esp, 4
.text :00022AD2 cmp eax, 2
.text :00022AD7 jnz loc_22BA4
.text :00022ADD inc esi
.text :00022ADE mov al, [esi-1]
.text :00022AE1 movsx eax, al
.text :00022AE4 cmp eax, '3'
.text :00022AE9 jz loc_22B84
.text :00022AEF cmp eax, '4'
.text :00022AF4 jz loc_22B94
.text :00022AFA cmp eax, '5'
.text :00022AFF jnz short loc_22B6B
.text :00022B01 movsx ebx, byte ptr [esi]
.text :00022B04 sub ebx, '0'
.text :00022B07 mov eax, 7
.text :00022B0C add eax, ebx
.text :00022B0E push eax
.text :00022B0F lea eax, [ebp+var_44]
.text :00022B12 push offset aDevSlD ; "/dev/sl%d"
.text :00022B17 push eax
.text :00022B18 call nl_sprintf
.text :00022B1D push 0 ; int
.text :00022B1F push offset aDevRbsl8 ; char *
.text :00022B24 call _access
1101
.text :00022B29 add esp, 14h
.text :00022B2C cmp eax, 0FFFFFFFFh
.text :00022B31 jz short loc_22B48
.text :00022B33 lea eax, [ebx+7]
.text :00022B36 push eax
.text :00022B37 lea eax, [ebp+var_44]
.text :00022B3A push offset aDevRbslD ; "/dev/rbsl%d"
.text :00022B3F push eax
.text :00022B40 call nl_sprintf
.text :00022B45 add esp, 0Ch
.text :00022B48
.text :00022B48 loc_22B48 : ; CODE XREF: SSQC+79j
.text :00022B48 mov edx, [edi]
.text :00022B4A test edx, edx
.text :00022B4C jle short loc_22B57
.text :00022B4E push edx ; int
.text :00022B4F call _close
.text :00022B54 add esp, 4
.text :00022B57
.text :00022B57 loc_22B57 : ; CODE XREF: SSQC+94j
.text :00022B57 push 2 ; int
.text :00022B59 lea eax, [ebp+var_44]
.text :00022B5C push eax ; char *
.text :00022B5D call _open
.text :00022B62 add esp, 8
.text :00022B65 test eax, eax
.text :00022B67 mov [edi], eax
.text :00022B69 jge short loc_22B78
.text :00022B6B
.text :00022B6B loc_22B6B : ; CODE XREF: SSQC+47j
.text :00022B6B mov eax, 0FFFFFFFFh
.text :00022B70 pop ebx
.text :00022B71 pop esi
.text :00022B72 pop edi
.text :00022B73 mov esp, ebp
.text :00022B75 pop ebp
.text :00022B76 retn
.text :00022B78
.text :00022B78 loc_22B78 : ; CODE XREF: SSQC+B1j
.text :00022B78 pop ebx
.text :00022B79 pop esi
.text :00022B7A pop edi
.text :00022B7B xor eax, eax
.text :00022B7D mov esp, ebp
.text :00022B7F pop ebp
.text :00022B80 retn
.text :00022B84
.text :00022B84 loc_22B84 : ; CODE XREF: SSQC+31j
.text :00022B84 mov al, [esi]
.text :00022B86 pop ebx
.text :00022B87 pop esi
.text :00022B88 pop edi
.text :00022B89 mov ds :byte_407224, al
1102
.text :00022B8E mov esp, ebp
.text :00022B90 xor eax, eax
.text :00022B92 pop ebp
.text :00022B93 retn
.text :00022B94
.text :00022B94 loc_22B94 : ; CODE XREF: SSQC+3Cj
.text :00022B94 mov al, [esi]
.text :00022B96 pop ebx
.text :00022B97 pop esi
.text :00022B98 pop edi
.text :00022B99 mov ds :byte_407225, al
.text :00022B9E mov esp, ebp
.text :00022BA0 xor eax, eax
.text :00022BA2 pop ebp
.text :00022BA3 retn
.text :00022BA4
.text :00022BA4 loc_22BA4 : ; CODE XREF: SSQC+1Fj
.text :00022BA4 movsx eax, ds :byte_407225
.text :00022BAB push esi
.text :00022BAC push eax
.text :00022BAD movsx eax, ds :byte_407224
.text :00022BB4 push eax
.text :00022BB5 lea eax, [ebp+var_44]
.text :00022BB8 push offset a46CCS ; "46%c%c%s"
.text :00022BBD push eax
.text :00022BBE call nl_sprintf
.text :00022BC3 lea eax, [ebp+var_44]
.text :00022BC6 push eax
.text :00022BC7 call strlen
.text :00022BCC add esp, 18h
.text :00022BCF cmp eax, 1Bh
.text :00022BD4 jle short loc_22BDA
.text :00022BD6 mov [ebp+var_29], 0
.text :00022BDA
.text :00022BDA loc_22BDA : ; CODE XREF: SSQC+11Cj
.text :00022BDA lea eax, [ebp+var_44]
.text :00022BDD push eax
.text :00022BDE call strlen
.text :00022BE3 push eax ; unsigned int
.text :00022BE4 lea eax, [ebp+var_44]
.text :00022BE7 push eax ; void *
.text :00022BE8 mov eax, [edi]
.text :00022BEA push eax ; int
.text :00022BEB call _write
.text :00022BF0 add esp, 10h
.text :00022BF3 pop ebx
.text :00022BF4 pop esi
.text :00022BF5 pop edi
.text :00022BF6 mov esp, ebp
.text :00022BF8 pop ebp
.text :00022BF9 retn
.text :00022BFA db 0Eh dup(90h)
.text :00022BFA SSQC endp
1103
Oui, en effet, le programme doit communiquer d’une façon ou d’une autre avec le
driver.
Le seul endroit où la fonction SSQC() est appelée est dans la fonction thunk :
.text :0000DBE8 public SSQ
.text :0000DBE8 SSQ proc near ; CODE XREF: sys_info+A9p
.text :0000DBE8 ; sys_info+CBp ...
.text :0000DBE8
.text :0000DBE8 arg_0 = dword ptr 8
.text :0000DBE8
.text :0000DBE8 push ebp
.text :0000DBE9 mov ebp, esp
.text :0000DBEB mov edx, [ebp+arg_0]
.text :0000DBEE push edx
.text :0000DBEF call SSQC
.text :0000DBF4 add esp, 4
.text :0000DBF7 mov esp, ebp
.text :0000DBF9 pop ebp
.text :0000DBFA retn
.text :0000DBFB SSQ endp
...
1104
...
1105
.text :0000D6E2 pop edi
.text :0000D6E3 mov esp, ebp
.text :0000D6E5 pop ebp
.text :0000D6E6 retn
.text :0000D6E8 OK : ; CODE XREF: sys_info+F0j
.text :0000D6E8 ; sys_info+FBj
.text :0000D6E8 mov al, _C_and_B[ebx]
.text :0000D6EE pop ebx
.text :0000D6EF pop edi
.text :0000D6F0 mov ds :ctl_model, al
.text :0000D6F5 mov esp, ebp
.text :0000D6F7 pop ebp
.text :0000D6F8 retn
.text :0000D6F8 sys_info endp
1106
Voyons où la valeur de la variable globale ctl_model est utilisée.
Un tel endroit est:
.text :0000D708 prep_sys proc near ; CODE XREF: init_sys+46Ap
.text :0000D708
.text :0000D708 var_14 = dword ptr -14h
.text :0000D708 var_10 = byte ptr -10h
.text :0000D708 var_8 = dword ptr -8
.text :0000D708 var_2 = word ptr -2
.text :0000D708
.text :0000D708 push ebp
.text :0000D709 mov eax, ds :net_env
.text :0000D70E mov ebp, esp
.text :0000D710 sub esp, 1Ch
.text :0000D713 test eax, eax
.text :0000D715 jnz short loc_D734
.text :0000D717 mov al, ds :ctl_model
.text :0000D71C test al, al
.text :0000D71E jnz short loc_D77E
.text :0000D720 mov [ebp+var_8], offset aIeCvulnvvOkgT_ ;
"Ie-cvulnvV\\\bOKG]T_"
.text :0000D727 mov edx, 7
.text :0000D72C jmp loc_D7E7
...
1107
.text :0000A43C push ebp
.text :0000A43D mov ebp, esp
.text :0000A43F sub esp, 54h
.text :0000A442 push edi
.text :0000A443 mov ecx, [ebp+arg_8]
.text :0000A446 xor edi, edi
.text :0000A448 test ecx, ecx
.text :0000A44A push esi
.text :0000A44B jle short loc_A466
.text :0000A44D mov esi, [ebp+arg_C] ; key
.text :0000A450 mov edx, [ebp+arg_4] ; string
.text :0000A453
.text :0000A453 loc_A453 : ; CODE XREF:
err_warn+28j
.text :0000A453 xor eax, eax
.text :0000A455 mov al, [edx+edi]
.text :0000A458 xor eax, esi
.text :0000A45A add esi, 3
.text :0000A45D inc edi
.text :0000A45E cmp edi, ecx
.text :0000A460 mov [ebp+edi+var_55], al
.text :0000A464 jl short loc_A453
.text :0000A466
.text :0000A466 loc_A466 : ; CODE XREF:
err_warn+Fj
.text :0000A466 mov [ebp+edi+var_54], 0
.text :0000A46B mov eax, [ebp+arg_0]
.text :0000A46E cmp eax, 18h
.text :0000A473 jnz short loc_A49C
.text :0000A475 lea eax, [ebp+var_54]
.text :0000A478 push eax
.text :0000A479 call status_line
.text :0000A47E add esp, 4
.text :0000A481
.text :0000A481 loc_A481 : ; CODE XREF:
err_warn+72j
.text :0000A481 push 50h
.text :0000A483 push 0
.text :0000A485 lea eax, [ebp+var_54]
.text :0000A488 push eax
.text :0000A489 call memset
.text :0000A48E call pcv_refresh
.text :0000A493 add esp, 0Ch
.text :0000A496 pop esi
.text :0000A497 pop edi
.text :0000A498 mov esp, ebp
.text :0000A49A pop ebp
.text :0000A49B retn
.text :0000A49C
.text :0000A49C loc_A49C : ; CODE XREF:
err_warn+37j
.text :0000A49C push 0
.text :0000A49E lea eax, [ebp+var_54]
.text :0000A4A1 mov edx, [ebp+arg_0]
1108
.text :0000A4A4 push edx
.text :0000A4A5 push eax
.text :0000A4A6 call pcv_lputs
.text :0000A4AB add esp, 0Ch
.text :0000A4AE jmp short loc_A481
.text :0000A4AE err_warn endp
C’est pourquoi nous étions incapable de trouver le message d’erreur dans les fichiers
exécutable, car ils sont chiffrés (ce qui est une pratique courante).
Un autre appel à la fonction de hachage SSQ() lui passe la chaîne « offln » et le
résultat est comparé avec 0xFE81 et 0x12A9.
Si ils ne correspondent pas, ça se comporte comme une sorte de fonction timer()
(peut-être en attente qu’un dongle mal connecté soit reconnecté et re-testé?) et
ensuite déchiffre un autre message d’erreur à afficher.
.text :0000DA55 loc_DA55 : ; CODE XREF:
sync_sys+24Cj
.text :0000DA55 push offset aOffln ; "offln"
.text :0000DA5A call SSQ
.text :0000DA5F add esp, 4
.text :0000DA62 mov dl, [ebx]
.text :0000DA64 mov esi, eax
.text :0000DA66 cmp dl, 0Bh
.text :0000DA69 jnz short loc_DA83
.text :0000DA6B cmp esi, 0FE81h
.text :0000DA71 jz OK
.text :0000DA77 cmp esi, 0FFFFF8EFh
.text :0000DA7D jz OK
.text :0000DA83
.text :0000DA83 loc_DA83 : ; CODE XREF:
sync_sys+201j
.text :0000DA83 mov cl, [ebx]
.text :0000DA85 cmp cl, 0Ch
.text :0000DA88 jnz short loc_DA9F
.text :0000DA8A cmp esi, 12A9h
.text :0000DA90 jz OK
.text :0000DA96 cmp esi, 0FFFFFFF5h
.text :0000DA99 jz OK
.text :0000DA9F
.text :0000DA9F loc_DA9F : ; CODE XREF:
sync_sys+220j
.text :0000DA9F mov eax, [ebp+var_18]
.text :0000DAA2 test eax, eax
.text :0000DAA4 jz short loc_DAB0
.text :0000DAA6 push 24h
.text :0000DAA8 call timer
.text :0000DAAD add esp, 4
.text :0000DAB0
.text :0000DAB0 loc_DAB0 : ; CODE XREF:
sync_sys+23Cj
.text :0000DAB0 inc edi
.text :0000DAB1 cmp edi, 3
.text :0000DAB4 jle short loc_DA55
1109
.text :0000DAB6 mov eax, ds :net_env
.text :0000DABB test eax, eax
.text :0000DABD jz short error
...
...
...
Passer outre le dongle est assez facile: il suffit de patcher tous les sauts après les
instructions CMP pertinentes.
Une autre option est d’écrire notre propre driver SCO OpenServer, contenant une
table de questions et de réponses, toutes celles qui sont présentent dans le pro-
1110
gramme.
À propos, nous pouvons aussi essayer de déchiffrer tous les messages d’erreurs.
L’algorithme qui se trouve dans la fonction err_warn() est très simple, en effet:
Listing 8.5: Decryption function
.text :0000A44D mov esi, [ebp+arg_C] ; clef
.text :0000A450 mov edx, [ebp+arg_4] ; chaîne
.text :0000A453 loc_A453 :
.text :0000A453 xor eax, eax
.text :0000A455 mov al, [edx+edi] ; charger l'octet
chiffré
.text :0000A458 xor eax, esi ; le déchiffré
.text :0000A45A add esi, 3 ; changé la clef pour
l'octet suivant
.text :0000A45D inc edi
.text :0000A45E cmp edi, ecx
.text :0000A460 mov [ebp+edi+var_55], al
.text :0000A464 jl short loc_A453
...
L’algorithme est un simple xor : chaque octet est xoré avec la clef, mais la clef est
incrémentée de 3 après le traitement de chaque octet.
Nous pouvons écrire un petit script Python pour vérifier notre hypothèse:
1111
Listing 8.6: Python 3.x
#!/usr/bin/python
import sys
msg=[0x74, 0x72, 0x78, 0x43, 0x48, 0x6, 0x5A, 0x49, 0x4C, 0x47, 0x47,
0x51, 0x4F, 0x47, 0x61, 0x20, 0x22, 0x3C, 0x24, 0x33, 0x36, 0x76,
0x3A, 0x33, 0x31, 0x0C, 0x0, 0x0B, 0x1F, 0x7, 0x1E, 0x1A]
key=0x17
tmp=key
for i in msg :
sys.stdout.write ("%c" % (i^tmp))
tmp=tmp+3
sys.stdout.flush()
Et il affiche: « check security device connection ». Donc oui, ceci est le message
déchiffré.
Il y a d’autres messages chiffrés, avec leur clef correspondante. Mais inutile de dire
qu’il est possible de les déchiffrer sans leur clef. Premièrement, nous voyons que
le clef est en fait un octet. C’est parce que l’instruction principale de déchiffrement
(XOR) fonctionne au niveau de l’octet. La clef se trouve dans le registre ESI, mais
seulement une partie de ESI d’un octet est utilisée. Ainsi, une clef pourrait être plus
grande que 255, mais sa valeur est toujours arrondie.
En conséquence, nous pouvons simplement essayer de brute-forcer, en essayant
toutes les clefs possible dans l’intervalle 0..255. Nous allons aussi écarter les mes-
sages comportants des caractères non-imprimable.
Listing 8.7: Python 3.x
#!/usr/bin/python
import sys, curses.ascii
msgs=[
[0x74, 0x72, 0x78, 0x43, 0x48, 0x6, 0x5A, 0x49, 0x4C, 0x47, 0x47,
0x51, 0x4F, 0x47, 0x61, 0x20, 0x22, 0x3C, 0x24, 0x33, 0x36, 0x76,
0x3A, 0x33, 0x31, 0x0C, 0x0, 0x0B, 0x1F, 0x7, 0x1E, 0x1A],
[0x49, 0x65, 0x2D, 0x63, 0x76, 0x75, 0x6C, 0x6E, 0x76, 0x56, 0x5C,
8, 0x4F, 0x4B, 0x47, 0x5D, 0x54, 0x5F, 0x1D, 0x26, 0x2C, 0x33,
0x27, 0x28, 0x6F, 0x72, 0x75, 0x78, 0x7B, 0x7E, 0x41, 0x44],
[0x45, 0x61, 0x31, 0x67, 0x72, 0x79, 0x68, 0x52, 0x4A, 0x52, 0x50,
0x0C, 0x4B, 0x57, 0x43, 0x51, 0x58, 0x5B, 0x61, 0x37, 0x33, 0x2B,
0x39, 0x39, 0x3C, 0x38, 0x79, 0x3A, 0x30, 0x17, 0x0B, 0x0C],
[0x40, 0x64, 0x79, 0x75, 0x7F, 0x6F, 0x0, 0x4C, 0x40, 0x9, 0x4D, 0x5A,
0x46, 0x5D, 0x57, 0x49, 0x57, 0x3B, 0x21, 0x23, 0x6A, 0x38, 0x23,
0x36, 0x24, 0x2A, 0x7C, 0x3A, 0x1A, 0x6, 0x0D, 0x0E, 0x0A, 0x14,
0x10],
1112
0x24, 0x32, 0x2E, 0x29, 0x28, 0x70, 0x20, 0x22, 0x38, 0x28, 0x36,
0x0D, 0x0B, 0x48, 0x4B, 0x4E]]
def is_string_printable(s) :
return all(list(map(lambda x : curses.ascii.isprint(x), s)))
cnt=1
for msg in msgs :
print ("message #%d" % cnt)
for key in range(0,256) :
result=[]
tmp=key
for i in msg :
result.append (i^tmp)
tmp=tmp+3
if is_string_printable (result) :
print ("key=", key, "value=", "".join(list(map(chr,⤦
Ç result))))
cnt=cnt+1
Et nous obtenons:
Listing 8.8: Results
message #1
key= 20 value= `eb^h%|``hudw|_af{n~f%ljmSbnwlpk
key= 21 value= ajc]i"}cawtgv{^bgto}g"millcmvkqh
key= 22 value= bkd\j#rbbvsfuz !cduh|d#bhomdlujni
key= 23 value= check security device connection
key= 24 value= lifbl !pd|tqhsx#ejwjbb !`nQofbshlo
message #2
key= 7 value= No security device found
key= 8 value= An#rbbvsVuz !cduhld#ghtme ?!#!' !#!
message #3
key= 7 value= Bk<waoqNUpu$`yreoa\wpmpusj,bkIjh
key= 8 value= Mj ?vfnrOjqv%gxqd``_vwlstlk/clHii
key= 9 value= Lm>ugasLkvw&fgpgag^uvcrwml.`mwhj
key= 10 value= Ol !td`tMhwx'efwfbf !tubuvnm !anvok
key= 11 value= No security device station found
key= 12 value= In#rjbvsnuz !{duhdd#r{`whho#gPtme
message #4
key= 14 value= Number of authorized users exceeded
key= 15 value= Ovlmdq !hg#`juknuhydk !vrbsp !Zy`dbefe
message #5
key= 17 value= check security device station
key= 18 value= `ijbh !td`tmhwx'efwfbf !tubuVnm !' !
Ici il y a un peu de déchet, mais nous pouvons rapidement trouver les messages en
anglais.
À propos, puisque l’algorithme est un simple chiffrement xor, la même fonction peut
être utilisée pour chiffrer les messages. Si besoin, nous pouvons chiffrer nos propres
messages, et patcher le programme en les insérant.
1113
8.8.3 Exemple #3: MS-DOS
Un autre très vieux logiciel pour MS-DOS de 1995, lui aussi développé par une société
disparue depuis longtemps.
À l’ ère pré-DOS extenders, presque tous les logiciels pour MS-DOS s’appuyaient sur
sur des CPUs 8086 ou 80286, donc la code était massivement 16-bit.
Le code 16-bit est presque le même que celui déjà vu dans le livre, mais tous les
registres sont 16-bit et il y a moins d’instructions disponibles.
L’environnement MS-DOS n’avait pas de système de drivers, et n’importe quel pro-
gramme pouvait s’adresser au matériel via les ports, donc vous pouvez voir ici les
instructions OUT/IN, qui sont présentes dans la plupart des drivers de nos jours (il est
impossible d’accéder directement aux ports en mode utilisateur sur tous les OSes
modernes).
Compte tenu de ceci, le programme MS-DOS qui fonctionne avec un dongle doit
accéder le port imprimante LPT directement.
Donc nous devons simplement chercher des telles instructions. Et oui, elles y sont:
seg030 :0034 out_port proc far ; CODE XREF: sent_pro+22p
seg030 :0034 ; sent_pro+2Ap ...
seg030 :0034
seg030 :0034 arg_0 = byte ptr 6
seg030 :0034
seg030 :0034 55 push bp
seg030 :0035 8B EC mov bp, sp
seg030 :0037 8B 16 7E E7 mov dx, _out_port ; 0x378
seg030 :003B 8A 46 06 mov al, [bp+arg_0]
seg030 :003E EE out dx, al
seg030 :003F 5D pop bp
seg030 :0040 CB retf
seg030 :0040 out_port endp
1114
seg030 :0056 88 46 FD mov [bp+var_3], al
seg030 :0059 80 E3 1F and bl, 1Fh
seg030 :005C 8A C3 mov al, bl
seg030 :005E EE out dx, al
seg030 :005F 68 FF 00 push 0FFh
seg030 :0062 0E push cs
seg030 :0063 E8 CE FF call near ptr out_port
seg030 :0066 59 pop cx
seg030 :0067 68 D3 00 push 0D3h
seg030 :006A 0E push cs
seg030 :006B E8 C6 FF call near ptr out_port
seg030 :006E 59 pop cx
seg030 :006F 33 F6 xor si, si
seg030 :0071 EB 01 jmp short loc_359D4
seg030 :0073
seg030 :0073 loc_359D3 : ; CODE XREF: sent_pro+37j
seg030 :0073 46 inc si
seg030 :0074
seg030 :0074 loc_359D4 : ; CODE XREF: sent_pro+30j
seg030 :0074 81 FE 96 00 cmp si, 96h
seg030 :0078 7C F9 jl short loc_359D3
seg030 :007A 68 C3 00 push 0C3h
seg030 :007D 0E push cs
seg030 :007E E8 B3 FF call near ptr out_port
seg030 :0081 59 pop cx
seg030 :0082 68 C7 00 push 0C7h
seg030 :0085 0E push cs
seg030 :0086 E8 AB FF call near ptr out_port
seg030 :0089 59 pop cx
seg030 :008A 68 D3 00 push 0D3h
seg030 :008D 0E push cs
seg030 :008E E8 A3 FF call near ptr out_port
seg030 :0091 59 pop cx
seg030 :0092 68 C3 00 push 0C3h
seg030 :0095 0E push cs
seg030 :0096 E8 9B FF call near ptr out_port
seg030 :0099 59 pop cx
seg030 :009A 68 C7 00 push 0C7h
seg030 :009D 0E push cs
seg030 :009E E8 93 FF call near ptr out_port
seg030 :00A1 59 pop cx
seg030 :00A2 68 D3 00 push 0D3h
seg030 :00A5 0E push cs
seg030 :00A6 E8 8B FF call near ptr out_port
seg030 :00A9 59 pop cx
seg030 :00AA BF FF FF mov di, 0FFFFh
seg030 :00AD EB 40 jmp short loc_35A4F
seg030 :00AF
seg030 :00AF loc_35A0F : ; CODE XREF: sent_pro+BDj
seg030 :00AF BE 04 00 mov si, 4
seg030 :00B2
seg030 :00B2 loc_35A12 : ; CODE XREF: sent_pro+ACj
seg030 :00B2 D1 E7 shl di, 1
1115
seg030 :00B4 8B 16 80 E7 mov dx, _in_port_2 ; 0x379
seg030 :00B8 EC in al, dx
seg030 :00B9 A8 80 test al, 80h
seg030 :00BB 75 03 jnz short loc_35A20
seg030 :00BD 83 CF 01 or di, 1
seg030 :00C0
seg030 :00C0 loc_35A20 : ; CODE XREF: sent_pro+7Aj
seg030 :00C0 F7 46 FE 08+ test [bp+var_2], 8
seg030 :00C5 74 05 jz short loc_35A2C
seg030 :00C7 68 D7 00 push 0D7h ; '+'
seg030 :00CA EB 0B jmp short loc_35A37
seg030 :00CC
seg030 :00CC loc_35A2C : ; CODE XREF: sent_pro+84j
seg030 :00CC 68 C3 00 push 0C3h
seg030 :00CF 0E push cs
seg030 :00D0 E8 61 FF call near ptr out_port
seg030 :00D3 59 pop cx
seg030 :00D4 68 C7 00 push 0C7h
seg030 :00D7
seg030 :00D7 loc_35A37 : ; CODE XREF: sent_pro+89j
seg030 :00D7 0E push cs
seg030 :00D8 E8 59 FF call near ptr out_port
seg030 :00DB 59 pop cx
seg030 :00DC 68 D3 00 push 0D3h
seg030 :00DF 0E push cs
seg030 :00E0 E8 51 FF call near ptr out_port
seg030 :00E3 59 pop cx
seg030 :00E4 8B 46 FE mov ax, [bp+var_2]
seg030 :00E7 D1 E0 shl ax, 1
seg030 :00E9 89 46 FE mov [bp+var_2], ax
seg030 :00EC 4E dec si
seg030 :00ED 75 C3 jnz short loc_35A12
seg030 :00EF
seg030 :00EF loc_35A4F : ; CODE XREF: sent_pro+6Cj
seg030 :00EF C4 5E 06 les bx, [bp+arg_0]
seg030 :00F2 FF 46 06 inc word ptr [bp+arg_0]
seg030 :00F5 26 8A 07 mov al, es :[bx]
seg030 :00F8 98 cbw
seg030 :00F9 89 46 FE mov [bp+var_2], ax
seg030 :00FC 0B C0 or ax, ax
seg030 :00FE 75 AF jnz short loc_35A0F
seg030 :0100 68 FF 00 push 0FFh
seg030 :0103 0E push cs
seg030 :0104 E8 2D FF call near ptr out_port
seg030 :0107 59 pop cx
seg030 :0108 8B 16 82 E7 mov dx, _in_port_1 ; 0x37A
seg030 :010C EC in al, dx
seg030 :010D 8A C8 mov cl, al
seg030 :010F 80 E1 5F and cl, 5Fh
seg030 :0112 8A C1 mov al, cl
seg030 :0114 EE out dx, al
seg030 :0115 EC in al, dx
seg030 :0116 8A C8 mov cl, al
1116
seg030 :0118 F6 C1 20 test cl, 20h
seg030 :011B 74 08 jz short loc_35A85
seg030 :011D 8A 5E FD mov bl, [bp+var_3]
seg030 :0120 80 E3 DF and bl, 0DFh
seg030 :0123 EB 03 jmp short loc_35A88
seg030 :0125
seg030 :0125 loc_35A85 : ; CODE XREF: sent_pro+DAj
seg030 :0125 8A 5E FD mov bl, [bp+var_3]
seg030 :0128
seg030 :0128 loc_35A88 : ; CODE XREF: sent_pro+E2j
seg030 :0128 F6 C1 80 test cl, 80h
seg030 :012B 74 03 jz short loc_35A90
seg030 :012D 80 E3 7F and bl, 7Fh
seg030 :0130
seg030 :0130 loc_35A90 : ; CODE XREF: sent_pro+EAj
seg030 :0130 8B 16 82 E7 mov dx, _in_port_1 ; 0x37A
seg030 :0134 8A C3 mov al, bl
seg030 :0136 EE out dx, al
seg030 :0137 8B C7 mov ax, di
seg030 :0139 5F pop di
seg030 :013A 5E pop si
seg030 :013B C9 leave
seg030 :013C CB retf
seg030 :013C sent_pro endp
Ceci est un «hashing » dongle Sentinel Pro, comme dans l’exemple précédent. C’est
remarquable car des chaînes de texte sont passées ici, aussi, et des valeurs 16-bit
sont renvoyées, puis comparées avec d’autres.
Donc, voici comment le Sentinel Pro est accédé via les ports.
L’adresse du port de sortie est en général 0x378, i.e., le port imprimante, où les
données pour les vieilles imprimantes de l’ère pré-USB étaient passées.
Le port est uni-directionnel, car lorsqu’il a été développé, personne n’imaginait que
quelqu’un aurait besoin de transférer de l’information depuis l’imprimante 19 .
Le seul moyen d’obtenir des informations de l’imprimante est le registre d’état sur
le port 0x379, qui contient des bits tels que «paper out », «ack », «busy »—ainsi
l’imprimante peut signaler si elle est prête ou non et si elle a du papier.
Donc, le dongle renvoie de l’information dans l’un de ces bits, un bit à chaque itéra-
tion.
_in_port_2 contient l’adresse du mot d’état (0x379) et _in_port_1 contient le re-
gistre de contrôle d’adresse (0x37A).
Il semble que le dongle renvoie de l’information via le flag «busy » en seg030:00B9 :
chaque bit est stocké dans le registre DI, qui est renvoyé à la fin de la fonction.
Que signifie tous ces octets envoyés sur le port de sortie? Difficile à dire. Peut-être
des commandes pour le dongle.
19. Si nous considérons seulement Centronics. Le standard IEEE 1284 suivant permet le transfert d’in-
formation depuis l’imprimante.
1117
Mais d’une manière générale, il n’est pas nécessaire de savoir: il est facile de ré-
soudre notre tâche sans le savoir.
Voici la routine de vérification du dongle:
00000000 struct_0 struc ; (sizeof=0x1B)
00000000 field_0 db 25 dup(?) ; string(C)
00000019 _A dw ?
0000001B struct_0 ends
1118
seg030 :017C 83 C4 04 add sp, 4
seg030 :017F 89 46 FE mov [bp+var_2], ax
seg030 :0182 8B C6 mov ax, si
seg030 :0184 6B C0 12 imul ax, 18
seg030 :0187 66 0F BF C0 movsx eax, ax
seg030 :018B 66 8B 56 FA mov edx, [bp+var_6]
seg030 :018F 66 03 D0 add edx, eax
seg030 :0192 66 89 16 D8+ mov _expiration, edx
seg030 :0197 8B DE mov bx, si
seg030 :0199 6B DB 1B imul bx, 27
seg030 :019C 8B 87 D5 3C mov ax, _Q._A[bx]
seg030 :01A0 3B 46 FE cmp ax, [bp+var_2]
seg030 :01A3 74 05 jz short loc_35B0A
seg030 :01A5 B8 01 00 mov ax, 1
seg030 :01A8 EB 02 jmp short loc_35B0C
seg030 :01AA
seg030 :01AA loc_35B0A : ; CODE XREF: check_dongle+1Fj
seg030 :01AA ; check_dongle+5Ej
seg030 :01AA 33 C0 xor ax, ax
seg030 :01AC
seg030 :01AC loc_35B0C : ; CODE XREF: check_dongle+63j
seg030 :01AC 5E pop si
seg030 :01AD C9 leave
seg030 :01AE CB retf
seg030 :01AE check_dongle endp
Puisque la routine peut être appelée très fréquemment, e.g., avant l’exécution de
chaque fonctionnalité importante du logiciel, et accéder au ongle est en général lent
(à cause du port de l’imprimante et aussi du MCU lent du dongle), ils ont probable-
ment ajouté un moyen d’éviter le test du dongle, en vérifiant l’heure courante dans
la fonction biostime().
La fonction get_rand() utilise la fonction C standard:
seg030 :01BF get_rand proc far ; CODE XREF: check_dongle+25p
seg030 :01BF
seg030 :01BF arg_0 = word ptr 6
seg030 :01BF
seg030 :01BF 55 push bp
seg030 :01C0 8B EC mov bp, sp
seg030 :01C2 9A 3D 21 00+ call _rand
seg030 :01C7 66 0F BF C0 movsx eax, ax
seg030 :01CB 66 0F BF 56+ movsx edx, [bp+arg_0]
seg030 :01D0 66 0F AF C2 imul eax, edx
seg030 :01D4 66 BB 00 80+ mov ebx, 8000h
seg030 :01DA 66 99 cdq
seg030 :01DC 66 F7 FB idiv ebx
seg030 :01DF 5D pop bp
seg030 :01E0 CB retf
seg030 :01E0 get_rand endp
Donc la chaîne de texte est choisi au hasard, passé au dongle, et ensuite le résultat
du hachage est comparé à la valeur correcte.
1119
Les chaînes de texte semblent être construites aléatoirement aussi, lors du dévelop-
pement du logiciel.
Et voici comment la fonction principale de vérification du dongle est appelée:
seg033 :087B 9A 45 01 96+ call check_dongle
seg033 :0880 0B C0 or ax, ax
seg033 :0882 74 62 jz short OK
seg033 :0884 83 3E 60 42+ cmp word_620E0, 0
seg033 :0889 75 5B jnz short OK
seg033 :088B FF 06 60 42 inc word_620E0
seg033 :088F 1E push ds
seg033 :0890 68 22 44 push offset aTrupcRequiresA ;
"This Software Requires a Software Lock\n"
seg033 :0893 1E push ds
seg033 :0894 68 60 E9 push offset byte_6C7E0 ; dest
seg033 :0897 9A 79 65 00+ call _strcpy
seg033 :089C 83 C4 08 add sp, 8
seg033 :089F 1E push ds
seg033 :08A0 68 42 44 push offset aPleaseContactA ; "Please Contact
..."
seg033 :08A3 1E push ds
seg033 :08A4 68 60 E9 push offset byte_6C7E0 ; dest
seg033 :08A7 9A CD 64 00+ call _strcat
Ceci est relatif au modèle de mémoire de MS-DOS. Vous pouvez en lire plus à ce
sujet ici: 11.6 on page 1309.
Donc, comme vous pouvez le voir, strcpy() et toute autre fonction qui prend un/des
pointeur(s) en argument travaille avec des paires 16-bit.
Retournons à notre exemple. DS est actuellement l’adresse du segment de données
dans l’exécutable, où la chaîne de texte est stockée.
Dans la fonction sent_pro(), chaque octet de la chaîne est chargé en
seg030:00EF : l’instruction LES charge simultanément la paire ES:BX depuis l’argu-
ment transmis.
1120
Le MOV en seg030:00F5 charge l’octet depuis la mémoire sur laquelle pointe la paire
ES:BX.
In[]:= BinaryStrings =
1121
Map[ImportString[#, {"Base64", "String"}] &, ListOfBase64Strings];
In[]:= Variance[Entropies]
Out[]= 0.0238614
La variance est basse. Cela signifie que l’entropie des valeurs ne sont pas très diffé-
rentes les unes des autres. Ceci est visible sur le graphique:
In[]:= ListPlot[Entropies]
La plupart des valeurs sont entre 5.0 et 5.4. Ceci est un signe que les données sont
compressées et/ou chiffrées
Pour comprendre la variance, calculons l’entropie de toutes les liens du livre de Co-
nan Doyle The Hound of the Baskervilles :
In[]:= BaskervillesLines = Import["http ://www.gutenberg.org/cache/epub⤦
Ç /2852/pg2852.txt", "List"];
In[]:= Variance[EntropiesT]
Out[]= 2.73883
In[]:= ListPlot[EntropiesT]
1122
La plupart des valeurs sont regroupées autour de 4, mais il y a aussi des valeurs qui
sont plus petites, et elles influencent la valeur finale de la variance.
Peut-être que les chaînes courtes ont une entropie plus petite, prenons les chaînes
courtes du livre de Conan Doyle.
In[]:= Entropy[2, "Yes, sir."] // N
Out[]= 2.9477
1123
Même des systèmes de chiffrement sans clef primitifs comme memfrob()20 et ROT13
fonctionnent bien sans erreur. C’est un gros défi d’écrire un compresseur depuis zé-
ro, en utilisant seulement sa fantaisie et son imagination de façon à ce qu’il n’ait
pas de bugs évidents. Certains programmeurs implémentent des fonctions de com-
pression de données en lisant des livres, mais ceci est aussi rare. Les deux moyens
les plus fréquents sont: 1) utiliser simplement la bibliothèque open-source zlib; 2)
copier/coller quelque chose de quelque part. Les algorithmes de compression open-
source mettent en général une sorte d’en-tête, ainsi que les algorithmes de sites
comme http://www.codeproject.com/.
1858 blocs ont une taille de 42 octets, 1235 blocs ont une taille de 38 octets, etc.
J’ai fait un graphe:
ListPlot[Counts[Map[StringLength[#] &, BinaryStrings]]]
20. http://linux.die.net/man/3/memfrob
1124
Donc, la plupart des blocs ont une taille entre ~36 et ~48. Il y a un autre chose à
remarquer: tous les blocs ont une taille paire. Pas un bloc n’a une taille impaire.
Il y a, toutefois, des flux de chiffrement qui opèrent au niveau de l’octet ou même
du bit.
8.9.4 CryptoPP
Le programme qui peut parcourir cette base de données chiffrées est écrit en C# et
le code .NET est fortement obscurci. Néanmoins, il y a une DLL avec du code x86, qui,
après un bref examen, contient des parties de la bibliothèque open-source connue
CryptoPP! (J’ai juste repéré des chaînes «CryptoPP » dedans.) Maintenant, c’est très
facile de trouver toutes les fonctions à l’intérieur de la DLL car la bibliothèque Cryp-
toPP est open-source.
La bibliothèque CryptoPP contient beaucoup de fonctions de chiffrement, AES in-
clus (AKA Rijndael). Les CPUs x86 récents possèdent des instructions dédiées à AES
comme AESENC, AESDEC et AESKEYGENASSIST21 . Elles ne font pas le chiffrement/dé-
chiffrement complètement, mais elles font une part significative du travail. Et les
nouvelles versions de CryptoPP les utilisent. Par exemple, ici: 1, 2. À ma surprise,
lors du déchiffrement, AESENC est exécutée, tandis que AESDEC ne l’est pas (j’ai vé-
rifié avec mon utilitaire tracer, mais n’importe quel débogueur peut être utilisé). J’ai
vérifié, si mon CPU supporte réellement les instructions AES. Certains CPUs Intel i3
ne les supportent pas. Et si non, la bibliothèque CryptoPP se rabat sur les fonctions
21. https://en.wikipedia.org/wiki/AES_instruction_set
1125
implémentées de l’ancienne façon 22 . Mais mon CPU les supporte. Pourquoi AESDEC
n’est pas exécuté? Pourquoi le programme utilise le chiffrement AES pour déchiffrer
la base de données?
OK, ce n’est pas un problème de trouver la fonction qui chiffre les blocs. Elle est
appelée
CryptoPP::Rijndael::Enc::ProcessAndXorBlock : src, et elle peut être appelée depuis
une autre fonction:
Rijndael::Enc::AdvancedProcessBlocks() src, qui, à son tour, appelle les deux fonc-
tions: ( AESNI_Enc_Block et AESNI_Enc_4_Blocks ) qui ont les instructions AESENC.
Donc, a en juger par les entrailles de CryptoPP
CryptoPP::Rijndael::Enc::ProcessAndXorBlock() chiffre un bloc 16-octet. Mettons un
point d’arrêt dessus et voyons ce qui se produit pendant le déchiffrement. J’utilise à
nouveau mon petit outil tracer. Le logiciel doit déchiffrer le premier bloc de données
maintenant. Oh, à propos, voici le premier bloc de données converti de l’encodage
en base64 vers des données hexadécimale, faisons le manuellement:
00000000: CA 39 B1 85 75 1B 84 1F F9 31 5E 39 72 13 EC 5D .9..u....1^9r⤦
Ç ..]
00000010: 95 80 27 02 21 D5 2D 1A 0F D9 45 9F 75 EE 24 C4 ..'.!.-...E.u.$⤦
Ç .
00000020: B1 27 7F 84 FE 41 37 86 C9 C0 .'...A7...
22. https://github.com/mmoss/cryptopp/blob/2772f7b57182b31a41659b48d5f35a7b6cedd34d/
src/rijndael.cpp#L355
1126
0038B920 : 01 00 00 00 FF FF FF FF-79 C1 69 0B 67 C1 04 7D "........y.i.g⤦
Ç ..}"
Argument 3/5
0038B978 : CD CD CD CD CD CD CD CD-CD CD CD CD CD CD CD CD ⤦
Ç "................"
(0) software.exe !0x4339a0() -> 0x0
Argument 3/5 difference
00000000: C7 39 4E 7B 33 1B D6 1F-B8 31 10 39 39 13 A5 5D ".9N⤦
Ç {3....1.99..]"
(0) software.exe !0x4339a0(0x38a828, 0x38a838, 0x38bb40, 0x0, 0x8) (called ⤦
Ç from software.exe !.text+0x3a407 (0x13eb407))
Argument 1/5
0038A828 : 95 80 27 02 21 D5 2D 1A-0F D9 45 9F 75 EE 24 C4 "..'.!.-...E.u.$⤦
Ç ."
Argument 2/5
0038A838 : B1 27 7F 84 FE 41 37 86-C9 C0 00 CD CD CD CD CD ".'...A7⤦
Ç ........."
Argument 3/5
0038BB40 : CD CD CD CD CD CD CD CD-CD CD CD CD CD CD CD CD ⤦
Ç "................"
(0) software.exe !0x4339a0() -> 0x0
(0) software.exe !0x4339a0(0x38b920, 0x38a828, 0x38bb30, 0x10, 0x0) (called ⤦
Ç from software.exe !.text+0x33c0d (0x13e4c0d))
Argument 1/5
0038B920 : CA 39 B1 85 75 1B 84 1F-F9 31 5E 39 72 13 EC 5D ".9..u....1^9r⤦
Ç ..]"
Argument 2/5
0038A828 : 95 80 27 02 21 D5 2D 1A-0F D9 45 9F 75 EE 24 C4 "..'.!.-...E.u.$⤦
Ç ."
Argument 3/5
0038BB30 : CD CD CD CD CD CD CD CD-CD CD CD CD CD CD CD CD ⤦
Ç "................"
(0) software.exe !0x4339a0() -> 0x0
Argument 3/5 difference
00000000: 45 00 20 00 4A 00 4F 00-48 00 4E 00 53 00 00 00 "E. .J.O.H.N.S⤦
Ç ..."
(0) software.exe !0x4339a0(0x38b920, 0x0, 0x38b978, 0x10, 0x0) (called from ⤦
Ç software.exe !.text+0x33c0d (0x13e4c0d))
Argument 1/5
0038B920 : 95 80 27 02 21 D5 2D 1A-0F D9 45 9F 75 EE 24 C4 "..'.!.-...E.u.$⤦
Ç ."
Argument 3/5
0038B978 : 95 80 27 02 21 D5 2D 1A-0F D9 45 9F 75 EE 24 C4 "..'.!.-...E.u.$⤦
Ç ."
(0) software.exe !0x4339a0() -> 0x0
Argument 3/5 difference
00000000: B1 27 7F E4 9F 01 E3 81-CF C6 12 FB B9 7C F1 BC ⤦
Ç ".'...........|.."
PID=1984|Process software.exe exited. ExitCode=0 (0x0)
1127
00000000: C7 39 4E 7B 33 1B D6 1F-B8 31 10 39 39 13 A5 5D ".9N⤦
Ç {3....1.99..]"
La première sortie est très similaire aux 16 premiers octets du buffer chiffré.
Sortie du premier appel à ProcessAndXorBlock() :
00000000: C7 39 4E 7B 33 1B D6 1F-B8 31 10 39 39 13 A5 5D ".9N⤦
Ç {3....1.99..]"
Il y a trop d’octets égaux! Comment le résultat du chiffrement AES peut-il être aus-
si similaire au buffer chiffré alors que ceci n’est pas du chiffrement mais bien du
déchiffrement?!
1128
Et le déchiffrement:
Maintenant regardons: le chiffrement AES génère 16 octets (ou 128 bits) de données
aléatoires destinées à être utilisées lors du XOR, qui nous oblige à utiliser tous les
16 octets? Si à la dernière itération nous n’avons qu’un octet de données, nous ne
chiffrons qu’un octet avec un octet de données aléatoires générée. Ceci conduit à
une propriété importante du mode CFB : les données ne doivent pas être adaptées
à une taille, des données de taille arbitraire peuvent être chiffrées et déchiffrées.
Oh, c’est pour ça que les blocs chiffrés ne sont pas complétés. Et c’est pourquoi
l’instruction AESDEC n’est jamais appelée.
Essayons de déchiffrer le premier bloc manuellement, en utilisant Python. Le mode
CFB utilise aussi un IV, comme semence pour CSPRNG24 . Dans notre cas, l’IV est le
bloc qui est chiffré à la première itération:
24. Cryptographically Secure Pseudorandom Number Generator (générateur de nombres pseudo-
aléatoire cryptographiquement sûr)
1129
0038B920 : 01 00 00 00 FF FF FF FF-79 C1 69 0B 67 C1 04 7D "........y.i.g⤦
Ç ..}"
1130
00000000: 5D 90 59 06 EF F4 96 B4 7C 33 A7 4A BE FF 66 AB ].Y.....|3.J..f⤦
Ç .
00000010: 49 00 47 00 47 00 53 00 00 00 00 00 00 C0 65 40 I.G.G.S.......⤦
Ç e@
00000020: D4 07 06 01 ....
00000000: D3 15 34 5D 21 18 7C 6E AA F8 2D FE 38 F9 D7 4E ..4]!.|n..-.8..⤦
Ç N
00000010: 41 00 20 00 44 00 4F 00 48 00 45 00 52 00 54 00 A. .D.O.H.E.R.T⤦
Ç .
00000020: 59 00 48 E1 7A 14 AE FF 68 40 D4 07 06 02 Y.H.z...h@....
00000000: 1E 8B 90 0A 17 7B C5 52 31 6C 4E 2F DE 1B 27 19 .....{.R1lN⤦
Ç ...'.
00000010: 41 00 52 00 43 00 55 00 53 00 00 00 00 00 00 60 A.R.C.U.S⤦
Ç .......
00000020: 66 40 D4 07 06 03 f@....
Tous les blocs déchiffrés semblent correct, à l’exception des 16 premiers octets.
…troisième:
0038B920 : 03 00 00 00 FD FF FF FF-79 C1 69 0B 67 C1 04 7D "........y.i.g⤦
Ç ..}"
Il semble que le premier et le cinquième octet changent à chaque fois. J’en ai finale-
ment conclu que le premier entier 32-bit est simplement OrderID du fichier XML, et le
second entier 32-bit est aussi OrderID, mais multiplié par -1. Tous les 8 autres octets
sont les mêmes pour chaque opération. Maintenant, j’ai déchiffré la base de données
entière: https://beginners.re/current-tree/examples/encrypted_DB1/decrypted.
full.txt.
Le script Python utilisé pour ceci est: https://beginners.re/current-tree/examples/
encrypted_DB1/decrypt_blocks.py.
1131
Peut-être que l’auteur voulait chiffrer chaque bloc différemment, donc il a utilisé
OrderID comme une partie de la clef. Il aurait aussi été possible de créer une clef
AES différente, au lieu de l’IV.
Dinc maintenant nous savons que l’IV affecte seulement le premier bloc lors du
déchiffrement en mode CFB, ceci en est une caractéristique. Tous les autres blocs
peuvent être déchiffrés sans connaître l’IV, mais en utilisant la clef.
OK, donc pourquoi le mode CFB ? Apparemment, parce que le tout premier exemple
sur le wiki de CryptoPP utilise le mode CFB : http://www.cryptopp.com/wiki/Advanced_
Encryption_Standard#Encrypting_and_Decrypting_Using_AES. On peut aussi sup-
poser que le développeur l’a choisi pour sa simplicité: l’exemple peut chiffrer/déchif-
frer des chaînes de texte de longueur arbitraire, sans remplissage.
Il est aussi probable que l’auteur du programme a juste copié/collé l’exemple depuis
la page wiki de CryptoPP. Beaucoup de programmeurs font ça.
La seule différence est que l’IV est choisi aléatoirement dans l’exemple du wiki de
CryptoPP, alors que cet indéterminisme n’était pas permis aux programmeurs du lo-
giciel que nous disséquons maintenant, donc ils ont choisi d’initialiser l’IV en utilisant
OrderID.
Nous pouvons maintenant procéder à l’analyse du cas de chaque octet dans le bloc
déchiffré.
00000000: 0F 00 FF FE 4D 00 45 00 4C 00 49 00 4E 00 44 00 ....M.E.L.I.N.D⤦
Ç .
00000010: 41 00 20 00 44 00 4F 00 48 00 45 00 52 00 54 00 A. .D.O.H.E.R.T⤦
Ç .
00000020: 59 00 48 E1 7A 14 AE FF 68 40 D4 07 06 02 Y.H.z...h@....
1132
On voit clairement des chaînes de textes encodées en UTF-16, ce sont les noms et
noms de famille. Le premier octet (ou mot de 16-bit) semble être la longueur de la
chaîne, nous pouvons vérifier visuellement. FF FE semble être le BOM Unicode.
Il y a 12 autres octets après chaque chaîne.
En utilisant ce script (https://beginners.re/current-tree/examples/encrypted_
DB1/dump_buffer_rest.py) j’ai obtenu une sélection aléatoire de fins (de bloc) :
dennis@...:$ python decrypt.py encrypted.xml | shuf | head -20
00000000: 48 E1 7A 14 AE 5F 62 40 DD 07 05 08 H.z.._b@....
00000000: 00 00 00 00 00 40 5A 40 DC 07 08 18 .....@Z@....
00000000: 00 00 00 00 00 80 56 40 D7 07 0B 04 ......V@....
00000000: 00 00 00 00 00 60 61 40 D7 07 0C 1C ......a@....
00000000: 00 00 00 00 00 20 63 40 D9 07 05 18 ..... c@....
00000000: 3D 0A D7 A3 70 FD 34 40 D7 07 07 11 =...p.4@....
00000000: 00 00 00 00 00 A0 63 40 D5 07 05 19 ......c@....
00000000: CD CC CC CC CC 3C 5C 40 D7 07 08 11 .......@....
00000000: 66 66 66 66 66 FE 62 40 D4 07 06 05 fffff.b@....
00000000: 1F 85 EB 51 B8 FE 40 40 D6 07 09 1E ...Q..@@....
00000000: 00 00 00 00 00 40 5F 40 DC 07 02 18 .....@_@....
00000000: 48 E1 7A 14 AE 9F 67 40 D8 07 05 12 H.z...g@....
00000000: CD CC CC CC CC 3C 5E 40 DC 07 01 07 ......^@....
00000000: 00 00 00 00 00 00 67 40 D4 07 0B 0E ......g@....
00000000: 00 00 00 00 00 40 51 40 DC 07 04 0B .....@Q@....
00000000: 00 00 00 00 00 40 56 40 D7 07 07 0A .....@V@....
00000000: 8F C2 F5 28 5C 7F 55 40 DB 07 01 16 ...(..U@....
00000000: 00 00 00 00 00 00 32 40 DB 07 06 09 ......2@....
00000000: 66 66 66 66 66 7E 66 40 D9 07 0A 06 fffff~f@....
00000000: 48 E1 7A 14 AE DF 68 40 D5 07 07 16 H.z...h@....
Nous voyons tout d’abord que les octets 0x40 et 0x07 sont présent dans chaque
fin. Le tout dernier octet est toujours dans l’intervalle 1..0x1F (1..31), j’ai vérifié. Le
pénultième octet est toujours dans l’intervalle 1..0xC (1..12). Ouah, ça ressemble à
une date! L’année peut être représentée comme une valeur 16-bit, et peut-être que
les 4 derniers octets sont une date (16 bits pour l’année, 8 bits pour le mois et les 8
restants pour le jour) ? 0x7DD est 2013, 0x7D5 est 2005, etc. Ça semble juste. Ceci
est une date. Il y a 8 octets supplémentaires. À en juger par le fait que ceci est une
base de données appelée orders, peut-être s’agit-il d’une sorte de somme ici? J’ai
essayé de les interpréter comme des réels en double précision IEEE 754 et ai affiché
toutes les valeurs!
Certaines sont:
71.0
134.0
51.95
53.0
121.99
96.95
98.95
15.95
85.95
184.99
1133
94.95
29.95
85.0
36.0
130.99
115.95
87.99
127.95
114.0
150.95
plain :
00000000: 0B 00 FF FE 4C 00 4F 00 52 00 49 00 20 00 42 00 ....L.O.R.I. .B⤦
Ç .
00000010: 41 00 52 00 52 00 4F 00 4E 00 CD CC CC CC CC CC A.R.R.O.N⤦
Ç .......
00000020: 1B 40 D4 07 06 01 .@....
OrderID= 2 name= LORI BARRON sum= 6.95 date= 2004 / 6 / 1
plain :
00000000: 0A 00 FF FE 47 00 41 00 52 00 59 00 20 00 42 00 ....G.A.R.Y. .B⤦
Ç .
00000010: 49 00 47 00 47 00 53 00 00 00 00 00 00 C0 65 40 I.G.G.S.......⤦
Ç e@
00000020: D4 07 06 01 ....
OrderID= 3 name= GARY BIGGS sum= 174.0 date= 2004 / 6 / 1
plain :
00000000: 0F 00 FF FE 4D 00 45 00 4C 00 49 00 4E 00 44 00 ....M.E.L.I.N.D⤦
Ç .
00000010: 41 00 20 00 44 00 4F 00 48 00 45 00 52 00 54 00 A. .D.O.H.E.R.T⤦
Ç .
00000020: 59 00 48 E1 7A 14 AE FF 68 40 D4 07 06 02 Y.H.z...h@....
OrderID= 4 name= MELINDA DOHERTY sum= 199.99 date= 2004 / 6 / 2
plain :
00000000: 0B 00 FF FE 4C 00 45 00 4E 00 41 00 20 00 4D 00 ....L.E.N.A. .M⤦
Ç .
00000010: 41 00 52 00 43 00 55 00 53 00 00 00 00 00 00 60 A.R.C.U.S⤦
Ç .......
00000020: 66 40 D4 07 06 03 f@....
OrderID= 5 name= LENA MARCUS sum= 179.0 date= 2004 / 6 / 3
1134
En voir plus: https://beginners.re/current-tree/examples/encrypted_DB1/decrypted.
full.with_data.txt. Ou filtré: https://beginners.re/current-tree/examples/
encrypted_DB1/decrypted.short.txt. Ça semble correct.
Ceci est une sorte de sérialisation POO, i.e., stockant différents types de valeurs
dans un buffer binaire pour le stocker et/ou le transmettre.
00000000: 0E 00 FF FE 54 00 48 00 45 00 52 00 45 00 53 00 ....T.H.E.R.E.S⤦
Ç .
00000010: 45 00 20 00 54 00 55 00 54 00 54 00 4C 00 45 00 E. .T.U.T.T.L.E⤦
Ç .
00000020: 66 66 66 66 66 1E 63 40 D4 07 07 1A 00 07 07 19 fffff.c@⤦
Ç ........
OrderID= 172 name= THERESE TUTTLE sum= 152.95 date= 2004 / 7 / 26
1135
00000000: 0D 00 FF FE 4C 00 4F 00 52 00 45 00 4E 00 45 00 ....L.O.R.E.N.E⤦
Ç .
00000010: 20 00 4F 00 54 00 4F 00 4F 00 4C 00 45 00 CD CC .O.T.O.O.L.E⤦
Ç ...
00000020: CC CC CC 3C 5E 40 D4 07 09 02 ...<^@....
OrderID= 285 name= LORENE OTOOLE sum= 120.95 date= 2004 / 9 / 2
00000000: 0C 00 FF FE 4D 00 45 00 4C 00 41 00 4E 00 49 00 ....M.E.L.A.N.I⤦
Ç .
00000010: 45 00 20 00 4B 00 49 00 52 00 4B 00 00 00 00 00 E. .K.I.R.K⤦
Ç .....
00000020: 00 20 64 40 D4 07 09 02 00 02 . d@......
OrderID= 286 name= MELANIE KIRK sum= 161.0 date= 2004 / 9 / 2
8.9.9 Conclusion
Résumé: Chaque rétro-ingénieur pratiquant doit être familier avec la majorité des
algorithmes ainsi que la majorité des modes de chiffrement. Quelques livres à ce
sujet: 12.1.10 on page 1328.
Le contenu chiffré de la base de données a été artificiellement construit par moi, pour
les besoins de la démonstration. J’ai obtenu les nom et noms de famille les plus répan-
dus au USA ici: http://stackoverflow.com/questions/1803628/raw-list-of-person-names,
et les ai combiné aléatoirement. Les dates et montants ont aussi été générés aléa-
toirement.
Tous les fichiers utilisés dans cette partie sont ici: https://beginners.re/current-tree/
examples/encrypted_DB1.
Néanmoins, j’ai observé de telles caractéristiques dans des logiciels réels. Cet exemple
est basé dessus.
1136
8.10 Overclocker le mineur de Bitcoin Cointerra
Il y avait le mineur de Bitcoin Cointerra, ressemblant à ceci:
Et il y avait aussi (peut-être leaké) l’utilitaire25 qui peut définir la fréquence d’horloge
pour la carte. Il fonctionne sur une carte additionnelle BeagleBone Linux ARM (petite
carte en bas de l’image).
Et on m’avait demandé une fois s’il est possible de modifier cet utilitaire pour voir
quelles sont les fréquences qui peuvent être définies, et celles qui ne peuvent pas
l’être. Et est-il possible de l’ajuster?
L’utilitaire doit être exécuté comme cela: ./cointool-overclock 0 0 900, où 900
est la fréquence en MHz. Si la fréquence est trop grande, l’utilitaire affiche «Error
with arguments » et se termine.
Ceci est le morceau de code autour de la référence à la chaîne de texte «Error with
25. Peut être téléchargé ici: https://beginners.re/current-tree/examples/bitcoin_miner/
files/cointool-overclock
1137
arguments » :
...
1138
.text :0000AC90 BGT errors_with_arguments
.text :0000AC94 LDR R2, [R11,#third_argument]
.text :0000AC98 MOV R3, #0x51EB851F
.text :0000ACA0 SMULL R1, R3, R3, R2
.text :0000ACA4 MOV R1, R3,ASR#4
.text :0000ACA8 MOV R3, R2,ASR#31
.text :0000ACAC RSB R3, R3, R1
.text :0000ACB0 MOV R1, #50
.text :0000ACB4 MUL R3, R1, R3
.text :0000ACB8 RSB R3, R3, R2
.text :0000ACBC CMP R3, #0
.text :0000ACC0 BEQ loc_ACEC
.text :0000ACC4
.text :0000ACC4 errors_with_arguments
.text :0000ACC4
.text :0000ACC4 LDR R3, [R11,#argv]
.text :0000ACC8 LDR R3, [R3]
.text :0000ACCC MOV R0, R3 ; path
.text :0000ACD0 BL __xpg_basename
.text :0000ACD4 MOV R3, R0
.text :0000ACD8 MOV R0, #aSErrorWithArgu ; format
.text :0000ACE0 MOV R1, R3
.text :0000ACE4 BL printf
.text :0000ACE8 B loc_ADD4
.text :0000ACEC ;
------------------------------------------------------------
.text :0000ACEC
.text :0000ACEC loc_ACEC ; CODE XREF: main+66C
.text :0000ACEC LDR R2, [R11,#third_argument]
.text :0000ACF0 MOV R3, #499
.text :0000ACF4 CMP R2, R3
.text :0000ACF8 BGT loc_AD08
.text :0000ACFC MOV R3, #0x64
.text :0000AD00 STR R3, [R11,#unk_constant]
.text :0000AD04 B jump_to_write_power
.text :0000AD08 ;
------------------------------------------------------------
.text :0000AD08
.text :0000AD08 loc_AD08 ; CODE XREF: main+6A4
.text :0000AD08 LDR R2, [R11,#third_argument]
.text :0000AD0C MOV R3, #799
.text :0000AD10 CMP R2, R3
.text :0000AD14 BGT loc_AD24
.text :0000AD18 MOV R3, #0x5F
.text :0000AD1C STR R3, [R11,#unk_constant]
.text :0000AD20 B jump_to_write_power
.text :0000AD24 ;
------------------------------------------------------------
.text :0000AD24
.text :0000AD24 loc_AD24 ; CODE XREF: main+6C0
.text :0000AD24 LDR R2, [R11,#third_argument]
.text :0000AD28 MOV R3, #899
.text :0000AD2C CMP R2, R3
.text :0000AD30 BGT loc_AD40
.text :0000AD34 MOV R3, #0x5A
.text :0000AD38 STR R3, [R11,#unk_constant]
1139
.text :0000AD3C B jump_to_write_power
.text :0000AD40 ;
------------------------------------------------------------
.text :0000AD40
.text :0000AD40 loc_AD40 ; CODE XREF: main+6DC
.text :0000AD40 LDR R2, [R11,#third_argument]
.text :0000AD44 MOV R3, #999
.text :0000AD48 CMP R2, R3
.text :0000AD4C BGT loc_AD5C
.text :0000AD50 MOV R3, #0x55
.text :0000AD54 STR R3, [R11,#unk_constant]
.text :0000AD58 B jump_to_write_power
.text :0000AD5C ;
------------------------------------------------------------
.text :0000AD5C
.text :0000AD5C loc_AD5C ; CODE XREF: main+6F8
.text :0000AD5C LDR R2, [R11,#third_argument]
.text :0000AD60 MOV R3, #1099
.text :0000AD64 CMP R2, R3
.text :0000AD68 BGT jump_to_write_power
.text :0000AD6C MOV R3, #0x50
.text :0000AD70 STR R3, [R11,#unk_constant]
.text :0000AD74
.text :0000AD74 jump_to_write_power ; CODE XREF:
main+6B0
.text :0000AD74 ; main+6CC ...
.text :0000AD74 LDR R3, [R11,#var_28]
.text :0000AD78 UXTB R1, R3
.text :0000AD7C LDR R3, [R11,#var_2C]
.text :0000AD80 UXTB R2, R3
.text :0000AD84 LDR R3, [R11,#unk_constant]
.text :0000AD88 UXTB R3, R3
.text :0000AD8C LDR R0, [R11,#third_argument]
.text :0000AD90 UXTH R0, R0
.text :0000AD94 STR R0, [SP,#0x44+var_44]
.text :0000AD98 LDR R0, [R11,#var_24]
.text :0000AD9C BL write_power
.text :0000ADA0 LDR R0, [R11,#var_24]
.text :0000ADA4 MOV R1, #0x5A
.text :0000ADA8 BL read_loop
.text :0000ADAC B loc_ADD4
...
...
Les noms de fonctions étaient présents dans les informations de débogage du binaire
original, comme write_power, read_loop. Mais j’ai nommé les labels à l’intérieur
des fonctions.
Le nom optind semble familier. Il provient de la bibliothèque *NIX getopt qui sert
à traiter les arguments de la ligne de commande—bien, c’est exactement ce qui se
1140
passe dans ce code. Ensuite, le 3ème argument (où la valeur de la fréquence est
passée) est converti d’une chaîne vers un nombre en utilisant un appel à la fonction
strtoll().
La valeur est ensuite encore comparée par rapport à diverses constantes. En 0xA-
CEC, elle est testée, si elle est inférieure ou égale à 499, et si c’est le cas, 0x64 est
passé à la fonction write_power() (qui envoie une commande par USB en utilisant
send_msg()). Si elle est plus grande que 499, un saut en 0xAD08 se produit.
En 0xAD08 on teste si elle est inférieure ou égale à 799. En cas de succès 0x5F est
alors passé à la fonction write_power().
Il y a d’autres tests: par rapport à 899 en 0xAD24, à 0x999 en 0xAD40 et enfin,
à 1099 en 0xAD5C. Si la fréquence est inférieure ou égale à 1099, 0x50 est pas-
sé (en 0xAD6C) à la fonction write_power(). Et il y a une sorte de bug. Si la va-
leur est encore plus grande que 1099, la valeur elle-même est passée à la fonction
write_power(). Oh, ce n’est pas un bug, car nous ne pouvons pas arriver là: la va-
leur est d’abord comparée à 950 en 0xAC88, et si elle est plus grande, un message
d’erreur est affiché et l’utilitaire s’arrête.
Maintenant, la table des fréquences en MHz et la valeur passée à la fonction write_power() :
1141
Je ne suis pas allé plus loin, mais si je devais, j’essayerai de diminuer la valeur qui
est passée à la fonction write_power().
Maintenant, le morceau de code effrayant que j’ai passé en premier:
.text :0000AC94 LDR R2, [R11,#third_argument]
.text :0000AC98 MOV R3, #0x51EB851F
.text :0000ACA0 SMULL R1, R3, R3, R2 ; R3=3rg_arg/3.125
.text :0000ACA4 MOV R1, R3,ASR#4 ; R1=R3/16=3rg_arg/50
.text :0000ACA8 MOV R3, R2,ASR#31 ; R3=MSB(3rg_arg)
.text :0000ACAC RSB R3, R3, R1 ; R3=3rd_arg/50
.text :0000ACB0 MOV R1, #50
.text :0000ACB4 MUL R3, R1, R3 ; R3=50*(3rd_arg/50)
.text :0000ACB8 RSB R3, R3, R2
.text :0000ACBC CMP R3, #0
.text :0000ACC0 BEQ loc_ACEC
.text :0000ACC4
.text :0000ACC4 errors_with_arguments
Cela signifie que l’instruction SMULL en 0xACA0 divise le 3ème argument par 3.125.
En fait, tout ce que la fonction modinv32() de mon calculateur fait est ceci:
1 232
input
=
232
input
x
x − (( ) ⋅ 50)
50
1142
8.11 Casser le simple exécutable cryptor
J’ai un fichier exécutable qui est chiffré par un chiffrement relativement simple. Il est
ici (seule la section exécutable est laissée ici).
Tout d’abord, tout ce que fait la fonction de chiffrement, c’est d’ajouter l’index de la
position dans le buffer à l’octet. Voici comment ça peut être implémenté:
def encrypt(buf) :
return e(buf[0], 0)+ e(buf[1], 1)+ e(buf[2], 2) + e(buf[3], 3)+ e(buf⤦
Ç [4], 4)+ e(buf[5], 5)+ e(buf[6], 6)+ e(buf[7], 7)+
e(buf[8], 8)+ e(buf[9], 9)+ e(buf[10], 10)+ e(buf[11], 11)+ e(⤦
Ç buf[12], 12)+ e(buf[13], 13)+ e(buf[14], 14)+ e(buf[15], 15)
Ainsi, si vous chiffrez un buffer avec 16 zéros, vous obtiendrez 0, 1, 2, 3 ... 12, 13,
14, 15.
La Propagating Cipher Block Chaining (PCBC) est aussi utilisée, voici comment elle
fonctionne:
Fig. 8.15: Chiffrement avec Propagating Cipher Block Chaining (l’image provient d’un
article Wikipédia)
Le problème est qu’il est trop ennuyant de retrouver l’IV à chaque fois. La force
brute n’est pas une option, car l’IV est trop long (16 octets). Voyons s’il est possible
de recouvrer l’IV pour un fichier binaire exécutable arbitraire?
Essayons la simple analyse de fréquence. Ceci est du code exécutable 32-bit x86,
donc collectons des statistiques sur les octets et les opcodes les plus fréquents. J’ai
1143
essayé le fichier géant oracle.exe d’ Oracle RDBMS version 11.2 pour windows x86
et j’ai trouvé que l’octet le plus fréquent (pas de surprise) est zéro ( 10%). L’octet
suivant le plus fréquent est (encore une fois, sans surprise) 0xFF ( 5%). Le suivant
est 0x8B ( 5%).
0x8B est l’opcode de MOV, ceci est en effet l’une des instructions x86 les plus fré-
quentes. Maintenant, que dire de la popularité de l’octet zéro? Si le compilateur doit
encoder une valeur plus grande que 127, il doit utiliser un déplacement 32-bit au
lieu d’un de 8-bit, mais les grandes valeurs sont très rares ( 2.1.8 on page 583), donc
il est complété par des zéros. C’est le cas au moins avec LEA, MOV, PUSH, CALL.
Par exemple:
8D B0 28 01 00 00 lea esi, [eax+128h]
8D BF 40 38 00 00 lea edi, [edi+3840h]
Les déplacements plus grand que 127 sont très fréquents, mais ils excèdent rare-
ment 0x10000 (en effet, des buffers mémoire/structures aussi grands sont aussi
rares).
Même chose avec MOV, les grandes constantes sont rares, les plus utilisées sont 0,
1, 10, 100, 2n , et ainsi de suite. Le compilateur doit compléter les petites constantes
avec des zéros pour les encoder comme des valeurs 32-bit:
BF 02 00 00 00 mov edi, 2
BF 01 00 00 00 mov edi, 1
Maintenant parlons des octets 00 et 0xFF combinés: les sauts (conditionnels inclus)
et appels peuvent transférer le flux d’exécution en avant ou en arrière, mais très
souvent, dans les limites du module exécutable courant. Si c’est en avant, le dépla-
cement n’est pas très grand et il y a des zéros ajoutés. Si c’est en arrière, le déplace-
ment est représenté par une valeur négative, donc complétée par des octets 0xFF.
Par exemple, transfert du flux d’exécution en avant:
E8 43 0C 00 00 call _function1
E8 5C 00 00 00 call _function2
0F 84 F0 0A 00 00 jz loc_4F09A0
0F 84 EB 00 00 00 jz loc_4EFBB8
En arrière:
E8 79 0C FE FF call _function1
E8 F4 16 FF FF call _function2
0F 84 F8 FB FF FF jz loc_8212BC
0F 84 06 FD FF FF jz loc_FF1E7D
L’octet 0xFF se rencontre aussi très souvent dans des déplacements négatifs, comme
ceux-ci:
8D 85 1E FF FF FF lea eax, [ebp-0E2h]
8D 95 F8 5C FF FF lea edx, [ebp-0A308h]
1144
Jusqu’ici, tout va bien. Maintenant nous devons essayer diverses clefs 16-octet, dé-
chiffrer la section exécutable et mesurer les occurences des octets 0, 0xFF et 0x8B.
Gardons en vue la façon dont le déchiffrement PCBC fonctionne:
Fig. 8.16: Propagating Cipher Block Chaining decryption (l’image provient d’un article
Wikipédia)
La bonne nouvelle est que nous n’avons pas vraiment besoin de déchiffrer l’en-
semble des données, mais seulement slice par slice, ceci est exactement comment
j’ai procédé dans mon exemple précédent: 9.1.5 on page 1232.
Maintenant j’essaye tous les octets possible (0..255) pour chaque octet dans la clef
et je prends l’octet produisant le plus grande nombre d’octets 0x0/0xFF/0x8B dans
le slice déchiffré:
# !/usr/bin/env python
import sys, hexdump, array, string, operator
KEY_LEN=16
def chunks(l, n) :
# split n by l-byte chunks
# https://stackoverflow.com/q/312443
n = max(1, n)
return [l[i :i + n] for i in range(0, len(l), n)]
def read_file(fname) :
file=open(fname, mode='rb')
content=file.read()
file.close()
return content
1145
def XOR_PCBC_step (IV, buf, k) :
prev=IV
rt=""
for c in buf :
new_c=decrypt_byte(c, k)
plain=chr(ord(new_c)^ord(prev))
prev=chr(ord(c)^ord(plain))
rt=rt+plain
return rt
each_Nth_byte=[""]*KEY_LEN
content=read_file(sys.argv[1])
# split input by 16-byte chunks:
all_chunks=chunks(content, KEY_LEN)
for c in all_chunks :
for i in range(KEY_LEN) :
each_Nth_byte[i]=each_Nth_byte[i] + c[i]
1146
(213, 1332)
N= 8
(225, 1251)
N= 9
(112, 1223)
N= 10
(143, 1177)
N= 11
(108, 1286)
N= 12
(10, 1164)
N= 13
(3, 1271)
N= 14
(128, 1253)
N= 15
(232, 1330)
def xor_strings(s,t) :
# https://en.wikipedia.org/wiki/XOR_cipher#Example_implementation
"""xor two strings together"""
return "".join(chr(ord(a)^ord(b)) for a,b in zip(s,t))
IV=array.array('B', [147, 94, 252, 218, 38, 192, 199, 213, 225, 112, 143, ⤦
Ç 108, 10, 3, 128, 232]).tostring()
def chunks(l, n) :
n = max(1, n)
return [l[i :i + n] for i in range(0, len(l), n)]
def read_file(fname) :
file=open(fname, mode='rb')
content=file.read()
file.close()
return content
def decrypt_byte(i, k) :
return chr ((ord(i)-k) % 256)
def decrypt(buf) :
return "".join(decrypt_byte(buf[i], i) for i in range(16))
fout=open(sys.argv[2], mode='wb')
prev=IV
content=read_file(sys.argv[1])
tmp=chunks(content, 16)
for c in tmp :
1147
new_c=decrypt(c)
p=xor_strings (new_c, prev)
prev=xor_strings(c, p)
fout.write(p)
fout.close()
...
5: 8b ff mov %edi,%edi
7: 55 push %ebp
8: 8b ec mov %esp,%ebp
a : 51 push %ecx
b : 53 push %ebx
c : 33 db xor %ebx,%ebx
e : 43 inc %ebx
f : 84 1d a0 e2 05 01 test %bl,0x105e2a0
15: 75 09 jne 0x20
17: ff 75 08 pushl 0x8(%ebp)
1a : ff 15 b0 13 00 01 call *0x10013b0
20: 6a 6c push $0x6c
22: ff 35 54 d0 01 01 pushl 0x101d054
28: ff 15 b4 13 00 01 call *0x10013b4
2e : 89 45 fc mov %eax,-0x4(%ebp)
31: 85 c0 test %eax,%eax
33: 0f 84 d9 00 00 00 je 0x112
39: 56 push %esi
3a : 57 push %edi
3b : 6a 00 push $0x0
3d : 50 push %eax
3e : ff 15 b8 13 00 01 call *0x10013b8
44: 8b 35 bc 13 00 01 mov 0x10013bc,%esi
4a : 8b f8 mov %eax,%edi
4c : a1 e0 e2 05 01 mov 0x105e2e0,%eax
51: 3b 05 e4 e2 05 01 cmp 0x105e2e4,%eax
57: 75 12 jne 0x6b
59: 53 push %ebx
5a : 6a 03 push $0x3
5c : 57 push %edi
5d : ff d6 call *%esi
...
Oui, ceci semble être un morceau correctement désassemblé de code x86. Le fichier
déchiffré entier peut être téléchargé ici.
En fait, ceci est la section text du regedit.exe de Windows 7. Mais cet exemple est
basé sur un cas réel que j’ai rencontré, seul l’exécutable est différent (et la clef),
l’algorithme est le même.
1148
8.11.1 Autres idées à prendre en considération
Et si j’avais échoué avec cette simple analyse des fréquences? Il y a d’autres idées
sur la façon de mesurer l’exactitude de code x86 déchiffré/décompressé:
• Les compilateurs modernes alignent les fonctions sur une limite de 0x10. Donc
l’espace libre avant est rempli avec de NOPs (0x90) ou d’autres instructions
avec des opcodes connus: .1.7 on page 1359.
• Peut-être que le pattern le plus fréquent dans tout langage d’assemblage est
l’appel de fonction:
PUSH chain / CALL / ADD ESP, X. Cette séquence peut facilement être dé-
tectée et trouvée. J’ai même collecté des statistiques sur le nombre moyen d’ar-
guments des fonctions: 11.2 on page 1301. (Ainsi, ceci est la longueur moyenne
d’une chaîne PUSH.)
En savoir plus sur le code désassemblé incorrectement/correctement: 5.11 on page 953.
8.12 SAP
8.12.1 À propos de la compression du trafic réseau par le
client SAP
(Tracer la connexion entre la variable d’environnement TDW_NOCOMPRESS SAPGUI28
et la fenêtre pop-up gênante et ennuyeuse et la routine de compression de données
actuelle.)
On sait que le trafic réseau entre le SAPGUI et SAP n’est pas chiffré par défaut, mais
compressé (voir ici29 et ici30 ).
Il est aussi connue que mettre la variable d’environnement TDW_NOCOMPRESS à 1,
permet d’arrêter la compression des paquets réseau.
Mais vous verrez toujours l’ennuyeuse fenêtre pop-up, qui ne peut pas être fermée:
28. client SAP GUI
29. http://blog.yurichev.com/node/44
30. blog.yurichev.com
1149
Fig. 8.17: Screenshot
31. http://www.farmanager.com/
1150
.text :6440D52E pop ecx
.text :6440D52F push offset byte_64443AF8
.text :6440D534 lea ecx, [ebp+2108h+var_211C]
La chaîne renvoyée par chk_env() via son second argument est ensuite traitée par
la fonction de chaîne MFC et ensuite atoi()32 est appelée. Après ça, la valeur nu-
mérique est stockée en edi+15h.
Jetons aussi un œil à la fonction chk_env() (nous avons donné ce nom manuelle-
ment) :
.text :64413F20 ; int __cdecl chk_env(char *VarName, int)
.text :64413F20 chk_env proc near
.text :64413F20
.text :64413F20 DstSize = dword ptr -0Ch
.text :64413F20 var_8 = dword ptr -8
.text :64413F20 DstBuf = dword ptr -4
.text :64413F20 VarName = dword ptr 8
.text :64413F20 arg_4 = dword ptr 0Ch
.text :64413F20
.text :64413F20 push ebp
.text :64413F21 mov ebp, esp
.text :64413F23 sub esp, 0Ch
.text :64413F26 mov [ebp+DstSize], 0
.text :64413F2D mov [ebp+DstBuf], 0
.text :64413F34 push offset unk_6444C88C
.text :64413F39 mov ecx, [ebp+arg_4]
32. fonction C standard qui convertit les chiffres d’une chaîne en un nombre
1151
.text :64413F51 push eax ; ReturnSize
.text :64413F52 call ds :getenv_s
.text :64413F58 add esp, 10h
.text :64413F5B mov [ebp+var_8], eax
.text :64413F5E cmp [ebp+var_8], 0
.text :64413F62 jz short loc_64413F68
.text :64413F64 xor eax, eax
.text :64413F66 jmp short loc_64413FBC
.text :64413F68
.text :64413F68 loc_64413F68 :
.text :64413F68 cmp [ebp+DstSize], 0
.text :64413F6C jnz short loc_64413F72
.text :64413F6E xor eax, eax
.text :64413F70 jmp short loc_64413FBC
.text :64413F72
.text :64413F72 loc_64413F72 :
.text :64413F72 mov ecx, [ebp+DstSize]
.text :64413F75 push ecx
.text :64413F76 mov ecx, [ebp+arg_4]
1152
.text :64413FBE pop ebp
.text :64413FBF retn
.text :64413FBF chk_env endp
La configuration de chaque variable est écrit dans le tableau via le pointeur dans le
registre EDI. EDI est renseigné avant l’appel à la fonction:
.text :6440EE00 lea edi, [ebp+2884h+var_2884] ; options
here like +0x15...
.text :6440EE03 lea ecx, [esi+24h]
.text :6440EE06 call load_command_line
.text :6440EE0B mov edi, eax
.text :6440EE0D xor ebx, ebx
.text :6440EE0F cmp edi, ebx
.text :6440EE11 jz short loc_6440EE42
.text :6440EE13 push edi
.text :6440EE14 push offset aSapguiStoppedA ; "Sapgui
stopped after commandline interp"...
.text :6440EE19 push dword_644F93E8
33. MSDN
34. Fonction de la bibliothèque C standard renvoyant une variable d’environnement
1153
.text :6440EE1F call FEWTraceError
…ou:
.text :6440237A push eax
.text :6440237B push offset aCclientStart_6 ;
"CClient::Start: set shortcut user to '%"...
.text :64402380 push dword ptr [edi+4]
.text :64402383 call dbg
.text :64402388 add esp, 0Ch
1154
.text :64404F6E lea edi, [esi+2854h]
.text :64404F74 push offset aEnvironmentInf ;
"Environment information:\n"
.text :64404F79 mov ecx, edi
;
demangled name: ATL::CStringT::operator+=(class ATL::CSimpleStringT<char, 1> const &)
.text :64404FA3 call ds :mfc90_941
.text :64404FA9
.text :64404FA9 loc_64404FA9 :
.text :64404FA9 mov eax, [esi+38h]
.text :64404FAC test eax, eax
.text :64404FAE jbe short loc_64404FD3
.text :64404FB0 push eax
.text :64404FB1 lea eax, [ebp+var_14]
.text :64404FB4 push offset aTraceLevelDAct ;
"trace level %d activated\n"
.text :64404FB9 push eax
;
demangled name: ATL::CStringT::operator+=(class ATL::CSimpleStringT<char, 1> const &)
.text :64404FC5 call ds :mfc90_941
.text :64404FCB xor ebx, ebx
.text :64404FCD inc ebx
.text :64404FCE mov [ebp+var_10], ebx
.text :64404FD1 jmp short loc_64404FD6
.text :64404FD3
.text :64404FD3 loc_64404FD3 :
.text :64404FD3 xor ebx, ebx
.text :64404FD5 inc ebx
1155
.text :64404FD6
.text :64404FD6 loc_64404FD6 :
.text :64404FD6 cmp [esi+38h], ebx
.text :64404FD9 jbe short loc_64404FF1
.text :64404FDB cmp dword ptr [esi+2978h], 0
.text :64404FE2 jz short loc_64404FF1
.text :64404FE4 push offset aHexdumpInTrace ;
"hexdump in trace activated\n"
.text :64404FE9 mov ecx, edi
1156
.text :6440503F jnz loc_64405142
.text :64405045 push offset aForMaximumData ;
"\nFor maximum data security delete\nthe s"...
1157
.text :644050DF call ds :DrawTextA
.text :644050E5 push 4 ; nIndex
.text :644050E7 call ds :GetSystemMetrics
.text :644050ED mov ecx, [ebp+rc.bottom]
.text :644050F0 sub ecx, [ebp+rc.top]
.text :644050F3 cmp [ebp+h], 0
.text :644050F7 lea eax, [eax+ecx+28h]
.text :644050FB mov [ebp+cy], eax
.text :644050FE jz short loc_64405108
.text :64405100 push [ebp+h] ; h
.text :64405103 push [ebp+var_10] ; hdc
.text :64405106 call edi ; SelectObject
.text :64405108
.text :64405108 loc_64405108 :
.text :64405108 push [ebp+var_10] ; hDC
.text :6440510B push 0 ; hWnd
.text :6440510D call ds :ReleaseDC
.text :64405113
.text :64405113 loc_64405113 :
.text :64405113 mov eax, [ebp+var_38]
.text :64405116 push 80h ; uFlags
.text :6440511B push [ebp+cy] ; cy
.text :6440511E inc eax
.text :6440511F push ebx ; cx
.text :64405120 push eax ; Y
.text :64405121 mov eax, [ebp+var_34]
.text :64405124 add eax, 0FFFFFED4h
.text :64405129 cdq
.text :6440512A sub eax, edx
.text :6440512C sar eax, 1
.text :6440512E push eax ; X
.text :6440512F push 0 ; hWndInsertAfter
.text :64405131 push dword ptr [esi+285Ch] ; hWnd
.text :64405137 call ds :SetWindowPos
.text :6440513D xor ebx, ebx
.text :6440513F inc ebx
.text :64405140 jmp short loc_6440514D
.text :64405142
.text :64405142 loc_64405142 :
.text :64405142 push offset byte_64443AF8
1158
.text :64405176 add esp, 0Ch
.text :64405179 mov dword_644F858C, 2
.text :64405183 call sub_6441C920
.text :64405188
.text :64405188 loc_64405188 :
.text :64405188 or [ebp+var_4], 0FFFFFFFFh
.text :6440518C lea ecx, [ebp+var_14]
Au début de la fonction, ECX a un pointeur sur l’objet (puisque c’est une fonction avec
le type d’appel thiscall ( 3.21.1 on page 715)). Dans notre cas, l’objet a étonnement
un type de classe de CDwsGui. En fonction de l’option mise dans l’objet, un message
spécifique est concaténé au message résultant.
Si la valeur à l’adresse this+0x3D n’est pas zéro, la compression est désactivée:
.text :64405007 loc_64405007 :
.text :64405007 cmp byte ptr [esi+3Dh], 0
.text :6440500B jz short bypass
.text :6440500D push offset aDataCompressio ;
"data compression switched off\n"
.text :64405012 mov ecx, edi
; ajoute les chaînes "For maximum data security delete" / "the setting(s) as
soon as possible !":
1159
.text :6440505A call ds :SystemParametersInfoA
.text :64405060 mov eax, [ebp+var_34]
.text :64405063 cmp eax, 1600
.text :64405068 jle short loc_64405072
.text :6440506A cdq
.text :6440506B sub eax, edx
.text :6440506D sar eax, 1
.text :6440506F mov [ebp+var_34], eax
.text :64405072
.text :64405072 loc_64405072 :
start drawing :
…remplaçons-le par un JMP, et nous obtenons SAPGUI fonctionnant sans que l’en-
nuyeuse fenêtre pop-up n’apparaisse!
Maintenant approfondissons et trouvons la relation entre l’offset 0x15 dans la fonction
load_command_line() (nous lui avons donné ce nom) et la variable this+0x3D dans
CDwsGui::PrepareInfoWindow. Sommes-nous sûrs que la valeur est la même?
Nous commençons par chercher toutes les occurrences de la valeur 0x15 dans le
code. Pour un petit programme comme SAPGUI, cela fonctionne parfois.a Voici la
première occurrence que nous obtenons:
.text :64404C19 sub_64404C19 proc near
.text :64404C19
.text :64404C19 arg_0 = dword ptr 4
.text :64404C19
.text :64404C19 push ebx
.text :64404C1A push ebp
.text :64404C1B push esi
.text :64404C1C push edi
.text :64404C1D mov edi, [esp+10h+arg_0]
.text :64404C21 mov eax, [edi]
.text :64404C23 mov esi, ecx ; ESI/ECX sont des
pointeurs sur un objet inconnu
.text :64404C25 mov [esi], eax
.text :64404C27 mov eax, [edi+4]
.text :64404C2A mov [esi+4], eax
.text :64404C2D mov eax, [edi+8]
.text :64404C30 mov [esi+8], eax
.text :64404C33 lea eax, [edi+0Ch]
.text :64404C36 push eax
1160
.text :64404C37 lea ecx, [esi+0Ch]
1161
Vérifions nos découvertes.
Remplaçons setz al par les instructions xor eax, eax / nop, effaçons la variable
d’environnement TDW_NOCOMPRESS et lançons SAPGUI. Ouah! La fenêtre ennuyeuse
n’est plus là (comme nous l’attendions, puisque la variable d’environnement n’est
pas mise) mais dans Wireshark nous pouvons voir que les paquets réseau ne sont
plus compressés! Visiblement, c’est le point où le flag de compression doit être défini
dans l’objet CConnectionContext.
Donc, le flag de compression est passé dans le 5ème argument de CConnectionCon-
text::CreateNetwork. À l’intérieur de la fonction, une autre est appelée:
...
.text :64403476 push [ebp+compression]
.text :64403479 push [ebp+arg_C]
.text :6440347C push [ebp+arg_8]
.text :6440347F push [ebp+arg_4]
.text :64403482 push [ebp+arg_0]
.text :64403485 call CNetwork__CNetwork
Le flag de compression est passé ici dans le 5ème argument au constructer CNet-
work::CNetwork.
Et voici comment le constructeur CNetwork défini le flag dans l’objet CNetwork sui-
vant son 5ème argument et une autre variable qui peut probablement aussi affecter
la compression des paquets réseau.
.text :64411DF1 cmp [ebp+compression], esi
.text :64411DF7 jz short set_EAX_to_0
.text :64411DF9 mov al, [ebx+78h] ; une autre valeur
pourrait affecter la compression?
.text :64411DFC cmp al, '3'
.text :64411DFE jz short set_EAX_to_1
.text :64411E00 cmp al, '4'
.text :64411E02 jnz short set_EAX_to_0
.text :64411E04
.text :64411E04 set_EAX_to_1 :
.text :64411E04 xor eax, eax
.text :64411E06 inc eax ; EAX -> 1
.text :64411E07 jmp short loc_64411E0B
.text :64411E09
.text :64411E09 set_EAX_to_0 :
.text :64411E09
.text :64411E09 xor eax, eax ; EAX -> 0
.text :64411E0B
.text :64411E0B loc_64411E0B :
.text :64411E0B mov [ebx+3A4h], eax ; EBX est un
pointeur sur l'object CNetwork
À ce point, nous savons que le flag de compression est stocké dans la classe CNet-
work à l’adresse this+0x3A4.
Plongeons-nous maintenant dans SAPguilib.dll à la recherche de la valeur 0x3A4. Et il
y a une seconde occurrence dans CDwsGui::OnClientMessageWrite (Merci infiniment
pour les informations de débogage) :
1162
.text :64406F76 loc_64406F76 :
.text :64406F76 mov ecx, [ebp+7728h+var_7794]
.text :64406F79 cmp dword ptr [ecx+3A4h], 1
.text :64406F80 jnz compression_flag_is_zero
.text :64406F86 mov byte ptr [ebx+7], 1
.text :64406F8A mov eax, [esi+18h]
.text :64406F8D mov ecx, eax
.text :64406F8F test eax, eax
.text :64406F91 ja short loc_64406FFF
.text :64406F93 mov ecx, [esi+14h]
.text :64406F96 mov eax, [esi+20h]
.text :64406F99
.text :64406F99 loc_64406F99 :
.text :64406F99 push dword ptr [edi+2868h] ; int
.text :64406F9F lea edx, [ebp+7728h+var_77A4]
.text :64406FA2 push edx ; int
.text :64406FA3 push 30000 ; int
.text :64406FA8 lea edx, [ebp+7728h+Dst]
.text :64406FAB push edx ; Dst
.text :64406FAC push ecx ; int
.text :64406FAD push eax ; Src
.text :64406FAE push dword ptr [edi+28C0h] ; int
.text :64406FB4 call sub_644055C5 ; routine de
compression actuelle
.text :64406FB9 add esp, 1Ch
.text :64406FBC cmp eax, 0FFFFFFF6h
.text :64406FBF jz short loc_64407004
.text :64406FC1 cmp eax, 1
.text :64406FC4 jz loc_6440708C
.text :64406FCA cmp eax, 2
.text :64406FCD jz short loc_64407004
.text :64406FCF push eax
.text :64406FD0 push offset aCompressionErr ;
"compression error [rc = %d]- program wi"...
.text :64406FD5 push offset aGui_err_compre ;
"GUI_ERR_COMPRESS"
.text :64406FDA push dword ptr [edi+28D0h]
.text :64406FE0 call SapPcTxtRead
Voilà! Nous avons trouvé la fonction qui effectue la compression des données. Comme
cela a été décrit dans le passé 35 ,
35. http://conus.info/utils/SAP_pkt_decompr.txt
1163
cette fonction est utilisé dans SAP et aussi dans le projet open-source MaxDB. Donc,
elle est disponible sous forme de code source.
La dernière vérification est faite ici:
.text :64406F79 cmp dword ptr [ecx+3A4h], 1
.text :64406F80 jnz compression_flag_is_zero
36. http://www.debuginfo.com/tools/typeinfodump.html
1164
Type : unsigned short*
Flags : d0
STATIC_LOCAL_VAR func
Address : 12274af0 Size : 8 bytes Index : 60495 ⤦
Ç TypeIndex : 60496
Type : wchar_t*
Flags : 80
LOCAL_VAR admhead
Address : Reg335+304 Size : 8 bytes Index : 60498 TypeIndex⤦
Ç : 60499
Type : unsigned char*
Flags : 90
LOCAL_VAR record
Address : Reg335+64 Size : 204 bytes Index : 60501 TypeIndex :⤦
Ç 60502
Type : AD_RECORD
Flags : 90
LOCAL_VAR adlen
Address : Reg335+296 Size : 4 bytes Index : 60508 TypeIndex⤦
Ç : 60509
Type : int
Flags : 90
Wow!
Une autre bonne nouvelle: les appels de debugging (il y en a beaucoup) sont très
utiles.
Ici, vous pouvez remarquer la variable globale ct_level37 , qui reflète le niveau actuel
de trace.
Il y a beaucoup d’ajout de débogage dans le fichier disp+work.exe :
cmp cs :ct_level, 1
jl short loc_1400375DA
1165
call DpLock
lea rcx, aDpxxtool4_c ; "dpxxtool4.c"
mov edx, 4Eh ; line
call CTrcSaveLocation
mov r8, cs :func_48
mov rcx, cs :hdl ; hdl
lea rdx, aSDpreadmemvalu ; "%s: DpReadMemValue (%d)"
mov r9d, ebx
call DpTrcErr
call DpUnlock
Si le niveau courant de trace est plus élevé ou égal à la limite défini dans le code ici,
un message de débogage est écrit dans les fichiers de log comme dev_w0, dev_disp,
et autres fichiers dev*.
Essayons de grepper dans le fichier que nous avons obtenu à l’aide de l’utilitaire
TYPEINFODUMP:
cat "disp+work.pdb.d" | grep FUNCTION | grep -i password
Nous obtenons:
FUNCTION rcui ::AgiPassword ::DiagISelection
FUNCTION ssf_password_encrypt
FUNCTION ssf_password_decrypt
FUNCTION password_logon_disabled
FUNCTION dySignSkipUserPassword
FUNCTION migrate_password_history
FUNCTION password_is_initial
FUNCTION rcui ::AgiPassword ::IsVisible
FUNCTION password_distance_ok
FUNCTION get_password_downwards_compatibility
FUNCTION dySignUnSkipUserPassword
FUNCTION rcui ::AgiPassword ::GetTypeName
FUNCTION `rcui ::AgiPassword ::AgiPassword' ::`1' ::dtor$2
FUNCTION `rcui ::AgiPassword ::AgiPassword' ::`1' ::dtor$0
FUNCTION `rcui ::AgiPassword ::AgiPassword' ::`1' ::dtor$1
FUNCTION usm_set_password
FUNCTION rcui ::AgiPassword ::TraceTo
FUNCTION days_since_last_password_change
FUNCTION rsecgrp_generate_random_password
FUNCTION rcui ::AgiPassword ::`scalar deleting destructor'
FUNCTION password_attempt_limit_exceeded
FUNCTION handle_incorrect_password
FUNCTION `rcui ::AgiPassword ::`scalar deleting destructor'' ::`1' ::dtor$1
FUNCTION calculate_new_password_hash
FUNCTION shift_password_to_history
FUNCTION rcui ::AgiPassword ::GetType
FUNCTION found_password_in_history
FUNCTION `rcui ::AgiPassword ::`scalar deleting destructor'' ::`1' ::dtor$0
FUNCTION rcui ::AgiObj ::IsaPassword
FUNCTION password_idle_check
FUNCTION SlicHwPasswordForDay
FUNCTION rcui ::AgiPassword ::IsaPassword
1166
FUNCTION rcui ::AgiPassword ::AgiPassword
FUNCTION delete_user_password
FUNCTION usm_set_user_password
FUNCTION Password_API
FUNCTION get_password_change_for_SSO
FUNCTION password_in_USR40
FUNCTION rsec_agrp_abap_generate_random_password
Essayons aussi de chercher des messages de debug qui contiennent les mots «pass-
word» et «locked». L’un d’entre eux se trouve dans la chaîne «user was locked by
subsequently failed password logon attempts», référencé dans
la fonction password_attempt_limit_exceeded().
D’autres chaînes que cette fonction peut écrire dans le fichier de log sont: «password
logon attempt will be rejected immediately (preventing dictionary attacks)», «failed-
logon lock: expired (but not removed due to ’read-only’ operation)», «failed-logon
lock: expired => removed».
Après avoir joué un moment avec cette fonction, nous remarquons que le problème
se situe exactement dedans. Elle est appelée depuis la fonction chckpass() —une
des fonctions de vérification u mot de passe.
d’abord, nous voulons être sûrs que nous sommes au bon endroit:
Lançons tracer :
tracer64.exe -a :disp+work.exe bpf=disp+work.exe !chckpass,args :3,unicode
L’enchaînement des appels est: syssigni() -> DyISigni() -> dychkusr() -> usrexist()
-> chckpass().
Le nombre 0x35 est une erreur renvoyée dans chckpass() à cet endroit:
.text :00000001402ED567 loc_1402ED567 : ; CODE XREF:
chckpass+B4
.text :00000001402ED567 mov rcx, rbx ; usr02
.text :00000001402ED56A call password_idle_check
.text :00000001402ED56F cmp eax, 33h
.text :00000001402ED572 jz loc_1402EDB4E
.text :00000001402ED578 cmp eax, 36h
.text :00000001402ED57B jz loc_1402EDB3D
.text :00000001402ED581 xor edx, edx ;
usr02_readonly
.text :00000001402ED583 mov rcx, rbx ; usr02
.text :00000001402ED586 call ⤦
Ç password_attempt_limit_exceeded
.text :00000001402ED58B test al, al
.text :00000001402ED58D jz short loc_1402ED5A0
.text :00000001402ED58F mov eax, 35h
1167
.text :00000001402ED594 add rsp, 60h
.text :00000001402ED598 pop r14
.text :00000001402ED59A pop r12
.text :00000001402ED59C pop rdi
.text :00000001402ED59D pop rsi
.text :00000001402ED59E pop rbx
.text :00000001402ED59F retn
Bien, vérifions:
tracer64.exe -a :disp+work.exe bpf=disp+work.exe !⤦
Ç password_attempt_limit_exceeded,args :4,unicode,rt :0
1168
Étonnement, la fonction sapgparam() est utilisée pour chercher la valeur de certains
paramètres de configuration. Cette fonction peut être appelée depuis 1768 endroits
différents. Il semble qu’avec l’aide de cette information, nous pouvons facilement
trouver les endroits dans le code, où le contrôle du flux est affecté par des configu-
rations spécifiques de paramètres.
C’est vraiment agréable. Le nom des fonctions est très clair, bien plus que dans
Oracle RDBMS. Il semble que le processus disp+work est écrit en C++. A-t-il été
récrit il y a quelques temps?
Et nous obtenons:
BANNER
--------------------------------------------------------
1169
.rodata :0800C4A8 dd 4
.rodata :0800C4AC dd offset _2__STRING_10103_0 ; "NULL"
.rodata :0800C4B0 dd 3
.rodata :0800C4B4 dd 0
.rodata :0800C4B8 dd 195h
.rodata :0800C4BC dd 4
.rodata :0800C4C0 dd 0
.rodata :0800C4C4 dd 0FFFFC1CBh
.rodata :0800C4C8 dd 3
.rodata :0800C4CC dd 0
.rodata :0800C4D0 dd 0Ah
.rodata :0800C4D4 dd offset _2__STRING_10104_0 ; "V$WAITSTAT"
.rodata :0800C4D8 dd 4
.rodata :0800C4DC dd offset _2__STRING_10103_0 ; "NULL"
.rodata :0800C4E0 dd 3
.rodata :0800C4E4 dd 0
.rodata :0800C4E8 dd 4Eh
.rodata :0800C4EC dd 3
.rodata :0800C4F0 dd 0
.rodata :0800C4F4 dd 0FFFFC003h
.rodata :0800C4F8 dd 4
.rodata :0800C4FC dd 0
.rodata :0800C500 dd 5
.rodata :0800C504 dd offset _2__STRING_10105_0 ; "GV$BH"
.rodata :0800C508 dd 4
.rodata :0800C50C dd offset _2__STRING_10103_0 ; "NULL"
.rodata :0800C510 dd 3
.rodata :0800C514 dd 0
.rodata :0800C518 dd 269h
.rodata :0800C51C dd 15h
.rodata :0800C520 dd 0
.rodata :0800C524 dd 0FFFFC1EDh
.rodata :0800C528 dd 8
.rodata :0800C52C dd 0
.rodata :0800C530 dd 4
.rodata :0800C534 dd offset _2__STRING_10106_0 ; "V$BH"
.rodata :0800C538 dd 4
.rodata :0800C53C dd offset _2__STRING_10103_0 ; "NULL"
.rodata :0800C540 dd 3
.rodata :0800C544 dd 0
.rodata :0800C548 dd 0F5h
.rodata :0800C54C dd 14h
.rodata :0800C550 dd 0
.rodata :0800C554 dd 0FFFFC1EEh
.rodata :0800C558 dd 5
.rodata :0800C55C dd 0
À propos, souvent, en analysant les entrailles dOracle RDBMS, vous pouvez vous
demander pourquoi les noms de fonctions et de variables globales sont si étranges.
Sans doute parce qu’Oracle RDBMS est un très vieux produit et a été développé en
C dans les années 80.
1170
Et c’était un temps où le standard C garantissait que les noms de fonction et de va-
riable pouvaient supporter seulement jusqu’à 6 caractères incluant: «6 charactères
significatifs dans un identifiant externe»38
Probablement que la table kqfviw contient la plupart (peut-être même toutes) des
vues préfixées avec V$, qui sont des vues fixées, toujours présentes. Superficielle-
ment, en remarquant la récurrence cyclique des données, nous pouvons facilement
voir que chaque élément de la table kqfviw a 12 champs de 32-bit. C’est très facile
de créer une structure de 12 éléments dans IDA et de l’appliquer à tous les éléments
de la table. Depuis Oracle RDBMS version 11.2, il y a 1023 éléments dans la table,
i.e., dans celle-ci sont décrites 1023 de toutes les vues fixées possible.
Nous reviendrons à ce nombre plus tard.
Comme on le voit, il n’y a pas beaucoup d’information sur les nombres dans les
champs. Le premier nombre est toujours égal au nom de la vue (sans le zéro de fin).
Nous savons aussi que l’information sur toutes ces vues fixes peut être récupérée
depuis une vue fixée appelée V$FIXED_VIEW_DEFINITION (à propos, l’information
pour cette vue est aussi prise dans les tables kqfviw et kqfvip.) Au fait, il y a aussi
1023 éléments dans celle-ci. Coïncidence? Non.
SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='V$VERSION' ;
VIEW_NAME
------------------------------
VIEW_DEFINITION
------------------------------
V$VERSION
select BANNER from GV$VERSION where inst_id = USERENV('Instance')
Donc, V$VERSION est une sorte de vue terminale pour une autre vue appelée GV$VERSION,
qui est, à son tour:
SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='GV$VERSION' ;
VIEW_NAME
------------------------------
VIEW_DEFINITION
------------------------------
GV$VERSION
select inst_id, banner from x$version
Les tables préfixées par X$ dans Oracle RDBMS sont aussi des tables de service,
non documentées, qui ne peuvent pas être modifiées par l’utilisateur et qui sont
rafraîchies dynamiquement.
Si nous cherchons le texte
38. Draft ANSI C Standard (ANSI X3J11/88-090) (May 13, 1988) (yurichev.com)
1171
select BANNER from GV$VERSION where inst_id =
USERENV('Instance')
...
La table semble avoir 4 champs dans chaque élément. À propos, elle a 1023 élé-
ments, encore, le nombre que nous connaissons déjà.
Le second champ pointe sur une autre table qui contient les champs de la table pour
cette vue fixée. Comme pour V$VERSION, cette table a seulement deux éléments, le
premier est 6 et le second est la chaîne BANNER (le nombre 6 est la longueur de la
chaîne) et après, un élément de fin qui contient 0 et une chaîne C null :
Listing 8.12: kqf.o
.rodata :080BBAC4 kqfv133_c_0 dd 6 ; DATA XREF: .rodata:08019574
.rodata :080BBAC8 dd offset _2__STRING_5017_0 ; "BANNER"
.rodata :080BBACC dd 0
.rodata :080BBAD0 dd offset _2__STRING_0_0
En joignant les données des deux tables kqfviw et kqfvip, nous pouvons obtenir la
déclaration SQL qui est exécutée lorsque l’utilisateur souhaite faire une requête sur
une vue fixée spécifique.
Ainsi nous pouvons écrire un programme oracle tables39 , pour collecter toutes ces
informations d’un fichier objet d’Oracle RDBMS pour Linux.
39. yurichev.com
1172
Listing 8.13: Résultat de oracle tables
kqfviw_element.viewname : [V$VERSION] ?: 0x3 0x43 0x1 0xffffc085 0x4
kqfvip_element.statement : [select BANNER from GV$VERSION where inst_id = ⤦
Ç USERENV('Instance')]
kqfvip_element.params :
[BANNER]
Et:
0DBAF574 0 1
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production
...
1173
.rodata :0803CAF0 dd 7
.rodata :0803CAF4 dd offset _2__STRING_13115_0 ; "X$KQFSZ"
.rodata :0803CAF8 dd 5
.rodata :0803CAFC dd offset _2__STRING_13116_0 ; "kqfsz"
.rodata :0803CB00 dd 1
.rodata :0803CB04 dd 38h
.rodata :0803CB08 dd 0
.rodata :0803CB0C dd 7
.rodata :0803CB10 dd 0
.rodata :0803CB14 dd 0FFFFC09Dh
.rodata :0803CB18 dd 2
.rodata :0803CB1C dd 0
Elle contient des informations à propos de tous les champs de la table X$VERSION.
La seule référence à cette table est dans la table kqftap :
Il est intéressant de voir que cet élément ici est 0x1f6th (502nd), tout comme le
pointeur sur la chaîne X$VERSION dans la table kqftab.
Sans doute que les tables kqftap et kqftab sont complémentaires l’une de l’autre,
tout comme kqfvip et kqfviw.
1174
Nous voyons aussi un pointeur sur la fonction kqvrow(). Enfin, nous obtenons quelque
chose d’utile!
Nous ajoutons donc ces tables à notre utilitaire oracle tables40 . Pour X$VERSION nous
obtenons:
Avec l’aide de tracer, il est facile de vérifier que cette fonction est appelée 6 fois
par ligne (depuis la fonction qerfxFetch()) lorsque l’on fait une requête sur la table
X$VERSION.
Lançons tracer en mode cc (il commente chaque instruction exécutée) :
tracer -a :oracle.exe bpf=oracle.exe !_kqvrow,trace :cc
push ebp
mov ebp, esp
sub esp, 7Ch
mov eax, [ebp+arg_14] ; [EBP+1Ch]=1
mov ecx, TlsIndex ; [69AEB08h]=0
mov edx, large fs :2Ch
mov edx, [edx+ecx*4] ; [EDX+ECX*4]=0xc98c938
cmp eax, 2 ; EAX=1
mov eax, [ebp+arg_8] ; [EBP+10h]=0xcdfe554
jz loc_2CE1288
mov ecx, [eax] ; [EAX]=0..5
mov [ebp+var_4], edi ; EDI=0xc98c938
40. yurichev.com
1175
loc_2CE10F6 : ; CODE XREF: _kqvrow_+10A
; _kqvrow_+1A9
cmp ecx, 5 ; ECX=0..5
ja loc_56C11C7
mov edi, [ebp+arg_18] ; [EBP+20h]=0
mov [ebp+var_14], edx ; EDX=0xc98c938
mov [ebp+var_8], ebx ; EBX=0
mov ebx, eax ; EAX=0xcdfe554
mov [ebp+var_C], esi ; ESI=0xcdfe248
1176
push ebx ; EBX=0xb
mov ebx, [ebp+arg_8] ; [EBP+10h]=0xcdfe554
push eax ;
EAX=0x65852100, "Oracle Database 11g Enterprise Edition Release %d.%d.%d.%d.%d %s"
mov eax, [ebp+Dest] ; [EBP-10h]=0xce2ffb0
push eax ; EAX=0xce2ffb0
call ds :__imp__sprintf ; op1=MSVCR80.dll!sprintf tracing
nested maximum level (1) reached, skipping this CALL
add esp, 38h
mov dword ptr [ebx], 1
1177
11.2.0.1.0 - Production"
mov [ebx+4], edx ; EDX=0xce2ffb0, "PL/SQL Release
11.2.0.1.0 - Production"
push edx ; EDX=0xce2ffb0, "PL/SQL Release
11.2.0.1.0 - Production"
call _lmxver ; tracing nested maximum level (1) reached,
skipping this CALL
add esp, 0Ch
mov dword ptr [ebx], 3
jmp short loc_2CE1192
1178
mov eax, ebx
mov ebx, [ebp+var_8]
mov ecx, 5
jmp loc_2CE10F6
Maintenant, il est facile de voir que le nombre est passé de l’extérieur. La fonction
renvoie une chaîne, construite comme ceci:
String 1 Using vsnstr, vsnnum, vsnban global variables.
Calls sprintf().
String 2 Calls kkxvsn().
String 3 Calls lmxver().
String 4 Calls npinli(), nrtnsvrs().
String 5 Calls lxvers().
C’est ainsi que les fonctions correspondantes sont appelées pour déterminer la ver-
sion de chaque module.
1179
Il y a une table fixée appelée X$KSMLRU qui suit les différentes
allocations dans le pool partagé qui force les autres objets du pool
partagé à vieillir. Cette table fixée peut être utilisée pour identifier ce
qui cause une grosse allocation.
Si plusieurs objets sont supprimés périodiquement du pool partagé,
alors ceci va poser des problèmes de temps de réponse et va probable-
ment provoquer des problèmes de contention du verrou de cache de
bibliothèque lorsque les objets seront rechargés dans le pool partagé.
Une chose inhabituelle à propos de la table fixée X$KSMLRU est que
le contenu de la table fixée est écrasé à chaque fois que quelqu’uni ef-
fectue requête dans la table fixée. Ceci est fait puisque la table fixée
ne contient que l’allocation la plus large qui s’est produite. Les valeurs
sont réinitialisées après avoir été sélectionnées, de sorte que les allo-
cations importantes suivantes puissent ête inscrites, même si elles ne
sont pas aussi laarges que celles qui se sont produites précédemment.
À cause de cette réinitialisation, la sortie produite par la sélection de
cette table doit être soigneusement conservée puisqu’elle ne peut plus
être récupérée après que la requête a été faite.
Toutefois, comme on peut le vérifier facilement, le contenu de cette table est effacé à
chaque fois qu’on l’interroge. Pouvons-nous trouver pourquoi? Retournons aux tables
que nous connaissons déjà: kqftab et kqftap qui sont générées avec l’aide d’oracle
tables41 , qui a toutes les informations concernant les table X$-. Nous pouvons voir
ici que la fonction ksmlrs() est appelée pour préparer les éléments de cette table:
41. yurichev.com
1180
En effet, avec l’aide de tracer, il est facile de voir que cette fonction est appelée à
chaque fois que nous interrogeons la table X$KSMLRU.
Ici nous voyons une référence aux fonctions ksmsplu_sp() et ksmsplu_jp(), cha-
cune d’elles appelle ksmsplu() à la fin. À la fin de la fonction ksmsplu() nous voyons
un appel à memset() :
Des constructions comme memset (block, 0, size) sont souvent utilisées pour
mettre à zéro un bloc de mémoire. Que se passe-t-il si nous prenons le risque de
bloquer l’appel à memset (block, 0, size) et regardons ce qui se produit?
Lançons tracer avec les options suivantes: mettre un point d’arrêt en 0x434C7A (le
point où les arguments sont passés à memset()), afin que tracer mette le compteur
de programme EIP au point où les arguments passés à memset() sont éffacés (en
0x434C8A). On peut dire que nous simulons juste un saut inconditionnel de l’adresse
0x434C7A à 0x434C8A.
tracer -a :oracle.exe bpx=oracle.exe !0x00434C7A,set(eip,0x00434C8A)
(Important: toutes ces adresses sont valides seulement pour la version win32 de
Oracle RDBMS 11.2)
En effet, nous pouvons maintenant interroger la table X$KSMLRU autant de fois que
nous voulons et elle n’est plus du tout effacée!
Au cas où, n’essayez pas ceci sur vos serveurs de production.
1181
Ce n’est probablement pas un comportement très utile ou souhaité, mais comme
une expérience pour déterminer l’emplacement d’un bout de code dont nous avons
besoin, ça remplit parfaitement notre besoin!
42
(From Oracle RDBMS documentation )
Il est intéressant que les périodes soient différentes pour Oracle pour win32 et pour
Linux. Allons-nous réussir à trouver la fonction qui génère cette valeur?
On voit que cette valeur est finalement prise de la table X$KSUTM.
SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='V$TIMER' ;
VIEW_NAME
------------------------------
VIEW_DEFINITION
------------------------------
V$TIMER
select HSECS from GV$TIMER where inst_id = USERENV('Instance')
VIEW_NAME
------------------------------
VIEW_DEFINITION
------------------------------
GV$TIMER
select inst_id,ksutmtim from x$ksutm
Nous maintenant bloqué par un petit problème, il n’y a pas de référence à une ou
des fonction(s) générants des valeurs dans les tables kqftab/kqftap :
42. http://docs.oracle.com/cd/B28359_01/server.111/b28320/dynviews_3104.htm
1182
kqftap_element.fn2=NULL
Lorsque nous essayons de trouver la chaîne KSUTMTIM, nous la voyons dans cette
fonction:
kqfd_DRN_ksutm_c proc near ; DATA XREF: .rodata:0805B4E8
push ebp
mov ebp, esp
push [ebp+arg_C]
push offset ksugtm
push offset _2__STRING_1263_0 ; "KSUTMTIM"
push [ebp+arg_8]
push [ebp+arg_0]
call kqfd_cfui_drain
add esp, 14h
mov esp, ebp
pop ebp
retn
kqfd_DRN_ksutm_c endp
Il y a une fonction ksugtm() référencée ici. Voyons ce qu’elle contient dans (Linux
x86) :
push ebp
mov ebp, esp
sub esp, 1Ch
lea eax, [ebp+var_1C]
push eax
call slgcs
pop ecx
mov edx, [ebp+arg_4]
1183
mov [edx], eax
mov eax, 4
mov esp, ebp
pop ebp
retn
ksugtm endp
Essayons encore:
SQL> select * from V$TIMER ;
HSECS
----------
27294929
HSECS
----------
27295006
HSECS
----------
27295167
1184
TID=2428|(0) oracle.exe !_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe !⤦
Ç __VInfreq__qerfxFetch+0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0 : 38 C9 "8. ⤦
Ç "
TID=2428|(0) oracle.exe !_ksugtm () -> 0x4 (0x4)
Argument 2/2 difference
00000000: BF 7D A0 01 ".}.. ⤦
Ç "
En effet—la valeur est la même que celle que nous voyons dans SQL*Plus et elle est
renvoyée dans le second argument.
Regardons ce que slgcs() contient (Linux x86) :
slgcs proc near
push ebp
mov ebp, esp
push esi
mov [ebp+var_4], ebx
mov eax, [ebp+arg_0]
call $+5
pop ebx
nop ; PIC mode
mov ebx, offset _GLOBAL_OFFSET_TABLE_
mov dword ptr [eax], 0
call sltrgatime64 ; PIC mode
push 0
push 0Ah
push edx
push eax
call __udivdi3 ; PIC mode
mov ebx, [ebp+var_4]
add esp, 10h
mov esp, ebp
pop ebp
retn
slgcs endp
1185
mov ebp, esp
mov eax, [ebp+8]
mov dword ptr [eax], 0
call ds :__imp__GetTickCount@0 ; GetTickCount()
mov edx, eax
mov eax, 0CCCCCCCDh
mul edx
shr edx, 3
mov eax, edx
mov esp, ebp
pop ebp
retn
_slgcs endp
Décompilons-le:
43. MSDN
44. yurichev.com
45. Wikipédia
1186
; conditions initiales: SP=0FFFEh, SS:[SP]=0
0100 58 pop ax
; AX=0, SP=0
0101 35 4F 21 xor ax, 214Fh
; AX = 214Fh et SP = 0
0104 50 push ax
; AX = 214Fh, SP = FFFEh et SS:[FFFE] = 214Fh
0105 25 40 41 and ax, 4140h
; AX = 140h, SP = FFFEh et SS:[FFFE] = 214Fh
0108 50 push ax
; AX = 140h, SP = FFFCh, SS:[FFFC] = 140h et SS:[FFFE] = 214Fh
0109 5B pop bx
; AX = 140h, BX = 140h, SP = FFFEh et SS:[FFFE] = 214Fh
010A 34 5C xor al, 5Ch
; AX = 11Ch, BX = 140h, SP = FFFEh et SS:[FFFE] = 214Fh
010C 50 push ax
010D 5A pop dx
; AX = 11Ch, BX = 140h, DX = 11Ch, SP = FFFEh et SS:[FFFE] = 214Fh
010E 58 pop ax
; AX = 214Fh, BX = 140h, DX = 11Ch et SP = 0
010F 35 34 28 xor ax, 2834h
; AX = 97Bh, BX = 140h, DX = 11Ch et SP = 0
0112 50 push ax
0113 5E pop si
; AX = 97Bh, BX = 140h, DX = 11Ch, SI = 97Bh et SP = 0
0114 29 37 sub [bx], si
0116 43 inc bx
0117 43 inc bx
0118 29 37 sub [bx], si
011A 7D 24 jge short near ptr word_10140
011C 45 49 43 ... db 'EICAR-STANDARD-ANTIVIRUS-TEST-FILE !$'
0140 48 2B word_10140 dw 2B48h ; CD 21 (INT 21) sera ici
0142 48 2A dw 2A48h ; CD 20 (INT 20) sera ici
0144 0D db 0Dh
0145 0A db 0Ah
J’ai ajouté des commentaires à propos des registres et de la pile après chaque ins-
truction.
En gros, toutes ces instructions sont là seulement pour exécuter ce code:
B4 09 MOV AH, 9
BA 1C 01 MOV DX, 11Ch
CD 21 INT 21h
CD 20 INT 20h
INT 21h avec la 9ème fonction (passée dans AH) affiche simplement une chaîne,
dont l’adresse est passée dans DS:DX. À propos, la chaîne doit être terminée par le
signe ’$’. Apparemment, c’est hérité de CP/M et cette fonction a été laissée dans
DOS pour la compatibilité. INT 20h renvoie au DOS.
Mais comme on peut le voir, l’opcode de cette instruction n’est pas strictement affi-
chable. Donc, la partie principale du fichier EICAR est:
1187
• prépare les valeurs du registre dont nous avons besoin (AH et DX) ;
• prépare les opcodes INT 21 et INT 20 en mémoire;
• exécute INT 21 et INT 20.
À propos, cette technique est largement utilisée dans la construction de shellcode,
lorsque l’on doit passer le code x86 sous la forme d’une chaîne.
Voici aussi une liste de toutes les instructions x86 qui ont des opcodes affichables: .1.6
on page 1357.
8.15 Démos
Les démos (ou démonstrations?) étaient un excellent moyen de s’éxercer en mathé-
matiques, programmation graphique et code x86 pointu.
1188
Il y a quelques implémentations connues en x86 16-bit.
La valeur pseudo aléatoire ici est en fait le temps qui a passé depuis le démarrage
du système, pris depuis le temps du chip 8253, dont la valeur est incrémentée 18,2
fois par seconde.
En écrivant zéro sur le port 43h, nous envoyons la commande «select counter 0 »,
”counter latch”, ”binary counter” (pas une valeur BCD).
Les interruptions sont ré-autorisées avec l’instruction POPF, qui restaure aussi le flag
IF.
Il n’est pas possible d’utiliser l’instruction IN avec des registres autres que AL, d’où
le mélange.
47. http://trixter.oldskool.org/2012/12/17/maze-generation-in-thirteen-bytes/
1189
Ma tentative de réduire la version de Trixter: 27 octets
Nous pouvons dire que puisque nous utilisons le timer non pas pour avoir une valeur
précise, mais une valeur pseudo aléatoire, nous n’avons pas besoin de passer du
temps (et du code) pour interdire les interruptions.
Une autre chose que l’on peut dire est que nous n’avons besoin que d’un bit de la
partie basse 8-bit, donc lisons-le seulement.
Nous pouvons réduire légèrement le code et obtenons 27 octets:
00000000: B9D007 mov cx,007D0 ; limiter la sortie à 2000 caractères
00000003: 31C0 xor ax,ax ; commande pour le chip timer
00000005: E643 out 043,al
00000007: E440 in al,040 ; lire 8-bit du timer
00000009: D1E8 shr ax,1 ; mettre le second bit dans le flag CF
0000000B : D1E8 shr ax,1
0000000D : B05C mov al,05C ; préparer '\'
0000000F : 7202 jc 000000013
00000011: B02F mov al,02F ; préparer '/'
; output character to screen
00000013: B40E mov ah,00E
00000015: CD10 int 010
00000017: E2EA loop 000000003
; exit to DOS
00000019: CD20 int 020
48. http://trixter.oldskool.org/2012/12/17/maze-generation-in-thirteen-bytes/
49. http://pferrie.host22.com/misc/10print.htm
1190
00000001: D6 setalc
; AL est mis à 0xFF si CF=1 ou à 0 autrement
00000002: 242D and al,02D ;'-'
; AL vaut ici 0x2D ou 0
00000004: 042F add al,02F ;'/'
; AL vaut ici 0x5C ou 0x2F
00000006: CD29 int 029 ; afficher AL sur l'écran
00000008: EBF6 jmps 000000000 ; boucler indéfiniment
Conclusion
Il vaut la peine de mentionner que le résultat peut être différent dans DOSBox, Win-
dows NT et même MS-DOS,
à cause de conditions différentes: le chip timer peut être émulé différemment et le
contenu initial du registre peut être différent aussi.
1191
8.15.2 Ensemble de Mandelbrot
Vous savez que si vous agrandissez la ligne
du littoral, elle ressemble toujours à une
côte, et de nombreuses autres choses ont
cette propriété. La nature a des algorithmes
récursifs qu’elle utilise pour générer les
nuages, le fromage Suisse et d’autres choses
comme ça.
1192
Théorie
Un nombre complexe est un nombre qui est constitué de deux parties—réelle (Re)
et imaginaire (Im).
Le plan complexe est un plan à deux dimensions où chaque nombre complexe peut
être placé: la partie réelle est une coordonnée, et la partie imaginaire est l’autre.
Quelques règles de base que nous devons garder à l’esprit:
• Addition: (a + bi) + (c + di) = (a + c) + (b + d)i
Autrement dit:
Re(sum) = Re(a) + Re(b)
Im(sum) = Im(a) + Im(b)
• Multiplication: (a + bi)(c + di) = (ac − bd) + (bc + ad)i
Autrement dit:
Re(product) = Re(a) ⋅ Re(c) − Re(b) ⋅ Re(d)
Im(product) = Im(b) ⋅ Im(c) + Im(a) ⋅ Im(d)
• Carré: (a + bi)2 = (a + bi)(a + bi) = (a2 − b2 ) + (2ab)i
Autrement dit:
Re(square) = Re(a)2 − Im(a)2
Im(square) = 2 ⋅ Re(a) ⋅ Im(a)
L’ensemble de Mandelbrot est l’ensemble des points pour lesquels la séquence ré-
cursive zn+1 = zn 2 + c (où z et c sont des nombres complexes et c est la valeur de
départ) ne tend pas vers l’infini.
En langage courant:
• Parcourir tous les points de l’écran.
• Vérifier si le point courant est dans l’ensemble de Mandelbrot.
• Voici comment le vérifier:
– Représenter le point comme un nombre complexe.
– Calculer son carré.
– Lui ajouter la valeur initiale du point.
– Vérifier si le résultat dépasse les limites. Si oui, arrêter.
– Déplacer le point à la coordonnées que nous venons de calculer.
1193
– Répéter tout ceci pour un nombre raisonnable d’itérations.
• Le point est-il toujours dans les limites? Alors dessiner le point.
• Le point a-t-il dépassé les limites?
– (pour une image en noir et blanc) ne rien dessiner.
– (pour une image en couleur) transformer le nombre d’itération en une cou-
leur. De façon à ce que la couleur montre la vitesse à laquelle le point est
sorti des limites.
Voici un algorithme en Python pour les représentations en nombres complexes et
entiers:
while True :
if (P>bounds) :
break
P=P^2+P_start
if iterations > max_iterations :
break
iterations++
return iterations
# noir et blanc
for each point on screen P :
if check_if_is_in_set (P) < max_iterations :
draw point
# couleur
for each point on screen P :
iterations = if check_if_is_in_set (P)
map iterations to color
draw color point
La version avec les entiers est celle où les opérations sur les nombres complexes
sont remplacées par des opérations sur des entiers, en suivant les règles qui ont été
expliquées plus haut.
while True :
if (X^2 + Y^2 > bounds) :
break
1194
new_X=X^2 - Y^2 + X_start
new_Y=2*X*Y + Y_start
if iterations > max_iterations :
break
iterations++
return iterations
# noir et blanc
for X = min_X to max_X :
for Y = min_Y to max_Y :
if check_if_is_in_set (X,Y) < max_iterations :
draw point at X, Y
# couleur
for X = min_X to max_X :
for Y = min_Y to max_Y :
iterations = if check_if_is_in_set (X,Y)
map iterations to color
draw color point at X,Y
Voici aussi un source en C# qui se trouve dans l’article51 de Wikipédia, mais nous
allons le modifier afin qu’il affiche le nombre d’itérations au lieu d’un symbole 52 :
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
namespace Mnoj
{
class Program
{
static void Main(string[] args)
{
double realCoord, imagCoord ;
double realTemp, imagTemp, realTemp2, arg ;
int iterations ;
for (imagCoord = 1.2; imagCoord >= -1.2; imagCoord -= 0.05)
{
for (realCoord = -0.6; realCoord <= 1.77; realCoord += ⤦
Ç 0.03)
{
iterations = 0;
realTemp = realCoord ;
imagTemp = imagCoord ;
arg = (realCoord * realCoord) + (imagCoord * imagCoord)⤦
Ç ;
while ((arg < 2*2) && (iterations < 40))
{
51. Wikipédia
52. Voici aussi le fichier exécutable: beginners.re
1195
realTemp2 = (realTemp * realTemp) - (imagTemp * ⤦
Ç imagTemp) - realCoord ;
imagTemp = (2 * realTemp * imagTemp) - imagCoord ;
realTemp = realTemp2 ;
arg = (realTemp * realTemp) + (imagTemp * imagTemp)⤦
Ç ;
iterations += 1;
}
Console.Write("{0,2:D} ", iterations) ;
}
Console.Write("\n") ;
}
Console.ReadKey() ;
}
}
}
Voici le fichier résultant, qui est trop large pour être inclus ici:
beginners.re.
Le nombre maximal d’itérations est 40, donc lorsque vous voyez 40 dans ce dump,
ça signifie que ce point s’est baladé pendant 40 itérations, sans sortir des limites.
Un nombre n inférieur à 40 signifie que le point est resté dans les limites pendant n
itérations, puis en est sorti.
1196
Il y a une démo cool disponible ici http://demonstrations.wolfram.com/MandelbrotSetDoodle/,
qui montre visuellement comment le point se déplace dans le plan à chaque itération
pour un point spécifique. Voici deux copies d’écran.
Premièrement, j’ai cliqué dans la zone jaune et vu que la trajectoire (ligne verte) fini
par tourner autour d’un point à l’intérieur:
Ceci implique que le point que nous avons cliqué appartient à l’ensemble de Man-
delbrot.
1197
Puis j’ai cliqué en dehors de la surface jaune et vu un mouvement du point bien plus
chaotique, qui sort rapidement des limites:
1198
Revenons à la démo
1199
45 xor si,si
46 ; BX (temp_X)=0; SI (temp_Y)=0
47
48 ; prendre le nombre maximal d'itérations
49 ; CX contient toujours 320 ici, donc ceci est aussi le nombre maximal
d'itérations
50 MandelLoop :
51 mov bp,si ; BP = temp_Y
52 imul si,bx ; SI = temp_X*temp_Y
53 add si,si ; SI = SI*2 = (temp_X*temp_Y)*2
54 imul bx,bx ; BX = BX^2 = temp_X^2
55 jo MandelBreak ; overflow?
56 imul bp,bp ; BP = BP^2 = temp_Y^2
57 jo MandelBreak ; overflow?
58 add bx,bp ; BX = BX+BP = temp_X^2 + temp_Y^2
59 jo MandelBreak ; overflow?
60 sub bx,bp ; BX = BX-BP = temp_X^2 + temp_Y^2 - temp_Y^2 = temp_X^2
61 sub bx,bp ; BX = BX-BP = temp_X^2 - temp_Y^2
62
63 ; corrige l'échelle:
64 sar bx,6 ; BX=BX/64
65 add bx,dx ; BX=BX+start_X
66 ; maintenant temp_X = temp_X^2 - temp_Y^2 + start_X
67 sar si,6 ; SI=SI/64
68 add si,ax ; SI=SI+start_Y
69 ; maintenant temp_Y = (temp_X*temp_Y)*2 + start_Y
70
71 loop MandelLoop
72
73 MandelBreak :
74 ; CX=itérations
75 xchg ax,cx
76 ; AX=itérations. stocke AL dans le buffer VGA en ES:[DI]
77 stosb
78 ; stosb incrémente aussi DI, donc DI pointe maintenant sur le point suivant
dans le buffer VGA
79 ; saute toujours, donc c'est une boucle infinie ici
80 jmp FillLoop
Algorithme:
• Change le mode vidéo à VGA 320*200 VGA, 256 couleurs. 320 ∗ 200 = 64000
(0xFA00).
Chaque pixel est encodé par un octet, donc la taille du buffer est de 0xFA00
octets. Il est accédé en utilisant la paire de registres ES:DI.
ES doit être 0xA000 ici, car c’est l’adresse du segment du buffer vidéo VGA,
mais mettre 0xA000 dans ES nécessite au moins 4 octets (PUSH 0A000h / POP
ES). Plus d’information sur le modèle de mémoire 16-bit de MS-DOS ici: 11.6 on
page 1309.
En supposant que BX vaut zéro ici, et que le Program Segment Prefix est à
l’adresse zéro, l’instruction en 2 octets LES AX,[BX] stocke 0x20CD dans AX
et 0x9FFF dans ES.
1200
Donc le programme commence à écrire 16 pixels (ou octets) avant le buffer
vidéo. Mais c’est MS-DOS,
Il n’y a pas de protection de la mémoire, donc l’écriture se produit tout à la fin
de la mémoire conventionnelle, et en général, il n’y a rien d’important. C’est
pourquoi vous voyez une bande rouge de 16 pixels de large sur le côté droit.
L’image complète est décalée à gauche de 16 pixels. C’est le prix pour écono-
miser 2 octets.
• Une boucle infinie traite chaque pixel.
La façon la plus courante de parcourir tous les pixels de l’écran est d’utiliser
deux boucles: une pour la coordonnée X, une autre pour la coordonnée Y. Mais
vous devrez multiplier une coordonnée pour accéder à un octet dans le buffer
vidéo VGA.
L’auteur de cette démo a décidé de faire autrement: énumérer tous les octets
dans le buffer vidéo en utilisant une seule boucle au lieu de deux, et obtenir
les coordonnées du point courant en utilisant une division Les coordonnées
résultantes sont: X dans l’intervalle −256..63 et Y dans l’intervalle −100..99. Vous
pouvez voir sur la copie d’écran que l’image est quelque peu décalée sur la
droite de l’écran.
C’est parce que la surface en forme de cœur apparaît en général aux coor-
données 0,0 et elles sont décalées vers la droite. Est-ce que l’auteur aurait pu
soustraire 160 de la valeur pour avoir X dans l’intervalle −160..159 ? Oui, mais
l’instruction SUB DX, 160 nécessite 4 octets, tandis que DEC DH—2 octets (ce
qui soustrait 0x100 (256) de DX). Donc l’ensemble de l’image est décalée pour
économiser 2 octets de code.
– Vérifier si le point courant est dans l’ensemble de Mandelbrot. L’algorithme
est celui qui a été décrit ici.
– La boucle est organisée en utilisant l’instruction LOOP, qui utilise le registre
CX comme compteur.
L’auteur pourrait mettre le nombre d’itérations à une valeur spécifique,
mais il ne l’a pas fait: 320 est déjà présent dans CX (il a été chargé à la
ligne 35), et de toutes façons une bonne valeur d’itération maximale. Nous
économisons encore un peu d’espace ici en ne rechargeant pas le registre
CX avec une autre valeur.
– IMUL est utilisé ici au lieu de MUL, car nous travaillons avec des valeurs
signées: gardez à l’esprit que les coordonnées 0,0 doivent être proches du
centre de l’écran.
C’est la même chose avec SAR (décalage arithmétique pour des valeurs
signées) : c’est utilisé au lieu de SHR.
– Une autre idée est de simplifier le test des limites. Nous devons tester une
paire de coordonnées, i.e., deux variables. Ce que je fais est de vérifier trois
fois le débordement: deux opérations de mise au carré et une addition.
En effet, nous utilisons des registres 16-bit, qui peuvent contenir des va-
leurs signées dans l’intervalle -32768..32767, donc si l’une des coordon-
1201
nées est plus grande que 32767 lors de la multiplication signée, ce point
est définitivement hors limites: nous sautons au label MandelBreak.
– Il y a aussi une division par 64 (instruction SAR). 64 met à l’échelle.
Il est possible d’augmenter la valeur pour regarder de plus près, ou de la
diminuer pour voir de plus loin.
• Nous sommes au label MandelBreak, il y a deux façons d’arriver ici: la boucle
s’est terminée avec CX=0 (le point est à l’intérieur de l’ensemble de Mandel-
brot) ; ou parce qu’un débordement s’est produit (CX contient toujours une va-
leur non zéro.) Nous écrivons la partie 8-bit basse de CX (CL) dans le buffer
vidéo.
La palette par défaut est grossière, néanmoins, 0 est noir: ainsi nous voyons du
noir aux endroits où les points sont dans l’ensemble de Mandelbrot. La palette
peut être initialisée au début du programme, mais gardez à l’esprit que ceci est
un programme de seulement 64 octets!
• le programme tourne en boucle infinie, car un test supplémentaire, ou une in-
terface utilisateur quelconque nécessiterait des instructions supplémentaires.
D’autres astuces d’optimisation:
• L’instruction en 1-octet CWD est utilisée ici pour effacer DX au lieu de celle en
2-octets XOR DX, DX ou même celle en 3-octets MOV DX, 0.
• L’instruction en 1-octet XCHG AX, CX est utilisée ici au lieu de celle en 2-octets
MOV AX,CX. La valeur courante de AX n’est plus nécessaire ici.
• DI (position dans le buffer vidéo) n’est pas initialisée, et contient 0xFFFE au
début 53 .
C’est OK, car le programme travaille pour tout DI dans l’intervalle 0..0xFFFF
éternellement, et l’utilisateur ne peut pas remarquer qu’il a commencé en de-
hors de l’écran (le dernier pixel d’un buffer vidéo de 320*200 est à l’adresse
0xF9FF). Donc du travail est en fait effectué en dehors des limites de l’écran.
Autrement, il faudrait une instruction supplémentaire pour mettre DI à 0 et
tester la fin du buffer vidéo.
Ma version «corrigée »
1202
9 mov cx, 100h
10 inc dx
11 l00 :
12 mov al, cl
13 shl ax, 2
14 out dx, al ; rouge
15 out dx, al ; vert
16 out dx, al ; bleu
17 loop l00
18
19 push 0a000h
20 pop es
21
22 xor di, di
23
24 FillLoop :
25 cwd
26 mov ax,di
27 mov cx,320
28 div cx
29 sub ax,100
30 sub dx,160
31
32 xor bx,bx
33 xor si,si
34
35 MandelLoop :
36 mov bp,si
37 imul si,bx
38 add si,si
39 imul bx,bx
40 jo MandelBreak
41 imul bp,bp
42 jo MandelBreak
43 add bx,bp
44 jo MandelBreak
45 sub bx,bp
46 sub bx,bp
47
48 sar bx,6
49 add bx,dx
50 sar si,6
51 add si,ax
52
53 loop MandelLoop
54
55 MandelBreak :
56 xchg ax,cx
57 stosb
58 cmp di, 0FA00h
59 jb FillLoop
60
61 ; attendre qu'une touche soit pressée
1203
62 xor ax,ax
63 int 16h
64 ; mettre le mode vidéo texte
65 mov ax, 3
66 int 10h
67 ; sortir
68 int 20h
J’ai essayé de corriger toutes ces bizarreries: maintenant la palette est un dégradé
de gris, le buffer vidéo est à la bonne place (lignes 19..20), l’image est dessinée au
centre de l’écran (ligne 30), le programme se termine et attend qu’une touche soit
pressée (lignes 58..68).
54
Mais maintenant c’est bien plus gros: 105 octets (ou 54 instructions) .
Voir aussi: petit programme C qui affiche l’ensemble de Mandelbrot en ASCII: https:
//people.sc.fsu.edu/~jburkardt/c_src/mandelbrot_ascii/mandelbrot_ascii.
html
https://miyuki.github.io/2017/10/04/gcc-archaeology-1.html.
54. Vous pouvez tester par vous-même: prenez DosBox et NASM et compilez-le avec: nasm file.asm
-fbin -o file.com
1204
8.16 Un méchant bogue dans MSVCRT.DLL
Ceci est un bogue qui m’a coûté plusieurs heures de débogage.
En 2013, j’utilisais MinGW, mon projet C semblait très instable et je voyais le mes-
sage d’erreur “Invalid parameter passed to C runtime function.” dans le débogueur.
Le message d’erreur était aussi visible en utilisant DebugView de Sysinternals. Et
mon projet n’avait pas un tel message d’erreur, ni de chaîne. Donc, j’ai commencé
à chercher dans l’ensemble de Windows et l’ai trouvé dans le fichier MSVCRT.DLL
(inutile de dire que j’utilisais Windows 7).
Donc le voici, le message d’erreur dans le fichier MSVCRT.DLL fourni avec Windows
7:
.text :6FFB69D0 OutputString db 'Invalid parameter passed to C runtime ⤦
Ç function.',0Ah,0
.text :6FFB69D0 ; DATA XREF:
sub_6FFB6930+83
Où est-il référencé?
.text :6FFB6930 sub_6FFB6930 proc near ;
CODE XREF: _wfindfirst64+203FC
.text :6FFB6930 ; sub_6FF62563+319AD
.text :6FFB6930
.text :6FFB6930 var_2D0 = dword ptr -2D0h
.text :6FFB6930 var_244 = word ptr -244h
.text :6FFB6930 var_240 = word ptr -240h
.text :6FFB6930 var_23C = word ptr -23Ch
.text :6FFB6930 var_238 = word ptr -238h
.text :6FFB6930 var_234 = dword ptr -234h
.text :6FFB6930 var_230 = dword ptr -230h
.text :6FFB6930 var_22C = dword ptr -22Ch
.text :6FFB6930 var_228 = dword ptr -228h
.text :6FFB6930 var_224 = dword ptr -224h
.text :6FFB6930 var_220 = dword ptr -220h
.text :6FFB6930 var_21C = dword ptr -21Ch
.text :6FFB6930 var_218 = dword ptr -218h
.text :6FFB6930 var_214 = word ptr -214h
.text :6FFB6930 var_210 = dword ptr -210h
.text :6FFB6930 var_20C = dword ptr -20Ch
.text :6FFB6930 var_208 = word ptr -208h
.text :6FFB6930 var_4 = dword ptr -4
.text :6FFB6930
.text :6FFB6930 mov edi, edi
.text :6FFB6932 push ebp
.text :6FFB6933 mov ebp, esp
.text :6FFB6935 sub esp, 2D0h
.text :6FFB693B mov eax, ___security_cookie
.text :6FFB6940 xor eax, ebp
.text :6FFB6942 mov [ebp+var_4], eax
.text :6FFB6945 mov [ebp+var_220], eax
.text :6FFB694B mov [ebp+var_224], ecx
.text :6FFB6951 mov [ebp+var_228], edx
1205
.text :6FFB6957 mov [ebp+var_22C], ebx
.text :6FFB695D mov [ebp+var_230], esi
.text :6FFB6963 mov [ebp+var_234], edi
.text :6FFB6969 mov [ebp+var_208], ss
.text :6FFB696F mov [ebp+var_214], cs
.text :6FFB6975 mov [ebp+var_238], ds
.text :6FFB697B mov [ebp+var_23C], es
.text :6FFB6981 mov [ebp+var_240], fs
.text :6FFB6987 mov [ebp+var_244], gs
.text :6FFB698D pushf
.text :6FFB698E pop [ebp+var_210]
.text :6FFB6994 mov eax, [ebp+4]
.text :6FFB6997 mov [ebp+var_218], eax
.text :6FFB699D lea eax, [ebp+4]
.text :6FFB69A0 mov [ebp+var_2D0], 10001h
.text :6FFB69AA mov [ebp+var_20C], eax
.text :6FFB69B0 mov eax, [eax-4]
.text :6FFB69B3 push offset OutputString ; "Invalid
parameter passed to C runtime f"...
.text :6FFB69B8 mov [ebp+var_21C], eax
.text :6FFB69BE call ds :OutputDebugStringA
.text :6FFB69C4 mov ecx, [ebp+var_4]
.text :6FFB69C7 xor ecx, ebp
.text :6FFB69C9 call @__security_check_cookie@4 ;
__security_check_cookie(x)
.text :6FFB69CE leave
.text :6FFB69CF retn
.text :6FFB69CF sub_6FFB6930 endp
...
1206
Ç arguments in stack : 0x12ffec, 0x7752365b, 0x401015, 0x7ffdf000, 0x0,⤦
Ç 0x0
(0) msvcrt.dll !0x6ffb6930() -> 0x12f94c
PID=3560|Process 1.exe exited. ExitCode=2147483647 (0x7fffffff)
J’ai trouvé que mon code appelait la fonction stricmp() avec un argument à NULL. En
fait, j’ai créé cet exemple en écrivant ceci:
#include <stdio.h>
#include <string.h>
int main()
{
stricmp ("asd", NULL) ;
};
1207
.text :6FF5DB5E pop esi
.text :6FF5DB5F pop ebp
.text :6FF5DB5F _strcmpi endp ; sp-analysis failed
1208
.text :6FF5DBA6 movsx eax, al
.text :6FF5DBA9 pop ebx
.text :6FF5DBAA pop esi
.text :6FF5DBAB pop edi
.text :6FF5DBAC leave
.text :6FF5DBAD retn
.text :6FF5DBAD sub_6FF5DB60 endp
1209
};
// do comparison
};
Comment se fait-il que cette erreur soit rare? Car les nouvelles versions de MSVC
lient avec le fichier MSVCR120.DLL, etc. (où 120 est le numéro de version).
Jetons un coup d’œil à l’intérieur du nouveau MSVCR120.DLL de Windows 7:
.text :1002A0D4 public _stricmp_l
.text :1002A0D4 _stricmp_l proc near ; CODE XREF:
_stricmp+18
.text :1002A0D4 ; _mbsicmp_l+47
.text :1002A0D4 ; DATA XREF: ...
.text :1002A0D4
.text :1002A0D4 var_10 = dword ptr -10h
.text :1002A0D4 var_8 = dword ptr -8
.text :1002A0D4 var_4 = byte ptr -4
.text :1002A0D4 arg_0 = dword ptr 8
.text :1002A0D4 arg_4 = dword ptr 0Ch
.text :1002A0D4 arg_8 = dword ptr 10h
.text :1002A0D4
.text :1002A0D4 ; FUNCTION CHUNK AT .text:1005AA7B SIZE 0000002A BYTES
.text :1002A0D4
.text :1002A0D4 push ebp
.text :1002A0D5 mov ebp, esp
.text :1002A0D7 sub esp, 10h
.text :1002A0DA lea ecx, [ebp+var_10]
.text :1002A0DD push ebx
.text :1002A0DE push esi
.text :1002A0DF push edi
.text :1002A0E0 push [ebp+arg_8]
.text :1002A0E3 call sub_1000F764
.text :1002A0E8 mov edi, [ebp+arg_0] ; arg==NULL?
.text :1002A0EB test edi, edi
.text :1002A0ED jz loc_1005AA7B
.text :1002A0F3 mov ebx, [ebp+arg_4] ; arg==NULL?
.text :1002A0F6 test ebx, ebx
.text :1002A0F8 jz loc_1005AA7B
.text :1002A0FE mov eax, [ebp+var_10]
.text :1002A101 cmp dword ptr [eax+0A8h], 0
.text :1002A108 jz loc_1005AA95
.text :1002A10E sub edi, ebx
...
1210
...
...
1211
.text :100A469B ; sub_10029704+2A792
.text :100A469B push 17h ; ProcessorFeature
.text :100A469D call IsProcessorFeaturePresent
.text :100A46A2 test eax, eax
.text :100A46A4 jz short loc_100A46AB
.text :100A46A6 push 5
.text :100A46A8 pop ecx
.text :100A46A9 int 29h ; Win8:
RtlFailFast(ecx)
.text :100A46AB ;
---------------------------------------------------------------------------
.text :100A46AB
.text :100A46AB loc_100A46AB : ; CODE XREF:
_invoke_watson+9
.text :100A46AB push esi
.text :100A46AC push 1
.text :100A46AE mov esi, 0C0000417h
.text :100A46B3 push esi
.text :100A46B4 push 2
.text :100A46B6 call sub_100A4519
.text :100A46BB push esi ; uExitCode
.text :100A46BC call __crtTerminateProcess
.text :100A46C1 add esp, 10h
.text :100A46C4 pop esi
.text :100A46C5 retn
.text :100A46C5 _invoke_watson endp
1212
Chapitre 9
Exemples de Reverse
Engineering de format de
fichier propriétaire
Ceci est un chiffrement assez intéressant (ou plutôt une obfuscation), car il possède
deux propriétés importantes: 1) fonction unique pour le chiffrement/déchiffrement,
il suffit de l’appliquer à nouveau; 2) les caractères résultants sont aussi imprimable,
donc la chaîne complète peut être utilisée dans du code source, sans caractères
d’échappement.
La seconde propriété exploite le fait que tous les caractères imprimables sont orga-
nisés en lignes: 0x2x-0x7x, et lorsque vous inversez les deux bits de poids faible, le
caractère est déplacé de 1 ou 3 caractères à droite ou à gauche, mais n’est jamais
déplacé sur une autre ligne (peut-être non imprimable) :
1213
Fig. 9.1: Table ASCII 7-bit dans Emacs
msg="@ABCDEFGHIJKLMNO"
Résultat:
CBA@GFEDKJIHONML
C’est comme si les caractères “@” et “C” avaient été échangés, ainsi que “B” et “a”.
Encore une fois, ceci est un exemple intéressant de l’exploitation des propriétés de
XOR plutôt qu’un chiffrement: le même effet de préservation de l’imprimabilité peut
être obtenu en échangeant chacun des 4 bits de poids faible, avec n’importe quelle
combinaison.
1214
9.1.2 Norton Guide: chiffrement XOR à 1 octet le plus simple
possible
Norton Guide était très populaire à l’époque de MS-DOS, c’était un programme ré-
sident qui fonctionnait comme un manuel de référence hypertexte.
Les bases de données de Norton Guide étaient des fichiers avec l’extension .ng, dont
le contenu avait l’air chiffré:
1215
Puisque l’octet 0x1A revient si souvent, nous pouvons essayer de décrypter le fichier,
en supposant qu’il est chiffré avec le chiffrement XOR le plus simple.
Si nous appliquons un XOR avec la constante 0x1A à chaque octet dans Hiew, nous
voyons des chaînes de texte familières en anglais:
1216
Plus d’informations sur le format de fichier de Norton Guide: http://www.davep.
org/norton-guides/file-format/.
Entropie
Une propriété très importante de tels systèmes de chiffrement est que l’entropie des
blocs chiffrés/déchiffrés est la même.
Voici mon analyse faite dans Wolfram Mathematica 10.
Wolfram Mathematica calcule l’entropie avec une base e (base des logarithmes na-
turels), et l’utilitaire1 UNIX ent utilise une base 2.
1. http://www.fourmilab.ch/random/
1217
Donc, nous avons mis explicitement une base 2 dans la commande Entropy, donc
Mathematica nous donne le même résultat que l’utilitaire ent.
1218
9.1.3 Chiffrement le plus simple possible avec un XOR de 4-
octets
Si un pattern plus long était utilisé, comme un pattern de 4 octets, ça serait facile à
repérer.
Par exemple, voici le début du fichier kernel32.dll (version 32-bit de Windows Server
2008) :
1219
Ici, il est «chiffré » avec une clef de 4-octet:
1220
Voici le début d’un entête PE au format hexadécimal:
1221
Le voici «chiffré » :
Il est facile de repérer que la clef est la séquence de 4 octets suivant: 8C 61 D2 63.
Avec cette information, c’est facile de déchiffrer le fichier entier.
Il est important de garder à l’esprit ces propriétés importantes des fichiers PE: 1)
l’entête PE comporte de nombreuses zones remplies de zéro; 2) toutes les sections
PE sont complétées avec des zéros jusqu’à une limite de page (4096 octets), donc
il y a d’habitude de longues zones à zéro après chaque section.
Quelques autres formats de fichier contiennent de longues zones de zéro.
C’est typique des fichiers utilisés par les scientifiques et les ingénieurs logiciels.
Pour ceux qui veulent inspecter ces fichiers eux-même, ils sont téléchargeables ici:
http://beginners.re/examples/XOR_4byte/.
1222
Exercice
• http://challenges.re/50
1223
Sera-t-il possible de le décrypter sans accéder au programme, en utilisant juste ce
fichier?
Il y a clairement un pattern visible de chaînes répétées. Si un simple chiffrement
avec un masque XOR a été appliqué, une répétition de telles chaînes en est une
signature notable, car, il y avait probablement de longues suites (lacunes3 ) d’octets
à zéro, qui, à tour de rôle, sont présentes dans de nombreux fichiers exécutables,
tout comme dans des fichiers de données binaires.
Ici, je vais afficher le début du fichier en utilisant l’utilitaire UNIX xxd :
...
0000030: 09 61 0d 63 0f 77 14 69 75 62 67 76 01 7e 1d 61 .a.c.w.iubgv.~.a
0000040: 7a 11 0f 72 6e 03 05 7d 7d 63 7e 77 66 1e 7a 02 z..rn..}}c~wf.z.
0000050: 75 50 02 4a 31 71 31 33 5c 27 08 5c 51 74 3e 39 uP.J1q13\'.\Qt>9
0000060: 50 2e 28 72 24 4b 38 21 4c 09 37 38 3b 51 41 2d P.(r$K8 !L.78;QA-
0000070: 1c 3c 37 5d 27 5a 1c 7c 6a 10 14 68 77 08 6d 1a .<7]'Z.|j..hw.m.
0000080: 6a 09 61 0d 63 0f 77 14 69 75 62 67 76 01 7e 1d j.a.c.w.iubgv.~.
0000090: 61 7a 11 0f 72 6e 03 05 7d 7d 63 7e 77 66 1e 7a az..rn..}}c~wf.z
00000a0 : 02 75 50 64 02 74 71 66 76 19 63 08 13 17 74 7d .uPd.tqfv.c...t}
00000b0 : 6b 19 63 6d 72 66 0e 79 73 1f 09 75 71 6f 05 04 k.cmrf.ys..uqo..
00000c0 : 7f 1c 7a 65 08 6e 0e 12 7c 6a 10 14 68 77 08 6d ..ze.n..|j..hw.m
00000d0 : 1a 6a 09 61 0d 63 0f 77 14 69 75 62 67 76 01 7e .j.a.c.w.iubgv.~
00000e0 : 1d 61 7a 11 0f 72 6e 03 05 7d 7d 63 7e 77 66 1e .az..rn..}}c~wf.
00000f0 : 7a 02 75 50 01 4a 3b 71 2d 38 56 34 5b 13 40 3c z.uP.J ;q-8V4[.@<
0000100: 3c 3f 19 26 3b 3b 2a 0e 35 26 4d 42 26 71 26 4b < ?.&;;*.5&MB&q&K
0000110: 04 2b 54 3f 65 40 2b 4f 40 28 39 10 5b 2e 77 45 .+T ?e@+O@(9.[.wE
0000120: 28 54 75 09 61 0d 63 0f 77 14 69 75 62 67 76 01 (Tu.a.c.w.iubgv.
0000130: 7e 1d 61 7a 11 0f 72 6e 03 05 7d 7d 63 7e 77 66 ~.az..rn..}}c~wf
0000140: 1e 7a 02 75 50 02 4a 31 71 15 3e 58 27 47 44 17 .z.uP.J1q.>X'GD.
0000150: 3f 33 24 4e 30 6c 72 66 0e 79 73 1f 09 75 71 6f ?3$N0lrf.ys..uqo
0000160: 05 04 7f 1c 7a 65 08 6e 0e 12 7c 6a 10 14 68 77 ....ze.n..|j..hw
...
1224
blocks = Partition[input, 81];
Et voici la sortie:
{{{80, 103, 2, 116, 113, 102, 118, 25, 99, 8, 19, 23, 116, 125, 107,
25, 99, 109, 114, 102, 14, 121, 115, 31, 9, 117, 113, 111, 5, 4,
127, 28, 122, 101, 8, 110, 14, 18, 124, 106, 16, 20, 104, 119, 8,
109, 26, 106, 9, 97, 13, 99, 15, 119, 20, 105, 117, 98, 103, 118,
1, 126, 29, 97, 122, 17, 15, 114, 110, 3, 5, 125, 125, 99, 126,
119, 102, 30, 122, 2, 117}, 1739},
{{80, 100, 2, 116, 113, 102, 118, 25, 99, 8, 19, 23, 116,
125, 107, 25, 99, 109, 114, 102, 14, 121, 115, 31, 9, 117, 113,
111, 5, 4, 127, 28, 122, 101, 8, 110, 14, 18, 124, 106, 16, 20,
104, 119, 8, 109, 26, 106, 9, 97, 13, 99, 15, 119, 20, 105, 117,
98, 103, 118, 1, 126, 29, 97, 122, 17, 15, 114, 110, 3, 5, 125,
125, 99, 126, 119, 102, 30, 122, 2, 117}, 1422},
{{80, 101, 2, 116, 113, 102, 118, 25, 99, 8, 19, 23, 116,
125, 107, 25, 99, 109, 114, 102, 14, 121, 115, 31, 9, 117, 113,
111, 5, 4, 127, 28, 122, 101, 8, 110, 14, 18, 124, 106, 16, 20,
104, 119, 8, 109, 26, 106, 9, 97, 13, 99, 15, 119, 20, 105, 117,
98, 103, 118, 1, 126, 29, 97, 122, 17, 15, 114, 110, 3, 5, 125,
125, 99, 126, 119, 102, 30, 122, 2, 117}, 1012},
{{80, 120, 2, 116, 113, 102, 118, 25, 99, 8, 19, 23, 116,
125, 107, 25, 99, 109, 114, 102, 14, 121, 115, 31, 9, 117, 113,
111, 5, 4, 127, 28, 122, 101, 8, 110, 14, 18, 124, 106, 16, 20,
104, 119, 8, 109, 26, 106, 9, 97, 13, 99, 15, 119, 20, 105, 117,
98, 103, 118, 1, 126, 29, 97, 122, 17, 15, 114, 110, 3, 5, 125,
125, 99, 126, 119, 102, 30, 122, 2, 117}, 377},
...
{{80, 2, 74, 49, 113, 21, 62, 88, 39, 71, 68, 23, 63, 51, 36, 78, 48,
108, 114, 102, 14, 121, 115, 31, 9, 117, 113, 111, 5, 4, 127, 28,
122, 101, 8, 110, 14, 18, 124, 106, 16, 20, 104, 119, 8, 109, 26,
106, 9, 97, 13, 99, 15, 119, 20, 105, 117, 98, 103, 118, 1, 126,
29, 97, 122, 17, 15, 114, 110, 3, 5, 125, 125, 99, 126, 119, 102,
30, 122, 2, 117}, 1},
{{80, 1, 74, 59, 113, 45, 56, 86, 52, 91, 19, 64, 60, 60, 63,
25, 38, 59, 59, 42, 14, 53, 38, 77, 66, 38, 113, 38, 75, 4, 43, 84,
63, 101, 64, 43, 79, 64, 40, 57, 16, 91, 46, 119, 69, 40, 84, 117,
9, 97, 13, 99, 15, 119, 20, 105, 117, 98, 103, 118, 1, 126, 29,
97, 122, 17, 15, 114, 110, 3, 5, 125, 125, 99, 126, 119, 102, 30,
122, 2, 117}, 1},
{{80, 2, 74, 49, 113, 49, 51, 92, 39, 8, 92, 81, 116, 62, 57,
80, 46, 40, 114, 36, 75, 56, 33, 76, 9, 55, 56, 59, 81, 65, 45, 28,
60, 55, 93, 39, 90, 28, 124, 106, 16, 20, 104, 119, 8, 109, 26,
106, 9, 97, 13, 99, 15, 119, 20, 105, 117, 98, 103, 118, 1, 126,
29, 97, 122, 17, 15, 114, 110, 3, 5, 125, 125, 99, 126, 119, 102,
30, 122, 2, 117}, 1}}
1225
La sortie de Tally est une liste de paires, chaque paire a un bloc de 81 octets et le
nombre de fois qu’il apparaît dans le fichier. Nous voyons que le bloc le plus fréquent
est le premier, il est apparu 1739 fois. Le second apparaît 1422 fois. Puis les autres:
1012 fois, 377 fois, etc. Les blocs de 81 octets qui ne sont apparus qu’une fois sont
à la fin de la sortie.
Essayons de comparer ces blocs. Le premier et le second. Y a-t-il une fonction dans
Mathematica qui compare les listes/tableaux? Certainement qu’il y en a une, mais
dans un but didactique, je vais utiliser l’opération XOR pour la comparaison. En effet:
si les octets dans deux tableaux d’entrée sont identiques, le résultat du XOR est 0.
Si ils sont différents, le résultat sera différent de zéro.
Comparons le premier bloc (qui apparaît 1739 fois) et le second (qui apparaît 1422
fois) :
In[]:= BitXor[stat[[1]][[1]], stat[[2]][[1]]]
Out[]= {0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
In[]:= DecryptBlockASCII[blocks[[1]]]
Out[]= {" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
1226
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "}
In[]:= DecryptBlockASCII[blocks[[2]]]
Out[]= {" ", "e", "H", "E", " ", "W", "E", "E", "D", " ", "O", \
"F", " ", "C", "R", "I", "M", "E", " ", "B", "E", "A", "R", "S", " ", \
"B", "I", "T", "T", "E", "R", " ", "F", "R", "U", "I", "T", " ?", \
" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", \
" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", \
" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", \
" "}
In[]:= DecryptBlockASCII[blocks[[3]]]
Out[]= {" ", " ?", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " \
"}
In[]:= DecryptBlockASCII[blocks[[4]]]
Out[]= {" ", "f", "H", "O", " ", "K", "N", "O", "W", "S", " ", \
"W", "H", "A", "T", " ", "E", "V", "I", "L", " ", "L", "U", "R", "K", \
"S", " ", "I", "N", " ", "T", "H", "E", " ", "H", "E", "A", "R", "T", \
"S", " ", "O", "F", " ", "M", "E", "N", " ?", " ", " ", " ", " ", \
" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", \
" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", \
" "}
BinaryWrite["/home/dennis/.../tmp", Flatten[decrypted]]
Close["/home/dennis/.../tmp"]
1227
Fig. 9.9: Fichier déchiffré dans Midnight Commander, 1er essai
Ceci ressemble a des sortes de phrases en anglais d’un jeu, mais quelque chose ne
va pas. Tout d’abord, la casse est inversée: les phrases et certains mots commence
avec une minuscule, tandis que d’autres caractères sont en majuscule. De plus, cer-
taines phrases commencent avec une mauvaise lettre. Regardez la toute première
phrase: « eHE WEED OF CRIME BEARS BITTER FRUIT ». Que signifie « eHE » ? Ne
devrait-on pas avoir «tHE » ici? Est-il possible que notre clef de déchiffrement ait un
mauvais octet à cet endroit?
Regardons à nouveau le second bloc dans le fichier, la clef et le résultat décrypté:
In[]:= blocks[[2]]
Out[]= {80, 2, 74, 49, 113, 49, 51, 92, 39, 8, 92, 81, 116, 62, \
57, 80, 46, 40, 114, 36, 75, 56, 33, 76, 9, 55, 56, 59, 81, 65, 45, \
28, 60, 55, 93, 39, 90, 28, 124, 106, 16, 20, 104, 119, 8, 109, 26, \
106, 9, 97, 13, 99, 15, 119, 20, 105, 117, 98, 103, 118, 1, 126, 29, \
97, 122, 17, 15, 114, 110, 3, 5, 125, 125, 99, 126, 119, 102, 30, \
122, 2, 117}
In[]:= key
Out[]= {80, 103, 2, 116, 113, 102, 118, 25, 99, 8, 19, 23, 116, \
125, 107, 25, 99, 109, 114, 102, 14, 121, 115, 31, 9, 117, 113, 111, \
5, 4, 127, 28, 122, 101, 8, 110, 14, 18, 124, 106, 16, 20, 104, 119, \
1228
8, 109, 26, 106, 9, 97, 13, 99, 15, 119, 20, 105, 117, 98, 103, 118, \
1, 126, 29, 97, 122, 17, 15, 114, 110, 3, 5, 125, 125, 99, 126, 119, \
102, 30, 122, 2, 117}
L’octet chiffré est 2, l’octet de la clef est 103, 2 ⊕ 103 = 101 et 101 est le code ASCII
du caractère «e ». A quoi devrait être égal l’octet de la clef, afin que le code ASCII
résultant soit 116 (pour le caractère «t ») ? 2 ⊕ 116 = 118, mettons 118 comme second
octet de la clef …
key = {80, 118, 2, 116, 113, 102, 118, 25, 99, 8, 19, 23, 116, 125,
107, 25, 99, 109, 114, 102, 14, 121, 115, 31, 9, 117, 113, 111, 5,
4, 127, 28, 122, 101, 8, 110, 14, 18, 124, 106, 16, 20, 104, 119, 8,
109, 26, 106, 9, 97, 13, 99, 15, 119, 20, 105, 117, 98, 103, 118,
1, 126, 29, 97, 122, 17, 15, 114, 110, 3, 5, 125, 125, 99, 126, 119,
102, 30, 122, 2, 117}
Ouah, maintenant, la grammaire est correcte, toutes les phrases commencent avec
1229
une lettre correcte. Mais encore, l’inversion de la casse est suspecte. Pourquoi est-
ce que le développeur les aurait écrites de cette façon? Peut-être que notre clef est
toujours incorrecte?
En regardant la table ASCII, nous pouvons remarquer que les codes des lettres ma-
juscules et des minuscules ne diffèrent que d’un bit (6ème bit en partant de 1,
0b100000) :
Cet octet avec seul le 6ème bit mis est 32 au format décimal. Mais 32 est le code
ASCII de l’espace!
En effet, on peut changer la casse juste en XOR-ant le code ASCII d’un caractère
avec 32 (plus à ce sujet: 3.19.3 on page 706).
Est-ce possible que les parties vides dans le fichier ne soient pas des octets à zéro,
mais plutôt des espaces? Modifions notre clef XOR encore une fois (je vais appliquer
Un XOR avec 32 à chaque octet de la clef) :
(* "32" is scalar and "key" is vector, but that's OK *)
1230
Fig. 9.12: Fichier déchiffré dans Midnight Commander, essai final
1231
9.1.5 Chiffrement simple utilisant un masque XOR, cas II
J’ai un autre fichier chiffré, qui est clairement chiffré avec quelque chose de simple,
comme un XOR:
1232
J’ai essayé de repérer des blocs de 17 octets se répétant avec Mathematica, comme
je l’ai fait dans l’exemple précédant ( 9.1.4 on page 1223) :
Out[]:={{{248,128,88,63,58,175,159,154,232,226,161,50,97,127,3,217,80},1},
{{226,207,67,60,42,226,219,150,246,163,166,56,97,101,18,144,82},1},
{{228,128,79,49,59,250,137,154,165,236,169,118,53,122,31,217,65},1},
{{252,217,1,39,39,238,143,223,241,235,170,91,75,119,2,152,82},1},
{{244,204,88,112,59,234,151,147,165,238,170,118,49,126,27,144,95},1},
{{241,196,78,112,54,224,142,223,242,236,186,58,37,50,17,144,95},1},
{{176,201,71,112,56,230,143,151,234,246,187,118,44,125,8,156,17},1},
...
{{255,206,82,112,56,231,158,145,165,235,170,118,54,115,9,217,68},1},
{{249,206,71,34,42,254,142,154,235,247,239,57,34,113,27,138,88},1},
{{157,170,84,32,32,225,219,139,237,236,188,51,97,124,21,141,17},1},
{{248,197,1,61,32,253,149,150,235,228,188,122,97,97,27,143,84},1},
{{252,217,1,38,42,253,130,223,233,226,187,51,97,123,20,217,69},1},
{{245,211,13,112,56,231,148,223,242,226,188,118,52,97,15,152,93},1},
{{221,210,15,112,28,231,158,141,233,236,172,61,97,90,21,149,92},1}}
Pas de chance, chaque bloc de 17 octets est unique dans le fichier, et n’apparaît donc
qu’une fois. Peut-être n’y a-t-il pas de zone de 17 octets à zéro, ou de zone contenant
seulement des espaces. C’est possible toutefois: de telles séries d’espace peuvent
être absentes dans des textes composés rigoureusement.
La première idée est d’essayer toutes les clefs de 17 octets possible et trouver celles
qui donnent un résultat lisible après déchiffrement. La force brute n’est pas une
option, car il y a 25617 clefs possible (~1040 ), c’est beaucoup trop. Mais il y a une bonne
nouvelle: qui a dit que nous devons tester la clef de 17 octets en entier, pourquoi ne
pas teste chaque octet séparémment? C’est possible en effet.
Maintenant, l’algorithme est:
• essayer chacun des 25 octets pour le premier octet de la clef;
• déchiffrer le 1er octet de chaque bloc de 17 octets du fichier;
• est-ce que tous les octets déchiffrés sont imprimable? garder un œil dessus;
• faire de même pour chacun des 17 octets de la clef.
J’ai écrit le script Python suivant pour essayer cette idée:
content=read_file(sys.argv[1])
# split input by 17-byte chunks:
1233
all_chunks=chunks(content, KEY_LEN)
for c in all_chunks :
for i in range(KEY_LEN) :
each_Nth_byte[i]=each_Nth_byte[i] + c[i]
1234
N= 16
[48, 49] len= 2
Il est possible de les vérifier toutes, mais alors nous devons vérifier visuellement si
le texte déchiffré à l’air d’un texte en anglais.
Prenons en compte le fait que nous avons à faire avec 1) un langage naturel 2)
de l’anglais. Les langages naturels ont quelques caractéristiques statistiques impor-
tantes. Tout d’abord, le ponctuation et la longueur des mots. Quelle est la longueur
moyenne des mots en anglais? Comptons les espaces dans quelques textes bien
connus en anglais avec Mathematica.
Voici le fichier texte de «The Complete Works of William Shakespeare » provenant
de la bibliothèque Gutenberg.
In[]:= Tally[input]
Out[]= {{239, 1}, {187, 1}, {191, 1}, {84, 39878}, {104,
218875}, {101, 406157}, {32, 1285884}, {80, 12038}, {114,
209907}, {111, 282560}, {106, 2788}, {99, 67194}, {116,
291243}, {71, 11261}, {117, 115225}, {110, 216805}, {98,
46768}, {103, 57328}, {69, 42703}, {66, 15450}, {107, 29345}, {102,
69103}, {67, 21526}, {109, 95890}, {112, 46849}, {108, 146532}, {87,
16508}, {115, 215605}, {105, 199130}, {97, 245509}, {83,
34082}, {44, 83315}, {121, 85549}, {13, 124787}, {10, 124787}, {119,
73155}, {100, 134216}, {118, 34077}, {46, 78216}, {89, 9128}, {45,
8150}, {76, 23919}, {42, 73}, {79, 33268}, {82, 29040}, {73,
55893}, {72, 18486}, {68, 15726}, {58, 1843}, {65, 44560}, {49,
982}, {50, 373}, {48, 325}, {91, 2076}, {35, 3}, {93, 2068}, {74,
2071}, {57, 966}, {52, 107}, {70, 11770}, {85, 14169}, {78,
27393}, {75, 6206}, {77, 15887}, {120, 4681}, {33, 8840}, {60,
468}, {86, 3587}, {51, 343}, {88, 608}, {40, 643}, {41, 644}, {62,
440}, {39, 31077}, {34, 488}, {59, 17199}, {126, 1}, {95, 71}, {113,
2414}, {81, 1179}, {63, 10476}, {47, 48}, {55, 45}, {54, 73}, {64,
3}, {53, 94}, {56, 47}, {122, 1098}, {90, 532}, {124, 33}, {38,
21}, {96, 1}, {125, 2}, {37, 1}, {36, 2}}
In[]:= Length[input]/1285884 // N
Out[]= 4.34712
1235
Maintenant voici Alice’s Adventures in Wonderland, par Lewis Carroll de la même
bibliothèque:
In[]:= Tally[input]
Out[]= {{239, 1}, {187, 1}, {191, 1}, {80, 172}, {114, 6398}, {111,
9243}, {106, 222}, {101, 15082}, {99, 2815}, {116, 11629}, {32,
27964}, {71, 193}, {117, 3867}, {110, 7869}, {98, 1621}, {103,
2750}, {39, 2885}, {115, 6980}, {65, 721}, {108, 5053}, {105,
7802}, {100, 5227}, {118, 911}, {87, 256}, {97, 9081}, {44,
2566}, {121, 2442}, {76, 158}, {119, 2696}, {67, 185}, {13,
3735}, {10, 3735}, {84, 571}, {104, 7580}, {66, 125}, {107,
1202}, {102, 2248}, {109, 2245}, {46, 1206}, {89, 142}, {112,
1796}, {45, 744}, {58, 255}, {68, 242}, {74, 13}, {50, 12}, {53,
13}, {48, 22}, {56, 10}, {91, 4}, {69, 313}, {35, 1}, {49, 68}, {93,
4}, {82, 212}, {77, 222}, {57, 11}, {52, 10}, {42, 88}, {83,
288}, {79, 234}, {70, 134}, {72, 309}, {73, 831}, {85, 111}, {78,
182}, {75, 88}, {86, 52}, {51, 13}, {63, 202}, {40, 76}, {41,
76}, {59, 194}, {33, 451}, {113, 135}, {120, 170}, {90, 1}, {122,
79}, {34, 135}, {95, 4}, {81, 85}, {88, 6}, {47, 24}, {55, 6}, {54,
7}, {37, 1}, {64, 2}, {36, 2}}
In[]:= Length[input]/27964 // N
Out[]= 5.99049
Le résultat est différent, sans soute à cause d’un formatage des textes différents
(indentation ou remplissage).
Ok, donc supposons que la fréquence moyenne de l’espace en anglais est de 1 es-
pace tous les 4..7 caractères.
Maintenant, encore une bonne nouvelle: nous pouvons mesurer la fréquence des
espaces au fur et à mesure du déchiffrement de notre fichier. Maintenant je compte
les espaces dans chaque slice et jette les clefs de 1 octets qui produise un résultat
avec un nombre d’espaces trop petit (ou trop grand, mais c’est presque impossible
avec une si petite clef) :
content=read_file(sys.argv[1])
# split input by 17-byte chunks:
all_chunks=chunks(content, KEY_LEN)
for c in all_chunks :
for i in range(KEY_LEN) :
each_Nth_byte[i]=each_Nth_byte[i] + c[i]
1236
for i in range(256) :
tmp_key=chr(i)*len(each_Nth_byte[N])
tmp=xor_strings(tmp_key,each_Nth_byte[N])
# are all characters in tmp[] are printable?
if is_string_printable(tmp)==False :
continue
# count spaces in decrypted buffer:
spaces=tmp.count(' ')
if spaces==0:
continue
spaces_ratio=len(tmp)/spaces
if spaces_ratio<4:
continue
if spaces_ratio>7:
continue
possible_keys.append(i)
print possible_keys, "len=", len(possible_keys)
1237
N= 16
[49] len= 1
In[]:= key = {144, 160, 33, 80, 79, 143, 251, 255, 133, 131, 207, 86, 65, ⤦
Ç 18, 122, 249, 49} ;
In[]:= Close["/home/dennis/tmp/plain2.txt"]
Holmes was sitting with his back to me, and I had given him no sign of
my occupation.
...
1238
• Analyse des fréquences.
• Il y a aussi une bonne technique pour détecter le langage d’un texte: les tri-
grammes. Chaque langage possède des triplets de lettres fréquences, qui peuvent
être « the » et « tha » en anglais. En lire plus à ce sujet: N-Gram-Based Text
Categorization, http://code.activestate.com/recipes/326576/. Fait suffi-
samment intéressant, la détection des trigrammes peut être utilisée lorsque
vous décryptez un texte chiffré progressivement, comme dans cet exemple
(yous devez juste tester les 3 caractères décryptez adjacents).
Pour les systèmes non-latin encodés en UTF-8, les choses peuvent être plus
simples. Par exemple, les textes en russe encodés en UTF-8 ont chaque octet
intercalé avec des octets 0xD0/0xD1. C’est parce que les caractères cyrilliques
sont situés dans le 4ème bloc de la table Unicode. D’autres systèmes d’écriture
on leurs propres blocs.
9.1.6 Devoir
Un ancien jeu d’aventure en texte pour MS-DOS, développé à la fin des années 1980.
Pour cacher les informations du jeu aux joueurs, les fichiers de données sont, le plus
probablement, XORé avec quelque chose: https://beginners.re/homework/XOR_
crypto_1/destiny.zip. Essayez d’y rentrer…
Par soucis de simplification, je dirais que l’entropie est une mesure, d’à quel point des
données peuvent être compressées. Par exemple, il est généralement impossible de
compresser un fichier archive déjà compressé, donc il a une entropie importante.
D’un autre coté, 1MiB d’octet à zéro peut être compressé en un tout petit fichier.
En effet, en français, un million de zéro peut être simplement décrit par “le fichier
résultant est un million d’octets à zéro”. Les fichiers compressés sont en général une
liste d’instructions destinées au dé-compresseur, comme ceci: “mettre 1000 zéros,
puis l’octet 0x23, puis l’octet 0x45, puis un bloc d’une taille de 10 octets que nous
avons vu 500 octets avant, etc.”
Les textes écrits en langage naturel peuvent aussi être fortement compressés, car le
langage naturel a beaucoup de redondance (autrement, une petite typo conduirait
toujours à une incompréhension, comme un bit inversé dans un fichier archive rend
la décompression presque impossible), certains mots sont utilisés très souvent, etc.
Dans le discours courant, il est possible de supprimer jusqu’à la moitié des mots et
il est toujours compréhensible.
1239
Le code pour les CPUs peut aussi être compressé, car certaines instructions ISA sont
utilisées plus souvent que d’autres. En x86, les instructions les plus utilisées sont
MOV/PUSH/CALL ( 5.11.2 on page 960).
La compression de données et le chiffrement tendent à produire des résultats avec
une très haute entropie. Un bon PRNG produit aussi des données qui ne peuvent pas
être compressées (il est possible de mesurer leur qualité par ce moyen).
Donc, autrement dit, la mesure de l’entropie peut aider à tester le contenu de bloc
de données inconnues.
(* slice blocks by 4k *)
blocks=Partition[input,BlockSize];
(* calculate entropy for each block. 2 in Entropy[] (base) is set with the ⤦
Ç intention so Entropy[]
function will produce the same results as Linux ent utility does *)
entropies=Map[N[Entropy[2,#]]&,blocks];
(* helper functions *)
fBlockToShow[input_,offset_]:=Take[input,{1+offset,1+offset+BlockSizeToShow⤦
Ç }]
fToASCII[val_]:=FromCharacterCode[val,"PrintableASCII"]
fToHex[val_]:=IntegerString[val,16]
fPutASCIIWindow[data_]:=Framed[Grid[Partition[Map[fToASCII,data],16]]]
fPutHexWindow[data_]:=Framed[Grid[Partition[Map[fToHex,data],16],Alignment⤦
Ç ->Right]]
(* main UI part *)
Dynamic[{ListLinePlot[entropies,GridLines->{{-1,offset/BlockSize,1}},⤦
Ç Filling->Axis,AxesLabel->{"offset","entropy"}],
CurrentBlock=fBlockToShow[input,offset];
1240
fPutHexWindow[CurrentBlock],
fPutASCIIWindow[CurrentBlock]}]
Commençons avec le fichier GeoIP (qui assigne un FAI au bloc d’adresses IP). Ce fi-
chier binaire GeoIPISP.dat a plusieurs tables (qui sont peut-être les intervalles d’adresses
IP) plus quelques blobs de texte à la fin du fichier (contenant les noms des FAI).
Lorsque je le charge dans Mathematica, je vois ceci:
1241
Il y a deux parties dans le graphe: la première est un peu chaotique, la seconde est
plus régulière.
0 sur l’axe horizontal du graphe signifie l’entropie la plus basse (les données qui
peuvent être compressée très fortement, ordonnées en d’autres mots) et 8 est la
plus haute (ne peuvent pas être compressées du tout, chaotique ou aléatoires en
d’autres mots). Pourquoi 0 et par 8? 0 signifie 0 bits par octet (l’octet en tant que
conteneur n’est pas rempli du tout) et 8 signifie 8 bits par octet, i.e., l’octet comme
conteneur est complètement rempli d’information.
Donc, je mets le curseur pour pointer sur le milieu du premier bloc, et je vois clai-
rement des tableaux d’entiers 32-bit. Maintenant je mets le curseur au milieu du
second bloc et je vois un texte en anglais:
1242
En effet, ceci sont les noms des FAIs. Donc, l’entropie de textes en anglais est 4.5-
5 bits par octet? Oui, quelque chose comme ça. Wolfram Mathematica comprend
quelques corpus de littérature anglaise bien connu, et nous pouvons voir l’entropie
de sonnets de Shakespeare:
In[]:= Entropy[2,ExampleData[{"Text","ShakespearesSonnets"}]]//N
Out[]= 4.42366
4,4 est proche de ce que nous obtenons ()4.7-5.3). Bien sûr, les textes de la litté-
rature anglaise classique sont quelques peu différents des noms des FAIs et autres
texte en anglais que nous pouvons trouver dans des fichiers binaires (débogage/tra-
ce/messages d’erreur), mais cette valeur est proche.
Nous voyons ici 3 blocs avec des vides. Puis le premier bloc avec une haute entropie
1243
(démarrant à l’adresse 0) est petit, le second (adresse quelque part en 0x22000) et
plus grand et le troisième (adresse 0x123000) est le plus grand. Je ne peux pas être
certain de l’entropie du premier bloc, mais le 2-ème et le 3-ème ont une entropie
très haute, signifiant que ces blocs sont soit compressés et/ou chiffrés.
J’ai essayé binwalk pour ce fichier de firmware:
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------⤦
Ç
0 0x0 TP-Link firmware header, firmware version : ⤦
Ç 0.-15221.3, image version : "", product ID : 0x0, product version : ⤦
Ç 155254789, kernel load address : 0x0, kernel entry point : 0x-7⤦
Ç FFFE000, kernel offset : 4063744, kernel length : 512, rootfs offset⤦
Ç : 837431, rootfs length : 1048576, bootloader offset : 2883584, ⤦
Ç bootloader length : 0
14832 0x39F0 U-Boot version string, "U-Boot 1.1.4 (Jun 27 ⤦
Ç 2014 - 14:56:49)"
14880 0x3A20 CRC32 polynomial table, big endian
16176 0x3F30 uImage header, header size : 64 bytes, header⤦
Ç CRC : 0x3AC66E95, created : 2014-06-27 06:56:50, image size : 34587 ⤦
Ç bytes, Data Address : 0x80010000, Entry Point : 0x80010000, data CRC⤦
Ç : 0xDF2DBA0B, OS : Linux, CPU : MIPS, image type : Firmware Image, ⤦
Ç compression type : lzma, image name : "u-boot image"
16240 0x3F70 LZMA compressed data, properties : 0x5D, ⤦
Ç dictionary size : 33554432 bytes, uncompressed size : 90000 bytes
131584 0x20200 TP-Link firmware header, firmware version : ⤦
Ç 0.0.3, image version : "", product ID : 0x0, product version : ⤦
Ç 155254789, kernel load address : 0x0, kernel entry point : 0x-7⤦
Ç FFFE000, kernel offset : 3932160, kernel length : 512, rootfs offset⤦
Ç : 837431, rootfs length : 1048576, bootloader offset : 2883584, ⤦
Ç bootloader length : 0
132096 0x20400 LZMA compressed data, properties : 0x5D, ⤦
Ç dictionary size : 33554432 bytes, uncompressed size : 2388212 bytes
1180160 0x120200 Squashfs filesystem, little endian, version ⤦
Ç 4.0, compression :lzma, size : 2548511 bytes, 536 inodes, blocksize :⤦
Ç 131072 bytes, created : 2014-06-27 07:06:52
En effet: il y a des choses au début, mais deux larges blocs compressés LZMA com-
mencent en 0x20400 et 0x120200. Ce sont en gros les adresses que nous avons vu
dans Mathematica. Oh, à propos, binwalk peut aussi afficher l’entropie (option -E) :
DECIMAL HEXADECIMAL ENTROPY
--------------------------------------------------------------------------------⤦
Ç
0 0x0 Falling entropy edge (0.419187)
16384 0x4000 Rising entropy edge (0.988639)
51200 0xC800 Falling entropy edge (0.000000)
133120 0x20800 Rising entropy edge (0.987596)
968704 0xEC800 Falling entropy edge (0.508720)
1181696 0x120800 Rising entropy edge (0.989615)
3727360 0x38E000 Falling entropy edge (0.732390)
1244
Les fronts ascendants correspondent à des fronts ascendants de blocs sur notre
graphe. Les fronts descendants sont des points où des espaces vides commencent.
Binwalk peut aussi générer un graphe PNG (-E -J) :
Que pouvons-nous dire à propos de ces espaces vides? En regardant dans un édi-
teur hexadécimal, nous voyons qu’ils sont simplement remplis avec des octets à
0xFF. Pourquoi les développeurs les ont-ils mises? Peut-être parce qu’ils n’ont pas
pu calculer précisément la taille des blocs compressés, et leurs ont donc alloué de
l’espace avec une marge.
Notepad
Un autre exemple est notepad.exe que j’ai pris dans Windows 8.1:
1245
Il y a un creux à ≈ 0x19000 (offset absolu dans le fichier). J’ai ouvert le fichier exé-
cutable dans un éditeur hexadécimal et trouvé des tables d’imports (qui ont une
entropie plus basse que le code x86-64 dans la première moitié du graphe).
Il y a aussi un bloc avec une grande entropie qui démarre ≈ 0x20000 :
1246
Dans un éditeur hexadécimal je peux y voir un fichier PNG, inséré dans la section
ressource du fichier PE (c’est une grosse image de l’icône de notepad). Les fichiers
PNG sont compressés, en effet.
Maintenant l’exemple le plus avancé dans cette partie est le firmware d’une dashcam
sans marque que j’ai reçu d’un ami:
1247
La creux au tout début est un texte en anglais: messages de débogage. J’ai vérifié
différents ISAs et j’ai trouvé que le premier tiers du fichier complet (avec le segment
de texte dedans) est en fait du code MIPS (petit-boutiste).
Par exemple, ceci est une fonction épilogue MIPS très typique:
ROM :000013B0 move $sp, $fp
ROM :000013B4 lw $ra, 0x1C($sp)
ROM :000013B8 lw $fp, 0x18($sp)
ROM :000013BC lw $s1, 0x14($sp)
1248
ROM :000013C0 lw $s0, 0x10($sp)
ROM :000013C4 jr $ra
ROM :000013C8 addiu $sp, 0x20
D’après notre graphe nous pouvons voir que le code MIPs a une entropie de 5-6 bits
par octet. En effet, j’ai mesuré une fois l’entropie de différents ISAs et j’ai obtenu
ces valeurs:
• x86: section .text du fichier ntoskrnl.exe de Windows 2003: 6.6
• x64: section .text du fichier ntoskrnl.exe de Windows 7 x64: 6.5
• ARM (mode thumb), Angry Birds Classic: 7.05
• ARM (mode ARM) Linux Kernel 3.8.0: 6.03
• MIPS (little endian), section .text du fichier user32.dll de Windows NT 4: 6.09
Donc l’entropie du code exécutable est plus grande que du texte en anglais, mais
peut encore être compressé.
Maintenant le second tiers qui commence en 0xF5000. Je ne sais pas ce que c’est.
J’ai essayé différents ISAs mais sans succès. L’entropie de ce bloc semble encore
plus régulière que celui de l’exécutable. Peut-être des sortes de données?
Il y a aussi un pic en ≈ 0x213000. Je l’ai vérifié dans un éditeur hexadécimal et j’y ai
trouvé un fichier JPEG (qui est, bien sûr, compressé) ! Je ne sais pas ce qu’il y a à la
fin. Essayons Binwalk pour ce fichier:
% binwalk FW96650A.bin
% binwalk -E FW96650A.bin
1249
2600960 0x27B000 Rising entropy edge (0.968167)
2607104 0x27C800 Rising entropy edge (0.958582)
2609152 0x27D000 Falling entropy edge (0.760989)
2654208 0x288000 Rising entropy edge (0.954127)
2670592 0x28C000 Rising entropy edge (0.967883)
2676736 0x28D800 Rising entropy edge (0.975779)
2684928 0x28F800 Falling entropy edge (0.744369)
Oui, il trouve un fichier JPEG et même des données MySQL! Mais je ne suis pas certain
que ça soit vrai—je ne l’ai pas encore vérifié.
Il est aussi intéressant d’essayer la clusterisation dans Mathematica:
Ceci est un exemple de la façon dont Mathematica groupe des valeurs d’entropie
diverses dans des groupes distincts. En effet, c’est quelque chose de plausible. Les
points bleus dans l’intervalle 5.0-5.5 sont probablement relatif à du texte en anglais,
Les points jaunes dans 5.5-6 sont du code MIPS. Beaucoup de points verts dans 6.0-
6.5 sont dans le second tiers non identifié. Les points orange proches de 8.0 sont
relatifs au fichier JPEG compressé. D’autres points orange sont probablement relatif
à la fin du firmware (données inconnues pour nous).
Liens
1250
9.2.2 Conclusion
L’entropie peut-être utilisée comme un moyen rapide d’investigation de fichiers in-
connus. En particulier, c’est un moyen rapide de trouver des morceaux de données
compressées/chiffrées. Quelqu’un a dit qu’il est possible de trouver des clefs RSA5
privées/publiques (et d’autres algorithmes cryptographiques) dans du code exécu-
table (les clefs ont aussi une très grande entropie), mais je n’ai pas essayé moi-
même.
9.2.3 Outils
L’utilitaire Linux ent est très pratique pour trouver l’entropie d’un fichier6 .
Il y a un excellent visualiseur d’entropie en ligne fait par Aldo Cortesi, que j’ai essayé
d’imiter avec Mathematica: http://binvis.io. Ses articles sur l’entropie valent la
peine d’être lus: http://corte.si/posts/visualisation/entropy/index.html, http:
//corte.si/posts/visualisation/malware/index.html, http://corte.si/posts/
visualisation/binvis/index.html.
Le quadriciel radare2 a la commande #entropy pour ceci.
Un outil pour IDA: IDAtropy7 .
1251
function endp
...
CALL function
...
CALL function
9.2.6 PRNG
Lorsque je lance GnuPG pour générer une nouvelle clef privée (secrète), il demande
de l’entropie …
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation ; this gives the random number
generator a better chance to gain enough entropy.
Not enough random bytes available. Please do some other work to give
the OS a chance to collect more entropy ! (Need 169 more bytes)
Ceci signifie qu’un bon PRNG prend longtemps pour produire des résultats avec une
haute entropie, et ceci est ce dont la clef cryptographique secrète à besoin. Mais
un CPRNG8 est compliqué (car un ordinateur est lui-même un dispositif hautement
déterministe), donc GnuPG demande du hasard supplémentaire à l’utilisateur.
Ceci signifie que presque tout l’espace disponible d’un octet est rempli d’information.
256 octets répartis dans l’intervalle 0..255 donnent exactement une valeur de 8:
8. Cryptographically secure PseudoRandom Number Generator
1252
# !/usr/bin/env python
import sys
for i in range(256) :
sys.stdout.write(chr(i))
L’ordre des octets est sans importance. Ceci signifie que tout l’espace dans un octet
est rempli.
L’entropie de tout bloc rempli d’octets à zéro est 0:
% dd bs=1M count=1 if=/dev/zero | ent
Entropy = 0.000000 bits per byte.
L’entropie d’une chaîne constituée d’un seul (n’importe lequel) octet est 0:
% echo -n "aaaaaaaaaaaaaaaaaaa" | ent
Entropy = 0.000000 bits per byte.
L’entropie d’une chaîne en base64 est la même que la données source, mais multi-
plié par 34 . Ceci car l’encodage base64 utilise 64 symboles au lieu de 256.
% dd bs=1M count=1 if=/dev/urandom | base64 | ent
Entropy = 6.022068 bits per byte.
Peut-être que 6.02, assez proche de 6, est dû au caractère de remplissage (=) qui
fausse un peu nos statistiques.
Uuencode utilise aussi 64 symboles:
% dd bs=1M count=1 if=/dev/urandom | uuencode - | ent
Entropy = 6.013162 bits per byte.
Ceci signifie que les chaînes base64 et Uuencode peuvent être transmises en utili-
sant des octets ou caractères sur 6-bit.
Toute information aléatoire au format hexadécimal a une entropie de 4 bits par octet:
% openssl rand -hex $\$$(( 2**16 )) | ent
Entropy = 4.000013 bits per byte.
L’entropie d’un texte en anglais pris au hasard dans la bibliothèque Gutenbert a une
entropie de ≈ 4.5. La raison de ceci est que les textes anglais utilisent principalement
26 symboles, et log2 (26) =≈ 4.7, i.e., vous aurez besoin d’octets de 5-bit pour trans-
mettre des textes en anglais non compressés, ça sera suffisant (ça l’était en effet
au temps du télétype).
Le texte choisi au hasard dans la bibliothèque http://lib.ru est l’“Idiot”9 , de F.M.Dostoevsky
qui est encodé en CP1251.
9. http://az.lib.ru/d/dostoewskij_f_m/text_0070.shtml
1253
Et ce fichier a une entropie de ≈ 4.98. Le russe comporte 33 caractères et log2 (33) =≈
5.04. Mais il le caractère “ё” est impopulaire et rare. Et log2 (32) = 5 (l’alphabet russe
sans ce caractère rare)—maintenant ceci est proche de ce que nous avons obtenu.
Quoiqu’il en soit, le texte dont nous parlons utilise la lettre “ё’, mais, sans doute y
est-elle rarement utilisée.
Le même fichier transcodé de CP1251 en UTF-8 donne une entropie de ≈ 4.23. Chaque
caractère cyrillique encodé en UTF-8 est généralement encodé en une paire, et le
premier octet est toujours: 0xD0 ou 0xD1. C’est peut-être ce qui cause ce biais.
Générons des bits aléatoirement et écrivons les avec les caractères “T” et “F”:
# !/usr/bin/env python
import random, sys
rt=""
for i in range(102400) :
if random.randint(0,1)==1:
rt=rt+"T"
else :
rt=rt+"F"
print rt
Échantillon: ...TTTFTFTTTFFFTTTFTTTTTTFTTFFTTTFTFTTFTTFFFFFF....
L’entropie est très proche de 1 (i.e., 1 bit par octet).
Générons des chiffres décimaux aléatoirement:
# !/usr/bin/env python
import random, sys
rt=""
for i in range(102400) :
rt=rt+"%d" % random.randint(0,9)
print rt
Échantillon: ...52203466119390328807552582367031963888032....
L’entropie sera proche de 3.32, en effet, c’est log2 (10).
1254
9.3 Fichier de sauvegarde du jeu Millenium
«Millenium Return to Earth » est un ancien jeu DOS (1991), qui vous permet d’extraire
des ressources, de construire des vaisseaux, de les équiper et de les envoyer sur
d’autres planêtes, et ainsi de suite10 .
Comme beaucoup d’autres jeux, il vous permet de sauvegarder l’état du jeu dans
un fichier.
Regardons si l’on peut y trouver quelque chose.
1255
Donc, il y a des mines dans le jeu. Sur certaines planêtes, les mines rapportent plus
vite, sur d’autres, moins vite. L’ensemble des ressources est aussi différent.
Ici, nous pouvons voir quelles ressources sont actuellement extraites.
1256
Fig. 9.15: Mine: état 2
1257
00000C01 : A8 28
00000C07 : D8 58
00000C09 : E4 A4
00000C0D : 38 B8
00000C0F : E8 68
...
La sortie est incomplète ici, il y a plus de différences, mais j’ai tronqué le résultat
pour montrer ce qu’il y a de plus intéressant.
Dans le premier état, nous avons 14 «unités » d’hydrogène et 102 «unités » d’oxy-
gène.
Nous avons respectivement 22 et 155 «unités » dans le second état. Si ces valeurs
sont sauvées dans le fichier de sauvegarde, nous devrions les voir dans la différence.
Et en effet, nous les voyons. Il y a 0x0E (14) à la position 0xBDA et cette valeur est
à 0x16 (22) dans la nouvelle version du fichier. Ceci est probablement l’hydrogène.
Il y a 0x66 (102) à la position 0xBDC dans la vieille version et x9B (155) dans la
nouvelle version du fichier. Il semble que ça soit l’oxygène.
Les deux fichiers sont disponibles sur le site web pour ceux qui veulent les inspecter
(ou expérimenter) plus: beginners.re.
1258
Voici la nouvelle version du fichier ouverte dans Hiew, j’ai marqué les valeurs rela-
tives aux ressources extraites dans le jeu:
1259
Vérifions nos hypothèses. Nous allons écrire la valeur 1234 (0x4D2) à la première
position (ceci doit être l’hydrogène) :
Puis nous chargeons le fichier modifié dans le jeu et jettons un coup d’œil aux sta-
tistiques des mines:
1260
Maintenant essayons de finir le jeu le plus vite possible, mettons les valeurs maxi-
males partout:
0xFFFF représente 65535, donc oui, nous avons maintenant beaucoup de ressources:
1261
Laissons passer quelques «jours » dans le jeu et oups! Nous avons un niveau plus
bas pour quelques ressources:
1262
fortune est un programme UNIX bien connu qui affiche une phrase aléatoire depuis
une collection. Certains geeks ont souvent configuré leur système de telle manière
que fortune puisse être appelé après la connexion. fortune prend les phrases depuis
des fichiers texte se trouvant dans /usr/share/games/fortunes (sur Ubuntu Linux).
Voici un exemple (fichier texte «fortunes ») :
A day for firm decisions !!!!! Or is it ?
%
A few hours grace before the madness begins again.
%
A gift of a flower will soon be made to you.
%
A long-forgotten loved one will appear soon.
Donc, il s’agit juste de phrases, parfois sur plusieurs lignes, séparées par le signe
pourcent. La tâche du programme fortune est de trouver une phrase aléatoire et de
l’afficher. Afin de réaliser ceci, il doit scanner le fichier texte complet, compter les
phrases, en choisir une aléatoirement et l’afficher. Mais le fichier texte peut être très
gros, et même sur les ordinateurs modernes, cet algorithme naïf est du gaspillage
de ressource. La façon simple de procéder est de garder un fichier index binaire
contenant l’offset de chaque phrase du fichier texte. Avec le fichier index, le pro-
gramme fortune peut fonctionner beaucoup plus vite: il suffit de choisir un index
aléatoirement, prendre son offset, se déplacer à cet offset dans le fichier texte et
d’y lire la phrase. C’est ce qui est effectivement fait dans le programme fortune.
Examinons ce qu’il y a dans ce fichier index (ce sont les fichiers .dat dans le même
répertoire) dans un éditeur hexadécimal. Ce programme est open-source bien sûr,
mais intentionnellement, je ne vais pas jeter un coup d’œil dans le code source.
% od -t x1 --address-radix=x fortunes.dat
000000 00 00 00 02 00 00 01 af 00 00 00 bb 00 00 00 0f
000010 00 00 00 00 25 00 00 00 00 00 00 00 00 00 00 2b
000020 00 00 00 60 00 00 00 8f 00 00 00 df 00 00 01 14
000030 00 00 01 48 00 00 01 7c 00 00 01 ab 00 00 01 e6
000040 00 00 02 20 00 00 02 3b 00 00 02 7a 00 00 02 c5
000050 00 00 03 04 00 00 03 3d 00 00 03 68 00 00 03 a7
000060 00 00 03 e1 00 00 04 19 00 00 04 2d 00 00 04 7f
000070 00 00 04 ad 00 00 04 d5 00 00 05 05 00 00 05 3b
000080 00 00 05 64 00 00 05 82 00 00 05 ad 00 00 05 ce
000090 00 00 05 f7 00 00 06 1c 00 00 06 61 00 00 06 7a
0000a0 00 00 06 d1 00 00 07 0a 00 00 07 53 00 00 07 9a
0000b0 00 00 07 f8 00 00 08 27 00 00 08 59 00 00 08 8b
0000c0 00 00 08 a0 00 00 08 c4 00 00 08 e1 00 00 08 f9
0000d0 00 00 09 27 00 00 09 43 00 00 09 79 00 00 09 a3
0000e0 00 00 09 e3 00 00 0a 15 00 00 0a 4d 00 00 0a 5e
0000f0 00 00 0a 8a 00 00 0a a6 00 00 0a bf 00 00 0a ef
000100 00 00 0b 18 00 00 0b 43 00 00 0b 61 00 00 0b 8e
000110 00 00 0b cf 00 00 0b fa 00 00 0c 3b 00 00 0c 66
1263
000120 00 00 0c 85 00 00 0c b9 00 00 0c d2 00 00 0d 02
000130 00 00 0d 3b 00 00 0d 67 00 00 0d ac 00 00 0d e0
000140 00 00 0e 1e 00 00 0e 67 00 00 0e a5 00 00 0e da
000150 00 00 0e ff 00 00 0f 43 00 00 0f 8a 00 00 0f bc
000160 00 00 0f e5 00 00 10 1e 00 00 10 63 00 00 10 9d
000170 00 00 10 e3 00 00 11 10 00 00 11 46 00 00 11 6c
000180 00 00 11 99 00 00 11 cb 00 00 11 f5 00 00 12 32
000190 00 00 12 61 00 00 12 8c 00 00 12 ca 00 00 13 87
0001a0 00 00 13 c4 00 00 13 fc 00 00 14 1a 00 00 14 6f
0001b0 00 00 14 ae 00 00 14 de 00 00 15 1b 00 00 15 55
0001c0 00 00 15 a6 00 00 15 d8 00 00 16 0f 00 00 16 4e
...
Sans aide particulière, nous pouvons voir qu’il y a quatre éléments de 4 octets sur
chaque ligne de 16 octets. Peut-être que c’est notre tableau d’index. J’essaye de
charger tout le fichier comme un tableau d’entier 32-bit dans Wolfram Mathematica:
In[]:= BinaryReadList["c :/tmp1/fortunes.dat", "UnsignedInteger32"]
Nope, quelque chose est faux, les nombres sont étrangement gros. Mais retournons
à la sortie de od : chaque élément de 4 octets a deux octets nuls et deux octets non
nuls. Donc les offsets (au moins au début du fichier) sont au maximum 16-bit. Peut-
être qu’un endianness différent est utilisé dans le fichier? L’endianness par défaut
dans Mathematica est little-endian, comme utilisé dans les CPUs Intel. Maintenant,
je le change en big-endian:
In[]:= BinaryReadList["c :/tmp1/fortunes.dat", "UnsignedInteger32",
ByteOrdering -> 1]
Out[]= {2, 431, 187, 15, 0, 620756992, 0, 43, 96, 143, 223, 276, \
328, 380, 427, 486, 544, 571, 634, 709, 772, 829, 872, 935, 993, \
1049, 1069, 1151, 1197, 1237, 1285, 1339, 1380, 1410, 1453, 1486, \
1527, 1564, 1633, 1658, 1745, 1802, 1875, 1946, 2040, 2087, 2137, \
2187, 2208, 2244, 2273, 2297, 2343, 2371, 2425, 2467, 2531, 2581, \
2637, 2654, 2698, 2726, 2751, 2799, 2840, 2883, 2913, 2958, 3023, \
3066, 3131, 3174, 3205, 3257, 3282, 3330, 3387, 3431, 3500, 3552, \
...
Oui, c’est quelque chose de lisible. Je choisi un élément au hasard (3066) qui s’écrit
0xBFA en format hexadécimal. J’ouvre le fichier texte ’fortunes’ dans un éditeur hexa-
décimal, je met l’offset 0xBFA et je vois cette phrase:
1264
% od -t x1 -c --skip-bytes=0xbfa --address-radix=x fortunes
000bfa 44 6f 20 77 68 61 74 20 63 6f 6d 65 73 20 6e 61
D o w h a t c o m e s n a
000c0a 74 75 72 61 6c 6c 79 2e 20 20 53 65 65 74 68 65
t u r a l l y . S e e t h e
000c1a 20 61 6e 64 20 66 75 6d 65 20 61 6e 64 20 74 68
a n d f u m e a n d t h
....
Ou:
Do what comes naturally. Seethe and fume and throw a tantrum.
%
D’autres offsets peuvent aussi être essayés, oui, ils sont valides.
Je peux aussi vérifier dans Mathematica que chaque élément consécutif est plus
grand que le précédent. I.e., les éléments du tableau sont croissants. Dans le jargon
mathématiques, ceci est appelé fonction monotone strictement croissante.
In[]:= Differences[input]
Out[]= {429, -244, -172, -15, 620756992, -620756992, 43, 53, 47, \
80, 53, 52, 52, 47, 59, 58, 27, 63, 75, 63, 57, 43, 63, 58, 56, 20, \
82, 46, 40, 48, 54, 41, 30, 43, 33, 41, 37, 69, 25, 87, 57, 73, 71, \
94, 47, 50, 50, 21, 36, 29, 24, 46, 28, 54, 42, 64, 50, 56, 17, 44, \
28, 25, 48, 41, 43, 30, 45, 65, 43, 65, 43, 31, 52, 25, 48, 57, 44, \
69, 52, 62, 73, 62, 53, 37, 68, 71, 50, 41, 57, 69, 58, 70, 45, 54, \
38, 45, 50, 42, 61, 47, 43, 62, 189, 61, 56, 30, 85, 63, 48, 61, 58, \
81, 50, 55, 63, 83, 80, 49, 42, 94, 54, 67, 81, 52, 57, 68, 43, 28, \
120, 64, 53, 81, 33, 82, 88, 29, 61, 32, 75, 63, 70, 47, 101, 60, 79, \
33, 48, 65, 35, 59, 47, 55, 22, 43, 35, 102, 53, 80, 65, 45, 31, 29, \
69, 32, 25, 38, 34, 35, 49, 59, 39, 41, 18, 43, 41, 83, 37, 31, 34, \
59, 72, 72, 81, 77, 53, 53, 50, 51, 45, 53, 39, 70, 54, 103, 33, 70, \
51, 95, 67, 54, 55, 65, 61, 54, 54, 53, 45, 100, 63, 48, 65, 71, 23, \
28, 43, 51, 61, 101, 65, 39, 78, 66, 43, 36, 56, 40, 67, 92, 65, 61, \
31, 45, 52, 94, 82, 82, 91, 46, 76, 55, 19, 58, 68, 41, 75, 30, 67, \
92, 54, 52, 108, 60, 56, 76, 41, 79, 54, 65, 74, 112, 76, 47, 53, 61, \
66, 53, 28, 41, 81, 75, 69, 89, 63, 60, 18, 18, 50, 79, 92, 37, 63, \
88, 52, 81, 60, 80, 26, 46, 80, 64, 78, 70, 75, 46, 91, 22, 63, 46, \
34, 81, 75, 59, 62, 66, 74, 76, 111, 55, 73, 40, 61, 55, 38, 56, 47, \
78, 81, 62, 37, 41, 60, 68, 40, 33, 54, 34, 41, 36, 49, 44, 68, 51, \
50, 52, 36, 53, 66, 46, 41, 45, 51, 44, 44, 33, 72, 40, 71, 57, 55, \
39, 66, 40, 56, 68, 43, 88, 78, 30, 54, 64, 36, 55, 35, 88, 45, 56, \
76, 61, 66, 29, 76, 53, 96, 36, 46, 54, 28, 51, 82, 53, 60, 77, 21, \
84, 53, 43, 104, 85, 50, 47, 39, 66, 78, 81, 94, 70, 49, 67, 61, 37, \
51, 91, 99, 58, 51, 49, 46, 68, 72, 40, 56, 63, 65, 41, 62, 47, 41, \
43, 30, 43, 67, 78, 80, 101, 61, 73, 70, 41, 82, 69, 45, 65, 38, 41, \
57, 82, 66}
Comme on le voit, excepté les 6 premières valeurs (qui appartiennent sans doute
à l’entête du fichier d’index), tous les nombres sont en fait la longueur des phrases
1265
de texte (l’offset de la phrase suivante moins l’offset de la phrase courante est la
longueur de la phrase courante).
Il est très important de garder à l’esprit que l’endianness peut être confondu avec un
début de tableau incorrect. En effet, dans la sortie d’od nous voyons que chaque élé-
ment débutait par deux zéros. Mais lorsque nous décalons les des octets de chaque
côté, nous pouvons interpréter ce tableau comme little-endian:
% od -t x1 --address-radix=x --skip-bytes=0x32 fortunes.dat
000032 01 48 00 00 01 7c 00 00 01 ab 00 00 01 e6 00 00
000042 02 20 00 00 02 3b 00 00 02 7a 00 00 02 c5 00 00
000052 03 04 00 00 03 3d 00 00 03 68 00 00 03 a7 00 00
000062 03 e1 00 00 04 19 00 00 04 2d 00 00 04 7f 00 00
000072 04 ad 00 00 04 d5 00 00 05 05 00 00 05 3b 00 00
000082 05 64 00 00 05 82 00 00 05 ad 00 00 05 ce 00 00
000092 05 f7 00 00 06 1c 00 00 06 61 00 00 06 7a 00 00
0000a2 06 d1 00 00 07 0a 00 00 07 53 00 00 07 9a 00 00
0000b2 07 f8 00 00 08 27 00 00 08 59 00 00 08 8b 00 00
0000c2 08 a0 00 00 08 c4 00 00 08 e1 00 00 08 f9 00 00
0000d2 09 27 00 00 09 43 00 00 09 79 00 00 09 a3 00 00
0000e2 09 e3 00 00 0a 15 00 00 0a 4d 00 00 0a 5e 00 00
...
1266
différence entre l’offset de la phrase courante et l’offset de la phrase suivante. Ceci
est plus rapide que de lire la chaîne à la recherche du caractère pourcent. Mais ceci
ne fonctionne pas pour le dernier élément. Donc un élément fictif est aussi ajouté à
la fin du tableau.
Donc les 6 première valeur entière 32-bit sont une sorte d’en-tête.
Oh, j’ai oublié de compter les phrases dans le fichier texte:
% cat fortunes | grep % | wc -l
432
Le nombre de phrases peut être présent dans dans l’index, mais peut-être pas. Dans
le cas de fichiers d’index simples, le nombre d’éléments peut être facilement déduit
de la taille du fichier d’index. Quoiqu’il en soit, il y a 432 phrases dans le fichier texte.
Et nous voyons quelque chose de très familier dans le second élément (la valeur
431). J’ai vérifié dans d’autres fichiers (literature.dat et riddles.dat sur Ubuntu Linux)
et oui, le second élément 32-bit est bien le nombre de phrases moins 1. Pourquoi
moins 1 ? Peut-être que ceci n’est pas le nombre de phrases, mais plutôt le numéro
de la dernière phrase (commençant à zéro) ?
Et il y a d’autres éléments dans l’entête. Dans Mathematica, je charge chacun des
trois fichiers disponible et je regarde leur en-tête:
Je n’ai aucune idée de la signification des autres valeurs, excepté la taille du fichier
d’index. Certains champs sont les même pour tous les fichiers, d’autres non. D’après
mon expérience, ils peuvent être:
• signature de fichier;
• version de fichier;
• checksum;
1267
• des flags;
• peut-être même des identifiants de langages;
• timestamp du fichier texte, donc le programme fortune regénèrerait le fichier
d’index isi l’utilisateur modifiait le fichier texte.
Par exemple, les fichiers Oracle .SYM ( 9.5 on the next page) qui contiennent la table
des symboles pour les fichiers DLL, contiennent aussi un timestamp correspondant
au fichier DLL, afin d’être sûr qu’il est toujours valide.
D’un autre côté, les timestamps des fichiers textes et des fichiers d’index peuvent
être désynchronisés après archivage/désarchivage/installation/déploiement/etc.
Mais ce ne sont pas des timestamps, d’après moi. La manière la plus compacte de
représenter la date et l’heure est la valeur UNIX, qui est un nombre 32-bit. Nous ne
voyons rien de tel ici. D’autres façons de les représenter sont encore moins com-
pactes.
Donc, voici supposément comment fonctionne l’algorithme de fortune :
• prendre le nombre du second élément de la dernière phrase;
• générer un nombre aléatoire dans l’intervalle 0..number_of_last_phrase;
• trouver l’élément correspondant dans le tableau des offsets, prendre aussi l’off-
set suivant;
• écrire sur stdout tous les caractères depuis le fichier texte en commençant à
l’offset jusqu’à l’offset suivant moins 2 (afin d’ignorer le caractère pourcent
terminal et le caractère de la phrase suivante).
9.4.1 Hacking
Effectuons quelques essais afin de vérifier nos hypothèses. Je vais créer ce fichier
texte dans le chemin et le nom /usr/share/games/fortunes/fortunes :
Phrase one.
%
Phrase two.
%
Puis, ce fichier fortunes.dat. Je prend l’entête du fichier original fortunes.dat, j’ai mis
à zéro changé le second champ (nombre de phrases) et j’ai laissé deux éléments
dans le tableau: 0 et 0x1c, car la longueur totale du fichier texte fortunes est 28
(0x1c) octets:
% od -t x1 --address-radix=x fortunes.dat
000000 00 00 00 02 00 00 00 00 00 00 00 bb 00 00 00 0f
000010 00 00 00 00 25 00 00 00 00 00 00 00 00 00 00 1c
Maintenant, je le lance:
% /usr/games/fortune
fortune : no fortune found
1268
Quelque chose ne va pas. Mettons le second champ à 1:
% od -t x1 --address-radix=x fortunes.dat
000000 00 00 00 02 00 00 00 01 00 00 00 bb 00 00 00 0f
000010 00 00 00 00 25 00 00 00 00 00 00 00 00 00 00 1c
11. Gibibyte
1269
-------------------- -------- -------------------- ⤦
Ç ----------------------------
_kqvrow() 00000000
_opifch2()+2729 CALLptr 00000000 23D4B914 E47F264 1F19AE2
EB1C8A8 1
_kpoal8()+2832 CALLrel _opifch2() 89 5 EB1CC74
_opiodr()+1248 CALLreg 00000000 5E 1C EB1F0A0
_ttcpip()+1051 CALLreg 00000000 5E 1C EB1F0A0 0
_opitsk()+1404 CALL ??? 00000000 C96C040 5E EB1F0A0 0 ⤦
Ç EB1ED30
EB1F1CC 53E52E 0 EB1F1F8
_opiino()+980 CALLrel _opitsk() 0 0
_opiodr()+1248 CALLreg 00000000 3C 4 EB1FBF4
_opidrv()+1201 CALLrel _opiodr() 3C 4 EB1FBF4 0
_sou2o()+55 CALLrel _opidrv() 3C 4 EB1FBF4
_opimai_real()+124 CALLrel _sou2o() EB1FC04 3C 4 EB1FBF4
_opimai()+125 CALLrel _opimai_real() 2 EB1FC2C
_OracleThreadStart@ CALLrel _opimai() 2 EB1FF6C 7C88A7F4 ⤦
Ç EB1FC34 0
4()+830 EB1FD04
77E6481C CALLreg 00000000 E41FF9C 0 0 E41FF9C 0 ⤦
Ç EB1FFC4
00000000 CALL ??? 00000000
Mais bien sûr, les exécutables d’Oracle RDBMS doivent avoir une sorte d’information
de débogage ou de fichiers de carte avec l’information sur les symboles incluse ou
quelque chose comme ça.
Oracle RDBMS sur Windows NT a l’information sur les symboles incluse dans des
fichiers avec l’extension .SYM, mais le format est propriétaire. (Les fichiers texte en
clair sont bons, mais nécessite une analyse supplémentaire, d’où un accès plus lent.)
Voyons si nous pouvons comprendre son format.
Nous allons choisir le plus petit fichier orawtc8.sym qui vient avec le fichier orawtc8.dll
dans Oracle 8.1.712 .
12. Nous pouvons choisir une version plus ancienne d’Oracle RDBMS intentionnellement à cause de la
plus petite taille de ses modules.
1270
Voici le fichier ouvert dans Hiew:
En comparant le fichier avec d’autres fichiers .SYM, nous voyons rapidement que
OSYM est toujours en entête (et en fin de fichier), donc c’est peut-être la signature
du fichier.
Nous voyons que, en gros, le format de fichier est: OSYM + des données binaires
+ un zéro délimiteur de chaîne de texte + OSYM. Les chaînes sont évidemment les
noms des fonctions et des variables globales.
1271
Nous allons marquer les signatures OSYM et les chaînes ici:
Bon, voyons. Dans Hiew, nous allons marquer le bloc de chaînes complet (excepté
les signatures OSYM) et les mettre dans un fichier séparé. Puis, nous lançons les
utilitaires UNIX strings et wc pour compter les chaînes de texte:
strings strings_block | wc -l
66
1272
00000020 d0 11 00 10 70 13 00 10 40 15 00 10 50 15 00 10 |[email protected]⤦
Ç ...|
00000030 60 15 00 10 80 15 00 10 a0 15 00 10 a6 15 00 10 ⤦
Ç |`...............|
....
Bien sûr, 0x42 n’est pas ici un octet, mais plus probablement une velaur 32-bit pa-
ckée en petit-boutiste, ainsi nous pouvons voir 0x42 et ensuite au moins 3 octets à
zéro.
Pourquoi croyons-nous que c’est 32-bit? Car les fichiers de symboles d’Oracle RDBMS
peuvent être plutôt gros.
Le fichier oracle.sym pour l’exécutable principal oracle.exe (version 10.2.0.4) contient
0x3A38E (238478) symboles.
Nous pouvons vérifier d’autres fichiers .SYM comme ceci et ça prouve notre suppo-
sition: la valeur après la signature 32-bit OSYM représente toujours le nombre de
chaînes de texte dans le fichier.
C’est une caractéristique générale de presque tous les fichiers binaires: un entête
avec une signature ainsi que d’autres informations sur le fichier.
Maintenant, examinons de plus près ce qu’est ce bloc binaire.
En utilisant Hiew, nous extrayons le bloc débutant à l’offset 8 (i.e., après la valeur
32-bit count) se terminant au bloc de chaînes, dans un fichier binaire séparé.
1273
Voyons le bloc binaire dans Hiew:
1274
Nous ajoutons des lignes rouges pour diviser le bloc:
Hiew, comme presque n’importe quel autre éditeur hexadécimal, montre 16 octets
par ligne. Donc le motif est clairement visible: il y a 4 valeurs 32-bit par ligne.
Le schéma est visible visuellement car certaines valeurs ici (jusqu’à l’adresse 0x104
sont toujours de la forme 0x1000xxxx, commençant par 0x10 et des octets à zéro.
D’autres valeurs (commençant à 0x108) sont de la forme 0x0000xxxx, donc com-
mencent toujours par deux octets à zéro.
Affichons le bloc comme un tableau de valeurs 32-bit:
1275
0000020 10001160 100011c0 100011d0 10001370
0000040 10001540 10001550 10001560 10001580
0000060 100015a0 100015a6 100015ac 100015b2
0000100 100015b8 100015be 100015c4 100015ca
0000120 100015d0 100015e0 100016b0 10001760
0000140 10001766 1000176c 10001780 100017b0
0000160 100017d0 100017e0 10001810 10001816
0000200 10002000 10002004 10002008 1000200c
0000220 10002010 10002014 10002018 1000201c
0000240 10002020 10002024 10002028 1000202c
0000260 10002030 10002034 10002038 1000203c
0000300 10002040 10002044 10002048 1000204c
0000320 10002050 100020d0 100020e4 100020f8
0000340 1000210c 10002120 10003000 10003004
0000360 10003008 1000300c 10003098 1000309c
0000400 100030a0 100030a4 00000000 00000008
0000420 00000012 0000001b 00000025 0000002e
0000440 00000038 00000040 00000048 00000051
0000460 0000005a 00000064 0000006e 0000007a
0000500 00000088 00000096 000000a4 000000ae
0000520 000000b6 000000c0 000000d2 000000e2
0000540 000000f0 00000107 00000110 00000116
0000560 00000121 0000012a 00000132 0000013a
0000600 00000146 00000153 00000170 00000186
0000620 000001a9 000001c1 000001de 000001ed
0000640 000001fb 00000207 0000021b 0000022a
0000660 0000023d 0000024e 00000269 00000277
0000700 00000287 00000297 000002b6 000002ca
0000720 000002dc 000002f0 00000304 00000321
0000740 0000033e 0000035d 0000037a 00000395
0000760 000003ae 000003b6 000003be 000003c6
0001000 000003ce 000003dc 000003e9 000003f8
0001020
Il y a 132 valeurs, qui vaut 66*3. Peut-être qu’il y a deux valeurs 32-bit pour chaque
symbole, mais peut-être y a-t-il deux tableaux? Voyons
Les valeurs débutant par 0x1000 peuvent être une adresse.
Ceci est un fichier .SYM pour une DLL après tout, et l’adresse de base par défaut des
DLL win32 est 0x10000000, et le code débute en général en 0x10001000.
Lorsque nous ouvrons le fichier orawtc8.dll dans IDA, l’adresse de base est différente,
mais néanmoins, la première fonction est:
.text :60351000 sub_60351000 proc near
.text :60351000
.text :60351000 arg_0 = dword ptr 8
.text :60351000 arg_4 = dword ptr 0Ch
.text :60351000 arg_8 = dword ptr 10h
.text :60351000
.text :60351000 push ebp
.text :60351001 mov ebp, esp
.text :60351003 mov eax, dword_60353014
1276
.text :60351008 cmp eax, 0FFFFFFFFh
.text :6035100B jnz short loc_6035104F
.text :6035100D mov ecx, hModule
.text :60351013 xor eax, eax
.text :60351015 cmp ecx, 0FFFFFFFFh
.text :60351018 mov dword_60353014, eax
.text :6035101D jnz short loc_60351031
.text :6035101F call sub_603510F0
.text :60351024 mov ecx, eax
.text :60351026 mov eax, dword_60353014
.text :6035102B mov hModule, ecx
.text :60351031
.text :60351031 loc_60351031 : ; CODE XREF: sub_60351000+1D
.text :60351031 test ecx, ecx
.text :60351033 jbe short loc_6035104F
.text :60351035 push offset ProcName ; "ax_reg"
.text :6035103A push ecx ; hModule
.text :6035103B call ds :GetProcAddress
...
1277
La chaîne «ax_unreg » est aussi la seconde chaîne dans le bloc de chaîne!
L’adresse de début de la seconde fonction est 0x60351080, et la seconde valeur
dans le bloc binaire est 10001080. Donc ceci est l’adresse, mais pour une DLL avec
l’adresse de base par défaut.
Nous pouvons rapidement vérifier et être sûr que les 66 premières valeurs dans
le tableau (i.e., la première moitié du tableau) sont simplement les adresses des
fonctions dans la DLL, incluant quelques labels, etc. Bien, qu’est-ce que l’autre partie
du tableau? Les autres 66 valeurs qui commencent par 0x0000 ? Elles semblent être
dans l’intervalle [0...0x3F8]. Et elles ne ressemblent pas à des champs de bits: la
série de nombres augmente.
Le dernier chiffre hexadécimal semble être aléatoire, donc, il est peu probable que
ça soit l’adresse de quelque chose (il serait divisible par 4 ou peut-être 8 ou 0x10
autrement).
Demandons-nous: qu’est-ce que les développeurs d’Oracle RDBMS pourraient avoir
sauvegardé ici, dans ce fichier?
Supposition rapide: ça pourrait être l’adresse de la chaîne de texte (nom de la fonc-
tion).
Ça peut être vérifié rapidement, et oui, chaque nombre est simplement la position
du premier caractère dans le bloc de chaînes.
Ça y est! C’est fini.
Nous allons écrire un utilitaire pour convertir ces fichiers .SYM en un script IDA, donc
nous pourrons charger le script .idc et mettre les noms de fonction:
#include <stdio.h>
#include <stdint.h>
#include <io.h>
#include <assert.h>
#include <malloc.h>
#include <fcntl.h>
#include <string.h>
// additional offset
assert (sscanf (argv[2], "%X", &offset)==1) ;
1278
assert (lseek (h, 0, SEEK_SET) !=-1) ;
// read signature
assert (read (h, &sig, 4)==4) ;
// read count
assert (read (h, &cnt, 4)==4) ;
array_size_in_bytes=cnt*sizeof(uint32_t) ;
printf ("}\n") ;
close (h) ;
free (d1) ; free (d2) ; free (d3) ;
};
static main() {
MakeName(0x60351000, "_ax_reg") ;
MakeName(0x60351080, "_ax_unreg") ;
MakeName(0x603510F0, "_loaddll") ;
MakeName(0x60351150, "_wtcsrin0") ;
MakeName(0x60351160, "_wtcsrin") ;
1279
MakeName(0x603511C0, "_wtcsrfre") ;
MakeName(0x603511D0, "_wtclkm") ;
MakeName(0x60351370, "_wtcstu") ;
...
}
Les fichiers d’exemple qui ont été utilisés dans cet exemple sont ici: beginners.re.
1280
Oh, essayons avec Oracle RDBMS pour win64. Les adresses devraient faire 64-bit,
n’est-ce pas?
Le motif sur 8 octets est visible encore plus facilement ici:
Donc oui, toutes les tables ont maintenant des éléments 64-bit, même les offsets de
chaîne!
La signature est maintenant OSYMAM64, pour distinguer la plate-forme cible, appa-
remment.
C’est fini!
Voici aussi la bibliothèque qui a des fonctions pour accéder les fichiers .SYM d’Oracle
RDBMS : GitHub.
1281
9.6 Oracle RDBMS : fichiers .MSB-files
When working toward the solution of a
problem, it always helps if you know the
answer.
Ceci est un fichier binaire qui contient les messages d’erreur avec leur numéro cor-
respondant. Essayons de comprendre son format et de trouver un moyen de les
extraire.
Il y a des fichiers de messages d’erreur d’Oracle RDBMS au format texte, donc nous
pouvons comparer le texte et les fichiers binaires paqués13 .
Ceci est le début du fichier texte ORAUS.MSG avec des commentaires non pertinents
supprimés:
Le premier nombre est le code d’erreur. Le second contient peut-être des flags sup-
plémentaires.
13. Les fichiers texte open-source n’existent pas dans Oracle RDBMS pour chaque fichier .MSB, c’est
donc pourquoi nous allons travailler sur leur format de fichier
1282
Maintenant ouvrons le fichier binaire ORAUS.MSB et trouvons ces chaînes de teste.
Et elles y sont:
Nous voyons les chaînes de texte (celles du début du fichier ORAUS.MSG inclues)
imbriquées avec d’autres sortes de valeurs binaire. Avec un examen rapide, nous
pouvons voir que la partie principale du fichier binaire est divisée en bloc de taille
0x200 (512) octets.
1283
Regardons le contenu du premier bloc:
Ici nous voyons les textes des premiers messages d’erreur. Ce que nous voyons aussi,
c’est qu’il n’y a pas d’octet à zéro entre les messages d’erreur. Ceci implique que ce
ne sont pas des chaînes C terminées par null. Par conséquent, la longueur de chaque
message d’erreur doit être encodée d’une façon ou d’une autre. Essayons aussi de
trouver le numéro d’erreur. Le fichier ORAUS.MSG débute par ceci: 0, 1, 17 (0x11),
18 (0x12), 19 (0x13), 20 (0x14), 21 (0x15), 22 (0x16), 23 (0x17), 24 (0x18)... Nous
allons trouver ces nombres au début du bloc et les marquer avec des lignes rouge.
La période entre les codes d’erreur est de 6 octets.
Ceci implique qu’il y a probablement 6 octets d’information alloués pour chaque
message d’erreur.
La première valeur 16-bit (0xA ici ou 10) indique le nombre de messages dans
1284
chaque bloc: ceci peut être vérifié en examinant d’autres blocs. En effet: les mes-
sages d’erreur ont une taille arbitraire. Certains sont plus long, d’autres plus court.
Mais la taille du bloc est toujours fixée, ainsi, vous ne savez jamais combien de mes-
sages d’erreur sont stockés dans chaque bloc.
Comme nous l’avons déjà noté, puisqu’il ne s’agit pas de chaîne C terminée par
null, leur taille doit être encodée quelque part. La taille de la première chaîne «nor-
mal, successful completion » est de 29 (0x1D) octets. La taille de la seconde chaîne
«unique constraint (%s.%s) violated » est de 34 (0x22) octets. Nous ne trouvons pas
ces valeurs (0x1D ou/et 0x22) dans le bloc.
Il y a aussi une autre chose. Oracle RDBMS doit déterminer la position de la chaîne
qu’il y a besoin de charger dans le bloc, exact? La première chaîne «normal, success-
ful completion » débute à la position 0x1444 (si nous comptons depuis le début du
fichier) ou en 0x44 (depuis le début du bloc). La seconde chaîne «unique constraint
(%s.%s) violated » débute à la position 0x1461 (depuis le début du fichier) ou en
0x61 (depuis le début du bloc). Ces nombres nous sont quelque peu familier! Nous
pouvons clairement les voir au début du bloc.
Donc, chaque bloc de 6 octets est:
• 16-bit numéro d’erreur;
• 16-bit à zéro (peut-être des flags additionnels) ;
• position du début de la chaîne de texte dans le bloc courant.
Nous pouvons rapidement vérifier les autres valeurs et être sûr que notre supposition
est correcte. Et il y a aussi le dernier bloc «factice » de 6 octets avec un numéro
d’erreur à zéro et débutant après le dernier caractère du dernier message d’erreur.
Peut-être est-ce ainsi que la longueur du texte du message est déterminée? Nous
énumérons juste les blocs de 6 octets pour trouver le numéro d’erreur dont nous
avons besoin, puis nous obtenons la position de la chaîne de texte, puis la longueur
de la chaîne de texte en cherchant le bloc de 6 octets suivant! De cette façon nous
déterminons les limites de la chaîne! Cette méthode nous permet d’économiser un
peu d’espace en ne sauvegardant pas la taille de la chaîne de texte dans le fichier!
Il n’est pas possible de dire si ça sauve beaucoup d’espace, mais c’est un truc astu-
cieux.
1285
Revenons à l’entête de fichier .MSB:
1286
Il y a aussi une table qui vient après l’entête, qui contient probablement des valeurs
16-bit:
Leurs tailles peuvent être déterminées visuellement (les lignes rouges sont dessi-
nées ici).
En regardant ces valeurs, nous avons trouvé que chaque nombre 16-bit est le dernier
code d’erreur pour chaque bloc.
C’est donc ainsi qu’Oracle RDBMS trouve rapidement le message d’erreur:
• charger la table que nous appellerons last_errnos (qui contient le dernier numé-
ro d’erreur pour chaque bloc) ;
• trouver un bloc qui contient le numéro d’erreur que nous cherchons, en assu-
mant que les codes d’erreur augmentent dans les blocs et aussi dans le fichier;
1287
• charger le bloc spécifique;
• énumérer les structures de 6 octets jusqu’à trouver le numéro d’erreur;
• obtenir la position du premier du bloc de 6 octets courant;
• obtenir la position du dernier caractère du bloc de 6 octets suivant;
• charger tous les caractères du message dans cet intervalle.
Ceci est un programme C que nous avons écrit qui extrait les fichiers .MSB: begin-
ners.re.
Voici aussi les deux fichiers que nous avons utilisé dans l’exemple (Oracle RDBMS
11.1.0.6) : beginners.re, beginners.re.
9.6.1 Résumé
Cette méthode est probablement désuète pour les ordinateurs moderne. Je suppose
que ce format de fichier a été développé au milieu des années 80 par quelqu’un
qui a aussi codé pour les big iron14 en ayant à l’esprit l’économie d’espace et de
mémoire. Néanmoins, ça a été une tâche intéressante mais facile de comprendre
un format de fichier propriétaire sans regarder dans le code d’Oracle RDBMS.
9.7 Exercices
Essayez de rétro-ingénieurer tous les fichiers binaires de votre jeu favori, fichier des
meilleurs scores inclus, ressources, etc.
Il y a aussi des fichiers binaires avec une structure connue: les fichiers utmp/wtmp,
essayer de comprendre leur structure sans documentation.
L’entête EXIF dans les fichiers JPEG est documenté, mais vous pouvez essayer de
comprendre sa structure sans aide, simplement en prenant des photos à différentes
heures/dates, lieux, et essayer de trouver la date/heure et position GPS dans les
données EXIF. Essayez de modifier la position GPS, uploadez le fichier JPEG dans
Facebook et regardez, comment il va mettre votre photo sur la carte.
Essayez de patcher toutes les informations dans un fichier MP3 et voyez comment
réagit votre lecteur de MP3 favori.
1288
Chapitre 10
Dynamic binary
instrumentation
Les outils DBI peuvent être vus comme des débogueurs très avancés et rapide.
1289
c :\pin-3.2-81205-msvc-windows\pin.exe -t XOR_ins.dll -- rar a -⤦
Ç pLongPassword tmp.rar test1.bin
c :\pin-3.2-81205-msvc-windows\pin.exe -t XOR_ins.dll -- rar a -⤦
Ç pLongPassword tmp.rar test2.bin
1290
.text :0000000140017B44 mov r8d, [rsi+rdx*4]
.text :0000000140017B48 xor r8d, [rsi+rcx*4+400h]
.text :0000000140017B50 movzx ecx, al
.text :0000000140017B53 mov eax, r11d
.text :0000000140017B56 shr eax, 18h
.text :0000000140017B59 xor r8d, [rsi+rcx*4+800h]
.text :0000000140017B61 movzx ecx, al
.text :0000000140017B64 mov eax, r11d
.text :0000000140017B67 shr eax, 10h
.text :0000000140017B6A xor r8d, [rsi+rcx*4+1000h]
.text :0000000140017B72 movzx ecx, al
.text :0000000140017B75 mov eax, r11d
.text :0000000140017B78 shr eax, 8
.text :0000000140017B7B xor r8d, [rsi+rcx*4+1400h]
.text :0000000140017B83 movzx ecx, al
.text :0000000140017B86 movzx eax, r9b
.text :0000000140017B8A xor r8d, [rsi+rcx*4+1800h]
.text :0000000140017B92 xor r8d, [rsi+rax*4+0C00h]
.text :0000000140017B9A movzx eax, r11b
.text :0000000140017B9E mov r11d, r8d
.text :0000000140017BA1 xor r11d, [rsi+rax*4+1C00h]
.text :0000000140017BA9 sub rdi, 1
.text :0000000140017BAD jnz loc_140017B21
0x4fce est 20430, qui est proche de la taille de test1.bin (30720 octets). 0x4463be
est 4481982, qui est proche de la taille de test2.bin (5547752 octets). Par égal, mais
proche.
Ceci est un morceau de code avec cette instruction XOR:
.text :000000014002C4EA loc_14002C4EA :
.text :000000014002C4EA movzx eax, byte ptr [r8]
.text :000000014002C4EE shl ecx, 5
.text :000000014002C4F1 xor ecx, eax
.text :000000014002C4F3 and ecx, 7FFFh
.text :000000014002C4F9 cmp [r11+rcx*4], esi
.text :000000014002C4FD jb short loc_14002C507
.text :000000014002C4FF cmp [r11+rcx*4], r10d
.text :000000014002C503 ja short loc_14002C507
.text :000000014002C505 inc ebx
1291
state est ensuite utilisé comme un index dans une table. Est-ce une sorte de CRC2 ?
Je ne sais pas, mais ça pourrait être une routine effectuant une somme de contrôle.
Ou peut-être une routine CRC optimisée? Une idée?
Le bloc suivant:
< ip=0x14004104a count=0x367
< ip=0x140041057 count=0x367
---
> ip=0x14004104a count=0x24193
> ip=0x140041057 count=0x24193
Ce morceau possède les instructions PXOR et AESENC (la dernière est une instruction
de chiffrement AES3 ). Donc oui, nous avons trouvé une fonction de chiffrement, RAR
utilise AES.
Il y a ensuite un autre gros bloc d’instructions XOR presque contigus:
< ip=0x140043e10 count=0x23006
---
> ip=0x140043e10 count=0x23004
499c510
< ip=0x140043e56 count=0x22ffd
---
> ip=0x140043e56 count=0x23002
1292
Mais le compteur n’est pas très différent pendant la compression/chiffrement de
test1.bin/test2.bin. Qu’y a-t-il à ces adresses?
.text :0000000140043E07 xor ecx, r9d
.text :0000000140043E0A mov r11d, eax
.text :0000000140043E0D and ecx, r10d
.text :0000000140043E10 xor ecx, r8d
.text :0000000140043E13 rol eax, 8
.text :0000000140043E16 and eax, esi
.text :0000000140043E18 ror r11d, 8
.text :0000000140043E1C add edx, 5A827999h
.text :0000000140043E22 ror r10d, 2
.text :0000000140043E26 add r8d, 5A827999h
.text :0000000140043E2D and r11d, r12d
.text :0000000140043E30 or r11d, eax
.text :0000000140043E33 mov eax, ebx
( http://www.tomshardware.com/reviews/password-recovery-gpu,2945-8.html
)
C’est la génération de la clef: le mot de passe entré est hashé plusieurs fois et le
hash est utilisé comme clef AES. C’est pourquoi nous voyons que le comptage de
l’instruction XOR est presque inchangé lorsque nous passons au fichier de test plus
gros.
C’est tout ce qu’il faut faire, ça m’a pris quelques heures d’écrire cet outil et d’obtenir
au moins 3 éléments: 1) c’est probablement une somme de contrôle; 2) chiffrement
AES ; 3) calcul de somme SHA-1. La première fonction est encore un mystère pour
moi.
Cependant, ceci est impressionnant, car je ne me suis pas plongé dans le code de
RAR (qui est propriétaire, bien sûr). Je n’ai même pas jeté un coup d’œil dans le code
source de UnRAR (qui est disponible).
Les fichiers, incluant les fichiers de test et l’exécutable RAR que j’ai utilisé (win64,
5.40) :
https://beginners.re/current-tree/DBI/XOR/files/.
1293
Le Minesweeper de Windows Vista et 7 est différent: il a probablement été (r)écrit
en C++, et l’information de la case n’est maintenant plus stockée dans un tableau
global, mais plutôt dans des blocs du heap alloués par malloc.
Ceci est un cas où nous pouvons essayer l’outil PIN DBI.
Durant le démarrage, PIN cherche tous les appels à la fonction rand() et ajoute
un hook juste après chaque appel. Le hook est la fonction RandAfter() que nous
avons défini: elle logue la valeur et l’adresse de retour. Voici un log que j’ai obte-
nu en lançant la configuration 9*9 standard (10 mines) : https://beginners.re/
current-tree/DBI/minesweeper/minesweeper1.out.10mines. La fonction rand()
a été appelée de nombreuses fois depuis différents endroits, mais a été appelée
depuis 0x10002770d exactement 10 fois. J’ai changé la configuration de Mineswee-
per à 16*16 (40 mines) et rand() a été appelée 40 fois depuis 0x10002770d. Donc
oui, c’est ce que l’on cherche. Lorsque je charge minesweeper.exe (depuis Windows
7) dans IDA et une fois que le PDB est récupéré depuis le site web de Microsoft, la
fonction qui appelle rand() en 0x10002770d est appelée Board::placeMines().
1294
Oui, contrairement à Minesweeper de Windows XP, les mines sont placées aléatoi-
rement après que l’utilisateur ai cliqué sur une case, afin de garantir qu’il n’y a pas
de mine sur la première case cliquée par l’utilisateur. Donc Minesweeper a placé les
mines dans des cases autres que celle la plus en haut à gauche (où j’ai cliqué).
Maintenant j’ai cliqué sur la case la plus en haut à droite:
C’est bien, car Minesweeper peut effectuer un placement correct même avec un
PRNG aussi mauvais!
1295
Mettons-nous dans la peau du programmeur. Il doit y avoir une boucle comme:
for (int i ; i<mines_total ; i++)
{
// get coordinates using rand()
// put a cell : in other words, modify a block allocated in heap
};
Comment pouvons-nous obtenir des information sur le bloc de qui est modifié à la
2nde étape? Ce que nous devons faire: 1) suivre toutes les allocations dans la heap
en interceptant malloc()/realloc()/free(). 2) suivre toutes les écritures en mémoire
(lent). 3) suivre les appels à rand().
Maintenant l’algorithme: 1) suivre tous les blocs du heap qui sont modifiés entre le
1er et le 2nd appel à rand() depuis 0x10002770d; 2) à chaque fois qu’un bloc du
heap est libéré, afficher son contenu.
Suivre toutes les écritures en mémoire est lent, mais après le 2nd appel à rand(),
nous n’avons plus besoin de les suivre (puisque nous avons déjà obtenu une liste de
blocs intéressants à ce point), donc nous arrêtons.
Maintenant le code: https://beginners.re/current-tree/DBI/minesweeper/minesweeper3.
cpp.
Il s’avère que seulement 4 blocs de heap sont modifiés entre les deux premiers
appels à rand(), voici à quoi ils ressemblent:
free(0x20aa6360)
free() : we have this block in our records, size=0x28
0x20AA6360 : 36 00 00 00 4E 00 00 00-2D 00 00 00 29 00 00 00 "6...N...-...)⤦
Ç ..."
0x20AA6370 : 06 00 00 00 37 00 00 00-35 00 00 00 19 00 00 00 ⤦
Ç "....7...5......."
0x20AA6380 : 46 00 00 00 0B 00 00 00- "F....... ⤦
Ç "
...
free(0x20af9d10)
free() : we have this block in our records, size=0x18
0x20AF9D10 : 0A 00 00 00 0A 00 00 00-0A 00 00 00 00 00 00 00 ⤦
Ç "................"
0x20AF9D20 : 60 63 AA 20 00 00 00 00- "`c. .... ⤦
Ç "
...
free(0x20b28b20)
free() : we have this block in our records, size=0x140
0x20B28B20 : 02 00 00 00 03 00 00 00-04 00 00 00 05 00 00 00 ⤦
Ç "................"
0x20B28B30 : 07 00 00 00 08 00 00 00-0C 00 00 00 0D 00 00 00 ⤦
Ç "................"
0x20B28B40 : 0E 00 00 00 0F 00 00 00-10 00 00 00 11 00 00 00 ⤦
Ç "................"
1296
0x20B28B50 : 12 00 00 00 13 00 00 00-14 00 00 00 15 00 00 00 ⤦
Ç "................"
0x20B28B60 : 16 00 00 00 17 00 00 00-18 00 00 00 1A 00 00 00 ⤦
Ç "................"
0x20B28B70 : 1B 00 00 00 1C 00 00 00-1D 00 00 00 1E 00 00 00 ⤦
Ç "................"
0x20B28B80 : 1F 00 00 00 20 00 00 00-21 00 00 00 22 00 00 00 ".... ⤦
Ç ...!..."..."
0x20B28B90 : 23 00 00 00 24 00 00 00-25 00 00 00 26 00 00 00 "#...\$⤦
Ç ...%...&..."
0x20B28BA0 : 27 00 00 00 28 00 00 00-2A 00 00 00 2B 00 00 00 ⤦
Ç "'...(...*...+..."
0x20B28BB0 : 2C 00 00 00 2E 00 00 00-2F 00 00 00 30 00 00 00 ⤦
Ç ",......./...0..."
0x20B28BC0 : 31 00 00 00 32 00 00 00-33 00 00 00 34 00 00 00 ⤦
Ç "1...2...3...4..."
0x20B28BD0 : 38 00 00 00 39 00 00 00-3A 00 00 00 3B 00 00 00 ⤦
Ç "8...9...:...;..."
0x20B28BE0 : 3C 00 00 00 3D 00 00 00-3E 00 00 00 3F 00 00 00 ⤦
Ç "<...=...>...?..."
0x20B28BF0 : 40 00 00 00 41 00 00 00-42 00 00 00 43 00 00 00 "@...A...B...C⤦
Ç ..."
0x20B28C00 : 44 00 00 00 45 00 00 00-47 00 00 00 48 00 00 00 "D...E...G...H⤦
Ç ..."
0x20B28C10 : 49 00 00 00 4A 00 00 00-4B 00 00 00 4C 00 00 00 "I...J...K...L⤦
Ç ..."
0x20B28C20 : 4D 00 00 00 4F 00 00 00-50 00 00 00 50 00 00 00 "M...O...P...P⤦
Ç ..."
0x20B28C30 : 50 00 00 00 50 00 00 00-50 00 00 00 50 00 00 00 "P...P...P...P⤦
Ç ..."
0x20B28C40 : 50 00 00 00 50 00 00 00-50 00 00 00 50 00 00 00 "P...P...P...P⤦
Ç ..."
0x20B28C50 : 50 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 "P⤦
Ç ..............."
...
free(0x20af9cf0)
free() : we have this block in our records, size=0x18
0x20AF9CF0 : 43 00 00 00 50 00 00 00-10 00 00 00 20 00 74 00 "C...P....... ⤦
Ç .t."
0x20AF9D00 : 20 8B B2 20 00 00 00 00- " .. .... ⤦
Ç "
Nous voyons facilement que les plus gros blocs (avec une taille de 0x28 et 0x140)
sont juste des tableaux de valeurs jusqu’à ≈ 0x50. Attendez... 0x50 est 80 en repré-
sentation décimale. et 9*9=81 (configuration standard de Minesweeper).
Après une rapide investigation, j’ai trouvé que chaque élément 32-bit est en fait les
coordonnées d’une case. Une case est représentée en utilisant un seul nombre, c’est
un nombre dans un tableau-2D. Ligne et colonne de chaque mine sont décodées
comme ceci: row=n / WIDTH; col=n % HEIGHT;
1297
Lorsque j’ai essayé de décoder ces deux blocs les plus gros, j’ai obtenu ces cartes
de case:
try_to_dump_cells(). unique elements=0xa
......*..
..*......
.......*.
.........
.....*...
*.......*
**.......
.......*.
......*..
...
Il semble que le premier bloc soit juste une liste des mines placées, tandis que le
second bloc est une liste des cases libres, mais le second semble quelque peu désyn-
chroniser du premier, et une version inversée du premier ne coïncide que partielle-
ment. Néanmoins, la première carte est correcte - nous pouvons jeter un coup d’œil
dans le fichier de log alors que Minesweeper est encore chargé et presque toutes
les cases sont cachées, et cliquer tranquillement sur les cases marquées d’un point
ici.
Il semble donc que lorsque l’utilisateur clique pour l première fois quelque part, Mi-
nesweeper place les 10 mines, puis détruit le bloc avec leurs liste (peut-être copie-t-il
toutes les données dans un autre bloc avant?), donc nous pouvons les voir lors de
l’appel à free().
Un autre fait: la méthode Array<NodeType>::Add(NodeType) modifie les blocs que
nous avons observé, et est appelée depuis de nombreux endroits, Board::placeMines()
incluse. Mais c’est cool: je ne suis jamais allé dans les détails, tout a été résolu sim-
plement en utilisant PIN.
Les fichiers: https://beginners.re/current-tree/DBI/minesweeper.
10.2.4 Exercice
Essayez de comprendre comment le résultat de rand() est converti en coordonnée(s).
Pour blaguer, faite que rand() renvoie des résultats tels que les mines soient placées
en formant un symbole ou une figure.
1298
10.3 Compiler Pin
Compiler Pin pour Windows peut s’avérer délicat. Ceci est ma recette qui fonctionne.
• Décompacter le dernier Pin, disons, C:\pin-3.7\
• Installer le dernier Cygwin, dans, disons, c:\cygwin64
• Installer MSVC 2015 ou plus récent.
• Ouvrir le fichier C:\pin-3.7\source\tools\Config\makefile.default.rules,
remplacer mkdir -p $@ par /bin/mkdir -p $@
• (Si nécessaire) dans C:\pin-3.7\source\tools\SimpleExamples\makefile.rules,
ajouter votre pintool à la liste TEST_TOOL_ROOTS.
• Ouvrir ”VS2015 x86 Native Tools Command Prompt”. Taper:
cd c :\pin-3.7\source\tools\SimpleExamples
c :\cygwin64\bin\make all TARGET=ia32
• Lancer pintool:
c :\pin-3.7\pin.exe -t C :\pin-3.7\source\tools\SimpleExamples\obj-ia32⤦
Ç \XOR_ins.dll -- program.exe arguments
1299
Chapitre 11
Autres sujets
1300
agir. Elle peut donc être détectée en positionnant un point d’arrêt déclenché par la
lecture de la mémoire contenant le code.
tracer possède l’option BPM pour ce faire.
La partie du fichier au format PE qui contient les informations de relogement ( 6.5.2
on page 998) ne doivent pas être modifiées par les patchs car le chargeur Windows
risquerait d’écraser les modifications apportées. (Ces parties sont présentées sous
forme grisées dans Hiew, par exemple: fig.1.22).
En dernier ressort, il est possible d’effectuer des modifications qui contournent les
relogements, ou de modifier directement la table des relogements.
1301
Fig. 11.1: Statistiques du nombre d’arguments moyen d’une fonction
Par exemple, il n’existe pas d’opérateur de décalage cyclique dans les langages
C/C++. La plupart des CPUs supportent cependant des instructions de ce type. Pour
faciliter la vie des programmeurs, le compilateur MSVC propose de telles pseudo
1
fonctions _rotl() and _rotr() qui sont directement traduites par le compilateur vers
les instructions x86 ROL/ROR.
Les fonctions intrinsèques qui permettent de générer des instructions SSE en sont
un autre exemple.
La liste complète des fonctions intrinsèques proposées par le compilateur MSVC fi-
gurent dans le MSDN.
1. MSDN
1302
11.4 Anomalies des compilateurs
11.4.1 Oracle RDBMS 11.2 et Intel C++ 10.1
Le compilateur Intel C++ 10.1, qui a été utilisé pour la compilation de Oracle RDBMS
11.2 pour Linux86, émettait parfois deux instructions JZ successives, sans que la
seconde instruction soit jamais référencée. Elle était donc inutile.
Listing 11.1: kdli.o from libserver11.a
.text :08114CF1 loc_8114CF1 : ;
CODE XREF: __PGOSF539_kdlimemSer+89A
.text :08114CF1 ; __PGOSF539_kdlimemSer+3994
.text :08114CF1 8B 45 08 mov eax, [ebp+arg_0]
.text :08114CF4 0F B6 50 14 movzx edx, byte ptr [eax+14h]
.text :08114CF8 F6 C2 01 test dl, 1
.text :08114CFB 0F 85 17 08 00 00 jnz loc_8115518
.text :08114D01 85 C9 test ecx, ecx
.text :08114D03 0F 84 8A 00 00 00 jz loc_8114D93
.text :08114D09 0F 84 09 08 00 00 jz loc_8115518
.text :08114D0F 8B 53 08 mov edx, [ebx+8]
.text :08114D12 89 55 FC mov [ebp+var_4], edx
.text :08114D15 31 C0 xor eax, eax
.text :08114D17 89 45 F4 mov [ebp+var_C], eax
.text :08114D1A 50 push eax
.text :08114D1B 52 push edx
.text :08114D1C E8 03 54 00 00 call len2nbytes
.text :08114D21 83 C4 08 add esp, 8
Il s’agit probablement d’un bug du générateur de code du compilateur qui ne fut pas
découvert durant les tests de celui-ci car le code produit fonctionnait conformément
aux résultats attendus.
Un autre exemple tiré d’Oracle RDBMS 11.1.0.6.0 pour win32.
.text :0051FBF8 85 C0 test eax, eax
.text :0051FBFA 0F 84 8F 00 00 00 jz loc_51FC8F
.text :0051FC00 74 1D jz short loc_51FC1F
1303
11.4.2 MSVC 6.0
Je viens juste de trouver celui-ci dans un vieux fragment de code :
fabs
fild [esp+50h+var_34]
fabs
fxch st(1) ; première instruction
fxch st(1) ; seconde instruction
faddp st(1), st
fcomp [esp+50h+var_3C]
fnstsw ax
test ah, 41h
jz short loc_100040B7
1304
.text :0000005F fstp dword ptr [esp]
.text :00000062 mov ecx, [esp]
.text :00000065 xor ecx, 80000000h
.text :0000006B add ecx, 7FFFFFFFh
.text :00000071 adc eax, 0
.text :00000074 mov edx, [esp+14h]
.text :00000078 adc edx, 0
.text :0000007B jmp short localexit
.text :0000007D ; ⤦
Ç ---------------------------------------------------------------------------⤦
Ç
.text :0000007D
.text :0000007D positive : ; CODE XREF : ⤦
Ç __ftol2+27
.text :0000007D fstp dword ptr [esp]
.text :00000080 mov ecx, [esp]
.text :00000083 add ecx, 7FFFFFFFh
.text :00000089 sbb eax, 0
.text :0000008C mov edx, [esp+14h]
.text :00000090 sbb edx, 0
.text :00000093 jmp short localexit
.text :00000095 ; ⤦
Ç ---------------------------------------------------------------------------⤦
Ç
.text :00000095
.text :00000095 integer_QnaN_or_zero : ; CODE XREF : ⤦
Ç __ftol2+21
.text :00000095 mov edx, [esp+14h]
.text :00000099 test edx, 7FFFFFFFh
.text :0000009F jnz short arg_is_not_integer_QnaN
.text :000000A1 fstp dword ptr [esp+18h] ; first
.text :000000A5 fstp dword ptr [esp+18h] ; second
.text :000000A9
.text :000000A9 localexit : ; CODE XREF : ⤦
Ç __ftol2+45
.text :000000A9 ; __ftol2+5D
.text :000000A9 leave
.text :000000AA retn
.text :000000AA __ftol2 endp
Notez les deux FSTP-s (stocker un float avec pop) identiques à la fin. D’abord, j’ai
cru qu’il s’agissait d’une anomalie du compilateur (je collectionne de tels cas tout
comme certains collectionnent les papillons), mais il semble qu’il s’agisse d’un mor-
ceau d’assembleur écrit à la main, dans msvcrt.lib il y a un fichier objet avec cette
fonction dedans et on peut y trouver cette chaîne:
f:\dd\vctools\crt_bld\SELF_X86\crt\prebuild\tran\i386\ftol2.asm — qui est
sans doute un chemin vers le fichier sur l’ordinateur du développeur où msvcrt.lib a
été généré.
Donc, bogue, typo induite par l’éditeur de texte ou fonctionnalité?
1305
11.4.4 Résumé
Des anomalies constatées dans d’autres compilateurs figurent également dans ce
livre: 1.28.2 on page 404, 3.10.3 on page 645, 3.18.7 on page 699, 1.26.7 on page 385, 1.18.4
on page 193, 1.28.5 on page 426.
Ces cas sont exposés dans ce livre afin de démontrer que ces compilateurs com-
portent leurs propres erreurs et qu’il convient de ne pas toujours se torturer le cer-
veau en tentant de comprendre pourquoi le compilateur a généré un code aussi
étrange.
11.5 Itanium
Bien qu’elle n’ai pas réussi à percer, l’architecture Intel Itanium (IA64) est très inté-
ressante.
Là où les CPUs OOE réarrangent les instructions afin de les exécuter en parallèle,
l’architecture EPIC2 a constitué une tentative pour déléguer cette décision au com-
pilateur.
Les compilateurs en question étaient évidemment particulièrement complexes.
Voici un exemple de code pour l’architecture IA64 qui implémente un algorithme de
chiffrage simple du noyau Linux:
y = le32_to_cpu(in[0]) ;
z = le32_to_cpu(in[1]) ;
k0 = ctx->KEY[0];
k1 = ctx->KEY[1];
k2 = ctx->KEY[2];
k3 = ctx->KEY[3];
n = TEA_ROUNDS ;
1306
}
out[0] = cpu_to_le32(y) ;
out[1] = cpu_to_le32(z) ;
}
1307
013C|00 00 04 00 nop.i 0;;
0140|0B 48 28 16 0F 20 xor r9 = r10, r11 ;;
0146|60 B9 24 1E 40 00 xor r22 = r23, r9
014C|00 00 04 00 nop.i 0;;
0150|11 00 00 00 01 00 nop.m 0
0156|E0 70 58 00 40 A0 add r14 = r14, r22
015C|A0 FF FF 48 br.cloop.sptk.few loc_F0 ;;
0160|09 20 3C 42 90 15 st4 [r33] = r15, 4 // store z
0166|00 00 00 02 00 00 nop.m 0
016C|20 08 AA 00 mov.i ar.lc = r2 ;; // restore lc ⤦
Ç register
0170|11 00 38 42 90 11 st4 [r33] = r14 // store y
0176|00 00 00 02 00 80 nop.i 0
017C|08 00 84 00 br.ret.sptk.many b0 ;;
Nous constatons tout d’abord que toutes les instructions IA64 sont regroupées par
3.
Chaque groupe représente 16 octets (128 bits) et se décompose en une catégorie
de code sur 5 bits puis 3 instructions de 41 bits chacune.
Dans IDA les groupes apparaissent sous la forme 6+6+4 octets —le motif est facile-
ment reconnaissable.
En règle générale les trois instructions d’un groupe s’exécutent en parallèle, sauf si
l’une d’elles est associée à un «stop bit ».
Il est probable que les ingénieurs d’Intel et de HP ont collecté des statistiques qui
leur ont permis d’identifier les motifs les plus fréquents. Ils auraient alors décidé
d’introduire une notion de type de groupe (AKA «templates »). Le type du groupe
définit la catégorie des instructions qu’il contient. Ces catégories sont au nombre de
12.
Par exemple, un groupe de type 0 représente MII. Ceci signifie que la première
instruction est une lecture ou écriture en mémoire (M), la seconde et la troisième
sont des manipulations d’entiers (I).
Un autre exemple est le groupe de type 0x1d: MFB. La première instruction est la
aussi de type mémoire (M), la seconde manipule un nombre flottant (F instruction
FPU), et la dernière est un branchement (B).
Lorsque le compilateur ne parvient pas à sélectionner une instruction à inclure dans
le groupe en cours de construction, il utilise une instruction de type NOP. Il existe
donc des instructions nop.i pour remplacer ce qui devrait être une manipulation
d’entier. De même un nop.m est utilisé pour combler un trou là où une instruction
de type mémoire devrait se trouver.
Lorsque le programme est directement rédigé en assembleur, les instructions NOPs
sont ajoutées de manière automatique.
Et ce n’est pas tout. Les groupes font eux-mêmes l’objet de regroupements.
Chaque instruction peut être marquée avec un «stop bit ». Le processeur exécute
en parallèle toutes les instructions, jusqu’à ce qu’il rencontre un «stop bit ».
1308
En pratique, les processeurs Itanium 2 peuvent exécuter jusqu’à deux groupes si-
multanément, soit un total de 6 instructions en parallèle.
Il faut évidemment que les instructions exécutées en parallèle, n’aient pas d’effet
de bord entre elles. Dans le cas contraire, le résultat n’est pas défini. Le compilateur
doit respecter cette contrainte ainsi que le nombre maximum de groupes simultanés
du processeur cible en plaçant les «stop bit » au bon endroit.
En langage d’assemblage, les bits d’arrêt sont identifiés par deux point-virgule situés
après l’instruction.
Ainsi dans notre exemple les instructions [90-ac] peuvent être exécutées simultané-
ment. Le prochain groupe est [b0-cc].
Nous observons également un bit d’arrêt en 10c. L’instruction suivante comporte
elle aussi un bit d’arrêt.
Ces deux instructions doivent donc être exécutées isolément des autres, (comme
dans une architecture CISC).
En effet, l’instruction en 110 utilise le résultat produit par l’instruction précédente
(valeur du registre r26). Les deux instructions ne peuvent s’exécuter simultanément.
Il semble que le compilateur n’ai pas trouvé de meilleure manière de paralléliser
les instructions, ou en d’autres termes, de plus charger la CPU. Les bits d’arrêt sont
donc en trop grand nombre.
La rédaction manuelle de code en assembleur est une tâche pénible. Le program-
meur doit effectuer lui-même les regroupements d’instructions.
Bien sûr, il peut ajouter un bit d’arrêt à chaque instruction mais cela dégrade les
performances telles qu’elles ont été pensée pour l’Itanium.
Les codes source du noyau Linux contiennent un exemple intéressant d’un code
assembleur produit manuellement pour IA64 :
http://lxr.free-electrons.com/source/arch/ia64/lib/.
On trouvera une introduction à l’assembleur Itanium dans : [Mike Burrell, Writing
Efficient Itanium 2 Assembly Code (2010)]3 , [papasutra of haquebright, WRITING
SHELLCODE FOR IA-64 (2001)]4 .
Deux autres caractéristiques très intéressantes d’Itanium sont l’exécution spécula-
tive et le bit NaT («not a thing ») qui ressemble un peu aux nombres NaN :
MSDN.
1309
Le 8086/8088 était un CPU 16-bit, mais était capable d’accèder à des adresses mé-
moire sur 20-bit (il était donc capable d’accèder 1MB de mémoire externe).
L’espace de la mémoire externe était divisé entre la RAM (max 640KB), la ROM, la
fenêtre pour la mémoire vidéo, les cartes EMS, etc.
Rappelons que le 8086/8088 était en fait un descendant du CPU 8-bit 8080.
Le 8080 avait un espace mémoire de 16-bit, i.e., il pouvait seulement adresser 64KB.
Et probablement pour une raison de portage de vieux logiciels5 , le 8086 peut sup-
porter plusieurs fenêtres de 64KB simultanément, situées dans l’espace d’adresse
de 1MB.
C’est une sorte de virtualisation de niveau jouet.
Tous les registres 8086 sont 16-bit, donc pour adresser plus, des registres spéciaux
de segment (CS, DS, ES, SS) ont été ajoutés.
Chaque pointeur de 20-bit est calculé en utilisant les valeurs d’une paire de registres,
de segment et d’adresse (p.e. DS:BX) comme suit:
real_address = (segment_register ≪ 4) + address_register
Par exemple, la fenêtre de RAM graphique (EGA6 , VGA7 ) sur les vieux compatibles
IBM-PC a une taille de 64KB.
Pour y accèder, une valeur de 0xA000 doit être stockée dans l’un des registres de
segments, p.e. dans DS.
Ainsi DS:0 adresse le premier octet de la RAM vidéo et DS:0xFFFF — le dernier octet
de RAM.
L’adresse réelle sur le bus d’adresse de 20-bit, toutefois, sera dans l’intervalle 0xA0000
à 0xAFFFF.
Le programme peut contenir des adresses codées en dur comme 0x1234, mais l’OS
peut avoir besoin de le charger à une adresse arbitraire, donc il recalcule les valeurs
du registre de segment de façon à ce que le programme n’ait pas à se soucier de
l’endroit où il est placé dans la RAM.
Donc, tout pointeur dans le vieil environnement MS-DOS consistait en fait en un seg-
ment d’adresse et une adresse dans ce segment, i.e., deux valeurs 16-bit. 20-bit
étaient suffisants pour cela, cependant nous devions recalculer les adresses très
souvent: passer plus d’informations par la pile semblait un meilleur rapport espace/-
facilité.
À propos, à cause de tout cela, il n’était pas possible d’allouer un bloc de mémoire
plus large que 64KB.
Les registres de segment furent réutilisés sur les 80286 comme sélecteurs, servant
a une fonction différente.
5. Je ne suis pas sûr à 100% de ceci
6. Enhanced Graphics Adapter
7. Video Graphics Array
1310
Lorsque les CPU 80386 et les ordinateurs avec plus de RAM ont été introduits, MS-
DOS était encore populaire, donc des extensions pour DOS ont émergés: ils étaient
en fait une étape vers un OS «sérieux », basculant le CPU en mode protégé et fournis-
sant des APIs mémoire bien meilleures pour les programmes qui devaient toujours
fonctionner sous MS-DOS.
Des examples très populaires incluent DOS/4GW (le jeux vidéo DOOM a été compilé
pour lui), Phar Lap, PMODE.
À propos, la même manière d’adresser la mémoire était utilisée dans la série 16-bit
de Windows 3.x, avant Win32.
; address 0x6030D86A
db 66h
nop
push ebp
mov ebp, esp
mov edx, [ebp+0Ch]
test edx, edx
jz short loc_6030D884
mov eax, [edx+30h]
test eax, 400h
jnz __VInfreq__skgfsync ; write to log
continue :
mov eax, [ebp+8]
1311
mov edx, [ebp+10h]
mov dword ptr [eax], 0
lea eax, [edx+0Fh]
and eax, 0FFFFFFFCh
mov ecx, [eax]
cmp ecx, 45726963h
jnz error ; exit with error
mov esp, ebp
pop ebp
retn
_skgfsync endp
...
; address 0x60B953F0
__VInfreq__skgfsync :
mov eax, [edx]
test eax, eax
jz continue
mov ecx, [ebp+10h]
push ecx
mov ecx, [ebp+8]
push edx
push ecx
push offset ... ;
"skgfsync(se=0x%x, ctx=0x%x, iov=0x%x)\n"
push dword ptr [edx+4]
call dword ptr [eax] ; write to log
add esp, 14h
jmp continue
error :
mov edx, [ebp+8]
mov dword ptr [edx], 69AAh ; 27050 "function called with
invalid FIB/IOV structure"
mov eax, [eax]
mov [edx+4], eax
mov dword ptr [edx+8], 0FA4h ; 4004
mov esp, ebp
pop ebp
retn
; END OF FUNCTION CHUNK FOR _skgfsync
1312
exécuté très souvent durant les tests effectués par les développeurs Oracle lors de
la collecte des statistiques. Il n’est même pas dit qu’elle ait jamais été exécutée.
Le bloc élémentaire qui écrit dans le journal s’achève par un retour à la partie «hot »
de la fonction.
Un autre bloc élémentaire «infrequent » est celui qui retourne le code erreur 27050.
Pour ce qui est des fichiers Linux au format ELF, le compilateur Intel C++ déplace
tous les fragments de code rarement exécutés vers une section séparée nommée
text.unlikely. Les fragments les plus souvent exécutés sont quant à eux regrou-
pés dans la section text.hot.
Cette information peut aider le rétro ingénieur à distinguer la partie principale d’une
fonction des parties qui assurent la gestion d’erreurs.
mov eax, 1
retn
l01 :
mov eax, 2
retn
f endp
1313
result = 2;
else
result = 1;
return result ;
}
f proc near
call f1
retn
l01 :
call f2
retn
f endp
...
Résultat:
1314
int __cdecl f(float a1, float a2, float a3, float a4)
{
double v5 ; // st7@1
char v6 ; // c0@1
int result ; // eax@2
v5 = a4 ;
if ( v6 )
result = f2(v5) ;
else
result = f1(v5) ;
return result ;
}
cdq
xor eax, edx
sub eax, edx
; EAX=abs(abs(a1)-a2)
retn
f endp
Résultat:
int __cdecl f(int a1, int a2)
{
__int64 v2 ; // rax@1
1315
v2 = abs(a1) - a2 ;
return (HIDWORD(v2) ^ v2) - HIDWORD(v2) ;
}
Peut-être est-ce le résultat de l’instruction CDQ ? Je ne suis pas sûr. Quoiqu’il en soit,
à chaque fois que vous voyez le type __int64 dans du code 32-bit, soyez attentif.
Ceci est aussi bizarre:
f proc near
mov eax, 2
retn
l00 :
mov eax, 1
retn
f endp
Résultat:
signed int __cdecl f(signed int a1)
{
signed int result ; // eax@3
retn
1316
f endp
Résultat:
int __cdecl f(int a1)
{
return (unsigned __int64)(715827883i64 * a1) >> 32;
}
11.8.3 Silence
extrn some_func :dword
f proc near
; use ECX
mov eax, ecx
retn
f endp
Résultat:
int __cdecl f(int a1, int a2)
{
int v2 ; // ecx@1
some_func(a2) ;
return v2 ;
}
La variable v2 (de ECX) est perdue …Oui, ce code est incorrect (la valeur de ECX
n’est pas sauvée lors de l’appel d’une autre fonction), mais il serait bon que Hex-
Rays donne un warning.
Un autre:
extrn some_func :dword
f proc near
1317
call some_func
jnz l01
mov eax, 1
retn
l01 :
mov eax, 2
retn
f endp
Résultat:
signed int f()
{
char v0 ; // zf@1
signed int result ; // eax@2
some_func() ;
if ( v0 )
result = 1;
else
result = 2;
return result ;
}
11.8.4 Virgule
La virgule en C/C++ a mauvaise presse, car elle peut conduire à du code confus.
Quiz rapide, que renvoie cette fonction C/C++ ?
int f()
{
return 1, 2;
};
C’est 2: lorsque le compilateur rencontre une expression avec des virgules, il génère
du code qui exécute toutes les sous-expressions, et renvoie la valeur de la dernière.
J’ai vu quelque chose comme ça dans du code en production:
if (cond)
return global_var=123, 456; // 456 is returned
else
return global_var=789, 321; // 321 is returned
1318
Il semble que le programmeur voulait rendre le code plus court sans parenthèses
supplémentaires. Autrement dit, la virgule permet de grouper plusieurs expressions
en une seule, sans déclaration/bloc de code dans des parenthèses.
La virgule en C/C++ est proche du begin en Scheme/Racket: https://docs.racket-lang.
org/guide/begin.html.
Peut-être que le seul usage largement accepté de la virgule est dans les déclarations
for() :
char *s="hello, world";
for(int i=0; *s ; s++, i++) ;
; i = string lenght
Ceci est correct, compile et fonctionne, et Dieu puisse vous aider à la comprendre.
La voici récrite:
if (cond1 || (comma_expr, cond2))
{
...
Le raccourci est effectif ici: d’abord cond1 est testé, si c’est true, le corps du if()
est exécuté, le reste de l’expression du if() est complètement ignoré. Si cond1 est
false, comma_expr est exécuté (dans l’exemple précédent, a est copié dans c), puis
cond2 est testée. Si cond2 est true, le corps du if() est exécuté, ou pas. Autrement
dit, le corps du if() est exécuté si cond1 est true ou si cond2 est true, mais si ce
dernier est true, comma_expr est aussi exécutée.
Maintenant, vous pouvez voir pourquoi la virgule est si célèbre.
Un mot sur les raccourcis. Une idée fausse répandue de débutant est que les
sous-conditions sont testées dans un ordre indéterminé, ce qui n’est pas vrai. Dans
l’expression a | b | c, a, b et c sont évalués dans un ordre indéterminé, donc c’est
pourquoi || a été ajouté à C/C++, pour appliquer des raccourcis explicitement.
1319
Un autre problème se pose avec les fonctions trop grosses, où un slot unique dans la
pile locale peut être utilisé par plusieurs variables durant l’exécution de la fonction.
Ce n’est pas un cas rare que lorsqu’un slot est utilisé pour une variable int, puis un
pointeur, puis une variable float. Hex-Rays le décompile correctement: il créé une
variable avec le même type, puis la caste sur un autre type dans diverses parties de
la fonction. J’ai résolu ce problème en découpant les grosses fonctions en plusieurs
plus petites. Met les variables locales comme des globales, etc., etc. Et n’oubliez pas
les tests.
Out[2]= Experimental`OptimizedExpression[
Block[{Compile`$1, Compile`$2}, Compile`$1 = v30 <= 5;
Compile`$2 =
v27 != -1; ! (v38 && Compile`$1 &&
Compile`$2) && ((! (v38 && Compile`$1) && Compile`$2) ||
v24 >= 5 || v26) && v25]]
1320
void f(int a, int b, int c, int d)
{
if (a>0 && b>0)
printf ("both a and b are positive\n") ;
else if (c>0 && d>0)
printf ("both c and d are positive\n") ;
else
printf ("something else\n") ;
};
…ça semble assez anodin, lorsque c’est compilé avec GCC 5.4.0 x64 avec optimisa-
tion:
; int __fastcall f(int a, int b, int c, int d)
public f
f proc near
test edi, edi
jle short loc_8
test esi, esi
jg short loc_30
loc_8 :
test edx, edx
jle short loc_20
test ecx, ecx
jle short loc_20
mov edi, offset s ; "both c and d are positive"
jmp puts
loc_20 :
mov edi, offset aSomethingElse ; "something else"
jmp puts
loc_30 :
mov edi, offset aAAndBPositive ; "both a and b are
positive"
loc_35 :
jmp puts
f endp
…ça semble donc anodin, mais Hex-Rays 2.2.0 n’arrive pas vraiment à détecter
qu’en fait des opérations double AND ont été utilisées dans le code source:
int __fastcall f(int a, int b, int c, int d)
{
int result ;
1321
{
result = puts("something else") ;
}
else
{
result = puts("both c and d are positive") ;
}
return result ;
}
Lequel est le meilleur? Je ne sais pas encore, mais pour une meilleure compréhension,
c’est bien de regarder les deux.
1322
11.8.9 Résumé
Quoiqu’il en soit, la qualité de Hex-Rays 2.2.0 est très, très bonne. Il rend la vie plus
facile.
1323
Functions should be no longer than 60 lines of text and define no
more than 6 parameters.
A function should not be longer than what can be printed on a single
sheet of paper in a standard reference format with one line per state-
ment and one line per declaration. Typically, this means no more than
about 60 lines of code per function. Long lists of function parameters
similarly compromise code clarity and should be avoided.
Each function should be a logical unit in the code that is unders-
tandable and verifiable as a unit. It is much harder to understand a
logical unit that spans multiple screens on a computer display or mul-
tiple pages when printed. Excessively long functions are often a sign
of poorly structured code.
1324
Voici le code source de certaines d’entre elles: do_check(), ext4_fill_super(), do_blockdev_direct_IO(),
do_jit().
Fonctions les plus complexes du fichier ntoskrnl.exe de Windows 7:
140569400 sub_140569400 edges=3070 nodes=1889 rets=1 E-N+2=1183 E-N+rets⤦
Ç =1182
14007c640 MmAccessFault edges=2256 nodes=1424 rets=1 E-N+2=834 E-N+rets=833
1401a0410 FsRtlMdlReadCompleteDevEx edges=1241 nodes=752 rets=1 E-N+2=491 E⤦
Ç -N+rets=490
14008c190 MmProbeAndLockPages edges=983 nodes=623 rets=1 E-N+2=362 E-N+rets⤦
Ç =361
14037fd10 ExpQuerySystemInformation edges=995 nodes=671 rets=1 E-N+2=326 E-⤦
Ç N+rets=325
140197260 MmProbeAndLockSelectedPages edges=875 nodes=551 rets=1 E-N+2=326 ⤦
Ç E-N+rets=325
140362a50 NtSetInformationProcess edges=880 nodes=586 rets=1 E-N+2=296 E-N+⤦
Ç rets=295
....
1325
Chapitre 12
12.1.2 Windows
• Mark Russinovich, Microsoft Windows Internals
• Peter Ferrie – The “Ultimate” Anti-Debugging Reference1
Blogs:
• Microsoft: Raymond Chen
1. http://pferrie.host22.com/papers/antidebug.pdf
1326
• nynaeve.net
12.1.3 C/C++
• Brian W. Kernighan, Dennis M. Ritchie, The C Programming Language, 2ed,
(1988)
• ISO/IEC 9899:TC3 (C C99 standard), (2007)2
• Bjarne Stroustrup, The C++ Programming Language, 4th Edition, (2013)
• C++11 standard3
• Agner Fog, Optimizing software in C++ (2015)4
• Marshall Cline, C++ FAQ5
• Dennis Yurichev, C/C++ programming language notes6
• JPL Institutional Coding Standard for the C Programming Language7
12.1.5 ARM
• Manuels ARM13
2. Aussi disponible en http://www.open-std.org/jtc1/sc22/WG14/www/docs/n1256.pdf
3. Aussi disponible en http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3690.pdf.
4. Aussi disponible en http://agner.org/optimize/optimizing_cpp.pdf.
5. Aussi disponible en http://www.parashift.com/c++-faq-lite/index.html
6. Aussi disponible en http://yurichev.com/C-book.html
7. Aussi disponible en https://yurichev.com/mirrors/C/JPL_Coding_Standard_C.pdf
8. Aussi disponible en http://www.intel.com/content/www/us/en/processors/
architectures-software-developer-manuals.html
9. Aussi disponible en http://developer.amd.com/resources/developer-guides-manuals/
10. Aussi disponible en http://agner.org/optimize/microarchitecture.pdf
11. Aussi disponible en http://www.agner.org/optimize/calling_conventions.pdf
12. Aussi disponible en https://github.com/jagregory/abrash-black-book
13. Aussi disponible en http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.
subset.architecture.reference/index.html
1327
• ARM(R) Architecture Reference Manual, ARMv7-A and ARMv7-R edition, (2012)
• [ARM Architecture Reference Manual, ARMv8, for ARMv8-A architecture profile,
(2013)]14
• Advanced RISC Machines Ltd, The ARM Cookbook, (1994)15
12.1.7 Java
[Tim Lindholm, Frank Yellin, Gilad Bracha, Alex Buckley, The Java(R) Virtual Machine
Specification / Java SE 7 Edition] 16 .
12.1.8 UNIX
Eric S. Raymond, The Art of UNIX Programming, (2003)
12.1.10 Cryptographie
• Bruce Schneier, Applied Cryptography, (John Wiley & Sons, 1994)
• (Free) lvh, Crypto 10117
• (Free) Dan Boneh, Victor Shoup, A Graduate Course in Applied Cryptography18 .
1328
Chapitre 13
Communautés
1. Reverse Engineering
2. freenode.net
1329
Épilogue
1330
13.1 Des questions ?
Pour toute question, n’hésitez pas à m’envoyer un mail :
<first_name @ last_name . com> / <first_name . last_name @ gmail . com>.
Vous avez une suggestion ou une proposition de contenu supplémentaire pour le
livre ? N’hésitez pas à envoyer toute correction (grammaire incluse), etc...
Je travaille beaucoup sur cette œuvre, c’est pourquoi les numéros de pages et les
numéros de parties peuvent changer rapidement. S’il vous plaît, ne vous fiez pas à
ces derniers si vous m’envoyez un email. Il existe une méthode plus simple : faites
une capture d’écran de la page, puis dans un éditeur graphique, soulignez l’endroit
où il y a une erreur et envoyez-moi l’image. Je la corrigerai plus rapidement. Et si
vous êtes familier avec git et LATEX vous pouvez corriger l’erreur directement dans le
code source :
https://beginners.re/src/.
N’hésitez surtout pas à m’envoyer la moindre erreur que vous pourriez trouver, aussi
petite soit-elle, même si vous n’êtes pas certain que ce soit une erreur. J’écris pour
les débutants après tout, il est donc crucial pour mon travail d’avoir les retours de
débutants.
1331
Appendice
1332
.1 x86
.1.1 Terminologie
Commun en 16-bit (8086/80286), 32-bit (80386, etc.), 64-bit.
octet 8-bit. La directive d’assembleur DB est utilisée pour définir les variables et les
tableaux d’octets. Les octets sont passés dans les parties 8-bit des registres:
AL/BL/CL/DL/AH/BH/CH/DH/SIL/DIL/R*L.
mot 16-bit. directive assembleur DW —”—. Les mots sont passés dans la partie 16-
bit des registres:
AX/BX/CX/DX/SI/DI/R*W.
double mot («dword ») 32-bit. directive assembleur DD —”—. Les double mots sont
passés dans les registres (x86) ou dans la partie 32-bit de registres (x64). Dans
du code 16-bit, les double mots sont passés dans une paire de registres 16-bit.
quadruple mot (« qword ») 64-bit. directive assembleur DQ —”—. En environne-
ment 32-bit, les quadruple mots sont passés dans une paire de registres 32-bit.
tbyte (10 bytes) 80-bit ou 10 octets (utilisé pour les registres FPU IEEE 754).
paragraph (16 bytes)—le terme était répandu dans l’environnement MS-DOS.
Des types de données de même taille (BYTE, WORD, DWORD) existent aussi dans
l’API Windows.
1333
RAX/EAX/AX/AL
Octet d’indice
7 6 5 4 3 2 1 0
RAXx64
EAX
AX
AH AL
AKA accumulateur Le résultat d’une fonction est en général renvoyé via ce registre.
RBX/EBX/BX/BL
Octet d’indice
7 6 5 4 3 2 1 0
RBXx64
EBX
BX
BH BL
RCX/ECX/CX/CL
Octet d’indice
7 6 5 4 3 2 1 0
RCXx64
ECX
CX
CH CL
AKA compteur: il est utilisé dans ce rôle avec les instructions préfixées par REP et
aussi dans les instructions de décalage (SHL/SHR/RxL/RxR).
RDX/EDX/DX/DL
Octet d’indice
7 6 5 4 3 2 1 0
RDXx64
EDX
DX
DH DL
RSI/ESI/SI/SIL
Octet d’indice
7 6 5 4 3 2 1 0
RSIx64
ESI
SI
SILx64
1334
AKA «source index ». Utilisé comme source dans les instructions REP MOVSx, REP
CMPSx.
RDI/EDI/DI/DIL
Octet d’indice
7 6 5 4 3 2 1 0
RDIx64
EDI
DI
DILx64
AKA « destination index ». Utilisé comme un pointeur sur la destination dans les
instructions REP MOVSx, REP STOSx.
R8/R8D/R8W/R8L
Octet d’indice
7 6 5 4 3 2 1 0
R8
R8D
R8W
R8L
R9/R9D/R9W/R9L
Octet d’indice
7 6 5 4 3 2 1 0
R9
R9D
R9W
R9L
R10/R10D/R10W/R10L
Octet d’indice
7 6 5 4 3 2 1 0
R10
R10D
R10W
R10L
1335
R11/R11D/R11W/R11L
Octet d’indice
7 6 5 4 3 2 1 0
R11
R11D
R11W
R11L
R12/R12D/R12W/R12L
Octet d’indice
7 6 5 4 3 2 1 0
R12
R12D
R12W
R12L
R13/R13D/R13W/R13L
Octet d’indice
7 6 5 4 3 2 1 0
R13
R13D
R13W
R13L
R14/R14D/R14W/R14L
Octet d’indice
7 6 5 4 3 2 1 0
R14
R14D
R14W
R14L
R15/R15D/R15W/R15L
Octet d’indice
7 6 5 4 3 2 1 0
R15
R15D
R15W
R15L
1336
RSP/ESP/SP/SPL
Octet d’indice
7 6 5 4 3 2 1 0
RSP
ESP
SP
SPL
AKA pointeur de pile. Pointe en général sur la pile courante excepté dans le cas où
il n’est pas encore initialisé.
RBP/EBP/BP/BPL
Octet d’indice
7 6 5 4 3 2 1 0
RBP
EBP
BP
BPL
AKA frame pointer. Utilisé d’habitude pour les variables locales et accéder aux argu-
ments de la fonction. En lire plus ici: ( 1.12.1 on page 93).
RIP/EIP/IP
Octet d’indice
7 6 5 4 3 2 1 0
RIPx64
EIP
IP
Ou:
PUSH value
RET
CS/DS/ES/SS/FS/GS
1337
FS dans win32 pointe sur TLS, GS prend ce rôle dans Linux. C’est fait pour accè-
der plus au TLS et autres structures comme le TIB.
Dans le passé, ces registres étaient utilisés comme registres de segments ( 11.6 on
page 1309).
Registre de flags
AKA EFLAGS.
1338
79 78 64 63 62 0
Mot de Contrôle
Les flags PM, UM, OM, ZM, DM, IM définissent si une exception est générée en cas
d’erreur correspondante.
Mot d’état
1339
Les bits SF, P, U, O, Z, D, I indiquent les exceptions.
Vous trouverez des précisions à propos de C3, C2, C1, C0 ici: ( 1.25.7 on page 302).
N.B.: Lorsque ST(x) est utilisé, le FPU ajoute x à TOP (modulo 8) et c’est ainsi qu’il
obtient le numéro du registre interne.
Mot Tag
SSE: 8 registres 128-bit: XMM0..XMM7. En x86-64 8 autres registres ont été ajoutés:
XMM8..XMM15.
AVX est l’extension de tous ces registres à 256 bits.
1340
• DR0 — adresse du point d’arrêt #1
• DR1 — adresse du point d’arrêt #2
• DR2 — adresse du point d’arrêt #3
• DR3 — adresse du point d’arrêt #4
• DR6 — la cause de l’arrêt est indiquée ici
• DR7 — les types de point d’arrêt sont mis ici
DR6
Bit (masque) Description
0 (1) B0 — le point d’arrêt #1 a été déclenché
1 (2) B1 — le point d’arrêt #2 a été déclenché
2 (4) B2 — le point d’arrêt #3 a été déclenché
3 (8) B3 — le point d’arrêt #4 a été déclenché
13 (0x2000) BD — tentative de modification d’un des registres DRx.
peut être déclenché si GD est activé
14 (0x4000) BS — point d’arrêt simple (le flag TF a été mis dans EFLAGS).
La plus haute priorité. D’autres bits peuvent être mis aussi.
15 (0x8000) BT (task switch flag)
N.B. Un point d’arrêt simple est un point d’arrêt qui se produit après chaque instruc-
tion. Il peut être enclenché en mettant le flag TF dans EFLAGS ( .1.2 on page 1338).
DR7
1341
Le type de point d’arrêt doit être mis comme suit (R/W) :
• 00 — exécution de l’instruction
• 01 — écriture de données
• 10 — lecture ou écriture I/O (non disponible en mode user)
• 11 — à la lecture ou l’écriture de données
N.B.: le type de point d’arrêt est absent pour la lecture de données, en effet.
.1.6 Instructions
Les instructions marquées avec un (M) ne sont généralement pas générées par le
compilateur: si vous rencontrez l’une d’entre elles, il s’agit probablement de code
assembleur écrit à la main, ou de fonctions intrinsèques ( 11.3 on page 1302).
Seules les instructions les plus fréquemment utilisées sont listées ici. Vous pouvez
lire 12.1.4 on page 1327 pour une documentation complète.
Devez-vous connaître tous les opcodes des instructions par cœur? Non, seulement
ceux qui sont utilisés pour patcher du code ( 11.1.1 on page 1300). Tout le reste des
opcodes n’a pas besoin d’être mémorisé.
Préfixes
LOCK force le CPU à faire un accès exclusif à la RAM dans un environnement multi-
processeurs. Par simplification, on peut dire que lorsqu’une instruction avec ce
préfixe est exécutée, tous les autres CPU dans un système multi-processeur
sont stoppés. Le plus souvent, c’est utilisé pour les sections critiques, les séma-
phores et les mutex. Couramment utilisé avec ADD, AND, BTR, BTS, CMPXCHG,
OR, XADD, XOR. Vous pouvez en lire plus sur les sections critiques ici ( 6.5.4 on
page 1035).
REP est utilisé avec les instructions MOVSx et STOSx: exécute l’instruction dans une
boucle, le compteur est situé dans le registre CX/ECX/RCX. Pour une description
plus détaillée de ces instructions, voir MOVSx ( .1.6 on page 1346) et STOSx
( .1.6 on page 1348).
Les instructions préfixées par REP sont sensibles au flag DF, qui est utilisé pour
définir la direction.
1342
REPE/REPNE (AKA REPZ/REPNZ) utilisé avec les instructions CMPSx et SCASx: exé-
cute la dernière instruction dans une boucle, le compteur est mis dans le re-
gistre CX/ECX/RCX. Elle s’arrête prématurément si ZF vaut 0 (REPE) ou si ZF
vaut 1 (REPNE).
Pour une description plus détaillée de ces instructions, voir CMPSx ( .1.6 on
page 1350) et SCASx ( .1.6 on page 1347).
Les instructions préfixées par REPE/REPNE sont sensibles au flag DF, qui est
utilisé pour définir la direction.
1343
JA AKA JNBE: saut si supérieur (non signé) : CF=0 et ZF=0
JBE saut si inférieur ou égal (non signé) : CF=1 ou ZF=1
JB AKA JC: saut si inférieur(non signé) : CF=1
JC AKA JB: saut si CF=1
JE AKA JZ: saut si égal ou zéro: ZF=1
JGE saut si supérieur ou égal (signé) : SF=OF
JG saut si supérieur (signé) : ZF=0 et SF=OF
JLE saut si inférieur ou égal (signé) : ZF=1 ou SF≠OF
JL saut si inférieur (signé) : SF≠OF
JNAE AKA JC: saut si non supérieur ou égal (non signé) : CF=1
JNA saut si non supérieur (non signé) : CF=1 et ZF=1
JNBE saut si non inférieur ou égal (non signé) : CF=0 et ZF=0
JNB AKA JNC: saut si non inférieur (non signé) : CF=0
JNC AKA JAE: saut si CF=0, synonyme de JNB.
JNE AKA JNZ: saut si non égal ou non zéro: ZF=0
JNGE saut si non supérieur ou égal (signé) : SF≠OF
JNG saut si non supérieur (signé) : ZF=1 ou SF≠OF
JNLE saut si non inférieur ou égal (signé) : ZF=0 et SF=OF
JNL saut si non inférieur (signé) : SF=OF
JNO saut si non débordement: OF=0
JNS saut si le flag SF vaut zéro
JNZ AKA JNE: saut si non égal ou non zéro: ZF=0
JO saut si débordement: OF=1
JPO saut si le flag PF vaut 0 (Jump Parity Odd)
JP AKA JPE : saut si le flag PF est mis
JS saut si le flag SF est mis
JZ AKA JE: saut si égal ou zéro: ZF=1
LAHF copie certains bits du flag dans AH:
7 6 4 2 0
SFZF AF PF CF
1344
LEA (Load Effective Address) forme une adresse
Cette instruction n’a pas été conçue pour sommer des valeurs et/ou les multi-
plier, mais pour former une adresse, e.g., pour calculer l’adresse d’un élément
d’un tableau en ajoutant l’adresse du tableau, l’index de l’élément multiplié par
la taille de l’élément5 .
Donc, la différence entre MOV et LEA est que MOV forme une adresse mémoire
et charge une valeur depuis la mémoire ou l’y stocke, alors que LEA forme
simplement une adresse.
Mais néanmoins, elle peut être utilisée pour tout autre calcul.
LEA est pratique car le calcul qu’elle effectue n’altère pas les flags du CPU.
Ceci peut être très important pour les processeurs OOE (afin de créer moins
de dépendances). À part ça, au moins à partir du Pentium, l’instruction LEA est
exécutée en 1 cycle.
int f(int a, int b)
{
return a*8+b ;
};
1345
MOVSB/MOVSW/MOVSD/MOVSQ copier l’octet/ le mot 16-bit/ le mot 32-bit/ le
mot 64-bit depuis l’adresse se trouvant dans SI/ESI/RSI vers celle se trouvant
dans DI/EDI/RDI.
Avec le préfixe REP, elle est répétée en boucle, le compteur étant stocker dans
le registre CX/ECX/RCX: ça fonctionne comme memcpy() en C. Si la taille du
bloc est connue pendant la compilation, memcpy() est souvent mise en ligne
dans un petit morceau de code en utilisant REP MOVSx, parfois même avec
plusieurs instructions.
L’équivalent de memcpy(EDI, ESI, 15) est:
; copier 15 octets de ESI vers EDI
CLD ; mettre la direction à en avant
MOV ECX, 3
REP MOVSD ; copier 12 octets
MOVSW ; copier 2 octets de plus
MOVSB ; copier l'octet restant
(Apparemment, c’est plus rapide que de copier 15 octets avec un seul REP
MOVSB).
MOVSX charger avec extension du signe voir aussi: ( 1.23.1 on page 263)
MOVZX icharger et effacer tous les autres bits voir aussi: ( 1.23.1 on page 264)
MOV charger une valeur. Le nom de cette instruction est inapproprié, ce qui en-
traîne des confusions (la donnée n’est pas déplacée, mais copiée), dans d’autres
architectures la même instruction est en général appelée «LOAD » et/ou «STORE »
ou quelque chose comme ça.
Une chose importante: si vous mettez la partie 16-bit basse d’un registre 32-bit
en mode 32-bit, les 16-bit haut restent comme ils étaient. Mais si vous modifiez
la partie 32-bit basse d’un registre en mode 64-bit, les 32-bits haut du registre
seront mis à zéro.
Peut-être que ça a été fait pour simplifier le portage du code sur x86-64.
MUL multiplier sans signe. IMUL est souvent utilisée au lieu de MUL, en lire plus
ici: 2.2.1 on page 587.
NEG négation: op = −op La même chose que NOT op / ADD op, 1.
NOP NOP. Son opcode est 0x90, qui est en fait l’instruction sans effet XCHG EAX,EAX.
Ceci implique que le x86 n’a pas d’instruction NOP dédiée (comme dans de nom-
breux RISC). Ce livre contient au moins un listing où GDB affiche NOP comme
l’instruction 16-bit XCHG: 1.11.1 on page 67.
Plus d’exemples de telles opérations: ( .1.7 on page 1359).
NOP peut être généré par le compilateur pour aligner des labels sur une limite
de 16-octets. Un autre usage très répandu de NOP est de remplacer manuelle-
ment (patcher) une instruction, comme un saut conditionnel, par NOP, afin de
désactiver cette exécution.
1346
NOT op1: op1 = ¬op1. inversion logique Caractéristique importante—l’instruction ne
change pas les flags.
OR «ou » logique
POP prend une valeur depuis la pile: value=SS:[ESP]; ESP=ESP+4 (ou 8)
PUSH pousse une valeur sur la pile: ESP=ESP-4 (ou 8) ; SS:[ESP]=value
RET Revient d’une sous-routine: POP tmp; JMP tmp.
En fait, RET est une macro du langage d’assemblage, sous les environnements
Windows et *NIX, elle est traduite en RETN («return near ») ou, du temps de
MS-DOS, où la mémoire était adressée différemment ( 11.6 on page 1309), en
RETF («return far »).
RET peut avoir un opérande. Alors il fonctionne comme ceci:
POP tmp; ADD ESP op1; JMP tmp. RET avec un opérande termine en général
les fonctions avec la convention d’appel stdcall, voir aussi: 6.1.2 on page 962.
SAHF copier des bits de AH vers les flags CPU:
7 6 4 2 0
SFZF AF PF CF
1347
add edi, 0FFFFFFFFh ; le corriger
not ecx
dec ecx
7 6 5 4 3 2 1 0
CF 7 6 5 4 3 2 1 0 0
7 6 5 4 3 2 1 0
0 7 6 5 4 3 2 1 0 CF
1348
MOV ECX, 3
REP STOSD ; écrit 12 octets
STOSW ; écrit 2 octets de plus
STOSB ; écrit l'octet restant
1349
facilitate the task of sign extension. These instructions were initial-
ly named SEX (sign extend) but were later renamed to the more
conservative CBW (convert byte to word) and CWD (convert word
to double word).
Listing 3: base\ntos\rtl\i386\movemem.asm
; ULONG
; RtlCompareMemory (
; IN PVOID Source1,
; IN PVOID Source2,
; IN ULONG Length
; )
;
; Routine Description:
;
; This function compares two blocks of memory and returns the number
; of bytes that compared equal.
;
; Arguments:
;
; Source1 (esp+4) - Supplies a pointer to the first block of memory to
; compare.
;
; Source2 (esp+8) - Supplies a pointer to the second block of memory to
; compare.
;
; Length (esp+12) - Supplies the Length, in bytes, of the memory to be
; compared.
;
; Return Value:
;
1350
; The number of bytes that compared equal is returned as the function
; value. If all bytes compared equal, then the length of the original
; block of memory is returned.
;
;--
CODE_ALIGNMENT
cPublicProc _RtlCompareMemory,3
cPublicFpo 3,0
;
; Compare dwords, if any.
;
;
; Compare residual bytes, if any.
;
;
; All bytes in the block match.
;
;
; When we come to rcm40, esi (and edi) points to the dword after the
1351
; one which caused the mismatch. Back up 1 dword and find the byte.
; Since we know the dword didn't match, we can assume one byte won't.
;
;
; When we come to rcm50, esi points to the byte after the one that
; did not match, which is TWO after the last byte that did match.
;
stdENDP _RtlCompareMemory
N.B.: cette fonction utilise une comparaison 32-bit (CMPSD) si la taille du bloc
est un multiple de 4, ou sinon une comparaison par octet (CMPSB).
CPUID renvoie des informations sur les fonctionnalités du CPU. Voir aussi: ( 1.30.6
on page 474).
DIV division non signée
IDIV division signée
INT (M) : INT x est similaire à PUSHF; CALL dword ptr [x*4] en environnement
16-bit. Elle était énormément utilisée dans MS-DOS, fonctionnant comme un
vecteur syscall. Les registres AX/BX/CX/DX/SI/DI étaient remplis avec les argu-
ments et le flux sautait à l’adresse dans la table des vecteurs d’interruption
(Interrupt Vector Table, située au début de l’espace d’adressage). Elle était ré-
pandue car INT a un opcode court (2 octets) et le programme qui a besoin d’un
service MS-DOS ne doit pas déterminer l’adresse du point d’entrée de ce ser-
vice. Le gestionnaire d’interruption renvoie le contrôle du flux à l’appelant en
utilisant l’instruction IRET.
Le numéro d’interruption les plus utilisé était 0x21, servant une grande partie
de on API. Voir aussi: [Ralf Brown Ralf Brown’s Interrupt List], pour les listes
d’interruption plus exhaustives et d’autres informations sur MS-DOS.
Durant l’ère post-MS-DOS, cette instruction était toujours utilisée comme un
syscall à la fois dans Linux et Windows ( 6.3 on page 981), mais fût remplacée
plus tard par les instructions SYSENTER ou SYSCALL.
INT 3 (M) : cette instruction est proche de INT, elle a son propre opcode d’1 octet
(0xCC), et est très utilisée pour le débogage. Souvent, les débogueurs écrivent
1352
simplement l’octet 0xCC à l’adresse du point d’arrêt à mettre, et lorsqu’une ex-
ception est levée, l’octet original est restauré et l’instruction originale à cette
adresse est ré-exécutée.
Depuis Windows NT, une exception EXCEPTION_BREAKPOINT est déclenchée lorsque
le CPU exécute cette instruction. Cet évènement de débogage peut être inter-
cepté et géré par un débogueur hôte, si il y en a un de chargé. S’il n’y en a pas
de charger, Windows propose de lancer un des débogueurs enregistré dans le
système. Si MSVS8 est installé, son débogueur peut être chargé et connecté
au processus. Afin de protéger contre le reverse engineering, de nombreuses
méthodes anti-débogage vérifient l’intégrité du code chargé.
MSVC possède une fonction intrinsèque pour l’instruction: __debugbreak()9 .
Il y a aussi une fonction win32 dans kernel32.dll appelée DebugBreak()10 , qui
exécute aussi INT 3.
IN (M) lire des données depuis le port. On trouve cette instruction dans les drivers
de l’OS ou dans de l’ancien code MS-DOS, par exemple ( 8.8.3 on page 1114).
IRET : était utilisée dans l’environnement MS-DOS pour retourner d’un gestionnaire
d’interruption appelé par l’instruction INT. Équivalent à POP tmp; POPF; JMP
tmp.
LOOP (M) décrémente CX/ECX/RCX, saute si il est toujours non zéro.
L’instruction LOOP était souvent utilisée dans le code DOS qui travaillait avec
des dispositifs externes. Pour ajouter un petit délai, on utilisait ceci:
MOV CX, nnnn
LABEL : LOOP LABEL
1353
7 6 5 4 3 2 1 0 CF
CF 7 6 5 4 3 2 1 0
CF 7 6 5 4 3 2 1 0
7 6 5 4 3 2 1 0 CF
7 6 5 4 3 2 1 0
CF 7 6 5 4 3 2 1 0
7 6 5 4 3 2 1 0
7 6 5 4 3 2 1 0 CF
En dépit du fait que presque presque tous les CPUs aient ces instructions, il n’y
a pas d’opération correspondante en C/C++, donc les compilateurs de ces LPs
ne génèrent en général pas ces instructions.
Par commodité pour le programmeur, au moins MSVC fourni les pseudo-fonctions
(fonctions intrinsèques du compilateur) _rotl() et _rotr()11 , qui sont traduites di-
rectement par le compilateur en ces instructions.
SAL Décalage arithmétique à gauche, synonyme de SHL
SAR Décalage arithmétique à droite
7 6 5 4 3 2 1 0
7 6 5 4 3 2 1 0 CF
1354
SETcc op: charge 1 dans l’opérande (octet seulement) si la condition est vraie et
zéro sinon. Les codes conditions sont les même que les instructions Jcc ( .1.6
on page 1343).
STC (M) met le flag CF
STD (M) Met le flag DF. Cette instruction n’est pas générée par les compilateurs et
est en général rare. Par exemple, elle peut être trouvée dans le fichier du noyau
de Windows ntoskrnl.exe, dans la routine de copie mémoire écrite à la main.
Instructions FPU
Le suffixe -R dans le mnémonique signifie en général que les opérandes sont inver-
sés, le suffixe -P implique qu’un élément est supprimé de la pile après l’exécution
de l’instruction, le suffixe -PP implique que deux éléments sont supprimés.
Les instructions -P sont souvent utiles lorsque nous n’avons plus besoin que la valeur
soit présente dans la pile FPU après l’opération.
FABS remplace la valeur dans ST(0) par sa valeur absolue
FADD op: ST(0)=op+ST(0)
FADD ST(0), ST(i) : ST(0)=ST(0)+ST(i)
FADDP ST(1)=ST(0)+ST(1) ; supprime un élément de la pile, i.e., les valeurs sur la
pile sont remplacées par leurs somme
FCHS ST(0)=-ST(0)
FCOM compare ST(0) avec ST(1)
FCOM op: compare ST(0) avec op
FCOMP compare ST(0) avec ST(1) ; supprime un élément de la pile
FCOMPP compare ST(0) avec ST(1) ; supprime deux éléments de la pile
1355
FDIVR op: ST(0)=op/ST(0)
FDIVR ST(i), ST(j) : ST(i)=ST(j)/ST(i)
FDIVRP op: ST(0)=op/ST(0) ; supprime un élément de la pile
FDIVRP ST(i), ST(j) : ST(i)=ST(j)/ST(i) ; supprime un élément de la pile
FDIV op: ST(0)=ST(0)/op
FDIV ST(i), ST(j) : ST(i)=ST(i)/ST(j)
FDIVP ST(1)=ST(0)/ST(1) ; supprime un élément de la pile, i.e, les valeurs du divi-
dende et du diviseur sont remplacées par le quotient
FILD op: convertit un entier n et le pousse sur la pile.
FIST op: convertit la valeur dans ST(0) en un entier dans op
FISTP op: convertit la valeur dans ST(0) en un entier dans op; supprime un élément
de la pile
FLD1 pousse 1 sur la pile
FLDCW op: charge le FPU control word ( .1.3 on page 1339) depuis le 16-bit op.
FLDZ pousse zéro sur la pile
FLD op: pousse op sur la pile.
FMUL op: ST(0)=ST(0)*op
FMUL ST(i), ST(j) : ST(i)=ST(i)*ST(j)
FMULP op: ST(0)=ST(0)*op; supprime un élément de la pile
FMULP ST(i), ST(j) : ST(i)=ST(i)*ST(j) ; supprime un élément de la pile
FSINCOS : tmp=ST(0) ; ST(1)=sin(tmp) ; ST(0)=cos(tmp)
√
FSQRT : ST (0) = ST (0)
FSTCW op: stocker le mot de contrôle FPU ( .1.3 on page 1339) dans l’op 16-bit
après avoir vérifié s’il y a des exceptions en attente.
FNSTCW op: stocker le mot de contrôle FPU ( .1.3 on page 1339) dans l’op 16-bit.
FSTSW op: stocker le mot d’état FPU ( .1.3 on page 1339) dans l’op 16-bit après
avoir vérifié s’il y a des exceptions en attente.
FNSTSW op: stocker le mot d’état FPU ( .1.3 on page 1339) dans l’op 16-bit.
FST op: copie ST(0) dans op
FSTP op: copie ST(0) dans op; supprime un élément de la pile
FSUBR op: ST(0)=op-ST(0)
FSUBR ST(0), ST(i) : ST(0)=ST(i)-ST(0)
FSUBRP ST(1)=ST(0)-ST(1) ; supprime un élément de la pile, i.e., la valeur dans la
pile est remplacée par la différence
1356
FSUB op: ST(0)=ST(0)-op
FSUB ST(0), ST(i) : ST(0)=ST(0)-ST(i)
FSUBP ST(1)=ST(1)-ST(0) ; supprime un élément de la pile, i.e., la valeur dans la
pile est remplacée par la différence
FUCOM ST(i) : compare ST(0) et ST(i)
FUCOM compare ST(0) et ST(1)
FUCOMP compare ST(0) et ST(1) ; supprime un élément de la pile.
FUCOMPP compare ST(0) et ST(1) ; supprime deux éléments de la pile.
L’instruction se comporte comme FCOM, mais une exception est levée seule-
ment si un opérande est SNaN, tandis que les nombres QNaN sont traités nor-
malement.
FXCH ST(i) échange les valeurs dans ST(0) et ST(i)
FXCH échange les valeurs dans ST(0) et ST(1)
1357
J 4a DEC
K 4b DEC
L 4c DEC
M 4d DEC
N 4e DEC
O 4f DEC
P 50 PUSH
Q 51 PUSH
R 52 PUSH
S 53 PUSH
T 54 PUSH
U 55 PUSH
V 56 PUSH
W 57 PUSH
X 58 POP
Y 59 POP
Z 5a POP
[ 5b POP
\ 5c POP
] 5d POP
^ 5e POP
_ 5f POP
` 60 PUSHA
a 61 POPA
f 66 en mode 32-bit, change pour une
taille d’opérande de 16-bit
g 67 en mode 32-bit, change pour une
taille d’adresse 16-bit
h 68 PUSH
i 69 IMUL
j 6a PUSH
k 6b IMUL
p 70 JO
q 71 JNO
r 72 JB
s 73 JAE
t 74 JE
u 75 JNE
v 76 JBE
w 77 JA
x 78 JS
y 79 JNS
z 7a JP
De même:
1358
f 66 en mode 32-bit, change pour une
taille d’opérande de 16-bit
g 67 en mode 32-bit, change pour une
taille d’adresse 16-bit
En résumé: AAA, AAS, CMP, DEC, IMUL, INC, JA, JAE, JB, JBE, JE, JNE, JNO, JNS, JO, JP,
JS, POP, POPA, PUSH, PUSHA, XOR.
.1.7 npad
C’est une macro du langage d’assemblage pour aligner les labels sur une limite
spécifique.
C’est souvent nécessaire pour des labels très utilisés, comme par exemple le début
d’un corps de boucle. Ainsi, le CPU peut charger les données ou le code depuis la
mémoire efficacement, à travers le bus mémoire, les caches, etc.
Pris de listing.inc (MSVC) :
À propos, c’est un exemple curieux des différentes variations de NOP. Toutes ces
instructions n’ont pas d’effet, mais ont une taille différente.
Avoir une seule instruction sans effet au lieu de plusieurs est accepté comme étant
meilleur pour la performance du CPU.
;; LISTING.INC
;;
;; This file contains assembler macros and is included by the files created
;; with the -FA compiler switch to be assembled by MASM (Microsoft Macro
;; Assembler).
;;
;; Copyright (c) 1993-2003, Microsoft Corporation. All rights reserved.
1359
add eax, DWORD PTR 0
else
if size eq 6
; lea ebx, [ebx+00000000]
DB 8DH, 9BH, 00H, 00H, 00H, 00H
else
if size eq 7
; lea esp, [esp+00000000]
DB 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H
else
if size eq 8
; jmp .+8; .npad 6
DB 0EBH, 06H, 8DH, 9BH, 00H, 00H, 00H, 00H
else
if size eq 9
; jmp .+9; .npad 7
DB 0EBH, 07H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H
else
if size eq 10
; jmp .+A; .npad 7; .npad 1
DB 0EBH, 08H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 90H
else
if size eq 11
; jmp .+B; .npad 7; .npad 2
DB 0EBH, 09H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8BH, 0FFH
else
if size eq 12
; jmp .+C; .npad 7; .npad 3
DB 0EBH, 0AH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 49H, 00H
else
if size eq 13
; jmp .+D; .npad 7; .npad 4
DB 0EBH, 0BH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 64H, 24⤦
Ç H, 00H
else
if size eq 14
; jmp .+E; .npad 7; .npad 5
DB 0EBH, 0CH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 05H, 00H, ⤦
Ç 00H, 00H, 00H
else
if size eq 15
; jmp .+F; .npad 7; .npad 6
DB 0EBH, 0DH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 9BH, ⤦
Ç 00H, 00H, 00H, 00H
else
%out error : unsupported npad size
.err
endif
endif
endif
endif
endif
endif
1360
endif
endif
endif
endif
endif
endif
endif
endif
endif
endm
.2 ARM
.2.1 Terminologie
ARM a été initialement développé comme un CPU 32-bit, c’est pourquoi ici un mot,
contrairement au x86, fait 32-bit.
octet 8-bit. La directive d’assemblage DB est utilisée pour définir des variables et
des tableaux d’octets.
demi-mot 16-bit. directive d’assemblage DCW —”—.
mot 32-bit. directive d’assemblage DCW —”—.
double mot 64-bit.
quadruple mot 128-bit.
.2.2 Versions
• ARMv4: Le mode Thumb mode a été introduit.
• ARMv6: Utilisé dans la 1ère génération d’iPhone, iPhone 3G (Samsung 32-bit
RISC ARM 1176JZ(F)-S qui supporte Thumb-2)
• ARMv7: Thumb-2 a été ajouté (2003). Utilisé dans l’iPhone 3GS, iPhone 4, iPad
1ère génération (ARM Cortex-A8), iPad 2 (Cortex-A9), iPad 3ème génération.
• ARMv7s: De nouvelles instructions ont été ajoutées. Utilisé dans l’iPhone 5,
l’iPhone 5c, l’iPad 4ème génération. (Apple A6).
• ARMv8: 64-bit CPU, AKA ARM64 AKA AArch64. Utilisé dans l’iPhone 5S, l’iPad
Air (Apple A7). Il n’y a pas de mode Thumb en mode 64-bit, seulement ARM
(instructions de 4 octets).
1361
• R13 — AKA SP (pointeur de pile)
• R14 — AKA LR (link register)
• R15 — AKA PC (program counter)
R0-R3 sont aussi appelés «registres scratch » : les arguments de la fonctions sont
d’habitude passés par eux, et leurs valeurs n’ont pas besoin d’être restaurées en
sortant de la fonction.
Bit Description
0..4 M — processor mode
5 T — Thumb state
6 F — FIQ disable
7 I — IRQ disable
8 A — imprecise data abort disable
9 E — data endianness
10..15, 25, 26 IT — if-then state
16..19 GE — greater-than-or-equal-to
20..23 DNM — do not modify
24 J — Java state
27 Q — sticky overflow
28 V — overflow
29 C — carry/borrow/extend
30 Z — zero bit
31 N — negative/less than
1362
En NEON ou «SIMD avancé » 16 autres registres-Q 128-bit ont été ajoutés, qui par-
tagent le même espace que D0..D31.
.2.5 Instructions
Il il y a un suffixe -S pour certaines instructions en ARM, indiquant que l’instruction
met les flags en fonction du résultat. Les instructions qui n’ont pas ce suffixe ne
modifient pas les flags. Par exemple ADD contrairement à ADDS ajoute deux nombres,
mais les flags sont inchangés. De telles instructions sont pratiques à utiliser entre
CMP où les flags sont mis et, e.g. les sauts conditionnels, où les flags sont utilisés.
Elles sont aussi meilleures en termes d’analyse de dépendance de données (car
moins de registres sont modifiés pendant leurs exécution).
12. Aussi disponible en http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/
IHI0055B_aapcs64.pdf
1363
Table des codes conditionnels
.3 MIPS
.3.1 Registres
(Convention d’appel O32)
1364
Registres en virgule flottante
Nom Description
$F0..$F1 Le résultat d’une est renvoyé ici.
$F2..$F3 Non utilisé.
$F4..$F11 Utilisé pour des données temporaires.
$F12..$F15 Deux premiers arguments de fonction.
$F16..$F19 Utilisé pour des données temporaires.
$F20..$F31 Utilisé pour des données temporaires.∗ .
∗
— L’appelée doit préservé le contenu.
∗∗
— L’appelée doit préserver le contenu (sauf dans du code PIC).
∗∗∗
— Accessible en utilisant les instructions MFHI et MFLO.
.3.2 Instructions
Il y a 3 types d’instructions:
• type-R: celles qui ont 3 registres, Les instructions-R ont habituellement la forme
suivante:
instruction destination, source1, source2
Une chose importante à garder à l’esprit est que lorsque le premier et le se-
cond registre sont les même, IDA peut montrer l’instruction sous une forme
plus courte:
instruction destination/source1, source2
Cela nous rappelle quelque peu la syntaxe Intel pour le langage d’assemblage
x86.
• type-I: celles qui ont 2 registres et une valeur immédiate 16-bit.
• type-J: instructions de saut/branchement, elles ont 26 bits pour encoder l’offset.
Instructions da saut
Quelle est la différence entre les instructions -B (BEQ, B, etc.) et le -J (JAL, JALR, etc.) ?
Les instructions-B ont un type-I, ainsi, l’offset de l’instruction-B est encodé comme
une valeur 16-bit immédiate. JR et JALR sont des types-R et sautent à une adresse
absolue spécifiée dans un registre. J et JAL sont des type-J, ainsi l’offset est encodé
en une valeur 26-bit immédiate.
En bref, les instructions-B peuvent encoder une condition (B est en fait une pseudo-
instruction pour BEQ $ZERO, $ZERO, LABEL), tandis que les instructions-J ne le peuvent
pas.
1365
.4 Quelques fonctions de la bibliothèque de GCC
nom signification
__divdi3 division signée
__moddi3 reste (modulo) d’une division signée
__udivdi3 division non signée
__umoddi3 reste (modulo) d’une division non signée
Le code source des ces fonctions peut être trouvé dans l’installation de MSVS, dans
VC/crt/src/intel/*.asm.
.6 Cheatsheets
.6.1 IDA
Anti-sèche des touches de raccourci:
1366
touche signification
Space échanger le listing et le mode graphique
C convertir en code
D convertir en données
A convertir en chaîne
* convertir en tableau
U rendre indéfini
O donner l’offset d’une opérande
H transformer en nombre décimal
R transformer en caractère
B transformer en nombre binaire
Q transformer en nombre hexa-décimal
N renommer l’identifiant
? calculatrice
G sauter à l’adresse
: ajouter un commentaire
Ctrl-X montrer les références à la fonction, au label, à la variable courant
inclure dans la pile locale
X montrer les références à la fonction, au label, à la variable, etc.
Alt-I chercher une constante
Ctrl-I chercher la prochaine occurrence d’une constante
Alt-B chercher une séquence d’octets
Ctrl-B chercher l’occurrence suivante d’une séquence d’octets
Alt-T chercher du texte (instructions incluses, etc.)
Ctrl-T chercher l’occurrence suivante du texte
Alt-P éditer la fonction courante
Enter sauter à la fonction, la variable, etc.
Esc retourner en arrière
Num - cacher/plier la fonction ou la partie sélectionnée
Num + afficher la fonction ou une partie
cacher une fonction ou une partie de code peut être utile pour cacher des par-
ties du code lorsque vous avez compris ce qu’elles font. ceci est utilisé dans mon
script13 pour cacher des patterns de code inline souvent utilisés.
.6.2 OllyDbg
Anti-sèche des touches de raccourci:
raccourci signification
F7 tracer dans la fonction
F8 enjamber
F9 démarrer
Ctrl-F2 redémarrer
.6.3 MSVC
. . Quelques options utiles qui ont été utilisées dans ce livre
13. GitHub
1367
option signification
/O1 minimiser l’espace
/Ob0 pas de mire en ligne
/Ox optimisation maximale
/GS- désactiver les vérifications de sécurité (buffer overflows)
/Fa(file) générer un listing assembleur
/Zi activer les informations de débogage
/Zp(n) aligner les structures sur une limite de n-octet
/MD l’exécutable généré utilisera MSVCR*.DLL
Quelques informations sur les versions de MSVC: 5.1.1 on page 917.
.6.4 GCC
Quelques options utiles qui ont été utilisées dans ce livre.
option signification
-Os optimiser la taille du code
-O3 optimisation maximale
-regparm= nombre d’arguments devant être passés dans les registres
-o file définir le nom du fichier de sortie
-g mettre l’information de débogage dans l’exécutable généré
-S générer un fichier assembleur
-masm=intel construire le code source en syntaxe Intel
-fno-inline ne pas mettre les fonctions en ligne
.6.5 GDB
Quelques commandes que nous avons utilisées dans ce livre:
1368
option
break filename.c:number mettre un point d’arrêt à la ligne number du code source
break function mettre un point d’arrêt sur une fonction
break *address mettre un point d’arrêt à une adresse
b —”—
p variable afficher le contenu d’une variable
run démarrer
r —”—
cont continuer l’exécution
c —”—
bt afficher la pile
set disassembly-flavor intel utiliser la syntaxe Intel
disas disassemble current function
disas function désassembler la fonction
disas function,+50 disassemble portion
disas $eip,+0x10 —”—
disas/r désassembler avec les opcodes
info registers afficher tous les registres
info float afficher les registres FPU
info locals afficher les variables locales
x/w ... afficher la mémoire en mot de 32-bit
x/w $rdi afficher la mémoire en mot de 32-bit
à l’adresse dans RDI
x/10w ... afficher 10 mots de la mémoire
x/s ... afficher la mémoire en tant que chaîne
x/i ... afficher la mémoire en tant que code
x/10c ... afficher 10 caractères
x/b ... afficher des octets
x/h ... afficher en demi-mots de 16-bit
x/g ... afficher des mots géants (64-bit)
finish exécuter jusqu’à la fin de la fonction
next instruction suivante (ne pas descendre dans les fonctions)
step instruction suivante (descendre dans les fonctions)
set step-mode on ne pas utiliser l’information du numéro de ligne en exécutant pas à pas
frame n échanger la stack frame
info break afficher les points d’arrêt
del n effacer un point d’arrêt
set args ... définir les arguments de la ligne de commande
1369
Acronymes utilisés
1370
OS Système d’exploitation (Operating System) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xx
RA Adresse de retour . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
PE Portable Executable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1371
DLL Dynamic-Link Library . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 994
LR Link Register . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1372
CISC Complex Instruction Set Computing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
ELF Executable and Linkable Format: Format de fichier exécutable couramment uti-
lisé sur les systèmes *NIX, Linux inclus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
NOP No Operation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1373
XOR eXclusive OR (OU exclusif) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1349
ASCIIZ ASCII Zero ( chaîne ASCII terminée par un octet nul (à zéro)) . . . . . . . . . . . . 125
1374
RE Reverse Engineering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1329
1375
RFC Request for Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 932
CD Compact Disc
1376
CPRNG Cryptographically secure PseudoRandom Number Generator . . . . . . . . . . 1252
1377
Glossaire
anti-pattern En général considéré comme une mauvaise pratique. 45, 104, 604
atomic operation « ατ oµoς » signifie « indivisible » en grec, donc il est garantie
qu’une opération atomique ne sera pas interrompue par d’autres threads. 842,
1037
callee Une fonction appelée par une autre. 46, 64, 91, 117, 131, 134, 137, 543, 604,
717, 857, 963, 965–967, 971, 972, 1365
caller Une fonction en appelant une autre. 8–11, 14, 41, 64, 117, 131–133, 136,
146, 205, 543, 616, 717, 963, 964, 966, 967, 972
compiler intrinsic Une foncion spécifique d’un compilateur, qui n’est pas une fonc-
tion usuelle de bibliothèque. Le compilateur génère du code machine spécifique
au lieu d’un appel à celui-ci. Souvent il s’agit d’une pseudo-fonction pour une
instruction CPU spécifique. Lire plus: ( 11.3 on page 1302). 1353
CP/M Control Program for Microcomputers: un OS de disque très basique utilisé
avant MS-DOS. 1187
endianness Ordre des octets: 2.8 on page 601. 30, 107, 1349
incrémenter Incrémenter de 1. 23, 27, 241, 246, 265, 271, 419, 422, 568, 1343
1378
jump offset une partie de l’opcode de l’instruction JMP ou Jcc, qui doit être ajoutée
à l’adresse de l’instruction suivante, et c’est ainsi que le nouveau PC est calculé.
Peut être négatif. 126, 173, 174, 1343
kernel mode Un mode CPU sans restriction dans lequel le noyau de l’OS et les
drivers sont exécutés. cf. user mode. 1380
leaf function Une fonction qui n’appelle pas d’autre fonction. 39, 45
link register (RISC) Un registre où l’adresse de retour est en général stockée. Ceci
permet d’appeler une fonction leaf sans utiliser la pile, i.e, plus rapidemment.
45, 1092, 1361, 1363
loop unwinding C’est lorsqu’un compilateur, au lieu de générer du code pour une
boucle de n itérations, génère juste n copies du corps de la boucle, afin de
supprimer les instructions pour la gestion de la boucle. 244
moyenne arithmétique la somme de toutes les valeurs, divisé par leur nombre.
681
quotient Résultat de la division. 284, 287, 289, 290, 295, 557, 651, 683
1379
register allocator La partie du compilateur qui assigne des registes du CPU aux
variables locales. 263, 394, 544
reverse engineering action d’examiner et de comprendre comment quelque chose
fonctionne, parfois dans le but de le reproduire. iv, 1352
security cookie Une valeur aléatoire, différente à chaque exécution. Vous pouvez
en lire plus à ce propos ici: 1.26.3 on page 357. 1025
stack frame Une partie de la pile qui contient des informations spécifiques à la
fonction courante: variables locales, arguments de la fonction, RA, etc.. 93, 132,
625, 626, 1025
stdout standard output, sortie standard. 29, 50, 205
tail call C’est lorsque le compilateur (ou l’interpréteur) transforme la récursion (ce
qui est possible: tail recursion) en une itération pour l’efficacité: wikipedia. 630
tas Généralement c’est un gros bout de mémoire fournit par l’OS et utilisé par les ap-
plications pour le diviser comme elles le souhaitent. malloc()/free() fonctionnent
en utilisant le tas. 43, 446, 739, 743, 760, 762, 783, 784, 828, 994, 996
thunk function Minuscule fonction qui a un seul rôle: appeler une autre fonction.
31, 58, 503, 1092, 1104
tracer Mon propre outil de debugging. Vous pouvez en lire plus à son propos ici: 7.2.1
on page 1040. 247–249, 712, 809, 923, 938, 942, 943, 1019, 1084–1086, 1167,
1175, 1181, 1184, 1206, 1301
type de donnée intégral nombre usuel, mais pas un réel. peut être utilisé pour
passer des variables de type booléen et des énumérations. 300
user mode Un mode CPU restreint dans lequel le code de toutes les applications
est exécuté. cf. kernel mode. 1114, 1379
Windows NT Windows NT, 2000, XP, Vista, 7, 8, 10. 372, 540, 856, 928, 983, 997,
1036, 1191, 1352
word Dans les ordinateurs plus vieux que les PCs, la taille de la mémoire était sou-
vent mesurée en mots plutôt qu’en octet. 576, 578–581, 588, 589, 747, 830
xoring souvent utilisé en anglais, qui signifie appliquer l’opération XOR. 1025, 1107,
1111
1380
Index
1381
LSR, 431 Mode Thumb, 3, 179, 230
LSRS, 411 Optional operators
MADD, 140 ASR, 427, 654
MLA, 139 LSL, 345, 380, 427, 570
MOV, 11, 27, 29, 427, 654 LSR, 427, 654
MOVcc, 193, 199 ROR, 427
MOVK, 570 RRX, 427
MOVT, 29, 654 Pipeline, 228
MOVT.W, 30 Registres
MOVW, 30 APSR, 332
MUL, 142 FPSCR, 332
MULS, 140 Link Register, 26, 27, 45, 74, 230,
MVNS, 273 1361
NEG, 664 R0, 144, 1361
ORR, 403 scratch registers, 272, 1361
POP, 26–28, 42, 45 X0, 1362
PUSH, 28, 42, 45 Z, 128, 1362
RET, 34 S-registres, 294, 1362
RSB, 185, 380, 427, 664 soft float, 296
SBC, 512 ARM64
SMMUL, 654 lo12, 75
STMEA, 43 ASLR, 997
STMED, 43 AWK, 940
STMFA, 43, 79
STMFD, 26, 43 Base address, 996
STMIA, 77 base32, 931
STMIB, 79 Base64, 930
STP, 33, 75 base64, 933, 1121, 1253
STR, 76, 345 base64scanner, 601, 930
SUB, 76, 380, 427 bash, 145
SUBcc, 705 BASIC
SUBEQ, 273 POKE, 952
SUBS, 512 BeagleBone, 1137
SXTB, 470 Bibliothèque standard C
SXTW, 386 alloca(), 49, 363, 604, 1013
TEST, 263 assert(), 370, 934
TST, 396, 427 atexit(), 746
VADD, 295 atoi(), 656, 1151
VDIV, 295 close(), 988
VLDR, 294 exit(), 616
VMOV, 294, 332 fread(), 824
VMOVGT, 332 free(), 604, 784
VMRS, 332 fwrite(), 824
VMUL, 295 getenv(), 1153
XOR, 186, 412 localtime(), 868
Mode ARM, 3 localtime_r(), 456
Mode switching, 139, 230 longjmp, 830
mode switching, 30 longjmp(), 205
Mode Thumb-2, 3, 230, 333, 335 malloc(), 447, 604, 784
1382
memchr(), 1347 RTTI, 735
memcmp(), 585, 674, 936, 1350 STL, 917
memcpy(), 17, 91, 671, 829, 1345 std::forward_list, 760
memmove(), 829 std::list, 747
memset(), 339, 669, 1181, 1348 std::map, 770
open(), 988 std::set, 770
pow(), 297 std::string, 738
puts(), 29 std::vector, 760
qsort(), 494, 615 C11, 976
rand(), 434, 922, 1054, 1056, 1083, Callbacks, 494
1119 Canary, 358
read(), 824, 988 cdecl, 59, 963
realloc(), 604 Chess, 600
scanf(), 90 Cipher Feedback mode, 1128
setjmp, 830 clusterization, 1250
srand(), 1083 code indépendant de la position, 26, 984
strcat(), 675 Code inline, 403
strcmp(), 585, 616, 666, 988 COFF, 1101
strcpy(), 17, 669, 1120 column-major order, 373
strlen(), 262, 538, 668, 692, 1347 Compiler intrinsic, 50, 588, 1302
strstr(), 615 Core dump, 803
strtok, 276 cracking de logiciel, 19, 199, 808
time(), 868, 1083 Cray, 525, 582, 596, 601
toupper(), 701 CRC32, 605, 630
va_arg, 681 CRT, 991, 1020
va_list, 686 CryptoMiniSat, 552
vprintf, 686 CryptoPP, 961, 1125
write(), 824 Cygwin, 919, 923, 1005, 1042
binary grep, 938, 1040
Binary Ninja, 1040 Data general Nova, 283
BIND.EXE, 1004 De Morgan’s laws, 1320
BinNavi, 1040 DEC Alpha, 523
binutils, 488 DES, 525, 544
Binwalk, 1244 dlopen(), 988
Bitcoin, 838, 1137 dlsym(), 988
Boehm garbage collector, 813 dmalloc, 803
Boolector, 57 Donald E. Knuth, 581
Booth’s multiplication algorithm, 283 DOSBox, 1191
Borland C++, 800 DosBox, 942
Borland C++Builder, 919 double, 285, 971
Borland Delphi, 20, 919, 925, 1355 Doubly linked list, 598, 747
BSoD, 982 dtruss, 1041
BSS, 998 Duff’s device, 647
Dynamically loaded libraries, 31
C++, 1169 Débordement de tampon, 349, 356, 1025
C++11, 760, 975
exceptions, 1013 Edsger W. Dijkstra, 786
ostream, 735 EICAR, 1186
References, 737 ELF, 108
1383
Entropy, 1217, 1239 8253, 1189
Epilogue de fonction, 74, 77, 469 80286, 1114, 1310
Error messages, 933 80386, 402, 1310
Espace de travail, 968 80486, 284
FPU, 284
fastcall, 20, 48, 90, 394, 965 Intel 4004, 577
fetchmail, 578 Intel C++, 13, 526, 1303, 1311, 1345
FidoNet, 930 iPod/iPhone/iPad, 25
FILETIME, 521 Itanium, 523, 1306
FIXUP, 1087
float, 285, 971 JAD, 7
Fonctions de hachage, 605 Java, 579, 871
Forth, 901 John Carmack, 689
FORTRAN, 32 JPEG, 1249
Fortran, 373, 676, 786, 919 jumptable, 220, 230
FreeBSD, 936
Function epilogue, 41, 178, 941 Keil, 25
Function prologue, 41, 45, 357, 941 kernel panic, 982
Fused multiply–add, 139 kernel space, 982
Fuzzing, 664
LAPACK, 32
Garbage collector, 812, 903 LARGE_INTEGER, 521
GCC, 919, 1366, 1368 LD_PRELOAD, 987
GDB, 39, 66, 71, 357, 504, 505, 1041, 1368 Linker, 110, 715
GeoIP, 1241 Linux, 395, 984, 1169
Glibc, 504, 830, 982 libc.so.6, 393, 503
GnuPG, 1252 LISP, 793
GraphViz, 811 LLDB, 1041
LLVM, 25
HASP, 936 long double, 285
Heartbleed, 828, 1136 Loop unwinding, 244
Heisenbug, 838, 849 LZMA, 1244
Hex-Rays, 145, 259, 382, 388, 814, 847,
1313 Mac OS Classic, 1090
Hiew, 125, 173, 202, 925, 932, 999, 1000,Mac OS X, 1042
1005, 1039, 1301 Mathematica, 786, 1069
Honeywell 6070, 578 MD5, 605, 935
memfrob(), 1123
ICQ, 953 MFC, 1001, 1153
IDA, 118, 202, 488, 676, 916, 928, 1040, Microsoft, 521
1278, 1366 Microsoft Word, 828
var_?, 77, 100 MIDI, 935
IEEE 754, 285, 406, 485, 553, 1333 MinGW, 919, 1205
Inline code, 251, 664, 723, 765 minifloat, 570
Integer overflow, 143 MIPS, 3, 946, 960, 998, 1091, 1248
Intel Branch delay slot, 11
8080, 271 Global Pointer, 381
8086, 271, 402, 1114 Instructions
Memory model, 1309 ADD, 143
Modèle de mémoire, 866 ADDIU, 36, 115, 116
1384
ADDU, 143 HI, 655
AND, 406 LO, 655
BC1F, 338 Mode Thumb-2, 30
BC1T, 338 MS-DOS, 20, 47, 360, 800, 861, 935, 942,
BEQ, 130, 181 953, 996, 1114, 1186, 1188, 1255,
BLTZ, 187 1309, 1333, 1346, 1352, 1353
BNE, 181 DOS extenders, 1310
BNEZ, 232 MSVC, 1366, 1367
BREAK, 655 MSVCRT.DLL, 1205
C.LT.D, 338 Multiplication-addition fusionnées, 140
J, 9, 11, 36
JAL, 143 Name mangling, 715
JALR, 36, 143 Native API, 997
JR, 218 Non-a-numbers (NaNs), 325
LB, 258 Notation polonaise inverse, 339
LBU, 258 Notepad, 1245
LI, 573 NSA, 601
LUI, 36, 115, 116, 409, 573
LW, 36, 101, 116, 218, 574 objdump, 488, 987, 1005, 1040
MFHI, 143, 656, 1365 octet, 578
MFLO, 143, 655, 1365 OEP, 995, 1005
MTC1, 491 OllyDbg, 61, 95, 107, 132, 149, 167, 222,
MULT, 143 246, 266, 288, 304, 315, 342, 351,
NOR, 275 354, 374, 417, 444, 467, 468, 474,
OR, 39 478, 498, 1000, 1041, 1367
ORI, 406, 573 OOP
SB, 258 Polymorphism, 715
SLL, 232, 278, 430 opaque predicate, 709
SLLV, 430 OpenMP, 838, 921
SLT, 181 OpenSSL, 828, 1136
SLTIU, 232 OpenWatcom, 919, 966
SLTU, 181, 183, 232 Oracle RDBMS, 13, 525, 932, 1008, 1169,
SRL, 284 1179, 1182, 1269, 1282, 1303, 1311
SUBU, 187
Page (mémoire), 540
SW, 84
Pascal, 925
Load delay slot, 218
PDP-11, 568
O32, 84, 90, 1364
PGP, 930
Pointeur Global, 34
Phrack, 930
Pseudo-instructions
Pile, 42, 131, 205
B, 254
Débordement de pile, 44
BEQZ, 183
Stack frame, 93
LA, 39
Pin, 687
LI, 11
PNG, 1247
MOVE, 36, 114
PowerPC, 3, 35, 1090
NEGU, 187
Prologue de fonction, 15, 76
NOP, 39, 114
Propagating Cipher Block Chaining, 1143
NOT, 275
Punched card, 339
Registres
puts() instead of printf(), 29, 98, 144, 175
FCCR, 338
Python, 688, 785
1385
ctypes, 974 Tabulation hashing, 600
Tagged pointers, 793
Qt, 19 TCP/IP, 603
Quake, 689 thiscall, 715, 717, 967
Quake III Arena, 494 thunk-functions, 31, 1003, 1092, 1104
TLS, 360, 975, 998, 1005, 1338
Racket, 1319 Callbacks, 980, 1005
rada.re, 18 Tor, 931
Radare, 1041 tracer, 247, 500, 502, 923, 938, 942, 1019,
radare2, 1251 1041, 1125, 1167, 1175, 1181, 1184,
rafind2, 1040 1300
RAID4, 597 Turbo C++, 800
RAM, 110
Raspberry Pi, 25 uClibc, 829
ReactOS, 1016 UCS-2, 579
Register allocation, 544 UFS2, 936
Relocation, 31 Unicode, 926
Resource Hacker, 1045 UNIX
RISC pipeline, 179 chmod, 6
ROM, 110, 111 diff, 954
ROT13, 1123 fork, 831
row-major order, 373 getopt, 1140
RSA, 7 grep, 932, 1301
RVA, 996 mmap(), 799
Récursivité, 41, 44, 629 od, 1039
Tail recursion, 630 strings, 931, 1039
xxd, 1039, 1224
SAP, 918, 1164 Unrolled loop, 251, 362, 647, 651, 670
Scheme, 1319 uptime, 987
SCO OpenServer, 1100 UPX, 1252
Security cookie, 357, 1025 USB, 1093
SHA1, 605 UseNet, 930
SHA512, 838 user space, 982
Shadow space, 136, 137, 554 user32.dll, 202
Shellcode, 708, 983, 997, 1188, 1357 UTF-16, 579
Signed numbers, 165, 585 UTF-16LE, 926, 927
SIMD, 553, 674 UTF-8, 926, 1254
SQLite, 810 Utilisation de grep, 249, 334, 917, 938,
SSE, 553 942, 1166
SSE2, 553 Uuencode, 1253
stdcall, 963, 1300 Uuencoding, 930
strace, 988, 1041
strtoll(), 1140 VA, 996
Stuxnet, 936 Valgrind, 849
Sucre syntaxique, 204 Variables globales, 104
Syntaxe AT&T, 16, 51 Variance, 1122
Syntaxe Intel, 16, 25
syscall, 393, 982, 1041 Watcom, 919
Sysinternals, 932, 1042 win32
Sécurité par l’obscurité, 933 FindResource(), 794
1386
GetOpenFileName, 276 ADD, 13, 59, 132, 657, 861, 1343
GetProcAddress(), 809 ADDSD, 554
HINSTANCE, 810 ADDSS, 567
HMODULE, 810 ADRcc, 189
LoadLibrary(), 809 AESDEC, 1125
MAKEINTRESOURCE(), 794 AESENC, 1125
WinDbg, 1041 AESKEYGENASSIST, 1130
Windows, 1036 AND, 15, 392, 397, 415, 432, 477,
API, 1333 1343, 1349
EnableMenuItem, 1047 BSF, 542, 1349
IAT, 996 BSR, 1349
INT, 996 BSWAP, 603, 1349
KERNEL32.DLL, 392 BT, 1349
MSVCR80.DLL, 496 BTC, 408, 1349
NTAPI, 1047 BTR, 408, 1037, 1349
ntoskrnl.exe, 1169 BTS, 408, 1349
PDB, 918, 999, 1047, 1056, 1164 CALL, 13, 44, 956, 1003, 1144, 1240,
Structured Exception Handling, 52, 1006 1343
TIB, 360, 1006, 1338 CBW, 587, 1349
Win32, 391, 927, 987, 995, 1311 CDQ, 520, 587, 1349
GetProcAddress, 1004 CDQE, 587, 1349
LoadLibrary, 1004 CLD, 1349
MulDiv(), 588, 1068 CLI, 1349
Ordinal, 1001 CMC, 1349
RaiseException(), 1006 CMOVcc, 179, 188, 191, 194, 199,
SetUnhandledExceptionFilter(), 1008 604, 1349
Windows 2000, 997 CMP, 117, 118, 615, 1343, 1358
Windows 3.x, 856, 1311 CMPSB, 936, 1350
Windows NT4, 997 CMPSD, 1350
Windows Vista, 995, 1047 CMPSQ, 1350
Windows XP, 997, 1005, 1056 CMPSW, 1350
Windows 2000, 522 COMISD, 563
Windows 98, 202 COMISS, 567
Windows File Protection, 202 CPUID, 474, 1352
Windows Research Kernel, 523 CWD, 587, 862, 1202, 1349
Wine, 1016 CWDE, 587, 1349
Wolfram Mathematica, 1217 DEC, 265, 1343, 1358
DIV, 587, 1352
x86 DIVSD, 554, 940
AVX, 525 FABS, 1355
Flags FADD, 1355
CF, 48, 1343, 1347, 1349, 1353, 1354 FADDP, 287, 294, 1355
DF, 1349, 1354 FATRET, 425, 426
IF, 1349, 1354 FCHS, 1355
FPU, 1338 FCMOVcc, 328
Instructions FCOM, 314, 325, 1355
AAA, 1358 FCOMP, 302, 1355
AAS, 1358 FCOMPP, 1355
ADC, 510, 861, 1343 FDIV, 287, 938, 939, 1355
1387
FDIVP, 287, 1355 JGE, 165, 1343
FDIVR, 293, 1355 JL, 165, 586, 1343
FDIVRP, 1355 JLE, 164, 1343
FDUP, 901 JMP, 44, 57, 74, 1003, 1300, 1343
FILD, 1355 JNA, 1343
FIST, 1355 JNAE, 1343
FISTP, 1355 JNB, 1343
FLD, 298, 302, 1356 JNBE, 326, 1343
FLD1, 1356 JNC, 1343
FLDCW, 1356 JNE, 117, 118, 165, 1343, 1358
FLDZ, 1356 JNG, 1343
FMUL, 287, 1356 JNGE, 1343
FMULP, 1356 JNL, 1343
FNSTCW, 1356 JNLE, 1343
FNSTSW, 302, 326, 1356 JNO, 1343, 1358
FSCALE, 492 JNS, 1343, 1358
FSINCOS, 1356 JNZ, 1343
FSQRT, 1356 JO, 1343, 1358
FST, 1356 JP, 303, 1343, 1358
FSTCW, 1356 JPO, 1343
FSTP, 298, 1356 JRCXZ, 1343
FSTSW, 1356 JS, 1343, 1358
FSUB, 1356 JZ, 128, 205, 1303, 1343
FSUBP, 1356 LAHF, 1344
FSUBR, 1356 LEA, 93, 135, 450, 618, 634, 657,
FSUBRP, 1356 969, 1052, 1144, 1344
FUCOM, 325, 1356 LEAVE, 15, 1344
FUCOMI, 328 LES, 1120, 1200
FUCOMP, 1356 LOCK, 1037
FUCOMPP, 325, 1356 LODSB, 1190
FWAIT, 285 LOOP, 241, 261, 941, 1201, 1353
FXCH, 1304, 1356 MAXSD, 563
IDIV, 587, 651, 1352 MOV, 11, 14, 17, 669–671, 956, 1000,
IMUL, 132, 385, 587, 793, 1343, 1358 1144, 1240, 1300, 1346
IN, 956, 1114, 1189, 1353 MOVDQA, 530
INC, 265, 1300, 1343, 1358 MOVDQU, 530
INT, 47, 1187, 1352 MOVSB, 1345
INT3, 923 MOVSD, 561, 672, 1345
IRET, 1352, 1353 MOVSDX, 561
JA, 165, 327, 586, 1343, 1358 MOVSQ, 1345
JAE, 165, 1343, 1358 MOVSS, 567
JB, 165, 586, 1343, 1358 MOVSW, 1345
JBE, 165, 1343, 1358 MOVSX, 263, 271, 467, 469, 470,
JC, 1343 587, 1346
Jcc, 130, 192 MOVSXD, 364
JCXZ, 1343 MOVZX, 264, 448, 1091, 1346
JE, 205, 1343, 1358 MUL, 587, 793, 1346
JECXZ, 1343 MULSD, 554
JG, 165, 586, 1343 NEG, 662, 1346
1388
NOP, 634, 1300, 1346, 1359 UD2, 1354
NOT, 270, 273, 1346 XADD, 1038
OR, 397, 692, 1346 XCHG, 1346, 1354
OUT, 956, 1114, 1353 XOR, 14, 118, 270, 680, 940, 1107,
PADDD, 530 1300, 1349, 1358
PCMPEQB, 541 MMX, 524
PLMULHW, 526 Préfixes
PLMULLD, 526 LOCK, 1037, 1342
PMOVMSKB, 541 REP, 1342, 1345, 1348
POP, 13, 42, 44, 1346, 1358 REPE/REPNE, 1342
POPA, 1353, 1358 REPNE, 1347
POPCNT, 1353 Registres
POPF, 1189, 1353 AF, 577
PUSH, 13, 15, 42, 44, 93, 956, 1144, AH, 1344, 1347
1240, 1346, 1358 CS, 1310
PUSHA, 1353, 1358 DF, 829
PUSHF, 1353 DR6, 1341
PXOR, 541 DR7, 1341
RCL, 941, 1353 DS, 1310
RCR, 1353 EAX, 117, 144
RET, 8, 10, 14, 44, 357, 717, 857, EBP, 93, 132
1300, 1346 ECX, 715
ROL, 425, 1302, 1353 ES, 1200, 1310
ROR, 1302, 1353 ESP, 59, 93
SAHF, 326, 1347 Flags, 118, 167, 1338
SAL, 845, 1354 FS, 978
SAR, 431, 587, 680, 845, 1201, 1354 GS, 359, 978, 982
SBB, 510, 1347 JMP, 226
SCASB, 1190, 1347 RIP, 986
SCASD, 1347 SS, 1310
SCASQ, 1347 ZF, 118, 392
SCASW, 1347 SSE, 525
SET, 611 SSE2, 525
SETcc, 181, 264, 326, 1354 x86-64, 20, 21, 69, 91, 98, 127, 134, 543,
SHL, 278, 341, 431, 845, 1348 553, 957, 967, 986, 1333, 1340
SHR, 283, 431, 477, 845, 1348 Xcode, 25
SHRD, 519, 1348 XML, 930, 1121
STC, 1354 XOR, 1128
STD, 1354
STI, 1354 Z80, 578
STOSB, 650, 1348 zlib, 830, 1124
STOSD, 1348 Zobrist hashing, 600
STOSQ, 670, 1348 ZX Spectrum, 593
STOSW, 1348
Éléments du langage C
SUB, 14, 15, 118, 205, 615, 657, 1343,
C99, 147
1348
bool, 391
SYSCALL, 1352, 1354
restrict, 676
SYSENTER, 984, 1352, 1354
variable length arrays, 363
TEST, 263, 392, 396, 432, 1349
Comma, 1318
1389
const, 13, 111, 612
for, 241, 632
if, 163, 204
Pointeurs, 91, 100, 148, 494, 543, 789
Post-décrémentation, 567
Post-incrémentation, 567
Pré-décrémentation, 567
Pré-incrémentation, 567
ptrdiff_t, 814
return, 14, 118, 146
Short-circuit, 691, 694, 1319
switch, 202, 204, 214
while, 262
1390