Esempio di Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1
Abbastanza standard, diamo un'occhiata a un'implementazione :-)
Variabile locale
Standard: comportamento indefinito.
Implementazione: il programma alloca lo spazio dello stack e non sposta mai nulla a quell'indirizzo, quindi viene utilizzato tutto ciò che c'era in precedenza.
#include <stdio.h>
int main() {
int i;
printf("%d\n", i);
}
compilare con:
gcc -O0 -std=c99 a.c
uscite:
0
e si decompila con:
objdump -dr a.out
per:
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
Dalla nostra conoscenza delle convenzioni di chiamata x86-64:
%rdi
è il primo argomento printf, quindi la stringa "%d\n"
all'indirizzo0x4005e4
%rsi
è il secondo argomento printf, quindi i
.
Viene da -0x4(%rbp)
, che è la prima variabile locale a 4 byte.
A questo punto, rbp
è nella prima pagina dello stack che è stato allocato dal kernel, quindi per capire quel valore dovremmo esaminare il codice del kernel e scoprire a cosa lo imposta.
TODO il kernel imposta quella memoria su qualcosa prima di riutilizzarla per altri processi quando un processo muore? In caso contrario, il nuovo processo sarebbe in grado di leggere la memoria di altri programmi finiti, perdendo dati. Vedi: I valori non inizializzati sono mai un rischio per la sicurezza?
Possiamo quindi anche giocare con le nostre modifiche allo stack e scrivere cose divertenti come:
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
Variabile locale in -O3
Analisi di implementazione su: Cosa significa <valore ottimizzato> in gdb?
Variabili globali
Standard: 0
Implementazione: .bss
sezione.
#include <stdio.h>
int i;
int main() {
printf("%d\n", i);
}
gcc -00 -std=c99 a.c
compila per:
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 che i
è all'indirizzo 0x601044
e:
readelf -SW a.out
contiene:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
che dice 0x601044
è proprio nel mezzo della .bss
sezione, che inizia da 0x601040
ed è lungo 8 byte.
Lo standard ELF garantisce quindi che la sezione denominata .bss
sia completamente piena di zeri:
.bss
Questa sezione contiene dati non inizializzati che contribuiscono all'immagine di memoria del programma. Per definizione, il sistema inizializza i dati con zeri all'avvio del programma. La sezione occu- torte senza spazio per i file, come indicato dal tipo di sezione, SHT_NOBITS
.
Inoltre, il tipo SHT_NOBITS
è efficiente e non occupa spazio sul file eseguibile:
sh_size
Questo membro fornisce le dimensioni della sezione in byte. A meno che non sia il tipo di SHT_NOBITS
sezione, la sezione occupa sh_size
byte nel file. Una sezione di tipo SHT_NOBITS
può avere dimensioni diverse da zero, ma non occupa spazio nel file.
Quindi spetta al kernel Linux azzerare quella regione di memoria quando si carica il programma in memoria all'avvio.