Introducción al lenguaje ensamblador


Ir a: Tutorial de introducción al lenguaje ensamblador


Para aprender el lenguaje ensamblador, primero debe comprender dos puntos de conocimiento: registros y modelos de memoria.

Veamos primero los registros. La propia CPU solo es responsable de los cálculos, no del almacenamiento de datos. Los datos generalmente se almacenan en la memoria, y la CPU va a la memoria para leer y escribir datos cuando es necesario usarlos. Sin embargo, la velocidad de cómputo de la CPU es mucho mayor que la velocidad de lectura y escritura de la memoria Para evitar que se ralentice, las CPU tienen su propia caché de primer nivel y caché de segundo nivel. Básicamente, la memoria caché de la CPU puede verse como una memoria con velocidades de lectura y escritura más rápidas.

Sin embargo, la caché de la CPU aún no es lo suficientemente rápida. Además, la dirección de los datos en la caché no es fija y la CPU debe direccionar cada vez que se lee y escribe, lo que ralentizará la velocidad. Por lo tanto, además de la caché, la CPU también tiene su propio registro (registro) para almacenar los datos más utilizados. En otras palabras, los datos de lectura y escritura más frecuentes (como las variables de bucle) se colocarán en el registro, la CPU dará prioridad para leer y escribir en el registro y luego el registro intercambiará datos con la memoria.


Los registros no se basan en direcciones para distinguir datos, sino en nombres. Cada registro tiene su propio nombre, le decimos a la CPU de qué registro obtener los datos para que la velocidad sea la más rápida. Algunas personas comparan el registro con la caché de nivel cero de la CPU.

Cuarto, los tipos de registros

Las primeras CPU x86 tenían solo 8 registros y cada uno tenía un propósito diferente. Ahora hay más de 100 registros, todos los cuales se han convertido en registros de propósito general y no están específicamente designados para su uso, pero se han conservado los nombres de los primeros registros.

EAX

EBX

ECX

EDX

LO SÉ

ESI

EBP

ESP

Entre los 8 registros anteriores, los primeros siete son todos comunes. El registro ESP tiene un propósito específico para guardar la dirección de la pila actual.

A menudo vemos nombres como CPU de 32 bits y CPU de 64 bits, que en realidad se refieren al tamaño de los registros. El tamaño de registro de una CPU de 32 bits es de 4 bytes.

Cinco, modelo de memoria: Montón

Los registros solo pueden almacenar una pequeña cantidad de datos. La mayoría de las veces, la CPU tiene que dirigir los registros para que intercambien datos directamente con la memoria. Por lo tanto, además de los registros, también debe comprender cómo almacena los datos la memoria.

Cuando el programa se está ejecutando, el sistema operativo le asignará una sección de memoria para almacenar el programa y los datos generados por la operación. Esta sección de memoria tiene una dirección inicial y una dirección final, por ejemplo, de 0x1000 a 0x8000, la dirección inicial es la dirección más pequeña y la dirección final es la dirección más grande.

Durante la ejecución del programa, para solicitudes de ocupación de memoria dinámica (como crear un nuevo objeto o usar el comando malloc), el sistema asignará una parte de la memoria preasignada al usuario. La regla específica es comenzar desde la dirección inicial División (en realidad, la dirección de inicio tendrá un dato estático, que se ignora aquí). Por ejemplo, si el usuario solicita 10 bytes de memoria, se le asignará desde la dirección inicial 0x1000 y continuará hasta la dirección 0x100A. Si se requieren 22 bytes, entonces se asignará a 0x1020.

Esta área de memoria dividida por la solicitud activa del usuario se llama Heap. Comienza desde la dirección inicial y crece desde el bit bajo (dirección) hasta el bit alto (dirección). Una característica importante de Heap es que no desaparecerá automáticamente, debe ser liberado manualmente o reciclado por el mecanismo de recolección de basura.

Sexto, modelo de memoria: Pila

A excepción de Heap, otro uso de memoria se denomina Stack. En pocas palabras, Stack es el área de memoria ocupada temporalmente por la función en ejecución.

Consulte el ejemplo siguiente.

intmain () {int a = 2; int b = 3;}

