Examen du code généré par le compilateur Visual Studio C ++, partie 1

Dupliquer possible:
Pourquoi un code aussi complexe est-il émis pour diviser un entier signé par une puissance de deux?

Contexte

J’apprends simplement x86 asm en examinant le code binary généré par le compilateur.

Code compilé à l’aide du compilateur C ++ dans Visual Studio 2010 beta 2 .

Microsoft (R) 32-bit C/C++ Optimizing Comstackr Version 16.00.21003.01 for 80x86 

Code C (sandbox.c)

 int mainCRTStartup() { int x=5;int y=1024; while(x) { x--; y/=2; } return x+y; } 

Comstackz-le à l’aide de l’invite de commande Visual Studio.

 cl /c /O2 /Oy- /MD sandbox.c link /NODEFAULTLIB /MANIFEST:NO /SUBSYSTEM:CONSOLE sandbox.obj 

Disasm sandbox.exe dans OllyDgb

Ce qui suit commence à partir du point d’entrée.

 00401000 >/$ B9 05000000 MOV ECX,5 00401005 |. B8 00040000 MOV EAX,400 0040100A |. 8D9B 00000000 LEA EBX,DWORD PTR DS:[EBX] 00401010 |> 99 /CDQ 00401011 |. 2BC2 |SUB EAX,EDX 00401013 |. D1F8 |SAR EAX,1 00401015 |. 49 |DEC ECX 00401016 |.^75 F8 \JNZ SHORT sandbox.00401010 00401018 \. C3 RETN 

Examen

 MOV ECX, 5 int x=5; MOV EAX, 400 int y=1024; LEA ... // no idea what LEA does here. seems like ebx=ebx. elaborate please. // in fact, NOPing it does nothing to the original procedure and the values. CQD // sign extends EAX into EDX:EAX, which here: edx = 0. no idea why. SUB EAX, EDX // eax=eax-edx, here: eax=eax-0. no idea, pretty redundant. SAR EAX,1 // okay, y/= 2 DEC ECX // okay, x--, sets the zero flag when reaches 0. JNZ ... // okay, jump back to CQD if the zero flag is not set. 

Cette partie me dérange:

 0040100A |. 8D9B 00000000 LEA EBX,DWORD PTR DS:[EBX] 00401010 |> 99 /CDQ 00401011 |. 2BC2 |SUB EAX,EDX 

Vous pouvez tout faire et les valeurs de EAX et ECX restront les mêmes à la fin. Alors, quel est le sharepoint ces instructions?

Le tout

 00401010 |> 99 /CDQ 00401011 |. 2BC2 |SUB EAX,EDX 00401013 |. D1F8 |SAR EAX,1 

représente le y /= 2 . Vous voyez, un SAR autonome ne réaliserait pas la division d’entiers signés comme le prévoyaient les auteurs du compilateur. La norme C ++ 98 recommande que la division entière signée arrondisse le résultat à 0, tandis que le SAR seul arrondirait vers l’infini négatif. (Il est permis d’arrondir vers l’infini négatif, le choix est laissé à la mise en œuvre). Afin de mettre en œuvre l’arrondi à 0 pour les opérandes négatifs, l’astuce ci-dessus est utilisée. Si vous utilisez un type non signé au lieu d’un type signé, le compilateur générera une seule instruction de décalage, car le problème avec la division négative n’aura pas lieu.

Le truc est assez simple: pour l’extension du signe y négatif, un motif de 11111...1 dans EDX sera placé, ce qui correspond à -1 dans la représentation du complément à 2. Le SUB suivant appenda effectivement 1 à EAX si la valeur y initiale était négative. Si l’original était positif (ou 0), l’ EDX conservera 0 après l’extension du signe et EAX restra inchangé.

En d’autres termes, lorsque vous écrivez y /= 2 avec y signé, le compilateur génère le code qui fait quelque chose de plus semblable à celui-ci:

 y = (y < 0 ? y + 1 : y) >> 1; 

