Des pointeurs et des hommes (2)

Previous Next

Et nous voilà repartis pour un nouveau chapitre dans notre découverte des pointeurs ! Maintenant que vous avez compris ce qu’est au final un pointeur, j’espère que tout cela vous fait beaucoup moins peur sur le côté théorique de la chose … Et oui, tout ça pour ça !

Pourtant le pointeur, malgré sa simplicité, est un concept fondamental du C. C’est vraiment l’un des points du langage qui le rendent, à mon humble avis, si proche de la machine et si passionnant ! Je vous propose donc de continuer notre parcours en regardant comment on les utilise sur un petit exemple de fonction.

Une fonction pourtant si simple …

Imaginons que nous écrivions un petit programme appelant une fonction d’affichage d’entier, que nous appellerons dis_moi_des_mots_doux:

main.c

#include <stdio.h>

void dis_moi_des_mots_doux( int ) ;

int 
main() 
{
    int A ;
    A = 69 ;
    dis_moi_des_mots_doux( A ) ;
    return 0 ;
}

void 
dis_moi_des_mots_doux( int n ) 
{
   printf("Oh ... un %d ! Comme c'est gentil !!!\n", n ) ;
}

On peut vérifier que notre fonction marche parfaitement :

$gcc -o thefunk main.c
$./thefunk
Oh ... un 69 ! Comme c'est gentil !!!

La valeur 69 est parfaitement transmise à la fonction dis_moi_des_mots_doux, qui l’affiche. Jusque là tout va bien !

Fort de ce succès, nous décidons d’ajouter la fonction remise_a_zero qui, comme vous l’aurez sûrement deviné, devra remettre à zéro la variable transmise. La plupart des débutants vont écrire une fonction ressemblant à celle-ci :

main_v2.c

#include <stdio.h>

void dis_moi_des_mots_doux( int ) ;
void remise_a_zero(int) ;

int 
main() 
{
    int A ;
    A = 69 ;
    dis_moi_des_mots_doux( A ) ;    // J'affiche A
    remise_a_zero( A ) ;            // Je remets A à zéro
    dis_moi_des_mots_doux( A ) ;    // Je réaffiche A
    return 0 ;
}

void 
dis_moi_des_mots_doux( int n ) 
{
   printf("Oh ... un %d ! Comme c'est gentil !!!\n", n ) ;
}

void
remise_a_zero( int k )
{
    k = 0 ;
}

Malheureusement, lorsque l’on teste cette fonction, notre déception est immense :

$gcc -o thefunk_v2 main_v2.c
$./thefunk_v2
Oh ... un 69 ! Comme c'est gentil !!!
Oh ... un 69 ! Comme c'est gentil !!!

A n’a pas changé de valeur.

Et c’est tout à fait normal !!! Car A elle-même n’est jamais transmis lors de l’appel de la fonction. C’est une copie de sa valeur qui est envoyée à la fonction.

Pour expliquer ce qui se passe, je vous propose deux manières de voir la chose : graphiquement, et en assembleur. Vous allez voir que c’est très intuitif.

Lorsqu’on appelle une fonction, les opérations suivantes sont effectuées, dans l’ordre :

  1. On évalue l’expression entre parenthèses ( C’est à dire on cherche sa valeur numérique )
  2. On saute à l’emplacement de la fonction
  3. Une variable locale est créée ( ici n dans la fonction dis_moi_des_mots_doux )
  4. La variable est initialisée avec notre valeur numérique.

Graphiquement, pour le premier appel de fonction, cela donne l’exécution suivante :

capture

Le problème, vous vous en doutez, est à l’appel de la fonction remise_a_zero, qui essaie de modifier A.

Voici les étapes suivantes de l’exécution de notre programme :

capture

Et oui, étant donné que seule une copie de la valeur contenue dans A est transmise à remise_a_zero, la variable A reste inchangée. Cela ne peut pas fonctionner de cette façon ! (trop dur pour nous !)

Voyons maintenant “the real thing”, et plongeons nous dans l’assembleur. Si cette étape vous fait peur, vous pouvez la sauter sans aucun problème : rendez-vous au chapitre suivant, un peu plus loin !

