Capa inferior del lenguaje gráfico C/C++: la creación y destrucción de marcos de pila de funciones en el proceso de llamada de función (on)

**


​ —— IMPULSADO POR CAIXYPROMISE


Creación y destrucción de marcos de pila de funciones

A través del estudio anterior, hemos aprendido la gramática y el uso del programa de lenguaje C más básico, pero ¿tiene alguna pregunta?

Por ejemplo:

  • ¿Cómo se forma el alcance de una función?

  • ¿Cómo se crean las variables locales?

  • ¿Por qué los valores de las variables locales no inicializadas son aleatorios o distorsionados?

  • ¿Cómo se pasan parámetros a la función? ¿Cuál es el orden de paso de los parámetros?

  • ¿Cuál es la relación entre los parámetros formales y los parámetros reales?

  • ¿Cómo se implementa la llamada a la función?

  • ¿Cómo regresa la función después del final de la llamada?

  • ¿Por qué hay una profundidad máxima de recursividad de funciones? ¿Qué significa el error de desbordamiento de pila que se genera al alcanzar la profundidad máxima?

Cuando comprenda la creación y destrucción de marcos de pila de funciones, ¡estas dudas se resolverán una por una! Con estas preguntas en mente, ¡entremos en el marco de la pila de funciones!

Debido a la extensión del artículo, esta serie de artículos se divide en dos partes. Este artículo es el primero y presentará principalmente:

Si tiene alguna pregunta, responda en el área de comentarios. Haga clic para ver el siguiente artículo de inmediato para conocer los nuevos conocimientos del próximo artículo lo antes posible.

  • ¿Qué son los registros?
  • ¿Qué es una pila?
  • El proceso de formación del marco de la pila de funciones
  • El proceso de formación de variables de función.

Comprender el marco de la pila de funciones requiere operaciones de desmontaje, y el autor lo presentará de acuerdo con las instrucciones de montaje relevantes.

Las brújulas están girando hacia adelante, ¡vamos al grano!


¿Qué son los registros?

Lo primero que debes saber: ¿qué es un registro?

En hardware informático, ¿qué es el hardware con función de almacenamiento? Son disco duro --> memoria --> caché (cache) --> registros, y la velocidad de acceso y la velocidad de almacenamiento de los cuatro aumentan de izquierda a derecha; al mismo tiempo, sus tamaños también disminuyen de de izquierda a derecha, hasta el último registro, su espacio de almacenamiento puede tener solo el tamaño de una unidad de almacenamiento de 4 bytes, y al mismo tiempo su velocidad de acceso es la más rápida, debido a que el registro generalmente está integrado en la CPU y es independiente. espacio de almacenamiento diferente de la memoria. Como dice el refrán, si la velocidad de la red es rápida, estás sentado en el servidor para jugar, y cuanto más rápida sea la velocidad de lectura, estás sentado en la CPU para leer. Esta es la razón por qué los registros se leen más rápido.

imagen-20220330211304684

Registrar clasificación

Hay muchos tipos de registros informáticos.

  • Registros generales: EAX, EBX, ECX, EDX

    ax: registro de acumulación, bx: registro base, cx: registro de conteo, ed: registro de datos

  • Registro de índice: ESI, EDI

    si: registro de índice de origen, di: registro de índice de destino

  • Pila, registros base: ESP, EBP

    sp: registro de índice de pila, bp: registro de índice base; estos dos registros son también los dos registros más importantes en el marco de pila de función

en:

  • EAX, ECX, EDX, EBX: extensiones de ax, bx, cx, dx, cada una de 32 bits
  • ESI, EDI, ESP, EBP: extensiones de si, di, sp, bp, 32 bits cada una
  • EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, etc. son los nombres de registros de propósito general en la CPU en lenguaje ensamblador X86, que son registros de 32 bits.

Propósito de registro

Entonces, ¿cuál es su uso en el programa?

