Análisis de escenario del código fuente siete del programador de idiomas de Go: proceso de llamada de función

El siguiente contenido se reproduce de  https://mp.weixin.qq.com/s/3RUjui-q6bgRnUW7TgOjmA

Awa encanta escribir programas originales Zhang  source Travels  2019-04-22

En las secciones anteriores, presentamos los conocimientos básicos de los registros de la CPU, la memoria, las instrucciones de ensamblaje y las pilas. Para lograr el propósito de comprender y profundizar la comprensión, en esta sección aplicaremos de manera integral lo que hemos aprendido y analizaremos la ejecución. y proceso de llamada de funciones.

Los problemas en los que debemos centrarnos en esta sección son:

  • ¿Cómo salta la CPU de la persona que llama a la ejecución de la función llamada?

  • ¿Cómo se pasan los parámetros de la persona que llama a la función llamada?

  • ¿Cómo está ocupada la memoria por las variables locales de función asignadas en la pila?

  • ¿Cómo se devuelve el valor de retorno de la función llamada a la persona que llama?

  • ¿Qué trabajo de limpieza se debe realizar después de que se ejecuta la función?

Después de resolver estos problemas, tenemos una comprensión general del principio de ejecución por computadora, que es muy importante para nuestra comprensión de la programación de gorutinas.

En comparación con go, el lenguaje C está más cerca del hardware, y el código ensamblador compilado es más simple e intuitivo, lo que nos facilita comprender los principios básicos de las llamadas a funciones, así que veamos primero cómo son las llamadas a funciones en lenguaje C en el nivel de instrucción de ensamblaje Realice y luego analice el proceso de llamada de función del lenguaje go sobre esta base.

Proceso de llamada de función en lenguaje C

Comencemos el análisis con un programa de ejemplo simple.

#include <stdio.h> 

// Suma los parámetros ayb 
int sum (int a, int b) 
{ 
        int s = a + b; 

        return s; 
} 

// función principal: entrada del programa 
int main (int argc, char * argv []) 
{ 
        int n = sum (1, 2); // llamar a la función sum para sum 

        printf ("n:% d \ n", n); // mostrar el valor de n en la pantalla 

        return 0 ; 
}

Compile este programa con gcc para obtener la llamada al programa ejecutable y luego use gdb para depurar. En gdb, usamos disass main para desensamblar la función principal y encontrar que la dirección de la primera instrucción de main es 0x0000000000400540, y luego usamos b * 0x0000000000400540 para colocar un punto de interrupción en esta dirección y ejecutar el programa:

bobo @ ubuntu: ~ / study / c $ gdb ./call 
(gdb) disass main 
Volcado del código ensamblador para la función main: 
  0x0000000000400540 <+0>: push% rbp 
  0x0000000000400541 <+1>: mov% rsp,% rbp 
  0x0000000000400544 < +4>: sub $ 0x20,% rsp 
  0x0000000000400548 <+8>: mov% edi, -0x14 (% rbp) 
  0x000000000040054b <+11>: mov% rsi, -0x20 (% rbp) 
  0x000000000040054f <+15>: mov $ 0 x2,% esi 
  0x0000000000400554 <+20>: mov $ 0x1,% edi 
  0x0000000000400559 <+25>: callq 0x400526 <sum> 
  0x000000000040055e <+30>: mov% eax, -0x4 (% rbp) 
  0x0000000000400561 <+33>: mov -0x4 (% rbp),% eax 
  0x0000000000400564 <+36>: mov% eax,% esi 
  0x0000000000400566 <+38>: mov era $ 0x400604,% 
  0x000000000040056b <+43>: mov $ 0x0,% eax
  0x0000000000400570 <+48>: callq 0x400400 <printf @ plt> 
  0x0000000000400575 <+53>: mov $ 0x0,% eax 
  0x000000000040057a <+58>: leaveq 
  0x000000000040057b <+59>: retq   
Fin del volcado del ensamblador. 
(gdb) b * 0x0000000000400540 
Punto de interrupción 1 en 0x400540 
(gdb) r 
Programa de inicio: / home / bobo / study / c / call 
Breakpoint 1, 0x0000000000400540 en main ()

El programa se detuvo en nuestro punto de interrupción, que es la posición de la primera instrucción de la función principal. Desensamble la función principal para que se ejecute nuevamente, veamos las primeras tres instrucciones:

(gdb) disass 
Volcado de código ensamblador para la función main: 
=> 0x0000000000400540 <+0>: push% rbp 
  0x0000000000400541 <+1>: mov% rsp,% rbp 
  0x0000000000400544 <+4>: sub $ 0x20,% rsp 
  ... ...

Estas 3 instrucciones generalmente se llaman prólogos de función. Básicamente, cada función comienza con el prólogo de función. Su función principal es guardar el registro rbp del llamador y asignar espacio de pila para la función actual. Introduciremos estas 3 instrucciones en detalle más adelante. explicar el formato del código desensamblado generado por gdb. El código desensamblado por gdb se divide principalmente en tres partes:

  • Dirección de instrucción

  • El desplazamiento de la instrucción con respecto a la dirección de inicio de la función actual en bytes

  • instrucción

Por ejemplo, la primera línea del código 0x0000000000400540 <+0>: push% rbp, lo que significa que la dirección de la primera instrucción push% rbp de la función principal en la memoria es 0x0000000000400540, y el desplazamiento es 0 (porque es la primera instrucción de la función principal). Los componentes de esta línea de código se muestran en la siguiente figura:

 

 

Una cosa a tener en cuenta aquí es que la dirección de instrucción y el desplazamiento en el resultado de la salida de desensamblaje de gdb simplemente se agregan por gdb para facilitarnos la lectura del código. El código almacenado en la memoria y ejecutado por la CPU es solo el instrucción parte de la figura anterior.

Tenga en cuenta que hay un símbolo => en el lado izquierdo de la primera línea de código en el resultado de desmontaje anterior, que indica que esta instrucción es la siguiente instrucción que ejecutará la CPU, es decir, el valor actual del registro de extracción es 0x0000000000400540, y el estado actual es La instrucción anterior se ha ejecutado, pero esta instrucción aún no se ha iniciado. Use ir rbp rsp rip para verificar los valores de los tres registros: rbp, rsp y rip:

(gdb) ir rbp rsp rip 
rbp 0x4005800x400580 <__libc_csu_init> 
rsp 0x7fffffffe5180x7fffffffe518 
rip 0x4005400x400540 <main>

Según los valores de estos registros, el estado de la pila de llamadas de función, rbp, rsp y rip en el momento actual y la relación entre ellos se muestran en la siguiente figura:

 

imagen

 

 

Debido a que rbp, rsp y rip almacenan direcciones, cada uno de estos registros es equivalente a un puntero. En la figura anterior, rip apunta a la primera instrucción de la función principal y rsp apunta a la pila de la pila de llamadas de función actual. , y el registro rbp no apunta a la pila ni a las instrucciones que nos preocupan, por lo que no se dibuja su punto específico, pero se muestra su valor.

Para entender más claramente el flujo de ejecución del programa, ahora comenzamos a simular la CPU desde la primera instrucción de la función principal hasta que se ejecuta la función principal completa.

Ahora comience a ejecutar la primera instrucción,

0x0000000000400540 <+0>: push% rbp # Guarde el valor del registro rbp de la persona que llama

Esta instrucción guarda temporalmente el valor del registro de direcciones de la base de la pila rbp en el marco de la pila de la función principal, porque la función principal necesita usar este registro para almacenar su propia dirección de la base de la pila, y el llamador también pone su base de la pila antes de llamar al función principal La dirección se almacena en este registro, por lo que la función principal necesita guardar el valor en este registro primero, y luego restaurar el registro a su estado original cuando main regresa después de la ejecución. Si no restaura su estado original, el llamador usa el registro rbp después de que regresa la función principal.Cuando se ejecuta el código de la persona que llama, rbp debe apuntar a la pila de la persona que llama, pero ahora apunta a la pila de la función principal.