Compilons, désassemblons, et visualisons le code de notre programme :

$gcc -g -o thefunk_v2 mainv2.c --static
$objdump -S ./thefunk_v2

( Si vous n’avez pas les outils nécessaires, vous pouvez récupérer ce code assembleur ici

[...]

int
main()
{
  40174d:       55                      push   %rbp
  40174e:       48 89 e5                mov    %rsp,%rbp
  401751:       48 83 ec 10             sub    $0x10,%rsp
    int A ;
    A = 69 ;
  401755:       c7 45 fc 45 00 00 00    movl   $0x45,-0x4(%rbp)
    dis_moi_des_mots_doux( A ) ;    // J'affiche A
  40175c:       8b 45 fc                mov    -0x4(%rbp),%eax
  40175f:       89 c7                   mov    %eax,%edi
  401761:       e8 1b 00 00 00          call   401781 <dis_moi_des_mots_doux>
    remise_a_zero( A ) ;            // Je remets A à zéro
  401766:       8b 45 fc                mov    -0x4(%rbp),%eax
  401769:       89 c7                   mov    %eax,%edi
  40176b:       e8 35 00 00 00          call   4017a5 <remise_a_zero>
    dis_moi_des_mots_doux( A ) ;    // Je réaffiche A
  401770:       8b 45 fc                mov    -0x4(%rbp),%eax
  401773:       89 c7                   mov    %eax,%edi
  401775:       e8 07 00 00 00          call   401781 <dis_moi_des_mots_doux>
    return 0 ;
  40177a:       b8 00 00 00 00          mov    $0x0,%eax
}
  40177f:       c9                      leave
  401780:       c3                      ret

0000000000401781 <dis_moi_des_mots_doux>:

void
dis_moi_des_mots_doux( int n )
{
  401781:       55                      push   %rbp
  401782:       48 89 e5                mov    %rsp,%rbp
  401785:       48 83 ec 10             sub    $0x10,%rsp
  401789:       89 7d fc                mov    %edi,-0x4(%rbp)
   printf("Oh ... un %d ! Comme c'est gentil !!!\n", n ) ;
  40178c:       8b 45 fc                mov    -0x4(%rbp),%eax
  40178f:       89 c6                   mov    %eax,%esi
  401791:       48 8d 3d 70 f8 07 00    lea    0x7f870(%rip),%rdi        # 481008 <_IO_stdin_used+0x8>
  401798:       b8 00 00 00 00          mov    $0x0,%eax
  40179d:       e8 5e 84 00 00          call   409c00 <_IO_printf>
}
  4017a2:       90                      nop
  4017a3:       c9                      leave
  4017a4:       c3                      ret

00000000004017a5 <remise_a_zero>:

void
remise_a_zero( int k )
{
  4017a5:       55                      push   %rbp
  4017a6:       48 89 e5                mov    %rsp,%rbp
  4017a9:       89 7d fc                mov    %edi,-0x4(%rbp)
    k = 0 ;
  4017ac:       c7 45 fc 00 00 00 00    movl   $0x0,-0x4(%rbp)
}
  4017b3:       90                      nop
  4017b4:       5d                      pop    %rbp
  4017b5:       c3                      ret

[...]

A étant une variable locale, elle est créée dans la pile. Ceci est effectué en agrandissant ladite pile grâce à la l’instruction suivante :

401751:       48 83 ec 10             sub    $0x10,%rsp

La pile est augmentée de 16 octets en changeant la valeur de %rsp ( le stack pointer ). Ceci est largement suffisant pour stocker un int (4 octets).

La valeur 69 en (0x45 en hexadécimal) est ensuite stockée dans A, qui est à l’adresse rbp-4 (donc dans la pile, et plus spécifiquement dans le frame de main():

401755:       c7 45 fc 45 00 00 00    movl   $0x45,-0x4(%rbp)

Voyons maintenant comment se fait l’appel de la fonction remise_a_zero dans la fonction main :

  401766:       8b 45 fc                mov    -0x4(%rbp),%eax
  401769:       89 c7                   mov    %eax,%edi
  40176b:       e8 35 00 00 00          call   4017a5 <remise_a_zero>

La valeur de A est tout d’abord chargée dans le registre %eax depuis la pile. Elle est ensuite copiée dans le registre %edi puis l’exécution “saute” à l’adresse 4017a5, où se trouve la fonction remise_a_zero.

Ce passage dans %eax puis dans %edi peut surprendre : pourquoi ne pas mettre directement la valeur de A dans %edi ? Et bien tout simplement parce que l’expression entre parenthèses est d’abord évaluée, et ensuite seulement l’appel se fait.

Ainsi, on pourrait avoir des choses comme :

remise_a_zero( 2*A+3 ) ;

Dans ce cas, il faudrait d’abord évaluer l’expression (2*A+3), puis appeler remise_a_zero. Le compilateur a donc généré un code en deux étapes :

Quoi qu’il en soit, ni A ni sa position en mémoire ne sont transmises à la fonction : seulement une copie de sa valeur !

Pointeurs et fonctions

Les choses sont un peu plus claires maintenant : la fonction remise_a_zero ne fonctionne pas parce qu’aucune variable n’est “transmise” : seulement une copie de sa valeur.

C’est là que les pointeurs entrent en jeu : au lieu de transmettre la valeur de A, nous allons transmettre son adresse, afin que la fonction appelée puisse savoir où écrire. Et les adresses, nous les manipulons avec ce nouveau type de variable que nous avons vu la dernière fois : le pointeur !

Allons-y et modifions le main(), afin de transmettre l’adresse de A à notre fonction. On utilisera tout simplement l’opérateur & que nous avions vu la dernière fois :

int 
main() 
{
    int A ;
    A = 69 ;
    dis_moi_des_mots_doux( A ) ;    
    remise_a_zero( &A ) ;            // <= La magie est ici !!!
    dis_moi_des_mots_doux( A ) ;    
    return 0 ;
}

Puisque nous ne transmettons plus un entier mais une adresse d’entier, il va nous falloir adapter remise_a_zero :

void 
remise_a_zero( int* k)
{
    *k = 0 ;                        // Ne pas oublier l'opérateur * ici !!!
}

La variable locale k est désormais un pointeur, qui contiendra donc une adresse.

Pour écrire un zéro à cette adresse, on utilise l’opérateur * comme vu la dernière fois. Encore une fois, ceci veut dire :

Le code complet, ça donne ça :

main_v3.c

#include <stdio.h>

void dis_moi_des_mots_doux( int ) ;
void remise_a_zero(int*) ;

int 
main() 
{
    int A ;
    A = 69 ;
    dis_moi_des_mots_doux( A ) ;    
    remise_a_zero( &A ) ;           
    dis_moi_des_mots_doux( A ) ;  
    return 0 ;
}

void 
dis_moi_des_mots_doux( int n ) 
{
   printf("Oh ... un %d ! Comme c'est gentil !!!\n", n ) ;
}

void
remise_a_zero( int* k )
{
    *k = 0 ;
}

Quoi ? Vous voulez un autre petit dessin ? Vraiment ?

Mais heuuuuuuu … Ça prend trois plombes à faireeeeeeuuuuu !!!!!!!!!!!!!!

Bon, comme je vous aime bien, le voici :

capture

Et ça devrait fonctionner ! On teste ça de suite :

$gcc -o thefunk_v3 main_v3.c
$./thefunk_v3
Oh ... un 69 ! Comme c'est gentil !!!
Oh ... un 0 ! Comme c'est gentil !!!

Je crois que nous pouvons appeler ceci une victoire !

Encore une fois, allons voir le code assembleur. Ceux qui veulent peuvent encore une fois sauter au prochain chapitre :)

[...]

int
main()
{
  40174d:       55                      push   %rbp
  40174e:       48 89 e5                mov    %rsp,%rbp
  401751:       48 83 ec 10             sub    $0x10,%rsp
  401755:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  40175c:       00 00
  40175e:       48 89 45 f8             mov    %rax,-0x8(%rbp)
  401762:       31 c0                   xor    %eax,%eax
    int A ;
    A = 69 ;
  401764:       c7 45 f4 45 00 00 00    movl   $0x45,-0xc(%rbp)
    dis_moi_des_mots_doux( A ) ;
  40176b:       8b 45 f4                mov    -0xc(%rbp),%eax
  40176e:       89 c7                   mov    %eax,%edi
  401770:       e8 31 00 00 00          call   4017a6 <dis_moi_des_mots_doux>
    remise_a_zero( &A ) ;
  401775:       48 8d 45 f4             lea    -0xc(%rbp),%rax
  401779:       48 89 c7                mov    %rax,%rdi
  40177c:       e8 49 00 00 00          call   4017ca <remise_a_zero>
    dis_moi_des_mots_doux( A ) ;
  401781:       8b 45 f4                mov    -0xc(%rbp),%eax
  401784:       89 c7                   mov    %eax,%edi
  401786:       e8 1b 00 00 00          call   4017a6 <dis_moi_des_mots_doux>
    return 0 ;
  40178b:       b8 00 00 00 00          mov    $0x0,%eax
}
  401790:       48 8b 55 f8             mov    -0x8(%rbp),%rdx
  401794:       64 48 2b 14 25 28 00    sub    %fs:0x28,%rdx
  40179b:       00 00
  40179d:       74 05                   je     4017a4 <main+0x57>
  40179f:       e8 dc 62 04 00          call   447a80 <__stack_chk_fail>
  4017a4:       c9                      leave
  4017a5:       c3                      ret

[...]

void
remise_a_zero( int* k )
{
  4017ca:       55                      push   %rbp
  4017cb:       48 89 e5                mov    %rsp,%rbp
  4017ce:       48 89 7d f8             mov    %rdi,-0x8(%rbp)
    *k = 0 ;
  4017d2:       48 8b 45 f8             mov    -0x8(%rbp),%rax
  4017d6:       c7 00 00 00 00 00       movl   $0x0,(%rax)
}
  4017dc:       90                      nop
  4017dd:       5d                      pop    %rbp
  4017de:       c3                      ret
  4017df:       90                      nop

[...]

Si la création de A n’a pas beaucoup changée (A est toujours en pile), La pile contient maintenant également un canari. Ce n’est pas l’objet du cours d’aujourd’hui, on en fera donc abstration.

Par contre, l’affectation de la valeur 69 à A est toujours réalisée de la même façon :

401764:       c7 45 f4 45 00 00 00    movl   $0x45,-0xc(%rbp)

L’appel de la fonction remise_a_zero a cependant bien changé :

  remise_a_zero( &A ) ;
401775:       48 8d 45 f4             lea    -0xc(%rbp),%rax
401779:       48 89 c7                mov    %rax,%rdi
40177c:       e8 49 00 00 00          call   4017ca <remise_a_zero>

On utilise non plus l’instruction mov, mais l’instruction lea. Et celle-ci va stocker l’adresse de A (%rbp-0xc) dans %rax.

Vous le voyez, plus de mensonge : tout se passe bien comme prévu !

Le mot de la fin

Ce chapitre est déjà bien long, alors nous allons repousser quelques-uns des points que je voulais traiter ici à une prochaine fois. Pour conclure, je voudrais juste souligner combien le C peut être clair quand il s’agit des fonctions.

Si vous voyez une fonction déclarée de cette façon-ci :

int fonction_mystere ( int A, double* B, int *C ) ;

Vous savez qu’après un appel à cette fonction, le premier paramètre transmis n’a pas pû être modifié par celle-ci, alors que les deux suivants oui ! Cela permet de “compartimenter” ses variables, et de garder un certain control même si vous utilisez des fonctions en provenance de librairies obscures.

Par contre, il va falloir faire très attention à ne pas utiliser des adresses invalides et on a très vite fait de se tirer une balle dans le pied ! Les typedefs, notamment, peuvent cacher des pointeurs à votre regard aiguisé.

Je vous dis à bientôt, et n’hésitez pas à m’envoyer vos questions !

Rancune.