Programme d'étude sur le C++ bas niveau n° 7 : les conditions (suite)

Alex Darby

Traducteur

ram-0000

L'objectif de cette série d'articles d'Alex Darby sur la programmation « bas-niveau » est de permettre aux développeurs ayant déjà des connaissances de la programmation C++ de mieux comprendre comment leurs programmes sont exécutés en pratique. Ce sixième article parle des conditions et de la manière dont un compilateur transforme une condition en C/C++ en son équivalent assembleur.

Retrouver l'ensemble des articles de cette série sur la page d'index.

N'hésitez pas à commenter cet article ! Commentez .

- Titre : Programme d'étude sur le C++ bas niveau n° 7 : les conditions (suite)

- Auteur : Alex Darby

- Traducteur : ram-0000

- Parution : 25 février 2013

- Mise à jour : 26 juin 2020

- Licence : Copyright ® 2012 Alex Darby. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. L'opérateur conditionnel

- III. L'opérateur conditionnel (deuxième partie)

- IV. L'instruction switch

- IV-A. Un aparté au sujet de l'instruction switch

- IV-B. Où en étais-je ?

- V. Résumé

- VI. Une chose enfin…

- VII. Remerciements

I - Introduction

Salut les Terriens. Bienvenue dans ce septième opus de la série d'articles sur le « C/C++ bas niveau » que j'écris. Cet article parle de l'opérateur conditionnel et de l'instruction switch. Comme d'habitude, je m'appuierai sur des morceaux de code C++ et leurs correspondances en assembleur x86 (celui produit par VS2010) pour vous montrer ce que votre code en langage haut niveau devient au niveau de l'assembleur.

Avertissement : dans un monde idéal, je voudrais essayer d'éviter trop de connaissances supposées, mais en regardant le niveau de détail de chaque article, c'est franchement, trop de travail. Par conséquent, je vais, dès à présent, vous renvoyer vers l'article 6 avant de continuer avec cet article…

Ensuite, voici les liens pour tous ceux qui veulent commencer dès le début de cette série d'articles (attention : ça peut prendre un certain temps, les premiers sont assez longs) :

- Programme d'étude sur le C++ bas niveau ;

- Programme d'étude sur le C++ bas niveau n° 2 : les types de données ;

- Programme d'étude sur le C++ bas niveau n° 3 : la Pile ;

- Programme d'étude sur le C++ bas niveau n° 4 : plus de Pile ;

- Programme d'étude sur le C++ bas niveau n° 5 : encore plus de Pile ;

- Programme d'étude sur le C++ bas niveau n° 6 : les conditions (voyez au début de cet article les détails pour compiler et exécuter le code de démonstration).

II - L'opérateur conditionnel

Je suppose que vous êtes familier avec l'opérateur conditionnel aussi appelé « point d'interrogation » ou encore « opérateur ternaire » (ternaire parce que c'est le seul opérateur en C/C++ qui prend trois opérandes).

Si vous ne l'êtes pas, voici un lien qui vous permettra de comprendre (je parie que vous serez tellement content d'en savoir plus à ce sujet que vous saurez tout dans la semaine).

Personnellement, j'approuve entièrement l'usage de l'opérateur conditionnel lorsqu'il est utilisé judicieusement, mais il n'est pas toujours idéal pour le débogage au niveau source, car il s'agit essentiellement d'une seule ligne if-else et cela peut être difficile à suivre dans le débogueur (en fait, j'ai entendu dire qu'il était banni par certaines normes de codage de plusieurs entreprises, mais c'est ainsi, nous ne pouvons pas tous être sain d'esprit, n'est-ce pas ?)

Quoi qu'il en soit, nous allons y jeter un coup d'œil sur cet opérateur avec un peu de code :

1. #include "stdafx.h"

3. int main(int argc, char* argv[])

4. {

5. // la ligne après ce commentaire est logiquement équivalente à ce code :

6. // int iLocal; if( argc > 2 ){ iLocal = 3; }else{ iLocal = 7; }

7. int iLocal = (argc > 2) ? 3 : 7;

10. return 0;

11. }

Si vous vous rappelez l'assembleur généré par une instruction basique if-else de l'article précédent, l'assembleur généré ici va probablement vous casser les pieds…