Antes de esta instrucción, el código todavía está usando el marco de pila de la persona que llama. Después de ejecutar esta instrucción, comienza a usar el marco de pila de la función principal. Actualmente, el marco de pila de la función principal solo guarda el valor rbp de la persona que llama. Antes de continuar ejecutar la siguiente instrucción, el estado de la pila y los registros es como se muestra en la figura siguiente La instrucción marcada en rojo en la figura indica la instrucción que se acaba de ejecutar. Puede ver que los valores de los registros rsp y rip han cambiado y todos apuntan a nuevas ubicaciones. rsp apunta a la posición inicial del marco de pila de la función principal y rip apunta a la segunda instrucción de la función principal.

 

imagen

 

 

En la sección de instrucciones de ensamblaje, introdujimos que la ejecución de la instrucción push modificará el valor del registro rsp, pero no modificará el registro rip. ¿Por qué rip cambia aquí? De hecho, esto lo hace automáticamente la CPU. La propia CPU sabe que la longitud de cada instrucción a ejecutar es de varios bytes. Por ejemplo, la instrucción push% rbp aquí tiene solo 1 byte de longitud, por lo que comienza a ejecutar esta instrucción El valor de rip será +1, porque el valor de rip antes de la ejecución de esta instrucción es 0x400540, y después de +1 se convierte en 0x400541, lo que significa que apunta a la segunda instrucción de la función principal.

Luego ejecute la segunda instrucción,

0x0000000000400541 <+1>: mov% rsp,% rbp # Ajusta el registro rbp para que apunte a la posición inicial del marco de pila de la función principal

Esta instrucción copia el valor de rsp al registro rbp y lo hace apuntar a la posición inicial del marco de pila de la función principal. Después de ejecutar esta instrucción, los registros rsp y rbp tienen el mismo valor, y todos apuntan al principio del marco de pila de la función principal. Posición de inicio, como se muestra en la figura siguiente:

 

imagen

 

 

Luego ejecute la tercera instrucción,

0x0000000000400544 <+4>: sub $ 0x20,% rsp # Ajuste el valor del registro rsp para reservar espacio de pila para variables locales y temporales

Esta instrucción resta 32 (0x20 en hexadecimal) del valor del registro rsp, haciéndolo apuntar a una posición más baja en el espacio de la pila. Este paso parece simplemente modificar el valor del registro rsp, pero su esencia es reservar 32 (0x20) bytes de espacio de pila para las variables locales y variables temporales de la función principal. ¿Por qué está reservado en lugar de asignado, porque el sistema operativo completa automáticamente la asignación de pila, y el sistema operativo lo hará cuando el programa starts Un gran bloque de memoria se nos asigna como una pila de llamadas de función La cantidad de memoria de pila que usa el programa está determinada por el registro superior de pila rsp.

Después de que se ejecuta la instrucción, la memoria de pila desde la posición apuntada por rsp hasta la parte apuntada por rbp constituye un marco de pila completo de la función principal, cuyo tamaño es de 40 bytes (se utilizan 8 bytes para guardar el rbp del llamador, y 32 Bytes se utilizan para las variables locales y temporales de la función principal), como se muestra en la siguiente figura:

 

imagen

 

 

Ejecutamos las siguientes 4 instrucciones juntas,

0x0000000000400548 <+8>: mov% edi, -0x14 (% rbp) 
0x000000000040054b <+11>: mov% rsi, -0x20 (% rbp) 
0x000000000040054f <+15>: mov $ 0x2,% esi #sum function second Pon dos parámetros en el registro esi 
0x0000000000400554 <+20>: mov $ 0x1,% edi # El primer parámetro de la función de suma se coloca en el registro edi

Las dos primeras instrucciones se encargan de guardar los dos parámetros obtenidos por la función main en el stack frame de la función main.Como puedes ver, el método de rbp plus offset se usa para acceder a la memoria de la pila. La razón para guardar los dos parámetros de la función principal aquí es porque la persona que llama usa los registros edi y rsi para pasar los dos parámetros argc (entero) y argv (puntero) a la función principal cuando llama a la función principal, y al usuario principal. necesita usar estos dos registros para pasar parámetros a la función de suma. Para no sobrescribir argc y argv, primero debe guardar estos dos parámetros en la pila y luego poner los dos parámetros 1 y 2 pasados ​​a la función de suma en Entre estos dos registros.

