Comment fonctionne l'allocation de pile sous Linux?


18

Le système d'exploitation réserve-t-il la quantité fixe d'espace virtuel valide pour la pile ou autre chose? Suis-je capable de produire un débordement de pile simplement en utilisant de grandes variables locales?

J'ai écrit un petit Cprogramme pour tester mon hypothèse. Il fonctionne sur X86-64 CentOS 6.5.

#include <string.h>
#include <stdio.h>
int main()
{
    int n = 10240 * 1024;
    char a[n];
    memset(a, 'x', n);
    printf("%x\n%x\n", &a[0], &a[n-1]);
    getchar();
    return 0;
}

L'exécution du programme donne &a[0] = f0ceabe0et&a[n-1] = f16eabdf

Les cartes proc montrent la pile: 7ffff0cea000-7ffff16ec000. (10248 * 1024B)

J'ai ensuite essayé d'augmenter n = 11240 * 1024

L'exécution du programme donne &a[0] = b6b36690et&a[n-1] = b763068f

Les cartes proc montrent la pile: 7fffb6b35000-7fffb7633000. (11256 * 1024B)

ulimit -simprime 10240sur mon PC.

Comme vous pouvez le voir, dans les deux cas, la taille de la pile est plus grande que ce qui ulimit -sdonne. Et la pile grandit avec une plus grande variable locale. Le haut de la pile est en quelque sorte de 3 à 5 Ko de plus &a[0](AFAIK, la zone rouge est de 128B).

Alors, comment cette carte de pile est-elle allouée?

Réponses:


14

Il semble que la limite de mémoire de la pile ne soit pas allouée (de toute façon, elle ne pouvait pas avec une pile illimitée). https://www.kernel.org/doc/Documentation/vm/overcommit-accounting dit:

La croissance de la pile du langage C fait un mremap implicite. Si vous voulez des garanties absolues et courez près du bord, vous DEVEZ mmapper votre pile pour la plus grande taille dont vous pensez avoir besoin. Pour une utilisation typique de la pile, cela n'a pas beaucoup d'importance, mais c'est un cas d'angle si vous vous souciez vraiment

Cependant, le mappage de la pile serait l'objectif d'un compilateur (s'il a une option pour cela).

EDIT: Après quelques tests sur une machine Debian x84_64, j'ai constaté que la pile se développe sans aucun appel système (selon strace). Donc, cela signifie que le noyau le fait croître automatiquement (c'est ce que signifie "implicite" ci-dessus), c'est-à-dire sans explicite mmap/ mremapdu processus.

Il était assez difficile de trouver des informations détaillées le confirmant. Je recommande de comprendre le gestionnaire de mémoire virtuelle Linux par Mel Gorman. Je suppose que la réponse se trouve dans la section 4.6.1 Gestion d'un défaut de page , à l'exception de "Région non valide mais à côté d'une région extensible comme la pile" et de l'action correspondante "Développez la région et allouez une page". Voir aussi D.5.2 Extension de la pile .

Autres références sur la gestion de la mémoire Linux (mais avec presque rien sur la pile):

EDIT 2: Cette implémentation a un inconvénient: dans les cas d'angle, une collision pile-tas peut ne pas être détectée, même dans le cas où la pile serait plus grande que la limite! La raison en est qu'une écriture dans une variable de la pile peut se retrouver dans la mémoire de tas allouée, auquel cas il n'y a pas de défaut de page et le noyau ne peut pas savoir que la pile devait être étendue. Voir mon exemple dans la discussion Collision pile-tas silencieuse sous GNU / Linux que j'ai commencée dans la liste d'aide gcc. Pour éviter cela, le compilateur doit ajouter du code lors de l'appel de fonction; cela peut être fait avec -fstack-checkpour GCC (voir la réponse de Ian Lance Taylor et la page de manuel de GCC pour plus de détails).


Cela semble être la bonne réponse à ma question. Mais cela m'embrouille davantage. Quand l'appel mremap sera-t-il déclenché? Sera-ce un appel système intégré au programme?
Amos

