#+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 #include 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//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//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//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//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 : 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 : 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 : 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]]