Qual é exatamente o ponteiro base e o ponteiro da pilha? Para o que eles apontam?

225

Usando este exemplo, da wikipedia, no qual DrawSquare () chama DrawLine (),

texto alternativo

(Observe que este diagrama tem endereços altos na parte inferior e endereços baixos na parte superior.)

Alguém poderia me explicar o que é ebpe espneste contexto?

Pelo que vejo, eu diria que o ponteiro da pilha aponta sempre para o topo da pilha e o ponteiro base para o início da função atual? Ou o que?


editar: quero dizer isso no contexto de programas do Windows

edit2: E como eipfunciona também?

edit3: Eu tenho o seguinte código do MSVC ++:

var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= dword ptr -4
hInstance= dword ptr  8
hPrevInstance= dword ptr  0Ch
lpCmdLine= dword ptr  10h
nShowCmd= dword ptr  14h

Todos eles parecem ser dwords, ocupando 4 bytes cada. Então eu posso ver que há uma diferença de hInstance para var_4 de 4 bytes. O que eles são? Presumo que seja o endereço de retorno, como pode ser visto na foto da wikipedia?


(nota do editor: removemos uma citação longa da resposta de Michael, que não pertence à pergunta, mas uma pergunta de acompanhamento foi editada em):

Isso ocorre porque o fluxo da chamada de função é:

* Push parameters (hInstance, etc.)
* Call function, which pushes return address
* Push ebp
* Allocate space for locals

Minha pergunta (por último, espero!) Agora é: o que é exatamente o que acontece desde o instante em que apareço os argumentos da função que quero chamar até o final do prólogo? Eu quero saber como o ebp, esp evolui durante esses momentos (eu já entendi como o prólogo funciona, eu só quero saber o que está acontecendo depois que eu empurrei os argumentos na pilha e antes do prólogo).

elísio devorado
fonte
23
Uma coisa importante a ser observada é que a pilha cresce "para baixo" na memória. Isso significa que, para mover o ponteiro da pilha para cima, você diminui seu valor.
BS
4
Uma dica para diferenciar o que o EBP / ESP e o EIP estão fazendo: EBP e ESP lidam com dados, enquanto o EIP lida com código.
mmmmmmmm
2
No seu gráfico, ebp (geralmente) é o "ponteiro do quadro", especialmente o "ponteiro da pilha". Isso permite acessar locais por meio de [ebp-x] e parâmetros de pilha por [ebp + x] de forma consistente, independentemente do ponteiro da pilha (que frequentemente muda em uma função). O endereçamento pode ser feito por meio do ESP, liberando o EBP para outras operações - mas, dessa forma, os depuradores não podem dizer a pilha de chamadas ou os valores dos locais.
Peterchen
4
@Ben. Não necessariamente. Alguns compiladores colocam quadros de pilha na pilha. O conceito de pilha crescente é exatamente isso, um conceito que facilita a compreensão. A implementação da pilha pode ser qualquer coisa (o uso de pedaços aleatórios do heap torna os hacks que substituem partes da pilha muito mais difíceis, pois não são tão determinísticos).
Martin York
1
em duas palavras: o ponteiro da pilha permite que as operações push / pop funcionem (então push e pop sabem onde colocar / obter dados). O ponteiro base permite que o código faça referência independente aos dados que foram enviados anteriormente na pilha.
usar o seguinte código

Respostas:

229

esp é como você diz que é, o topo da pilha.

ebpé geralmente definido como espno início da função. Os parâmetros de função e variáveis ​​locais são acessados ​​adicionando e subtraindo, respectivamente, um deslocamento constante de ebp. Todas as convenções de chamada x86 definem ebpcomo sendo preservadas nas chamadas de função. ebpem si, na verdade, aponta para o ponteiro base do quadro anterior, o que permite caminhar pela pilha em um depurador e exibir outras variáveis ​​locais dos quadros para trabalhar.

A maioria dos prólogos de funções se parece com:

push ebp      ; Preserve current frame pointer
mov ebp, esp  ; Create new frame pointer pointing to current stack top
sub esp, 20   ; allocate 20 bytes worth of locals on stack.

Mais tarde, na função, você pode ter um código semelhante (presumindo que as duas variáveis ​​locais sejam 4 bytes)

mov [ebp-4], eax    ; Store eax in first local
mov ebx, [ebp - 8]  ; Load ebx from second local

A otimização de omissão de ponteiro de quadro ou FPO que você pode ativar realmente elimina isso e usa ebpcomo outro registro e acessa locais diretamenteesp , mas isso torna a depuração um pouco mais difícil, pois o depurador não pode mais acessar diretamente os quadros de pilha de chamadas de função anteriores.

EDITAR:

Para sua pergunta atualizada, as duas entradas ausentes na pilha são:

var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
*savedFramePointer = dword ptr 0*
*return address = dword ptr 4*
hInstance = dword ptr  8h
PrevInstance = dword ptr  0C
hlpCmdLine = dword ptr  10h
nShowCmd = dword ptr  14h

Isso ocorre porque o fluxo da chamada de função é:

  • Parâmetros push (hInstance , etc.)
  • Função de chamada, que pressiona o endereço de retorno
  • Empurrar ebp
  • Alocar espaço para os habitantes locais
Michael
fonte
1
Obrigada pelo esclarecimento! Mas agora estou meio confuso. Vamos assumir que eu chamo uma função e estou na primeira linha do seu prólogo, ainda sem ter executado uma única linha a partir dela. Nesse ponto, qual é o valor do ebp? A pilha tem algo nesse momento além dos argumentos enviados? Obrigado!
elysium devoradas
3
O EBP não é alterado magicamente; portanto, até que você estabeleça um novo EBP para sua função, você ainda terá o valor de quem chama. E além do mais argumentos, a pilha também irá realizar o EIP de idade (endereço de retorno)
MSalters
3
Boa resposta. Embora não possa ser completo sem mencionar o que está no epílogo: instruções "leave" e "ret".
Calmarius
2
Penso que esta imagem ajudará a esclarecer algumas coisas sobre qual é o fluxo. Lembre-se também de que a pilha cresce para baixo. ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre
Sou eu ou todos os sinais de menos estão faltando no snippet de código acima?
precisa saber é o seguinte
96

ESP é o ponteiro atual da pilha, que será alterado sempre que uma palavra ou endereço for pressionado ou ativado / desativado na pilha. EBP é uma maneira mais conveniente para o compilador acompanhar os parâmetros de uma função e variáveis ​​locais do que usar o ESP diretamente.

Geralmente (e isso pode variar de compilador para compilador), todos os argumentos para uma função que está sendo chamada são empurrados para a pilha pela função de chamada (geralmente na ordem inversa em que são declarados no protótipo da função, mas isso varia) . Em seguida, a função é chamada, que envia o endereço de retorno (EIP) para a pilha.

Após a entrada na função, o antigo valor de EBP é empurrado para a pilha e o EBP é definido como o valor de ESP. Em seguida, o ESP é decrementado (porque a pilha cresce para baixo na memória) para alocar espaço para as variáveis ​​locais e temporários da função. A partir desse momento, durante a execução da função, os argumentos para a função estão localizados na pilha com compensações positivas do EBP (porque foram pressionados antes da chamada da função) e as variáveis ​​locais estão localizadas com compensações negativas do EBP (porque eles foram alocados na pilha após a entrada da função). É por isso que o EBP é chamado de ponteiro de quadro , porque aponta para o centro do quadro de chamada de função .

Ao sair, tudo o que a função precisa fazer é definir ESP para o valor de EBP (que desaloca as variáveis ​​locais da pilha e expõe a entrada EBP na parte superior da pilha) e, em seguida, exibe o antigo valor de EBP da pilha, e a função retorna (inserindo o endereço de retorno no EIP).

Ao retornar para a função de chamada, ele pode incrementar o ESP para remover os argumentos da função que colocou na pilha antes de chamar a outra função. Neste ponto, a pilha está de volta no mesmo estado em que estava antes de chamar a função chamada.

David R Tribble
fonte
15

Você está certo. O ponteiro da pilha aponta para o item superior da pilha e o ponteiro base aponta para o topo "anterior" da pilha antes da chamada da função.

Quando você chama uma função, qualquer variável local será armazenada na pilha e o ponteiro da pilha será incrementado. Quando você retorna da função, todas as variáveis ​​locais na pilha ficam fora do escopo. Você faz isso configurando o ponteiro da pilha de volta no ponteiro base (que era o topo "anterior" antes da chamada da função).

Fazer a alocação de memória dessa maneira é muito , muito rápida e eficiente.

Robert Cartaino
fonte
14
@ Robert: Quando você diz "anterior" no topo da pilha antes da função ser chamada, você está ignorando os dois parâmetros, que são empurrados para a pilha imediatamente antes de chamar a função e o chamador EIP. Isso pode confundir os leitores. Digamos que, em um quadro de pilha padrão, o EBP aponte para o mesmo local em que o ESP apontou logo após a entrada na função.
wigy
7

EDIT: Para uma descrição melhor, consulte Desmontagem / funções x86 e quadros de pilha em um WikiBook sobre montagem x86. Eu tento adicionar algumas informações que você possa estar interessado em usar o Visual Studio.

O armazenamento do EBP do chamador como a primeira variável local é chamado de quadro de pilha padrão e isso pode ser usado para quase todas as convenções de chamada no Windows. Existem diferenças se o chamador ou o destinatário desaloca os parâmetros passados ​​e quais parâmetros são passados ​​nos registradores, mas estes são ortogonais ao problema do quadro de pilha padrão.

Falando sobre programas do Windows, você provavelmente pode usar o Visual Studio para compilar seu código C ++. Esteja ciente de que a Microsoft usa uma otimização chamada Frame Pointer Omission, que torna quase impossível caminhar pela pilha sem usar a biblioteca dbghlp e o arquivo PDB para o executável.

Essa omissão de ponteiro de quadro significa que o compilador não armazena o EBP antigo em um local padrão e usa o registro EBP para outra coisa; portanto, é difícil encontrar o EIP do chamador sem saber quanto espaço as variáveis ​​locais precisam para uma determinada função. É claro que a Microsoft fornece uma API que permite fazer caminhadas pela pilha, mesmo nesse caso, mas pesquisar o banco de dados da tabela de símbolos nos arquivos PDB leva muito tempo para alguns casos de uso.

Para evitar o FPO em suas unidades de compilação, você deve evitar usar / O2 ou adicionar explicitamente / Oy- aos sinalizadores de compilação C ++ em seus projetos. Provavelmente, você vincula o tempo de execução C ou C ++, que usa o FPO na configuração da versão, portanto, será difícil fazer caminhadas pela pilha sem o dbghlp.dll.

peruca
fonte
Não entendo como o EIP é armazenado na pilha. Não deveria ser um registro? Como um registro pode estar na pilha? Obrigado!
elysium devoradas
O EIP do chamador é empurrado para a pilha pela própria instrução CALL. A instrução RET apenas busca o topo da pilha e a coloca no EIP. Se você tiver excedentes de buffer, esse fato poderá ser usado para acessar o código do usuário a partir de um encadeamento privilegiado.
Wigy 09/09/2009
@devouredelysium O conteúdo (ou valor ) do registro EIP é colocado (ou copiado) na pilha, não no próprio registro.
precisa saber é o seguinte
@BarbaraKwarc Obrigado pela entrada com valor agregado. Não pude ver o que o OP estava faltando na minha resposta. De fato, os registros permanecem onde estão, apenas seu valor é enviado para a RAM a partir da CPU. No modo amd64, isso fica um pouco mais complexo, mas deixe isso para outra pergunta.
Wigy
E esse amd64? Estou curioso.
precisa saber é o seguinte
6