Note :

- j'ai volontairement ignoré l'assembleur du prologue et de l'épilogue dans la fonction ci-dessous et juste montré l'assembleur impliqué dans l'affectation avec l'opérateur conditionnel ;

- si la vue du désassembleur ne vous montre pas les noms de variables, alors vous devez faire un clic droit de la fenêtre et cocher l'option « Show Symbol Names ».

1. 5:     int iLocal = (argc > 2) ? 3 : 7;

2. 01311249  xor         eax,eax

3. 0131124B  cmp         dword ptr [argc],2

4. 0131124F  setle       al

5. 01311252  lea         eax,[eax*4+3]

6. 01311259  mov         dword ptr [iLocal],eax

Il est clair que c'est totalement différent du code if-else que nous avons examiné précédemment.

C'est parce qu'il y a des magouilles et que le compilateur a décidé de faire le sournois avec les branches de code pour mettre en œuvre la logique spécifiée par le code C++.

Nous allons donc l'examiner ligne par ligne :

- la ligne 1 utilise l'instruction xor pour initialiser la valeur du registre eax avec 0. Le « ou exclusif » de n'importe quoi avec lui-même donne 0 ;

- la ligne 2, comme dans l'exemple précédent avec if, utilise l'instruction cmp pour tester la condition en positionnant un drapeau dans un registre spécial de la CPU sur la base du résultat de la comparaison ;

- à la ligne 3, nous voyons une nouvelle instruction ! L'instruction setle (set less equal) initialise son opérande à 1 si le premier opérande du cmp précédent était inférieur ou égal au deuxième opérande et à 0 s'il était plus grand. Nous n'avons pas vu auparavant l'opérande al, il s'agit du nom d'un registre du 386 qui est en fait l'octet de poids faible du registre eax (si vous êtes attentif et que vous déboguez ce code en pas à pas avec une fenêtre des registres ouverte, vous verrez que cette instruction fait que le registre eax est mis à 1 - notons aussi que cela ne fonctionne que parce que eax a déjà été mis à 0) ;

- la ligne 4 utilise l'instruction de chargement d'adresse lea (load effective address) pour faire des calculs sournois qui reposent sur la valeur du registre eax fixée par l'instruction setle de la ligne 3 ;

- la ligne 5 recopie la valeur de eax dans l'adresse de mémoire stockant la valeur de la variable iLocal.

C'est très bien, mais comment ça marche ?

