conteúdo da aula 4
This commit is contained in:
parent
3f832c6e9c
commit
194e25f9de
1 changed files with 833 additions and 0 deletions
833
curso/aula-04.org
Normal file
833
curso/aula-04.org
Normal file
|
@ -0,0 +1,833 @@
|
|||
#+title: 4 -- Mapeamento de memória
|
||||
#+author: Blau Araujo
|
||||
#+email: cursos@blauaraujo.com
|
||||
|
||||
|
||||
#+options: toc:3
|
||||
|
||||
* Objetivos
|
||||
|
||||
- Compreender como programas são executados no GNU/Linux.
|
||||
- Descobrir como a execução de programas é gerenciada.
|
||||
- Conhecer a organização do espaço de endereçamento de processos.
|
||||
- Visualizar o layout de memória de programas reais.
|
||||
|
||||
* Como programas são executados
|
||||
|
||||
Como vimos, na criação de arquivos objeto no formato ELF, os dados e instruções
|
||||
são organizados em várias seções, o que inclui:
|
||||
|
||||
- O código do programa (seção =.text=)
|
||||
- Dados globais constantes (seção =.rodata=)
|
||||
- Dados globais variáveis inicializados (seção =.data=)
|
||||
- Dados globais variáveis não inicializados (seção =.bss=)
|
||||
|
||||
Também vimos que arquivos objeto podem ser ligados dinâmica ou estaticamente
|
||||
entre si para formar um programa completo:
|
||||
|
||||
- *Ligação estática:* os objetos são vinculados para formar um único binário
|
||||
executável.
|
||||
- *Ligação dinâmica:* um arquivo executável e os objetos compartilhados de que
|
||||
depende são carregados em um mesmo espaço de memória no momento da execução
|
||||
do programa.
|
||||
|
||||
Assim, quando um programa é executado, as seções de seu binário -- bem como
|
||||
as seções de objetos compartilhados, se forem requeridos -- devem ser mapeadas
|
||||
e organizadas em uma região da memória principal designada e gerenciada pelo
|
||||
sistema operacional. Cada programa em execução, portanto, terá seu próprio
|
||||
espaço isolado na memória onde seus dados e instruções serão organizados.
|
||||
|
||||
Contudo, a região da memória atribuída à execução do programa não contém apenas
|
||||
código e dados carregados do executável -- e, eventualmente, de outros objetos
|
||||
compartilhados. Nela, também haverá estruturas adicionais, como a /pilha/, a
|
||||
/heap/ e outros dados de controle para o gerenciamento da execução. O conjunto
|
||||
individual de estruturas na memória associado à execução de um programa é o
|
||||
que nós chamamos de /processo/.
|
||||
|
||||
** O que são processos
|
||||
|
||||
Um processo pode ser entendido como uma entidade ativa no sistema operacional
|
||||
que representa a execução de um programa, incluindo sua identificação, os
|
||||
recursos a que tem acesso, seu contexto de execução, permissões e informações
|
||||
de gerenciamento. É por meio dos processos que o sistema operacional organiza
|
||||
e administra a execução simultânea de programas, garantindo que cada um deles
|
||||
funcione corretamente e de forma isolada dos demais.
|
||||
|
||||
Portanto, ao executar um programa, o sistema operacional:
|
||||
|
||||
- Cria um novo espaço de memória de uso exclusivo;
|
||||
- Carrega as seções do executável e de objetos compartilhados (se necessário);
|
||||
- Inicializa estruturas auxiliares (como /pilha/, /heap/ e dados de ambiente);
|
||||
- Inicia a execução no ponto de entrada do programa;
|
||||
- Monitora e controla suas atividades como um novo processo.
|
||||
|
||||
** Layout da memória virtual de um processo
|
||||
|
||||
Quando um programa é executado, ele não acessa diretamente a memória física
|
||||
(a memória RAM). Em vez disso, é designada para ele uma faixa contínua de
|
||||
endereços simbólicos, conhecida como /espaço de endereços virtuais/ ou,
|
||||
simplesmente, /memória virtual/. Assim, quando o programa acessa um endereço
|
||||
em seu espeço de memória virtual, o sistema, com auxílio do processador,
|
||||
traduz este acesso para o endereço correspondente na memória física.
|
||||
|
||||
O espaço de endereços virtuais é dividido em regiões específicas, organizadas
|
||||
de forma previsível, e o diagrama abaixo mostra uma representação de seu
|
||||
layout típico em sistemas GNU/Linux x86_64, com as seções dispostas do
|
||||
endereço mais alto para o mais baixo.
|
||||
|
||||
#+begin_example
|
||||
Endereços mais altos (ex: 0x7fffffffffff)
|
||||
┌─────────────────────────────┐
|
||||
│ ESPAÇO DO KERNEL │ ← informações de controle interno do kernel.
|
||||
├─────────────────────────────┤
|
||||
│ VETOR DE AMBIENTE │ ← Variáveis exportadas.
|
||||
├─────────────────────────────┤
|
||||
│ VETOR DE ARGUMENTOS │ ← Argumentos de linha de comando.
|
||||
├─────────────────────────────┤
|
||||
│ │
|
||||
│ PILHA (STACK) │
|
||||
│ │
|
||||
├───────────── ▼ ─────────────┤
|
||||
│ │
|
||||
│ │ ← Espaço livre.
|
||||
│ │
|
||||
├─────────────────────────────┤
|
||||
│ VDSO │ ← Assistente de chamadas de sistema.
|
||||
├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
||||
│ VVAR │ ← Acesso a tempo real do kernel.
|
||||
├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
||||
│ │
|
||||
│ MMAP │ ← Objetos compartilhados, arquivos mapeados,
|
||||
│ │ alocação com 'mmap', etc.
|
||||
├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
||||
│ │
|
||||
│ HEAP │ ← Memória alocada com 'malloc'.
|
||||
│ │
|
||||
├─────────────────────────────┤ ─┐
|
||||
│ .BSS │ │
|
||||
├─────────────────────────────┤ │
|
||||
│ .DATA │ │
|
||||
├─────────────────────────────┤ │
|
||||
│ .RODATA │ ├─ Conteúdo mapeado do arquivo executável.
|
||||
├─────────────────────────────┤ │
|
||||
│ .TEXT │ │
|
||||
├─────────────────────────────┤ │
|
||||
│ TABELAS ELF │ │
|
||||
└─────────────────────────────┘ ─┘
|
||||
Endereços mais baixos (ex: 0x400000)
|
||||
#+end_example
|
||||
|
||||
Onde, de cima para baixo, nós temos...
|
||||
|
||||
- Espaço do kernel ::
|
||||
|
||||
Informações internas do kernel para configuração da pilha, do ambiente de
|
||||
execução (argumentos e ambiente) e, em alguns casos, "trampolins de
|
||||
sinais". Esta região não pode ser acessada diretamente pelo programa.
|
||||
|
||||
- Vetor de ambiente ::
|
||||
|
||||
Vetor de strings no formato de atribuições de variáveis (=NOME=VALOR=)
|
||||
correspondendo às definições das variáveis exportadas para o processo.
|
||||
|
||||
- Vetor de argumentos ::
|
||||
|
||||
Também chamado de /vetor de parâmetros/, é um vetor de strings que contém,
|
||||
em ordem, as palavras da linha de comando usada para executar o programa.
|
||||
O primeiro elemento do vetor sempre existe e corresponde à primeira palavra
|
||||
da linha do comando -- geralmente, o nome ou caminho do programa.
|
||||
|
||||
- Pilha (/stack/) ::
|
||||
|
||||
É uma estrutura dinâmica que /empilha/ blocos de dados conforme as funções
|
||||
do programa são chamadas, armazenando temporariamente informações como
|
||||
endereços de retorno e dados de variáveis locais. A cada nova chamada de
|
||||
função, um novo bloco (chamado de /quadro de pilha/) é empilhado. Quando a
|
||||
função termina, esse quadro é removido e a pilha volta ao seu estado
|
||||
anterior. Ao ser inicializada, a pilha recebe, do endereço mais alto para
|
||||
o mais baixo: os endereços dos elementos no vetor de ambiente, os endereços
|
||||
dos elementos do vetor de argumentos e, no topo da pilha, a quantidade de
|
||||
argumentos...
|
||||
|
||||
#+begin_example
|
||||
Endereço mais alto na pilha (sua base)
|
||||
┌─────────────┐
|
||||
│ NULL │ ← Fim do vetor de ambiente
|
||||
├──── ... ────┤
|
||||
│ envp[1] │
|
||||
├─────────────┤
|
||||
│ envp[0] │ ← Definição da primeira variável exportada
|
||||
├─────────────┤
|
||||
│ NULL │ ← Fim do vetor de argumentos
|
||||
├──── ... ────┤
|
||||
│ argv[1] │
|
||||
├─────────────┤
|
||||
│ argv[0] │ ← Nome/caminho do programa
|
||||
├──── ... ────┤
|
||||
│ argc │ ← Quantidade de argumentos.
|
||||
└─────────────┘
|
||||
Endereço mais baixo da pilha (na direção de seu topo)
|
||||
#+end_example
|
||||
|
||||
- Espaço livre entre pilha e endereços mapeados ::
|
||||
|
||||
Em versões mais modernas do GNU/Linux, a folga de segurança para o
|
||||
crescimento da pilha (/guard gap/) não é compartilhada com a região de dados
|
||||
alocados dinamicamente pelo programa (/heap/, de "amontoado"), como
|
||||
costumava ser. Atualmente, o sistema insere várias regiões mapeadas
|
||||
automaticamente com bibliotecas compartilhadas, inclusive do próprio
|
||||
sistema. Deste modo, a tradicional região do /heap/ deixa de ficar
|
||||
imediatamente abaixo da pilha no espaço de endereços virtuais.
|
||||
|
||||
- Região de mapeamento automático de bibliotecas ::
|
||||
|
||||
Atualmente, logo abaixo da folga de segurança, nós temos várias áreas
|
||||
mapeadas com objetos compartilhados e do sistema, como a biblioteca
|
||||
especial =linux-vdso.so.1= (que possibilita realizar chamadas de sistema
|
||||
sem utilizar interrupções), a biblioteca C padrão (=glibc=) e o carregador
|
||||
dinâmico (=ld-linux=). Essas bibliotecas são mapeadas automaticamente pelo
|
||||
sistema e podem variar de acordo com as dependências do programa e as
|
||||
configurações do ambiente de execução.
|
||||
|
||||
- Regiões mapeadas com 'mmap' ::
|
||||
|
||||
Áreas criadas com a chamada de sistema =mmap=, que possibilita mapear
|
||||
arquivos, carregar bibliotecas explicitamente e alocar de grandes blocos
|
||||
de memória.
|
||||
|
||||
- Região de alocação dinâmica de memória (/heap/) ::
|
||||
|
||||
É um grande espaço de endereços reservado para que o programa possa
|
||||
carregar e manipular dados dinamicamente -- ou seja, dados que não foram
|
||||
associados previamente a variáveis ou vetores, o que geralmente é feito
|
||||
com funções da família =malloc=. Em termos gerais, diz-se que o /heap/ cresce
|
||||
no sentido dos endereços mais altos, a partir de um ponto inicial acima
|
||||
das regiões estáticas do programa. Contudo, novas alocações não seguem
|
||||
necessariamente um padrão linear, podendo ocupar regiões já liberadas ou
|
||||
até mesmo fora do espaço tradicional do /heap/, via =mmap=. De todo modo, o
|
||||
limite de crescimento do /heap/ está nos espaços superiores já ocupados
|
||||
com mapeamentos automáticos feitos pelo sistema.
|
||||
|
||||
- Conteúdo estático do arquivo executável ::
|
||||
|
||||
No início da faixa de endereços da memória virtual, ficam os dados
|
||||
carregados diretamente do arquivo executável, o que inclui as seções
|
||||
do binário e as tabelas de cabeçalhos e segmentos do formato ELF.
|
||||
|
||||
* Explorando os espaços de endereços de processos
|
||||
|
||||
Conhecendo o aspecto geral do layout de organização da memória virtual de
|
||||
processos em execução em sistemas GNU/Linux, é importante enfatizar que o
|
||||
conteúdo nesse espaço virtual de endereços pode variar em função de diversos
|
||||
fatores como, entre outros:
|
||||
|
||||
- O tipo do programa e seu grau de abstração;
|
||||
- A necessidade, ou não, de carregar objetos compartilhados;
|
||||
- A forma como a memória é alocada em tempo de execução;
|
||||
- A versão do sistema operacional.
|
||||
|
||||
Sendo assim, a melhor forma de consolidar a compreensão sobre os espaços de
|
||||
memória é observando diretamente como eles são organizados durante a execução
|
||||
de programas reais, escritos em diferentes linguagens de programação e com
|
||||
diferentes níveis de abstração.
|
||||
|
||||
* Exemplo 1: mapeamento de memória de um programa em C
|
||||
|
||||
Arquivo: [[exemplos/04/mappings.c][mappings.c]]
|
||||
|
||||
#+begin_src c :tangle exemplos/04/mappings.c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int global_var = 42; // .data
|
||||
static int static_var = 7; // .data
|
||||
int uninit_var; // .bss
|
||||
const int const_var = 99; // .rodata
|
||||
|
||||
int main() {
|
||||
int local_var = 10; // stack
|
||||
static int static_main = 5; // .data
|
||||
int *heap_var = malloc(sizeof(int)); // heap
|
||||
|
||||
*heap_var = 123;
|
||||
|
||||
printf("local_var : %p : %d\n", &local_var, local_var);
|
||||
printf("*heap_var : %p : %d\n", heap_var, *heap_var);
|
||||
printf("uninit_var : %p : %d\n", &uninit_var, uninit_var);
|
||||
printf("static_main: %p : %d\n", &static_main, static_main);
|
||||
printf("static_var : %p : %d\n", &static_var, static_var);
|
||||
printf("global_var : %p : %d\n", &global_var, global_var);
|
||||
printf("const_var : %p : %d\n", &const_var, const_var);
|
||||
|
||||
// Código da função main (.text)...
|
||||
printf("main() : %p\n", &main);
|
||||
|
||||
// Pausa para análise na linha de comandos...
|
||||
puts("Tecle algo para terminar...");
|
||||
getchar();
|
||||
|
||||
free(heap_var);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#+end_src
|
||||
|
||||
Compilando e executando, nós termos algo como...
|
||||
|
||||
#+begin_example
|
||||
:~$ gcc -Wall mappings.c
|
||||
:~$ ./a.out
|
||||
local_var : 0x7fff8803eb94 : 10
|
||||
*heap_var : 0x563f84c752a0 : 123
|
||||
uninit_var : 0x563f63a47048 : 0
|
||||
static_main: 0x563f63a47040 : 5
|
||||
static_var : 0x563f63a4703c : 7
|
||||
global_var : 0x563f63a47038 : 42
|
||||
const_var : 0x563f63a45004 : 99
|
||||
main() : 0x563f63a44179
|
||||
Tecle algo para terminar...
|
||||
#+end_example
|
||||
|
||||
Como podemos ver, a impressão foi planejada de modo a apresentar os endereços
|
||||
na ordem em que seriam mapeados na memória virtual, do mais alto para o mais
|
||||
baixo. Isso facilitará a continuação do experimento, que é a localização
|
||||
desses endereços nas faixas de mapeamento obtidas com o utilitário =pmap= e
|
||||
no arquivo =/proc/<pid>/maps=.
|
||||
|
||||
** Análise com 'pmap'
|
||||
|
||||
O utilitário =pmap= exibe o mapa de memória de um ou mais processos:
|
||||
|
||||
#+begin_example
|
||||
pmap [opções] PID [PID ...]
|
||||
#+end_example
|
||||
|
||||
Por padrão, o utilitário exibe o mapeamento de forma simplificada (ideal
|
||||
para os nossos propósitos), mas outros detalhes podem ser exibidos com as
|
||||
opções listadas em:
|
||||
|
||||
#+begin_example
|
||||
pmap --help
|
||||
#+end_example
|
||||
|
||||
Para utilizá-lo nos nossos experimentos, com o programa de exemplo ainda
|
||||
em execução, nós teremos que abrir outro terminal e executar:
|
||||
|
||||
#+begin_example
|
||||
$ pmap $(pidof a.out)
|
||||
#+end_example
|
||||
|
||||
O resultado impresso deve ser algo assim:
|
||||
|
||||
#+begin_example
|
||||
2782565: ./a.out
|
||||
0000563f63a43000 4K r---- a.out
|
||||
0000563f63a44000 4K r-x-- a.out
|
||||
0000563f63a45000 4K r---- a.out
|
||||
0000563f63a46000 4K r---- a.out
|
||||
0000563f63a47000 4K rw--- a.out
|
||||
0000563f84c75000 132K rw--- [ anôn ]
|
||||
00007f6207f5f000 12K rw--- [ anôn ]
|
||||
00007f6207f62000 160K r---- libc.so.6
|
||||
00007f6207f8a000 1428K r-x-- libc.so.6
|
||||
00007f62080ef000 344K r---- libc.so.6
|
||||
00007f6208145000 16K r---- libc.so.6
|
||||
00007f6208149000 8K rw--- libc.so.6
|
||||
00007f620814b000 52K rw--- [ anôn ]
|
||||
00007f620817b000 8K rw--- [ anôn ]
|
||||
00007f620817d000 16K r---- [ anôn ]
|
||||
00007f6208181000 8K r-x-- [ anôn ]
|
||||
00007f6208183000 4K r---- ld-linux-x86-64.so.2
|
||||
00007f6208184000 160K r-x-- ld-linux-x86-64.so.2
|
||||
00007f62081ac000 44K r---- ld-linux-x86-64.so.2
|
||||
00007f62081b7000 8K r---- ld-linux-x86-64.so.2
|
||||
00007f62081b9000 4K rw--- ld-linux-x86-64.so.2
|
||||
00007f62081ba000 4K rw--- [ anôn ]
|
||||
00007fff88020000 132K rw--- [ pilha ]
|
||||
total 2560K
|
||||
#+end_example
|
||||
|
||||
Na primeira linha...
|
||||
|
||||
#+begin_example
|
||||
2782565: ./a.out
|
||||
#+end_example
|
||||
|
||||
Nós temos o número do processo do nosso programa e o comando que invocou
|
||||
sua execução.
|
||||
|
||||
Nas linhas seguintes, as informações seguem este padrão:
|
||||
|
||||
#+begin_example
|
||||
ENDEREÇO_INICIAL TAMANHO(BYTES) PERMISSÕES MAPEAMENTO
|
||||
#+end_example
|
||||
|
||||
Finalmente, na última linha, nós temos o tamanho total da memória mapeada
|
||||
para o processo:
|
||||
|
||||
#+begin_example
|
||||
total 2560K
|
||||
#+end_example
|
||||
|
||||
Nosso principal objetivo é comparar os endereços na saída do nosso programa
|
||||
com os endereços iniciais listados por =pmap= para localizar onde os dados
|
||||
e instruções foram posicionados na memória:
|
||||
|
||||
#+begin_example
|
||||
2782565: ./a.out
|
||||
0000563f63a43000 4K r---- a.out
|
||||
0000563f63a44000 4K r-x-- a.out <-------- main()
|
||||
0000563f63a45000 4K r---- a.out <-------- const_var
|
||||
0000563f63a46000 4K r---- a.out
|
||||
0000563f63a47000 4K rw--- a.out <-------- global_var até uninit_var
|
||||
0000563f84c75000 132K rw--- [ anôn ] <--- *heap_var
|
||||
00007f6207f5f000 12K rw--- [ anôn ]
|
||||
00007f6207f62000 160K r---- libc.so.6
|
||||
00007f6207f8a000 1428K r-x-- libc.so.6
|
||||
00007f62080ef000 344K r---- libc.so.6
|
||||
00007f6208145000 16K r---- libc.so.6
|
||||
00007f6208149000 8K rw--- libc.so.6
|
||||
00007f620814b000 52K rw--- [ anôn ]
|
||||
00007f620817b000 8K rw--- [ anôn ]
|
||||
00007f620817d000 16K r---- [ anôn ]
|
||||
00007f6208181000 8K r-x-- [ anôn ]
|
||||
00007f6208183000 4K r---- ld-linux-x86-64.so.2
|
||||
00007f6208184000 160K r-x-- ld-linux-x86-64.so.2
|
||||
00007f62081ac000 44K r---- ld-linux-x86-64.so.2
|
||||
00007f62081b7000 8K r---- ld-linux-x86-64.so.2
|
||||
00007f62081b9000 4K rw--- ld-linux-x86-64.so.2
|
||||
00007f62081ba000 4K rw--- [ anôn ]
|
||||
00007fff88020000 132K rw--- [ pilha ] <-- local_var
|
||||
total 2560K
|
||||
#+end_example
|
||||
|
||||
Note que a região iniciada em =0x563f63a47000= recebeu os dados de todas as
|
||||
variáveis globais e estáticas inicializadas e não inicializadas:
|
||||
|
||||
#+begin_example
|
||||
uninit_var : 0x563f63a47048 : 0
|
||||
static_main: 0x563f63a47040 : 5
|
||||
static_var : 0x563f63a4703c : 7
|
||||
global_var : 0x563f63a47038 : 42
|
||||
#+end_example
|
||||
|
||||
Isso mostra que, pelo menos neste caso, a seção =.bss= iniciou imediatamente
|
||||
após o fim da seção =.data= na mesma faixa de endereços -- aqui, ocupando um
|
||||
total de 4kbytes:
|
||||
|
||||
#+begin_example
|
||||
0000563f63a47000 4K rw--- a.out <-------- global_var até uninit_var
|
||||
#+end_example
|
||||
|
||||
Nosso segundo objetivo é comparar o mapeamento com o layout típico da
|
||||
memória virtual de um processo. Portanto, nós temos...
|
||||
|
||||
*** Tabelas de cabeçalhos e seções ELF
|
||||
|
||||
A primeira parte do arquivo mapeada na memória é o conteúdo das tabelas
|
||||
de cabeçalhos e seções:
|
||||
|
||||
#+begin_example
|
||||
0000563f63a43000 4K r---- a.out
|
||||
#+end_example
|
||||
|
||||
*** Seção =.text=
|
||||
|
||||
Em seguida, é mapeado o conteúdo da seção =.text= do arquivo:
|
||||
|
||||
#+begin_example
|
||||
0000563f63a44000 4K r-x-- a.out <-- main()
|
||||
#+end_example
|
||||
|
||||
Observe que esta região foi mapeada com permissões apenas para leitura e
|
||||
execução (=r-x--=) e é nela que estão as instruções da função =main= do nosso
|
||||
programa.
|
||||
|
||||
*** Seção =.rodata=
|
||||
|
||||
A seção seguinte a ser mapeada é =.rodata=, onde temos os dados que não
|
||||
poderão ser alterados:
|
||||
|
||||
#+begin_example
|
||||
0000563f63a45000 4K r---- a.out <-- const_var
|
||||
#+end_example
|
||||
|
||||
A região mapeada na memória, portanto, só tem permissão para leitura (=r=).
|
||||
|
||||
*** Seções =.data= e =.bss=
|
||||
|
||||
#+begin_example
|
||||
0000563f63a47000 4K rw--- a.out <-- global_var até uninit_var
|
||||
#+end_example
|
||||
|
||||
Com permissões de leitura e escrita, esta região de endereços recebe todos
|
||||
os dados globais e estáticos que poderão ser alterados ao longo da execução
|
||||
do programa.
|
||||
|
||||
*** Fim do mapeamento do arquivo executável
|
||||
|
||||
Até aqui, todos os dados mapeados correspondem ao conteúdo do arquivo
|
||||
executável do programa (=a.out=). Agora, porém, nós entraremos nas regiões
|
||||
dos endereços mapeados pelo sistema para viabilizar a sua execução como
|
||||
um processo.
|
||||
|
||||
*** Região de alocação dinâmica (heap)
|
||||
|
||||
#+begin_example
|
||||
0000563f84c75000 132K rw--- [ anôn ] <--- *heap_var
|
||||
#+end_example
|
||||
|
||||
*** Região de mapeamento automático e com 'mmap'
|
||||
|
||||
#+begin_example
|
||||
00007f6207f5f000 12K rw--- [ anôn ]
|
||||
00007f6207f62000 160K r---- libc.so.6
|
||||
00007f6207f8a000 1428K r-x-- libc.so.6
|
||||
00007f62080ef000 344K r---- libc.so.6
|
||||
00007f6208145000 16K r---- libc.so.6
|
||||
00007f6208149000 8K rw--- libc.so.6
|
||||
00007f620814b000 52K rw--- [ anôn ]
|
||||
00007f620817b000 8K rw--- [ anôn ]
|
||||
00007f620817d000 16K r---- [ anôn ]
|
||||
00007f6208181000 8K r-x-- [ anôn ]
|
||||
00007f6208183000 4K r---- ld-linux-x86-64.so.2
|
||||
00007f6208184000 160K r-x-- ld-linux-x86-64.so.2
|
||||
00007f62081ac000 44K r---- ld-linux-x86-64.so.2
|
||||
00007f62081b7000 8K r---- ld-linux-x86-64.so.2
|
||||
00007f62081b9000 4K rw--- ld-linux-x86-64.so.2
|
||||
00007f62081ba000 4K rw--- [ anôn ]
|
||||
#+end_example
|
||||
|
||||
Observe que é nesta região que o conteúdo da =glibc= (=libc.so.6=) e do
|
||||
carregador do sistema (=ld-linux-x86-64.so.2=) são mapeados.
|
||||
|
||||
*** Pilha (stack)
|
||||
|
||||
A partir dos endereços mais altos da memória virtual, nós temos a pilha
|
||||
do processo, onde são carregados os dados locais das funções à medida em
|
||||
que elas são chamadas, como é o caso da variável =local_var=.
|
||||
|
||||
#+begin_example
|
||||
00007fff88020000 132K rw--- [ pilha ] <-- local_var
|
||||
#+end_example
|
||||
|
||||
** Análise com o arquivo /proc/<pid>/maps
|
||||
|
||||
Nós também podemos analisar o mapeamento de memória do processo associado
|
||||
à execução do nosso programa pela mesma fonte de dados utilizada pelo =pmap=:
|
||||
o arquivo =/proc/<pid>/maps=. Para isso, com o nosso programa rodando, nós
|
||||
podemos executar:
|
||||
|
||||
#+begin_example
|
||||
:~$ cat /proc/$(pidof a.out)/maps
|
||||
563f63a43000-563f63a44000 r--p 00000000 08:12 4856488 /home/blau/git/pbn/curso/exemplos/04/a.out
|
||||
563f63a44000-563f63a45000 r-xp 00001000 08:12 4856488 /home/blau/git/pbn/curso/exemplos/04/a.out
|
||||
563f63a45000-563f63a46000 r--p 00002000 08:12 4856488 /home/blau/git/pbn/curso/exemplos/04/a.out
|
||||
563f63a46000-563f63a47000 r--p 00002000 08:12 4856488 /home/blau/git/pbn/curso/exemplos/04/a.out
|
||||
563f63a47000-563f63a48000 rw-p 00003000 08:12 4856488 /home/blau/git/pbn/curso/exemplos/04/a.out
|
||||
563f84c75000-563f84c96000 rw-p 00000000 00:00 0 [heap]
|
||||
7f6207f5f000-7f6207f62000 rw-p 00000000 00:00 0
|
||||
7f6207f62000-7f6207f8a000 r--p 00000000 08:12 6584950 /usr/lib/x86_64-linux-gnu/libc.so.6
|
||||
7f6207f8a000-7f62080ef000 r-xp 00028000 08:12 6584950 /usr/lib/x86_64-linux-gnu/libc.so.6
|
||||
7f62080ef000-7f6208145000 r--p 0018d000 08:12 6584950 /usr/lib/x86_64-linux-gnu/libc.so.6
|
||||
7f6208145000-7f6208149000 r--p 001e2000 08:12 6584950 /usr/lib/x86_64-linux-gnu/libc.so.6
|
||||
7f6208149000-7f620814b000 rw-p 001e6000 08:12 6584950 /usr/lib/x86_64-linux-gnu/libc.so.6
|
||||
7f620814b000-7f6208158000 rw-p 00000000 00:00 0
|
||||
7f620817b000-7f620817d000 rw-p 00000000 00:00 0
|
||||
7f620817d000-7f6208181000 r--p 00000000 00:00 0 [vvar]
|
||||
7f6208181000-7f6208183000 r-xp 00000000 00:00 0 [vdso]
|
||||
7f6208183000-7f6208184000 r--p 00000000 08:12 6584935 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
|
||||
7f6208184000-7f62081ac000 r-xp 00001000 08:12 6584935 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
|
||||
7f62081ac000-7f62081b7000 r--p 00029000 08:12 6584935 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
|
||||
7f62081b7000-7f62081b9000 r--p 00034000 08:12 6584935 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
|
||||
7f62081b9000-7f62081ba000 rw-p 00036000 08:12 6584935 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
|
||||
7f62081ba000-7f62081bb000 rw-p 00000000 00:00 0
|
||||
7fff88020000-7fff88041000 rw-p 00000000 00:00 0 [stack]
|
||||
#+end_example
|
||||
|
||||
Neste arquivo, os dados são brutos e menos fáceis de analisar, mas nós
|
||||
podemos observar algumas informações que ficaram de fora da listagem
|
||||
simplificada do utilitário =pmap=. Por exemplo, aqui é possível visualizar
|
||||
onde as funções e variáveis auxiliares do kernel foram mapeadas:
|
||||
|
||||
#+begin_example
|
||||
7f620817d000-7f6208181000 r--p 00000000 00:00 0 [vvar]
|
||||
7f6208181000-7f6208183000 r-xp 00000000 00:00 0 [vdso]
|
||||
#+end_example
|
||||
|
||||
Na listagem do =pmap=, essas mesmas regiões aparecem como "anônimas", mas
|
||||
é possível ver seus tamanhos:
|
||||
|
||||
#+begin_example
|
||||
00007f620817d000 16K r---- [ anôn ]
|
||||
00007f6208181000 8K r-x-- [ anôn ]
|
||||
#+end_example
|
||||
|
||||
Outra região mais facilmente identificável no arquivo é o /heap/:
|
||||
|
||||
#+begin_example
|
||||
563f84c75000-563f84c96000 rw-p 00000000 00:00 0 [heap]
|
||||
#+end_example
|
||||
|
||||
Listada pelo =pmap= apenas como:
|
||||
|
||||
#+begin_example
|
||||
0000563f84c75000 132K rw--- [ anôn ]
|
||||
#+end_example
|
||||
|
||||
* Exemplo 2: mapeamento de memória de um programa em Assembly
|
||||
|
||||
Alocar dados no /heap/ (via chamadas de sistema =brk= ou =sbrk=) seria complexo
|
||||
demais para este momento dos nossos estudos. Imprimir os valores e endereços
|
||||
também exigiria um código muito maior e a antecipação de técnicas e instruções
|
||||
que não compensariam ser vistas agora, Por isso, vamos nos limitar à definição
|
||||
de dados.
|
||||
|
||||
Arquivo: [[exemplos/04/mappings.asm][mappings.asm]]
|
||||
|
||||
#+begin_src asm :tangle exemplos/04/mappings.asm
|
||||
section .rodata
|
||||
msg db `Salve, simpatia!\n`
|
||||
len equ $ - msg
|
||||
|
||||
section .data
|
||||
global_data dq 42
|
||||
|
||||
section .bss
|
||||
uninit_data resq 1
|
||||
|
||||
section .text
|
||||
global _start
|
||||
|
||||
_start:
|
||||
; Fazer algo com os dados em .data e .bss
|
||||
mov rax, [rel global_data] ; acessando .data
|
||||
mov rcx, [rel uninit_data] ; acessando .bss
|
||||
|
||||
; Imprimir a mensagem em .rodata
|
||||
mov rax, 1 ; syscall: write
|
||||
mov rdi, 1 ; fd 1 = stdout
|
||||
mov rsi, msg ; endereço da mensagem
|
||||
mov rdx, len ; tamanho da mensagem
|
||||
syscall
|
||||
|
||||
; Encerrar processo
|
||||
mov rax, 60 ; syscall: exit
|
||||
xor rdi, rdi ; status 0
|
||||
syscall
|
||||
#+end_src
|
||||
|
||||
Montando e executando o exemplo:
|
||||
|
||||
#+begin_example
|
||||
:~$ nasm -f elf64 -g mappings.asm
|
||||
:~$ ld -o mappings mappings.o
|
||||
:~$ ./mappings
|
||||
Salve, simpatia!
|
||||
#+end_example
|
||||
|
||||
** Análise com o GNU Debugger (GDB)
|
||||
|
||||
Note que a opção =-g= foi utilizada para incluir os símbolos de depuração do
|
||||
programa. Isso permite que a análise do mapeamento seja feita com o =gdb=,
|
||||
já que precisamos observar o processo em execução.
|
||||
|
||||
*Inicializando o GDB:*
|
||||
|
||||
#+begin_example
|
||||
:~$ gdb mappings
|
||||
Reading symbols from mappings...
|
||||
#+end_example
|
||||
|
||||
*Definindo um ponto de parada:*
|
||||
|
||||
#+begin_example
|
||||
(gdb) break _start
|
||||
Breakpoint 1 at 0x401000: file mappings.asm, line 16.
|
||||
#+end_example
|
||||
|
||||
*Executando o programa:*
|
||||
|
||||
#+begin_example
|
||||
(gdb) run
|
||||
Starting program: /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
|
||||
Breakpoint 1, _start () at mappings.asm:16
|
||||
16 mov rax, [rel global_data] ; acessando .data
|
||||
#+end_example
|
||||
|
||||
*Listando o mapeamento de memória do processo:*
|
||||
|
||||
#+begin_example
|
||||
(gdb) info proc mappings
|
||||
process 2793082
|
||||
Mapped address spaces:
|
||||
|
||||
Start Addr End Addr Size Offset Perms File
|
||||
0x0000000000400000 0x0000000000401000 0x1000 0x0 r--p /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
0x0000000000401000 0x0000000000402000 0x1000 0x1000 r-xp /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
0x0000000000402000 0x0000000000403000 0x1000 0x2000 r--p /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
0x0000000000403000 0x0000000000404000 0x1000 0x2000 rw-p /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
0x00007ffff7ff9000 0x00007ffff7ffd000 0x4000 0x0 r--p [vvar]
|
||||
0x00007ffff7ffd000 0x00007ffff7fff000 0x2000 0x0 r-xp [vdso]
|
||||
0x00007ffffffde000 0x00007ffffffff000 0x21000 0x0 rw-p [stack]
|
||||
#+end_example
|
||||
|
||||
Observe que a listagem segue o estilo do arquivo =/proc/<pid>/maps=, que,
|
||||
como o programa está em execução, nós também podemos exibir em outro
|
||||
terminal com:
|
||||
|
||||
#+begin_example
|
||||
:~$ cat /proc/$(pidof mappings)/maps
|
||||
00400000-00401000 r--p 00000000 08:12 4856707 /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
00401000-00402000 r-xp 00001000 08:12 4856707 /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
00402000-00403000 r--p 00002000 08:12 4856707 /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
00403000-00404000 rw-p 00002000 08:12 4856707 /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
7ffff7ff9000-7ffff7ffd000 r--p 00000000 00:00 0 [vvar]
|
||||
7ffff7ffd000-7ffff7fff000 r-xp 00000000 00:00 0 [vdso]
|
||||
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
|
||||
#+end_example
|
||||
|
||||
Ou com o =pmap=:
|
||||
|
||||
#+begin_example
|
||||
:~$ pmap $(pidof mappings)
|
||||
2793082: /home/blau/git/pbn/curso/exemplos/04/mappings
|
||||
0000000000400000 4K r---- mappings
|
||||
0000000000401000 4K r-x-- mappings
|
||||
0000000000402000 4K r---- mappings
|
||||
0000000000403000 4K rw--- mappings
|
||||
00007ffff7ff9000 16K r---- [ anôn ]
|
||||
00007ffff7ffd000 8K r-x-- [ anôn ]
|
||||
00007ffffffde000 132K rw--- [ pilha ]
|
||||
total 172K
|
||||
#+end_example
|
||||
|
||||
De qualquer forma, as regiões do layout de memória do processo são
|
||||
facilmente localizadas:
|
||||
|
||||
#+begin_example
|
||||
0000000000400000 4K r---- mappings <-- Tabelas ELF
|
||||
0000000000401000 4K r-x-- mappings <-- .text
|
||||
0000000000402000 4K r---- mappings <-- .rodata
|
||||
0000000000403000 4K rw--- mappings <-- .data .bss
|
||||
00007ffff7ff9000 16K r---- [ anôn ] <-- vvar
|
||||
00007ffff7ffd000 8K r-x-- [ anôn ] <-- vdso
|
||||
00007ffffffde000 132K rw--- [ pilha ] <-- pilha
|
||||
#+end_example
|
||||
|
||||
*** Localizando o dado em =.rodata=
|
||||
|
||||
O dado iniciado no rótulo =msg= tem 17 bytes, portanto:
|
||||
|
||||
#+begin_example
|
||||
(gdb) x /17bx &msg
|
||||
0x402000 <msg>: 0x53 0x61 0x6c 0x76 0x65 0x2c 0x20 0x73
|
||||
0x402008: 0x69 0x6d 0x70 0x61 0x74 0x69 0x61 0x21
|
||||
0x402010: 0x0a
|
||||
#+end_example
|
||||
|
||||
Como podemos ver, o endereço inicial dos dados no rótulo =msg= é =0x402000=,
|
||||
que corresponde ao mapeamento da seção =.rodata=:
|
||||
|
||||
#+begin_example
|
||||
0000000000402000 4K r---- mappings <-- .rodata
|
||||
#+end_example
|
||||
|
||||
*** Localizando os dados em =.data= e =.bss=
|
||||
|
||||
O dado no rótulo =global_data= foi inicializado com o valor 42 (=0x2c=, em hexa)
|
||||
e ocupa o espaço de uma /quad-word/ (8 bytes), que podemos exibir assim:
|
||||
|
||||
#+begin_example
|
||||
(gdb) x /8bx &global_data
|
||||
0x403014 <global_data>: 0x2a 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
#+end_example
|
||||
|
||||
Localizando seu endereço (=0x403014=) no mapeamento, nós vemos que os 8 bytes
|
||||
foram escritos na região da seção compartilhada por =.data= e =.bss=:
|
||||
|
||||
#+begin_example
|
||||
0000000000403000 4K rw--- mappings <-- .data .bss
|
||||
#+end_example
|
||||
|
||||
O mesmo ocorre com o espaço de 8 bytes preenchidos com zeros reservados no
|
||||
endereço identificado pelo rótulo =uninit_data=:
|
||||
|
||||
#+begin_example
|
||||
(gdb) x /8bx &uninit_data
|
||||
0x40301c <uninit_data>: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
|
||||
#+end_example
|
||||
|
||||
Repare, também, que o dado em =uninit_data= inicia exatamente 8 bytes após o
|
||||
dado em =global_data=:
|
||||
|
||||
#+begin_example
|
||||
:~$ echo $((0x40301c - 0x403014))
|
||||
8
|
||||
#+end_example
|
||||
|
||||
Novamente demonstrando que as seções =.data= e =.bss= são vizinhas.
|
||||
|
||||
** Executáveis independentes de posição (PIE)
|
||||
|
||||
Uma coisa que você deve ter notado, é a diferença de distância entre o
|
||||
mapeamento do executável gerado com =nasm+ld= em relação aos mapeamentos do
|
||||
sistema, se comparado com o mapeamento do exemplo em C:
|
||||
|
||||
#+begin_example
|
||||
0x563f63a43000 --> 0x7fff88020000 : Programa compilado com gcc
|
||||
0x000000400000 --> 0x7ffffffde000 : Programa montado com o nasm+ld
|
||||
#+end_example
|
||||
|
||||
Isso acontece porque, por padrão, o =gcc= produz arquivos executáveis do tipo
|
||||
PIE (/Position-Independent Executable/) que são programas que podem ser
|
||||
carregados em qualquer lugar da memória, pois seu código não depende de
|
||||
endereços fixos.
|
||||
|
||||
Isso permite que o sistema operacional utilize técnicas para aumentar a
|
||||
segurança da memória (como ASLR, para randomizar o layout de memória),
|
||||
dificultando explorações de falhas como, por exemplo, de /buffer overflow/.
|
||||
|
||||
Programas em Assembly também podem gerar executáveis PIE, mas isso depende
|
||||
de que o código seja escrito com vistas à sua montagem e link-edição para
|
||||
este tipo de executável. Por enquanto, é mais fácil demonstrar as diferenças
|
||||
de arquivos PIE e não-PIE compilando o exemplo em C com a opção =-no-pie=:
|
||||
|
||||
#+begin_example
|
||||
:~$ gcc -Wall mappings.c
|
||||
:~$ ./a.out
|
||||
local_var : 0x7fff26577c94 : 10
|
||||
*heap_var : 0x13aa92a0 : 123
|
||||
uninit_var : 0x404048 : 0
|
||||
static_main: 0x404040 : 5
|
||||
static_var : 0x40403c : 7
|
||||
global_var : 0x404038 : 42
|
||||
const_var : 0x402004 : 99
|
||||
main() : 0x401166
|
||||
Tecle algo para terminar...
|
||||
#+end_example
|
||||
|
||||
Isso mostra como os endereços mapeados se parecem mais com os que vimos com
|
||||
o exemplo em Assembly.
|
||||
|
||||
* Exercícios propostos
|
||||
|
||||
1. Adicione um vetor global de 10 inteiros no exemplo em C e verifique onde seus
|
||||
elementos são mapeados.
|
||||
2. Ainda no exemplo em C, investigue o efeito do qualificador =volatile= no
|
||||
mapeamento de variáveis.
|
||||
3. Modifique o código em C para usar =mmap= em vez de =malloc= e veja se há mudanças
|
||||
quanto a região mapeada.
|
||||
4. No exemplo em Assembly, mova as definições de =msg= e =len= para a seção =.data= e
|
||||
investigue o que se altera.
|
||||
|
||||
* Referências
|
||||
|
||||
- =man pmap=
|
||||
- =man proc_pid_maps=
|
||||
- =man 3 maloc=
|
||||
- =man 2 mmap=
|
||||
- =man 2 brk=
|
||||
- [[https://www.kernel.org/doc/gorman/pdf/understand.pdf][Mel Gorman: Understanding The Linux Virtual Memory Manager]]
|
||||
- [[https://docs.kernel.org/mm/][Linux: Memory Management Documentation]]
|
Loading…
Add table
Reference in a new issue