Exemplo de Ubuntu 15.10, Kernel 4.2.0, x86-64, GCC 5.2.1
Padrões suficientes, vejamos uma implementação :-)
Variável local
Padrões: comportamento indefinido.
Implementação: o programa aloca espaço na pilha e nunca move nada para esse endereço; portanto, o que quer que estivesse lá anteriormente é usado.
#include <stdio.h>
int main() {
int i;
printf("%d\n", i);
}
ajuntar com:
gcc -O0 -std=c99 a.c
saídas:
0
e descompila com:
objdump -dr a.out
para:
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
Pelo nosso conhecimento das convenções de chamada x86-64:
%rdi
é o primeiro argumento printf, portanto, a string "%d\n"
no endereço0x4005e4
%rsi
é o segundo argumento printf, portanto i
.
É proveniente da -0x4(%rbp)
primeira variável local de 4 bytes.
Neste ponto, rbp
está na primeira página da pilha que foi alocada pelo kernel, para entender esse valor, devemos procurar no código do kernel e descobrir como ele define isso.
TODO o kernel define essa memória para algo antes de reutilizá-la para outros processos quando um processo morre? Caso contrário, o novo processo seria capaz de ler a memória de outros programas concluídos, vazando dados. Consulte: Valores não inicializados são sempre um risco à segurança?
Também podemos jogar com nossas próprias modificações de pilha e escrever coisas divertidas como:
#include <assert.h>
int f() {
int i = 13;
return i;
}
int g() {
int i;
return i;
}
int main() {
f();
assert(g() == 13);
}
Variável local em -O3
Análise de implementação em: O que <valor otimizado> significa em gdb?
Variáveis globais
Padrões: 0
Implementação: .bss
seção.
#include <stdio.h>
int i;
int main() {
printf("%d\n", i);
}
gcc -00 -std=c99 a.c
compila para:
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>
diz que i
está no endereço 0x601044
e:
readelf -SW a.out
contém:
[25] .bss NOBITS 0000000000601040 001040 000008 00 WA 0 0 4
que diz que 0x601044
está bem no meio da .bss
seção, que começa em 0x601040
e tem 8 bytes de comprimento.
O padrão ELF garante que a seção nomeada .bss
seja completamente preenchida com zeros:
.bss
Esta seção contém dados não inicializados que contribuem para a imagem da memória do programa. Por definição, o sistema inicializa os dados com zeros quando o programa começa a ser executado. A seção não ocupa espaço no arquivo, conforme indicado pelo tipo de seção SHT_NOBITS
,.
Além disso, o tipo SHT_NOBITS
é eficiente e não ocupa espaço no arquivo executável:
sh_size
Este membro fornece o tamanho da seção em bytes. A menos que o tipo de seção seja SHT_NOBITS
, a seção ocupa sh_size
bytes no arquivo. Uma seção do tipo SHT_NOBITS
pode ter um tamanho diferente de zero, mas não ocupa espaço no arquivo.
Cabe ao kernel do Linux zerar a região de memória ao carregar o programa na memória quando ele é iniciado.