Allocation de gaspillage en mémoire pour les variables locales

C’est mon programme:

void test_function(int a, int b, int c, int d){ int flag; char buffer[10]; flag = 31337; buffer[0] = 'A'; } int main() { test_function(1, 2, 3, 4); } 

Je comstack ce programme avec l’option de débogage:

 gcc -g my_program.c 

J’utilise gdb et je désassemble la fonction test_function avec la syntaxe Intel:

 (gdb) disassemble test_function Dump of assembler code for function test_function: 0x08048344 : push ebp 0x08048345 : mov ebp,esp 0x08048347 : sub esp,0x28 0x0804834a : mov DWORD PTR [ebp-12],0x7a69 0x08048351 : mov BYTE PTR [ebp-40],0x41 0x08048355 : leave 0x08048356 : ret End of assembler dump. 

Et je démonte le principal:

 (gdb) disassemble main Dump of assembler code for function main: 0x08048357 : push ebp 0x08048358 : mov ebp,esp 0x0804835a : sub esp,0x18 0x0804835d : and esp,0xfffffff0 0x08048360 : mov eax,0x0 0x08048365 : sub esp,eax 0x08048367 : mov DWORD PTR [esp+12],0x4 0x0804836f : mov DWORD PTR [esp+8],0x3 0x08048377 : mov DWORD PTR [esp+4],0x2 0x0804837f : mov DWORD PTR [esp],0x1 0x08048386 : call 0x8048344  0x0804838b : leave 0x0804838c : ret End of assembler dump. 

Je place un point d’arrêt à cette adresse: 0x08048355 (instruction de sortie pour la fonction test_function) et je lance le programme.

Je regarde la stack comme ceci:

 (gdb) x/16w $esp 0xbffff7d0: 0x00000041 0x08049548 0xbffff7e8 0x08048249 0xbffff7e0: 0xb7f9f729 0xb7fd6ff4 0xbffff818 0x00007a69 0xbffff7f0: 0xb7fd6ff4 0xbffff8ac 0xbffff818 0x0804838b 0xbffff800: 0x00000001 0x00000002 0x00000003 0x00000004 

0x0804838b est l’adresse de retour, 0xbffff818 est le pointeur de l’image enregistrée (ebp principal) et la variable flag est stockée 12 octets supplémentaires. Pourquoi 12?

Je ne comprends pas cette instruction:

 0x0804834a : mov DWORD PTR [ebp-12],0x7a69 

Pourquoi ne stockons-nous pas la variable de contenu 0x00007a69 dans ebp-4 au lieu de 0xbffff8ac?

Même question pour le tampon. Pourquoi 40?

Nous ne perdons pas la mémoire? 0xb7fd6ff4 0xbffff8ac et 0xb7f9f729 0xb7fd6ff4 0xbffff818 0x08049548 0xbffff7e8 0x08048249 ne sont pas utilisés?

Voici le résultat de la commande gcc -Q -v -g my_program.c :

 Reading specs from /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/specs Configured with: ../src/configure -v --enable-languages=c,c++ --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-gxx-include-dir=/usr/include/c++/3.3 --enable-shared --enable-__cxa_atexit --with-system-zlib --enable-nls --without-included-gettext --enable-clocale=gnu --enable-debug i486-linux-gnu Thread model: posix gcc version 3.3.6 (Ubuntu 1:3.3.6-15ubuntu1) /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/cc1 -v -D__GNUC__=3 -D__GNUC_MINOR__=3 -D__GNUC_PATCHLEVEL__=6 notesearch.c -dumpbase notesearch.c -auxbase notesearch -g -version -o /tmp/ccGT0kTf.s GNU C version 3.3.6 (Ubuntu 1:3.3.6-15ubuntu1) (i486-linux-gnu) comstackd by GNU C version 3.3.6 (Ubuntu 1:3.3.6-15ubuntu1). GGC heuristics: --param ggc-min-expand=99 --param ggc-min-heapsize=129473 options passed: -v -D__GNUC__=3 -D__GNUC_MINOR__=3 -D__GNUC_PATCHLEVEL__=6 -auxbase -g options enabled: -fpeephole -ffunction-cse -fkeep-static-consts -fpcc-struct-return -fgcse-lm -fgcse-sm -fsched-interblock -fsched-spec -fbranch-count-reg -fcommon -fgnu-linker -fargument-alias -fzero-initialized-in-bss -fident -fmath-errno -ftrapping-math -m80387 -mhard-float -mno-soft-float -mieee-fp -mfp-ret-in-387 -maccumulate-outgoing-args -mcpu=pentiumpro -march=i486 ignoring nonexistent directory "/usr/local/include/i486-linux-gnu" ignoring nonexistent directory "/usr/i486-linux-gnu/include" ignoring nonexistent directory "/usr/include/i486-linux-gnu" #include "..." search starts here: #include  search starts here: /usr/local/include /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/include /usr/include End of search list. gnu_dev_major gnu_dev_minor gnu_dev_makedev stat lstat fstat mknod fatal ec_malloc dump main print_notes find_user_note search_note Execution times (seconds) preprocessing : 0.00 ( 0%) usr 0.01 (25%) sys 0.00 ( 0%) wall lexical analysis : 0.00 ( 0%) usr 0.01 (25%) sys 0.00 ( 0%) wall parser : 0.02 (100%) usr 0.01 (25%) sys 0.00 ( 0%) wall TOTAL : 0.02 0.04 0.00 as -V -Qy -o /tmp/ccugTYeu.o /tmp/ccGT0kTf.s GNU assembler version 2.17.50 (i486-linux-gnu) using BFD version 2.17.50 20070103 Ubuntu /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/../../../crt1.o /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/../../../crti.o /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/crtbegin.o -L/usr/lib/gcc-lib/i486-linux-gnu/3.3.6 -L/usr/lib/gcc-lib/i486-linux-gnu/3.3.6/../../.. /tmp/ccugTYeu.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/crtend.o /usr/lib/gcc-lib/i486-linux-gnu/3.3.6/../../../crtn.o 

