Qual é a finalidade do registro de ponteiro de quadro EBP?

95

Sou um iniciante em linguagem assembly e percebi que o código x86 emitido por compiladores geralmente mantém o ponteiro do frame por perto, mesmo no modo de liberação / otimizado, quando poderia usar o EBPregistro para outra coisa.

Eu entendo por que o ponteiro do frame pode tornar o código mais fácil de depurar e pode ser necessário se alloca()for chamado dentro de uma função. No entanto, o x86 tem poucos registradores e usar dois deles para manter a localização do quadro de pilha quando um seria suficiente simplesmente não faz sentido para mim. Por que omitir o ponteiro do frame é considerado uma má ideia, mesmo em compilações otimizadas / liberadas?

dsimcha
fonte
19
Se você acha que x86 tem poucos registros, verifique 6502 :)
Sedat Kapanoglu
1
O C99 VLA também pode se beneficiar com isso.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
2
stackoverflow.com/questions/1395591/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
O ponteiro do frame não torna o ponteiro da pilha redundante? . TL; DR: 1. alinhamento de pilha não trivial 2. alocação de pilha ( alloca) 3. facilidade de implementação de tempo de execução: exceto manipulação, sandbox, GC
Alexander Malakhov

Respostas:

102

O ponteiro de quadro é um ponteiro de referência que permite a um depurador saber onde está a variável local ou um argumento com um único deslocamento constante. Embora o valor do ESP mude ao longo da execução, o EBP permanece o mesmo, tornando possível alcançar a mesma variável no mesmo deslocamento (como o primeiro parâmetro sempre estará em EBP + 8 enquanto os deslocamentos do ESP podem mudar significativamente, já que você estará pressionando / estourando coisas)

Por que os compiladores não descartam o ponteiro do frame? Porque com o ponteiro de quadro, o depurador pode descobrir onde as variáveis ​​locais e os argumentos estão usando a tabela de símbolos, uma vez que eles têm a garantia de estar em um deslocamento constante para EBP. Caso contrário, não há uma maneira fácil de descobrir onde uma variável local está em qualquer ponto do código.

Como Greg mencionou, ele também ajuda a desenrolar a pilha para um depurador, já que o EBP fornece uma lista vinculada reversa de quadros de pilha, portanto, permitindo que o depurador descubra o tamanho do quadro de pilha (variáveis ​​locais + argumentos) da função.

A maioria dos compiladores fornece uma opção para omitir ponteiros de quadro, embora isso torne a depuração muito difícil. Essa opção nunca deve ser usada globalmente, mesmo em código de lançamento. Você não sabe quando precisará depurar a falha de um usuário.

Sedat Kapanoglu
fonte
10
O compilador provavelmente sabe o que faz com o ESP. Os outros pontos são válidos, porém, +1
erikkallen
8
Os depuradores modernos podem fazer backtraces de pilha, mesmo em código compilado com -fomit-frame-pointer. Essa configuração é o padrão no gcc recente.
Peter Cordes,
2
@SedatKapanoglu: uma seção de dados registra as informações necessárias: yosefk.com/blog/…
Peter Cordes,
3
@SedatKapanoglu: a .eh_frame_hdrseção também é usada para exceções de tempo de execução. Você o encontrará (com objdump -h) na maioria dos binários em um sistema Linux, é cerca de 16k para /bin/bash, vs. 572B para GNU /bin/true, 108k para ffmpeg. Há uma opção do gcc para desativar a geração, mas é uma seção de dados "normal", não uma seção de depuração que stripremove por padrão. Caso contrário, você não poderia retroceder por meio de uma função de biblioteca que não tivesse símbolos de depuração. Essa seção pode ser maior do que as push/mov/popinstruções que substitui, mas tem custo de tempo de execução quase zero (por exemplo, cache uop).
Peter Cordes,
3
Quanto a "tal como o primeiro parâmetro estará sempre em EBP-4": Não é o primeiro parâmetro em EBP + 8 (em x86)?
Aydin K.
31

Apenas adicionando meus dois centavos a respostas já boas.

É parte de uma boa arquitetura de linguagem ter uma cadeia de frames de pilha. O BP aponta para o quadro atual, onde as variáveis ​​locais da sub-rotina são armazenadas. (Os locais estão em deslocamentos negativos e os argumentos estão em deslocamentos positivos.)

A ideia de que está impedindo que um registro perfeitamente bom seja usado na otimização levanta a questão: quando e onde a otimização realmente vale a pena?

A otimização só vale a pena em loops estreitos que 1) não chamam funções, 2) onde o contador do programa gasta uma fração significativa de seu tempo e 3) no código que o compilador realmente verá (ou seja, funções não pertencentes à biblioteca). Geralmente, essa é uma fração muito pequena do código geral, especialmente em sistemas grandes.

Outro código pode ser torcido e comprimido para se livrar dos ciclos, e isso simplesmente não importa, porque o contador do programa praticamente nunca está lá.

Eu sei que você não perguntou isso, mas na minha experiência, 99% dos problemas de desempenho não têm nada a ver com a otimização do compilador. Eles têm tudo a ver com design exagerado.

Mike Dunlavey
fonte
Obrigado @Mike, achei sua resposta muito útil.
sixtyfootersdude
2
Acabar com o ponteiro de quadro também economiza algumas instruções a cada chamada de função, o que é uma pequena otimização por si só. BTW, seu uso de "implora a questão" está incorreto; você quer dizer "levanta a questão".
augurar
@augurar: Corrigido. Obrigado. Eu mesmo sou meio
mal-
3
@augurar A linguagem evolui: "levanta a questão" agora significa apenas "levanta a questão". Ser um detalhista prescritivista para uso desatualizado não acrescenta nada.
user3364825
9

Depende do compilador, certamente. Já vi código otimizado emitido por compiladores x86 que usa livremente o registro EBP como um registro de propósito geral. (Não me lembro com qual compilador notei isso, no entanto.)

Os compiladores também podem optar por manter o registro EBP para auxiliar no desenrolar da pilha durante o tratamento de exceções, mas novamente isso depende da implementação precisa do compilador.

Greg Hewgill
fonte
A maioria dos compiladores -fomit-frame-pointerassume quando a otimização está habilitada. (quando a ABI permite). GCC, clang, ICC e MSVC fazem isso, IIRC, mesmo quando direcionados ao Windows de 32 bits. Sim, minha resposta em Por que é melhor usar o ebp do que o registrador esp para localizar parâmetros na pilha? mostra que mesmo o Windows de 32 bits pode omitir o ponteiro do quadro. O Linux x86 de 32 bits definitivamente pode e faz. E, claro, os ABIs de 64 bits permitiram a omissão do ponteiro do quadro desde o início.
Peter Cordes
4

No entanto, x86 tem poucos registros

Isso é verdade apenas no sentido de que os opcodes podem endereçar apenas 8 registradores. O próprio processador terá, na verdade, muito mais registros do que isso e usará renomeação de registro, pipelining, execução especulativa e outros chavões do processador para contornar esse limite. A Wikipedia tem um bom parágrafo introdutório sobre o que um processador x86 pode fazer para superar o limite de registro: http://en.wikipedia.org/wiki/X86#Current_implementations .

MSN
fonte
1
A questão original é sobre o código gerado, que é estritamente limitado aos registros referenciáveis ​​por opcodes.
Darron
1
Sim, mas é por isso que omitir o ponteiro do frame em compilações otimizadas não é tão importante hoje em dia.
Michael
1
Entretanto, renomear registradores não é exatamente a mesma coisa que ter um grande número de registradores disponíveis. Ainda existem muitas situações em que a renomeação de registro não ajuda, mas registros mais "regulares" ajudariam.
jalf
1

O uso de stack frames tornou-se incrivelmente barato em qualquer hardware, mesmo remotamente moderno. Se você tiver frames de pilha baratos, salvar alguns registros não é tão importante. Tenho certeza de que frames de pilha rápidos versus mais registradores foi uma troca de engenharia e os frames de pilha rápidos venceram.

Quanto você está economizando com o registro puro? Vale a pena?

dwc
fonte
Mais registros são limitados pela codificação da instrução. x86-64 usa bits no byte de prefixo REX para estender a parte que especifica o registro das instruções de 3 para 4 bits para os registros src e dest. Se houvesse espaço, o x86-64 provavelmente teria ido para 32 registros de arquitetura, embora salvar / restaurar tantos nas opções de contexto comece a somar. 15 é um grande avanço em relação ao 7, mas 31 é uma melhoria muito menor na maioria dos casos. (sem contar o ponteiro da pilha como de propósito geral.) Tornar push / pop rápido é ótimo para mais do que apenas frames de pilha. Não é uma troca com # de regs, no entanto.
Peter Cordes,