Cada uno de estos registros de 32 bits tiene "especialidades" y tiene sus propias características especiales.

  • EAX es el "acumulador", que es el registro predeterminado para muchas instrucciones de suma y multiplicación.

  • EBX es el registro de "dirección base" (base), que almacena la dirección base durante el direccionamiento de memoria.

  • ECX es un contador (contador), que es el contador predeterminado para instrucciones de prefijo de repetición (REP) e instrucciones de LOOP.

  • EDX siempre se usa para contener el resto de la división entera.

  • ESI/EDI se denominan "registros de índice de origen/destino", porque en muchas instrucciones de manipulación de cadenas, DS:ESI apunta a la cadena de origen y ES:EDI apunta a la cadena de destino.

  • EBP es el "puntero base", que se usa con mayor frecuencia como un "puntero de marco" para llamadas a funciones de lenguaje de alto nivel. Al descifrar el software, a menudo puede ver un código ensamblador de inicio de función estándar:

    push ebp ;保存当前ebp
    mov ebp,esp ;EBP设为当前堆栈指针
    sub esp, xxx ;预留xxx字节给函数临时变量.
    ...
    这样一来,EBP 构成了该函数的一个框架, 在EBP上方分别是原来的EBP, 返回地址和参数. EBP下方则是临时变量. 函数返回时作 mov esp,ebp/pop ebp/ret 即可.
    
  • ESP se usa especialmente como un puntero de pila y se le llama acertadamente el puntero superior de la pila. La parte superior de la pila es un área con direcciones pequeñas.Cuantos más datos se insertan en la pila, más pequeño se vuelve el ESP. En plataformas operativas de 32 bits, ESP se reducirá en 4 bytes cada vez.

Hasta aquí el concepto de registros. En la práctica, el contenido se almacena en un registro y se utiliza su dirección. Lo que está muy relacionado con la formación del marco de la pila de funciones es: las dos direcciones de registro de EBP y ESP.

¿Qué es una "pila"?

Antes de comenzar a explicar, debe prestar atención a otra palabra clave: ¿qué es una "pila"? La pila es un tipo de estructura de datos. Este artículo no explicará demasiado sobre su método de implementación. Solo necesita comprender una de sus características: después de que los datos se colocan en la pila en secuencia, el orden en que se toman los elementos out es el primero en entrar y el último en salir; por ejemplo, coloque un montón de libros en un barril de madera, y cuando necesite sacar el libro de abajo, primero debe sacar la parte superior para sacar la parte inferior . El área de pila mencionada en este artículo se ejecuta principalmente en la memoria del sistema .

El concepto de marco de pila de funciones

En el registro, los dos registros EBP y ESP almacenan direcciones, y estas dos direcciones se utilizan para mantener el marco de la pila de funciones.

Cada vez que se llama a una función, se debe crear un espacio en el área de la pila; cualquiera que sea la función que se llame, las dos direcciones de EBP y ESP mantendrán el espacio de memoria de esta función, que es el marco de la pila de la función; por ejemplo , la función principal se está ejecutando Entre ellos, las dos direcciones de puntero de esp y ebp apuntarán a la parte superior e inferior de la pila al mismo tiempo.

Dicho esto, es posible que no lo entiendas. ¡Entonces haz un dibujo!

imagen-20220110184503700

El proceso de apilamiento de funciones.

Para facilitar la demostración y la comprensión, usaré la versión VS2013 para demostrar el proceso de apilamiento de funciones, lo guiaré paso a paso para leer las instrucciones de ensamblaje del programa y explicaré qué tipo de operación realizará cada paso, y finalmente le dará el todo El comando hace un resumen. Debido a que diferentes compiladores pueden tener diferentes métodos para ensamblar y empaquetar programas, y los compiladores de nivel superior empaquetarán los programas con más cuidado, lo que no conduce a la observación. Al mismo tiempo, la dirección de las siguientes instrucciones de ensamblaje cambiará con cada compilación del programa (porque el contenido se asigna aleatoriamente), si también está depurando localmente, manténgalo en la misma situación de compilación. Pero en principio son todos iguales.