Las siguientes dos instrucciones están preparando parámetros para la función de suma. Podemos ver que el primer parámetro pasado a suma se coloca en el registro edi y el segundo parámetro se coloca en esi. Puede preguntar, ¿cómo sabe la función llamada que los parámetros se colocan en estos dos registros? De hecho, esto es solo un acuerdo. Todos están de acuerdo: al llamar a una función, el llamador es responsable de poner el primer parámetro en rdi y el segundo parámetro en rsi, y la función llamada va directamente a estos dos registros para tomar los parámetros. .salir. Hay otro detalle aquí. Los dos parámetros pasados ​​a sum son edi y esi en lugar de rdi y rsi. La razón es que int es de 32 bits en lenguaje C, mientras que rdi y rsi son de 64 bits, edi y esi. utilizarse como parte de rdi y rsi respectivamente.

Volviendo al tema, el diagrama de estado de la pila y los registros después de ejecutar estas 4 instrucciones (tenga en cuenta que argc en la figura siguiente usa los 4 bytes superiores de la memoria continua de 8 bytes en la figura, y los 4 bytes inferiores no se usan) :

 

imagen

 

 

Una vez que los parámetros estén listos, ejecute la instrucción de llamada para llamar a la función de suma,

0x0000000000400559 <+25>: callq 0x400526 <sum> # Llamar a la función de suma

La instrucción de llamada es un poco especial. Cuando se ejecuta por primera vez, rip apunta a la siguiente instrucción de la instrucción de llamada, lo que significa que el valor del registro de extracción es la dirección 0x40055e, pero durante la ejecución de la instrucción de llamada, la llamada La instrucción cambiará el valor actual de rip. (0x40055e) en la pila, y luego modificará el valor de rip al operando después de la instrucción de llamada, aquí está 0x400526, que es la dirección de la primera instrucción de la función de suma, de modo que la CPU saltará a la función de suma para su ejecución.

Después de que se ejecuta la instrucción de llamada, el estado de la pila y los registros se muestra en la figura siguiente. Puede ver que rip ha apuntado a la primera instrucción de la función de suma. La dirección de la instrucción que debe ejecutarse después de la suma. devuelve la función, y la dirección 0x40055e también se ha guardado en la función principal.

 

imagen

 

 

Dado que la instrucción de llamada que llama a la función de suma se ejecuta en main, la CPU ahora salta a la función de suma para iniciar la ejecución,

0x0000000000400526 <+0>: push% rbp           
0x0000000000400527 <+1>: mov% rsp,% rbp  
0x000000000040052a <+4>: mov% edi, -0x14 (% rbp)   
0x000000000040052d <+7>: mov% esi, -0x18 ( % rbp)   
0x0000000000400530 <+10>: mov -0x14 (% rbp),% edx 
0x0000000000400533 <+13>: mov -0x18 (% rbp),% eax 
0x0000000000400536 <+16>: agregar% edx,% eax 
0x0000000000400538 <+ 18>: mov% eax, -0x4 (% rbp) 
0x000000000040053b <+21>: mov -0x4 (% rbp),% eax 
0x000000000040053e <+24>: pop% rbp 
0x000000000040053f <+25>: retq  

Las dos primeras instrucciones de la función de suma son exactamente las mismas que las dos primeras instrucciones de la función principal.

0x0000000000400526 <+0>: push% rbp # El preámbulo de la función de suma, guarda el rbp de la persona que llama 
0x0000000000400527 <+1>: mov% rsp,% rbp # El preámbulo de la función de suma, ajusta el registro rbp para que apunte al inicio posición del marco de la pila

Todos guardan el rbp de la persona que llama y luego configuran el nuevo valor para que apunte a la posición inicial del marco de pila de funciones actual, donde la función de suma guarda el valor del registro rbp de la función principal (0x7fffffffe510) y hace que el registro rbp apuntar a su propio marco de pila La posición inicial (la dirección es 0x7fffffffe4e0).

