Como a alocação de pilha funciona no Linux?

18

O sistema operacional reserva a quantidade fixa de espaço virtual válido para a pilha ou outra coisa? Posso produzir um estouro de pilha apenas usando grandes variáveis ​​locais?

Eu escrevi um pequeno Cprograma para testar minha suposição. Está sendo executado no 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;
}

A execução do programa fornece &a[0] = f0ceabe0e&a[n-1] = f16eabdf

Os mapas proc mostram a pilha: 7ffff0cea000-7ffff16ec000. (10248 * 1024B)

Então eu tentei aumentar n = 11240 * 1024

A execução do programa fornece &a[0] = b6b36690e&a[n-1] = b763068f

Os mapas proc mostram a pilha: 7fffb6b35000-7fffb7633000. (11256 * 1024B)

ulimit -simprime 10240no meu PC.

Como você pode ver, em ambos os casos, o tamanho da pilha é maior do que o indicado ulimit -s. E a pilha cresce com maior variável local. O topo da pilha está de alguma forma 3-5kB a mais &a[0](AFAIK, a zona vermelha é 128B).

Então, como esse mapa de pilha é alocado?

Amos
fonte

Respostas:

14

Parece que o limite de memória da pilha não está alocado (de qualquer forma, não poderia com pilha ilimitada). https://www.kernel.org/doc/Documentation/vm/overcommit-accounting diz:

O crescimento da pilha da linguagem C cria um mremap implícito. Se você quer garantias absolutas e corre perto do limite, DEVE mapear sua pilha para o maior tamanho que achar necessário. Para o uso típico da pilha, isso não importa muito, mas é uma caixa de canto se você realmente se importa

No entanto, o mapeamento da pilha seria o objetivo de um compilador (se houver uma opção para isso).

EDIT: Após alguns testes em uma máquina Debian x84_64, eu descobri que a pilha cresce sem nenhuma chamada do sistema (de acordo com strace). Então, isso significa que o kernel cresce automaticamente (é o que o "implícito" significa acima), ou seja, sem explícito mmap/ mremapdo processo.

Foi muito difícil encontrar informações detalhadas confirmando isso. Eu recomendo Noções básicas sobre o Linux Virtual Memory Manager, de Mel Gorman. Suponho que a resposta esteja na Seção 4.6.1 Manipulando uma falha de página , com a exceção "Região inválida, mas está ao lado de uma região expansível como a pilha" e a ação correspondente "Expandir a região e alocar uma página". Veja também D.5.2 Expandindo a pilha .

Outras referências sobre o gerenciamento de memória do Linux (mas com quase nada sobre a pilha):

EDIT 2: Esta implementação tem uma desvantagem: em casos de canto, uma colisão de pilha não pode ser detectada, mesmo no caso em que a pilha seja maior que o limite! O motivo é que uma gravação em uma variável na pilha pode acabar na memória heap alocada; nesse caso, não há falha de página e o kernel não pode saber que a pilha precisava ser estendida. Veja meu exemplo na discussão Colisão silenciosa de pilha e pilha no GNU / Linux que iniciei na lista de ajuda do gcc. Para evitar isso, o compilador precisa adicionar algum código na chamada de função; isso pode ser feito com o -fstack-checkGCC (consulte a resposta de Ian Lance Taylor e a página de manual do GCC para obter detalhes).

vinc17
fonte
Essa parece a resposta correta para minha pergunta. Mas isso me confunde mais. Quando a chamada do mremap será acionada? Será um syscall embutido no programa?
Amos
@amos Suponho que a chamada mremap será acionada se for necessário em uma chamada de função ou quando alloca () for chamado.
vinc17
Provavelmente seria uma boa idéia mencionar o que é o mmap, para pessoas que não sabem.
Faheem Mitha
@FaheemMitha Adicionei algumas informações. Para quem não sabe o que é o mmap, consulte as perguntas frequentes sobre memória mencionadas acima. Aqui, para a pilha, teria sido "mapeamento anônimo" para que o espaço não utilizado não ocupasse memória física, mas, como explicado por Mel Gorman, o kernel faz o mapeamento (memória virtual) e a alocação física ao mesmo tempo. .
vinc17
1
@max Tentei o programa do OP ulimit -sfornecendo 10240, como nas condições do OP, e recebo um SIGSEGV conforme o esperado (é o que é exigido pelo POSIX: "Se esse limite for excedido, o SIGSEGV será gerado para o encadeamento. "). Suspeito de um bug no kernel do OP.
vinc17 21/02
6

Kernel Linux 4.2

Programa de teste mínimo

Em seguida, podemos testá-lo com um programa NASM mínimo de 64 bits:

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

Certifique-se de desativar o ASLR e remover as variáveis ​​de ambiente, pois elas vão para a pilha e ocupam espaço:

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

O limite está um pouco abaixo do meu ulimit -s(8MiB para mim). Parece que isso se deve a dados adicionais especificados no System V, inicialmente colocados na pilha, além do ambiente: Parâmetros da linha de comando do Linux 64 no Assembly | Estouro de pilha

Se você é sério sobre isso, o TODO cria uma imagem mínima do initrd que começa a gravar a partir do topo da pilha e diminui e, em seguida, execute-a com QEMU + GDB . Coloque um dprintfno loop imprimindo o endereço da pilha e um ponto de interrupção em acct_stack_growth. Será glorioso.

Relacionado:

Ciro Santilli adicionou uma nova foto
fonte
2

Por padrão, o tamanho máximo da pilha é configurado para 8 MB por processo,
mas pode ser alterado usando ulimit:

Mostrando o padrão em kB:

$ ulimit -s
8192

Defina como ilimitado:

ulimit -s unlimited

afetando o shell e subshells atuais e seus processos filhos.
( ulimité um comando interno do shell)

Você pode mostrar o intervalo de endereços da pilha real em uso com:
cat /proc/$PID/maps | grep -F '[stack]'
no Linux.

Volker Siegel
fonte
Portanto, quando um programa é carregado pelo shell atual, o sistema operacional torna um segmento de memória de ulimit -sKB válido para o programa. No meu caso, é 10240KB. Mas quando eu declaro uma matriz local char a[10240*1024]e o conjunto a[0]=1, o programa sai corretamente. Por quê?
Amos
Tente definir o último elemento também. E verifique se eles não estão otimizados.
vinc17
@amos Acho que o que significa vinc17 é que você nomeou uma região de memória que não caberia na pilha do seu programa , mas como na verdade você não a acessa na parte que não cabe , a máquina nunca nota isso - ela não até obter essa informação .
Volker Siegel
@amos Tente int n = 10240*1024; char a[n]; memset(a,'x',n);... falha de seg.
20914 goldilocks
2
@amos Então, como você pode ver, a[]não foi alocado na sua pilha de 10 MB. O compilador pode ter visto que não poderia haver uma chamada recursiva e fez alocação especial, ou algo mais como uma pilha descontínua ou algum indireto.
vinc17