Cabe señalar que en las versiones anteriores a VS2013, cuando ve la pila de llamadas al ejecutar la depuración del programa, encontrará que otras funciones también llaman a la función principal .

Son funciones __tmainCRTStartup y mainCRTStartup respectivamente, donde se presiona mainCRTStartup en la parte inferior.

La lógica de llamada es mainCRTStartup -> __tmainCRTStartup --> función principal

Pila de funciones __tmainCRTStartup

imagen-20220110211617884

imagen-20220110211729352

pila de inserción de la función mainCRTStartup

imagen-20220110212352354

A partir de esto, podemos entender que la pila de memoria en este momento se expresa como

imagen-20220110220522449

Al observar en el entorno de compilación VS2013, se puede encontrar que durante el proceso de ejecución de la función, el puntero superior de la pila esp y el puntero inferior de la pila ebp formarán un espacio de memoria para formar el marco de la pila de la función. Entonces, ¿qué hace exactamente el programa? Podemos estudiar su proceso de inserción de pila mirando el código de desensamblado del programa.

Lo siguiente es parte del código de desensamblado de la función principal, ahora veamos cómo funciona su principio específico.

Código de muestra e instrucciones de ensamblaje de la función principal (pieza)

Código de programa, este artículo utilizará este código como ejemplo para presentar el proceso de generación y destrucción de marcos de pila de funciones, variables locales y llamadas a funciones.

imagen-20220111163038299

Las siguientes instrucciones de montaje son parte de lo que se discutirá a continuación.

imagen-20220110221517882

En este artículo, explicaré las instrucciones de ensamblaje en este artículo combinando los documentos de instrucciones de ensamblaje de los detalles de generación de código X86 del lenguaje C. Las siguientes son las instrucciones de ensamblaje que se usarán comúnmente.

imagen-20220113123135672

Cuando el programa ingresa a la función principal, solo mencionamos que la función principal también es llamada por otras funciones, entonces, ¿la función original que llamó a la función principal creó su marco de pila de funciones? La respuesta es sí. En este momento, la función original __tmainCRTStartup se mantiene mediante los dos punteros superior/inferior de la pila de esp y ebp.

El área de pila al comienzo del inicio inicial debe ser como se muestra en la figura

imagen-20220110223836717

Instrucciones de montaje: preparación del marco de la pila de la función de construcción (1)

A continuación, veamos la primera instrucción de ensamblaje en la que viene la función principal.

imagen-20220110223528221

La primera oración que aparece es push ebp. En la instrucción de ensamblaje, significa poner el valor de ebp en la parte superior de la pila.

imagen-20220110224216553

Entonces, ¿podemos suponer: porque esp mantiene la parte superior de la pila del programa, en este momento esp ha llegado a la parte superior de la pila y la dirección de esp apuntará al valor de ebp? como muestra la imagen

imagen-20220110224316200

¿Cómo justificar esta hipótesis?

Cuando abre el monitor para monitorear esp, puede encontrar que su valor cambiará.

Actualmente el valor inicial del puntero superior de la pila esp

imagen-20220110224605878

Después de que se completa el push ebp, ¿la dirección esp va de mayor a menor, por lo que la dirección debería disminuir?

Esto se evidencia cuando el monitor ingresa al proceso paso a paso: a8 a a4 se reducen en 4 bytes

imagen-20220111000052774

Entonces, ¿el valor de esp será el valor de ebp? Abra el bloque de memoria, busque la dirección del nuevo esp será el valor de ebp, ¡la respuesta es clara de un vistazo! ~

¿Cuál era el valor de ebp en este momento? 008ffbf4, ahora el valor de la dirección de la búsqueda esp es 008ffbf4, la suposición es verdadera.

esp mantiene la parte superior de la pila del programa. En este momento, esp ha llegado a la parte superior de la pila, y la dirección del nuevo esp apuntará al valor de ebp

imagen-20220110225136909

Y que tipo de ebp es el ebp prensado, te lo explicamos a continuación.

