Sur mon système Debian GNU / Linux 9, lorsqu'un binaire est exécuté,
- la pile n'est pas initialisée mais
- le tas est initialisé à zéro.
Pourquoi?
Je suppose que l'initialisation à zéro favorise la sécurité mais, si pour le tas, pourquoi pas aussi pour la pile? La pile n'a-t-elle pas non plus besoin de sécurité?
Pour autant que je sache, ma question n'est pas spécifique à Debian.
Exemple de code C:
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 8;
// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
const int *const p, const size_t size, const char *const name
)
{
printf("%s at %p: ", name, p);
for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
printf("\n");
}
// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
int a[n];
int *const b = malloc(n*sizeof(int));
print_array(a, n, "a");
print_array(b, n, "b");
free(b);
return 0;
}
Production:
a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0
La norme C ne demande pas malloc()
d'effacer la mémoire avant de l'allouer, bien sûr, mais mon programme C est simplement à titre d'illustration. La question n'est pas une question sur C ou sur la bibliothèque standard de C. Au contraire, la question est de savoir pourquoi le noyau et / ou le chargeur d'exécution mettent à zéro le tas mais pas la pile.
UNE AUTRE EXPÉRIENCE
Ma question concerne le comportement GNU / Linux observable plutôt que les exigences des documents de normes. Si vous ne savez pas ce que je veux dire, essayez ce code, qui invoque d'autres comportements non définis ( non définis, c'est-à-dire en ce qui concerne la norme C) pour illustrer le point:
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 4;
int main()
{
for (size_t i = n; i; --i) {
int *const p = malloc(sizeof(int));
printf("%p %d ", p, *p);
++*p;
printf("%d\n", *p);
free(p);
}
return 0;
}
Sortie de ma machine:
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
En ce qui concerne la norme C, le comportement n'est pas défini, donc ma question ne concerne pas la norme C. Un appel à malloc()
ne pas avoir besoin de renvoyer la même adresse à chaque fois mais, puisque cet appel à malloc()
arrive effectivement à renvoyer la même adresse à chaque fois, il est intéressant de remarquer que la mémoire, qui est sur le tas, est mise à zéro à chaque fois.
La pile, en revanche, ne semblait pas être mise à zéro.
Je ne sais pas ce que ce dernier code fera sur votre machine, car je ne sais pas quelle couche du système GNU / Linux est à l'origine du comportement observé. Vous ne pouvez que l'essayer.
MISE À JOUR
@Kusalananda a observé dans les commentaires:
Pour ce qu'il vaut, votre code le plus récent renvoie différentes adresses et données (occasionnelles) non initialisées (non nulles) lorsqu'il est exécuté sur OpenBSD. Cela ne dit évidemment rien sur le comportement que vous observez sous Linux.
Que mon résultat diffère du résultat sur OpenBSD est en effet intéressant. Apparemment, mes expériences ne découvraient pas un protocole de sécurité du noyau (ou éditeur de liens), comme je l'avais pensé, mais un simple artefact d'implémentation.
Dans cette optique, je pense qu'ensemble, les réponses ci-dessous de @mosvy, @StephenKitt et @AndreasGrapentin tranchent ma question.
Voir aussi sur Stack Overflow: Pourquoi malloc initialise-t-il les valeurs à 0 dans gcc? (crédit: @bta).
new
opérateur en C ++ (également "tas") est sous Linux juste un wrapper pour malloc (); le noyau ne sait ni ne se soucie de ce qu'est le "tas".Réponses:
Le stockage renvoyé par malloc () n'est pas initialisé à zéro. Ne présumez jamais que c'est le cas.
Dans votre programme de test, ce n'est qu'un coup de chance: je suppose que le
malloc()
nouveau bloc vient d'être retirémmap()
, mais ne vous fiez pas non plus à cela.Par exemple, si j'exécute votre programme sur ma machine de cette façon:
Votre deuxième exemple est simplement d'exposer un artefact de l'
malloc
implémentation dans la glibc; si vous répétezmalloc
/free
avec un tampon supérieur à 8 octets, vous verrez clairement que seuls les 8 premiers octets sont mis à zéro, comme dans l'exemple de code suivant.Production:
la source
Quelle que soit la façon dont la pile est initialisée, vous ne voyez pas une pile vierge, car la bibliothèque C fait un certain nombre de choses avant d'appeler
main
, et elles touchent la pile.Avec la bibliothèque GNU C, sur x86-64, l'exécution commence au point d'entrée _start , qui appelle
__libc_start_main
pour configurer les choses, et ce dernier finit par appelermain
. Mais avant d'appelermain
, il appelle un certain nombre d'autres fonctions, ce qui entraîne l'écriture de divers éléments de données dans la pile. Le contenu de la pile n'est pas effacé entre les appels de fonction, donc lorsque vous entrezmain
, votre pile contient les restes des appels de fonction précédents.Cela explique seulement les résultats que vous obtenez de la pile, voir les autres réponses concernant votre approche générale et vos hypothèses.
la source
main()
est appelé, les routines d'initialisation peuvent très bien avoir modifié la mémoire retournée parmalloc()
- en particulier si les bibliothèques C ++ sont liées. Supposer que le "tas" est initialisé à n'importe quoi est une très, très mauvaise hypothèse.Dans les deux cas, vous obtenez de la mémoire non initialisée et vous ne pouvez faire aucune hypothèse sur son contenu.
Lorsque le système d'exploitation doit attribuer une nouvelle page à votre processus (que ce soit pour sa pile ou pour l'arène utilisée par
malloc()
), il garantit qu'il n'exposera pas les données d'autres processus; la façon habituelle de le faire est de le remplir de zéros (mais il est également valable de l'écraser avec autre chose, y compris même une page valant/dev/urandom
- en fait, certainesmalloc()
implémentations de débogage écrivent des modèles non nuls, pour capturer des hypothèses erronées comme la vôtre).Si
malloc()
peut satisfaire la demande de la mémoire déjà utilisée et libérée par ce processus, son contenu ne sera pas effacé (en fait, l'effacement n'a rien à voir avecmalloc()
ce qu'il ne peut pas être - il doit se produire avant que la mémoire ne soit mappée dans votre espace d'adressage). Vous pouvez obtenir de la mémoire qui a déjà été écrite par votre processus / programme (par exemple avantmain()
).Dans votre exemple de programme, vous voyez une
malloc()
région qui n'a pas encore été écrite par ce processus (c'est-à-dire directement à partir d'une nouvelle page) et une pile qui a été écrite (par précodagemain()
dans votre programme). Si vous examinez davantage la pile, vous constaterez qu'elle est remplie de zéros plus bas (dans son sens de croissance).Si vous voulez vraiment comprendre ce qui se passe au niveau du système d'exploitation, je vous recommande de contourner la couche Bibliothèque C et d'interagir à l'aide d'appels système tels que
brk()
et à lammap()
place.la source
malloc()
et àfree()
plusieurs reprises. Bien que rien ne nécessitemalloc()
de réutiliser le même stockage récemment libéré, dans l'expérience,malloc()
cela s'est produit. Il est arrivé de renvoyer la même adresse à chaque fois, mais a également annulé la mémoire à chaque fois, ce à quoi je ne m'attendais pas. C'était intéressant pour moi. De nouvelles expériences ont conduit à la question d'aujourd'hui.malloc()
ne font absolument rien avec la mémoire qu'ils vous remettent - c'est soit déjà utilisé, soit fraîchement assigné (et donc mis à zéro par le système d'exploitation). Dans votre test, vous avez évidemment obtenu ce dernier. De même, la mémoire de pile est donnée à votre processus à l'état effacé, mais vous ne l'examinez pas assez loin pour voir les parties que votre processus n'a pas encore touchées. La mémoire de votre pile est effacée avant d'être transmise à votre processus.calloc
peut être une option (au lieu dememset
)mmap(MAP_ANONYMOUS)
moins que vous ne l'utilisiezMAP_POPULATE
également. Les nouvelles pages de pile sont, espérons-le, soutenues par de nouvelles pages physiques et câblées (mappées dans les tables de pages du matériel, ainsi que la liste des pointeurs / longueurs des mappages du noyau) lors de leur croissance, car normalement, la nouvelle mémoire de la pile est écrite lors de la première utilisation. . Mais oui, le noyau doit éviter les fuites de données d'une manière ou d'une autre, et la mise à zéro est la moins chère et la plus utile.Votre prémisse est fausse.
Ce que vous décrivez comme «sécurité» est vraiment de la confidentialité , ce qui signifie qu'aucun processus ne peut lire la mémoire d'un autre processus, à moins que cette mémoire ne soit explicitement partagée entre ces processus. Dans un système d'exploitation, il s'agit d'un aspect de l' isolement d'activités ou de processus simultanés.
Ce que le système d'exploitation fait pour garantir cette isolation, c'est chaque fois que la mémoire est demandée par le processus d'allocation de tas ou de pile, cette mémoire provient soit d'une région de la mémoire physique qui est remplie de zéros, ou qui est remplie d'ordure qui est provenant du même processus .
Cela garantit que vous ne voyez que des zéros ou vos propres ordures, de sorte que la confidentialité est garantie et que le tas et la pile sont `` sécurisés '', bien qu'ils ne soient pas nécessairement (zéro) initialisés.
Vous lisez trop dans vos mesures.
la source