833 lines
32 KiB
Org Mode
833 lines
32 KiB
Org Mode
#+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]]
|