Przykład Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1
Dość standardów, spójrzmy na realizację :-)
Zmienna lokalna
Normy: niezdefiniowane zachowanie.
Implementacja: program przydziela miejsce na stosie i nigdy nie przenosi niczego na ten adres, więc to, co było wcześniej, jest używane.
#include <stdio.h>
int main() {
int i;
printf("%d\n", i);
}
Połącz z:
gcc -O0 -std=c99 a.c
wyjścia:
0
i dekompiluje się z:
objdump -dr a.out
do:
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
Z naszej wiedzy o konwencjach wywoływania x86-64:
%rdi
jest pierwszym argumentem printf, a więc łańcuchem "%d\n"
pod adresem0x4005e4
%rsi
jest więc drugim argumentem printf i
.
Pochodzi z -0x4(%rbp)
, która jest pierwszą 4-bajtową zmienną lokalną.
W tym momencie rbp
to pierwsza strona stosu została przydzielona przez jądro, więc aby zrozumieć tę wartość, powinniśmy zajrzeć do kodu jądra i dowiedzieć się, na co to ustawia.
DO ZROBIENIA, czy jądro ustawia tę pamięć na coś przed ponownym użyciem jej dla innych procesów, gdy proces umiera? Jeśli nie, nowy proces byłby w stanie odczytać pamięć innych ukończonych programów, powodując wyciek danych. Zobacz: Czy niezainicjowane wartości stanowią kiedykolwiek zagrożenie bezpieczeństwa?
Możemy wtedy również bawić się naszymi własnymi modyfikacjami stosu i pisać zabawne rzeczy, takie jak:
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
Zmienna lokalna w -O3
Analiza implementacji pod adresem: Co oznacza <wartość zoptymalizowana na zewnątrz> w gdb?
Zmienne globalne
Normy: 0
Realizacja: .bss
sekcja.
#include <stdio.h>
int i;
int main() {
printf("%d\n", i);
}
gcc -00 -std=c99 a.c
kompiluje się do:
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>
mówi, że i
jest pod adresem 0x601044
i:
readelf -SW a.out
zawiera:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
który mówi, że 0x601044
znajduje się dokładnie w środku .bss
sekcji, która zaczyna się od 0x601040
i ma długość 8 bajtów.
Standard ELF gwarantuje następnie, że wymieniona sekcja .bss
jest całkowicie wypełniona zerami:
.bss
Ta sekcja zawiera niezainicjowane dane, które mają wpływ na obraz pamięci programu. Z definicji system inicjuje dane zerami, gdy program zaczyna działać. Sekcja plasuje ma miejsca pliku, jak wskazano rodzaju Sekcji SHT_NOBITS
.
Ponadto typ SHT_NOBITS
jest wydajny i nie zajmuje miejsca w pliku wykonywalnym:
sh_size
Ten element członkowski podaje rozmiar sekcji w bajtach. O ile typ sekcji to nie jest SHT_NOBITS
, sekcja zajmuje sh_size
bajty w pliku. Sekcja typu SHT_NOBITS
może mieć niezerowy rozmiar, ale nie zajmuje miejsca w pliku.
Następnie do jądra Linuksa należy wyzerowanie tego regionu pamięci podczas ładowania programu do pamięci po uruchomieniu.