Se puede ver que el prólogo de la función de suma no reserva espacio de pila para las variables locales y variables temporales para la función de suma ajustando el valor del registro rsp como la función principal. ¿Significa esto que la función de suma no usa la pila? para guardar Las variables locales, de hecho, no lo son. Del análisis posterior, podemos ver que la variable local s de la función de suma todavía está almacenada en la pila. ¿Por qué podemos usarlo si no hay reserva? La razón también se mencionó anteriormente, la memoria en la pila no necesita ser asignada en el código de la capa de aplicación, el sistema operativo ya la ha asignado para nosotros, solo úsela. La función principal necesita ajustar el valor del registro rsp porque necesita usar la instrucción de llamada para llamar a la función de suma, y ​​la instrucción de llamada restará automáticamente 8 del valor del registro rsp y guardará la dirección de retorno de la función en la ubicación de la memoria de pila apuntada por rsp Si la función principal no ajusta el valor de rsp, la instrucción de llamada sobrescribirá el valor de la variable local o variable temporal cuando se guarde la dirección de retorno de la función; y no hay instrucción en la suma función que utilizará automáticamente el registro rsp para guardar los datos en la pila, por lo que no es necesario ajustar el registro rsp.

Las siguientes 4 instrucciones,

0x000000000040052a <+4>: mov% edi, -0x14 (% rbp) # Ponga el primer parámetro a en la variable temporal 
0x000000000040052d <+7>: mov% esi, -0x18 (% rbp) # Ponga el segundo parámetro b Ponga el variable temporal 
0x0000000000400530 <+10>: mov -0x14 (% rbp),% edx # Leer el primero de la variable temporal al registro edx 
0x0000000000400533 <+13>: mov -0x18 (% rbp),% eax # Leer el segundo de la variable temporal al registro eax

Guarde los parámetros pasados ​​por main to sum en el marco de pila actual agregando el desplazamiento al registro rbp, y luego sáquelos y póngalos en los registros. Esto es un poco redundante, porque no especificamos el nivel de optimización para gcc cuando compilamos, gcc No se realiza ninguna optimización de forma predeterminada al compilar el programa, por lo que el código parece detallado.

Las siguientes instrucciones

0x0000000000400536 <+16>: agregar% edx,% eax # Ejecutar a + by guardar el resultado en el registro eax 
0x0000000000400538 <+18>: mov% eax, -0x4 (% rbp) # Asignar el resultado de la suma a la variable s 
0x000000000040053b <+21>: mov -0x4 (% rbp),% eax # lee el valor de la variable s en el registro eax

La primera instrucción sumar es responsable de realizar la operación de suma y almacenar el resultado 3 en el registro eax, la segunda instrucción es responsable de guardar el valor del registro eax en la memoria donde se encuentra la variable s, y la tercera instrucción leer el valor de la variable s a eax Register, puede ver que la variable local s está ordenada por el compilador en la memoria correspondiente a la dirección rbp-0x4.

En este punto, la función principal de la función de suma se ha completado. Antes de continuar con la ejecución de las dos últimas instrucciones, echemos un vistazo al estado de los registros y la pila:

 

imagen

 

 

Hay 1 punto en la imagen de arriba que debe explicarse:

  • Los dos parámetros y el valor de retorno de la función de suma son de tipo int, que ocupa solo 4 bytes en la memoria. En nuestro diagrama esquemático, cada unidad de memoria de pila ocupa 8 bytes y está alineada de acuerdo con el límite de dirección de 8 bytes. es lo que parece ahora en el diagrama.

 

Continuemos ejecutando la instrucción pop% rbp, que contiene dos operaciones:

  1. Coloque el valor en la memoria de pila al que apunta el rsp actual en el registro rbp, de modo que rbp se restaure al valor cuando la primera instrucción de la función de suma no se haya ejecutado, es decir, apunte a la dirección de inicio del stack frame de la función principal nuevamente.

  2. Agregue 8 al valor en el registro rsp, de modo que rsp apunte a la memoria de pila que contiene el valor 0x40055e, y el valor en esta unidad de pila se coloca en la instrucción de llamada cuando la función principal llama a la función de suma, y ​​el valor se ingresa is tight La dirección de la siguiente instrucción que sigue a la instrucción de llamada.

