Principio de pila

img
A menudo decimos stack stack, pero heap y stack son en realidad dos conceptos completamente diferentes. La pila está diseñada en realidad para llamadas de función , entonces, ¿cómo se pueden implementar las llamadas de función a través de la pila? ¿Cuál es la diferencia en el comportamiento de la pila entre diferentes métodos de llamada a funciones?

Cómo funciona la pila del sistema:

1. Diferentes usos de la memoria:

Si le preocupa la seguridad de la red, debe haber escuchado el término desbordamiento de búfer. En pocas palabras: ** El desbordamiento del búfer es el proceso de copiar los datos en el búfer grande al búfer pequeño. Dado que no se presta atención al límite del búfer pequeño, el búfer más pequeño se "explota" y, por lo tanto, se lava Problemas de memoria causados ​​por otros datos en el área de memoria adyacente al búfer pequeño. ** El desbordamiento del búfer es uno de los errores de memoria más comunes, y también es el tipo de método de explotación más poderoso y clásico utilizado por los atacantes cuando invaden el sistema. El búfer utilizado en el programa puede ser un área de almacenamiento dinámico, un área de pila y un área de datos que almacena variables estáticas. El método de usar el desbordamiento del búfer es inseparable del área de memoria a la que pertenece el búfer.

Explotar con éxito las vulnerabilidades de desbordamiento del búfer puede modificar los valores de las variables en la memoria e incluso secuestrar procesos, ejecutar código malicioso y, finalmente, obtener el control del host. Para comprender a fondo este método de ataque, necesitamos revisar algunos conocimientos básicos de la arquitectura de la computadora y comprender cómo la CPU, los registros y la memoria funcionan juntos para que el programa se ejecute sin problemas.
De acuerdo con los diferentes sistemas operativos, un proceso puede asignarse a diferentes áreas de memoria para ejecutar. Pero no importa qué sistema operativo o arquitectura de computadora, la memoria utilizada por el proceso se puede dividir aproximadamente en las siguientes cuatro partes según la función .

(1) Área de código: esta área almacena el código de máquina binario que se carga y ejecuta, y el procesador buscará instrucciones y lo ejecutará en esta área .
(2) Área de datos: se utiliza para almacenar variables globales, etc.
(3) Área de almacenamiento dinámico : el proceso puede solicitar dinámicamente un cierto tamaño de memoria en el área de almacenamiento dinámico y devolverlo al área de almacenamiento dinámico después de que se agote. La asignación dinámica de memoria y la recuperación de memoria son características del área de almacenamiento dinámico.
(4) Área de pila: cuando se produce una llamada a una función, se usa para almacenar dinámicamente la relación de llamada entre funciones, pasar parámetros y guardar la dirección de retorno de la función. Para garantizar que la función llamada vuelva a la función principal para continuar la ejecución cuando regrese.

Bajo la plataforma Windows, los programas escritos en lenguajes de alto nivel eventualmente se convertirán en los llamados archivos PE (es decir, archivos ejecutables portátiles) después de ser compilados y vinculados. Cuando el archivo PE se carga y se ejecuta, se convierte en un proceso llamado .
** Después de eso, el código de máquina de nivel binario contenido en el segmento de código del archivo PE se cargará en el área de código (.text) de la memoria. El procesador buscará instrucciones y operandos uno por uno en esta área de la memoria y lo enviará a la aritmética La unidad lógica realiza la operación; ** Si el código solicita abrir la memoria dinámica, se asignará un área de tamaño apropiado en el área de almacenamiento dinámico de la memoria y se devolverá al código en el área de código. Cuando se produce una llamada a una función, la información como la relación de llamada de la función se guardará dinámicamente en el área de la pila de la memoria para que el procesador regrese a la función principal después de ejecutar el código de la función llamada. Este proceso de colaboración se muestra en la Figura 2.1.1.
img

2. Pila y pila del sistema:

Desde la perspectiva de la informática, la pila se refiere a una estructura de datos, que es una tabla de datos avanzada. Hay dos operaciones más comunes en la pila: PUSH y POP: también se utilizan dos atributos para identificar la pila: la parte superior de la pila (TOP) y la parte inferior de la pila (BASE).
Piense en una pila como una pila de naipes.

  • PUSH: La operación de agregar un elemento a la pila se llama PUSH, que es equivalente a poner una carta más encima de la pila de cartas.
  • POP: La operación de eliminar un elemento de la pila se llama POP, lo que equivale a tomar la carta más alta de la pila de cartas.
  • TOP: identifica la posición superior de la pila y cambia dinámicamente. Cada vez que se realiza una operación PUSH, aumentará en 1; por el contrario, disminuirá en 1 cada vez que se realice una operación POP. El elemento superior de la pila es equivalente a la carta de juego más alta, solo el palo de esta carta es visible actualmente.
  • BASE: identifica la posición inferior de la pila y registra la posición de la carta inferior de la carta de juego. BASE se usa para evitar que la pila continúe jugando después de que la pila esté vacía (la carta ya no se puede revelar después de que se reparte la carta). Obviamente, en circunstancias normales, BASE no cambiará.
  • El área de la pila de memoria en realidad se refiere a la pila del sistema. El sistema mantiene automáticamente la pila del sistema y se usa para implementar llamadas de función en lenguajes de alto nivel.

3. ¿Qué sucedió cuando se llamó a la función?

Exploremos cómo la naturaleza de la llamada a funciones y la recursividad en lenguajes de alto nivel se implementa ingeniosamente a través de la pila del sistema. Por favor vea el siguiente código:

int func_B(int arg_B1, int arg_B2)
{
    int var_B1;
    int var_B2;
    var_B1 = arg_B1 + arg_B2;
    var_B2 = arg_B1 - arg_B2;
    return var_B1 * var_B2;
}
int func_A(int arg_A1, int arg_A2)
{
      int var_A;
      var_A = func_B(arg_A1, arg_A2) + arg_A1;
      return var_A;
}
int main(int argc, char** argv, char** envp)
{
      int var_main;
      var_main = func_A(3, 4);
      return 0;
}

Después de que el compilador compila este código, las instrucciones de la máquina correspondientes a cada función se dispersan y son irrelevantes en el área del código.

** Cuando la CPU está llamando a la función func_A, saltará del área de instrucciones de la máquina correspondiente a la función principal en el área de código al área de instrucciones de la máquina correspondiente a la función func_A, donde la instrucción se obtiene y ejecuta; Al regresar a la reunión, volverá al área de instrucción correspondiente a la función principal y luego llamará a la instrucción después de func_A para continuar ejecutando el código de la función principal. ** Durante este proceso, la trayectoria de recuperación de la CPU se muestra en la Figura 2.1.3.
img
** Entonces, ¿cómo sabe la CPU ir al área de código de func_A, y cómo sabe volver a la función principal (no al área de código de func_B) después de ejecutar func_A? ** Estas direcciones de salto no se indican directamente en lenguaje C. ¿De dónde obtuvo la CPU la información sobre la llamada y el retorno de estas funciones?
Resulta que los saltos precisos en estas áreas de código se realizan de manera inteligente con la pila del sistema. Cuando se llama a la función, la pila del sistema abrirá un nuevo marco de pila para esta función y la empujará a la pila. El espacio de memoria en este marco de pila está monopolizado por la función a la que pertenece y, en circunstancias normales, no se compartirá con otras funciones. Cuando la función regrese, la pila del sistema mostrará el marco de la pila correspondiente a la función.
Como se muestra en la Figura 2.1.4, durante la llamada a la función, las operaciones en la pila del sistema adjunta son las siguientes.