En el código anterior, cuando el sistema comienza a ejecutar la función principal, creará un marco en la memoria para ella, y todas las variables internas principales (como ayb) se almacenan en este marco. Una vez finalizada la ejecución de la función principal, el marco se reciclará, liberará todas las variables internas y ya no ocupará espacio.

¿Qué sucede si se llaman otras funciones dentro de la función?

intmain () {int a = 2; int b = 3; returnadd_a_and_b (a, b);}

En el código anterior, la función add_a_and_b se llama dentro de la función principal. Cuando se ejecuta esta línea, el sistema también creará un nuevo marco para que add_a_and_b almacene sus variables internas. En otras palabras, hay dos marcos al mismo tiempo: principal y add_a_and_b. En términos generales, hay tantos marcos como hay en la pila de llamadas.

Cuando add_a_and_b termine de ejecutarse, su marco se reciclará y el sistema volverá al lugar donde la función principal interrumpió la ejecución en este momento y continuará la ejecución. A través de este mecanismo, se realizan llamadas de función capa por capa y cada capa puede usar sus propias variables locales.

Todos los fotogramas se almacenan en Pila. Debido a que los fotogramas se superponen capa por capa, Pila se denomina pila. Genere un nuevo marco, llamado "empujar a la pila", el inglés es empujar, la recuperación de la pila se llama "pop", el inglés es pop. La característica de Stack es que el último marco que se inserta en la pila es el primero en salir de la pila (porque se llama a la función más interna, la operación termina primero), que se denomina estructura de datos de "último en entrar, primero en salir". Cada vez que finaliza la ejecución de la función, se libera automáticamente un marco, finaliza la ejecución de todas las funciones y se libera toda la pila.

La pila comienza desde la dirección final del área de memoria y se asigna desde el orden superior (dirección) al orden inferior (dirección). Por ejemplo, la dirección final del área de memoria es 0x8000, se supone que la primera trama tiene 16 bytes, luego la siguiente dirección asignada comenzará desde 0x7FF0; la segunda trama asume que se necesitan 64 bytes, luego la dirección se moverá a 0x7FB0.

Siete, instrucciones de la CPU

7.1 Un ejemplo

Después de comprender el modelo de registro y memoria, puede ver qué es el lenguaje ensamblador. A continuación se muestra un ejemplo de programa simple. C.

intadd_a_and_b (int a, int b) {returnna + b;} intmain () {returnadd_a_and_b (2,3);}

gcc convierte este programa en lenguaje ensamblador.

$ gcc-S example.c

Después de ejecutar el comando anterior, se generará un archivo de texto example.s, que es lenguaje ensamblador y contiene docenas de líneas de instrucciones. Digámoslo de esta manera, para una operación simple de un lenguaje de alto nivel, la capa inferior puede consistir en varias o incluso docenas de instrucciones de CPU. La CPU ejecuta estas instrucciones en secuencia para completar este paso.

Después de simplificar example.s, se parece a lo siguiente.

_add_a_and_b: push% ebx mov% eax, [% esp + 8] mov% ebx, [% esp + 12] add% eax,% ebx pop% ebx ret _main: push3push2call _add_a_and_b add% esp, 8ret

Se puede ver que las dos funciones add_a_and_b y main del programa original corresponden a las dos etiquetas _add_a_and_b y _main. Dentro de cada etiqueta está el proceso de ejecución de la CPU convertido por la función.

Cada línea es una operación realizada por la CPU. Está dividido en dos partes, solo tome una de ellas como ejemplo.

empujar% ebx

En esta línea, push es la instrucción de la CPU y% ebx es el operador utilizado por la instrucción. Una instrucción de CPU puede tener cero o más operadores.

A continuación, explicaré el ensamblador línea por línea Se recomienda que los lectores copien este programa en otra ventana, para no desplazarse hacia arriba en la página al leer.

7.2 comando de empuje

De acuerdo con la convención, el programa comienza la ejecución desde la etiqueta _main. En este momento, se crea un marco para main en el Stack y la dirección apuntada por el Stack se escribe en el registro ESP. Si hay datos para escribir en el marco principal más tarde, se escribirán en la dirección guardada en el registro ESP.

