Solución:
En el diagrama que publicaste, la “interrupción”, la dirección manipulada por brk
y sbrk
—Es la línea punteada en la parte superior del montón.
La documentación que ha leído describe esto como el final del “segmento de datos” porque en las tradicionales (bibliotecas precompartidas, premmap
) Unix, el segmento de datos era continuo con el montón; antes del inicio del programa, el kernel cargaba los bloques de “texto” y “datos” en la RAM comenzando en la dirección cero (en realidad un poco por encima de la dirección cero, de modo que el puntero NULL realmente no apuntaba a nada) y establecía la dirección de interrupción en el final del segmento de datos. La primera llamada a malloc
entonces usaría sbrk
para mover la ruptura y crear el montón entre la parte superior del segmento de datos y la nueva dirección de ruptura más alta, como se muestra en el diagrama, y el uso posterior de malloc
lo usaría para hacer el montón más grande según sea necesario.
Mientras tanto, la pila comienza en la parte superior de la memoria y crece hacia abajo. La pila no necesita llamadas explícitas al sistema para hacerla más grande; o comienza con tanta RAM asignada como pueda tener (este era el enfoque tradicional) o hay una región de direcciones reservadas debajo de la pila, a la que el kernel asigna RAM automáticamente cuando nota un intento de escribir allí (este es el enfoque moderno). De cualquier manera, puede haber o no una región de “protección” en la parte inferior del espacio de direcciones que se puede utilizar para la pila. Si esta región existe (todos los sistemas modernos hacen esto) está permanentemente sin mapear; si cualquiera la pila o el montón intenta crecer en él, se produce un error de segmentación. Tradicionalmente, sin embargo, el núcleo no hizo ningún intento por imponer un límite; la pila podría crecer en la pila, o la pila podría crecer en la pila, y de cualquier manera, garabatearían los datos de los demás y el programa fallaría. Si tuvieras mucha suerte, se estrellaría de inmediato.
No estoy seguro de dónde proviene el número de 512 GB en este diagrama. Implica un espacio de direcciones virtuales de 64 bits, que es inconsistente con el mapa de memoria muy simple que tiene allí. Un espacio de direcciones real de 64 bits se parece más a esto:
Legend: t: text, d: data, b: BSS
Esto no se puede escalar de manera remota, y no debe interpretarse exactamente como un sistema operativo determinado hace las cosas (después de dibujarlo, descubrí que Linux en realidad coloca el ejecutable mucho más cerca de la dirección cero de lo que pensaba, y las bibliotecas compartidas en direcciones sorprendentemente altas). Las regiones negras de este diagrama no están mapeadas (cualquier acceso provoca una falla de segmentación inmediata) y son gigantesco en relación con las áreas grises. Las regiones de color gris claro son el programa y sus bibliotecas compartidas (puede haber docenas de bibliotecas compartidas); cada uno tiene un independiente segmento de texto y datos (y segmento “bss”, que también contiene datos globales pero se inicializa a todo-bits-cero en lugar de ocupar espacio en el ejecutable o biblioteca en el disco). El montón ya no es necesariamente continuo con el segmento de datos del ejecutable; lo dibujé de esa manera, pero parece que Linux, al menos, no hace eso. La pila ya no está vinculada a la parte superior del espacio de direcciones virtuales, y la distancia entre el montón y la pila es tan enorme que no tiene que preocuparse por cruzarla.
La ruptura sigue siendo el límite superior del montón. Sin embargo, lo que no mostré es que podría haber docenas de asignaciones independientes de memoria en algún lugar negro, hechas con mmap
en lugar de brk
. (El sistema operativo intentará mantenerlos lejos del brk
área para que no choquen).
Ejemplo mínimo ejecutable
¿Qué hace la llamada al sistema brk ()?
Pide al kernel que le permita leer y escribir en una porción de memoria contigua llamada heap.
Si no lo pregunta, es posible que lo separe.
Sin brk
:
#define _GNU_SOURCE
#include <unistd.h>
int main(void) {
/* Get the first address beyond the end of the heap. */
void *b = sbrk(0);
int *p = (int *)b;
/* May segfault because it is outside of the heap. */
*p = 1;
return 0;
}
Con brk
:
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
int main(void) {
void *b = sbrk(0);
int *p = (int *)b;
/* Move it 2 ints forward */
brk(p + 2);
/* Use the ints. */
*p = 1;
*(p + 1) = 2;
assert(*p == 1);
assert(*(p + 1) == 2);
/* Deallocate back. */
brk(b);
return 0;
}
GitHub en sentido ascendente.
Es posible que lo anterior no llegue a una nueva página y no se produzca un error de segmentación incluso sin el brk
, por lo que aquí hay una versión más agresiva que asigna 16MiB y es muy probable que se produzca un error de segmentación sin el brk
:
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
int main(void) {
void *b;
char *p, *end;
b = sbrk(0);
p = (char *)b;
end = p + 0x1000000;
brk(end);
while (p < end) {
*(p++) = 1;
}
brk(b);
return 0;
}
Probado en Ubuntu 18.04.
Visualización del espacio de direcciones virtuales
Antes brk
:
+------+ <-- Heap Start == Heap End
Después brk(p + 2)
:
+------+ <-- Heap Start + 2 * sizof(int) == Heap End
| |
| You can now write your ints
| in this memory area.
| |
+------+ <-- Heap Start
Después brk(b)
:
+------+ <-- Heap Start == Heap End
Para comprender mejor los espacios de direcciones, debe familiarizarse con la paginación: ¿Cómo funciona la paginación x86?
¿Por qué necesitamos ambos? brk
y sbrk
?
brk
Por supuesto, podría implementarse con sbrk
+ cálculos de compensación, ambos existen solo por conveniencia.
En el backend, el kernel de Linux v5.0 tiene una única llamada al sistema brk
que se usa para implementar ambos: https://github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64.tbl#L23
12 common brk __x64_sys_brk
Es brk
POSIX?
brk
solía ser POSIX, pero se eliminó en POSIX 2001, de ahí la necesidad de _GNU_SOURCE
para acceder al contenedor glibc.
Es probable que la eliminación se deba a la introducción mmap
, que es un superconjunto que permite asignar varios rangos y más opciones de asignación.
Creo que no hay un caso válido en el que deba usar brk
en lugar de malloc
o mmap
hoy en día.
brk
vs malloc
brk
es una vieja posibilidad de implementar malloc
.
mmap
es el mecanismo más nuevo estrictamente más poderoso que probablemente todos los sistemas POSIX utilizan actualmente para implementar malloc
. Aquí hay un mínimo ejecutable mmap
ejemplo de asignación de memoria.
Puedo mezclar brk
y malloc?
Si tu malloc
se implementa con brk
, No tengo idea de cómo eso no puede hacer estallar las cosas, ya que brk
solo gestiona un único rango de memoria.
Sin embargo, no pude encontrar nada al respecto en los documentos de glibc, por ejemplo:
- https://www.gnu.org/software/libc/manual/html_mono/libc.html#Resizing-the-Data-Segment
Es probable que las cosas funcionen allí, supongo, ya que mmap
es probable que se use para malloc
.
Ver también:
- ¿Qué tiene de inseguro / heredado brk / sbrk?
- ¿Por qué llamar a sbrk (0) dos veces da un valor diferente?
Más información
Internamente, el kernel decide si el proceso puede tener tanta memoria y asigna páginas de memoria para ese uso.
Esto explica cómo se compara la pila con el montón: ¿Cuál es la función de las instrucciones push / pop utilizadas en los registros en el ensamblaje x86?
Puedes usar brk
y sbrk
usted mismo para evitar la “sobrecarga de malloc” de la que todo el mundo siempre se queja. Pero no se puede utilizar fácilmente este método junto con malloc
por lo que solo es apropiado cuando no es necesario free
cualquier cosa. Porque no puedes. Además, debe evitar cualquier llamada a la biblioteca que pueda usar malloc
internamente. Es decir. strlen
probablemente sea seguro, pero fopen
probablemente no lo es.
Llama sbrk
como llamarías malloc
. Devuelve un puntero a la ruptura actual e incrementa la ruptura en esa cantidad.
void *myallocate(int n){
return sbrk(n);
}
Si bien no puede liberar asignaciones individuales (porque no hay malloc-overhead, te recuerdo pueden gratis todo el espacio llamando brk
con el valor devuelto por la primera llamada a sbrk
, por lo tanto rebobinando el brk.
void *memorypool;
void initmemorypool(void){
memorypool = sbrk(0);
}
void resetmemorypool(void){
brk(memorypool);
}
Incluso podría apilar estas regiones, descartando la región más reciente rebobinando la ruptura al inicio de la región.
Una cosa más …
sbrk
también es útil en golf de código porque es 2 caracteres más corto que malloc
.