La stack de llamadas no dice “de dónde vienes”, sino “hacia dónde vas”.

En una pregunta anterior ( Obtener jerarquía de llamadas a objetos ), obtuve esta interesante respuesta :

La stack de llamadas no está ahí para decirte de dónde vienes. Es para decirle a dónde va después.

Por lo que sé, al llegar a una llamada de función, un progtwig generalmente hace lo siguiente:

  1. En el código de llamada :

    • dirección de devolución de la tienda (en la stack de llamadas)
    • guardar estados de registros (en la stack de llamadas)
    • escribir los parámetros que se pasarán a la función (en la stack de llamadas o en los registros)
    • saltar a la función de destino
  2. En el código de destino llamado :

    • Recuperar variables almacenadas (si es necesario)
  3. Proceso de devolución : Deshaga lo que hicimos cuando llamamos a la función, es decir, desenrollar / abrir la stack de llamadas:

    • eliminar variables locales de la stack de llamadas
    • eliminar variables de función de la stack de llamadas
    • restablecer el estado de los registros (el que almacenamos antes)
    • saltar a la dirección de retorno (la que almacenamos antes)

Pregunta:

¿Cómo se puede ver esto como algo que “te dice hacia dónde te diriges luego” en lugar de “decirte de dónde vienes” ?

¿Hay algo en el entorno de tiempo de ejecución de C # JIT o C # que haga que la stack de llamadas funcione de manera diferente?

Gracias por cualquier información sobre la descripción de una stack de llamadas: hay mucha documentación sobre cómo funciona una stack de llamadas tradicional.

Lo has explicado tú mismo. La “dirección de devolución” por definición le dice a dónde va después .

No hay ningún requisito en absoluto que la dirección de devolución que se coloca en la stack sea una dirección dentro del método que llamó al método en el que se encuentra ahora. Normalmente lo es, lo que hace que sea más fácil de depurar. Pero no es un requisito que la dirección de retorno sea una dirección dentro de la persona que llama. El optimizador está permitido, y algunas veces lo hace, con la dirección de retorno si hace que el progtwig sea más rápido (o más pequeño, o lo que sea que esté optimizando) sin cambiar su significado.

El objective de la stack es asegurarse de que cuando termine esta subrutina, su continuación , lo que sucede a continuación, sea correcta. El propósito de la stack no es decirte de dónde vienes. Que generalmente lo hace es un feliz accidente.

Además: la stack es solo un detalle de implementación de los conceptos de continuación y activación . No hay ningún requisito de que ambos conceptos sean implementados por la misma stack; podría haber dos stacks, una para activaciones (variables locales) y una para continuación (direcciones de devolución). Es evidente que estas architectures son mucho más resistentes a los ataques aplastantes de malware porque la dirección de retorno no se acerca a los datos.

Lo que es más interesante, ¡no hay ningún requisito de que haya ninguna stack en absoluto! Utilizamos call stacks para implementar la continuación porque son convenientes para el tipo de progtwigción que normalmente hacemos: llamadas síncronas basadas en subrutinas. Podríamos elegir implementar C # como un lenguaje de “Estilo de paso de continuación”, donde la continuación se reifica como un objeto en el montón , no como un grupo de bytes empujados en una stack de sistema de un millón de bytes . Ese objeto se pasa de un método a otro, ninguno de los cuales usa ninguna stack. (Las activaciones se reifican dividiendo cada método en posiblemente muchos delegates, cada uno de los cuales está asociado con un objeto de activación).

En el estilo de continuación de paso, simplemente no hay stack, y no hay manera de saber de dónde vienes; el objeto de continuación no tiene esa información. Solo sabe hacia dónde te diriges.

Esto podría parecer un gran juego teórico, pero esencialmente estamos haciendo que C # y VB continúen pasando los lenguajes de estilo en la próxima versión; la próxima característica “asincrónica” es solo el estilo de paso de continuación en un delgado disfraz. En la próxima versión, si utiliza la función asíncrona, esencialmente estará renunciando a la progtwigción basada en la stack; no habrá forma de mirar la stack de llamadas y saber cómo llegaste aquí, porque la stack con frecuencia estará vacía.

Continuaciones reificadas como algo más que una stack de llamadas es una idea difícil para mucha gente para hacer sus mentes; Ciertamente fue para mí. Pero una vez que lo tienes, simplemente hace clic y tiene perfecto sentido. Para una introducción suave, aquí hay una serie de artículos que he escrito sobre el tema:

Una introducción a CPS, con ejemplos en JScript:

http://blogs.msdn.com/b/ericlippert/archive/2005/08/08/recursion-part-four-continuation-passing-style.aspx

http://blogs.msdn.com/b/ericlippert/archive/2005/08/11/recursion-part-five-more-on-cps.aspx

http://blogs.msdn.com/b/ericlippert/archive/2005/08/15/recursion-part-six-making-cps-work.aspx

Aquí hay una docena de artículos que comienzan haciendo una inmersión más profunda en CPS, y luego explican cómo funciona todo esto con la próxima función “asincrónica”. Comienza desde abajo:

http://blogs.msdn.com/b/ericlippert/archive/tags/async/

Los lenguajes que admiten el estilo de continuación de paso a menudo tienen una primitiva de flujo de control mágico llamada “llamada con continuación actual” o “call / cc” para abreviar. En esta pregunta de stackoverflow, explico la diferencia trivial entre “esperar” y “llamar / cc”:

¿Cómo podría implementarse la nueva característica de asincronización en c # 5.0 con call / cc?

Para tener en sus manos la “documentación” oficial (un montón de libros blancos), y una versión preliminar de C # y la nueva función “async await” de VB, más un foro para Q & A de soporte, diríjase a:

http://msdn.com/vstudio/async

Considera el siguiente código:

void Main() { // do something A(); // do something else } void A() { // do some processing B(); } void B() { } 

Aquí, lo último que hace la función A es llamar a B A regresa inmediatamente después de eso. Un optimizador inteligente podría optimizar la llamada a B y reemplazarla con solo un salto a la dirección de inicio de B (No estoy seguro de si los comstackdores actuales de C # realizan tales optimizaciones, pero sí lo hacen casi todos los comstackdores de C ++). ¿Por qué esto funcionaría? Debido a que hay una dirección de la persona que llama de la A en la stack, entonces cuando B termina, no volvería a A , sino directamente a la persona que llama.

Entonces, puede ver que la stack no contiene necesariamente la información acerca de dónde proviene la ejecución, sino a dónde debería ir.

Sin optimización, dentro de B la stack de llamadas es (omito las variables locales y otras cosas para mayor claridad):

 ---------------------------------------- |address of the code calling A | ---------------------------------------- |address of the return instruction in A| ---------------------------------------- 

Entonces, el retorno de B vuelve a A e inmediatamente sale de `A.

Con la optimización, la stack de llamadas es solo

 ---------------------------------------- |address of the code calling A | ---------------------------------------- 

Entonces, B regresa directamente a Main .

En su respuesta, Eric menciona otros casos (más complicados) donde la información de la stack no contiene la persona que llama real.

Lo que Eric está diciendo en su publicación es que el puntero de ejecución no necesita saber de dónde viene, solo dónde tiene que ir cuando finaliza el método actual. Estas dos cosas superficialmente parecerían ser lo mismo, pero si el caso de (por ejemplo) la recursión de cola de dónde venimos y hacia dónde vamos puede divergir.

Hay más en esto de lo que piensas.

En C es completamente posible tener un progtwig reescribir la stack de llamadas. De hecho, esa técnica es la base misma de un estilo de explotación conocido como progtwigción orientada al retorno .

También escribí código en un idioma que le daba control directo sobre la stack de llamadas. Podrías desconectar la función que llamó a la tuya y empujar a otra en su lugar. Podría duplicar el elemento en la parte superior de la stack de llamadas, para que el rest del código en la función de llamada se ejecute dos veces, y un montón de otras cosas interesantes. De hecho, la manipulación directa de la stack de llamadas fue la principal estructura de control proporcionada por este lenguaje. (Desafío: ¿alguien puede identificar el idioma de esta descripción?)

Demostró claramente que la stack de llamadas indica hacia dónde se dirige, no dónde ha estado.

Creo que está tratando de decir que le dice al método llamado a dónde ir a continuación.

  • El Método A llama al Método B.
  • El Método B completa, ¿a dónde va ahora?

Aparece la dirección de los métodos de llamada en la parte superior de la stack y luego va hacia allí.

Entonces el Método B sabe a dónde ir después de que se complete. Método B, realmente no le importa de dónde vino.