Académique Documents
Professionnel Documents
Culture Documents
Dans ce qui suit, nous nous limitons d'une part à C++Builder pour PC
et d'autre part à l'assembleur du 80386 de chez Intel. Pour utiliser
cet assembleur, vous avez deux possibilités : soit insérer dans votre
programme C/C++ des lignes d'assembleur, soit utiliser l'assembleur en
tant que tel sous DOS via une "invite de commandes" et donc écrire un
programme intégralement en assembleur. Nous abordons l'un et l'autre
aspect de la question.
1
bits du code opératoire indiquent la nature de l'opération et les deux
chiffres octaux suivants indiquent soit un type d'opération soit un
registre impliqué dans l'opération. Ainsi si le code opératoire
commence par "01", cela signifie "mov", ensuite viennent les deux
registres impliqués. Les registres étaient indicés de 0 à 7
(BCDEHLMA). Par exemple mov A,B (le registre B est recopié dans A)
s'écrit en octal 170 (soit 01111000 en binaire) car 7 signifie A et 0
signifie B. Inversement mov B,A s'écrit 107. Bref, le 8080 fut un
microprocesseur minimal mais parfaitement efficace puisqu'on pouvait
tout programmer avec. L'expérience montre qu'un jeu d'instructions
réduit suffit largement à la programmation en assembleur, le 8080 en a
donné la preuve avec élégance.
MOV AH,4
MOV AL,3
puisque 4*256+3=1027.
Il a également deux registres d'index SI et DI (source index et
destination index) et BP (base pointeur) utilisé dans certains modes
d'adressage.
MOV AL,[SI]
2
signifie que le contenu pointé par SI est lu dans le registre 8 bits
AL. Mais le registre SI n'est que l'offset à l'intérieur d'un segment,
le registre de segment impliqué est ici DS (data segment), l'adresse
20 bits lue sera donc DS*16+SI. Sinon, si on veut utiliser un autre
registre de segment, il faut alors préfixer l'instruction et écrire
par exemple
MOV AL,ES:[SI]
qui signifie qu'on ne calcule plus l'adresse via DS mais ES, l'octet
lu dans AL est donc celui situé à l'adresse ES*16+SI. D'ailleurs le
désassemblage donnerait
ES:
MOV AL,[SI]
BX+SI+dep
BX+DI+dep
BP+SI+dep
BP+DI+dep
SI+dep
DI+dep
BP+dep
BX+dep
Principe de l'assembleur
Le registre 32 bits EIP (instruction pointer) lit l'instruction qu'il
pointe en mémoire vive. Le premier octet de chaque instruction est un
code opératoire en fonction duquel le microprocesseur déduit les
opérandes, il sait donc en fonction du code opératoire qu'il décode
3
sur combien d'octets est codée l'instruction et la signification de
ces octets. Puis le microprocesseur exécute cette instruction lue. Si
un sous-programme est appelé par l'instruction CALL, l'adresse 32 bits
pointée par EIP juste après le CALL qu'il vient de lire est
sauvegardée dans la pile. La pile est un espace mémoire spécial où
l'on peut sauvegarder des valeurs pour les récupérer ensuite. Quand on
écrit par exemple
PUSH EAX
cela signifie que le registre 32 bits EAX est sauvegardé dans cet
espace particulier, vous n'avez pas besoin de savoir où. Pour
récupérer ensuite EAX on écrit simplement
POP EAX
Bien entendu c'est au programmeur à gérer ses PUSH et ses POP. Si vous
sauvegardez EAX par PUSH EAX et que vous écriviez par la suite
POP EBX, il est clair que vous récupérez dans EBX la valeur de EAX. En
général, les PUSH correspondent aux POP en sens inverse, ils obéissent
à la loi LIFO (last in, first out). Par exemple, imaginons une
fonction qui va utiliser les registres EAX, EBX et ESI, on écrira
PUSH EAX
PUSH EBX
PUSH ESI
POP ESI
POP EBX
POP EAX
RET
On voit donc que les POP s'effectuent dans l'ordre inverse des PUSH,
la dernière valeur empilée étant ESI, c'est la première à être dépilée
de manière à ce qu'elle soit bien lue dans ESI. L'intérêt d'une telle
démarche est que le programme appelant n'a pas perdu le contenu de ses
registres. Si par exemple EAX est égal à 2 avant cet appel, on aura
bien EAX égal à 2 au retour de la fonction. Si toutefois votre
fonction assembleur renvoie une valeur dans EAX que la fonction est
censée calculer, il ne faut alors plus programmer le couple
PUSH EAX/POP EAX sinon la valeur ne serait pas retournée. En C++ on se
pose la question de savoir si on envoie un paramètre en tant que
valeur ou en tant que référence via l'opérateur &, en assembleur on se
pose la question de savoir si on doit ou non "pusher" les registres.
Par défaut en C++, un paramètre est du type valeur (cela correspond au
couple PUSH/POP en assembleur) sinon, si on utilise l'opérateur &, la
valeur est retournée à l'appelant (ce qui équivaut à l'absence de
PUSH/POP pour le registre concerné, son contenu est donc renvoyé à
l'appelant).
4
L'instruction CALL FONCTION où FONCTION est l'adresse où débute une
fonction correspond donc à PUSH EIP et à LEA EIP, FONCTION (mais ces
instructions n'existent pas, c'est uniquement pour vous donner une
correspondance). Quand le microprocesseur rencontre l'instruction RET,
il suppose qu'il y a en pile l'adresse de retour (c'est au programmeur
à faire en sorte qu'il en soit ainsi), il lit donc cette adresse dans
EIP qui pointe donc, si la programmation est correcte, l'adresse
sauvegardée c'est-à-dire juste après le CALL. On comprend donc que
l'instruction RET équivaudrait à l'instruction moins parlante POP EIP
qui n'existe pas.
TEST AL,AL
JZ ALNUL // saute à l'adresse ALNUL si ZF=1
ici le saut n'a pas eu lieu donc AL n'est pas nul
ALNUL:
ici le saut a eu lieu donc AL est nul
5
D'une manière générale, il faut considérer que les flags répondent à
une question. Ainsi le flag ZF (zéro flag) répond à la question : le
résultat de la dernière opération est-il nul? ZF=1 oui, ZF=0 non. JZ
signifie "saute à l'adresse indiquée si Z c'est-à-dire si ZF=1", JNZ
signifie "saute à l'adresse indiquée si Non Z c'est-à-dire si ZF=0".
Le flag CF (carry flag) répond à la question : le résultat de la
dernière opération a-t-il provoqué un dépassement de capacité?
CF=1 oui, CF=0 non. On appelle "dépassement de capacité" le fait que
le résultat de l'opération ne peut pas tenir dans le registre qui
reçoit le résultat. Imaginons l'instruction
add al,6
cmp ax,bx
cmp ax,bx
6
jeu d'instructions, mais il faut pour chaque type d'instruction
connaître le comportement des indicateurs. D'une manière générale,
voici ce qu'il faut à peu près savoir.
Les instructions d'affectation du type MOV, PUSH, POP, LEA (load
effective adresse), XCHG (swap de deux registres), XLAT (chargement
dans AL de l'octet pointé par BX+AL), les manipulation de chaînes
(REP MOVSB, REP MOVSW, REP MOVSD, qui recopient ECX contenus mémoire
de ESI source vers EDI destination), les initialisations mémoire
(REP STOSB, REP STOSW, REP STOSD qui recopient ECX fois respectivement
AL, AX ou EAX à partir de EDI) etc. ne positionnent aucun indicateur.
C'est d'ailleurs assez pratique car on peut insérer ce type
d'instructions avant un saut conditionnel puisque ces chargements ne
modifient pas les flags. De même les sauts conditionnels ou non et les
appels qui ne sont autres que des chargements divers et variés de EIP.
Les incrémentations (INC) et décrémentations (DEC) positionnent tous
les indicateurs sauf CF. On épargne ici CF qui reste intouché.
D'ailleurs, si ZF=1 après une incrémentation, le résultat est nul et
il y a eu dépassement de capacité, ZF eût donc fait double emploi avec
CF suite à une incrémentation, ce pourquoi il a été épargné.
Les opérations arithmétiques, addition (ADD), addition avec CF (ADC,
cela signifie qu'on ajoute en plus l'indicateur CF, ceci revient à
incrémenter le résultat si CF=1, ADC équivaut à ADD si CF=0),
soustraction (SUB), soustraction avec carry (SBB, on soustrait en plus
CF), comparaison (CMP) positionnent tous les indicateurs.
Les instructions logiques, ET logique (AND), OU logique (OR), OU
exclusif (XOR) positionnent tous les indicateurs et remettent CF à 0.
C'est très pratique, s'il y a eu une instruction logique, on sait
qu'on a CF=0, c'est très fréquemment utile. À noter l'instruction TEST
qui est un ET logique non destructif. Quand vous écrivez AND BH,DH,
vous faites un ET logique bit à bit entre les deux registres 8 bits BH
et DH et le résultat de ce ET logique se trouve après exécution dans
BH mais quand vous faites TEST BH,DH, le résultat n'est transféré
nulle part, cette instruction ne fait que positionner les indicateurs
en fonction du résultat d'ailleurs perdu. TEST est donc un ET logique
virtuel de même que CMP est une soustraction virtuelle, le résultat
est perdu mais on a positionné les indicateurs en fonction de ce
résultat, si donc ZF=1 après une comparaison, cela signifie que le
résultat de la soustraction eût été nul, en conséquence de quoi les
deux opérandes comparées sont égales. À noter l'instruction XOR AL,AL
qui est un Ou exclusif entre AL et lui-même, c'est une petite astuce
pour remettre AL à 0 (ou bien sûr AX ou EAX car ces syntaxes sont
extensibles, on peut écrire XOR AX,AX ou XOR EAX,EAX). Imaginons
maintenant qu'on veuille savoir la position du bit 3 et AL, on écrira
TEST AL, 00001000b, si ZF=1 après exécution de cette instruction alors
le résultat du ET logique aurait été nul en conséquence de quoi le
bit 3 testé est nul donc au zéro logique et si ZF=0 alors le résultat
du ET logique n'aurait pas été nul en conséquence de quoi le bit 3 est
au 1 logique, le bit testé est donc l'inverse de ZF, il est à 1 si
ZF=0 et à 0 si ZF=1. Imaginons qu'on veuille positionner au 1 logique
les bits 2 et 10 de BX, on écrira OR BX, 0000010000000100b. Imaginons
qu'on veuille annuler le bit 8 de EDX, on écrira
AND EDX, 11111111111111111111111011111111b ou encore, si l'on veut
éviter ce type d'écriture binaire AND EDX, 0FFFFFEFFh (dans la
notation assembleur, on doit faire précéder les valeurs hexadéciamales
par 0 lorsque celles-ci commencent par une lettre pour éviter la
confusion possible avec un label). Imaginons qu'on veuille inverser la
valeur du bit 5 de CH, on écrira XOR CH,00100000b. En règle générale,
on utilise le ET logique pour lire les bits ou les positionner au
0 logique, le OU logique pour positionner les bits au 1 logique et le
7
OU exclusif pour inverser l'état logique des bits.
Les instructions de rotations et décalages, ROL et ROR (rotation
circulaire à gauche et à droite, ROR rotation left, ROR rotation
right), RCL et RCR (décalage avec CF entrant), SHL et SHR (décalage
avec 0 entrant, SHL et SHR équivalent donc à RCL et RCR si CF=0, shift
left et shift right) et enfin SAR (décalage à droite avec réinjection
du bit de signe, shift arithmétique right) ne positionnent que CF qui
représente donc le bit sortant du décalage. À noter qu'il n'y a que 7
types de rotations et décalages (et non 8) car SAR n'a pas sa réplique
à gauche (le très gentil tasm32 accepte cependant l'instruction SAL
mais le convertit en SHL sans autre forme de procès). En effet, s'il
est signifiant de réinjecter le bit de signe i.e. le bit le plus à
gauche sur un décalage à droite, il n'est pas signifiant de réinjecter
le bit 0 sur un décalage à gauche. SAR permet de diviser par deux en
maintenant le signe tel quel, si donc la valeur était négative (en
considérant le complément à 2), elle restera négative après SAR car le
bit de signe a été réinjecté. À noter que s'agissant des rotations et
décalages, on a le choix entre l'exécuter une seule fois (e.g.
ROR AL,1) ou CL fois (e.g. ROR AL,CL), dans ce cas bien sûr il faut
renseigner CL par un mov juste avant par exemple MOV CL,3, ce qui aura
pour effet de faire exécuter trois fois le décalage ou la rotation.
La mémoire
La mémoire n'est jamais qu'une suite d'octets consécutifs. Le 8080 ne
pouvait lire ou écrire que huit bits à la fois (byte), le 8086 pouvait
lire ou écrire deux octets consécutifs (word), le 80386, lui, peut
lire ou écrire jusqu'à quatre octets consécutifs (double word). Mais
que la mémoire soit pointée par byte, word ou double word, elle reste
une suite d'octets numérotés et ces numéros ou indices sont des
adresses. Par conséquent, la règle est simple :
8
Syntaxes d'accès avec C++Builder
Il y a deux cas, le cas où la mémoire à pointer se trouve dans la pile
et le cas où elle se trouve dans le tas (heap en anglais). C'est sur
ce plan-là que les syntaxes pour pointer la mémoire diffèrent. Quand
vous déclarez une zone mémoire char zone[50] à l'intérieur d'une
fonction, cette zone se situe en pile mais quand vous déclarez un
pointeur P de type char* et que vous allouiez pour P de la mémoire par
new ou malloc, seul le pointeur déclaré (char* P;) se trouve en pile
mais la mémoire accordée par le système se situe dans le tas. Dans le
premier cas, on fera pointer par exemple ESI à l'adresse zone par
lea esi, zone alors que dans le deuxième cas, il faut lire le
contenu P dans ESI pour que ESI pointe la mémoire allouée par new ou
malloc et on doit alors écrire mov esi, [P] (les crochets entourant P
sont facultatifs, on voit que cette notation est un peu ambiguë car en
réalité on lit dans esi quatre octets à partir de l'adresse &P ce qui
fait que dans cette instruction assembleur P ne signifie pas P du C++
mais &P et esi équivaut alors après exécution au P du C++). ESI
pointant correctement la mémoire (puisqu'il équivaut au P du C++), la
suite du programme sera la même, simplement dans le premier cas ESI
pointe une zone dans la pile et dans le deuxième il pointe une zone
dans le tas.
#include <vcl.h>
#pragma hdrstop
#include "Unit14.h"
//--------------------------------------------------------------------
-------
#pragma package(smart_init)
#pragma resource "*.dfm"
#pragma inline
TForm1 *Form1;
//--------------------------------------------------------------------
-------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
9
l'avoir déposé n'importe où sur Form1). À l'intérieur de ce
gestionnaire nous allons déclarer une chaîne de caractères que l'on va
initialiser avec un message quelconque, on va déclarer une autre zone
de caractères que l'on ne va pas du tout initialiser puis en
assembleur, nous allons recopier la chaîne initialisée dans la zone
prévue à cet effet puis nous allons afficher dans un MessageBox le
contenu de cette zone où s'est effectuée la recopie. On constatera
alors que la chaîne a bien été recopiée puisque c'est la chaîne
recopiée que MessageBox va afficher et ce sera pour nous la preuve
qu'on pointe correctement la mémoire.
_asm
{
cld
lea esi,Message
lea edi,Zone
mov ecx,40
REP MOVSB
}
Application->MessageBox(Zone, "ok",MB_OK);
}
10
_asm
{
LongECX: push esi
mov ecx,0
LongECX10: mov al, [esi]
inc ecx
inc esi
test al,al
jnz LongECX10
pop esi
ret
}
Cette routine est une fonction comme une autre donc vous l'insérez
dans le source C/C++ où vous voulez entre deux fonctions C++. En
entrant dans le sous-programme LongECX, ESI est censé pointer
correctement la mémoire. Cela dit, on sauvegarde son adresse en pile,
ainsi au retour, esi pointera toujours le début de la chaîne à
recopier et ce, bien qu'il va bouger dans la routine elle-même. On
initialise le compteur ECX à 0. Puis la boucle commence. On lit dans
AL l'octet pointé par esi. À la place de mov AL, [esi] nous aurions pu
écrire mov AL, byte ptr [esi] mais la précision byte ptr n'est pas
utile ici puisque le compilateur sait bien que AL est sur 8 bits. On
n'écrit donc byte ptr ou word ptr ou encore dword ptr que lorsque
cette précision est nécessaire. Par exemple, si l'on veut écrire 0 à
l'adresse ESI, il faudra préciser s'il s'agit de 0 sur 8 bits, 0 sur
16 bits ou 0 sur 32 bits et donc choisir entre mov byte ptr [esi],0 ou
mov word ptr [esi],0 ou encore mov dword ptr [esi],0. L'octet étant lu
dans AL, on incrémente le compteur ecx et le pointeur esi puis on
teste al par l'instruction test al,al qui fait un ET logique entre al
et lui-même et ce, à seule fin de positionner les flags car nous
voulons savoir si l'octet lu est nul ou non. Cette instruction est
strictement équivalente à and al,al ou même or al,al car le ET logique
et le OU logique sont idempotents. Si l'octet lu dans AL n'est pas
nul, on saute à l'adresse LongECX10 où l'on va lire encore la mémoire
(mais esi a avancé d'une case entre temps donc on va lire l'octet
suivant). Tant qu'on aura à ce stade ZF=0, le saut aura lieu. Quand ZF
sera au 1 logique après le test, la boucle s'arrêtera et ce, parce que
le saut n'aura plus lieu. En effet, le saut a lieu si NZ mais n'a pas
lieu si Z. On restitue alors à esi sa position d'origine par pop esi
puis on retourne à l'appelant par l'instruction ret avec ECX qui
contient la longueur de la chaîne incluant le zéro de fin. On voit
qu'ici une erreur de programmation aurait été d'écrire le couple
PUSH ECX/POP ECX car alors le contenu de ECX ne serait pas retourné.
La relation entre le C++ et l'assembleur est inversée au sens où rien
en C++ correspond au PUSH/POP en assembleur (argument du type valeur)
alors que l'opérateur & en C++ correspond à rien en assembleur
(absence de PUSH/POP, argument du type référence). Dans ces
conditions, la fonction de recopie précédente sera légèrement
modifiée, notamment ECX étant calculé par la fonction LongECX, il n'a
plus à être initialisé par programme. Donc après avoir donné l'adresse
Message à esi par lea esi, Message on appelle la fonction LongECX par
call LongECX, puis on positionne EDI mais comme ECX a été calculé par
la routine LongECX, on supprime l'instruction qui lui donnait dans
notre exemple précédent la valeur 40. Voici l'ensemble de l'unité cpp
qui contient maintenant plus d'assembleur que de C++. Remarquez bien
la place de la routine LongECX à savoir comme une fonction C/C++ entre
deux fonctions.
11
#include <vcl.h>
#pragma hdrstop
#include "Unit14.h"
//-------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
#pragma inline
TForm1 *Form1;
//-------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//-------------------------------------------------------
_asm
{
LongECX: push esi
mov ecx,0
LongECX10: mov al, [esi]
inc ecx
inc esi
test al,al
jnz LongECX10
pop esi
ret
}
//-------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
char Message[]="Ceci est un message, vive l'assembleur!";
char Zone[100];
_asm
{
cld
lea esi,Message
call LongECX
lea edi,Zone
REP MOVSB
}
Application->MessageBox(Zone, "ok",MB_OK);
}
//-------------------------------------------------------
12
Deuxième cas : mémoire déclarée dans le
tas
Modifions le programme précédent. Nous allons déclarer notre message
de la même façon mais nous allons déclarer un pointeur P de type char*
puis nous allons allouer à ce pointeur une zone de 100 octets par new.
Cette zone va alors se situer dans le tas. On ne va donc plus pointer
la zone via l'instruction lea (load effective adresse) car lea
signifie qu'on donne au pointeur l'adresse directement et cette
adresse nous ne la connaissons pas mais nous savons qu'elle se situe à
l'adresse &P donc on va écrire mov edi, [P](car &P du C++ correspond à
P en assembleur, il en est ainsi dans la notation), ce qui signifie
que les 32 bits situés à partir de l'adresse &P sont lus dans EDI, le
microprocesseur va donc lire quatre contenus aux adresses consécutives
&P, &P+1, &P+2 et &P+3, ce qui va faire quatre octets qu'il va donner
au registre 32 bits edi (adresse de destination de la future recopie
du message initialisé). Dans ces conditions, EDI pointe la mémoire
allouée par new. La recopie s'effectue de la même façon.
_asm
{
cld
lea esi,Message
call LongECX //ECX=longueur de la chaîne
mov edi, [P]
REP MOVSB
}
Application->MessageBox(P, "ok",MB_OK);
delete P;
}
13
assembleur push esi puis faites F4 (exécution jusqu'au curseur). Là,
le programme s'exécute, cliquez sur le bouton, cela provoque l'arrêt
du programme à l'endroit où vous avez positionné le curseur. Faites
Alt v d c, vous obtenez la fenêtre CPU (Alt active le menu, v pour
voir, d pour debug et c pour CPU) ou encore appuyez simultanément sur
ctrl et Alt et faites c, la fenêtre CPU s'affiche. Le mieux est le
mode mixte où l'on vous donne à la fois le C++ et l'assembleur,
cliquez à droite dans la fenêtre où se trouve le code et sélectionnez
l'option "mixte" si elle n'est pas activée, cela permet le double
affichage C++/assembleur. Si vous vous êtes perdu suite à des scrolls
pour consulter le code de la fenêtre assembleur, cliquez à droite et
faites "allez à l'EIP en cours", vous vous retrouvez à l'instruction
où l'on est arrêté. En haut à droite, vous avez les registres internes
du microprocesseur, en bas à gauche une zone mémoire quelconque et en
bas à droite la pile. Regardez la valeur de esp, le pointeur de pile,
vous constatez que la fenêtre de pile vous affiche l'état de la pile,
une petite flèche verte vous montre la position du pointeur de pile
esp et en regard la dernière valeur empilée. Si vous voulez consulter
de la mémoire, utilisez la fenêtre en bas à gauche prévue à cet effet,
cliquez à droite et choisissez "aller à l'adresse". En général on veut
consulter une adresse contenue dans un registre, donc on donne le nom
de ce registre et on vous affiche le contenu de la mémoire à cet
endroit. Mais vous pouvez aussi donner une adresse, dans ce cas
n'oubliez pas d'entrer le préfixe "0x" de manière à donner une valeur
en hexadécimal (sinon C++Builder considère que c'est du décimal).
C:\Windows>
C:\Windows>cd ..
C:\>
14
petit go.bat qui va nous faire aller directement au répertoire asm.
Pour cela entrons sous Edit, le petit éditeur de texte du DOS (vous
pouvez créer ce petit "point bat" sous Windows avec NotePad, le bloc-
notes standard, ce sera la même chose). Donc entrons sous Edit :
C:>\Windows>edit go.bat
cd ..\"program files"\borland\cbuilder5\asm
C:>\Windows>go
C:>\Program Files\Borland\CBuilder5\asm>
Là, vous êtes sous éditeur de texte, entrez ces deux lignes :
15
Là, entrez le programme suivant ou mieux, comme c'est un peu long,
faites un copier-coller sous Windows avec NotePad, le résultat sera le
même, sauvegardez ce fichier sous le nom prg.asm.
.386
locals
jumps
.model flat,STDCALL
.data
titre db "In girum imus nocte et consumimur igni",0
texte db "Bienvenue dans les tutoriaux",10,10
db "Vive l'assembleur!",0
.code
Programme:
push 0 ; type de fenêtre
push offset titre
push offset texte
push 0
call MessageBoxA
ret ; retour sous DOS
End Programme
C:>\Program Files\Borland\CBuilder5\asm>c
C:>\Program Files\Borland\CBuilder5\asm>prg
16
de votre programme avec les codes hexadécimaux, rajouter l'option -l à
la commande tasm32 c'est-à-dire écrivez dans le c.bat la ligne
#pragma inline
void main(void)
{
_asm
{
Programme:
push 0
push offset titre
push offset texte
push 0
call MessageBoxA
jmp fin
fin:
}
}
17
"vive l''assembleur" (une double quote n'en vaut qu'une en
assembleur), on déclare la fonction externe MessageBoxA, on supprime
"End Programme" qui ne vaut qu'en assembleur pur. Ce fichier étant au
point, sauvez-le dans le répertoire nouveau précédemment créé sous le
nom prg.cpp. Puis faites "Fichier|Nouveau" et choisissez
"Expert console", là donnez dans la fenêtre le nom du programme
prg.cpp avec le chemin correct et cochez la case "Spécifiez le source
du projet". Puis faites "Fichier|Enregistrer le projet sous" et donnez
un nom à ce projet (laissez par exemple Projet1 proposé) car même un
simple programme console doit avoir un nom de projet associé en
C++Builder. Maintenant, vous pouvez exécutez le programme et donc
aussi l'exécuter au pas à pas en mode debug avec la fenêtre CPU.
glouise@club-internet.fr
Mai 2001
18