¿Cuál es el propósito del registro de puntero de trama EBP?


94

Soy un principiante en lenguaje ensamblador y he notado que el código x86 emitido por los compiladores generalmente mantiene el puntero del marco incluso en modo de lanzamiento / optimizado cuando podría usar el EBPregistro para otra cosa.

Entiendo por qué el puntero del marco puede hacer que el código sea más fácil de depurar, y podría ser necesario si alloca()se llama dentro de una función. Sin embargo, x86 tiene muy pocos registros y usar dos de ellos para mantener la ubicación del marco de la pila cuando uno sería suficiente simplemente no tiene sentido para mí. ¿Por qué omitir el puntero del marco se considera una mala idea incluso en compilaciones optimizadas / de lanzamiento?


19
Si cree que x86 tiene muy pocos registros, debe verificar 6502 :)
Sedat Kapanoglu


1
C99 VLA también puede beneficiarse de él.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功


1
¿No hace el puntero de marco redundante el puntero de pila? . TL; DR: 1. alineación de pila no trivial 2. asignación de pila ( alloca) 3. facilidad de implementación en tiempo de ejecución: manejo de exceptoins, sandbox, GC
Alexander Malakhov

Respuestas:


102

El puntero de marco es un puntero de referencia que permite a un depurador saber dónde está la variable local o un argumento con un único desplazamiento constante. Aunque el valor de ESP cambia durante el transcurso de la ejecución, EBP sigue siendo el mismo, lo que hace posible alcanzar la misma variable en el mismo desplazamiento (como el primer parámetro siempre estará en EBP + 8, mientras que los desplazamientos de ESP pueden cambiar significativamente ya que estará presionando / haciendo estallar cosas)

¿Por qué los compiladores no desechan el puntero de marco? Porque con el puntero de marco, el depurador puede averiguar dónde están las variables y los argumentos locales usando la tabla de símbolos, ya que se garantiza que estarán en un desplazamiento constante de EBP. De lo contrario, no hay una manera fácil de averiguar dónde está una variable local en cualquier punto del código.

Como mencionó Greg, también ayuda a desenrollar la pila para un depurador, ya que EBP proporciona una lista vinculada inversa de marcos de pila, lo que permite que el depurador determine el tamaño del marco de pila (variables locales + argumentos) de la función.

La mayoría de los compiladores ofrecen una opción para omitir los punteros a los marcos, aunque dificulta mucho la depuración. Esa opción nunca debe usarse globalmente, incluso en el código de lanzamiento. No sabe cuándo tendrá que depurar el bloqueo de un usuario.


10
El compilador probablemente sepa lo que le hace a ESP. Los otros puntos son válidos, sin embargo, +1
erikkallen

8
Los depuradores modernos pueden hacer retrocesos de pila incluso en código compilado con -fomit-frame-pointer. Esa configuración es la predeterminada en gcc reciente.
Peter Cordes

2
@SedatKapanoglu: Una sección de datos registra la información necesaria: yosefk.com/blog/…
Peter Cordes

3
@SedatKapanoglu: la .eh_frame_hdrsección también se usa para excepciones en tiempo de ejecución. Lo encontrará (con objdump -h) en la mayoría de los binarios en un sistema Linux, tiene aproximadamente 16k para /bin/bash, frente a 572B para GNU /bin/true, 108k para ffmpeg. Hay una opción de gcc para deshabilitar su generación, pero es una sección de datos "normal", no una sección de depuración que se stripelimina por defecto. De lo contrario, no podría rastrear una función de biblioteca que no tuviera símbolos de depuración. Esa sección puede ser más grande que las push/mov/popinstrucciones que reemplaza, pero tiene un costo de tiempo de ejecución cercano a cero (por ejemplo, uop cache).
Peter Cordes

3
Con respecto a "como el primer parámetro siempre estará en EBP-4": ¿No es el primer parámetro en EBP + 8 (en x86)?
Aydin K.

31

Solo agrego mis dos centavos a las ya buenas respuestas.

Es parte de una buena arquitectura de lenguaje tener una cadena de marcos de pila. El BP apunta al marco actual, donde se almacenan las variables locales de subrutina. (Los locales tienen compensaciones negativas y los argumentos tienen compensaciones positivas).

La idea de que impide que se utilice un registro perfectamente bueno en la optimización plantea la pregunta: ¿cuándo y dónde vale la pena la optimización?

