Compare commits
No commits in common. "0f2dad4b0c3ea710cdff01da0da5fafd06e45437" and "dfe8916cbb1cf18c42445a63bb71f43cc65d6783" have entirely different histories.
0f2dad4b0c
...
dfe8916cbb
3 changed files with 0 additions and 714 deletions
|
@ -1,684 +0,0 @@
|
||||||
#+title: 5 -- Pilha de hardware e pilha de memória
|
|
||||||
#+author: Blau Araujo
|
|
||||||
#+email: cursos@blauaraujo.com
|
|
||||||
|
|
||||||
#+options: toc:3
|
|
||||||
|
|
||||||
* Objetivos
|
|
||||||
|
|
||||||
- Entender o que é uma pilha.
|
|
||||||
- Diferenciar pilha de hardware da pilha de memória.
|
|
||||||
- Explorar a movimentação do ponteiro da pilha.
|
|
||||||
- Observar a dinâmica dos quadros de pilha.
|
|
||||||
- Observar como a pilha se altera com a execução de sub-rotinas.
|
|
||||||
- Utilizar a pilha para salvar e restaurar estados de registradores.
|
|
||||||
- Escrever funções simples que manipulam a pilha explicitamente.
|
|
||||||
|
|
||||||
* O conceito de pilha (stack)
|
|
||||||
|
|
||||||
Pilhas são estruturas lineares de dados que seguem a política LIFO (/Last In,
|
|
||||||
First Out/), ou seja, o último elemento inserido é o primeiro a ser removido.
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
┌──────────────┐ TOPO DA PILHA
|
|
||||||
│ ELEMENTO N │
|
|
||||||
├──────────────┤
|
|
||||||
│ ELEMENTO N-1 │
|
|
||||||
├──────────────┤
|
|
||||||
│ ···· │
|
|
||||||
├──────────────┤
|
|
||||||
│ ELEMENTO 3 │
|
|
||||||
├──────────────┤
|
|
||||||
│ ELEMENTO 2 │
|
|
||||||
├──────────────┤
|
|
||||||
│ ELEMENTO 1 │
|
|
||||||
└──────────────┘ BASE DA PILHA
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Por analogia, podemos imaginar uma pilha de pratos: os pratos são colocados um
|
|
||||||
por cima do outro e, quando precisamos de um prato no meio da pilha, todos que
|
|
||||||
estiverem acima dele terão que ser retirados primeiro. Isso ilustra bem a
|
|
||||||
principal característica da estrutura, que é o acesso imediato restrito ao
|
|
||||||
elemento que estiver no topo da pilha.
|
|
||||||
|
|
||||||
** Implementação da pilha como estrutura de dados
|
|
||||||
|
|
||||||
Uma pilha pode ser implementada de duas formas principais: utilizando uma
|
|
||||||
área contígua de memória (como um vetor) ou uma estrutura que cresce conforme
|
|
||||||
a necessidade (como uma sequência de elementos ligados uns aos outros).
|
|
||||||
|
|
||||||
- Como um vetor ::
|
|
||||||
|
|
||||||
A pilha é representada por um arranjo fixo de elementos e um índice que
|
|
||||||
identifica o elemento que estiver no topo. Essa abordagem é simples e
|
|
||||||
eficiente, mas exige que o tamanho máximo da pilha seja definido
|
|
||||||
previamente.
|
|
||||||
|
|
||||||
- Como uma lista encadeada ::
|
|
||||||
|
|
||||||
Os elementos da pilha são criados conforme a necessidade, ligando cada
|
|
||||||
novo elemento ao que já estiver ocupando a posição do topo. Assim, a pilha
|
|
||||||
pode crescer ou diminuir dinamicamente, utilizando apenas o espaço de
|
|
||||||
memória realmente necessário.
|
|
||||||
|
|
||||||
** Aplicações típicas
|
|
||||||
|
|
||||||
As pilhas são amplamente utilizadas por serem simples e pela sua propriedade
|
|
||||||
de oferecerem acesso aos dados na ordem inversa de sua inserção. Algumas
|
|
||||||
aplicações comuns incluem:
|
|
||||||
|
|
||||||
- Avaliação de expressões em notação pós-fixada (notação polonesa reversa).
|
|
||||||
- Operações de desfazer e refazer em editores.
|
|
||||||
- Verificação do balanceamento de estruturas aninhadas (parêntesis, chaves
|
|
||||||
e colchetes).
|
|
||||||
- Algoritmos de busca e retrocesso (/backtracking/), como para explorar
|
|
||||||
labirintos ou solucionar problemas combinatórios por tentativa e erro.
|
|
||||||
- Controle de chamadas de funções durante a execução de um programa.
|
|
||||||
- Tratamento de interrupções de hardware e chamadas de funções e sub-rotinas.
|
|
||||||
|
|
||||||
Entre tantas outras. No entanto, as aplicações que nos interessam dizem
|
|
||||||
respeito às duas últimas da lista, como veremos um pouco mais adiante.
|
|
||||||
|
|
||||||
** Operações associadas às pilhas
|
|
||||||
|
|
||||||
De modo geral, as operações associadas a uma pilha são:
|
|
||||||
|
|
||||||
- *Push:* para inserir um novo elemento no topo da pilha;
|
|
||||||
- *Pop:* para acessar o elemento do topo da pilha e removê-lo;
|
|
||||||
- *Peek:* para acessar o elemento no topo da pilha sem removê-lo.
|
|
||||||
|
|
||||||
*Operação PUSH:*
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
TOPO DA PILHA (novo)
|
|
||||||
┌──────┐
|
|
||||||
TOPO DA PILHA │ 23 │
|
|
||||||
┌──────┐ ├──────┤
|
|
||||||
│ 64 │ │ 64 │
|
|
||||||
├──────┤ ├──────┤
|
|
||||||
│ 42 │ PUSH 23 │ 42 │
|
|
||||||
├──────┤ ├──────┤
|
|
||||||
│ 16 │ │ 16 │
|
|
||||||
└──────┘ └──────┘
|
|
||||||
BASE DA PILHA BASE DA PILHA
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
*Operação POP:*
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
TOPO DA PILHA
|
|
||||||
┌──────┐
|
|
||||||
│ 23 │ TOPO DA PILHA (removido)
|
|
||||||
├──────┤ ┌──────┐
|
|
||||||
│ 64 │ A = ??? │ 64 │ A = 23
|
|
||||||
├──────┤ ├──────┤
|
|
||||||
│ 42 │ POP A │ 42 │
|
|
||||||
├──────┤ ├──────┤
|
|
||||||
│ 16 │ │ 16 │
|
|
||||||
└──────┘ └──────┘
|
|
||||||
BASE DA PILHA BASE DA PILHA
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
*Operação PEEK:*
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
TOPO DA PILHA TOPO DA PILHA (não alterado)
|
|
||||||
┌──────┐ ┌──────┐
|
|
||||||
│ 23 │ │ 23 │
|
|
||||||
├──────┤ ├──────┤
|
|
||||||
│ 64 │ A = ??? │ 64 │ A = 23
|
|
||||||
├──────┤ ├──────┤
|
|
||||||
│ 42 │ PEEK A │ 42 │
|
|
||||||
├──────┤ ├──────┤
|
|
||||||
│ 16 │ │ 16 │
|
|
||||||
└──────┘ └──────┘
|
|
||||||
BASE DA PILHA BASE DA PILHA
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
* Pilha de memória
|
|
||||||
|
|
||||||
No contexto de sistemas GNU/Linux, a /pilha de memória/ é uma região especial da
|
|
||||||
memória virtual associada a um processo que cresce dinamicamente no sentido dos
|
|
||||||
endereços mais baixo. Essa estrutura é usada para armazenar dados temporários
|
|
||||||
durante a execução de um programa, especialmente:
|
|
||||||
|
|
||||||
- Endereços de retorno de chamadas de função.
|
|
||||||
- Valores recebidos pelas funções como argumentos.
|
|
||||||
- Dados em variáveis locais de funções.
|
|
||||||
- Dados nos registradores da CPU (/registros/) que foram salvos durante chamadas
|
|
||||||
e interrupções.
|
|
||||||
|
|
||||||
A pilha de memória é controlada pelo hardware e pelo sistema operacional, e seu
|
|
||||||
comportamento segue a política LIFO, como em toda estrutura de pilha: o último
|
|
||||||
valor inserido é o primeiro a ser removido. Além disso, em arquiteturas x86_64,
|
|
||||||
a pilha é acessada principalmente pelos registradores =rsp= (/stack pointer/) e
|
|
||||||
=rbp= (/base pointer/), e seu uso está profundamente ligado à organização de
|
|
||||||
chamadas de função e à convenção de chamadas da ABI do sistema.
|
|
||||||
|
|
||||||
* Pilha de hardware
|
|
||||||
|
|
||||||
Além da pilha de memória, gerenciada pelo sistema e pelas convenções de
|
|
||||||
chamadas de função, o processador possui um mecanismo chamado /pilha de
|
|
||||||
hardware/. Trata-se de um recurso interno da CPU que suporta operações de
|
|
||||||
empilhar (/push/) e desempilhar (/pop/) valores diretamente, facilitando o
|
|
||||||
gerenciamento da pilha de memória no nível do hardware.
|
|
||||||
|
|
||||||
Em processadores Intel 64, as instruções =push= e =pop= operam sobre a pilha de
|
|
||||||
memória associada ao registrador =rsp= (/stack pointer/), que contém o endereço
|
|
||||||
do dado que se encontra no topo da pilha.
|
|
||||||
|
|
||||||
Quando a instrução =push= é executada, o processador decrementa o valor em =rsp=
|
|
||||||
(o endereço do dado no topo da pilha) e, em seguida, armazena o novo dado no
|
|
||||||
endereço resultante. Já a instrução =pop= realiza o oposto: ela lê o valor no
|
|
||||||
endereço atualmente indicado em =rsp=, copia esse valor para um registrador ou
|
|
||||||
para outro local na memória, e então incrementa o endereço em =rsp=.
|
|
||||||
|
|
||||||
Resumindo:
|
|
||||||
|
|
||||||
- *Instrução =push=:* /Decrementa/ do endereço em =rsp= e insere um novo valor no topo
|
|
||||||
da pilha (a pilha de memória cresce no sentido dos endereços mais baixos).
|
|
||||||
|
|
||||||
Exemplo:
|
|
||||||
|
|
||||||
#+begin_src asm
|
|
||||||
push rax ; Copia o valor em rax para um novo
|
|
||||||
; elemento no topo da pilha.
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
- *Instrução =pop=:* Copia o dado no topo da pilha para um destino e incrementa o
|
|
||||||
endereço em =rsp=, o que faz com que o dado no endereço anterior seja ignorado
|
|
||||||
(e eventualmente sobrescrito) em operações subsequentes.
|
|
||||||
|
|
||||||
Exemplo:
|
|
||||||
|
|
||||||
#+begin_src asm
|
|
||||||
pop rax ; Copia para rax o valor que será
|
|
||||||
; removido do topo da pilha.
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
#+begin_quote
|
|
||||||
A implementação em hardware torna operações de pilha muito eficientes, e é uma
|
|
||||||
das razões pelas quais a pilha de memória é utilizada para controle de fluxo,
|
|
||||||
armazenamento temporário de dados, e manipulação de chamadas de função. Mas,
|
|
||||||
embora a CPU tenha suporte para operações com a pilha, sua manipulação depende
|
|
||||||
também do sistema operacional, da ABI e do compilador, que definem regras para
|
|
||||||
uso, alinhamento e preservação de seus dados.
|
|
||||||
#+end_quote
|
|
||||||
|
|
||||||
* Registradores e a pilha de memória
|
|
||||||
|
|
||||||
Como vimos, registradores são pequenas unidades de armazenamento internas da
|
|
||||||
CPU para guardar valores temporários. Eles são utilizados constantemente
|
|
||||||
durante a execução de programas, pois permitem a manipulação de dados sem
|
|
||||||
depender da memória principal (RAM), que é bem mais lenta.
|
|
||||||
|
|
||||||
Antes da execução de funções e sub-rotinas, é comum que o conteúdo de alguns
|
|
||||||
desses registradores precise ser preservado para que, ao retomar o controle
|
|
||||||
da sua execução, o programa encontre os mesmos valores que estavam presentes
|
|
||||||
antes da chamada. Para isso, os valores dos registradores costumam ser salvos
|
|
||||||
na pilha de memória e restaurados posteriormente.
|
|
||||||
|
|
||||||
#+begin_quote
|
|
||||||
A especificação de quais registradores precisam ser salvos depende do contexto
|
|
||||||
e, principalmente, das convenções envolvidas, como veremos a seguir.
|
|
||||||
#+end_quote
|
|
||||||
|
|
||||||
* Convenções de chamadas de funções (System V AMD64 ABI)
|
|
||||||
|
|
||||||
Ao compilar programas em linguagens como C para sistemas Linux 64 bits, o
|
|
||||||
código gerado segue uma convenção de chamadas definida pela ABI (/Application
|
|
||||||
Binary Interface/) do sistema. Essa convenção especifica:
|
|
||||||
|
|
||||||
- Quais registradores são usados para passar argumentos.
|
|
||||||
- Qual registrador é usado para o valor de retorno.
|
|
||||||
- Quais registradores devem ser preservados entre chamadas de função.
|
|
||||||
- Como a pilha deve ser utilizada.
|
|
||||||
|
|
||||||
** Ordem dos argumentos
|
|
||||||
|
|
||||||
Os primeiros seis argumentos de uma função são passados em registradores, na
|
|
||||||
seguinte ordem:
|
|
||||||
|
|
||||||
| Argumento | Registrador |
|
|
||||||
|-----------+-------------|
|
|
||||||
| =arg1= | =RDI= |
|
|
||||||
| =arg2= | =RSI= |
|
|
||||||
| =arg3= | =RDX= |
|
|
||||||
| =arg4= | =RCX= |
|
|
||||||
| =arg5= | =R8= |
|
|
||||||
| =arg6= | =R9= |
|
|
||||||
|
|
||||||
#+begin_quote
|
|
||||||
Argumentos adicionais são passados na pilha.
|
|
||||||
#+end_quote
|
|
||||||
|
|
||||||
** Valor de retorno
|
|
||||||
|
|
||||||
O valor de retorno de uma função é armazenado em:
|
|
||||||
|
|
||||||
- =RAX=: para valores inteiros ou ponteiros.
|
|
||||||
- =RAX + RDX=: para estruturas de até 128 bits.
|
|
||||||
- =XMM0= (e seguintes): para valores em ponto flutuante.
|
|
||||||
|
|
||||||
** Registradores preservados e não preservados
|
|
||||||
|
|
||||||
Durante uma chamada de função, há uma distinção entre os registradores que
|
|
||||||
devem ser preservados e os que podem ser sobrescritos...
|
|
||||||
|
|
||||||
Preservados pela função chamada (/callee-saved/):
|
|
||||||
|
|
||||||
- =RBX=
|
|
||||||
- =RBP=
|
|
||||||
- =R12= a =R15=
|
|
||||||
|
|
||||||
#+begin_quote
|
|
||||||
=RSP= também deve ser restaurado à posição original.
|
|
||||||
#+end_quote
|
|
||||||
|
|
||||||
Não presenrvados (/caller-saved/):
|
|
||||||
|
|
||||||
- =RAX=
|
|
||||||
- =RCX=
|
|
||||||
- =RDX=
|
|
||||||
- =RSI=
|
|
||||||
- =RDI=
|
|
||||||
- =R8= a =R11=
|
|
||||||
|
|
||||||
Os registradores não preservados podem ser modificados pela função chamada.
|
|
||||||
Assim, se uma função chamadora deseja manter o valor de algum deles após a
|
|
||||||
chamada, ela será responsável por salvá-los antes.
|
|
||||||
|
|
||||||
** Exemplo em C
|
|
||||||
|
|
||||||
Arquivo: [[exemplos/05/soma.c][soma.c]]
|
|
||||||
|
|
||||||
#+begin_src c :tangle exemplos/05/soma.c
|
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
int soma(int a, int b) {
|
|
||||||
return a + b;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
int x = 10;
|
|
||||||
int y = 20;
|
|
||||||
int r = soma(x, y);
|
|
||||||
printf("Soma: %d\n", r);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
Durante a execução de =soma(x, y)=, os valores de =x= e =y= são passados via =RDI=
|
|
||||||
e =RSI=, respectivamente. O resultado é retornado em =RAX=. Se a função =soma=
|
|
||||||
quiser usar, por exemplo, =RBX=, ela deverá salvá-lo na pilha no início da
|
|
||||||
função (com =push rbx=) e restaurá-lo (com =pop rbx=) antes de retornar.
|
|
||||||
|
|
||||||
** Quadro de pilha (/stack frame/)
|
|
||||||
|
|
||||||
Em geral, cada função chamada cria seu próprio /quadro de pilha/, que inclui:
|
|
||||||
|
|
||||||
- Valores salvos de registradores preservados.
|
|
||||||
- Variáveis locais.
|
|
||||||
- Espaço para armazenar argumentos extras ou retorno de funções chamadas.
|
|
||||||
|
|
||||||
A base do quadro costuma ser registrada em =RBP=, como podemos demonstrar
|
|
||||||
compilando o programa do exemplo e analisando o assembly desmontado pelo
|
|
||||||
GDB.
|
|
||||||
|
|
||||||
*Compilação:*
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
:~$ gcc -O0 -no-pie -g -o soma soma.c
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Aqui, nós desabilitamos as otimizações (=-O0=) e a geração de um executável
|
|
||||||
independente de posição (=-no-pie=) para facilitar a análise dos endereços
|
|
||||||
absolutos das instruções.
|
|
||||||
|
|
||||||
*Início do GDB:*
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
:~$ gdb salve
|
|
||||||
Reading symbols from soma...
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
*Desmontagem da função =soma=:*
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
(gdb) disassemble soma
|
|
||||||
Dump of assembler code for function soma:
|
|
||||||
0x0000000000401126 <+0>: push rbp
|
|
||||||
0x0000000000401127 <+1>: mov rbp,rsp
|
|
||||||
0x000000000040112a <+4>: mov DWORD PTR [rbp-0x4],edi
|
|
||||||
0x000000000040112d <+7>: mov DWORD PTR [rbp-0x8],esi
|
|
||||||
0x0000000000401130 <+10>: mov edx,DWORD PTR [rbp-0x4]
|
|
||||||
0x0000000000401133 <+13>: mov eax,DWORD PTR [rbp-0x8]
|
|
||||||
0x0000000000401136 <+16>: add eax,edx
|
|
||||||
0x0000000000401138 <+18>: pop rbp
|
|
||||||
0x0000000000401139 <+19>: ret
|
|
||||||
End of assembler dump.
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Logo nas duas primeiras instruções, nós podemos ver a inicialização do quadro
|
|
||||||
de pilha:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
0x0000000000401126 <+0>: push rbp
|
|
||||||
0x0000000000401127 <+1>: mov rbp,rsp
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Onde o endereço da base da pilha (em =rbp=) é inserido como um novo elemento no
|
|
||||||
topo da pilha. Em seguida, o novo endereço do topo da pilha (em =rsp=) é
|
|
||||||
copiado (em =rbp=) como o endereço da base da pilha, utilizado nas instruções
|
|
||||||
seguintes como referencial fixo para a inserção dos dados da função.
|
|
||||||
|
|
||||||
Antes do retorno (=ret=), o endereço do topo da pilha é decrementado e o
|
|
||||||
resultado, salvo em =rbp=, será o endereço que ele armazenava originalmente --
|
|
||||||
em outras palavras, o estado original de =rbp= é restaurado.
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
0x0000000000401138 <+18>: pop rbp
|
|
||||||
0x0000000000401139 <+19>: ret
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Repare também que, entre o início e a remoção do quadro de pilha, os
|
|
||||||
argumentos passados na chamada da função (registrados em =edi= e =esi=) são
|
|
||||||
copiados em endereços, respectivamente, 4 e 8 bytes mais baixos que o
|
|
||||||
endereço em =rbp=:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
0x000000000040112a <+4>: mov DWORD PTR [rbp-0x4],edi
|
|
||||||
0x000000000040112d <+7>: mov DWORD PTR [rbp-0x8],esi
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Deste modo, os dados são inseridos na pilha sem que novos elementos sejam
|
|
||||||
empilhados, ou seja, para todos os efeitos, o endereço do topo da pilha ainda
|
|
||||||
será o registrado em =rbp=.
|
|
||||||
|
|
||||||
* Convenções de chamadas de sistema
|
|
||||||
|
|
||||||
Diferente das chamadas de função ,que utilizam a pilha de memória para
|
|
||||||
armazenar argumentos e preservar o estado dos registradores, chamadas de
|
|
||||||
sistema interagem diretamente com o kernel e seguem uma convenção distinta,
|
|
||||||
mais enxuta e orientada ao uso de registradores.
|
|
||||||
|
|
||||||
Em sistemas Linux x86-64, a transição entre espaço de usuário e espaço do
|
|
||||||
kernel é feita por meio da instrução =syscall=. Os argumentos são passados
|
|
||||||
exclusivamente por registradores:
|
|
||||||
|
|
||||||
| Registrador | Conteúdo |
|
|
||||||
|-------------+------------------------------|
|
|
||||||
| =rax= | Número da chamada de sistema |
|
|
||||||
| =rdi= | 1º argumento |
|
|
||||||
| =rsi= | 2º argumento |
|
|
||||||
| =rdx= | 3º argumento |
|
|
||||||
| =r10= | 4º argumento |
|
|
||||||
| =r8= | 5º argumento |
|
|
||||||
| =r9= | 6º argumento |
|
|
||||||
| =rax= | Valor de retorno |
|
|
||||||
|
|
||||||
A pilha permanece intacta durante a chamada de sistema — ou seja, não há
|
|
||||||
necessidade de alocar um novo quadro de pilha nem de salvar/restaurar
|
|
||||||
registradores preservados, como ocorre nas convenções de chamadas entre
|
|
||||||
funções. Em vez disso, o kernel assume que os registradores não preservados
|
|
||||||
(=rax=, =rcx= e =r11=) podem ser sobrescritos e o processo que realiza a chamada
|
|
||||||
é que deve se encarregar de salvar qualquer valor que deseje manter.
|
|
||||||
|
|
||||||
Essa característica destaca o papel dos registradores temporários: eles são
|
|
||||||
utilizados livremente pela chamada de sistema, e qualquer valor importante
|
|
||||||
deve ser salvo na pilha pelo chamador se necessário, o que geralmente é feito
|
|
||||||
com =push <registrador>=, antes da chamada de sistema, e =pop <registrador> após
|
|
||||||
a chamada.
|
|
||||||
|
|
||||||
** Exemplo em Assembly
|
|
||||||
|
|
||||||
Arquivo: [[exemplos/05/salve.asm][salve.asm]]
|
|
||||||
|
|
||||||
#+begin_src asm :tangle exemplos/05/salve.asm
|
|
||||||
section .rodata
|
|
||||||
msg db "Salve, simpatia!", 10
|
|
||||||
len equ $ - msg
|
|
||||||
|
|
||||||
section .text
|
|
||||||
global _start
|
|
||||||
|
|
||||||
_start:
|
|
||||||
mov rax, 1 ; syscall write
|
|
||||||
mov rdi, 1 ; stdout
|
|
||||||
mov rsi, msg
|
|
||||||
mov rdx, len
|
|
||||||
syscall
|
|
||||||
|
|
||||||
mov rax, 60 ; syscall exit
|
|
||||||
mov rdi, 0 ; código de saída 0
|
|
||||||
syscall
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
Com este exemplo, nós queremos demonstrar que, nas chamadas de sistema:
|
|
||||||
|
|
||||||
- Nenhum dado é empilhado.
|
|
||||||
- Nenhum registrador é preservado.
|
|
||||||
- O controle é transferido diretamente ao kernel por meio de =syscall=.
|
|
||||||
- Após o retorno do kernel, o processo continua sua execução ou é finalizado.
|
|
||||||
|
|
||||||
Para isso, vamos montá-lo e abrir o executável com o GDB:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
:~$ nasm -f elf64 -g salve.asm
|
|
||||||
:~$ ld -o salve salve.o
|
|
||||||
:~$ gdb salve
|
|
||||||
Reading symbols from salve...
|
|
||||||
(gdb)
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Com o comando =list=, nós descobrimos que a instrução que processará a chamada
|
|
||||||
de sistema =write= (=sycall=) está na linha 13:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
(gdb) list
|
|
||||||
1 section .rodata
|
|
||||||
2 msg db "Salve, simpatia!", 10
|
|
||||||
3 len equ $ - msg
|
|
||||||
4
|
|
||||||
5 section .text
|
|
||||||
6 global _start
|
|
||||||
7
|
|
||||||
8 _start:
|
|
||||||
9 mov rax, 1 ; syscall write
|
|
||||||
10 mov rdi, 1 ; stdout
|
|
||||||
(gdb)
|
|
||||||
11 mov rsi, msg
|
|
||||||
12 mov rdx, len
|
|
||||||
13 syscall
|
|
||||||
14
|
|
||||||
15 mov rax, 60 ; syscall exit
|
|
||||||
16 mov rdi, 0 ; código de saída 0
|
|
||||||
17 syscall
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Sendo assim, ela será o nosso ponto de parada:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
(gdb) break 13
|
|
||||||
Breakpoint 1 at 0x401019: file salve.asm, line 13.
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Nosso objetivo é comparar o estado dos registradores antes e depois da
|
|
||||||
execução do ponto de parada para determinar o que mudou. Então, vamos
|
|
||||||
executar o programa e inspecionar o estado corrente dos registradores:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
(gdb) run
|
|
||||||
Starting program: /home/blau/git/pbn/curso/exemplos/05/salve
|
|
||||||
|
|
||||||
Breakpoint 1, _start () at salve.asm:13
|
|
||||||
13 syscall
|
|
||||||
(gdb) info registers
|
|
||||||
rax 0x1 1
|
|
||||||
rbx 0x0 0
|
|
||||||
rcx 0x0 0
|
|
||||||
rdx 0x11 17
|
|
||||||
rsi 0x402000 4202496
|
|
||||||
rdi 0x1 1
|
|
||||||
rbp 0x0 0x0
|
|
||||||
rsp 0x7fffffffdfd0 0x7fffffffdfd0
|
|
||||||
r8 0x0 0
|
|
||||||
r9 0x0 0
|
|
||||||
r10 0x0 0
|
|
||||||
r11 0x0 0
|
|
||||||
r12 0x0 0
|
|
||||||
r13 0x0 0
|
|
||||||
r14 0x0 0
|
|
||||||
r15 0x0 0
|
|
||||||
rip 0x401019 0x401019 <_start+25>
|
|
||||||
eflags 0x202 [ IF ]
|
|
||||||
cs 0x33 51
|
|
||||||
ss 0x2b 43
|
|
||||||
ds 0x0 0
|
|
||||||
es 0x0 0
|
|
||||||
fs 0x0 0
|
|
||||||
gs 0x0 0
|
|
||||||
fs_base 0x0 0
|
|
||||||
gs_base 0x0 0
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Note que, até aqui, nós utilizamos apenas os registradores...
|
|
||||||
|
|
||||||
- =RAX=: Identificação da chamada de sistema =write= (=0x1=);
|
|
||||||
- =RDX=: Quantidade de bytes a serem impressos (=0x11 = 17=);
|
|
||||||
- =RSI=: O endereço do primeiro byte da mensagem (=0x402000=);
|
|
||||||
- =RDI=: O número do descritor de arquivos (=0x1 = stdout=).
|
|
||||||
|
|
||||||
Todos os demais estão em seus estados originais, menos =RIP=, o ponteiro de
|
|
||||||
instruções, que sempre é atualizado para indicar o endereço da próxima
|
|
||||||
instrução a ser executada. No caso, =RIP= contém o endereço =0x401019=, que é
|
|
||||||
exatamente o endereço da instrução =syscall=, no nosso ponto de parada.
|
|
||||||
|
|
||||||
Mas vejamos o que acontece quando a chamada de sistema é processada:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
(gdb) next
|
|
||||||
Salve, simpatia!
|
|
||||||
15 mov rax, 60 ; syscall exit
|
|
||||||
(gdb) info registers
|
|
||||||
rax 0x11 17
|
|
||||||
rbx 0x0 0
|
|
||||||
rcx 0x401004 4198404
|
|
||||||
rdx 0x11 17
|
|
||||||
rsi 0x402000 4202496
|
|
||||||
rdi 0x1 1
|
|
||||||
rbp 0x0 0x0
|
|
||||||
rsp 0x7fffffffdfd0 0x7fffffffdfd0
|
|
||||||
r8 0x0 0
|
|
||||||
r9 0x0 0
|
|
||||||
r10 0x0 0
|
|
||||||
r11 0x302 770
|
|
||||||
r12 0x0 0
|
|
||||||
r13 0x0 0
|
|
||||||
r14 0x0 0
|
|
||||||
r15 0x0 0
|
|
||||||
rip 0x40101b 0x40101b <_start+27>
|
|
||||||
eflags 0x202 [ IF ]
|
|
||||||
cs 0x33 51
|
|
||||||
ss 0x2b 43
|
|
||||||
ds 0x0 0
|
|
||||||
es 0x0 0
|
|
||||||
fs 0x0 0
|
|
||||||
gs 0x0 0
|
|
||||||
fs_base 0x0 0
|
|
||||||
gs_base 0x0 0
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Observe que apenas três registradores foram alterados:
|
|
||||||
|
|
||||||
| Registrador | Antes | Depois |
|
|
||||||
|-------------+-------+-----------|
|
|
||||||
| =RAX= | =0x1= | =0x11 (17)= |
|
|
||||||
| =RCX= | =0x0= | =0x401004= |
|
|
||||||
| =R11= | =0x0= | =0x302= |
|
|
||||||
|
|
||||||
*Causa da mudança em RAX:*
|
|
||||||
|
|
||||||
O registrador =RAX= é utilizado como retorno da chamada de sistema =write= e
|
|
||||||
recebe a quantidade de bytes impressos (17).
|
|
||||||
|
|
||||||
*Causa da mudança em RCX:*
|
|
||||||
|
|
||||||
Quando uma chamada de sistema é processada, o endereço da instrução seguinte
|
|
||||||
(originalmente em =RIP=) é salvo em =RCX= e restaurado para =RIP= antes do retorno
|
|
||||||
da chamada, mas isso não significa que, no espaço de usuário, após a linha de
|
|
||||||
=syscall=, nós encontraremos em =RCX= o mesmo endereço registrado em =RIP=. Not
|
|
||||||
e que os valores em =RCX= e =RIP= são diferentes após a chamada de sistema:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
(gdb) info registers rcx rip
|
|
||||||
rcx 0x401004 4198404
|
|
||||||
rip 0x40101b 0x40101b <_start+27>
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
Acontece que, depois de restaurado o endereço em =RIP=, o kernel está livre
|
|
||||||
para utilizar =RCX= antes de retornar ao espaço de usuário. Por isso, em
|
|
||||||
chamadas de sistema, =RCX= é considerado um registrador /volátil/, ou seja, seu
|
|
||||||
conteúdo pode ser alterado a qualquer momento pelos próprios mecanismos
|
|
||||||
internos da transição entre os modos de execução, sem qualquer garantia de
|
|
||||||
preservação.
|
|
||||||
|
|
||||||
*Causa da mudança em R11:*
|
|
||||||
|
|
||||||
Internamente, em uma chamada de sistema, =R11= é utilizado para salvar e
|
|
||||||
restaurar o estado do registrador =RFLAGS= (=eflags= no GDB). Contudo, assim
|
|
||||||
como acontece com =RCX=, após a restauração de =RFLAGS= o kernel está livre
|
|
||||||
para utilizar =R11= no processo de transição de volta ao espaço de usuário,
|
|
||||||
o que justifica a diferença encontrada nesses dois registradores:
|
|
||||||
|
|
||||||
#+begin_example
|
|
||||||
(gdb) info registers r11 eflags
|
|
||||||
r11 0x302 770
|
|
||||||
eflags 0x202 [ IF ]
|
|
||||||
#+end_example
|
|
||||||
|
|
||||||
** Preservação de registradores com a pilha
|
|
||||||
|
|
||||||
Se for necessário, é possível salvar e restaurar os registradores =RAX=, =RCX= e
|
|
||||||
=R11= para a execução de uma chamada de sistema. A técnica mais comum em
|
|
||||||
arquiteturas x86_64 envolve a utilização da pilha de memória:
|
|
||||||
|
|
||||||
#+begin_src asm
|
|
||||||
push rax ; salva rax como elemento N da pilha
|
|
||||||
push rcx ; salva rcx como elemento N+1 da pilha
|
|
||||||
push r11 ; salva r11 como elemento N+2 da pilha (atual topo)
|
|
||||||
|
|
||||||
; código da chamada de sistema...
|
|
||||||
|
|
||||||
pop r11 ; restaura e remove elemento N+2 da pilha (atual topo)
|
|
||||||
pop rcx ; restaura e remove elemento N+1 da pilha
|
|
||||||
pop rax ; restaura e remove elemento N da pilha
|
|
||||||
#+end_src
|
|
||||||
|
|
||||||
É importante observar a ordem de salvamento e restauração, porque estamos
|
|
||||||
lidando com uma pilha, e o elemento em seu topo sempre será o último que
|
|
||||||
tiver sido empilhado.
|
|
||||||
|
|
||||||
** Resumo comparativo com chamadas de funções
|
|
||||||
|
|
||||||
Finalmente, aqui está uma tabela comparando as principais diferenças entre
|
|
||||||
as convenções de chamadas de funções e a ABI do kernel para chamadas de
|
|
||||||
sistema:
|
|
||||||
|
|
||||||
| Aspecto | Chamadas de função | Chamadas de sistema |
|
|
||||||
|------------------------+-----------------------+---------------------------------|
|
|
||||||
| Interface | Conveção System V ABI | ABI do kernel (syscall ABI) |
|
|
||||||
| Usa a pilha de memória | Sim (quadro de pilha) | Não (usa registradores) |
|
|
||||||
| Preserva registradores | Sim | Não (registradores temporários) |
|
|
||||||
| Instrução de transição | =call=, =ret= | =syscall= |
|
|
||||||
| Participação do kernel | Não | Sim |
|
|
||||||
|
|
||||||
* Exercícios propostos
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
* Referências
|
|
||||||
|
|
||||||
- man 2 syscall
|
|
||||||
- [[https://wiki.osdev.org/Calling_Conventions][OS Dev: Calling Conventions]]
|
|
||||||
- [[https://wiki.osdev.org/Stack][OS Dev: Stack]]
|
|
|
@ -1,17 +0,0 @@
|
||||||
section .rodata
|
|
||||||
msg db "Salve, simpatia!", 10
|
|
||||||
len equ $ - msg
|
|
||||||
|
|
||||||
section .text
|
|
||||||
global _start
|
|
||||||
|
|
||||||
_start:
|
|
||||||
mov rax, 1 ; syscall write
|
|
||||||
mov rdi, 1 ; stdout
|
|
||||||
mov rsi, msg
|
|
||||||
mov rdx, len
|
|
||||||
syscall
|
|
||||||
|
|
||||||
mov rax, 60 ; syscall exit
|
|
||||||
mov rdi, 0 ; código de saída 0
|
|
||||||
syscall
|
|
|
@ -1,13 +0,0 @@
|
||||||
#include <stdio.h>
|
|
||||||
|
|
||||||
int soma(int a, int b) {
|
|
||||||
return a + b;
|
|
||||||
}
|
|
||||||
|
|
||||||
int main() {
|
|
||||||
int x = 10;
|
|
||||||
int y = 20;
|
|
||||||
int r = soma(x, y);
|
|
||||||
printf("Soma: %d\n", r);
|
|
||||||
return 0;
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue