Corrigé du TP 3 : Manipulation de la pile



Variables locales/globales

Pile.c Pile.s
  1. /* Déclaration des variables globales */
  2. char a,b;
  3. unsigned char c,d;
  4. short e;
  5. unsigned short f;
  6. short g;
  7. int h,i;
  8. long j;
  9. double k;
  10. int main()
  11. {
  12. /* Déclaration des variables locales */
  13. char ma,mb;
  14. unsigned char mc;md;
  15. short me = 12;
  16. unsigned short mf;
  17. short mg;
  18. int mh,mi;
  19. long mj;
  20. double mk=2,5;
  21. /* Quelques instructions quand même... */
  22. a=ma; b=mb; c=mc; d=md; e=me; f=mf;
  23. g=mg; h=mh; i=mi; j=mj; k=mk;
  24. g=-5;
  25. return 0;
  26. }
  1. LC0:
  2. .long 0, 1074003968 ; 10000000000010...0b (40 bits à 0)
  3. .text
  4. .globl main
  5. main:
  6. pushl %ebp
  7. movl %esp, %ebp
  8. subl $40, %esp ; allocation de 40 octets sur la pile
  9. andl $-16, %esp
  10. movl $0, %eax
  11. subl %eax, %esp
  12. movw $12, -6(%ebp) ; me <= 12
  13. fldl LC0 ; charge LC0..LC0+7 dans le coprocesseur
  14. fstpl -32(%ebp) ; mk <= valeur flottante (coprocesseur)
  15. movzbl -1(%ebp), %eax ; eax <= ma (1 octet), remplissage avec 24x0
  16. movb %al, a
  17. movzbl -2(%ebp), %eax ; eax <= mb (1 octet), remplissage avec 24x0
  18. movb %al, b
  19. movzbl -3(%ebp), %eax ; eax <= mc (1 octet), remplissage avec 24x0
  20. movb %al, c
  21. movzbl -4(%ebp), %eax ; eax <= md (1 octet), remplissage avec 24x0
  22. movb %al, _d
  23. movzwl -6(%ebp), %eax ; eax <= me (2 octets), remplissage avec 16x0
  24. movw %ax, e
  25. movl -8(%ebp), %eax ; eax <= mf (2 octets), lecture de 32 bits
  26. movw %ax, f
  27. movzwl -10(%ebp), %eax ; eax <= mg (2 octets), remplissage avec 16x0
  28. movw %ax, g
  29. movl -16(%ebp), %eax ; eax <= mh (4 octets)
  30. movl %eax, h
  31. movl -20(%ebp), %eax ; eax <= mi (4 octets)
  32. movl %eax, i
  33. movl -24(%epp), %eax ; eax <= mj (4 octets)
  34. movl %eax, j
  35. fldl -32(%ebp) ; coprocesseur <= mk (8 octets)
  36. fstpl k ; k <= coprocesseur (8 octets)
  37. movw $-5, g
  38. movl $0, %eax
  39. leave
  40. ret
  41. ;réservation des variables globales
  42. .comm a, 16 # 1
  43. .comm b, 16 # 1
  44. .comm c, 16 # 1
  45. .comm d, 16 # 1
  46. .comm e, 16 # 2
  47. .comm f, 16 # 2
  48. .comm g, 16 # 2
  49. .comm h, 16 # 4
  50. .comm i, 16 # 4
  51. .comm j, 16 # 4
  52. .comm k, 16 # 8
Contenu de la mémoire :
-
32
-
31
-
30
-
29
-
28
-
27
-
26
-
25
-
24
-
23
-
22
-
21
-
20
-
19
-
18
-
17
-
16
-
15
-
14
-
13
-
12
-
11
-
10
-
9
-
8
-
7
-
6
-
5
-
4
-
3
-
2
-
1
ebp
 
mk mj mi mh mg mf me md mc mb ma

Vous noterez que les variables globales a...k sont situées en dehors de la pile, sur le tas statique.
La directive .comm réserve en effet de la mémoire dans le segment de données et lui attribue un nom (une étiquette).

Noter le trou situé entre mg et mh, aux adresses ebp-12 et ebp-11.
C'est une optimisation faite par GCC pour aligner mh, mi, mj et mk sur des mots-mémoire de 32 bits.
En effet, le 80386 a un bus de données de 32 bits. Il lit donc de toute façon 32 bits simultanément.
Lire 32 bits à partir de l'adresse 18 demanderait donc deux accès successifs : de 16 à 19, puis de 20 à 23.