Instrucciones de montaje: preparación del marco de la pila de la función de construcción (2)

Ahora veamos la segunda instrucción de ensamblaje: mov le da el valor de esp a ebp.

imagen-20220110225937687

¿Es este realmente el caso? Ejecutamos el siguiente paso de depuración y los comentarios del monitor son los siguientes

imagen-20220111000207900

En este punto, su diagrama de pila debe ser

imagen-20220111000344361

Instrucciones de montaje: construye el alcance del marco de la pila de funciones

Veamos la tercera oración para ensamblar la instrucción: la dirección de sub esp, menos 0E4h. (sub significa disminuir en inglés, add significa agregar de la misma manera)

imagen-20220111000446712

En términos generales, el valor sustraído de ebp es 0E4h, y 0E4h aquí es en realidad un número octal. Cuando desee verificar qué número es 0E4h, puede colocarlo en el área de monitoreo y mostrar su valor hexadecimal, y luego verificar el número decimal

imagen-20220111001133085

imagen-20220111001200430

Ven aquí, ¿no es equivalente a restar el valor de 0E4h de esp? ¿Habrá cambiado la esp en este momento? Monitor para ver los resultados proceso por proceso

imagen-20220111001414197

En este momento, el valor de esp ha cambiado a 0x008ffac0, lo que significa que el valor de la dirección de esp se vuelve más pequeño y se mueve hacia arriba y ya no apunta al lugar original, sino que apunta a un área determinada por encima de la dirección original.

imagen-20220111002520582

En este momento, ¿ha descubierto que los punteros de la parte superior de la pila esp y la parte superior de la pila ebp han formado un nuevo espacio de dimensión después de ingresar a la función principal, y esp y ebp ya no mantienen el espacio de función original? Así es, esta nueva área es el área del marco de la pila de funciones abierta previamente para la función principal. Y sub es cuántos bytes de espacio se proponen para la función principal.

El diagrama esquemático del área de la pila se puede entender como la siguiente imagen

imagen-20220111002230000

Instrucción de montaje: poner en tres registros no volátiles

Los ebx, esi y edi aquí son los registros base, de índice de origen y de índice de destino en los registros que mencionamos anteriormente, y los tres se denominan colectivamente como registros no volátiles aquí. Esta es una convención de llamadas en el lenguaje C. La razón para colocar los tres registros en la pila aquí es lograr un uso multiplataforma. El propósito de estos 3 registros bajo la convención de llamadas bajo la plataforma X86 es que al llamar a una función, se requiere presionar estos 3 registros para guardar los datos antes de la llamada, y deben almacenarse durante mucho tiempo durante la llamada.

Están siendo empujados a la pila aquí. No olvides que mientras empujas a la pila, el puntero superior de la pila esp cambia constantemente.

imagen-20220111003749180

Los detalles del proceso de apilamiento pueden ser los siguientes:

Observar los valores de esp y ebx en el monitor

imagen-20220111005406787

¿Cómo cambiará esp cuando ebx comience a ser empujado a la pila? La respuesta es sí, el valor de esp disminuirá y subirá

imagen-20220111005510565

Al abrir la memoria, encontrará que la dirección correspondiente a esp es el valor de ebx 0x007e5000

imagen-20220111005640832

De manera similar, cuando continúa presionando el esi, el cambio de esp es el siguiente

imagen-20220111005902323

imagen-20220111010033722

Al presionar edi, el cambio de esp es el siguiente

imagen-20220111010112098

imagen-20220111010131509

En resumen, el valor del puntero superior de la pila esp original ha cambiado del 008ffac0 inicial al 008ffab4 actual, la dirección disminuye constantemente y la parte superior de la pila se mueve constantemente hacia arriba.

El diagrama esquemático del área de pila actual se puede entender como

imagen-20220111004521750

Instrucciones de montaje: espacio efectivo del marco de la pila de carga

En este punto, para la conveniencia de una experiencia y comprensión intuitivas, mostraremos el nombre del símbolo del ensamblaje.

