Coger el desbordamiento de la stack

¿Cuál es la mejor manera de atrapar el desbordamiento de stack en C?

Más específicamente:

El progtwig AC contiene un intérprete para un lenguaje de scripting.

Los scripts no son de confianza y pueden contener errores de recursión infinitos. El intérprete debe poder atraparlos y continuar sin problemas. (Obviamente, esto se puede manejar en parte usando una stack de software, pero el rendimiento mejora considerablemente si se pueden escribir grandes porciones de código de biblioteca en C; como mínimo, esto implica funciones de C que se ejecutan sobre estructuras de datos recursivas creadas por scripts).

La forma preferida de atrapar un desbordamiento de stack involucraría longjmp de regreso al bucle principal. (Es perfectamente correcto descartar todos los datos que se guardaron en cuadros de stack debajo del bucle principal).

La solución portátil alternativa es usar direcciones de variables locales para monitorear la profundidad de stack actual, y para cada función recursiva contener una llamada a una función de verificación de stack que usa este método. Por supuesto, esto incurre en una sobrecarga de tiempo de ejecución en el caso normal; También significa que si me olvido de poner la llamada de verificación de stack en un lugar, el intérprete tendrá un error latente.

¿Hay una mejor manera de hacerlo? Específicamente, no espero una mejor solución portátil, pero si tuviera una solución específica del sistema para Linux y otra para Windows, estaría bien.

He visto referencias a algo llamado manejo estructurado de excepciones en Windows, aunque las referencias que he visto han sido sobre la traducción de esto al mecanismo de manejo de excepciones de C ++; ¿Se puede acceder desde C, y si es así, es útil para este escenario?

Entiendo que Linux te permite capturar una señal de falla de segmentación; ¿Es posible convertir esto de manera confiable en un bucle largo de regreso a su bucle principal?

Java parece admitir la captura de excepciones de desbordamiento de stack en todas las plataformas; ¿Cómo implementa esto?

En la parte superior de mi cabeza, una forma de detectar un crecimiento excesivo de la stack es verificar la diferencia relativa en las direcciones de los marcos de stack:

 #define MAX_ROOM (64*1024*1024UL) // 64 MB static char * first_stack = NULL; void foo(...args...) { char stack; // Compare addresses of stack frames if (first_stack == NULL) first_stack = &stack; if (first_stack > &stack && first_stack - &stack > MAX_ROOM || &stack > first_stack && &stack - first_stack > MAX_ROOM) printf("Stack is larger than %lu\n", (unsigned long)MAX_ROOM); ...code that recursively calls foo()... } 

Esto compara la dirección del primer marco de stack para foo() con la dirección de marco de stack actual, y si la diferencia excede MAX_ROOM , escribe un mensaje.

Esto supone que estás en una architecture que usa una stack lineal siempre creciente o siempre creciente, por supuesto.

No tiene que hacer esta comprobación en todas las funciones, pero a menudo se detecta un crecimiento de stack excesivamente grande antes de llegar al límite que ha elegido.

AFAIK, todos los mecanismos para detectar el desbordamiento de la stack incurrirán en un costo de tiempo de ejecución. Podría dejar que la CPU detecte fallas seguras, pero eso ya es demasiado tarde; Probablemente ya has garabateado algo importante.

Usted dice que desea que su intérprete llame el código de la biblioteca precomstackda tanto como sea posible. Eso está bien, pero para mantener la noción de sandbox, su motor de intérprete siempre debe ser responsable, por ejemplo, de las transiciones de stack y la asignación de memoria (desde el punto de vista del lenguaje interpretado); Las rutinas de su biblioteca probablemente deberían implementarse como devoluciones de llamada. La razón es que necesita estar manejando este tipo de cosas en un solo punto, por las razones que ya ha señalado (errores latentes).

Cosas como Java lidian con esto generando código de máquina, por lo que es simplemente un caso de generación de código para verificar esto en cada transición de stack.

(No molestaré esos métodos dependiendo de plataformas particulares para soluciones “mejores”. Crean problemas , al limitar el diseño del lenguaje y la facilidad de uso, con poca ganancia. Para las respuestas “solo funcionan” en Linux y Windows, consulte más arriba).

En primer lugar, en el sentido de C, no puedes hacerlo de manera portátil . De hecho, ISO C no obliga a “astackr” en absoluto. Pedante, incluso parece que cuando falla la asignación de objetos automáticos, el comportamiento es literalmente indefinido, según la Cláusula 4p2, simplemente no hay garantía de lo que sucedería cuando las llamadas se anidaran demasiado. Tienes que confiar en algunas suposiciones adicionales de implementación (de ISA o OS ABI ) para hacer eso, por lo que terminas con C + algo más, no solo C. La generación de código de máquina en tiempo de ejecución tampoco es portátil en el nivel C.

(Por cierto, ISO C ++ tiene la noción de que la stack se desenrolla , pero solo en el contexto del manejo de excepciones. Y aún no hay garantía de comportamiento portátil en el desbordamiento de stack; aunque parece no estar especificado, no está definido).

Además de limitar la profundidad de la llamada, todas las formas tienen algún costo de tiempo de ejecución adicional. El costo sería bastante fácil de observar, a menos que existan algunos medios asistidos por hardware para amortizarlo (como la tabla de páginas caminando). Lamentablemente, este no es el caso ahora.

La única forma portátil que encuentro es no confiar en la stack nativa de la architecture de la máquina subyacente. En general, esto significa que debe asignar los marcos de registro de activación como parte de la tienda libre (en el montón), en lugar de la stack nativa proporcionada por ISA. Esto no solo funciona para implementaciones de lenguaje interpretado, sino también para comstackdas, por ejemplo, SML / NJ. Este enfoque de stack de software no siempre genera un peor rendimiento porque permite proporcionar una abstracción de mayor nivel en el lenguaje del objeto, por lo que los progtwigs pueden tener más oportunidades de optimización, aunque no es probable que sea un intérprete ingenuo.

Tienes varias opciones para lograrlo. Una forma es escribir una máquina virtual . Puede asignar memoria y construir la stack en ella.

Otra forma es escribir código de estilo asíncrono sofisticado (por ejemplo, trampolines o transformación de CPS ) en su implementación, confiando en los marcos de llamada menos nativos como sea posible. En general, es difícil hacerlo bien, pero funciona. Las capacidades adicionales habilitadas de esta manera son una optimización de llamadas de cola más fácil y una captura de continuación de primera clase más fácil.