Echemos un vistazo al diagrama esquemático:

 

imagen

 

 

Continuando con la instrucción retq, esta instrucción toma el 0x40055e en la unidad de pila señalada por rsp al registro de extracción y, al mismo tiempo, suma 8 a rsp, de modo que el valor en el registro de extracción se convierte en la siguiente instrucción de la instrucción de llamada. que llama a sum en la función principal, por lo que vuelve a la función principal para continuar con la ejecución. Tenga en cuenta que el valor en el registro eax es 3, que es el valor de retorno después de que se ejecuta la función de suma. Echemos un vistazo al estado.

 

imagen

 

 

Continuar ejecutando en la función principal

mov% eax, -0x4 (% rbp) # Asignar el valor de retorno de la función de suma a la variable n

Esta instrucción coloca el valor (3) en el registro eax en la memoria apuntada por rbp-4, donde se encuentra la variable n, por lo que esta instrucción realmente asigna el valor de retorno de la función de suma a la variable n. El estado en este momento es:

 

imagen

 

 

Las siguientes instrucciones

0x0000000000400561 <+33>: mov -0x4 (% rbp),% eax 
0x0000000000400564 <+36>: mov% eax,% esi 
0x0000000000400566 <+38>: mov $ 0x400604,% edi 
0x000000000040056b <+43>: mov $ 0x0, % eax 
0x0000000000400570 <+48>: callq 0x400400 <printf @ plt> 
0x0000000000400575 <+53>: mov $ 0x0,% eax

Primero prepare los parámetros para la función printf y luego llame a la función printf. No los analizaremos aquí, porque el proceso de llamar a printf y sum es similar. Dejamos que la CPU ejecute rápidamente estas instrucciones y luego haga una pausa en la penúltima de las función principal. En la instrucción leaveq, los estados de la pila y el registro en este momento son los siguientes:

 

 

 

La función de una instrucción mov $ 0x0,% eax por encima de la instrucción leaveq es poner el valor de retorno 0 de la función principal en el registro eax, y la función que llama a la función principal después de los retornos principales puede obtener este valor de retorno. Ahora ejecute el comando leaveq,

0x000000000040057a <+58>: dejarq

Esta instrucción es equivalente a las siguientes dos instrucciones:

mov% rbp,% rsp 
pop% rbp

La instrucción leaveq primero copia el valor en el registro rbp a rsp, de modo que rsp apunte a la unidad de pila apuntada por rbp, y luego POPs el valor en la unidad de memoria al registro rbp, de modo que los valores de rbp y rsp se restauran a recién ingresados ​​El estado de la función principal es ahora. Mira la foto:

 

imagen

 

 

En este punto, la función principal solo queda con la instrucción retq, la cual ha sido analizada en detalle al analizar la función suma, una vez ejecutada esta instrucción, regresará completamente a la función que llamó a la función principal para continuar con la ejecución.

Proceso de llamada de función en idioma go

Pasé mucho tiempo analizando el proceso de llamada de función del lenguaje C, incluido el paso de parámetros, la instrucción de llamada, la instrucción ret y cómo se devuelve el valor de retorno de la función llamada a la función que llama. Con estos fundamentos, vamos a continuación Mirando el proceso de llamada a la función en el lenguaje go, de hecho, el principio de los dos es el mismo, pero hay una pequeña diferencia en los detalles. Todavía use un ejemplo simple para analizar.

package main 

// Calcula la 
suma de los cuadrados de a, b func sum (a, b int) int { 
        a2: = a * a 
        b2: = b * b 
        c: = a2 + b2 

        return c 
} 

func main () { 
sum (1, 2) 
}

Utilice go build para compilar el programa. Tenga en cuenta que debe especificar -gcflags "-N -l" para desactivar la optimización del compilador, de lo contrario, el compilador puede optimizar la llamada a la función de suma.