Primeiro, o ponteiro da pilha aponta para a parte inferior da pilha, pois as pilhas x86 são construídas de valores altos de endereço para valores mais baixos de endereço. O ponteiro da pilha é o ponto em que a próxima chamada a ser pressionada (ou chamada) colocará o próximo valor. Sua operação é equivalente à instrução C / C ++:

 // push eax
 --*esp = eax
 // pop eax
 eax = *esp++;

 // a function call, in this case, the caller must clean up the function parameters
 move eax,some value
 push eax
 call some address  // this pushes the next value of the instruction pointer onto the
                    // stack and changes the instruction pointer to "some address"
 add esp,4 // remove eax from the stack

 // a function
 push ebp // save the old stack frame
 move ebp, esp
 ... // do stuff
 pop ebp  // restore the old stack frame
 ret

O ponteiro da base está no topo do quadro atual. ebp geralmente aponta para o seu endereço de retorno. ebp + 4 aponta para o primeiro parâmetro de sua função (ou o valor deste método de classe). ebp-4 aponta para a primeira variável local de sua função, geralmente o valor antigo de ebp, para que você possa restaurar o ponteiro de quadro anterior.

jmucchiello
fonte
2
Não, o ESP não aponta para o final da pilha. O esquema de endereçamento de memória não tem nada a ver com isso. Não importa se a pilha cresce para endereços inferiores ou superiores. O "topo" da pilha é sempre onde o próximo valor será empurrado (colocado no topo da pilha) ou, em outras arquiteturas, onde o último valor empurrado foi colocado e onde está atualmente. Portanto, o ESP sempre aponta para o topo da pilha.
BarbaraKwarc
1
A parte inferior ou base da pilha, por outro lado, é onde o primeiro (ou mais antigo ) valor foi colocado e depois coberto por valores mais recentes. É daí que o nome "ponteiro base" para EBP veio: ele deveria apontar para a base (ou parte inferior) da pilha local atual de uma sub-rotina.
BarbaraKwarc
Barbara, no Intel x86, a pilha está de cabeça para baixo. A parte superior da pilha contém o primeiro item empurrado para a pilha e cada item depois é empurrado ABAIXO do item superior. A parte inferior da pilha é onde novos itens são colocados. Os programas são colocados na memória a partir de 1k e crescem até o infinito. A pilha começa no infinito, realisticamente no máximo, menos ROMs, e cresce em direção a 0. ESP aponta para um endereço cujo valor é menor que o primeiro endereço enviado.
jmucchiello
1

Muito tempo desde que eu fiz a programação de montagem, mas esse link pode ser útil ...

O processador possui uma coleção de registros que são usados ​​para armazenar dados. Alguns desses são valores diretos, enquanto outros estão apontando para uma área dentro da RAM. Os registros tendem a ser usados ​​para determinadas ações específicas e todo operando em assembly exigirá uma certa quantidade de dados em registros específicos.

O ponteiro da pilha é usado principalmente quando você está chamando outros procedimentos. Com os compiladores modernos, um monte de dados será descartado primeiro na pilha, seguido pelo endereço de retorno, para que o sistema saiba para onde retornar quando for solicitado que retorne. O ponteiro da pilha apontará para o próximo local onde novos dados podem ser enviados para a pilha, onde permanecerão até que sejam exibidos novamente.

Registradores de base ou de segmento apontam apenas para o espaço de endereço de uma grande quantidade de dados. Combinado com um segundo registrador, o ponteiro Base dividirá a memória em grandes blocos enquanto o segundo registrador apontará para um item dentro desse bloco. Os ponteiros de base apontam para a base de blocos de dados.

Lembre-se de que o Assembly é muito específico da CPU. A página à qual vinculei fornece informações sobre os diferentes tipos de CPU.

Wim ten Brink
fonte
Os registradores de segmento são separados no x86 - são gs, cs, ss e, a menos que você esteja escrevendo um software de gerenciamento de memória, nunca os toca.
Michael Michael
ds também é um registro de segmento e, nos dias de código do MS-DOS e de 16 bits, você definitivamente precisava alterar esses registros de segmento ocasionalmente, pois eles nunca podiam apontar para mais de 64 KB de RAM. No entanto, o DOS podia acessar memória de até 1 MB porque usava ponteiros de endereço de 20 bits. Mais tarde, obtivemos sistemas de 32 bits, alguns com registradores de endereço de 36 bits e agora registradores de 64 bits. Hoje em dia você não precisará mais alterar esses registros de segmento.
Wim ten Brink
No sistema operacional moderno usa 386 segmentos
Ana Betts
@Paul: ERRADO! ERRADO! ERRADO! Os segmentos de 16 bits são substituídos por segmentos de 32 bits. No modo protegido, isso permite a virtualização da memória, basicamente permitindo que o processador mapeie endereços físicos para endereços lógicos. No entanto, dentro do seu aplicativo, as coisas ainda parecem estáveis, pois o sistema operacional virtualizou a memória para você. O kernel opera no modo protegido, permitindo que os aplicativos sejam executados em um modelo de memória plana. Veja também en.wikipedia.org/wiki/Protected_mode
Wim ten Brink
@Workshop ALex: Isso é um detalhe técnico. Todos os sistemas operacionais modernos definem todos os segmentos para [0, FFFFFFFF]. Isso realmente não conta. E se você ler a página vinculada, verá que todo o material sofisticado é feito com páginas, que são muito mais refinadas que os segmentos.
MSalters 9/09/09
-4

Editar Sim, isso está errado. Descreve algo completamente diferente caso alguém esteja interessado :)

Sim, o ponteiro da pilha aponta para o topo da pilha (seja esse o primeiro local de pilha vazio ou o último em que não tenho certeza). O ponteiro base aponta para o local da memória da instrução que está sendo executada. Isso está no nível dos códigos de operação - a instrução mais básica que você pode obter em um computador. Cada código de operação e seus parâmetros são armazenados em um local de memória. Uma linha C ou C ++ ou C # pode ser convertida em um código de operação ou em uma sequência de duas ou mais, dependendo da complexidade. Estes são gravados na memória do programa sequencialmente e executados. Em circunstâncias normais, o ponteiro base é incrementado em uma instrução. Para controle de programa (GOTO, IF, etc), pode ser incrementado várias vezes ou apenas substituído pelo próximo endereço de memória.

Nesse contexto, as funções são armazenadas na memória do programa em um determinado endereço. Quando a função é chamada, certas informações são pressionadas na pilha que permite ao programa descobrir que estava de volta para onde a função foi chamada, bem como os parâmetros para a função, e o endereço da função na memória do programa é empurrado para o diretório ponteiro base. No próximo ciclo do relógio, o computador começa a executar instruções a partir desse endereço de memória. Então, em algum momento, ele retornará ao local da memória APÓS a instrução que chamou a função e continuará a partir daí.

Stephen Friederichs
fonte
Estou com um pouco de dificuldade para entender o que é o fluxo. Se tivermos 10 linhas de código MASM, isso significa que, à medida que executamos essas linhas, o ebp estará sempre aumentando?
elysium devoradas
1
@ Devoured - Não. Isso não é verdade. eip estará aumentando.
Michael
Você quer dizer que o que eu disse é certo, mas não para EBP, mas para IEP, é isso?
elysium devoradas
2
Sim. O EIP é o ponteiro da instrução e é implicitamente modificado após a execução de cada instrução.
Michael
2
Oooh meu mal. Estou pensando em um ponteiro diferente. Acho que vou lavar meu cérebro.
Stephen Friederichs
-8

esp significa "Extended Stack Pointer" ... ebp para "Something Base Pointer" .... eip para "Something Instruction Pointer" ...... O ponteiro da pilha aponta para o endereço de deslocamento do segmento da pilha . O ponteiro base aponta para o endereço de deslocamento do segmento extra. O ponteiro de instruções aponta para o endereço de deslocamento do segmento de código. Agora, sobre os segmentos ... são pequenas divisões de 64 KB da área de memória dos processadores ..... Esse processo é conhecido como Segmentação de Memória. Espero que este post tenha sido útil.

Adarsha Kharel
fonte
3
Esta é uma pergunta antiga, no entanto, sp significa ponteiro de pilha, bp significa ponteiro de base e ip como ponteiro de instrução. O e no começo de todo mundo está apenas dizendo que é um ponteiro de 32 bits.
Hyden
1
A segmentação é irrelevante aqui.
precisa saber é o seguinte