Précédent   Forum du club des développeurs et IT Pro > Autres langages > Assembleur > x86 32-bits / 64-bits
x86 32-bits / 64-bits Architectures x86 32/64 bits et leurs outils (assembleurs, debuggers, émulateurs...)
Partagez cette discussion sur d'autres réseaux sociaux : Viadeo Twitter Google Facebook Digg Delicious MySpace Yahoo
Réponse
 
Outils de la discussion
Publicité
'
Vieux 27/09/2012, 19h14   #1
PyNub
Membre régulier
 
Inscription : mai 2010
Messages : 253
Détails du profil
Informations forums :
Inscription : mai 2010
Messages : 253
Points : 95
Points : 95
Par défaut Besoin d'explication sur une sortie de g++ en assembleur

Bonjour,

Le programme en assembleur suivant :
Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
 
_Z1fi:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	%edi, -4(%rbp)
	addl	$1, -4(%rbp)
	movl	-4(%rbp), %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	_Z1fi, .-_Z1fi
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	$1, -4(%rbp)
	movl	-4(%rbp), %eax
	movl	%eax, %edi
	call	_Z1fi
	movl	%eax, -4(%rbp)
	movl	$0, %eax
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
correspond à la sortie de g++ en assembleur du programme suivant en c++ :
Code :
1
2
3
4
5
6
int f(int x){return ++x;}
 
int main(){
	int x=1;
	x=f(x);
}
Je ne comprends pas pourquoi le programme effectue la copie dans deux registre de la valeur de x et place de plus cette valeur sur la pile :
Code :
1
2
3
4
5
6
7
 
# là
	movl	$1, -4(%rbp)
# là
	movl	-4(%rbp), %eax
# et là
	movl	%eax, %edi
d'autant que la fonction f ne semble pas utiliser la valeur passer par la pile et utilise à la place le contenu de edi :
Code :
1
2
3
 
	movl	%edi, -4(%rbp)
	addl	$1, -4(%rbp)
Quelqu'un a-t-il une explication ?
PyNub est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 28/09/2012, 14h25   #2
Obsidian
Modérateur
 
Avatar de Obsidian
 
Homme
Chercheur d'emploi
Inscription : septembre 2007
Messages : 4 639
Détails du profil
Informations personnelles :
Sexe : Homme
Âge : 37
Localisation : France, Essonne (Île de France)

Informations professionnelles :
Activité : Chercheur d'emploi
Secteur : High Tech - Éditeur de logiciels

Informations forums :
Inscription : septembre 2007
Messages : 4 639
Points : 11 097
Points : 11 097
Bonjour,