La optimización solo vale la pena en bucles estrechos que 1) no llaman a funciones, 2) donde el contador del programa pasa una fracción significativa de su tiempo, y 3) en el código que el compilador realmente verá (es decir, funciones que no son de biblioteca). Suele ser una fracción muy pequeña del código general, especialmente en sistemas grandes.

Otro código puede retorcerse y exprimirse para deshacerse de los ciclos, y simplemente no importará, porque el contador del programa prácticamente nunca está allí.

Sé que no preguntaste esto, pero en mi experiencia, el 99% de los problemas de rendimiento no tienen nada que ver con la optimización del compilador. Tienen mucho que ver con el diseño excesivo.


Gracias @ Mike, tu respuesta me resultó muy útil.
sixtyfootersdude

2
Eliminar el puntero del marco también le ahorra un par de instrucciones en cada llamada de función, que es una pequeña optimización por sí sola. Por cierto, su uso de "plantea la pregunta" es incorrecto; quieres decir "plantea la cuestión".
augurar

@augurar: Fijo. Gracias. Yo mismo soy un poco gruñón gramatical :)
Mike Dunlavey

3
@augurar El lenguaje evoluciona: "plantea la pregunta" ahora solo significa "plantea la pregunta". Ser un quisquilloso prescriptivista para el uso desactualizado no agrega nada.
user3364825

9

Depende del compilador, ciertamente. He visto código optimizado emitido por compiladores x86 que utilizan libremente el registro EBP como un registro de propósito general. (Sin embargo, no recuerdo con qué compilador noté eso).

Los compiladores también pueden optar por mantener el registro EBP para ayudar a desenrollar la pila durante el manejo de excepciones, pero nuevamente esto depende de la implementación precisa del compilador.


La mayoría de los compiladores utilizan de forma predeterminada -fomit-frame-pointercuando la optimización está habilitada. (cuando la ABI lo permite). GCC, clang, ICC y MSVC lo hacen, IIRC, incluso cuando apuntan a Windows de 32 bits. Sí, mi respuesta a ¿Por qué es mejor usar el registro ebp que el esp para ubicar parámetros en la pila? muestra que incluso Windows de 32 bits puede omitir el puntero del marco. Linux x86 de 32 bits definitivamente puede y lo hace. Y, por supuesto, las ABI de 64 bits han permitido la omisión del puntero de trama desde el principio.
Peter Cordes

4

Sin embargo, x86 tiene muy pocos registros

Esto es cierto solo en el sentido de que los códigos de operación solo pueden abordar 8 registros. El procesador en sí tendrá muchos más registros que eso y utilizará el cambio de nombre de los registros, la canalización, la ejecución especulativa y otras palabras de moda del procesador para sortear ese límite. Wikipedia tiene un buen párrafo introductorio sobre lo que puede hacer un procesador x86 para superar el límite de registro: http://en.wikipedia.org/wiki/X86#Current_implementations .


1
La pregunta original es sobre el código generado, que está estrictamente limitado a los registros referenciables por códigos de operación.
Darron

1
Sí, pero esta es la razón por la que omitir el puntero del marco en compilaciones optimizadas no es tan importante hoy en día.
Michael

1
Sin embargo, cambiar el nombre de los registros no es lo mismo que tener una mayor cantidad de registros disponibles. Todavía hay muchas situaciones en las que el cambio de nombre de registros no ayudará, pero los registros más "regulares" sí lo harían.
jalf

1

El uso de marcos de pila se ha vuelto increíblemente barato en cualquier hardware, incluso remotamente moderno. Si tiene marcos de pila baratos, guardar un par de registros no es tan importante. Estoy seguro de que los marcos de pila rápidos frente a más registros fue una compensación de ingeniería, y ganaron los marcos de pila rápidos.

¿Cuánto estás ahorrando al registrarte puro? ¿Vale la pena?


Más registros está limitado por la codificación de instrucciones. x86-64 usa bits en el byte de prefijo REX para extender la parte de las instrucciones que especifica el registro de 3 a 4 bits para los registros src y dest. Si hubiera espacio, x86-64 probablemente habría ido a 32 registros arquitectónicos, aunque guardar / restaurar esa cantidad en los cambios de contexto comienza a sumarse. 15 es un gran paso desde 7, pero 31 es una mejora mucho menor en la mayoría de los casos. (sin contar el puntero de pila como de uso general). Hacer push / pop rápido es genial para algo más que marcos de pila. Sin embargo, no es una compensación con el número de reglas.
Peter Cordes
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.