Sur mon système Debian GNU/Linux 9, lorsqu'un binaire est exécuté,
Pourquoi?
Je suppose que l'initialisation à zéro favorise la sécurité mais, si pour le tas, alors 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
Le standard 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éfini, 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()
n'a pas besoin de renvoyer la même adresse à chaque fois mais, comme cet appel à malloc()
se trouve effectivement renvoyer la même adresse à chaque fois, il est intéressant de noter que la mémoire, qui est sur le tas, est remise à 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 (occasionnelles) données 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).
Le stockage retourné par malloc () est pas initialisé à zéro. Ne présumez jamais que c'est le cas.
Dans votre programme de test, c'est juste un coup de chance: je suppose que la malloc()
vient de retirer un nouveau bloc mmap()
, mais ne vous fiez pas à cela non plus.
Par exemple, si j'exécute votre programme sur ma machine de cette façon:
$ echo 'void __attribute__((constructor)) p(void){
void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so
$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036
Votre deuxième exemple montre simplement un artefact de l'implémentation malloc
dans glibc; si vous faites cela malloc
/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.
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 4;
const size_t m = 0x10;
int main()
{
for (size_t i = n; i; --i) {
int *const p = malloc(m*sizeof(int));
printf("%p ", p);
for (size_t j = 0; j < m; ++j) {
printf("%d:", p[j]);
++p[j];
printf("%d ", p[j]);
}
free(p);
printf("\n");
}
return 0;
}
Production:
0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4
Quelle que soit la façon dont la pile est initialisée, vous ne voyez pas de 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 appeler main
. Mais avant d'appeler main
, 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 entrez dans main
, votre pile contient les restes des appels de fonction précédents.
Cela explique uniquement les résultats que vous obtenez de la pile, voir les autres réponses concernant votre approche générale et vos hypothèses.
Dans les deux cas, vous obtenez non initialisé mémoire, et vous ne pouvez faire aucune hypothèse sur son contenu.
Lorsque le système d'exploitation doit allouer 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 manière habituelle de le faire est de le remplir de zéros (mais il est également valable de l'écraser avec quoi que ce soit d'autre, y compris même une page de /dev/urandom
- en fait, certaines implémentations de débogage malloc()
écrivent non -modèles zéro, pour capturer des hypothèses erronées telles que les vôtres).
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 avec malloc()
et il peut pas - cela doit arriver 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 avant main()
).
Dans votre exemple de programme, vous voyez une région malloc()
qui n'a pas encore été écrite par ce processus (c'est-à-dire qu'elle provient directement d'une nouvelle page) et une pile qui a été écrite (par pré-main()
code dans votre programme). Si vous examinez davantage la pile, vous constaterez qu'elle est remplie de zéros plus bas (dans sa direction 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 mmap()
à la place.
Votre prémisse est fausse.
Ce que vous décrivez comme "sécurité" est vraiment 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 des activités ou processus simultanés.
Ce que le système d'exploitation fait pour assurer 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 avec des zéros, ou qui est remplie de courrier indésirable qui est venant de le même processus.
Cela garantit que vous ne voyez que des zéros ou vos propres ordures, la confidentialité est donc garantie et les deux tas et la pile sont 'sécurisées', mais pas nécessairement (zéro -) initialisé.
Vous lisez trop dans vos mesures.