Tout d'abord, notez qu'au niveau de l'assembleur l'instruction de comparaison setle teste (comme dans les exemples de l'article précédent) la condition inverse à celle spécifiée dans le code C++.

Cela signifie que le registre eax sera mis à 0 dans la ligne 3. Si argc est supérieur à 2, cela fait que [eax x 4+3] de la ligne 5 sera évalué comme (0 x 4) + 3, soit 3.

À l'inverse, si argc est inférieur ou égal à 2, le registre eax sera mis à 1 ce qui signifie que la ligne 5 sera évaluée comme (1 x 4) + 3, soit 7.

Donc, comme vous pouvez le voir, l'assembleur exécute la même branche de code indépendamment de l'état, mais en utilisant le résultat 0 ou 1 de l'instruction conditionnelle dans le calcul pour annuler ou inclure un des termes et ainsi donner ce que j'aime appeler un « si mathématique ». Futé.

D'ailleurs ce genre de « condition sans branches » a été un peu / beaucoup un sujet d'actualité au cours des dernières années, en particulier sur les consoles depuis que les processeurs sont particulièrement sensibles aux erreurs de prédiction de saut.

L'utilisation judicieuse de l'idiome « condition sans branches » est un outil qui peut être utilisé pour lutter contre les problèmes de performances avec les erreurs de prédiction de saut - pour un exemple à ce sujet, voir l'utilisation de l'instruction fsel (floating point select) dans cet article écrit par Tony Albrecht et pour une discussion brève des problèmes de prédiction de branchement (liés aux PC), voir cet article écrit par Igor Ostrovsky (qui travaille pour Microsoft).

III - L'opérateur conditionnel (deuxième partie)

Donc, clairement notre super-simple code ci-dessus fait que le compilateur va générer un assembleur intelligent à cause des valeurs constantes dans celui-ci. C'est certes intéressant, mais pas nécessairement représentatif de majorité du code du « monde réel ».

Voyons ce qui se passe si nous utilisons des variables avec l'opérateur conditionnel…

1. #include "stdafx.h"

3. int main(int argc, char* argv[])

4. {

5. int iOperandTwo = 3;

6. int iOperandThree = 7;

7. int iLocal = (argc > 2) ? iOperandTwo : iOperandThree;

9. return 0;

10. }

Voici la partie pertinente du désassembleur :

1. 5:     int iOperandTwo = 3;

2. 00CF1619  mov         dword ptr [iOperandTwo],3

3. 6:     int iOperandThree = 7;

4. 00CF1620  mov         dword ptr [iOperandThree],7

5. 7:     int iLocal = (argc > 2) ? iOperandTwo : iOperandThree;

6. 00CF1627  cmp         dword ptr [argc],2

7. 00CF162B  jle         main+25h (0CF1635h)

8. 00CF162D  mov         eax,dword ptr [iOperandTwo]

9. 00CF1630  mov         dword ptr [ebp-50h],eax

10. 00CF1633  jmp         main+2Bh (0CF163Bh)

11. 00CF1635  mov         ecx,dword ptr [iOperandThree]

12. 00CF1638  mov         dword ptr [ebp-50h],ecx

13. 00CF163B  mov         edx,dword ptr [ebp-50h]

14. 00CF163E  mov         dword ptr [iLocal],edx

Maintenant que l'opérateur conditionnel doit utiliser des variables, nous nous attendions à ce qu'il génère quelque chose qui ressemble plus au genre de code que nous avons vu la dernière fois avec une instruction if-else basique.

Nous avons l'instruction cmp attendue qui teste la condition inverse suivie d'un saut conditionnel, puis deux blocs d'assembleur. Ce premier bloc (lignes 7 à 9) fait un saut inconditionnel au-dessus du second (lignes 10 et 11) s'il est exécuté. Donc effectivement, cela se passe à peu près comme prévu, mais il a clairement des choses intéressantes qui se passent dedans.

- Les deux blocs utilisent des registres différents pour stocker leurs valeurs intermédiaires, le premier utilise eax, le second utilise ecx.

- Les deux branches stockent leur résultat à la même adresse mémoire dans la Pile (voir cet article si vous ne savez pas ou plus ce qu'est le « Stack Frame »), c'est-à-dire [ebp-50h].

- Le code qui attribue la valeur à iLocal (lignes 12 et 13) n'existe qu'une fois et est exécuté quelle que soit la branche qui a été utilisée, elle lit la valeur de [ebp-50h] et l'écrit dans iLocal en utilisant un troisième registre (edx).

L'utilisation de registres différents pour les différentes branches de l'étape 1 semble être importante, mais (selon les sources de plusieurs experts), c'est apparemment un comportement parfaitement normal du compilateur et il n'a rien à en redire.

Les étapes 2 et 3 montrent que le code généré par l'opérateur conditionnel (au moins avec VS2010) n'est pas directement équivalent à son équivalent intuitif if-else :

1. // équivalent intuitif if-else de

2. // int iLocal = (argc > 2 ) ? iOperandTwo : iOperandThree;

3. int iLocal;

4. if( argc > 2 )

5. {

6. iLocal = iOperandTwo;

7. }

8. else

9. {

10. iLocal = iOperandThree;

11. }

Plutôt que de choisir entre l'une des deux affectations comme dans ce if-else, l'assembleur généré pour cette utilisation de l'opérateur conditionnel fait exactement ce que nous lui avons dit : choisir l'une des deux valeurs (la stocker temporairement dans la Pile) et l'assigner à iLocal.

Quelques remarques finales sur l'opérateur conditionnel :

- vous voyez bien que « moins de lignes de code C++ » n'est pas synonyme de « moins d'assembleur » ;

- il peut être imbriqué, mais ne le faites pas ! C'est affreux et sera également très difficile à suivre lors du débogage au niveau du code ;

- soyez très prudent avec la priorité des opérateurs lors de son utilisation. Utilisez les parenthèses pour vous assurer qu'il va bien résoudre comme vous en avez l'intention.

IV - L'instruction switch

Le dernier type de déclaration conditionnelle que nous allons examiner est l'instruction switch. Comme l'opérateur conditionnel, l'instruction switch est une construction souvent maltraitée et décriée, mais vous ne voudriez pas vivre sans.

Pour être juste avec l'instruction switch, ce n'est pas la faute à cause d'elle qu'il est possible pour les maniaques d'écrire du code fragile et pour les fous de l'utiliser.

IV-A - Un aparté au sujet de l'instruction switch

Toutes les fois où j'ai toujours trouvé des exemples vraiment horribles de déclarations switch, c'est quand un système à l'origine synchrone et sans état a été transformé pour devenir asynchrone avec état sous la pression du temps. Cette situation semble toujours produire le genre de déclarations switch monolithiques, difficile à suivre, difficile à maintenir, d'une architecture fragile qui a donné à l'instruction switch une mauvaise réputation au fil des ans.

Un code qui a eu des fonctionnalités réseau modernisées (à mon avis) un endroit très fréquent de trouver des instructions switch à problèmes. Il est toujours préférable de fixer un système correctement s'il semble qu'il soit systématiquement cassé que de vouloir le persévérer. S'il semble que vous deviez mettre en place un ensemble d'états dans un système, il est architecturalement plus judicieux d'utiliser un comportement polymorphique (par exemple, une classe d'états avec une ou plusieurs fonctions virtuelles) plutôt qu'une instruction switch.

IV-B - Où en étais-je ?

Désolé, allons-y et jetons un coup d'œil à une instruction switch…

1. #include "stdafx.h"

3. int main(int argc, char* argv[])

4. {

5. int iLocal = 0;

7. // n.b. no "break" in case 1 so we can

8. // see what "fall through" looks like

9. switch( argc )

10. {

11. case 1:

12. iLocal = 6;

13. case 3:

14. iLocal = 7;

15. break;

16. case 5:

17. iLocal = 8;

18. break;

19. default:

20. iLocal = 9;

21. break;

22. }

24. return 0;

25. }

Et voici le désassembleur…

1. 9:     switch( argc )

2. 00C61620  mov         eax,dword ptr [argc]

3. 00C61623  mov         dword ptr [ebp-48h],eax

4. 00C61626  cmp         dword ptr [ebp-48h],1

5. 00C6162A  je          main+2Ah (0C6163Ah)

6. 00C6162C  cmp         dword ptr [ebp-48h],3

7. 00C61630  je          main+31h (0C61641h)

8. 00C61632  cmp         dword ptr [ebp-48h],5

9. 00C61636  je          main+3Ah (0C6164Ah)

10. 00C61638  jmp         main+43h (0C61653h)

11. 10:     {

12. 11:     case 1:

13. 12:         iLocal = 6;

14. 00C6163A  mov         dword ptr [iLocal],6

15. 13:     case 3:

16. 14:         iLocal = 7;

17. 00C61641  mov         dword ptr [iLocal],7

18. 15:         break;

19. 00C61648  jmp         main+4Ah (0C6165Ah)

20. 16:     case 5:

21. 17:         iLocal = 8;

22. 00C6164A  mov         dword ptr [iLocal],8

23. 18:         break;

24. 00C61651  jmp         main+4Ah (0C6165Ah)

25. 19:     default:

26. 20:         iLocal = 9;

27. 00C61653  mov         dword ptr [iLocal],9

28. 21:         break;

29. 22:     }

C'est plus ou moins exactement ce que vous attendiez :

- la ligne 1 recopie argc sur la Pile à [ebp-48h] ;

- ensuite, les lignes 2 à 9 mettent en œuvre la logique du switch par une série de comparaisons de cette valeur par rapport aux constantes spécifiées dans les instructions case associées aux sauts conditionnels et au code de l'instruction case correspondante ;

- si aucun des sauts conditionnels n'est déclenché, la logique provoque un saut inconditionnel à l'instruction default.

Il faut noter que :

- quand le mot-clé break est utilisé, cela provoque un saut inconditionnel à la fin du code de l'instruction switch ;

- le « tomber à travers » (NDT : absence de l'instruction break à la fin du bloc case) du bloc case 1 vers le bloc case 3 dans le code de haut niveau se traduit au niveau de l'assembleur généré par le compilateur par les blocs d'instructions adjacentes de l'instruction switch et l'absence de saut inconditionnel à la fin du bloc de code assembleur pour du case 1.

Si vous regardez l'assembleur de l'exemple if-else-if-else de l'article précédent, vous devriez être capable de voir que l'assembleur généré pour cette instruction switch est (plus ou moins) ce qui arriverait si nous l'avions comme if-else-if-else puis réorganisé l'assembleur. Toute la logique était déjà en place au début et l'assembleur généré pour chaque bloc de code a été laissé là où il était.

Donc, à part le fait que l'instruction switch soit une fonctionnalité du langage C/C++ très utile pour gérer ce qui serait souvent un sale désordre avec des risques d'erreurs dans les déclarations if-else-if-else, sur la base de cet exemple, il ne semble pas faire quoi que ce soit qui pourrait offrir un avantage significatif au niveau assembleur. Alors pourquoi aurais-je prétendu que le compilateur peut générer un « assembleur plutôt cool » pour une instruction switch ?

Avant de dire que nous avons tout vu, essayons d'utiliser une plage contiguë de valeurs pour les constantes dans les case du switch. Vous savez, juste pour le plaisir, et pour des raisons de simplicité nous allons commencer à 0.

1. #include "stdafx.h"

3. int main(int argc, char* argv[])

4. {

5. int iLocal = 0;

7. switch( argc )

8. {

9. case 0:

10. iLocal = 4;

11. break;

12. case 1:

13. iLocal = 5;

14. break;

15. case 2:

16. iLocal = 6;

17. break;

18. case 3:

19. iLocal = 7;

20. break;

21. }

23. return 0;

24. }

Et voici le code assembleur :

OK, donc cette fois, quelque chose de plus intéressant est en train de se passer - N.B. : J'ai utilisé une capture d'écran plutôt que de simplement coller le texte, car nous avons besoin de regarder dans une fenêtre de mémoire pour lui donner un sens.

Alors, qu'est-ce que cela fait ?

- Il copie argc dans eax et ensuite il le recopie sur la Pile à [ebp-48h],

- Il compare ensuite la valeur stockée dans l'adresse [ebp-48h] avec 3 (la constante la plus grande de nos blocs case).

- Si cette valeur est supérieure à 3 alors l'instruction ja de la ligne suivante va provoquer un saut à 8D1658h qui est la première instruction après les blocs de code case et donc sauter l'instruction switch.

- Si la valeur est plus petite ou égale à 3 alors cette valeur est recopiée dans ecx et il y a ensuite un saut inconditionnel vers … quelque part. :-/

OK, ce saut inconditionnel final a une syntaxe que nous n'avons pas encore vue de par son opérande d'adresse et qui n'est manifestement pas une constante :

jmp    dword ptr    (1B1664h)[ecx*4]

Ceci dit « sauter à l'emplacement stocké dans l'adresse mémoire à un décalage de 4 fois la valeur de ecx depuis l'adresse mémoire 8D1664h ». Comment est-ce que cela met en œuvre la logique C++ de l'instruction switch ?

Pour répondre à cette question, nous devons regarder dans une fenêtre de mémoire à l'adresse 8D1664h (NB : pour ouvrir une fenêtre mémoire depuis le menu de VS2010 lors du débogage, sélection le menu « Debug » puis « Windows » puis « Memory » et choisissez l'une des fenêtres mémoire. Pour définir l'adresse, il suffit de la copier à partir du désassembleur et de la coller dans le champ « Adresse: ». Vous devrez également faire un clic droit et choisir l'option « 4-byte integer » et définir la valeur du champ « Columns » à 1 pour lui donner le même aspect que la capture d'écran ci-dessus).

Donc, si vous portez votre regard jusqu'à la fenêtre de mémoire sur la gauche de la capture d'écran ci-dessus, vous verrez que les 4 premières lignes sont mises en évidence. Ces valeurs commencent à l'adresse 8D1664h et sont des entiers sur 4 octets (d'où le ecx * 4 dans l'opérande) - qui précisément dans ce cas sont des pointeurs.

L'instruction « jmp dword ptr (8D1664h) [ecx * 4] » va sauter à la valeur stockée dans l'adresse :

- 8D1664h + 0 = 8D1664h si la valeur de ecx est 0 ;

- 8D1664h + 4 >= 8D1668h si la valeur de ecx est 1 ;

- 8D1664h + 8 = 8D166Ch si la valeur de ecx est 2 ;

- 8D1664h + Ch = 8D1670h si la valeur de ecx est 3.

Ainsi, les quatre lignes ci-dessus représentent une table de saut et comme les constantes de nos case vont de 0 à 3, c'est un tableau de 4 pointeurs. Chaque élément de ce tableau pointe vers l'adresse d'exécution du bloc case correspondant à son indice dans la table.

Vous pouvez le vérifier en regardant l'adresse de la première instruction générée pour chaque bloc case avec les 4 valeurs stockées dans cette table.

Peut-être que c'est juste moi, mais je pense que c'est de l'assembleur plutôt cool. Il est certainement plus élégant que l'assembleur généré par le premier switch que nous avons examiné. Mais quel est l'avantage (s'il y en a un) de cette solution par rapport à l'exemple précédent ?

En théorie, cette forme de table de saut atteint le code en un temps constant pour tous les case alors qu'avec la forme if-else-if-else, le temps pour atteindre le code correspondant à chaque case est proportionnel au nombre de case précédents dans l'instruction switch.

Il y a très peu de chances de trouver qu'une instruction switch est un goulot d'étranglement dans votre code (sauf si vous avez fait des choses stupides), mais, toutes choses étant égales par ailleurs, l'approche par table de saut utilise moins d'instructions ce qui, normalement, est une bonne chose et qui, en théorie, devrait la rendre en moyenne plus rapide.

Un dernier mot sur les déclarations switch, je suis sûr que, en plus du comportement linéaire de recherche dans le cas du if-else-if-else pour trouver le bloc correct à exécuter, les compilateurs les plus modernes sont également capables de générer une recherche binaire dans le bloc cases des instructions switch possédant des valeurs constantes appropriées.

Utiliser une recherche binaire plutôt qu'une recherche linéaire permet d'améliorer le temps moyen de recherche de linéaire à logarithmique (c'est-à-dire de O(n) à O(log n)). Toutefois, une recherche binaire dans une instruction switch aura encore presque toujours plus d'instructions et de branches pour atteindre le bloc de code correct comparé à une table de sauts.

Il est également possible que le compilateur puisse choisir d'utiliser une ou plusieurs de ces méthodes dans un seul switch. Toutefois, cela nécessiterait probablement un grand nombre de case dans le switch et des intervalles de constantes avec des propriétés très spécifiques, il est donc peu probable que vous ayez à en voir très souvent.

Quelques remarques supplémentaires à noter à propos des instructions switch :

- le compilateur doit être capable de générer une table de saut indépendamment de l'ordre dans des constantes dans votre code (par exemple, cas n ° 2 : … case 1 : … case 3 : … devrait très bien fonctionner) ;

- avoir une plage de constantes pour les case qui commencent à 0 rend le code autour d'une table de saut plus simple, car il élimine les bornes inférieures à tester.

Une table de saut doit être créée tant que la plage des constantes est assez grande et / ou suffisamment serrée pour que le compilateur puisse se décider si cela vaut la peine, même si elles ne sont pas complètement contiguës. Regardez-le désassembleur si vous voulez vérifier.

V - Résumé

Donc, cela conclut notre examen sur les conditions, j'espère que vous avez trouvé cela intéressant et éclairant. ;)

Un dernier point à ne pas perdre de vue sur les conditions, c'est que tandis que le compilateur peut générer le même assembleur pour un if-else que pour l'opérateur conditionnel, il ne le fait pas. De même qu'il pourrait générer le même assembleur pour un if-else-if-else que pour une instruction switch, mais il ne le fait pas non plus.

En partie, cela montre les limites du compilateur, mais montre également l'importance d'utiliser la condition appropriée pour un but donné, l'avantage est que ce que vous utilisez rend votre intention claire aux lecteurs humains de votre code.

Nous avons maintenant couvert suffisamment de terrain si bien que vous devriez pouvoir appliquer les informations que je vous ai données à des problèmes courants de programmation pour débogage du code en mode release ou du code dont vous n'avez pas les informations de débogage.

Les principales choses que je voudrais que vous preniez en considération sur les conditions sont des choses qui vous aideront lors du débogage sans symboles :

- quand vous voyez une instruction cmp suivie d'une instruction jxx à une adresse à proximité dans le désassembleur, vous êtes probablement en train de regarder le code généré par une instruction de condition dans le code C/C++ ;

- si l'opérande adresse de l'instruction de saut est inférieure à l'adresse de l'instruction courante (c'est-à-dire qu'il y a un saut en arrière), vous êtes probablement en train de regarder une boucle ;

- l'assembleur généré par une condition teste en général à l'opposé du test spécifié dans le C/C++.

En utilisant ces méthodes heuristiques, regarder les valeurs dans les registres, les valeurs dans la Pile qui ont été écrits par l'assembleur et en consultant votre adresse actuelle dans le fichier de symboles pour savoir dans quelle fonction vous êtes (si vous ne générez pas un fichier de symboles pour tous vos projets, vous devriez le faire, regardez dans la documentation du compilateur pour savoir comment), vous devriez être en mesure de faire une supposition éclairée sur les variables dans le code C/C++ de votre plateforme susceptible d'être l'origine du problème actuel et ceci vous dira généralement pourquoi il s'est crashé ou vous donner une indication de sorte que vous pouvez jouer votre Sherlock Holmes pour remonter à la racine du problème. C'est certainement beaucoup plus rapide que l'insertion omniprésente de nombreux printf()…

Notre prochain sujet traitera des boucles, ce qui évidemment aussi utilisera des sauts conditionnels (ce qui explique pourquoi nous avons d'abord couvert les conditions…).

VI - Une chose enfin…

Merci à Tony, Bruce et Fabian pour leurs informations supplémentaires, leurs conseils et leur relecture.

Et, pour ceux d'entre vous qui voudraient aller plus loin (la plupart d'entre vous j'espère !), j'ai récemment découvert ce site wiki sur l'assembleur x86. Il a un grand chevauchement avec cette série d'articles et couvre également la programmation en assembleur x86. Fortement recommandé, je l'ai certainement trouvé très utile.

Finalement, une pépite finale de sagesse par Bruce Dawson :

Un autre problème que j'ai vu avec est qu'il y a des gens qui ont « intelligemment » créé des classes string qui ont à la fois un constructeur const char * et un opérateur de conversion const char *. Cela a le potentiel extraordinairement dangereux et inefficace de permettre de nombreuses conversions cachées. Ils comprennent alors avec ce code :

return bFlag ? mStringObject : "Hello world";

La question est de connaître le type de cette expression ? Est-ce que mStringObject est converti en un const char * ou bien est-ce que c'est « Bonjour tout le monde » qui est converti en string ? Je n'ai aucune idée et personne ne devrait mémoriser les règles pertinentes, ce code est trop fragile et dangereux.

Certaines personnes pourraient penser que le type de l'opérateur conditionnel dépend du type de retour de la fonction, mais ce n'est pas vrai. Ils sont indépendants. Ainsi, il est fort possible que « Bonjour tout le monde » soit converti en un objet string, puis (si le type de retour de la fonction est const char *) cet objet string (temporaire !!) sera reconverti en un const char *. En plus d'être inefficace, cela conduit à un comportement indéfini, puisque nous retournons un pointeur vers la mémoire appartenant à un objet qui est détruit quand la fonction se termine.

L'utilisation de l'opérateur conditionnel avec des types incompatibles est mal, mal, mal. Les classes string avec un opérateur de conversion const char * est mal, mal, mal. Les mettre ensemble… cela n'a pas de prix.

VII - Remerciements

Cet article est la traduction de l'article « C / C++ Low Level Curriculum Part 7: More Conditionals » écrit en anglais par Alex Darby. Alex Darby a aimablement autorisé l'équipe C/C++ de Developpez.com à traduire et diffuser son article en français.

Je tiens à remercier zoom61 pour la relecture orthographique de cette traduction.