Alocação mínima de pilha

Alocação mínima de pilha

Estou estudando a especificação ELF (http://www.skyfree.org/linux/references/ELF_Format.pdf), e um ponto que não está claro para mim sobre o processo de carregamento do programa é como a pilha é inicializada e qual é o tamanho inicial da página. Aqui está o teste (no Ubuntu x86-64):

$ cat test.s
.text
  .global _start
_start:
  mov $0x3c,%eax
  mov $0,%edi
  syscall
$ as test.s -o test.o && ld test.o
$ gdb a.out -q
Reading symbols from a.out...(no debugging symbols found)...done.
(gdb) b _start
Breakpoint 1 at 0x400078
(gdb) run
Starting program: ~/a.out 

Breakpoint 1, 0x0000000000400078 in _start ()
(gdb) print $sp
$1 = (void *) 0x7fffffffdf00
(gdb) info proc map
process 20062
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
            0x400000           0x401000     0x1000        0x0 ~/a.out
      0x7ffff7ffa000     0x7ffff7ffd000     0x3000        0x0 [vvar]
      0x7ffff7ffd000     0x7ffff7fff000     0x2000        0x0 [vdso]
      0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

A especificação ELF tem muito pouco a dizer sobre como ou por que esta página de pilha existe, mas posso encontrar referências que dizem que a pilha deve ser inicializada com SP apontando para argc, com argv, envp e o vetor auxiliar logo acima isso, e eu confirmei isso. Mas quanto espaço está disponível abaixo de SP? No meu sistema, há 0x1FF00bytes mapeados abaixo de SP, mas provavelmente isso está em contagem regressiva a partir do topo da pilha 0x7ffffffff000e há 0x21000bytes no mapeamento completo. O que influencia esse número?

Estou ciente de que a página logo abaixo da pilha é uma "página de proteção" que se torna automaticamente gravável e "cresce na pilha" se eu escrever nela (presumivelmente para que o manuseio ingênuo da pilha "simplesmente funcione"), mas se eu alocar um enorme quadro de pilha, então eu poderia ultrapassar a página de proteção e o segfault, então quero determinar quanto espaço já está alocado adequadamente para mim logo no início do processo.

EDITAR: Mais alguns dados me deixam ainda mais inseguro sobre o que está acontecendo. O teste é o seguinte:

.text
  .global _start
_start:
  subq $0x7fe000,%rsp
  movq $1,(%rsp)
  mov $0x3c,%eax
  mov $0,%edi
  syscall

Brinquei com diferentes valores da constante 0x7fe000aqui para ver o que acontece, e para esse valor não é determinístico se recebo uma falha de segurança ou não. De acordo com o GDB, a subqinstrução por si só irá expandir o tamanho do mmap, o que é misterioso para mim (como o Linux sabe o que está no meu registro?), mas este programa geralmente irá travar o GDB ao sair por algum motivo. Não pode ser o ASLR causando o não determinismo porque não estou usando uma seção GOT ou PLT; o executável é sempre carregado nos mesmos locais da memória virtual. Então isso é alguma aleatoriedade do PID ou da memória física? Resumindo, estou muito confuso sobre quanta pilha está realmente disponível legalmente para acesso aleatório e quanto é solicitado ao alterar o RSP ou ao gravar em áreas "fora do alcance" da memória legal.

Responder1

Não acredito que esta questão tenha realmente a ver com ELF. Pelo que eu sei, a ELF define uma forma de "pacote plano"uma imagem de programa em arquivos e, em seguida, remonte-a, pronta para a primeira execução. A definição do que é a pilha e como ela é implementada fica em algum lugar entre específico da CPU e específico do sistema operacional, se o comportamento do sistema operacional não tiver sido elevado para POSIX. Embora não haja dúvida de que a especificação ELF faz algumas exigências sobre o que é necessário na pilha.

Alocação mínima de pilha

Da sua pergunta:

Estou ciente de que a página logo abaixo da pilha é uma "página de proteção" que se torna automaticamente gravável e "cresce na pilha" se eu escrever nela (presumivelmente para que o manuseio ingênuo da pilha "simplesmente funcione"), mas se eu alocar um enorme quadro de pilha, então eu poderia ultrapassar a página de proteção e o segfault, então quero determinar quanto espaço já está alocado adequadamente para mim logo no início do processo.

Estou lutando para encontrar uma referência confiável para isso. Mas encontrei um número grande o suficiente de referências não oficiais para sugerir que isso está incorreto.

Pelo que li, a página de proteção é usada para capturar acesso fora da alocação máxima de pilha, e não para crescimento "normal" da pilha. A alocação real de memória (mapeamento de páginas para endereços de memória) é feita sob demanda. Ou seja: quando são acessados ​​endereços não mapeados na memória que estão entre stack-base e stack-base - max-stack-size + 1, uma exceção pode ser acionada pela CPU, mas o Kernel tratará a exceção mapeando uma página de memória, não em cascata uma falha de segmentação.

Portanto, acessar a pilha dentro da alocação máxima não deve causar falha de segmentação. Como você descobriu

Alocação máxima de pilha

A investigação da documentação deve seguir as linhas da documentação do Linux sobre criação de threads e carregamento de imagens (garfo (2),clonar(2),executivo(2)). A documentação do execvemenciona algo interessante:

Limites no tamanho dos argumentos e ambiente

...recorte...

No kernel 2.6.23 e posterior, a maioria das arquiteturas suporta um limite de tamanho derivado do softRLIMIT_STACKlimite de recursos (vergetrlimit(2))

...recorte...

Isso confirma que o limite exige que a arquitetura o suporte e também faz referência onde é limitado (getrlimit(2)).

RLIMIT_STACK

Este é o tamanho máximo da pilha de processos, em bytes. Ao atingir este limite, é gerado um sinal SIGSEGV. Para lidar com este sinal, um processo deve empregar uma pilha de sinais alternativa (sigaltstack(2)).

Desde o Linux 2.6.23, esse limite também determina a quantidade de espaço usado para os argumentos de linha de comando e variáveis ​​de ambiente do processo; para obter detalhes, consulte execve(2).

Aumentando a pilha alterando o registro RSP

Não conheço montador x86. Mas vou chamar sua atenção para a "Exceção de falha de pilha" que pode ser acionada por CPUs x86 quando o registro SS é alterado. Por favor me corrija se eu estiver errado, mas acredito que em x86-64 SS:SP acabou de se tornar "RSP". Então, se bem entendi, uma exceção de falha de pilha pode ser acionada por RSP decrementado ( subq $0x7fe000,%rsp).

Veja a página 222 aqui:https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol3/o_fe12b1e2a880e0ce.html

Responder2

Cada região de memória de processo (por exemplo, código, dados estáticos, heap, pilha, etc.) possui limites, e um acesso à memória fora de qualquer região ou um acesso de gravação a uma região somente leitura gera uma exceção de CPU. O kernel mantém essas regiões de memória. Um acesso fora de uma região se propaga até o espaço do usuário na forma de um sinal de falha de segmentação.

Nem todas as exceções são geradas pelo acesso à memória fora das regiões. Um acesso na região também pode gerar uma exceção. Por exemplo, se a página não estiver mapeada para a memória física, o manipulador de falhas de página tratará isso de forma transparente para o processo em execução.

A região da pilha principal do processo inicialmente possui apenas um pequeno número de quadros de página mapeados para ela, mas cresce automaticamente quando mais dados são enviados para ela por meio do ponteiro da pilha. O manipulador de exceções verifica se o acesso ainda está dentro da região reservada para a pilha e aloca um novo quadro de página, se estiver. Isso acontece automaticamente do ponto de vista do código de nível de usuário.

Uma página de proteção é colocada logo após o final da região da pilha, para detectar uma saturação da região da pilha. Recentemente (em 2017) algumas pessoas perceberam que uma única página de proteção não é suficiente, porque um programa pode ser potencialmente enganado para diminuir o ponteiro da pilha em uma grande quantidade, o que pode fazer o ponteiro da pilha apontar para alguma outra região que permita gravações. A "solução" para esse problema foi substituir a página de proteção de 4 KB por uma região de proteção de 1 MB. Veja issoArtigo LWN.

Deve-se notar que esta vulnerabilidade não é totalmente trivial de explorar, requer, por exemplo, que o usuário possa controlar a quantidade de memória que um programa aloca através de uma chamada para alloca. Programas robustos devem verificar o parâmetro passado para alloca, especialmente se for derivado da entrada do usuário.

informação relacionada