Jump to content

Recommended Posts

Eu tava estudando sobre esses 2 registradores, e eu não entendi muito bem sobre eles. O ESP aponta para o topo da pilha, e o EBP pelo q eu tava pesquisando, aponta pra base da pilha. Mas como assim ? O EBP aponta pro endereço de memória onde começa o stack frame ? É isso ?

E o ESP vai apontar pro topo do stack frame ?

  • Curtir 1
Link to post
Share on other sites

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.
 

Edited by fredericopissarra
  • Agradecer 2
  • Curtir 1
Link to post
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:

 

  • Agradecer 1
Link to post
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.

Edited by fredericopissarra
  • Agradecer 1
Link to post
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.

Edited by fredericopissarra
  • Agradecer 1
Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   1 member

×
×
  • Create New...