Jump to content

Registrador ESP e EBP


Jonathan403

Recommended Posts

Nos modos de operação real (16 bits) e i386 (32 bits), sempre que se usa ESP ou EBP numa referência à memória, automaticamente o seletor de segmento SS é usado. Por exemplo:

mov eax,[esp]    ; Lê dword de SS:ESP
mov eax,[ebp]    ; Lê dword de SS:EBP

Assim, EBP pode ser usado para apontar para qualquer lugar da pilha.

Em códigos não otimizados os compiladores de linguagens de alto nível usam EBP para marcar o ínicio do stack frame usado pela função. Desse jeito:
 

f:
  push ebp       ; salva EBP na pilha para recuperar depois.
  mov  ebp,esp   ; EBP agora têm o endereço desse novo "topo" da pilha.

A partir daí você pode manipular ESP como bem entender (PUSHes, POPs) que EBP conterá o endereço da "base" do stack frame.

No final da função basta recuperar o antigo valor de EBP e sair:

  pop  ebp   ; Recupera EBP da pilha.
  ret

Mas, isso não significa que EBP seja "especializado" como registrador de "base" da pilha. Somente sua associação com SS é que o torna "especial". Mas, ele pode ser usado como um registrador de uso geral como os demais (EAX, EBX, ECX, EDX, ESI e EDI).

Eu disse que esse "macete" é usado em códigos antigos e não otimizados porque é perfeitamente possível usar ESP para acessar memória em códigos de 32 e 64 bits (não é possível em 16 bits usando SP - a não ser usando ESP). E, de fato, o uso do prólogo (ajustar EBP) e o epílogo (recuperar o EBP) não é muito desejável por questões de performance e tamanho de código (compiladores como GCC, por exemplo, se usadas otimizações, implementam a opção -fomit-frame-pointer, que evitam a criação do prólogo e epílogo).

Então, sim... no contexto de uma função, pode-se pensar no EBP como a "base" do stack frame. No contexto do processador, no entanto, ele é um registrador de uso geral como qualquer outro, exceto que referências à memória, usando-o, são associadas ao seletor SS, ao invés do seletor DS.
 

Link to comment
Share on other sites

Sim, muito interessante a explicação do Frederico, não sabia que sempre que se usa ESP ou EBP numa referência à memória, automaticamente um seletor seria usado. Na verdade eu achava que os seletores, como dito em duas video aulas de Assembly (e acho que no material de x64 que o Frederico escreveu) seriam usados em uma situação bem específica, por exemplo, programar um sistema operacional ou um driver, e que não deveríamos nos preocupar em usar os seletores.

E outra coisa, não sabia que a ausencia do prólogo é sinônimo de código otimizado, já que os poucos códigos que ví , que foram gerados por um disassembler, todos eles usam o prólogo pra começar uma função. Também não sabia que o uso dele era opcional.

Para divulgação de material daqueles que trazem conteúdo para o mente binária, o Fernando Fresteiro  (M4st3r3k) também tem um vídeo mostrando o uso do Stack Frame e também explica sobre:

 

Link to comment
Share on other sites

14 horas atrás, Sckoofer B disse:

Sim, muito interessante a explicação do Frederico, não sabia que sempre que se usa ESP ou EBP numa referência à memória, automaticamente um seletor seria usado. Na verdade eu achava que os seletores, como dito em duas video aulas de Assembly (e acho que no material de x64 que o Frederico escreveu) seriam usados em uma situação bem específica, por exemplo, programar um sistema operacional ou um driver, e que não deveríamos nos preocupar em usar os seletores.

E outra coisa, não sabia que a ausencia do prólogo é sinônimo de código otimizado, já que os poucos códigos que ví , que foram gerados por um disassembler, todos eles usam o prólogo pra começar uma função. Também não sabia que o uso dele era opcional.

Pois é... nos antigos 8086 até o 80286 só tínhamos os registradores AX, BX, CX, DX, SI, DI, BP, SP, além do IP e FLAGS e os seletores. Nos modos de endereçamento era possível usar apenas BX ou BP como endereço base e SI ou DI como índice e, ao usar BP o seletor SS era usado (ao usar BX, DS era usado). Nos 386 isso mudou e qualquer registrador de uso geral pode ser usado como base e índice, mas a seleção automática de SS quando usados ESP ou EBP continua valendo.

Quanto ao "sinônimo" de código otimizado eis um exemplo simples:

int fma_( int a, int b, int c )
{ return a*b+c; }

Se você compilar isso sem o uso de alguma opção de otimização:

$ cc -S -o test1.s test.c

Obterá algo assim:
 

fma_:
  push  rbp             ; Prólogo
  mov   rbp, rsp

  mov   [rbp-4], edi    ; Armazena os argumentos.
  mov   [rbp-8], esi
  mov   [rbp-12], edx

  ; Multiplica e soma, pegando os argumentos da pilha.
  mov   eax, [rbp-4]
  imul  eax, [rbp-8]
  mov   edx, eax
  mov   eax, [rbp-12]
  add   eax, edx

  pop   rbp             ; Epílogo

  ret

Note que, apesar os argumentos serem passados por registradores o compilador armazenou os argumentos na pilha (porque os argumentos são locais à função) e os leu, pela segunda vez, antes das operações... E os prólogo e epílogo estão lá... Basta adicionar -O2 na compilação para obter um código otimizado:

fma_:
  imul  edi, esi
  lea   eax, [rdi+rdx]
  ret

Sem armazenamento local e sem aquela preparação para o stack frame.

No modo x86-64 a falta de otimização leva a aquela redundância de cópia/leitura... No modo i386 é um pouco melhor, uma vez que os argumentos já são passados pela pilha (exceto nos casos de convenções como fastcall, por exemplo):
 

; Sem -O2                 
fma_:                     
  push  ebp               
  mov   ebp, esp          
                          
  mov   eax, [ebp+8]      
  imul  eax, [ebp+12]
  mov   edx, eax
  mov   eax, [ebp+16]
  add   eax, edx

  pop ebp

  ret
------------------------------
; Com -O2
fma_:
  mov   eax, [esp+8]
  imul  eax, [esp+4]
  add   eax, [esp+12]
  ret

Note, no entanto que a função otimizada não usa EBP e acessa os argumentos diretamente da pilha.

Link to comment
Share on other sites

Um "macete" para evitar ficar lembrando os offsets na pilha é usar estruturas (exemplo com o NASM, código para i386):
 

struc fmastkf
.retaddr:  resd 1   ; CALL colocar isso na pilha.
.a: resd 1
.b: resd 1
.c: resd 1
endstruc

fma_:
  mov  eax,[esp+fmastkf.b]  ; O compilador preferiu pegar b primeiro...
  imul eax,[esp+fmastkf.a]  ; ... e multiplicar por a.
  add  eax,[esp+fmastkf.c]
  ret

O empilhamento, na chamada de fma_(), é claro, é feito de trás para frente. Assim, como a é empilhado por último, ele fica mais próximo de retaddr.

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...