Luego, se ejecuta la primera línea de código.

empujar3

La instrucción push se usa para poner el operador en la pila, aquí es para escribir 3 en el marco principal.

Aunque parece simple, la instrucción push en realidad tiene una operación previa. Primero tomará la dirección en el registro ESP, le restará 4 bytes y luego escribirá la nueva dirección en el registro ESP. La resta se usa porque Stack se desarrolla de mayor a menor, y 4 bytes se deben a que el tipo de 3 es int, que ocupa 4 bytes. Después de obtener la nueva dirección, se escribirá 3 en los cuatro bytes al comienzo de esta dirección.

empujar2

La segunda línea es la misma, la instrucción push escribe 2 en el marco principal, la posición está cerca del 3 escrito anteriormente. En este momento, el registro ESP se restará en 4 bytes (acumulados menos 8).

7.3 instrucción de llamada

La instrucción de llamada en la tercera línea se usa para llamar a la función.

llamar a _add_a_and_b

El código anterior significa llamar a la función add_a_and_b. En este momento, el programa buscará la etiqueta _add_a_and_b y creará un nuevo marco para la función.

El código de _add_a_and_b se ejecutará a continuación.

empujar% ebx

Esta línea indica que el valor en el registro EBX está escrito en el marco _add_a_and_b. Esto se debe a que si desea usar este registro más adelante, primero debe quitar el valor que contiene y luego volver a escribirlo después de usarlo.

En este momento, la instrucción push restará 4 bytes de la dirección en el registro ESP (acumulativamente menos 12).

7.4 instrucción mov

La instrucción mov se usa para escribir un valor en un registro.

mov% eax, [% esp + 8]

Esta línea de código significa que primero agregue 8 bytes a la dirección en el registro ESP para obtener una nueva dirección, y luego obtenga datos de la Pila de acuerdo con esta dirección. De acuerdo con los pasos anteriores, se puede deducir que lo que se saca aquí es 2, y luego 2 se escribe en el registro EAX.

La siguiente línea de código hace lo mismo.

mov% ebx, [% esp + 12]

El código anterior agrega 12 bytes al valor del registro ESP y luego obtiene los datos del Stack de acuerdo con esta dirección, esta vez obtiene 3 y los escribe en el registro EBX.

7.5 agregar comando

La instrucción de suma se usa para sumar dos operadores y escribir el resultado en el primer operador.

añadir% eax,% ebx

El código anterior agrega el valor del registro EAX (es decir, 2) al valor del registro EBX (es decir, 3) para obtener el resultado 5, y luego escribe este resultado en el primer registro EAX del operador.

7.6 comando pop

La instrucción pop se usa para buscar el último valor escrito en Stack (es decir, el valor de la dirección más baja) y escribir este valor en la ubicación especificada por el operador.

pop% ebx

El código anterior significa sacar el valor escrito recientemente por Stack (es decir, el valor original del registro EBX) y luego volver a escribir este valor en el registro EBX (debido a que la suma ya se ha realizado, el registro EBX no se usa).

Tenga en cuenta que la instrucción pop también agregará 4 a la dirección en el registro ESP, es decir, se recuperarán 4 bytes.

7.7 instrucción ret

La instrucción ret se usa para terminar la ejecución de la función actual y devolver la ejecución derecha a la función superior. Es decir, se reciclará el marco de la función actual.

derecho

Como puede ver, esta instrucción no tiene operadores.

Cuando la función add_a_and_b termina la ejecución, el sistema regresa al lugar donde se interrumpió la función principal hace un momento y continúa la ejecución.

agregar% esp, 8

El código anterior significa que agregue manualmente 8 bytes a la dirección en el registro ESP y vuelva a escribirlo en el registro ESP. Esto se debe a que el registro ESP es la dirección de inicio de escritura de la pila. La operación pop anterior ya recuperó 4 bytes. Aquí se recuperan 8 bytes, lo que equivale a toda la recuperación.

derecho

Finalmente, la función principal finaliza y la instrucción ret sale de la ejecución del programa.

Supongo que te gusta

Origin blog.csdn.net/qq_36171263/article/details/96834171
Recomendado
Clasificación