tipos programacion lenguaje ejemplos assembly x86

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.