#+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]]
