assembly - programacion - ¿Algún lenguaje/compilador utiliza la instrucción x86 ENTER con un nivel de anidación distinto de cero?
lenguaje de programacion (4)
Aquellos que estén familiarizados con la programación de ensamblaje x86 están muy acostumbrados al típico prólogo / epílogo de funciones:
push ebp
mov esp, ebp
sub esp, [size of local variables]
...
mov esp, ebp
pop ebp
ret
Esta misma secuencia de código también se puede implementar con las instrucciones ENTER
y LEAVE
:
enter [size of local variables], 0
...
leave
ret
El segundo operando de la instrucción ENTER
es el nivel de anidamiento , que permite el acceso a múltiples marcos principales desde la función llamada.
Esto no se usa en C porque no hay funciones anidadas; las variables locales solo tienen el alcance de la función en la que están declaradas. Este constructo no existe (aunque a veces desearía que fuera así):
void func_a(void)
{
int a1 = 7;
void func_b(void)
{
printf("a1 = %d/n", a1); /* a1 inherited from func_a() */
}
func_b();
}
Python, sin embargo , tiene funciones anidadas que se comportan de esta manera:
def func_a():
a1 = 7
def func_b():
print ''a1 = %d'' % a1 # a1 inherited from func_a()
func_b()
Por supuesto, el código de Python no se traduce directamente al código máquina x86, y por lo tanto no podría (¿no es probable?) Aprovechar esta instrucción.
¿Hay algún lenguaje que compile x86 y proporcione funciones anidadas? ¿Hay compiladores que emitirán una instrucción ENTER
con un segundo operando distinto de cero?
Intel invirtió una cantidad de tiempo / dinero distinta de cero en ese operando de nivel de anidación, y básicamente estoy curioso si alguien lo usa :-)
Referencias
Como Iwillnotexist Idonotexist señaló , GCC admite funciones anidadas en C, utilizando la sintaxis exacta que he mostrado anteriormente.
Sin embargo, no usa la instrucción ENTER
. En cambio, las variables que se usan en funciones anidadas se agrupan en el área de variables locales, y un puntero a este grupo se pasa a la función anidada. Curiosamente, este "puntero a variables primarias" se pasa a través de un mecanismo no estándar: en x64 se pasa en r10
, y en x86 (cdecl) se pasa en ecx
, que está reservado para this
puntero en C ++ (que no admitir funciones anidadas de todos modos).
#include <stdio.h>
void func_a(void)
{
int a1 = 0x1001;
int a2=2, a3=3, a4=4;
int a5 = 0x1005;
void func_b(int p1, int p2)
{
/* Use variables from func_a() */
printf("a1=%d a5=%d/n", a1, a5);
}
func_b(1, 2);
}
int main(void)
{
func_a();
return 0;
}
Produce el siguiente código (fragmento de código) cuando se compila para 64 bits:
00000000004004dc <func_b.2172>:
4004dc: push rbp
4004dd: mov rbp,rsp
4004e0: sub rsp,0x10
4004e4: mov DWORD PTR [rbp-0x4],edi
4004e7: mov DWORD PTR [rbp-0x8],esi
4004ea: mov rax,r10 ; ptr to calling function "shared" vars
4004ed: mov ecx,DWORD PTR [rax+0x4]
4004f0: mov eax,DWORD PTR [rax]
4004f2: mov edx,eax
4004f4: mov esi,ecx
4004f6: mov edi,0x400610
4004fb: mov eax,0x0
400500: call 4003b0 <printf@plt>
400505: leave
400506: ret
0000000000400507 <func_a>:
400507: push rbp
400508: mov rbp,rsp
40050b: sub rsp,0x20
40050f: mov DWORD PTR [rbp-0x1c],0x1001
400516: mov DWORD PTR [rbp-0x4],0x2
40051d: mov DWORD PTR [rbp-0x8],0x3
400524: mov DWORD PTR [rbp-0xc],0x4
40052b: mov DWORD PTR [rbp-0x20],0x1005
400532: lea rax,[rbp-0x20] ; Pass a, b to the nested function
400536: mov r10,rax ; in r10 !
400539: mov esi,0x2
40053e: mov edi,0x1
400543: call 4004dc <func_b.2172>
400548: leave
400549: ret
Salida de objdump --no-show-raw-insn -d -Mintel
Esto sería equivalente a algo más detallado como este:
struct func_a_ctx
{
int a1, a5;
};
void func_b(struct func_a_ctx *ctx, int p1, int p2)
{
/* Use variables from func_a() */
printf("a1=%d a5=%d/n", ctx->a1, ctx->a5);
}
void func_a(void)
{
int a2=2, a3=3, a4=4;
struct func_a_ctx ctx = {
.a1 = 0x1001,
.a5 = 0x1005,
};
func_b(&ctx, 1, 2);
}
Nuestro compilador PARLANSE (para programas paralelos de grano fino en SMP x86) tiene un alcance léxico.
PARLANSE intenta generar muchos, muchos pequeños granos paralelos de computación, y luego los multiplexa en la parte superior de los hilos (1 por CPU). De hecho, los marcos de pila se asignan en montones; no queríamos pagar el precio de una "gran pila" por cada grano, ya que tenemos muchos, y no queríamos poner un límite a la profundidad con la que podría recurrirse. Debido a las horquillas paralelas, la pila es en realidad una pila de cactus.
Cada procedimiento, al ingresar, crea una pantalla léxica para permitir el acceso a los ámbitos léxicos circundantes. Consideramos usar la instrucción ENTER, pero decidimos no hacerlo por dos razones:
- Como otros han notado, no es particularmente rápido. Las instrucciones MOV funcionan igual de bien.
- Observamos que la pantalla a menudo es escasa y tiende a ser más densa en el lado léxico más profundo. La mayoría de las funciones auxiliares internas funcionan bien con acceso solo a su padre léxico directo; no siempre necesitas tener acceso a todos tus padres. A veces ninguno
En consecuencia, el compilador determina exactamente a qué ámbitos léxicos necesita acceder una función y genera, en el prólogo de función donde iría ENTER, solo las instrucciones MOV para copiar la parte de la pantalla principal que realmente se necesita. Eso a menudo resulta ser 1 o 2 pares de movimientos.
Así que ganamos dos veces en rendimiento sobre el uso de ENTER.
En mi humilde opinión, ENTER ahora es una de esas instrucciones CISC heredadas, que parecía una buena idea en el momento en que se definió, pero se supera con las secuencias de instrucciones RISC que incluso Intel x86 optimiza.
Realicé algunas instrucciones para contar las estadísticas sobre las botas de Linux utilizando la plataforma virtual de Simics, y descubrí que nunca se utilizó ENTER. Sin embargo, hubo bastantes instrucciones LEAVE en la mezcla. Hubo casi una correlación 1-1 entre CALL y LEAVE. Eso parece corroborar la idea de que ENTER es lento y costoso, mientras que LEAVE es bastante útil. Esto se midió en un kernel de 2.6 series.
Los mismos experimentos en un kernel de series 4.4 y 3.14 mostraron cero uso de LEAVE o ENTER. Presumiblemente, la generación del código gcc para los gccs más nuevos utilizados para compilar estos núcleos dejó de emitir LEAVE (o las opciones de la máquina se configuran de forma diferente).
enter
se evita en la práctica ya que funciona bastante mal: consulte las respuestas en "enter" frente a "push ebp; mov ebp, esp; sub esp, imm" y "leave" frente a "mov esp, ebp; pop ebp" . Hay un montón de instrucciones x86 que están obsoletas, pero que todavía son compatibles por razones de compatibilidad con versiones anteriores: enter
es una de ellas. ( leave
está bien, sin embargo, y los compiladores están felices de emitirlo.)
Implementar funciones anidadas con total generalidad como en Python es en realidad un problema considerablemente más interesante que simplemente seleccionar unas pocas instrucciones de administración de cuadros: buscar la "conversión de cierre" y el "problema de funcionamiento ascendente / descendente" y encontrará muchas discusiones interesantes.
Tenga en cuenta que el x86 fue diseñado originalmente como una máquina Pascal, por lo que hay instrucciones para admitir funciones anidadas ( enter
, leave
), la convención de llamadas pascales en la que el destinatario revela un número conocido de argumentos de la pila ( ret K
). comprobación de límites ( bound
), y así sucesivamente. Muchas de estas operaciones ahora están obsoletas.