conteúdo da aula 5
This commit is contained in:
parent
dfe8916cbb
commit
7df3f44f24
1 changed files with 684 additions and 0 deletions
684
curso/aula-05.org
Normal file
684
curso/aula-05.org
Normal file
|
@ -0,0 +1,684 @@
|
|||
#+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]]
|
Loading…
Add table
Reference in a new issue