diff --git a/curso/aula-04.org b/curso/aula-04.org new file mode 100644 index 0000000..4185ba9 --- /dev/null +++ b/curso/aula-04.org @@ -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 +#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]]