27 votes

x64 memset core, l'adresse du tampon passé est tronquée ?

1. Contexte du problème

Récemment, un vidage de noyau s'est produit sur l'un de nos serveurs de recherche en ligne. Le noyau se trouve dans memset() en raison de la tentative d'écriture à une adresse invalide, et a donc reçu le signal SIGSEGV. Les informations suivantes proviennent de dmsg :

is_searcher_ser[17405]: segfault at 000000002c32a668 rip 0000003da0a7b006 rsp 0000000053abc790 error 6

L'environnement de nos serveurs en ligne se présente comme suit :

  • OS : RHEL 5.3
  • Noyau : 2.6.18-131.el5.custom, x86_64 (64 bits)
  • GCC : 4.1.2 20080704 (Red Hat 4.1.2-44)
  • Glibc : glibc-2.5-49.6

Voici l'extrait de code correspondant :

CHashMap<…>::CHashMap(…)
{
     …
     typedef HashEntry *HashEntryPtr;              
     m_ppEntry = new HashEntryPtr[m_nHashSize];   // m_nHashSize is 389 when core
     assert(m_ppEntry != NULL);
     memset(m_ppEntry, 0x0, m_nHashSize*sizeof(HashEntryPtr)); // Core in this memset() invocation 
     …
}

Le code d'assemblage du code ci-dessus est :

…
0x000000000091fe9e <+110>:   callq  0x502638 <_Znam@plt>  // new HashEntryPtr[m_nHashSize]
0x000000000091fea3 <+115>:   mov    0xc(%rbx),%edx         // Get the value of m_nHashSize
0x000000000091fea6 <+118>:   mov    %rax,%rdi               // Put m_ppEntry pointer to %rdi for later memset invocation
0x000000000091fea9 <+121>:   mov    %rax,0x20(%rbx)        // Store the pointer to m_ppEntry member variable(%rbx holds the this pointer)
0x000000000091fead <+125>:   xor    %esi,%esi               // Generate 0
0x000000000091feaf <+127>:   shl    $0x3,%rdx               // m_nHashSize*sizeof(HashEntryPtr)
0x000000000091feb3 <+131>:   callq  0x502b38 <memset@plt> // Call the memset() function
…

Dans la décharge centrale, l'assemblage de memset@plt est :

(gdb) disassemble 0x502b38
Dump of assembler code for function memset@plt:
    0x0000000000502b38 <+0>:     jmpq   *0x771b92(%rip)        # 0xc746d0 <memset@got.plt>
    0x0000000000502b3e <+6>:     pushq  $0x53
    0x0000000000502b43 <+11>:    jmpq   0x5025f8
End of assembler dump.
 (gdb) x/ag 0x0000000000502b3e+0x771b92
    0xc746d0 <memset@got.plt>:      0x3da0a7acb0 <memset>
 (gdb) disassemble 0x3da0a7acb0
 Dump of assembler code for function memset:
    0x0000003da0a7acb0 <+0>:     cmp    $0x1,%rdx
    0x0000003da0a7acb4 <+4>:     mov    %rdi,%rax
    …

Pour l'analyse GDB ci-dessus, nous savons que l'adresse de memset() a été résolu dans la table PLT de relocalisation. C'est-à-dire que le premier jmpq *0x771b92(%rip) sautera directement à la première instruction de la fonction memset() . De plus, le programme s'était déroulé sur près d'une journée en ligne, l'adresse de relocalisation de memset() aurait dû être résolu plus tôt.

2. Phénomène étrange

Ce noyau a tiré sur l'instruction => 0x0000003da0a7b006 <+854>: mov %rdx,-0x8(%rdi) dans le memset() . En fait, il s'agit de l'instruction dans le memset() pour définir le 0 à la position de début droite du tampon qui est le premier paramètre de la fonction memset() .

En cas de carottage, dans le cadre 0, la valeur de $rdi es 0x2c32a670 et $rax es 0x2c32a668 . De l'analyse de l'assemblage et du test hors ligne, $rax doit contenir le tampon source du memset c'est à dire le premier paramètre de l'équation memset() .

Donc, dans notre exemple, $rax doit être la même que l'adresse de m_ppEntry dont la valeur est stockée dans le this objet ( this Le pointeur est stocké dans %rbx ) d'abord avant d'être mis à zéro par memset plus tard. Cependant, la valeur de m_ppEntry es 0x2ab02c32a668 .

Ensuite, utilisez info files Commande GDB à vérifier, l'adresse 0x2c32a668 est en effet invalide (non mappé), et l'adresse 0x2ab02c32a668 est une adresse valide.

3. Pourquoi c'est bizarre ?

L'endroit bizarre de ce noyau est que : Si l'adresse réelle de memset a déjà été résolu (très très probablement), alors il n'y a que très peu d'instructions entre l'opération consistant à placer la valeur du pointeur dans le fichier m_ppEntry et la tentative de memset il. Et en fait, la valeur du registre $rax (contenant l'adresse de la mémoire tampon transmise) ne sont pas modifiées du tout pendant ces instructions. Ainsi, comment m_ppEntry n'est pas égal à $rax ?

Ce qui est bizarre Plus de est la suivante : lorsque le noyau, la valeur de $rax ( 0x2c32a668 ) est en fait la valeur des 4 octets inférieurs de l'adresse de l'utilisateur. m_ppEntry ( 0x2ab02c32a668 ). S'il y a effectivement une relation entre les deux valeurs, est-ce que la m_ppEntry passé à memset étant tronquée ? Cependant, les différentes instructions concernées utilisent toutes %rax plutôt que %eax . D'ailleurs, je ne peux pas reproduire ce problème hors ligne.

Donc,

1) Quelle adresse est valide ? Si 0x2c32a668 est valable ? Le tas est-il corrompu juste entre les différentes instructions ? Et comment paraphraser que la valeur de m_ppEntry es 0x2ab02c32a668 et pourquoi les 4 octets inférieurs de ces deux valeurs sont identiques ?

2) Si 0x2ab02c32a668 est valide, pourquoi l'adresse est tronquée lorsqu'elle est transmise dans le système 64-bit memset() ? Dans quelles conditions cette erreur se produit-elle ? Je ne peux pas reproduire ce problème hors ligne. Ce problème est-il un bogue connu ? Je ne l'ai pas trouvé via Google.

3) Ou bien, est-ce dû à un problème de matériel ou d'alimentation pour faire en sorte que les 4 octets supérieurs de %rdi transmis à memset mis à zéro ? (Je suis très très réticent à le croire).

Enfin, tout commentaire sur ce noyau est apprécié.

Merci,

Gary Hu

1voto

AlgebraWinter Points 51

Je suppose que la plupart du temps, ce code fonctionne bien, étant donné que vous avez mentionné une journée de fonctionnement. Je suis d'accord que les signaux valent la peine d'être inspectés, il semble suspect que la troncature de pointeur se produise ailleurs.

La seule autre chose que je pense, c'est que ça pourrait être un problème avec le nouveau. Est-il possible qu'à l'occasion vous puissiez appeler un nouvel opérateur surchargé ? Par ailleurs, pour être complet, quelle est la déclaration de m_ppEntry ? Je suppose que vous utilisez un no throw new, sinon l'élément assert(m_ppEntry != NULL); n'aurait aucun sens.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X