¿Cómo funciona la asignación de pila en Linux?


18

¿El sistema operativo reserva la cantidad fija de espacio virtual válido para la pila o algo más? ¿Puedo producir un desbordamiento de pila simplemente usando grandes variables locales?

He escrito un pequeño Cprograma para probar mi suposición. Se ejecuta en 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;
}

Ejecutar el programa da &a[0] = f0ceabe0y&a[n-1] = f16eabdf

Los mapas de proceso muestran la pila: 7ffff0cea000-7ffff16ec000. (10248 * 1024B)

Entonces traté de aumentar n = 11240 * 1024

Ejecutar el programa da &a[0] = b6b36690y&a[n-1] = b763068f

Los mapas de proceso muestran la pila: 7fffb6b35000-7fffb7633000. (11256 * 1024B)

ulimit -simprime 10240en mi PC.

Como puede ver, en ambos casos el tamaño de la pila es mayor de lo que ulimit -sda. Y la pila crece con una variable local más grande. La parte superior de la pila está de alguna manera 3-5kB más apagada &a[0](AFAIK la zona roja es 128B).

Entonces, ¿cómo se asigna este mapa de pila?

Respuestas:


14

Parece que el límite de memoria de la pila no está asignado (de todos modos, no podría con una pila ilimitada). https://www.kernel.org/doc/Documentation/vm/overcommit-accounting dice:

El crecimiento de la pila del lenguaje C hace un mremap implícito. Si desea garantías absolutas y ejecuta cerca del borde, DEBE mapear su pila para el tamaño más grande que cree que necesitará. Para el uso típico de la pila, esto no importa mucho, pero es un caso de esquina si realmente te importa

Sin embargo, mmapping la pila sería el objetivo de un compilador (si tiene una opción para eso).

EDITAR: Después de algunas pruebas en una máquina x84_64 Debian, descubrí que la pila crece sin ninguna llamada al sistema (según strace). Entonces, esto significa que el núcleo crece automáticamente (esto es lo que el "implícito" significa arriba), es decir, sin explícito mmap/ mremapdel proceso.

Fue bastante difícil encontrar información detallada que lo confirmara. Recomiendo Entender The Linux Virtual Memory Manager por Mel Gorman. Supongo que la respuesta se encuentra en la Sección 4.6.1 Manejo de un error de página , con la excepción "Región no válida pero está al lado de una región expandible como la pila" y la acción correspondiente "Expandir la región y asignar una página". Ver también D.5.2 Expandir la pila .

Otras referencias sobre la administración de memoria de Linux (pero casi sin nada sobre la pila):

EDITAR 2: Esta implementación tiene un inconveniente: en casos de esquina, una colisión de montón de pila puede no ser detectada, ¡incluso en el caso de que la pila sea mayor que el límite! La razón es que una escritura en una variable en la pila puede terminar en la memoria de montón asignada, en cuyo caso no hay falla de página y el núcleo no puede saber que la pila necesitaba ser extendida. Vea mi ejemplo en la discusión Colisión silenciosa de pila-montón en GNU / Linux Empecé en la lista de ayuda de gcc. Para evitar eso, el compilador necesita agregar algo de código en la llamada a la función; esto se puede hacer con -fstack-checkGCC (consulte la respuesta de Ian Lance Taylor y la página de manual de GCC para más detalles).


Esa parece la respuesta correcta a mi pregunta. Pero me confunde más. ¿Cuándo se activará la llamada mremap? ¿Será un syscall integrado en el programa?
Amós

@amos Supongo que la llamada mremap se activará si es necesario en una llamada de función o cuando se llama alloca ().
vinc17

Probablemente sea una buena idea mencionar qué es mmap, para las personas que no lo saben.
Faheem Mitha

@FaheemMitha He agregado alguna información. Para aquellos que no saben qué es mmap, consulte las Preguntas frecuentes sobre memoria mencionadas anteriormente. Aquí, para la pila, habría sido un "mapeo anónimo" para que el espacio no utilizado no tomara ninguna memoria física, pero como explicó Mel Gorman, el núcleo hace el mapeo (memoria virtual) y la asignación física al mismo tiempo .
vinc17

1
@max He probado el programa del OP con ulimit -s10240, como en las condiciones del OP, y obtengo un SIGSEGV como se esperaba (esto es lo que requiere POSIX: "Si se excede este límite, se generará SIGSEGV para el hilo. "). Sospecho que hay un error en el núcleo del OP.
vinc17

6

Kernel de Linux 4.2

Programa de prueba mínima

Luego podemos probarlo con un programa NASM mínimo de 64 bits:

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

Asegúrese de desactivar ASLR y eliminar las variables de entorno, ya que irán a la pila y ocuparán espacio:

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

El límite está en algún lugar ligeramente por debajo de mi ulimit -s(8MiB para mí). Parece que esto se debe a los datos adicionales especificados por el Sistema V inicialmente colocados en la pila además del entorno: parámetros de la línea de comandos de Linux 64 en Ensamblado | Desbordamiento de pila

Si se toma esto en serio, TODO crea una imagen inicial mínima que comienza a escribir desde la parte superior de la pila y baja, y luego ejecútela con QEMU + GDB . Coloque un dprintfbucle que imprima la dirección de la pila y un punto de interrupción en acct_stack_growth. Será glorioso

Relacionado:


2

De forma predeterminada, el tamaño máximo de la pila está configurado para ser de 8 MB por proceso,
pero se puede cambiar usando ulimit:

Mostrando el valor predeterminado en kB:

$ ulimit -s
8192

Establecer en ilimitado:

ulimit -s unlimited

afectando el shell actual y los subshells y sus procesos secundarios.
( ulimites un comando integrado de shell)

Puede mostrar el rango real de direcciones de pila en uso con:
cat /proc/$PID/maps | grep -F '[stack]'
en Linux.


Entonces, cuando el shell actual carga un programa, el sistema operativo hará que un segmento de memoria de ulimit -sKB sea válido para el programa. En mi caso son 10240 KB. Pero cuando declaro un conjunto char a[10240*1024]y conjunto local a[0]=1, el programa sale correctamente. ¿Por qué?
Amós

Intenta establecer el último elemento también. Y asegúrese de que no estén optimizados.
vinc17

@amos Creo que lo que significa vinc17 es que usted nombró una región de memoria que no cabría en la pila de su programa , pero como no accede a ella en la parte que no encaja , la máquina nunca se da cuenta de eso, no incluso obtener esa información .
Volker Siegel

@amos Intenta int n = 10240*1024; char a[n]; memset(a,'x',n);... seg culpa.
Ricitos

2
@amos Entonces, como puede ver, a[]no se ha asignado en su pila de 10 MB. El compilador podría haber visto que no podía haber una llamada recursiva y había hecho una asignación especial, o algo más, como una pila discontinua o alguna indirección.
vinc17
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.