bobo @ ubuntu: ~ / study / go $ go build -gcflags "-N -l" sum.go

Después de la compilación, se obtiene la suma del programa ejecutable binario. Primero, veamos el código de desmontaje de la función principal:

Volcado de código ensamblador para la función main.main: 
  0x000000000044f4e0 <+0>: mov% fs: 0xfffffffffffffff8,% rcx # No preste atención a 
  0x000000000044f4e9 <+9>: cmp 0x10 (% rcx),% rsp # No preste atención a 
  0x000000000044f4ed <+ 13>: jbe 0x44f51d <main.main + 61> # No preste atención a 
  0x000000000044f4ef por ahora <+15>: sub $ 0x20,% rsp #Reserve 32 bytes de espacio de pila para la función principal 
  0x000000000044f4f3 <+19>: mov% rbp, 0x18 (% rsp) #Guardar el registro rbp de la persona que llama 
  0x000000000044f4f8 <+24>: lea 0x18 (% rsp),% rbp # Ajustar rbp para apuntar a la dirección de inicio de la pila de funciones principal frame 
  0x000000000044f4fd <+29>: movq $ 0x1, (% rsp) # El primer parámetro de la función de suma (1) en la pila 
  0x000000000044f505 <+37>: movq $ 0x2,0x8 (% rsp) # El segundo parámetro de la función suma (2) en la pila 
  0x000000000044f50e <+46>: callq 0x44f480 <main.sum> # Llame a la función suma  
  0x000000000044f513 <+51>:mov 0x18 (% rsp),% rbp # Restaurar el valor del registro rbp en el rbp de la persona que llama
  0x000000000044f518 <+56>:agregue $ 0x20,% rsp #Ajuste rsp para apuntar a la unidad de pila que contiene la dirección de retorno de la persona que llama
  0x000000000044f51c <+60>: retq # Regresar a la persona que llama 
  0x000000000044f51d <+61>: callq 0x447390 <runtime.morestack_noctxt> # No prestes atención a 
  0x000000000044f522 <+66>: jmp 0x44f4e0 <main.main> # Don't pay atención al 
final del vaciado del ensamblador temporalmente .

Las primeras tres y las dos últimas instrucciones de la función principal son el código insertado por el compilador go para comprobar el desbordamiento de la pila. No es necesario que prestemos atención ahora. Las otras partes son similares a las funciones en el lenguaje C, pero la diferencia es que los parámetros se colocan en la pila cuando se llama a la función del lenguaje Go (las instrucciones 7 y 8 colocan los parámetros en la pila), como se puede ver de la cuarta instrucción El compilador reserva 32 bytes para que la función principal almacene la dirección base de la pila principal rbp y los dos parámetros cuando se llama a la función suma. Estos tres elementos ocupan cada uno 8 bytes, por lo que ocupan 24 bytes en total. otros 8 bytes utilizados para? Como se puede ver en la función de suma a continuación, los 8 bytes restantes se utilizan para almacenar el valor de retorno de la función de suma.