Concernant la représentation des données en mémoire, le programme mentionne trois constantes 12, -5, et 2.5.
12 se traduit naturellement par la valeur 12... soit 0000 0000 0000 0000 0000 0000 0000 11002.
-5 se traduit par 0xFFFB, soit 1111 1111 1111 10112. Ce qui donne, en complément à 2 : compl(1..1 1011 - 1) = 0..0 01012 = 5.
2.5 se traduit par 0100 0000 0000 0100 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 00002. Soit :


Pile_.c Pile_.s
  1. /* Déclaration des variables globales */
  2. char a;
  3. short e;
  4. unsigned short f;
  5. short g;
  6. char b;
  7. int h;
  8. unsigned char c;
  9. long j;
  10. unsigned char d;
  11. double k;
  12. int i;
  13. int main()
  14. {
  15. /* Déclaration des variables locales */
  16. char ma;
  17. short me;
  18. unsigned short mf;
  19. short mg;
  20. char mb;
  21. int mh;
  22. unsigned char mc;
  23. long mj;
  24. unsigned char md;
  25. double mk;
  26. int mi;
  27. /* Quelques instructions quand même... */
  28. a=ma; b=mb; c=mc; d=md; e=me; f=mf;
  29. g=mg; h=mh; i=mi; j=mj; k=mk;
  30. return 0;
  31. }
  1. .text
  2. .globl main
  3. main:
  4. pushl %ebp
  5. movl %esp, %ebp
  6. subl $56, %esp ; allocation de 56 octets sur la pile
  7. andl $-16, %esp
  8. movl $0, %eax
  9. subl %eax, %esp
  10. movzbl -1(%ebp), %eax ; eax <= ma (1 octet), remplissage avec 24x0
  11. movb %al, a
  12. movzbl -9(%ebp), %eax ; eax <= mb (1 octet), remplissage avec 24x0
  13. movb %al, b
  14. movzbl -17(%ebp), %eax ; eax <= mc (1 octet), remplissage avec 24x0
  15. movb %al, c
  16. movzbl -25(%ebp), %eax ; eax <= md (1 octet), remplissage avec 24x0
  17. movb %al, d
  18. movl -4(%ebp), %eax ; eax <= me (2 octets), remplissage avec 16x0
  19. movw %ax, e
  20. movzwl -6(%ebp), %eax ; eax <= mf (2 octets), lecture de 32 bits
  21. movw %ax, f
  22. movl -8(%ebp), %eax ; eax <= mg (2 octets), remplissage avec 16x0
  23. movw %ax, g
  24. movl -16(%ebp), %eax ; eax <= mh (4 octets)
  25. movl %eax, h
  26. movl -44(%ebp), %eax ; eax <= mi (4 octets)
  27. movl %eax, i
  28. movl -24(%ebp), %eax ; eax <= mj (4 octets)
  29. movl %eax, j
  30. fldl -40(%ebp) ; coprocesseur <= mk (8 octets)
  31. fstpl k ; k <= coprocesseur (8 octets)
  32. movl $0, %eax
  33. leave
  34. ret
  35. ;réservation des variables globales
  36. .comm a, 16 # 1
  37. .comm e, 16 # 2
  38. .comm f, 16 # 2
  39. .comm g, 16 # 2
  40. .comm b, 16 # 1
  41. .comm h, 16 # 4
  42. .comm c, 16 # 1
  43. .comm j, 16 # 4
  44. .comm d, 16 # 1
  45. .comm k, 16 # 8
  46. .comm i, 16 # 4
Contenu de la mémoire :
-
44
-
43
-
42
-
41
-
40
-
39
-
38
-
37
-
36
-
35
-
34
-
33
-
32
-
31
-
30
-
29
-
28
-
27
-
26
-
25
-
24
-
23
-
22
-
21
-
20
-
19
-
18
-
17
-
16
-
15
-
14
-
13
-
12
-
11
-
10
-
09
-
08
-
07
-
06
-
05
-
04
-
03
-
02
-
01
ebp
 
mi mk md mj mc mh mb mg mf me ma

On notait que GCC laisse un certain nombre de trous afin d'aligner les variables short sur des adresses paires, les variables int sur des adresses multiples de 4, et la variable double sur une adresse multiple de 8.

Ce comportement a changé avec les versions récentes de GCC. Il réordonne désormais les variables pour éviter un maximum de trous dans la pile...


Si l'on remplace les lignes int h et int mh respectivement par static int h et static int mh, on observe que la variable locale mh devient globale, c'est à dire située hors de la pile. Son nom devient alors mh.xyz avec xyz un entier aléatoire.De cette façon, sa valeur est conservée entre deux appels de la fonction.


