Técnicamente, ese código va más allá de lo que define el estándar C, por lo que podría hacer cualquier cosa. Está haciendo una gran cantidad de suposiciones a las que no tiene derecho, y esas suposiciones ciertamente no son universalmente ciertas. Sin embargo, puedo presentar una explicación muy probable de por qué ve la salida que hace:
Está en lo correcto hasta el punto donde ha copiado el código de la función g()
en la memoria ocupada por la variable de matriz local a
.
Para comprender la próxima línea, necesita saber un poco acerca de cómo se suelen llamar las funciones en las arquitecturas comunes basadas en la pila. Cuando se invoca una función, los parámetros se envían a la pila, luego se inserta la dirección de retorno en la pila, y la ejecución salta al punto de inicio de la función. Dentro de la función, se inserta el puntero de marco anterior en la pila, luego se hace espacio para las variables locales. Las pilas tienden a crecer hacia abajo en la memoria (desde direcciones altas a direcciones bajas), aunque este no es el caso en todas las arquitecturas comunes.
lo tanto, cuando principales llamadas en función de f()
, la pila se ve inicialmente como esto (el puntero marco y puntero de pila son dos registros de la CPU que contengan direcciones de lugares en la pila):
| ... | (higher addresses)
| char **argv (parameter) |
|-------------------------|
| int argc (parameter) |
|-------------------------|
FRAME POINTER -> | saved frame pointer |
|-------------------------|
| int a |
|-------------------------|
| int x (parameter) | &x
|-------------------------|
STACK POINTER -> | return address | &x - 1
|-------------------------|
| ... | (lower addresses)
El prólogo de función guarda el puntero de marco de la función de llamada y mueve el puntero de pila para crear espacio para las variables locales en f()
. Así que cuando el código C en f()
comienza a ejecutar, la pila ahora se ve algo como esto:
| ... | (higher addresses)
| char **argv (parameter) |
|-------------------------|
| int argc (parameter) |
|-------------------------|
| saved frame pointer |
|-------------------------|
| int a |
|-------------------------|
| int x (parameter) | &x
|-------------------------|
| return address | &x - 1
|-------------------------|
FRAME POINTER -> | saved frame pointer |
|-------------------------|
| a[99] | &a[99]
| a[98] | &a[98]
| ... | ...
STACK POINTER -> | a[0] | &a[0]
| ... | (lower addresses)
¿Cuál es el puntero de marco? Se usa para hacer referencia a variables y parámetros locales dentro de una función. El compilador sabe que cuando se está ejecutando f()
, la dirección de la variable local a
es siempreFRAME_POINTER - 100 * sizeof(int)
, y la dirección del parámetro x
es FRAME_POINTER + sizeof(FRAME_POINTER) + sizeof(RETURN_ADDRESS)
. Se puede acceder a todas las variables y parámetros locales como un desplazamiento fijo desde el puntero del marco, sin importar cómo se mueve el puntero de la pila a medida que se asigna y se desasigna el espacio de la pila.
De todos modos, volvamos al código. Cuando esta línea realiza:
x = *(&x-1) ;
Se copia el valor que se almacena 1 número entero de tamaño menor en la memoria que x
, en x
. Si miras mi ASCII-art, verás que esa es la dirección de retorno.Así que en realidad llevar a cabo esto:
x = RETURN_ADDRESS;
La siguiente línea:
*(&x-1) = (int)(&a) ;
a continuación, establece la dirección de retorno a la dirección de la matriz a
. Que en realidad está diciendo:
RETURN_ADDRESS = &a;
Se requiere que el yeso porque se está tratando la dirección de retorno como un int
, y no un puntero (por lo que, de hecho, este código única trabajo en arquitecturas donde int
es el mismo tamaño como un puntero - ¡esto NO funcionará en sistemas POSIX de 64 bit, por ejemplo!).
Ahora se completa el código C en la función f()
, y el epílogo de función desasigna las variables locales (moviendo el puntero de pila hacia atrás) y restaura el puntero de marco de la persona que llama. En este punto, la pila se ve como:
| ... | (higher addresses)
| char **argv (parameter) |
|-------------------------|
| int argc (parameter) |
|-------------------------|
FRAME POINTER -> | saved frame pointer |
|-------------------------|
| int a |
|-------------------------|
| int x (parameter) | &x
|-------------------------|
STACK POINTER -> | return address | &x - 1
|-------------------------|
| saved frame pointer |
|-------------------------|
| a[99] | &a[99]
| a[98] | &a[98]
| ... | ...
| a[0] | &a[0]
| ... | (lower addresses)
ahora la función devuelve al saltar al valor de RETURN_ADDRESS - pero establece que a &a
, así que en vez de ir de nuevo a donde fue llamado desde, salta a el valor de inicio de la matriz a
- ahora está ejecutando el código de la pila. Aquí es donde copió el código de la función g()
, por lo que el código (aparentemente) se ejecuta felizmente. Tenga en cuenta que debido a que el puntero de la pila se ha movido hacia atrás sobre la matriz aquí, cualquier código asíncrono que se ejecute con la misma pila (como una señal UNIX que llega en el momento incorrecto) sobrescribirá el código.
Así que aquí es lo que la pila ahora se ve como en el inicio de g()
, antes de que el prólogo de la función:
| ... | (higher addresses)
| char **argv (parameter) |
|-------------------------|
| int argc (parameter) |
|-------------------------|
FRAME POINTER -> | saved frame pointer |
|-------------------------|
| int a |
|-------------------------|
STACK POINTER -> | int x (parameter) |
|-------------------------|
| return address |
|-------------------------|
| saved frame pointer |
|-------------------------|
| a[99] |
| a[98] |
| ... |
| a[0] |
| ... | (lower addresses)
El prólogo de g()
continuación, establece una estructura de pila de forma normal, lo ejecuta, y desenrolla, que deja el puntero del marco y el puntero de la pila como en el último diagrama de arriba.
ahora g()
devoluciones, por lo que busca un valor de retorno en la parte superior de la pila - pero la parte superior de la pila (en la que el puntero de pila está apuntando) es en realidad el lugar donde el parámetro x
funcione f()
vivieron - y aquí es donde guardamos el valor de retorno original anterior, por lo que vuelve al lugar desde donde se llamó a f()
.
Como nota al margen, la pila está ahora desincronizados en main()
, ya que espera que el puntero de pila para estar donde estaba cuando se llama f()
(que está apuntando hacia el lugar donde se almacena el parámetro x
) - pero ahora es en realidad apuntando a la variable local a
. Esto causaría algunos efectos extraños: si llamara a otra función desde main
en este punto, el contenido de a
se alteraría.
espero que usted (y otros) han aprendido algo valioso de esta discusión, pero es importante recordar que esto es como la Técnica de cinco puntos palma de estallido del corazón de programación - NUNCA lo utilizan en un sistema real. Una nueva sub-arquitectura, compilador o incluso solo diferentes banderas de compilación pueden cambiar el entorno de ejecución lo suficiente como para hacer que este tipo de código demasiado inteligente por medio falle por completo en todo tipo de formas deliciosas y divertidas.
tengo actualicé sustancialmente mi respuesta para poder responder sus preguntas adicionales en el comentario. – caf