Cuando se trata de la declaración lea en la séptima oración, su nombre completo debe ser cargar dirección efectiva (cargar dirección efectiva); como su nombre lo indica, a partir de aquí, el programa cargará oficialmente el área de marco de pila efectiva del actual función. Veamos cómo va.

imagen-20220111024656812

lea edi, [ebp-0E4h], ¿te resulta familiar el 0E4h aquí? Así es, es el tamaño preaplicado en el marco de la pila de funciones de la función principal preaplicada en este momento. El significado aquí es almacenar el espacio de tamaño ebp-0E4h en edi, y ¿no es este edi el puntero inferior de la pila? Del diagrama de pila, podemos observar que el espacio realmente expresado por ebp - 0E4h es el espacio de pila de la función principal actual. En este punto, se han almacenado en el registro edi.

imagen-20220111015833316

¿Cómo argumentar? Gire a la dirección de esp cuando los tres registros no volátiles justo antes no se inserten en la pila

imagen-20220111001414197

Ahora, abrimos el monitor para verificar la dirección de ebp-0E4h y edi, ¡la respuesta es obvia! La dirección de ~ebp-0E4h es la ubicación de la tercera pila, edi apuntada por el esp actual, y también es la dirección del esp cuando los tres registros no volátiles no se insertan en la pila.

imagen-20220115204906978

mov ecx, 39h y mov eax, 0CCCCCCCCh significan que los correspondientes 39h y 0CCCCCCCCh se colocan respectivamente en los registros ecx y eax.

Mire la siguiente oración: rep stos dword ptr es:[edi], aquí es muy interesante, aquí eventualmente formará el espacio efectivo del marco de pila de la función. Echemos un vistazo a la expresión de la declaración de instrucción: desde el lugar marcado en edi, copie el contenido de eax repetidamente ecx veces hasta el puntero inferior ebp de la pila. Cabe señalar que dword expresa el significado de palabra doble de doble byte, una palabra es de 2 bytes y palabra doble es de doble byte equivalente a 4 bytes.

¿Cuál es su proceso específico? A partir del ebp-0E4h marcado por edi, copie los bytes en la parte de la dirección alta y copie 4 bytes cada vez. El contenido copiado es el contenido de eax (0CCCCCCCCh), el número de copias es 39h veces y se detiene en el puntero inferior ebp de la pila.

Combinado con la teoría actual, el programa copiará bytes desde 008ffac0 a la dirección alta hasta el puntero inferior de la pila ebp; cuando abre el mapa de memoria para verificar la situación de la memoria en este momento, puede probar este punto ~

Copiar a la dirección alta de 008ffac0

imagen-20220111113038428

Termina en el puntero inferior de la pila 008ffba4

imagen-20220111113353635

Puede que tengas dudas, ¿qué significa este cccccccc? Pueden ser ligeramente diferentes en cada compilador, y cuando generalmente escribimos un programa, cuando el valor inicial de la variable no está definido, la salida impresa es "caliente, caliente, caliente" caracteres ilegibles, no la autoexpresión de la computadora de su propia temperatura. : ), de hecho, este es el carácter 0CCCCCCCCh colocado en la memoria.

En resumen, el diagrama esquemático del área de la pila puede ser el siguiente

imagen-20220111031852347

El programa se ejecuta hasta ahora, el programa ha pasado por cinco pasos, y el marco de pila de funciones abierto para la función principal se completa oficialmente. Esta área formada por esp y ebp es el alcance de una función, y se usa un espacio formado por 0CCCCCCCCh para almacenar el espacio variable local. A continuación, el programa ejecuta formalmente un código válido.

Instrucciones de montaje: generar variables locales de función

Después de muchas operaciones anteriores, se ha formado el área de marco de pila efectiva de una función, y el programa ejecutará su código efectivo en este momento. De acuerdo con los requisitos del código escritos anteriormente, las variables locales se crearán cuando entre el programa. ¿Cómo se crean las variables locales en el área del marco de pila?

imagen-20220111161949940