Tout d'abord, il faut savoir qu'en notation A&AT (celle produite par défaut par GCC et à l'opposée de la syntaxe Intel habituelle) place l'opérande source avant la destination : « mov source, dest ».

Ensuite, la pile est utilisée à tout et n'importe quoi, mais principalement à ces choses-là :
  • Au niveau de l'assembleur, elle sert à sauvegarder des registres temporairement et à enregistrer les adresses de retour lors de CALL ou d'interruptions, logicielles comme matérielles ;
  • Au niveau du C, le compilateur s'en sert pour définir les cadres de pile, passer les arguments à une fonction et stocker les variables locales. C'est le moyen le plus facile pour allouer de la mémoire rapidement, spécialement lorsque tu utilises des fonctions récursives, et c'est aussi pour cela qu'elles disparaissent une fois que tu as quitté la fonction.

L'accumulateur A (donc « AX » sur Intel et ses déclinaisons AL, AH, EAX et RAX) est le seul registre dont tu es à peu près sûr qu'il soit disponible sur tous les micro-processeurs et qu'il soit accessible par l'ALU, et c'est à cela qu'il sert initialement (un peu comme l'écran d'une calculatrice quatre opérations qui sert à la fois à entrer la donnée et à recevoir le résultat de l'opération). Donc, dans cet esprit et sauf dispositions contraires, RAX est utilisé par défaut pour recevoir le résultat d'une évaluation.

C'est aussi lui qui contiendra la valeur finale d'une fonction. En C et C++, on t'oblige en principe à utiliser « return » mais certains langages comme le Perl utilise la dernière valeur évaluée. C'est pour cela qu'on voit parfois une constante toute seule sur une ligne à la fin d'un programme.

D'autre part, si la manière normale d'exécuter une fonction est donc de passer les arguments sur la pile et d'en recevoir le résultat dans EAX par défaut, il est possible de demander au compilateur de passer les premiers arguments dans des registres lorsqu'ils sont disponibles, et le reste dans pile, normalement. C'est intéressant parce que cela permet d'exploiter les ressources à disposition, d'éviter des transactions mémoire (et donc d'accélérer notablement les choses), et d'économiser la pile, spécialement dans le cas des fonctions récursives, encore une fois.

Donc, fort de ces informations :
  • movl $1, -4(%rbp) met la valeur « 1 » dans la pile. Ça correspond à l'initialisation de ta variable : « int x=1; » ;
  • movl -4(%rbp), %eax met la valeur de « x » dans EAX. Ça correspond à l'évaluation de l'expression précédente. L'affectation d'une valeur à une variable est une expression qui peut être évaluée, qui correspond à la valeur affectée et qui permet donc d'écrire des choses comme « x=y=1 » ;
  • movl %eax, %edi correspond au passage de « x », en paramètre de ta fonction, avec optimisation puisque l'on transmet ce paramètre directement dans un registre plutôt que par la pile.

À noter que dans ce dernier cas, on aurait pu directement utiliser EAX, ce qui en plus aurait bien collé à la philosophie d'accumulateur de ce registre. Mais il arrive que les fonctions aient besoin d'un registre à tout faire en entrant dans la fonction et, surtout, la manière dont les registres vont être utilisés lors de ces optimisations doit nécessairement être normalisé.

Notons enfin que si on ne s'intéresse qu'à la valeur d'une valeur et que l'on n'a aucune intention de s'y référer par adresse (en passant un pointeur dessus à quelque chose), on pouvait utiliser explicitement le mot-clé « register » devant la déclaration d'une variable. Ce mot-clé est cependant tombé en désuétude car il ne s'agit que d'une indication au compilateur, qui peut l'ignorer si quelqu'un récupère le pointeur de la variable ou s'il n'y a pas assez de registres disponibles et, a contrario, peut se permettre de faire directement l'optimisation même sans indication du programmeur s'il sait que cette variable ne sera jamais atteinte autrement que par la fonction concernée et sans utiliser son pointeur.
Obsidian est déconnecté   Envoyer un message privé Réponse avec citation 10
Vieux 28/09/2012, 17h57   #3
PyNub
Membre régulier
 
Inscription : mai 2010
Messages : 253
Détails du profil
Informations forums :
Inscription : mai 2010
Messages : 253
Points : 95
Points : 95
Bonjour et merci pour cette réponse détaillée,

Citation:
Envoyé par Obsidian Voir le message
Donc, fort de ces informations :
  • movl $1, -4(%rbp) met la valeur « 1 » dans la pile. Ça correspond à l'initialisation de ta variable : « int x=1; » ;
  • movl -4(%rbp), %eax met la valeur de « x » dans EAX. Ça correspond à l'évaluation de l'expression précédente. L'affectation d'une valeur à une variable est une expression qui peut être évaluée, qui correspond à la valeur affectée et qui permet donc d'écrire des choses comme « x=y=1 » ;
  • movl %eax, %edi correspond au passage de « x », en paramètre de ta fonction, avec optimisation puisque l'on transmet ce paramètre directement dans un registre plutôt que par la pile.

À noter que dans ce dernier cas, on aurait pu directement utiliser EAX, ce qui en plus aurait bien collé à la philosophie d'accumulateur de ce registre. Mais il arrive que les fonctions aient besoin d'un registre à tout faire en entrant dans la fonction et, surtout, la manière dont les registres vont être utilisés lors de ces optimisations doit nécessairement être normalisé.
Oui, il est évident qu'un passage par registre prend moins de temps aussi pourquoi ne pas faire depuis le main directement movl $1, %edi et récupéré dans la fonction la valeur de edi ? Je comprends que le compilateur puisse utiliser plusieurs "options" de passage d'argument quand la fonction appelée ne fait pas partie de la même unité de compilation mais là le compilateur à toutes les informations nécessaire pour choisir un passage par pile ou par registre.

edit : après je comprends que l'automatisation de la traduction du code source en code machine par un programme puisse générer des comportement automatique de la part du compilateur. Peut-être s'agit il de cela ?

re edit: de plus il est nécessaire de stocker en mémoire et non pas de la laisser en registre la valeur de x. Donc l'écriture de 1 sur la pile se justifie. Ce que je ne comprend pas c'est le passage par eax alors que movl -4(%rbp), %edi aurait suffit...
PyNub est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 28/09/2012, 18h17   #4
PyNub
Membre régulier
 
Inscription : mai 2010
Messages : 253
Détails du profil
Informations forums :
Inscription : mai 2010
Messages : 253
Points : 95
Points : 95
ah je crois que j'ai compris ce que tu veux dire par évaluer en plaçant la valeur dans eax on peut évaluer x=1 par le résultat de cette opération sur le registre d'état (?)

ce que ne permet peut-être pas (movl $1, -4(%rbp) (?)
PyNub est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 28/09/2012, 22h47   #5
Obsidian
Modérateur
 
Avatar de Obsidian
 
Homme
Chercheur d'emploi
Inscription : septembre 2007
Messages : 4 639
Détails du profil
Informations personnelles :
Sexe : Homme
Âge : 37
Localisation : France, Essonne (Île de France)

Informations professionnelles :
Activité : Chercheur d'emploi
Secteur : High Tech - Éditeur de logiciels

Informations forums :
Inscription : septembre 2007
Messages : 4 639
Points : 11 097
Points : 11 097
Voila. C'est tout cela à la fois.

Pour le reste, bien des optimisations peuvent être faites mais cela demande une heuristique qui consiste à analyser la fonction entière. On ne sait pas où se trouve « l'horizon » du compilateur en ce domaine.

Par contre, il serait intéressant que tu passes des options d'optimisation explicites telles que « -O3 » à g++ et voir quel genre de code il produit dans ce cas.
Obsidian est déconnecté   Envoyer un message privé Réponse avec citation 10
Vieux 28/09/2012, 23h12   #6
PyNub
Membre régulier
 
Inscription : mai 2010
Messages : 253
Détails du profil
Informations forums :
Inscription : mai 2010
Messages : 253
Points : 95
Points : 95
Citation:
Par contre, il serait intéressant que tu passes des options d'optimisation explicites telles que « -O3 » à g++ et voir quel genre de code il produit dans ce cas.
En effet en faisant une optimisation maximale ce n'est plus la même chose :
Code :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
	.file	"exo10.cpp"
	.text
	.p2align 4,,15
	.globl	_Z1fi
	.type	_Z1fi, @function
_Z1fi:
.LFB0:
	.cfi_startproc
	leal	1(%rdi), %eax
	ret
	.cfi_endproc
.LFE0:
	.size	_Z1fi, .-_Z1fi
	.section	.text.startup,"ax",@progbits
	.p2align 4,,15
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	xorl	%eax, %eax
	ret
	.cfi_endproc
.LFE1:
	.size	main, .-main
	.ident	"GCC: (SUSE Linux) 4.6.2"
	.section	.comment.SUSE.OPTs,"MS",@progbits,1
	.string	"Ospwg"
	.section	.note.GNU-stack,"",@progbits
Mise à zéro du registre eax, appelle de la fonction, incrémentation de eax et retour...

Merci encore pour tes explications

edit : en fait même pas... la fonction n'est pas appelée...
PyNub est déconnecté   Envoyer un message privé Réponse avec citation 00
Vieux 29/09/2012, 00h31   #7
Obsidian
Modérateur
 
Avatar de Obsidian
 
Homme
Chercheur d'emploi
Inscription : septembre 2007
Messages : 4 639
Détails du profil
Informations personnelles :
Sexe : Homme
Âge : 37
Localisation : France, Essonne (Île de France)

Informations professionnelles :
Activité : Chercheur d'emploi
Secteur : High Tech - Éditeur de logiciels

Informations forums :
Inscription : septembre 2007
Messages : 4 639
Points : 11 097
Points : 11 097
Citation:
Envoyé par PyNub Voir le message
edit : en fait même pas... la fonction n'est pas appelée...
Non, effectivement. :-)
Le compilo s'est rendu compte que non seulement tu ne l'appelles qu'une fois, mais tu n'utilise jamais sa valeur de retour, même si tu l'affectes à x. Dans le même esprit, on aurait très bien pu se passer de compiler la fonction elle-même, mais ce n'était pas possible pour plusieurs raisons :

— Il aurait fallu pousser l'heuristique au delà des frontières de la fonction pour la poursuivre dans la fonction appelée. C'est possible mais ça complique beaucoup les choses ;
— Une fonction peut avoir des effets de bords même lorsque l'on utilise jamais sa valeur de retour. C'est le cas de toutes les fonctions que l'on utilise comme des commandes ;
— Le programme compilé, même s'il s'agit d'un exécutable, reste une ressource qui peut être utilisée comme une bibliothèque sous certaines conditions. La fonction se doit donc d'exister quand même pour pouvoir éventuellement être exploitée par un projet externe.
Obsidian est déconnecté   Envoyer un message privé Réponse avec citation 10
Vieux 29/09/2012, 01h04   #8
PyNub
Membre régulier
 
Inscription : mai 2010
Messages : 253
Détails du profil
Informations forums :
Inscription : mai 2010
Messages : 253
Points : 95
Points : 95
Oui en effet l'utilisation de la fonction par une autre unité de compilation doit pouvoir être envisagée et cette information est hors de portée du compilateur.

L'optimisation permet dans cette exemple d'économiser plusieurs accès mémoire. Autant dire que les options d'optimisation de gcc sont indispensable à la création d'un programme efficace.

En fait la compilation sans optimisation équivaut à une traduction "mot à mot" du programme source.

Merci pour toutes ces précisions.
PyNub est déconnecté   Envoyer un message privé Réponse avec citation 00
Réponse Cette discussion est résolue.
Outils de la discussion

Navigation rapide


Fuseau horaire GMT +2. Il est actuellement 03h11.


 
 
 
 
Partenaires

Hébergement Web