Ubuntu 15.10, Kernel 4.2.0, x86-64, ejemplo de GCC 5.2.1
Suficientes estándares, veamos una implementación :-)
Variable local
Estándares: comportamiento indefinido.
Implementación: el programa asigna espacio de pila, y nunca mueve nada a esa dirección, por lo que se usa lo que estaba allí anteriormente.
#include <stdio.h>
int main() {
int i;
printf("%d\n", i);
}
compilar con:
gcc -O0 -std=c99 a.c
salidas:
0
y descompila con:
objdump -dr a.out
a:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 48 83 ec 10 sub $0x10,%rsp
40053e: 8b 45 fc mov -0x4(%rbp),%eax
400541: 89 c6 mov %eax,%esi
400543: bf e4 05 40 00 mov $0x4005e4,%edi
400548: b8 00 00 00 00 mov $0x0,%eax
40054d: e8 be fe ff ff callq 400410 <printf@plt>
400552: b8 00 00 00 00 mov $0x0,%eax
400557: c9 leaveq
400558: c3 retq
De nuestro conocimiento de las convenciones de llamadas x86-64:
%rdi
es el primer argumento printf, por lo tanto, la cadena "%d\n"
en la dirección0x4005e4
%rsi
es el segundo argumento printf, por lo tanto i
.
Proviene de -0x4(%rbp)
, que es la primera variable local de 4 bytes.
En este punto, rbp
está en la primera página de la pila que ha sido asignada por el núcleo, por lo que para comprender ese valor, deberíamos analizar el código del núcleo y averiguar en qué se establece.
TODO ¿el núcleo establece esa memoria en algo antes de reutilizarla para otros procesos cuando un proceso muere? De lo contrario, el nuevo proceso podría leer la memoria de otros programas terminados, filtrando datos. Ver: ¿Los valores no inicializados son siempre un riesgo para la seguridad?
Entonces también podemos jugar con nuestras propias modificaciones de pila y escribir cosas divertidas como:
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
Variable local en -O3
Análisis de implementación en: ¿Qué significa <valor optimizado fuera> en gdb?
Variables globales
Estándares: 0
Implementación: .bss
sección.
#include <stdio.h>
int i;
int main() {
printf("%d\n", i);
}
gcc -00 -std=c99 a.c
compila a:
0000000000400536 <main>:
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 8b 05 04 0b 20 00 mov 0x200b04(%rip),%eax # 601044 <i>
400540: 89 c6 mov %eax,%esi
400542: bf e4 05 40 00 mov $0x4005e4,%edi
400547: b8 00 00 00 00 mov $0x0,%eax
40054c: e8 bf fe ff ff callq 400410 <printf@plt>
400551: b8 00 00 00 00 mov $0x0,%eax
400556: 5d pop %rbp
400557: c3 retq
400558: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
40055f: 00
# 601044 <i>
dice que i
está en la dirección 0x601044
y:
readelf -SW a.out
contiene:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
que dice 0x601044
está justo en el medio de la .bss
sección, que comienza en 0x601040
y tiene 8 bytes de longitud.
El estándar ELF garantiza que la sección nombrada .bss
esté completamente llena de ceros:
.bss
Esta sección contiene datos no inicializados que contribuyen a la imagen de memoria del programa. Por definición, el sistema inicializa los datos con ceros cuando el programa comienza a ejecutarse. La sección ocu- pasteles sin espacio de archivos, como lo indica el tipo de sección, SHT_NOBITS
.
Además, el tipo SHT_NOBITS
es eficiente y no ocupa espacio en el archivo ejecutable:
sh_size
Este miembro da el tamaño de la sección en bytes. A menos que el tipo de sección sea SHT_NOBITS
, la sección ocupa sh_size
bytes en el archivo. Una sección de tipo SHT_NOBITS
puede tener un tamaño distinto de cero, pero no ocupa espacio en el archivo.
Luego, corresponde al núcleo de Linux poner a cero esa región de memoria al cargar el programa en la memoria cuando se inicia.