Volcado del código ensamblador para la función main.sum: 
  0x000000000044f480 <+0>: sub $ 0x20,% rsp #Reserva 32 bytes de espacio de pila para la función de suma 
  0x000000000044f484 <+4>: mov% rbp, 0x18 (% rsp) # Save el rbp de la función principal 
  0x000000000044f489 <+9>: lea 0x18 (% rsp),% rbp # Establezca el rbp de la función suma 
  0x000000000044f48e <+14>: movq $ 0x0,0x38 (% rsp) # El valor de retorno se inicializa a 0 
  0x000000000044f497 <+ 23>: mov 0x28 (% rsp),% rax # Lee el primer parámetro a (1) de la memoria a rax 
  0x000000000044f49c <+28>: mov 0x28 (% rsp),% rcx # Lee el primer parámetro de la memoria Un parámetro a (1) a rcx 
  0x000000000044f4a1 <+33>: imul% rax,% rcx # Calcule a * a, y ponga el resultado en rcx 
  0x000000000044f4a5 <+37>: mov% rcx, 0x10 (% rsp) # put El valor de rcx (a * a) se asigna a la variable a2 
  0x000000000044f4aa <+42>: mov 0x30 (% rsp),% rax # Lee el segundo parámetro a (2) de la memoria a rax 
  0x000000000044f4af <+47> :mov 0x30 (% rsp),% rcx # Lee el segundo parámetro a (2) de la memoria a rcx
  0x000000000044f4b4 <+52>: imul% rax,% rcx # Calcule b * b, y ponga el resultado en rcx 
  0x000000000044f4b8 <+56>: mov% rcx, 0x8 (% rsp) #Asigne el valor de rcx (b * b) Dar la variable b2 
  0x000000000044f4bd <+61>: mov 0x10 (% rsp),% rax # Leer a2 de la memoria para registrar rax 
  0x000000000044f4c2 <+66>: agregar% rcx,% rax # Calcular a2 + b2, y guardar el resultado en rax 
  0x000000000044f4c5 <+69>: mov% rax, (% rsp) # Asignar rax a la variable c, c = a2 + b2 
  0x000000000044f4c9 <+73>: mov% rax, 0x38 (% rsp) #Establecer el valor de rax (a2 + b2) Copiar al valor de retorno 
  0x000000000044f4ce <+78>: mov 0x18 (% rsp),% rbp # Restaurar el rbp de la función principal 
  0x000000000044f4d3 <+83>: agregar $ 0x20,% rsp # Ajustar rsp para apuntar al save return La unidad de pila de la dirección 
  0x000000000044f4d7 <+87>: retq #Return a la función principal 
Fin del volcado del ensamblador.

El código ensamblador de la función de suma es relativamente intuitivo. Básicamente es una traducción directa de la función de suma del lenguaje go. Puede ver que la función de suma obtiene parámetros de la pila de funciones principal a través del registro rsp, y el valor de retorno es también se almacena en el marco de pila de la función principal a través de rsp.

La siguiente figura muestra la relación entre la pila y los registros de pila cuando la función de suma 0x000000000044f4c9 <+73>: mov% rax, 0x38 (% rsp) se ha ejecutado pero la siguiente instrucción aún no se ha ejecutado. Los lectores pueden combinar lo anterior. El código ensamblador y esta figura profundizan la comprensión de la posición y la relación del paso de parámetros, el valor de retorno y las variables locales en la pila en el proceso de llamada a la función.

 

imagen

 

 

para resumir

Finalmente, resumamos el proceso de llamada a la función:

  1. Paso de parámetros. El código C / c ++ compilado por gcc generalmente pasa parámetros a través de registros. En la plataforma Linux AMD64, gcc está de acuerdo en que los primeros seis parámetros de la llamada de función se pasan a través de rdi, rsi, rdx, r10, r8 y r9 respectivamente; mientras que los parámetros de go language la llamada a la función se pasa a la función llamada a través de la pila, el último parámetro se inserta primero en la pila y el primer parámetro se inserta en la pila por último. El parámetro está en el marco de la pila del llamador y la función llamada obtiene el parámetro por agregar un cierto desplazamiento a rsp;

  2. La instrucción de llamada es responsable de empujar el registro de extracción (dirección de retorno de la función) cuando la instrucción de llamada se ejecuta en la pila;

  3. gcc accede a las variables locales y temporales agregando un desplazamiento al rbp, mientras que el compilador go usa el registro rsp y un desplazamiento para acceder a ellas;

  4. La instrucción ret es responsable de introducir la dirección de retorno de la instrucción de llamada en la pila para extraer, para realizar el retorno de la función llamada a la función de llamada para continuar la ejecución;

  5. gcc usa el registro rax para devolver el valor de retorno de la llamada a la función, y go usa la pila para devolver el valor de retorno de la llamada a la función.

     

 


Finalmente, si crees que este artículo te es útil, por favor ayúdame a hacer clic en "Mirar" en la esquina inferior derecha del artículo o reenviarlo al círculo de amigos, ¡muchas gracias!

imagen

Supongo que te gusta

Origin blog.csdn.net/pyf09/article/details/115219485
Recomendado
Clasificación