Come funziona l'allocazione dello stack in Linux?


18

Il sistema operativo riserva la quantità fissa di spazio virtuale valido per lo stack o qualcos'altro? Sono in grado di produrre un overflow dello stack usando solo grandi variabili locali?

Ho scritto un piccolo Cprogramma per testare il mio presupposto. Funziona su 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;
}

L'esecuzione del programma dà &a[0] = f0ceabe0e&a[n-1] = f16eabdf

Le mappe proc mostrano lo stack: 7ffff0cea000-7ffff16ec000. (10248 * 1024B)

Poi ho provato ad aumentare n = 11240 * 1024

L'esecuzione del programma dà &a[0] = b6b36690e&a[n-1] = b763068f

Le mappe proc mostrano lo stack: 7fffb6b35000-7fffb7633000. (11256 * 1024B)

ulimit -s stampe 10240 sul mio PC.

Come puoi vedere, in entrambi i casi la dimensione dello stack è maggiore di quella che ulimit -sdà. E lo stack cresce con una variabile locale più grande. La parte superiore dello stack è in qualche modo 3-5kB in più&a[0] (AFAIK la zona rossa è 128B).

Quindi, come viene allocata questa mappa dello stack?

Risposte:


14

Sembra che il limite di memoria dello stack non sia allocato (comunque, non è possibile con stack illimitato). https://www.kernel.org/doc/Documentation/vm/overcommit-accounting dice:

La crescita dello stack del linguaggio C ha una mappa implicita. Se desideri garanzie assolute e corri vicino al bordo, DEVI utilizzare lo stack per la dimensione più grande che ritieni necessaria. Per l'utilizzo tipico dello stack questo non ha molta importanza, ma è davvero un caso se davvero ti interessa

Tuttavia, il mmapping dello stack sarebbe l'obiettivo di un compilatore (se ha un'opzione per quello).

EDIT: Dopo alcuni test su una macchina Debian x84_64, ho scoperto che lo stack cresce senza alcuna chiamata di sistema (secondo strace). Quindi, questo significa che il kernel lo cresce automaticamente (questo è ciò che "implicito" significa sopra), cioè senza esplicito mmap/mremap dal processo.

È stato abbastanza difficile trovare informazioni dettagliate a conferma di ciò. Consiglio di comprendere Linux Virtual Memory Manager di Mel Gorman. Suppongo che la risposta sia nella Sezione 4.6.1 Gestione di un errore di pagina , con l'eccezione "Regione non valida ma accanto a una regione espandibile come lo stack" e l'azione corrispondente "Espandi la regione e alloca una pagina". Vedi anche D.5.2 Espansione dello stack .

Altri riferimenti sulla gestione della memoria di Linux (ma con quasi nulla sullo stack):

EDIT 2: Questa implementazione ha uno svantaggio: in casi d'angolo, una collisione stack-heap potrebbe non essere rilevata, anche nel caso in cui lo stack sarebbe più grande del limite! Il motivo è che una scrittura in una variabile nello stack può finire nella memoria heap allocata, nel qual caso non c'è nessun errore di pagina e il kernel non può sapere che lo stack deve essere esteso. Vedi il mio esempio nella discussione Silent stack-heap collision in GNU / Linux Ho iniziato nella lista di aiuto gcc. Per evitarlo, il compilatore deve aggiungere del codice alla chiamata di funzione; questo può essere fatto con -fstack-checkGCC (per i dettagli vedere la risposta di Ian Lance Taylor e la pagina man di GCC).


Sembra la risposta corretta alla mia domanda. Ma mi confonde di più. Quando verrà attivata la chiamata mremap? Sarà una syscall integrata nel programma?
Amos,

@amos Presumo che la chiamata mremap verrà attivata se necessario in una chiamata di funzione o quando viene chiamato alloca ().
vinc17,

Probabilmente sarebbe una buona idea menzionare cos'è mmap, per le persone che non lo sanno.
Faheem Mitha,

@FaheemMitha Ho aggiunto alcune informazioni. Per coloro che non sanno cos'è mmap, consultare le FAQ sulla memoria menzionate sopra. Qui, per lo stack, sarebbe stata una "mappatura anonima" in modo che lo spazio inutilizzato non occupasse memoria fisica, ma come spiegato da Mel Gorman, il kernel esegue contemporaneamente la mappatura (memoria virtuale) e l'allocazione fisica .
vinc17,

1
@max Ho provato il programma dell'OP ulimit -sdando 10240, come nelle condizioni dell'OP, e ottengo un SIGSEGV come previsto (questo è ciò che è richiesto da POSIX: "Se questo limite viene superato, SIGSEGV deve essere generato per il thread. "). Sospetto un bug nel kernel dell'OP.
vinc17,

6

Kernel Linux 4.2

Programma di test minimo

Possiamo quindi provarlo con un programma NASM a 64 bit minimo:

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

Assicurati di disattivare ASLR e rimuovere le variabili di ambiente poiché andranno nello stack e occuperanno spazio:

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

Il limite è leggermente inferiore al mio ulimit -s(8 MiB per me). Sembra che ciò sia dovuto ai dati aggiuntivi specificati dal System V inizialmente messi nello stack oltre all'ambiente: parametri della riga di comando di Linux 64 in Assembly | Stack Overflow

Se sei serio su questo, TODO crea un'immagine minima di initrd che inizia a scrivere dall'alto dello stack e scende, quindi eseguirla con QEMU + GDB . Inserisci a dprintfsul ciclo stampando l'indirizzo dello stack e un punto di interruzione su acct_stack_growth. Sarà glorioso.

Relazionato:


2

Per impostazione predefinita, la dimensione massima dello stack è configurata su 8 MB per processo,
ma può essere modificata utilizzandoulimit :

Mostra il valore predefinito in kB:

$ ulimit -s
8192

Impostato su illimitato:

ulimit -s unlimited

influenzando l'attuale shell e subshells e i loro processi figlio.
(ulimit è un comando incorporato della shell)

Puoi mostrare l'attuale intervallo di indirizzi dello stack in uso con:
cat /proc/$PID/maps | grep -F '[stack]'
su Linux.


Pertanto, quando un programma viene caricato dalla shell corrente, il sistema operativo renderà ulimit -svalido un segmento di memoria di KB per il programma. Nel mio caso sono 10240 KB. Ma quando dichiaro un array char a[10240*1024]e un set locale a[0]=1, il programma si chiude correttamente. Perché?
Amos,

Prova a impostare anche l'ultimo elemento. E assicurati che non siano ottimizzati.
vinc17,

@amos Penso che ciò che significhi vinc17 sia che hai chiamato una regione di memoria che non si adatterebbe allo stack nel tuo programma , ma dato che in realtà non accedi alla parte che non si adatta , la macchina non se ne accorge mai - non lo fa persino ottenere quelle informazioni .
Volker Siegel,

@amos Prova int n = 10240*1024; char a[n]; memset(a,'x',n);... seg colpa.
Riccioli d'oro,

2
@amos Quindi, come puoi vedere, a[]non è stato allocato nel tuo stack da 10 MB. Il compilatore potrebbe aver visto che non potrebbe esserci una chiamata ricorsiva e ha effettuato un'allocazione speciale o qualcos'altro come uno stack discontinuo o qualche indiretta.
vinc17,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.