Forum Programmation.c Différence affectation de structure et memcpy

Posté par (page perso) .
Tags : aucun
3
27
mar.
2012

Bonjour,

Je dev un OS (à titre perso) avec sa libc associée et je viens de résoudre un bug qui affectait bochs mais pas qemu :

En gros, mon application userspace reconfigure le tty pour le passer en raw (copie de la conf courante, passage en raw, application de la nouvelle conf). J'ai donc écrit pour recopier la conf courante : struct termios newt = oldt;
avec oldt un struct termios déclaré juste au dessus et initialisé comme il faut par un syscall.

C'est précisément cette ligne qui me fait planter mon OS sous bochs mais qui marche parfaitement sous QEMU. Alors par curiosité j'ai remplacé par un memcpy(&newt, &oldt, sizeof(newt)) et là, ô miracle, ça marche.

Est-ce que quelqu'un pourrait m'expliquer la différence fondamentale entre ces 2 solutions ? Et pourquoi ça marcherait avec un émulateur et pas un autre.

Merci.


Mise à jour du 26/01/2015 :

Le problème était lié à la valeur du registre « es » que j'avais mal initialisé. QEMU aurait dû me lever une exception (GPE) comme le fait Bochs mais il est visiblement assez permissif.

  • # Type de compilation ?

    Posté par . Évalué à 1.

    Est-ce que tu programmes en C pur ou en C++ ? D'une manière générale, même si c'est autorisé depuis C99, c'est toujours une mauvaise idée de déclarer une nouvelle variable au milieu d'une procédure.

    D'autre part, il se peut également que ça fonctionne « par accident ».
    Tu peux nous montrer ton code ?

    • [^] # Re: Type de compilation ?

      Posté par (page perso) . Évalué à 2.

      C'est du C99.

      char *readline(const char *prompt)
      {    
          int i;
          int end = 0;
          int cur_col = 0;
          if (prompt) {
              printf("%s", prompt);
              cur_col = strlen(prompt);
              fflush(stdout);
          }
      
          struct winsize ws;
          ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws);
          int colonnes = ws.ws_col;
      
          struct termios oldt;
          tcgetattr(0, &oldt);
      
          //XXX: Cette ligne plante sous bochs... remplacée par un memcpy...
          //struct termios newt = oldt;
          struct termios newt;
          memcpy(&newt, &oldt, sizeof(newt));
      
          cfmakeraw(&newt);
          tcsetattr(0, TCSANOW, &newt);
      
      [...]
      
      
      • [^] # Re: Type de compilation ?

        Posté par . Évalué à 2.

        Pour autant que je sache, la seule différence entre une affectation de structure et un memcpy est que memcpy initialise aussi les octets de padding/rembourrage, ce qui a priori n'a pas d'impact.
        Donc plusieurs hypothèses:
        1) tu t'es trompé et le problème ne vient pas de là
        2) la différence sur les octets de bourrage ont un impact sur l'émulateur (??)
        3) bug du compilateur: essayer de regarder l'assembleur généré.

        • [^] # Re: Type de compilation ?

          Posté par (page perso) . Évalué à 2.

          1) tu t'es trompé et le problème ne vient pas de là

          J'ai eu ce problème aussi avec une autre application qui cherchait à faire un truc similaire et remplacer newt = oldt; par un memcpy a aussi résolu le bug. Si je ne me trompe pas de bug, sur cette application, l'exception levée était un General Protection Fault (GDT:0x00000000) et j'avais le bug avec bochs, kvm et virtualbox.

          Mais sinon, je veux bien croire à un effet de bord, c'est justement la raison pour laquelle je pose la question au lieu de me dire que le problème est réglé. Mais je ne vois pas trop… J'essaierai de faire d'autres tests ce soir pour voir si le problème se pose avec toutes les structures, si les syscall qui précèdent ont une importance, etc.

          2) la différence sur les octets de bourrage ont un impact sur l'émulateur (??)

          wtf :).

          3) bug du compilateur: essayer de regarder l'assembleur généré.

          Ouais, j'y ai songé, mais ce qui m'étonne c'est que justement ça varie d'un émulateur à l'autre (et compilé avec glibc puis exécuté sous Linux, je n'ai aucun problème non plus).

          • [^] # Re: Type de compilation ?

            Posté par (page perso) . Évalué à 3.

            Voici un code minimaliste qui fait planter sous Bochs :

              struct termios a;
              struct termios b;
            
              b = a;
            
            
            • [^] # Re: Type de compilation ?

              Posté par (page perso) . Évalué à 2.

              J'ai donc compilé avec ma libc une fonction qui contient le code ci-dessus.

              Code qui plante :

              .LCFI1:
                  .cfi_def_cfa_register 5
                  pushl   %edi
                  pushl   %esi
                  pushl   %ebx
                  subl    $176, %esp
                  .loc 1 9 0
                  leal    -188(%ebp), %edx
                  leal    -100(%ebp), %ebx
                  .cfi_offset 3, -20
                  .cfi_offset 6, -16
                  .cfi_offset 7, -12
                  movl    $22, %eax
                  movl    %edx, %edi
                  movl    %ebx, %esi
                  movl    %eax, %ecx
                  rep movsl
                  .loc 1 12 0
                  movl    $0, %eax
                  .loc 1 13 0
                  addl    $176, %esp
                  popl    %ebx
                  .cfi_restore 3
                  popl    %esi
                  .cfi_restore 6
                  popl    %edi
                  .cfi_restore 7
                  popl    %ebp
              
              

              code qui ne plante pas :

              .LCFI1:
                  .cfi_def_cfa_register 5
                  andl    $-16, %esp
                  subl    $192, %esp
                  .loc 1 10 0
                  movl    $88, 8(%esp)
                  leal    104(%esp), %eax
                  movl    %eax, 4(%esp)
                  leal    16(%esp), %eax
                  movl    %eax, (%esp)
                  call    memcpy
                  .loc 1 12 0
                  movl    $0, %eax
                  .loc 1 13 0
                  leave
                  .cfi_restore 5
              
              

              Est-ce que quelqu'un qui parle l'assembleur x86 couramment pourrait en faire un petit commentaire ?

              • [^] # Re: Type de compilation ?

                Posté par (page perso) . Évalué à 2.

                Dans ton premier extrait de code il y a trois push et quatre pop, d'où problème. Mais il est probable que ton extrait ne soit pas complet à ce sujet, sinon les deux émulateurs planteraient.

                • [^] # Re: Type de compilation ?

                  Posté par (page perso) . Évalué à 3.

                  Je pense que mon extrait est assez complet, voici un diff pour compléter :

                  18,32c18,26
                  <   pushl   %edi
                  <   pushl   %esi
                  <   pushl   %ebx
                  <   subl    $176, %esp
                  <   .loc 1 9 0
                  <   leal    -188(%ebp), %edx
                  <   leal    -100(%ebp), %ebx
                  <   .cfi_offset 3, -20
                  <   .cfi_offset 6, -16
                  <   .cfi_offset 7, -12
                  <   movl    $22, %eax
                  <   movl    %edx, %edi
                  <   movl    %ebx, %esi
                  <   movl    %eax, %ecx
                  <   rep movsl
                  ---
                  >   andl    $-16, %esp
                  >   subl    $192, %esp
                  >   .loc 1 10 0
                  >   movl    $88, 8(%esp)
                  >   leal    104(%esp), %eax
                  >   movl    %eax, 4(%esp)
                  >   leal    16(%esp), %eax
                  >   movl    %eax, (%esp)
                  >   call    memcpy
                  36,43c30,31
                  <   addl    $176, %esp
                  <   popl    %ebx
                  <   .cfi_restore 3
                  <   popl    %esi
                  <   .cfi_restore 6
                  <   popl    %edi
                  <   .cfi_restore 7
                  <   popl    %ebp
                  ---
                  >   leave
                  >   .cfi_restore 5
                  46d33
                  <   .cfi_restore 5
                  55c42
                  <   .long   0x11f
                  ---
                  >   .long   0x11e
                  191,192c178,179
                  <   .byte   0x91
                  <   .sleb128 -108
                  ---
                  >   .byte   0x74
                  >   .sleb128 104
                  198,200c185,187
                  <   .byte   0x3
                  <   .byte   0x91
                  <   .sleb128 -196
                  ---
                  >   .byte   0x2
                  >   .byte   0x74
                  >   .sleb128 16
                  
                  

                  Qu'est-ce qui pourrait être à l'origine de ce problème ? D'ailleurs, j'ai remplacé la structure termios par :

                  struct toto {
                  int c[17];
                  };

                  Et j'ai le bug pour une valeur >= 17. En dessous, il ne fait pas de push/pop mais plein de movl.

                  • [^] # Re: Type de compilation ?

                    Posté par (page perso) . Évalué à 3.

                    En fait, mon extrait était bien incomplet, le pushl %ebp est juste au dessus, commun aux 2 codes :

                    .LFB0:
                            .file 1 "testbug.c"
                            .loc 1 8 0
                            .cfi_startproc
                            pushl   %ebp
                    .LCFI0:
                            .cfi_def_cfa_offset 8
                            .cfi_offset 5, -8
                            movl    %esp, %ebp
                    .LCFI1:
                    [...]
                    
                    
                • [^] # Re: Type de compilation ?

                  Posté par (page perso) . Évalué à 3.

                  Après avoir fait du pas à pas avec le debugguer de bochs, voici des infos complémentaires :

                  (0).[6946995606] [0x0000000002669023] 001b:40000023 (unk. ctxt): rep movsd dword ptr es:[edi], dword ptr ds:[esi] ; f3a5
                  CPU 0: Exception 0x0d - (#GP) general protection fault occured (error_code=0x0000)
                  CPU 0: Interrupt 0x0d occured (error_code=0x0000)

                  • [^] # Re: Type de compilation ?

                    Posté par . Évalué à 2.

                    Voir le code commenté si dessous, je pense que si ta pile à l'entrée de ta fonction est bien alignée sur 4 octets cela marche, sinon cela provoque le bug (l'instruction movsl requiert un alignement 32 bits pour les adresses source/destination).

                    • [^] # Re: Type de compilation ?

                      Posté par (page perso) . Évalué à 2.

                      C'est une excellente remarque, peut-être que qemu n'est pas aussi regardant sur les problèmes d'alignement. Ce qui m'étonne c'est que j'ai tout un tas d'autres applications et que je n'ai jamais rencontré ce problème avant.

                      Ce soir, je jouerai un peu avec la pile pour voir. C'est vrai qu'on y place un tas d'infos (argc, argv, envp) sans se soucier de grand chose en terme d'alignement… Cette partie ne m'est pas entièrement familière car elle a été codée par un pote.

                      • [^] # Re: Type de compilation ?

                        Posté par . Évalué à 2.

                        Regarder aussi l'etat du bit de direction dans le registre EFLAGS
                        normalement on place une instruction cld ou std AVANT le "rep movsl"
                        suffit qu'il soit par defaut a 1 et le rep movsl compte dans la mauvaise direction donc illegal acces.
                        Je sait que sous linux (en tout cas dans le kernel) cela a causé des soucis avec gcc sup a 4.3 plus de detail la http://em386.blogspot.fr/2008/03/cldstd-and-gcc-430.html

                        • [^] # Re: Type de compilation ?

                          Posté par (page perso) . Évalué à 2.

                          Merci pour le lien, je regarderai ça plus tard. J'ai testé vite fait l'affichage des adresses des variables a et b :

                          #include <stdio.h>
                          
                          struct toto {
                              int t[17];
                          };
                          
                          int main() {
                              struct toto a;
                              struct toto b;
                          
                              printf("%u %u\n", &a, &b);
                              printf("%d %d\n", sizeof(a), sizeof(b));
                          
                              b = a;
                              return 0;
                          }
                          
                          

                          Et là, j'obtiens bien des adresses multiples de 4… Et ça plante après les printf.

                          • [^] # Re: Type de compilation ?

                            Posté par . Évalué à 1.

                            Tu peut compiler et essayer en rajoutant l'option -mcld à gcc ?

                            • [^] # Re: Type de compilation ?

                              Posté par (page perso) . Évalué à 2.

                              Je viens d'essayer mais visiblement, ça ne change rien au code assembleur généré.

                              Du coup j'ai essayé d'afficher l'eflag et même de le modifier et : il est bien à 0 (vérifié depuis bochs mais aussi en rajoutant du code assembleur dans le code de test) et si je le change de sens, ça plante aussi.

                              (bon cela dit, effectivement, on s'en est pas soucié non plus il me semble donc ça aurait pu être une source de problème similaire à linux)

              • [^] # Re: Type de compilation ?

                Posté par . Évalué à 3.

                ATTENTION je fais ca de mémoire ca remonte a loin le temps ou je codais en asm x86
                donc il peut y avoir des erreurs de ma part

                Code qui plante

                   pushl   %ebp     ->sauve ebp
                    movl    %esp, %ebp  ->recupere addresse stack frame
                    pushl   %edi    -> sauve les registrers (consomme donc +12 octet de stack)
                    pushl   %esi
                    pushl   %ebx
                    subl    $176, %esp   ->allocation d'espace dans la stack frame (176=2*88) qui doit etre la taille de chaque struct
                    leal    -188(%ebp), %edx       ->calcul adresse des structures
                    leal    -100(%ebp), %ebx       ->100=88+12 (pose des registre dans la stack) 
                
                    movl    $22, %eax   ->taille des data à copier (en dword,22*4=88)
                    movl    %edx, %edi  ->destination
                    movl    %ebx, %esi  ->source
                    movl    %eax, %ecx  ->taille
                    rep movsl             ->copie de 22 dword de -188 à -100 puis incremente 
                    movl    $0, %eax    ->return 0
                    addl    $176, %esp  ->liberation d'espace de pile
                
                    popl    %ebx         ->restoration des registres 
                    popl    %esi
                    popl    %edi
                    popl    %ebp
                
                

                Code qui plante pas

                    andl    $-16, %esp    ->align la pile sur 16 octet 
                    subl    $192, %esp    ->alloue taille 192 octet dans la stack frame (2*88+16)
                    movl    $88, 8(%esp)    ->arg 3 88 ->dans la stack
                    leal    104(%esp), %eax   ->calcule une adresse STK+104 (104=88+16)
                    movl    %eax, 4(%esp)     ->pose l'adresse en arg2
                    leal    16(%esp), %eax    ->calcul une adresse STK+16
                    movl    %eax, (%esp)      ->pose l'adresse en arg1
                    call    memcpy            ->apelle sous fonction
                    movl    $0, %eax          ->return 0
                    leave                    ->oui mais il est ou le ENTER ? a mon avis il manque encore un peut de code en préambule.
                
                
    • [^] # Re: Type de compilation ?

      Posté par . Évalué à 6.

      « D'une manière générale, même si c'est autorisé depuis C99, c'est toujours une mauvaise idée de déclarer une nouvelle variable au milieu d'une procédure. »

      Pas d'accord! C'est toujours une bonne idée de ne déclarer une variable que lorsqu'on en a besoin afin qu'elle ai la plus faible portée (scope) possible. Ça facilite la compréhension et l'optimisation.

      • [^] # Re: Type de compilation ?

        Posté par . Évalué à 2.

        C'est ce que je pense aussi mais je les ramène quand même en début du bloc le plus proche.

      • [^] # Re: Type de compilation ?

        Posté par (page perso) . Évalué à 3.

        Sauf que si tu as une fonction qui n'utilise certaines variables qu'à partir de la ligne 20, tu aurais sans doute mieux fait de la diviser en plus petites fonctions.

        pertinent adj. Approprié : qui se rapporte exactement à ce dont il est question.

  • # Alignement?

    Posté par . Évalué à 4.

    Peut être une erreur d'alignement? Ta plateforme ne supporte pas les writes en 32bits non alignés, et le compilateur pense que si? Bon je dis ça, mais apparemment tu es sous x86 et la structure contient des int donc devrait être alignée.

    • [^] # Re: Alignement?

      Posté par (page perso) . Évalué à 3.

      Oui, cela serait étrange… Surtout que ça plante aussi avec

      struct toto {
        int c[17];
      };
      
      

      Mais peut-être l'utilisation d'une instruction non supportée par la plateforme émulée par Bochs ?

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.