img

  • Al llamar a func_A en la función principal, primero inserte la dirección de retorno de la función en su propio marco de pila, luego cree un nuevo marco de pila para func_A e introdúzcalo en la pila del sistema .
  • Cuando func__A llama a func_B, también empuja la dirección de retorno de la función en su propio marco de pila, y luego crea un nuevo marco de pila para func_B y lo empuja a la pila del sistema .
  • Cuando regresa func_B, el marco de la pila de func_B es expulsado de la pila del sistema y la dirección de retorno en el marco de la pila de func_A está "expuesta" en la parte superior de la pila. En este momento, el procesador salta al área de código de func_A de acuerdo con la dirección de retorno.
  • Al mismo tiempo que regresa func_A, el marco de la pila de func_A se expulsa de la pila del sistema. La dirección de retorno en el marco de la pila de la función principal está "expuesta" en la parte superior de la pila, y el procesador salta al área del código de la función principal para la ejecución de acuerdo con esta dirección de retorno.

4. Registro y marco de pila de funciones

Cada función monopoliza su propio espacio de marco de pila. El marco de la pila de la función actualmente en ejecución siempre está en la parte superior de la pila. El sistema de la CPU proporciona dos registros especiales para identificar los atributos del marco de la pila en la parte superior de la pila del sistema.

  • (1) ESP : registro de puntero de pila (puntero de pila extendido), que almacena un puntero, que siempre apunta a la parte superior de la pila del sistema, la parte superior de un marco de pila.

  • (2) EBP : registro de puntero de dirección base (puntero base extendido): contiene un puntero que siempre apunta a la parte inferior del marco de la pila superior de la pila del sistema.
    Nota: EBP apunta a la parte inferior del marco de la pila actualmente en la parte superior de la pila del sistema, no a la parte inferior de la pila del sistema. Estrictamente hablando, "parte inferior del marco de la pila" y "parte inferior del marco de la pila" son conceptos diferentes. En este artículo, el término "parte inferior del marco de la pila" se utilizará en la narración para mostrar la diferencia; la parte superior del marco de la pila y la pila del sistema a que hace referencia ESP la primera posición es la misma , se describe más adelante y por lo tanto no son estrictamente distinguir el concepto de "parte superior del marco de pila" y "pila" de . Por favor, preste atención a las diferencias aquí, para no confundir los conceptos.

El efecto del registro en el marco de la pila se muestra en la Figura 2.1.5 @functionstack
img
frame @: El espacio de memoria entre ESP y EBP es el marco de la pila actual. EBP identifica la parte inferior del marco de la pila actual, y ESP identifica la parte superior del marco de la pila actual.

El marco de la pila de funciones generalmente contiene los siguientes tipos importantes de información.

  • (1) Variables locales : espacio de memoria creado para variables locales de función.
  • (2) Valor de estado del marco de la pila : guarde la parte superior e inferior del marco de la pila frontal (en realidad solo se guarda la parte inferior del marco de la pila frontal, la parte superior del marco de la pila frontal se puede obtener mediante el cálculo del saldo de la pila), que se utiliza para recuperar después de que se abre este marco Marco de pila anterior .
  • (3) Dirección de retorno de la función: guarde la información del "punto de interrupción" antes de la llamada a la función actual, es decir, la posición de la instrucción antes de la llamada a la función, de modo que cuando la función regrese, se pueda restaurar al área de código antes de llamar a la función para continuar ejecutando la instrucción.

Además de los registros relacionados con la pila, también debe recordar otro registro vital.
EIP: Registro de instrucciones (puntero de instrucción extendido), que contiene un puntero que siempre apunta a la dirección de la próxima instrucción que espera ser ejecutada. Se puede decir que si controla el contenido del registro EIP, controla el proceso, ¿dónde dejamos que el punto EIP apunte? , Donde la CPU ejecutará las instrucciones.

5. Convención de llamada a funciones e instrucciones relacionadas