ou mieux

 y = (y + (y < 0)) >> 1; 

Notez que la norme C ++ n’exige pas que le résultat de la division soit arrondi à zéro. Le compilateur a donc le droit de ne faire qu’un seul décalage, même pour les types signés. Cependant, les compilateurs suivent normalement la recommandation d’arrondir vers zéro (ou offrent une option pour contrôler le comportement).

PS Je ne sais pas trop quel est le but de cette instruction de la LEA . C’est en effet un no-op. Cependant, je soupçonne qu’il s’agit peut-être simplement d’une instruction d’espace réservé insérée dans le code pour permettre une correction ultérieure. Si je me souviens bien, le compilateur MS dispose d’une option qui force l’insertion d’instructions d’espace réservé au début et à la fin de chaque fonction. À l’avenir, le patcher pourra écraser cette instruction avec une instruction CALL ou JMP qui exécutera le code du correctif. Cette LEA spécifique a été choisie simplement parce qu’elle génère une instruction de substitution non opérante de la longueur correcte. Bien sûr, cela pourrait être quelque chose de complètement différent.

Le lea ebx,[ebx] n’est qu’une opération NOP. Son but est d’aligner le début de la boucle en mémoire, ce qui le rendra plus rapide. Comme vous pouvez le voir ici, le début de la boucle commence à l’adresse 0x00401010, qui est divisible par 16, grâce à cette instruction.

Les opérations CDQ et SUB EAX,EDX s’assurent que la division arrondira un nombre négatif vers zéro – sinon, SAR le arrondirait vers le bas, donnant des résultats incorrects pour les nombres négatifs.

La raison pour laquelle le compilateur émet ceci:

 LEA EBX,DWORD PTR DS:[EBX] 

au lieu de l’équivalent sémantique:

 NOP NOP NOP NOP NOP NOP 

..est qu’il est plus rapide pour le processeur d’exécuter une instruction de 6 octets que six instructions de 1 octet. C’est tout.

Cela ne répond pas vraiment à la question, mais est un indice utile. Au lieu de manipuler OllyDbg.exe, vous pouvez faire en sorte que Visual Studio génère le fichier asm à votre place, ce qui présente l’avantage supplémentaire de pouvoir append le code source d’origine sous forme de commentaires. Ce n’est pas un gros problème pour votre petit projet actuel, mais au fur et à mesure de la croissance de votre projet, vous risquez de perdre du temps à déterminer quel code d’assemblage correspond à quel code source.

A partir de la ligne de commande, vous voulez les options / FA et / Fa ( MSDN ).

Voici une partie de la sortie de votre exemple de code (j’ai compilé du code de débogage, donc le .asm est plus long, mais vous pouvez faire la même chose pour votre code optimisé):

 _wmain PROC ; COMDAT ; 8 : { push ebp mov ebp, esp sub esp, 216 ; 000000d8H push ebx push esi push edi lea edi, DWORD PTR [ebp-216] mov ecx, 54 ; 00000036H mov eax, -858993460 ; ccccccccH rep stosd ; 9 : int x=5; int y=1024; mov DWORD PTR _x$[ebp], 5 mov DWORD PTR _y$[ebp], 1024 ; 00000400H $LN2@wmain: ; 10 : while(x) { x--; y/=2; } cmp DWORD PTR _x$[ebp], 0 je SHORT $LN1@wmain mov eax, DWORD PTR _x$[ebp] sub eax, 1 mov DWORD PTR _x$[ebp], eax mov eax, DWORD PTR _y$[ebp] cdq sub eax, edx sar eax, 1 mov DWORD PTR _y$[ebp], eax jmp SHORT $LN2@wmain $LN1@wmain: ; 11 : return x+y; mov eax, DWORD PTR _x$[ebp] add eax, DWORD PTR _y$[ebp] ; 12 : } pop edi pop esi pop ebx mov esp, ebp pop ebp ret 0 _wmain ENDP 

J’espère que cela pourra aider!