Appel de fonctions

Pile2.c Pile2.s
  1. int a;
  2. main2()
  3. {
  4. int b;
  5. a = appel_fonction(b);
  6. return 0;
  7. }
  8. main()
  9. {
  10. return main2();
  11. }
  12. int appel_fonction(int c)
  13. {
  14. int d;
  15. d = c;
  16. return(d);
  17. }
  1. .comm a,4,4 ; variable globale : a
  2. .text
  3. .globl main2
  4. main2:
  5. pushl %ebp
  6. movl %esp, %ebp
  7. subl $40, %esp ; allocation de 40 octets sur la pile
  8. movl -12(%ebp), %eax
  9. movl %eax, (%esp) ; appel_fonction.c <= eax = b
  10. call appel_fonction
  11. movl %eax, a ; a <= résultat(appel_fonction)
  12. movl $0, %eax ; résultat(main) <= 0
  13. leave
  14. ret
  15. .globl main
  16. main:
  17. ; Cadre de pile de la fonction main... à ignorer
  18. pushl %ebp
  19. movl %esp, %ebp
  20. andl $-16, %esp
  21. call main2
  22. leave
  23. ret
  24. .globl appel_fonction
  25. appel_fonction:
  26. pushl %ebp
  27. movl %esp, %ebp
  28. subl $16, %esp
  29. movl 8(%ebp), %eax ; résultat(appel_fonction) = c
  30. movl %eax, -4(%ebp)
  31. movl -4(%ebp), %eax
  32. leave
  33. ret

Sachant que le compilateur C lit le programme de haut en bas (dans ce sens), il ne connait pas la fonction appel_fonction au moment où elle est appelée dans la fonction principale. Il suppose donc que son format est correct, et crée le profil correspondant. Il vérifiera alors les appels suivant en se basant sur ce profil.

Contenu de la mémoire à la ligne 40 (ligne 17 du C) :
...
adresse de retour Fonction main2
EBPt-1 EBPt-2(main)
b
 
 
 
 
c Fonction appel_fonction (Créé par main2)
adresse de retour
(= ligne 13)
EBPt EBPt-1
ESP d

On remarque que l'appel de la fonction main2 par la fonction main ne traduit pas le return. En effet, main2 stocke son résultat dans le registre eax. main doit également stocker son résultat dans le registre eax. Il faudrait donc écrire movl %eax, %eax, ce qui est totalement inutile. Le compilateur retire donc cette instruction. Intelligent, non ?


Optimisation de l'appel de fonctions

$gcc -S -O3 pile2.c -o pile2.opt.s

Pile2.c Pile2.opt.s
  1. int a;
  2. main2()
  3. {
  4. int b;
  5. a = appel_fonction(b);
  6. return 0;
  7. }
  8. main()
  9. {
  10. return main2();
  11. }
  12. int appel_fonction(int c)
  13. {
  14. int d;
  15. d = c;
  16. return(d);
  17. }
  1. .text
  2. .globl main2
  3. main2:
  4. ; Pas de cadre de pile ici
  5. movl $0, a ; La valeur de b est inconnue. pourquoi pas 0 ?
  6. xorl %eax, %eax ;return 0;
  7. ret
  8. .globl main
  9. main:
  10. movl $0, a ; La valeur de b est inconnue. pourquoi pas 0 ?
  11. xorl %eax, %eax ; return 0;
  12. ret
  13. .globl appel_fonction
  14. appel_fonction:
  15. ; Pas de cadre de pile ici
  16. movl 4(%esp), %eax
  17. ret
  18. .comm a,4,4 ; variable globale a

On constate que la version optimisée de ce programme ne contient plus les variables b et d. Le schéma mémoire se réduit donc aux deux cadres de piles... vides !

Et encore, on peut noter que la fonction appel_fonction n'est plus appelée du tout. GCC a fait ce que l'on nomme inlining : Il a copié-collé le contenu de la fonction dans main2 et dans main.
La fonction main se résume donc à remplir la variable a aléatoirement ( ici 0 ) et à retourner 0.

Les fonctions ne sont conservées que parce qu'elles sont publiques. Si un programme lie le fichier objet à d'autres, ces fonctions doivent exister sous peine d'erreur d'édition des liens...
Si on rend la fonction privée static int appel_fonction(int c), elle disparaît totalement du fichier objet et de son source assembleur associé...



Retour à la liste des TP