No meu sistema Debian GNU / Linux 9, quando um binário é executado,
- a pilha não foi inicializada, mas
- o heap é inicializado com zero.
Por quê?
Suponho que a inicialização zero promova segurança, mas, se for para o heap, por que não também para a pilha? A pilha também não precisa de segurança?
Minha pergunta não é específica para o Debian, tanto quanto eu sei.
Código C de exemplo:
#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;
}
Resultado:
a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0
O padrão C não pede malloc()
para limpar a memória antes de alocá-la, é claro, mas meu programa C é meramente ilustrativo. A questão não é sobre C ou sobre a biblioteca padrão de C. Em vez disso, a pergunta é sobre por que o kernel e / ou o carregador de tempo de execução estão zerando a pilha, mas não a pilha.
OUTRA EXPERIÊNCIA
Minha pergunta diz respeito ao comportamento observável do GNU / Linux, e não aos requisitos dos documentos de padrões. Se não souber o que quero dizer, tente este código, que invoca um comportamento indefinido adicional ( indefinido, isto é, no que diz respeito ao padrão C) para ilustrar o ponto:
#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;
}
Saída da minha máquina:
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
No que diz respeito ao padrão C, o comportamento é indefinido, portanto minha pergunta não considera o padrão C. Uma chamada para malloc()
não precisar retornar o mesmo endereço todas as vezes, mas, como essa chamada malloc()
realmente retorna o mesmo endereço toda vez, é interessante notar que a memória, que está na pilha, é zerada toda vez.
A pilha, por outro lado, não parecia zerada.
Não sei o que o último código fará em sua máquina, pois não sei qual camada do sistema GNU / Linux está causando o comportamento observado. Você pode apenas tentar.
ATUALIZAR
@Kusalananda observou nos comentários:
Pelo que vale a pena, seu código mais recente retorna endereços diferentes e dados (não ocasionais) não inicializados (diferentes de zero) quando executados no OpenBSD. Obviamente, isso não diz nada sobre o comportamento que você está testemunhando no Linux.
Que meu resultado seja diferente do resultado no OpenBSD é realmente interessante. Aparentemente, meus experimentos estavam descobrindo não um protocolo de segurança do kernel (ou vinculador), como eu pensava, mas um mero artefato de implementação.
Nesta perspectiva, acredito que, juntas, as respostas abaixo de @mosvy, @StephenKitt e @AndreasGrapentin resolvam minha pergunta.
Veja também no Stack Overflow: Por que o malloc inicializa os valores para 0 em gcc? (crédito: @bta).
new
operador em C ++ (também "heap") está no Linux apenas um invólucro para malloc (); o kernel não sabe nem se importa com o que é o "heap".Respostas:
O armazenamento retornado por malloc () não é inicializado com zero. Nunca assuma que seja.
No seu programa de teste, é apenas um acaso: acho que
malloc()
acabamos de ganhar um novo blocommap()
, mas também não confie nisso.Por exemplo, se eu executar o seu programa na minha máquina desta maneira:
Seu segundo exemplo é simplesmente expor um artefato da
malloc
implementação na glibc; se você fizer isso repetidomalloc
/free
com um buffer maior que 8 bytes, verá claramente que apenas os 8 primeiros bytes são zerados, como no código de exemplo a seguir.Resultado:
fonte
Independentemente de como a pilha é inicializada, você não está vendo uma pilha intocada, porque a biblioteca C faz várias coisas antes da chamada
main
e elas tocam na pilha.Com a biblioteca GNU C, em x86-64, a execução inicia no ponto de entrada _start , que chama
__libc_start_main
para configurar as coisas, e a última acaba chamandomain
. Porém, antes de chamarmain
, ele chama várias outras funções, o que faz com que vários dados sejam gravados na pilha. O conteúdo da pilha não é limpo entre as chamadas de função; portanto, quando você entramain
, sua pilha contém as sobras das chamadas de função anteriores.Isso explica apenas os resultados que você obtém da pilha; veja as outras respostas sobre sua abordagem geral e suposições.
fonte
main()
é chamado, as rotinas de inicialização podem muito bem ter modificado a memória retornada pormalloc()
- especialmente se as bibliotecas C ++ estiverem vinculadas. Supondo que o "heap" seja inicializado para qualquer coisa é uma suposição muito, muito ruim.Nos dois casos, você obtém memória não inicializada e não pode fazer nenhuma suposição sobre seu conteúdo.
Quando o sistema operacional precisa distribuir uma nova página ao seu processo (seja para a pilha ou para a arena usada
malloc()
), garante que não exporá dados de outros processos; a maneira usual de garantir isso é preenchê-lo com zeros (mas é igualmente válido substituí-lo por qualquer outra coisa, incluindo até uma página/dev/urandom
- de fato, algumasmalloc()
implementações de depuração escrevem padrões diferentes de zero, para capturar suposições equivocadas como a sua).Se
malloc()
puder satisfazer a solicitação da memória já usada e liberada por esse processo, seu conteúdo não será limpo (na verdade, a limpeza não tem nada a ver commalloc()
isso e não pode - ela tem que acontecer antes que a memória seja mapeada para seu espaço de endereço). Você pode obter memória que foi gravada anteriormente pelo seu processo / programa (por exemplo, antesmain()
).No seu exemplo de programa, você está vendo uma
malloc()
região que ainda não foi gravada por esse processo (ou seja, é direta a partir de uma nova página) e uma pilha que foi gravada (por pré-main()
código no seu programa). Se você examinar mais da pilha, verá que ela é preenchida com zero mais abaixo (em sua direção de crescimento).Se você realmente deseja entender o que está acontecendo no nível do sistema operacional, recomendo que você ignore a camada da Biblioteca C e interaja usando chamadas do sistema como
brk()
emmap()
.fonte
malloc()
efree()
repetidamente. Embora nada exijamalloc()
reutilizar o mesmo armazenamento liberado recentemente, no experimento,malloc()
isso aconteceu. Por acaso, retornava o mesmo endereço todas as vezes, mas também anulava a memória todas as vezes, o que eu não esperava. Isso foi interessante para mim. Outras experiências levaram à pergunta de hoje.malloc()
não faz absolutamente nada com a memória que eles lhe entregam - seja usada anteriormente ou recém-atribuída (e, portanto, zerada pelo sistema operacional). No seu teste, você evidentemente conseguiu o último. Da mesma forma, a memória da pilha é fornecida ao seu processo no estado limpo, mas você não a examina o suficiente para ver as partes que o seu processo ainda não tocou. A memória da pilha é limpa antes de ser entregue ao seu processo.calloc
pode ser uma opção (em vez dememset
)mmap(MAP_ANONYMOUS)
menos que você useMAP_POPULATE
também. Espera-se que novas páginas de pilha sejam apoiadas por novas páginas físicas e conectadas (mapeadas nas tabelas de páginas de hardware, bem como na lista de mapeamentos de ponteiro / comprimento do kernel) ao crescer, porque normalmente uma nova memória de pilha está sendo gravada quando tocada pela primeira vez . Mas sim, o kernel deve evitar o vazamento de dados de alguma forma, e zerar é o mais barato e mais útil.Sua premissa está errada.
O que você descreve como 'segurança' é realmente confidencialidade , significando que nenhum processo pode ler a memória de outro processo, a menos que essa memória seja explicitamente compartilhada entre esses processos. Em um sistema operacional, esse é um aspecto do isolamento de atividades ou processos simultâneos.
O que o sistema operacional está fazendo para garantir esse isolamento é quando a memória é solicitada pelo processo para alocações de heap ou pilha, essa memória é proveniente de uma região na memória física preenchida com zeros ou preenchida com lixo eletrônico provenientes do mesmo processo .
Isso garante que você apenas veja zeros ou seu próprio lixo, para garantir a confidencialidade, e o heap e a pilha são 'seguros', embora não sejam necessariamente inicializados com zero.
Você está lendo muito em suas medidas.
fonte