Primero, echemos un vistazo a las instrucciones de montaje: ¿le resulta familiar esta sintaxis? El significado de la declaración es: coloque 0Ah, 14h, 0 en las posiciones de ebp - 8, ebp - 14h y ebp - 20h en secuencia.

imagen-20220111135625127

ebp - 8, ebp - 14h y ebp - 20h son una serie de direcciones que se reducen a la dirección inferior según el puntero inferior de la pila. Aquí, se asigna un espacio a 0Ah, 14h y 0, y este 0Ah , 14h y 0 son la representación hexadecimal de la computadora de 10, 20, 0. Lugar

Ahora vamos a probar lo que acabamos de decir. Primero, continúe observando el valor del puntero en la parte inferior de la pila ebp para ver si almacena variables en direcciones bajas;

imagen-20220111160123356

Paso a paso en la declaración, la respuesta es obvia, desde el puntero inferior de la pila 008ffba4 hasta la dirección baja - 8, el valor almacenado es 0ah. En este punto se ha creado la variable local a = 10

imagen-20220111160512467

Sigamos mirando el siguiente paso y creemos una variable local b = 20, y la siguiente c es la misma.

imagen-20220111161648219

De acuerdo con la observación del proceso de creación de variables locales en el marco de pila, podemos encontrar que las variables locales se almacenan desde direcciones altas a direcciones bajas después de formar un espacio de marco de pila válido. Si no se establece el valor inicial de la variable, el programa delineará un área como la dirección de la variable.

El diagrama esquemático del área de la pila en este momento puede ser el siguiente

imagen-20220111165749903

Resumen de este artículo

En este artículo, presentamos brevemente el proceso básico de construcción de un marco de pila de una función basada en la función principal; aprendimos que:

  • El marco de pila de una función es en realidad una pieza de espacio de memoria mantenida conjuntamente por los dos punteros superior e inferior de la pila de esp y ebp;
  • Cuando una función comienza a generar un marco de pila, primero empujará la dirección ebp del puntero inferior de la pila de la función anterior.
  • En el proceso de generar el marco de pila, el marco de pila en constante expansión, empujando nuevos contenidos o registros hará que el puntero superior de la pila esp se desplace hacia arriba;
  • Al realizar operaciones relacionadas con la determinación de la orientación, la posición del puntero inferior ebp de la pila se usa como desplazamiento para comenzar a desplazar a la dirección inferior.
  • Después de presionar los 3 registros no volátiles, el programa llenará un área basada en el puntero superior de la pila ebp a la dirección baja, y esta área es el alcance de una función. En este ámbito, el programa generará las variables correspondientes según la dirección ascendente (dirección baja) del puntero ebp.

En el próximo artículo, presentaremos el proceso de llamada y devolución de funciones, y haremos un resumen de las preguntas que planteamos al principio.

Si tiene alguna pregunta, por favor pregunte en el área de comentarios.

Conclusión

Se han introducido la destrucción y el proceso (1) del marco de la pila de funciones, y hay un segundo capítulo sobre temas relacionados, que tiene más contenido y está lleno de productos secos. Si crees que este artículo es útil para ti, ¡no olvides darle me gusta y mirar + seguir!

¡La creación no es fácil, su atención y aprecio es el mayor estímulo para el autor! El autor continuará compartiendo conocimientos sobre el aprendizaje de C/C++ y el uso práctico de Python. ¡Tu apoyo hará que el autor de seguimiento trabaje más duro para publicar más artículos de alta calidad y aprenda a actualizar y luchar contra monstruos contigo!

Ver el siguiente artículo

Se espera que el próximo artículo se publique dentro de esta semana. Esta serie de artículos se ha publicado primero en la cuenta pública de WeChat: 01 Cabina de programación . Bienvenidos a todos a prestar atención a la primera vez para aprender nuevos conocimientos; sigan la Cabina para aprender a programar. y no perderse
inserte la descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/weixin_43654363/article/details/123858937
Recomendado
Clasificación