La convención de llamada de función describe los detalles técnicos de cómo la función pasa los parámetros y la pila funciona en conjunto. Los diferentes sistemas operativos, diferentes idiomas y diferentes compiladores tienen el mismo principio al implementar llamadas a funciones, pero las convenciones de llamadas específicas siguen siendo diferentes. ** Esto incluye el método de paso de parámetros, si el orden de los parámetros se empuja de derecha a izquierda o de izquierda a antigua, y si la operación para restaurar el equilibrio de la pila cuando la función regresa se realiza en la función secundaria o en la función madre. ** La Tabla 2-1-1 enumera las diferencias entre varios métodos de llamada.
img
Específicamente, para Visual C ++, se pueden admitir las siguientes tres convenciones de llamadas a funciones: como se muestra en la Tabla 2-1-2,
img
si desea utilizar una determinada convención de llamadas explícitamente, solo necesita agregar la declaración de la convención de llamadas antes de la función. De lo contrario, el método de llamada de __cdecl se usará de manera predeterminada.

La llamada a la función @ incluye aproximadamente los siguientes pasos @: (énfasis)

  • (1) Los parámetros se insertan en la pila : los parámetros se insertan en la pila del sistema en orden de derecha a izquierda.
  • (2) Dirección de retorno en la pila : inserte la dirección de la siguiente instrucción en la instrucción de llamada del área de código actual en la pila para continuar la ejecución cuando la función regrese.
  • (3) Salto del área de código : el procesador salta del área de código actual a la entrada de la función llamada.
  • (4) Ajuste del marco de la pila : incluya específicamente lo siguiente
    1. Guarde el valor del estado del marco de la pila actual, que se usa para restaurar el marco de la pila más tarde (EBP en la pila), presione ebp
    2. Cambie el marco de pila actual a un nuevo marco de pila (cargue el valor ESP actual en el EBP para actualizar la parte inferior del marco de pila) mov ebp esp
    3. Asigne espacio para el nuevo marco de pila ** (reste el tamaño del espacio requerido de ESP y levante la parte superior de la pila) ** sub esp, xxx
      (2) y (3) se completan con la llamada.
      Para la convención de llamada __stdcall, la secuencia de instrucciones utilizadas en la llamada de función es aproximadamente la siguiente.
      img
      El cambio en la pila causado por la instrucción anterior para la llamada a la función se muestra en la Figura 2.1.7.
      img
      img
      Del mismo modo, 返回los pasos de la función son los siguientes:
      (1) Guardar el valor de retorno: el valor de retorno de la función generalmente se guarda en el registro EAX .
      (2) Pop el marco de pila actual y restaurar el marco de pila anterior.
      Esto incluye:
  • Según el balance 加上de la pila, se reduce el tamaño del marco de la pila ESP , se reduce la parte superior de la pila y se recupera el espacio del marco de la pila actual. agregar esp, xxx
  • Introduzca el valor de EBP del marco de pila anterior guardado en la parte inferior del marco de pila actual en el registro EBP para restaurar el marco de pila anterior. pop ebp
  • Introduzca la dirección de retorno de la función en el registro EIP. Retn
    todavía toma el lenguaje C y la plataforma Win32 como ejemplo, la secuencia de instrucciones relevante cuando la función regresa es la siguiente.
    agregue esp, xxx; baje la parte superior de la pila, recupere el
    ebp emergente del marco de la pila actual ; restaure la posición inferior del marco de la pila anterior a ebp.
    retn; Esta instrucción tiene dos funciones:
    a) Pop el elemento superior actual de la pila, es decir, pop la dirección de retorno en el marco de la pila. La restauración del marco de la pila se ha completado.
    b) Deje que el procesador salte a la dirección de retorno emergente y restaure el área de código antes de la llamada.

(3) Saltar: volver a la función principal de acuerdo con la dirección de retorno de la función para continuar la ejecución.

La estructura de la pila del sistema organizada de acuerdo con esta convención de llamada de función se muestra en la Figura 2.1.8:
img
Referencia: https://www.jianshu.com/p/ffe303d96dbd

8 artículos originales publicados · Me gusta4 · Visitas 290

Supongo que te gusta

Origin blog.csdn.net/qq_45521281/article/details/105365438
Recomendado
Clasificación