Jonathan403 Posted January 21, 2021 at 06:39 PM Share Posted January 21, 2021 at 06:39 PM 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 ? Link to comment Share on other sites More sharing options...
fredericopissarra Posted January 21, 2021 at 08:00 PM Share Posted January 21, 2021 at 08:00 PM 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 More sharing options...
Sckoofer B Posted January 21, 2021 at 10:24 PM Share Posted January 21, 2021 at 10:24 PM 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 More sharing options...
Jonathan403 Posted January 22, 2021 at 12:16 AM Author Share Posted January 22, 2021 at 12:16 AM Entendido! Muito obrigado pela resposta Frederico e Sckoofer! Link to comment Share on other sites More sharing options...
fredericopissarra Posted January 22, 2021 at 01:04 PM Share Posted January 22, 2021 at 01:04 PM 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 More sharing options...
fredericopissarra Posted January 22, 2021 at 01:23 PM Share Posted January 22, 2021 at 01:23 PM 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 More sharing options...
Recommended Posts
Archived
This topic is now archived and is closed to further replies.