NOTE: J’ai lu le livre ” L’art de l’exploitation ” et j’utilise la VM fournie avec le livre.

Le compilateur tente de maintenir un alignement de 16 octets sur la stack. Cela s’applique également au code 32 bits de nos jours (pas seulement 64 bits). L’idée est que, au moment précédant l’exécution d’une instruction CALL , la stack doit être alignée sur une limite de 16 octets.

Parce que vous avez compilé sans optimisations, il existe des instructions superflues.

 0x0804835a : sub esp,0x18 ; Allocate local stack space 0x0804835d : and esp,0xfffffff0 ; Ensure `main` has a 16 byte aligned stack 0x08048360 : mov eax,0x0 ; Extraneous, not needed 0x08048365 : sub esp,eax ; Extraneous, not needed 

ESP est maintenant aligné sur 16 octets après la dernière instruction ci-dessus. Nous déplaçons les parameters de l’appel en commençant par le haut de la stack à ESP . C’est fait avec:

 0x08048367 : mov DWORD PTR [esp+12],0x4 0x0804836f : mov DWORD PTR [esp+8],0x3 0x08048377 : mov DWORD PTR [esp+4],0x2 0x0804837f : mov DWORD PTR [esp],0x1 

L’ appel appelle ensuite une adresse de retour de 4 octets sur la stack. Nous atteignons ensuite ces instructions après l’appel:

 0x08048344 : push ebp ; 4 bytes pushed on stack 0x08048345 : mov ebp,esp ; Setup stackframe 

Cela pousse encore 4 octets sur la stack. Avec les 4 octets de l’adresse de retour, nous sums maintenant mal alignés de 8 octets. Pour atteindre à nouveau un alignement de 16 octets, nous devrons perdre 8 octets supplémentaires sur la stack. C’est pourquoi dans cette déclaration, 8 octets supplémentaires sont alloués:

 0x08048347 : sub esp,0x28 
  • 0x08 octets déjà sur la stack en raison de l’adresse de retour (4 octets) et d’ EBP (4 octets)
  • 0x08 octets de remplissage nécessaires pour aligner la stack sur un alignement de 16 octets
  • 0x20 octets nécessaires pour l’allocation de variables locales = 32 octets. 32/16 est divisible par 16 alors l’alignement est maintenu

Les deuxième et troisième nombres ajoutés ci-dessus sont la valeur 0x28 calculée par le compilateur et utilisée dans le sub esp,0x28 .

 0x0804834a : mov DWORD PTR [ebp-12],0x7a69 

Alors pourquoi [ebp-12] dans cette instruction? Les 8 premiers octets [ebp-8] à [ebp-1] sont les octets d’alignement utilisés pour aligner la stack sur 16 octets. Les données locales apparaîtront ensuite sur la stack après cela. Dans ce cas, [ebp-12] à [ebp-9] sont les 4 octets de l’ flag nombre entier sur 32 bits.

Ensuite, nous avons ceci pour mettre à jour le buffer[0] avec le caractère ‘A’:

 0x08048351 : mov BYTE PTR [ebp-40],0x41 

La bizarrerie serait alors pourquoi un tableau de 10 octets de caractères apparaîtrait de [ebp+40] (début du tableau) à [ebp+13] qui est de 28 octets. La meilleure hypothèse que je puisse faire est que le compilateur a estimé qu’il pouvait traiter le tableau de caractères de 10 octets comme un vecteur de 128 bits (16 octets). Cela obligerait le compilateur à aligner le tampon sur une limite de 16 octets et à étendre le tableau à 16 octets (128 bits). Du sharepoint vue du compilateur, votre code semble agir un peu comme il a été défini comme suit:

 #include  void test_function(int a, int b, int c, int d){ int flag; union { char buffer[10]; __m128 m128buffer; ; 16-byte variable that needs to be 16-bytes aligned } bufu; flag = 31337; bufu.buffer[0] = 'A'; } 

La sortie sur GodBolt pour GCC 4.9.0 générant du code 32 bits avec SSE2 activé apparaît comme suit:

 test_function: push ebp # mov ebp, esp #, sub esp, 40 #,same as: sub esp,0x28 mov DWORD PTR [ebp-12], 31337 # flag, mov BYTE PTR [ebp-40], 65 # bufu.buffer, leave ret 

Cela ressemble beaucoup à votre désassemblage dans GDB .

Si vous avez compilé avec des optimisations (telles que -O1 , -O2 , -O3 ), l’optimiseur aurait pu simplifier la fonction test_function car il s’agit d’une fonction feuille de votre exemple. Une fonction feuille est une fonction qui n’appelle pas une autre fonction. Certains raccourcis auraient pu être appliqués par le compilateur.

Pourquoi le tableau de caractères semble-t-il être aligné sur une limite de 16 octets et complété de 16 octets? On ne pourra probablement pas répondre à cette question avec certitude jusqu’à ce que nous sachions quel compilateur GCC vous utilisez ( gcc --version vous le dira). Il serait également utile de connaître votre système d’exploitation et sa version. Encore mieux serait d’append le résultat de cette commande à votre question gcc -Q -v -g my_program.c

À moins que vous n’essayiez d’améliorer le code de gcc lui-même, comprendre pourquoi un code non optimisé est aussi mauvais qu’il le sera sera surtout une perte de temps. Regardez la sortie de -O3 si vous voulez voir ce qu’un compilateur fait avec votre code, ou de -Og si vous voulez voir une traduction plus littérale de votre source en asm. Écrivez des fonctions qui prennent des entrées dans les arguments et produisent des sorties en globales ou qui renvoient des valeurs, de sorte que l’asm optimisé ne soit pas simplement ret .


Vous ne devriez rien attendre d’efficace de la part de gcc -O0 . C’est la traduction littérale la plus braise de votre source.

Je ne peux pas reproduire cette sortie asm avec aucune version de gcc ou clang sur http://gcc.godbolt.org/ . (GCC 4.4.7 à GCC 5.3.0, Clang 3.0 à 3.7.1). (Notez que godbolt utilise g++ , mais vous pouvez utiliser -xc pour traiter l’entrée en C, au lieu de la comstackr en C ++. Cela peut parfois modifier la sortie asm, même si vous n’utilisez aucune des fonctionnalités de C99 / C11, mais C ++. ne le fait pas (par exemple, les tableaux de longueur variable C99).

Certaines versions de gcc émettent par défaut du code supplémentaire, sauf si j’utilise -fno-stack-protector .

J’ai d’abord pensé que l’espace supplémentaire réservé par test_function consistait à copier ses arguments dans son cadre de stack, mais au moins le gcc moderne ne le fait pas. ( Gcc 64 bits stocke ses arguments en mémoire lorsqu’ils arrivent dans des registres , mais c’est différent. Gcc 32 bits incrémente un argument en place sur la stack, sans le copier .)

L’ABI autorise la fonction appelée à graver ses arguments sur la stack. Par conséquent, un appelant qui souhaite effectuer des appels de fonction répétés avec les mêmes arguments doit continuer à les stocker entre les appels.

clang 3.7.1 avec -O0 copie ses -O0 dans des sections locales , mais cela ne réserve que 32 octets ( 0x20 ).

C’est à peu près la meilleure réponse que vous obtiendrez à moins de nous dire quelle version de gcc vous utilisez …