@amos Je suppose que l'appel mremap sera déclenché si besoin est lors d'un appel de fonction ou lorsque alloca () est appelée.
vinc17

Ce serait probablement une bonne idée de mentionner ce qu'est mmap, pour les personnes qui ne savent pas.
Faheem Mitha

@FaheemMitha J'ai ajouté quelques informations. Pour ceux qui ne savent pas ce qu'est mmap, consultez la FAQ sur la mémoire mentionnée ci-dessus. Ici, pour la pile, cela aurait été un "mappage anonyme" afin que l'espace inutilisé ne prenne pas de mémoire physique, mais comme expliqué par Mel Gorman, le noyau fait le mappage (mémoire virtuelle) et l'allocation physique en même temps .
vinc17

1
@max J'ai essayé le programme de l'OP en ulimit -sdonnant 10240, comme dans les conditions de l'OP, et j'obtiens un SIGSEGV comme prévu (c'est ce qui est requis par POSIX: "Si cette limite est dépassée, SIGSEGV doit être généré pour le thread. "). Je soupçonne un bogue dans le noyau de l'OP.
vinc17

6

Noyau Linux 4.2

Programme de test minimal

Nous pouvons ensuite le tester avec un programme NASM 64 bits minimal:

global _start
_start:
    sub rsp, 0x7FF000
    mov [rsp], rax
    mov rax, 60
    mov rdi, 0
    syscall

Assurez-vous que vous désactivez ASLR et supprimez les variables d'environnement car celles-ci iront sur la pile et prendront de l'espace:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
env -i ./main.out

La limite est quelque part légèrement en dessous de ma ulimit -s(8 Mo pour moi). Il semble que cela soit dû aux données supplémentaires spécifiées par System V initialement placées sur la pile en plus de l'environnement: Paramètres de ligne de commande Linux 64 dans Assembly | Débordement de pile

Si vous êtes sérieux à ce sujet, TODO crée une image initrd minimale qui commence à écrire à partir du haut de la pile et descend, puis exécutez-la avec QEMU + GDB . Mettez un dprintfsur la boucle imprimant l'adresse de la pile et un point d'arrêt à acct_stack_growth. Ce sera glorieux.

En relation:


2

Par défaut, la taille maximale de la pile est configurée pour être de 8 Mo par processus,
mais elle peut être modifiée à l'aide de ulimit:

Affichage de la valeur par défaut en Ko:

$ ulimit -s
8192

Réglez sur illimité:

ulimit -s unlimited

affectant le shell et les sous-shell actuels et leurs processus enfants.
( ulimitest une commande intégrée au shell)

Vous pouvez afficher la plage d'adresses de pile réelle utilisée avec:
cat /proc/$PID/maps | grep -F '[stack]'
sous Linux.


Ainsi, lorsqu'un programme est chargé par le shell actuel, le système d'exploitation rend un segment de mémoire de ulimit -sKo valide pour le programme. Dans mon cas, c'est 10240 Ko. Mais lorsque je déclare un tableau local char a[10240*1024]et que je définis a[0]=1, le programme se ferme correctement. Pourquoi?
Amos

Essayez également de définir le dernier élément. Et assurez-vous qu'ils ne sont pas optimisés.
vinc17

@amos Je pense que ce que signifie vinc17 est que vous avez nommé une région mémoire qui ne rentrerait pas dans la pile de votre programme , mais comme vous n'y accédez pas réellement dans la partie qui ne rentre pas , la machine ne le remarque jamais - elle ne le fait pas même obtenir cette information .
Volker Siegel

@amos Essayez int n = 10240*1024; char a[n]; memset(a,'x',n);... faute de seg.
goldilocks

2
@amos Donc, comme vous pouvez le voir, a[]n'a pas été alloué dans votre pile de 10 Mo. Le compilateur a peut-être vu qu'il ne pouvait pas y avoir d'appel récursif et a effectué une allocation spéciale, ou quelque chose d'autre comme une pile discontinue ou une indirection.
vinc17
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.