IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C Discussion :

Problème manipulation du contexte d'exécution


Sujet :

C

  1. #1
    Nouveau membre du Club
    Homme Profil pro
    etudiant info
    Inscrit en
    Mars 2016
    Messages
    32
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Nord (Nord Pas de Calais)

    Informations professionnelles :
    Activité : etudiant info

    Informations forums :
    Inscription : Mars 2016
    Messages : 32
    Points : 30
    Points
    30
    Par défaut Problème manipulation du contexte d'exécution
    Bonsoir,

    J'étudie actuellement les contextes d'exécution en langage C. Pour cela, nous avons parfois à introduire du code assembleur modifiant les registres esp et ebp.

    Voici un petit programme que j'ai créé pour essayer de comprendre le fonctionnement de la pile d'exécution.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    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
     
    #include <stdio.h>
     
    int esp, ebp;
     
    /* Cette fonction replace les registres esp et ebp aux valeurs sauvegardées précedemement dans le main */
    void f() {
      asm("movl %0, %%esp\n"
          :
          :"r"(esp));
      asm("movl %0, %%ebp\n"
          :
          :"r"(ebp));
    }
     
    int main(void) {
      asm("movl %%esp, %0\n"
          : "=r"(esp));
      asm("movl %%ebp, %0\n"
          : "=r"(ebp));
      printf("esp = %x\n",esp);
      printf("ebp = %x\n",ebp);
      f();  
      return 0;
    }
    Je compile ce code en 32 bits (gcc -m32) mais l'exécution m'indique une erreur de segmentation et j'aimerais comprendre d'où vient cette eurreur.

    Merci d'avance pour votre aide.

  2. #2
    Expert éminent sénior
    Avatar de Médinoc
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Septembre 2005
    Messages
    27 369
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 40
    Localisation : France

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Septembre 2005
    Messages : 27 369
    Points : 41 518
    Points
    41 518
    Par défaut
    Si tu veux faire ce genre de truc, fais directement un code assembleur plutôt que de l'assembleur inline, car il est probable que le prologue et l'épilogue de f() affectent esp et/ou ebp.
    SVP, pas de questions techniques par MP. Surtout si je ne vous ai jamais parlé avant.

    "Aw, come on, who would be so stupid as to insert a cast to make an error go away without actually fixing the error?"
    Apparently everyone.
    -- Raymond Chen.
    Traduction obligatoire: "Oh, voyons, qui serait assez stupide pour mettre un cast pour faire disparaitre un message d'erreur sans vraiment corriger l'erreur?" - Apparemment, tout le monde. -- Raymond Chen.

  3. #3
    Nouveau membre du Club
    Homme Profil pro
    etudiant info
    Inscrit en
    Mars 2016
    Messages
    32
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Nord (Nord Pas de Calais)

    Informations professionnelles :
    Activité : etudiant info

    Informations forums :
    Inscription : Mars 2016
    Messages : 32
    Points : 30
    Points
    30
    Par défaut
    Bonjour Médinoc et merci de ta réponse,
    L'objectif de ce programme est de comprendre et de manipuler la pile d'exécution pour un programme C car par la suite je devrai implémenter une bibliothèque de changement de contexte et d'autres choses plus complexes. Or, si je ne comprends pas comment se déroule l'exécution d'un programme aussi simple, je me vois mal passer à des concepts plus complexes.

  4. #4
    Responsable Systèmes


    Homme Profil pro
    Gestion de parcs informatique
    Inscrit en
    Août 2011
    Messages
    17 427
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Paris (Île de France)

    Informations professionnelles :
    Activité : Gestion de parcs informatique
    Secteur : High Tech - Matériel informatique

    Informations forums :
    Inscription : Août 2011
    Messages : 17 427
    Points : 43 044
    Points
    43 044
    Par défaut
    Voir dans l'autre discussion que tu as créé sur le même sujet :
    https://www.developpez.net/forums/d1...e-d-execution/
    Ma page sur developpez.com : http://chrtophe.developpez.com/ (avec mes articles)
    Mon article sur le P2V, mon article sur le cloud
    Consultez nos FAQ : Windows, Linux, Virtualisation

  5. #5
    Membre expérimenté
    Avatar de sambia39
    Homme Profil pro
    No Comment
    Inscrit en
    Mai 2010
    Messages
    543
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Loiret (Centre)

    Informations professionnelles :
    Activité : No Comment
    Secteur : High Tech - Matériel informatique

    Informations forums :
    Inscription : Mai 2010
    Messages : 543
    Points : 1 745
    Points
    1 745
    Par défaut
    Bonsoir,
    Citation Envoyé par barcelonais59 Voir le message
    J'étudie actuellement les contextes d'exécution en langage C. Pour cela, nous avons parfois à introduire du code assembleur modifiant les registres esp et ebp.Voici un petit programme que j'ai créé pour essayer de comprendre le fonctionnement de la pile d'exécution.
    Ce qui suit est pour la compréhension des contextes de fonction.
    (1) La pile est un espace qui est alloué à chaque application/programme en cours d'exécution qui sert à stocker diverses données. Elle est basée sur le principe LIFO ; Last Input First Output (soit le dernier entré, le premier sorti), il n’y a donc pour la pile aucun accès aléatoire, mais plutôt des opérations d’empilement et de dépilement et ne peut donc qu’admettre deux types d’opérations «*push et pop*». Une opération push est utilisée pour pousser un élément vers la pile, ce qui fait que la pile ne peut que croître vers les adresses basses de la mémoire. Quant au pop, il fait l’opération inverse, c’est-à-dire qu’il prend le premier élément au sommet de la pile et l'extrait.

    (2)La pile étant une zone de stockage «*temporaire*», elle peut y donc stocker différentes informations comme les arguments de fonction , les variables locales, le Save Frame Pointeur (SFP) et l’adresse de retour de fonctions de façon à ce que l’on puisse revenir à la fonction appelante en positionnant le registre pointeur RIP/EIP pour qu’il pointe juste après l’appel de la fonction. L’ensemble de ces informations sont précisément sauvegardées dans ce que l’on appelle un bloc d’activation (que l’on appelle également cadre de pile). Le SFP mentioné précédemment est un pointeur que l’on sauvegarde dans le bloc d’activcation et utilisé pour restituer la valeur de RBP/EBP qui est le registre pointeur de base (RPB pour une architecture 64 bits et EBP pour une architecture 32 Bits). Il faut donc comprendre que la pile est une structure qui contient des blocs d’activation et qu'il y'a un registre pointeur qui pointe constamment sur le sommet de la pile dite également l’adresse de base/de fin de la pile (RSP/ESP). La pile a donc une évolution constante dû au fait de l’empilement et du dépilement et elle n’a pas de taille figée. Pour comprendre plus en détail, nous allons prendre comme exemple le code source ci-dessous qui s’apparente au vôtre et que l’on désassemble pour comprendre le mécanisme du contexte de fonction :
    Code C : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
     
    #include <stdio.h>
    #include <stdlib.h>
     
    void call_func_foo( void ){
    	for( ;;)
    		(void)puts("Hoops louper");
    }
    int main( void ){
     
    	(void)puts("Je rentre dans la boucle infini");
    	call_func_foo();
    	(void)puts("retour dans main");
    	return EXIT_SUCCESS;
    }

    Code gdb : Sélectionner tout - Visualiser dans une fenêtre à part
    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
    	(gdb) set disassenbly-flavor intel
    	(gdb) disass main
    	Dump of assembler code for function main:
    		0x0000000100000f10 <+0>:	push   rbp					<= (3) <Début du prologue>
    		0x0000000100000f11 <+1>:	mov    rbp,rsp
    		0x0000000100000f14 <+4>:	sub    rsp,0x10
    		0x0000000100000f18 <+8>:	lea    rdi,[rip+0x5c]        # 0x100000f7b
    		0x0000000100000f1f <+15>:	mov    DWORD PTR [rbp-0x4],0x0
    	        0x0000000100000f26 <+22>:	call   0x100000f4c <puts>
    		0x0000000100000f2b <+27>:	mov    DWORD PTR [rbp-0x8],eax
    	=>   0x0000000100000f2e <+30>:	call   0x100000ee0 <call_func_foo>
    		0x0000000100000f33 <+35>:	lea    rdi,[rip+0x61]        # 0x100000f9b
    		0x0000000100000f3a <+42>:	call   0x100000f4c <puts>
    		0x0000000100000f3f <+47>:	xor    ecx,ecx
    		0x0000000100000f41 <+49>:	mov    DWORD PTR [rbp-0xc],eax
    		0x0000000100000f44 <+52>:	mov    eax,ecx
    		0x0000000100000f46 <+54>:	add    rsp,0x10  <= (4) <Début de l'épilogque> 
    		0x0000000100000f4a <+58>:	pop    rbp
    		0x0000000100000f4b <+59>:	ret    
    	End of assembler dump.
    
            (gdb) disass call_func_foo
            Dump of assembler code for function call_func_foo:
    		0x0000000100000ee0 <+0>:	push   rbp		<= <Début du prologue de la fonction call_func_foo>
    		0x0000000100000ee1 <+1>:	mov    rbp,rsp
    	=> 0x0000000100000ee4 <+4>:	sub    rsp,0x10
    		0x0000000100000ee8 <+8>:	jmp    0x100000eed <call_func_foo+13>
    		0x0000000100000eed <+13>:	lea    rdi,[rip+0x7a]        # 0x100000f6e 
    		0x0000000100000ef4 <+20>:	call   0x100000f4c <puts>
    		0x0000000100000ef9 <+25>:	mov    DWORD PTR [rbp-0x4],eax
    		0x0000000100000efc <+28>:	jmp    0x100000eed <call_func_foo+13> <= [saut de la boucle]
    	End of assembler dump.

    (3)Sur ma machine, vous remarquerez que le point d’entrée de la fonction main est à l’adresse 0x0000000100000f10 et la fonction call_func_foo à l’adresse 0x100000ee0(0x0000000100000ee0). Les deux premières instructions de la fonction main /call_func_foo sont ce qui caractérise le prologue de la fonction main; on dit prologue de la fonction, les premières instructions exécutée dans la fonction. On commence donc par sauvegarder la valeur du registre frame pointeur RBP(64 bit)/EBP (32 bits) sur la pile. L’instruction «*movq/mov*» est utilisée pour transférer/déplacer les données entre des registres de même taille; utiliser sur le registre pointeurs de pile RSP et RBP cela a pour effet la mise à jour du frame pointeur, ce qui veut dire que l’on se positionne dans le contexte de la fonction main.

    À la onzième ligne, l’instruction «*call*» va commencer à sauver l’adresse retour de la fonction main et positionne au passage le registre pointeur d’instruction RIP/EIP au début de la fonction call_func_foo pour y faire un saut (saut dans la fonction) et dans la fonction call_func_foo, on retrouve à nouveau un autre prologue et le processus recommence. On sauvegarde le frame pointeur sur la pile qui est le contexte de la fonction précédente (le main) de façon à ce que lorsque l’on sort de la fonction call_func_foo, le programme saura alors comment revenir pour continuer l’exécution du code dans la fonction main. Puis l’on procède à la mise à jour du frame pointeur.

    (4)La restauration de contexte est effectuée lors de l’épilogue d’une fonction. On parle d’épilogue d’une fonction, l’ensemble des instructions juste avant la sortie de la fonction. Ces instructions vont permettre de retourner au bon endroit du code. Les épilogues sont généralement effectués par deux instructions leave et ret. Ce qui est l’équivalent de pop rbp suivie de ret. Et donc, pour restituer l’ancien frame pointeur qui a été sauvegardé par le prologue, il faut proceder à un dépilement, puis un ret qui déclenche le retour à l’appelant, plus exactement juste après appel à la fonction call_func_foo grâce à la valeur/adresse retour stocké dans la pile.

    Citation Envoyé par barcelonais59 Voir le message
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    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
     
    #include <stdio.h>
     
    int esp, ebp;
     
    /* Cette fonction replace les registres esp et ebp aux valeurs sauvegardées précedemement dans le main */
    void f() {
      asm("movl %0, %%esp\n"
          :
          :"r"(esp));
      asm("movl %0, %%ebp\n"
          :
          :"r"(ebp));
    }
     
    int main(void) {
      asm("movl %%esp, %0\n"
          : "=r"(esp));
      asm("movl %%ebp, %0\n"
          : "=r"(ebp));
      printf("esp = %x\n",esp);
      printf("ebp = %x\n",ebp);
      f();  
      return 0;
    }
    Je compile ce code en 32 bits (gcc -m32) mais l'exécution m'indique une erreur de segmentation et j'aimerais comprendre d'où vient cette eurreur.

    Merci d'avance pour votre aide.

    L’instruction dans votre code source ci-dessus n’est pas correcte parce que vous ne restituez aucun contexte pour faire simple. Vous écrasez les valeurs présentes dans les différents registres et tentez d'accéder à un emplacement mémoire invalide d'où la fameuse erreur de segmentation. Et comme je l’ai mentionné plus haut, l’accès à la pile n’est pas aléatoire, il vous fallait donc user des instructions comme pop, add et ret pour provoquer la restitution du contexte de la fonction et ainsi pouvoir revenir dans la fonction appelante. Il se présente donc à vous deux possibilités :
    ->méthode barbare qui consiste à écraser les valeurs de la pile de façon à corrompre l’adresse de retour (écraser/remplacer l’adresse de retour par une autre) de façon à rediriger/contrôler l’exécution du programme, mais cette pratique n’est pas une option à développer ici et de plus, des mécanismes ont été implémentés comme Stack Canari, NX ou ASLR qui permettent de détecter une possible corruption ou manœuvre de ce genre.
    -> ou alors écrire l’épilogue a la mano juste avant de rentrer dans la boucle infinie (ça reviendra a faire un return). Mais attention, cela nécessite l’adaptions du code en fonction du prologue; ce qui veux dire qu'avant de vouloir tester le code au pif comme vous le faites jusqu’à maintenant, il faut savoir comment le prologue de votre programme est conçu de façon à ce que les instructions de notre épilogue puisse nous faire revenir sans encombre dans la fonction appelante. Partant de notre précédant exemple.
    Après désasemblage du code, si vous avez bien remarque il y avait cette instruction sub rsp,0x10, ce qui veut dire que le programme alloue sur la pile 16 bit de mémoire. Donc, si vous voulez revenir dans la fonction main, il va falloir avant tout chercher à revenir au bon endroit, c’est à dire remettre la pile en place avant allocation de la mémoire pour pouvoir revenir dans la situation précédente (l’ancien contexte de fonction et donc positionner le register pointer d’instruction EIP sur l’adresse retour de la fonction main afin d’y faire un saut). Il faut donc écrire non pas vos instructions précédentes mais comme dans l'exemple ci-dessous. Attention, l’organisation de la pile varie d'une machine à une autre donc le code ci-dessous copié/collé tel qu'il est, peut ne pas fonctionner chez vous.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
     
    #include <stdio.h>
    #include <stdlib.h>
     
    void call_func_foo( void ){
     
    	/* retour à la fonction main	*/
    	__asm__("addq $0x10, %rsp");
    	__asm__("popq %rbp");
    	__asm__("ret");
     
    	for( ;;)
    		(void)puts("Hoops louper");
    }
     
    int main( void ){
     
    	(void)puts("Je rentre dans la boucle infini");
    	call_func_foo();
    	(void)puts("retour dans main");
    	return EXIT_SUCCESS;
    }

    Au passage, si vous souhaitez récupérer les informations comme l'adresse retour de vos fonction ou de l'appelant, vous pouvez utiliser sur GCC void * __builtin_return_address (unsigned int level) et void * __builtin_frame_address (unsigned int level) pour les frames
    exemple :
    Code C : Sélectionner tout - Visualiser dans une fenêtre à part
    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
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
     
    #include <stdio.h>
    #include <stdlib.h>
     
    void call_func_foo( void ){
     
    	void *addr_return_func = __builtin_return_address(0x0);
    	void *addr_frame_func = __builtin_frame_address(0x0);
     
    	void *addr_return_func_main = __builtin_return_address(0x1);
    	void *addr_frame_func_main = __builtin_frame_address(0x1);
     
    	(void)fprintf(stderr,
    			"=> Adresse de retour fonction call_func_foo \t:%p\n", 
    			addr_return_func );
    	(void)fprintf(stderr,
    			"=> Adresse frame fonction call_func_foo \t:%p\n", 
    			addr_frame_func );
     
    	(void)fprintf(stderr,
    			"[Adresse de retour fonction main\t:%p]\n", 
    			addr_return_func_main );
    	(void)fprintf(stderr,
    			"[Adresse frame fonction main\t:%p]\n", 
    			addr_frame_func_main );
     
     
    	/* retour a la function main	*/
    	__asm__("addq $0x40, %rsp");
    	__asm__("popq %rbp");
    	__asm__("ret");
     
    	for( ;;)
    		(void)puts("Hoops louper");
    }
     
    int main( void ){
     
    	void *addr_return = __builtin_return_address(0x0);
    	void *addr_frame = __builtin_frame_address(0x0);
     
    	(void)fprintf(stderr,
    		"Adresse de retour fonction main\t:%p\n", addr_return );
    	(void)fprintf(stderr,
    			"Adresse frame fonction main\t:%p\n", addr_frame );
    	call_func_foo();
    	(void)puts("retour dans main");
    	return EXIT_SUCCESS;
    }
    Bien entendu, ces fonction ne doit être utilisée qu'avec un argument non nul et à des fins de débogage.
    Code resultat : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
     
    Adresse de retour fonction main	:0x7fff66036085
    Adresse frame fonction main	:0x7ffeefbff740
    => Adresse de retour fonction call_func_foo 	:0x100000e51
    => Adresse frame fonction call_func_foo 	:0x7ffeefbff700
    [Adresse de retour fonction main	:0x7fff66036085]
    [Adresse frame fonction main	:0x7ffeefbff740]
    retour dans main

    à bientôt

    Celui qui peut, agit. Celui qui ne peut pas, enseigne.
    Il y a deux sortes de savants: les spécialistes, qui connaissent tout sur rien,
    et les philosophes, qui ne connaissent rien sur tout.
    George Bernard Shaw

Discussions similaires

  1. Problème avec le contexte graphique associé à JPanel
    Par Virgile le chat dans le forum AWT/Swing
    Réponses: 6
    Dernier message: 20/06/2007, 23h31
  2. Réponses: 5
    Dernier message: 14/06/2007, 11h34
  3. Problème de JTree dans un exécutable
    Par amelA dans le forum Composants
    Réponses: 2
    Dernier message: 21/04/2007, 23h32
  4. Réponses: 1
    Dernier message: 24/01/2007, 13h34
  5. [librairie zip] problème manipulation archive
    Par winnux dans le forum Entrée/Sortie
    Réponses: 6
    Dernier message: 13/07/2005, 12h08

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo