Ir al contenido

La memoria de un proceso en Linux

🎯 Al terminar podrás

  1. Ubicar en el espacio virtual de un proceso el text, data, heap, librerías y stack, y leer /proc/PID/maps.
  2. Explicar con la MMU por qué escribir en una dirección de kernel da SIGSEGV y no corrompe la RAM.
  3. Predecir qué devuelve malloc en casos límite (overflow de size_t, short, 0, más que la RAM) y por qué funciona el overcommit.
  4. Reconocer un buffer overflow que «no falla» y saber qué herramienta lo detecta.

Antes necesitas: Paginación por demanda · Protección con la MMU

  • Detective escribe tu hipótesis del bug antes de abrir el diagnóstico
  • Visual mira el diagrama o la tabla y explica qué cambia en cada paso

Así se suele representar un programa en memoria (lab 10, diap. 12):

direcciones altaskernel—inaccesible en modo usuario (la MMU lo bloquea)stack ↓rw-variables locales y direcciones de retornolibrerías y mmapr-x / rw-libc, ld.so y los malloc grandesheap ↑rw-malloc / free; crece moviendo brkdata / bssrw-globales y statictext (código)r-xinstrucciones: escribir aquí da SIGSEGVbrkfin del heapdirecciones bajas

Cómo leerlo: de abajo hacia arriba suben las direcciones. A la derecha de cada zona van sus permisos (r = leer, w = escribir, x = ejecutar). El stack crece hacia abajo y el heap hacia arriba, hacia el hueco del medio (rayado), que no tiene páginas mapeadas: tocarlo da SIGSEGV. brk marca dónde termina el heap.

Cada segmento es un mapeo (mapping) de páginas con sus permisos. Se ven en /proc/PID/maps o, desde GDB, con info proc mappings. Este es el mapa real de stack.c mientras está detenido en pause() (ejecución de prueba):

5fd878169000-5fd87816a000 r--p /tmp/fin/stack ← cabecera ELF
5fd87816a000-5fd87816b000 r-xp /tmp/fin/stack ← text: lectura + ejecución
5fd87816b000-5fd87816c000 r--p /tmp/fin/stack ← constantes (rodata)
5fd87816d000-5fd87816e000 rw-p /tmp/fin/stack ← data/bss: lectura + escritura
5fd8a932b000-5fd8a934c000 rw-p [heap]
705ecde28000-705ecdfb1000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
705ece17d000-705ece1a8000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffea5870000-7ffea5891000 rw-p [stack]
7ffea58ae000-7ffea58b2000 r--p [vvar]
7ffea58b2000-7ffea58b4000 r-xp [vdso]

Cómo se lee:

  • Las columnas son el rango virtual, los permisos (r, w, x, y p = privado) y el archivo de respaldo. Cada rango es un múltiplo de 4 KB (una página).
  • text no tiene w (escribir en él da SIGSEGV), y data no tiene x: protección por página.
  • Las direcciones son virtuales y cambian en cada ejecución por ASLR (aleatorización del espacio de direcciones).
  • [vdso] es código del kernel mapeado en el proceso para hacer syscalls rápidas (gettimeofday) sin cambiar a modo kernel.

main llama a foo, que llama a bar, que llama a bar_is_now_closed, que se detiene en pause(). Cada función imprime la dirección de su variable local:

In function main; &localvar = 0x7ffdb32528d4
In function foo; &localvar = 0x7ffdb32528a4
In function bar; &localvar = 0x7ffdb3252884
In function bar_is_now_closed; &localvar = 0x7ffdb3252864
  • Las direcciones bajan (…8d4 → …8a4 → …884 → …864): el stack crece hacia direcciones menores, y cada llamada apila un marco (frame) nuevo.
  • La diferencia entre marcos (0x30, 0x20, 0x20 bytes) es la variable local más la dirección de retorno, el puntero de marco guardado y la alineación.
direcciones altas ↑main#40x7ffdb32528d4foo#30x7ffdb32528a4bar#20x7ffdb3252884bar_is_now_closed#10x7ffdb3252864pause() · #0−0x30 (48 B)−0x20 (32 B)−0x20 (32 B)el stackcrece hacia abajo&localvarDirecciones de la ejecución de prueba. #k es el número de marco que muestra bt en GDB.

Cómo leerlo: cada caja es el marco de una función, con la dirección real de su localvar a la izquierda. main está arriba porque se llamó primero; cada llamada apila un marco debajo, en direcciones menores. A la derecha va cuánto bajó cada marco respecto del anterior.

El laboratorio pide conectarse con gdb attach PID y ejecutar bt. En sistemas con ptrace_scope = 1 (el valor por defecto de Ubuntu), adjuntarse a un proceso que no es hijo requiere sudo. La alternativa es lanzarlo desde GDB y detenerlo en pause:

$ gdb -q ./stack
(gdb) break pause
(gdb) run
(gdb) bt
#0 __libc_pause () at ../sysdeps/unix/sysv/linux/pause.c:27
#1 0x0000555555555224 in bar_is_now_closed () at stack.c:17
#2 0x0000555555555287 in bar () at stack.c:24
#3 0x00005555555552ea in foo () at stack.c:31
#4 0x0000555555555354 in main (argc=1, argv=0x7fffffffe108) at stack.c:39
(gdb) frame 2
(gdb) p localvar
$1 = 2

bt (backtrace) recorre los marcos del stack de arriba hacia abajo. Con frame k te paras en uno y puedes ver sus variables locales. Dentro de GDB las direcciones son «bonitas» (0x5555…, 0x7fff…) porque GDB desactiva ASLR.

void *memcpy(void *dest, const void *src, size_t n); // copia n bytes; las zonas no deben solaparse

La idea de la diapositiva 9: «App1 empieza en el offset físico 0x0; copiar de 300 KB a 400 KB». Pero un programa no trabaja con direcciones físicas: en main.c, src = 0x300 * 1024 es solo un número, no un puntero válido del proceso.

void *malloc(size_t size); // size_t = unsigned long en x86-64: 8 bytes

malloc_limits.c prueba casos extremos. Ejecución de prueba (x86-64):

sizeof size_t = 8; max value of the param to malloc = 18446744073709551616
ld_num2alloc = -288225969623711744
szt_num2alloc = 18158518104085839872
1. long int used: malloc(-288225969623711744) returns (nil)
2. size_t used: malloc(18158518104085839872) returns (nil)
3. short int used: malloc(6144) returns 0x625e5b4d22b0
4. short int used: malloc(-4096) returns (nil)
5. size_t used: malloc(18446744073709547520) returns (nil)
6. malloc(18446744073709551615) returns (nil)
7. malloc(0) returns 0x625e5b4d22b0

Lo que hay que entender de cada línea:

  • 1 y 2: 28 000 000 × 1 PB desborda el entero: como long, da un número negativo; como size_t, uno gigantesco. malloc recibe siempre un size_t, así que el negativo se convierte en un número enorme. Ninguno se puede satisfacer: NULL.
  • 3 y 4: short tiene 16 bits con signo, máximo 32 767. 6·1024 = 6144 cabe. 60·1024 = 61 440 no cabe y se convierte en −4096; pasado a size_t es casi 2642^{64}: NULL.
60 · 1024 = 61 440guardado en un short1111 0000 0000 0000bit de signo = 1como short: −4096pasa a size_t (64 bits): se extiende el signo0xFFFF FFFF FFFF F000= 18 446 744 073 709 547 520≈ 2⁶⁴ → malloc devuelve NULLEs el caso 4 de malloc_limits.c; ese mismo número aparece en el caso 5.

Cómo leerlo: los mismos 16 bits significan 61 440 sin signo o −4096 con signo, según cómo se lean. Al pasar a size_t, el −4096 se extiende con unos a la izquierda y queda un número enorme, imposible de reservar.

  • 5 y 6: -1UL es el size_t máximo. Imposible: NULL.
  • 7: malloc(0) devuelve un puntero válido (no NULL) que no se debe usar, pero sí se puede pasar a free. Fíjate que es la misma dirección que el caso 3: después del free, el bloque se reutilizó.
  • Siempre hay que revisar si malloc devolvió NULL.
void *realloc(void *ptr, size_t new_size);

Agranda o achica un bloque. Puede moverlo a otra dirección (copiando el contenido) si no hay espacio contiguo. Por eso:

p = realloc(p, n); // ✘ si falla devuelve NULL, se pierde p y hay una fuga
q = realloc(p, n); // ✔
if (q) p = q; else { /* p sigue siendo válido */ }

¿De dónde saca malloc la memoria? (diap. 23)

Sección titulada «¿De dónde saca malloc la memoria? (diap. 23)»

«El consenso general es que malloc obtiene su memoria del heap. No siempre es así.»

glibc usa dos mecanismos:

  • Bloques chicos: el heap, que se agranda moviendo el «break» con brk/sbrk.
  • Bloques ≥ 128 KB (M_MMAP_THRESHOLD por defecto): un mapeo anónimo propio con mmap, que free devuelve al sistema con munmap.

Ejecución de prueba (mallocmap.c):

fin del heap antes : 0x5d8f3f3e1000
malloc(1000) : 0x5d8f3f3e12a0 ← heap
malloc(100 KB) : 0x5d8f3f3e1690 ← heap
malloc(200 KB) : 0x7ca57ffcd010 ← zona de mmap (cerca de las librerías)
malloc(64 MB) : 0x7ca57bfcc010 ← zona de mmap
fin del heap después : 0x5d8f3f402000

Los dos bloques chicos quedan dentro del [heap]. Los de 200 KB y 64 MB aparecen en la zona alta, junto a libc.so.6, en un mapeo anónimo rw-p nuevo. Lo mismo explica por qué en la medición de page faults el comportamiento cambia desde N ≥ 32 páginas (128 KB).

stacklibc.so.6mmap anónimo 200 KBmmap anónimo 64 MB[heap]100 KB1000 Bdata · textmalloc(200 KB) → 0x7ca57ffcd010malloc(64 MB) → 0x7ca57bfcc010malloc(100 KB) → 0x5d8f3f3e1690malloc(1000) → 0x5d8f3f3e12a0≥ 128 KB → mmap propio (free hace munmap)< 128 KB → dentro del heap (brk)Direcciones de la ejecución de prueba de mallocmap.c. Tamaños de las cajas no a escala.

Cómo leerlo: cada flecha une una llamada con el lugar donde quedó su bloque. Las dos de abajo (direcciones 0x5d8f…) caen dentro del heap; las dos de arriba (0x7ca5…), en la zona de mmap junto a libc. El corte es el umbral de 128 KB.

Autoevaluación

0/6 correctas · 0 respondidas
1. En stack.c, las direcciones de localvar…
2. memcpy1.c copia 1024 bytes a un buffer de 20 con -O0. ¿Qué pasó en la ejecución de prueba?
3. memcpy2.c con un argumento escribe en 0xffff…601000 y…
4. malloc(60*1024) pasando el tamaño en un short…
5. Un malloc de 200 KB en glibc…
6. ¿Por qué un malloc de 2 GB funciona en una máquina con 3 GB aunque haya otros procesos?

📈 ¿Qué tan bien lo dominas?

Elige pensando en lo que hiciste, no en lo que leíste. Para marcar 3 tienes que haber resuelto un ejercicio nuevo sin pistas y haberlo justificado. Si solo lo leíste y sientes que «ya entendí», todavía es un 1.

Fuente ·sem_9/10-OSL-Memory.pdf (26 diap.) · Código del curso sem9/: stack.c, memcpy1.c, memcpy2.c, main.c, malloc.c, malloc_limits.c (ejecuciones de prueba) · Prueba propia: mallocmap.c · man 3 malloc, man 3 memcpy, proc(5)