No meu sistema Debian GNU/Linux 9, quando um binário é executado,
- a pilha não é inicializada, mas
- o heap é inicializado com zero.
Por quê?
Presumo que a inicialização zero promova a segurança, mas, se 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 até onde eu sei.
Exemplo de código 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;
}
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 pergunta 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 o heap, mas não a pilha.
OUTRO EXPERIMENTO
Minha pergunta diz respeito ao comportamento GNU/Linux observável em vez dos requisitos dos documentos de padrões. Se não tiver certeza do que quero dizer, tente este código, que invoca mais comportamento indefinido ( indefinido, ou seja, 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, então minha pergunta não diz respeito ao padrão C. Uma chamada para malloc()
não precisa retornar o mesmo endereço todas as vezes, mas, como essa chamada para de malloc()
fato retorna o mesmo endereço todas as vezes, é interessante notar que a memória, que está no heap, é zerada a cada vez.
A pilha, por outro lado, não parecia estar 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, mas tente.
ATUALIZAR
@Kusalananda observou nos comentários:
Para o que vale a pena, seu código mais recente retorna diferentes endereços e (ocasionalmente) dados não inicializados (diferentes de zero) quando executados no OpenBSD. Isso obviamente não diz nada sobre o comportamento que você está testemunhando no Linux.
Que meu resultado difere do resultado no OpenBSD é realmente interessante. Aparentemente, meus experimentos estavam descobrindo não um protocolo de segurança do kernel (ou linker), como eu pensava, mas um mero artefato de implementação.
Diante disso, acredito que, juntas, as respostas abaixo de @mosvy, @StephenKitt e @AndreasGrapentin resolvem minha dúvida.
Veja também no Stack Overflow: Por que o malloc inicializa os valores como 0 no gcc? (crédito: @bta).
O armazenamento retornado por malloc() não é inicializado com zero. Nunca assuma que é.
Em seu programa de teste, é apenas um acaso: acho que
malloc()
acabou de receber um novo blocommap()
, mas também não confie nisso.Por exemplo, se eu executar seu programa na minha máquina desta forma:
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 primeiros 8 bytes são zerados, como no código de exemplo a seguir.Resultado:
Independentemente de como a pilha é inicializada, você não está vendo uma pilha original, porque a biblioteca C faz várias coisas antes de chamar
main
, e elas tocam a pilha.Com a biblioteca GNU C, em x86-64, a execução começa no ponto de entrada _start , que chama
__libc_start_main
para configurar as coisas, e o último acaba chamandomain
. Mas 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ê entra emmain
, sua pilha contém 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 e suposições gerais.
Em ambos os casos, você obtém memória não inicializada e não pode fazer suposições sobre seu conteúdo.
Quando o SO tem que distribuir uma nova página ao seu processo (seja para sua pilha ou para a arena usada por
malloc()
), ele garante que não irá expor dados de outros processos; a maneira usual de garantir isso é preenchê-lo com zeros (mas é igualmente válido substituir por qualquer outra coisa, incluindo até mesmo uma página de valor/dev/urandom
- na verdade, algumas implementações de depuraçãomalloc()
escrevem padrões diferentes de zero, para capturar suposições equivocadas como a sua).Se
malloc()
puder satisfazer o pedido da memória já usada e liberada por este processo, seu conteúdo não será limpo (na verdade, a limpeza não tem nada a ver commalloc()
e não pode ser - 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()
de ).Em seu programa de exemplo, você está vendo uma
malloc()
região que ainda não foi escrita por este processo (ou seja, é direto de uma nova página) e uma pilha que foi escrita (por prémain()
-código em seu programa). Se você examinar mais da pilha, descobrirá que ela está cheia de zeros mais abaixo (na direção de crescimento).Se você realmente quiser entender o que está acontecendo no nível do sistema operacional, recomendo que você ignore a camada C Library e interaja usando chamadas de sistema como
brk()
emmap()
em vez disso.Sua premissa está errada.
O que você descreve como 'segurança' é realmente confidencialidade , o que significa 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 é que sempre que a memória é solicitada pelo processo para alocações de heap ou pilha, essa memória vem de uma região da memória física que está preenchida com zeros ou está cheia de lixo que é provenientes do mesmo processo .
Isso garante que você esteja vendo apenas zeros, ou seu próprio lixo, de modo que a confidencialidade é garantida, e tanto o heap quanto a pilha são 'seguros', embora não necessariamente (zero) inicializados.
Você está lendo demais em suas medidas.