c assembly c11 function-call noreturn

¿Por qué vuelve la función "noreturn"?



assembly c11 (9)

Leí this pregunta sobre el atributo noreturn , que se usa para funciones que no vuelven a la persona que llama.

Luego hice un programa en C.

#include <stdio.h> #include <stdnoreturn.h> noreturn void func() { printf("noreturn func/n"); } int main() { func(); }

Y generó el ensamblaje del código usando this :

.LC0: .string "func" func: pushq %rbp movq %rsp, %rbp movl $.LC0, %edi call puts nop popq %rbp ret // ==> Here function return value. main: pushq %rbp movq %rsp, %rbp movl $0, %eax call func

¿Por qué la función func() regresa después de proporcionar el atributo noreturn ?


¿Por qué la función func () regresa después de proporcionar el atributo noreturn?

Porque escribiste un código que se lo dijo.

Si no desea que su función regrese, llame a exit() o abort() o similar para que no regrese.

¿Qué otra cosa haría su función además de regresar después de haber llamado printf() ?

El Estándar C en 6.7.4 Especificadores de funciones , el párrafo 12 incluye específicamente un ejemplo de una función de noreturn que realmente puede devolver, y etiqueta el comportamiento como indefinido :

EJEMPLO 2

_Noreturn void f () { abort(); // ok } _Noreturn void g (int i) { // causes undefined behavior if i<=0 if (i > 0) abort(); }

En resumen, noreturn es una restricción que usted coloca en su código: le dice al compilador "MI código nunca volverá" . Si violas esa restricción, eso es todo tuyo.


Como otros han mencionado, este es un comportamiento clásico indefinido. func que func no volvería, pero lo hiciste de todos modos. Puedes recoger las piezas cuando eso se rompa.

Aunque el compilador compila func de la manera habitual (a pesar de su noreturn ), el noreturn afecta las funciones de llamada.

Puede ver esto en la lista de ensamblaje: el compilador ha asumido, en main , que la func no volverá. Por lo tanto, literalmente eliminó todo el código después de la call func ( call func usted mismo en https://godbolt.org/g/8hW6ZR ). La lista de ensamblaje no se trunca, literalmente termina después de la call func porque el compilador asume que cualquier código después de eso sería inalcanzable. Entonces, cuando la func realmente regresa, main comenzará a ejecutar cualquier basura que siga a la función main , ya sea relleno, constantes inmediatas o un mar de 00 bytes. De nuevo, un comportamiento muy indefinido.

Esto es transitivo: se puede suponer que una función que llama a una función de noreturn en todas las rutas de código posibles es en sí misma un noreturn .


De acuerdo a this

Si la función declarada _Noreturn regresa, el comportamiento es indefinido. Se recomienda un diagnóstico del compilador si se puede detectar.

Es responsabilidad del programador asegurarse de que esta función nunca regrese, por ejemplo, salir (1) al final de la función.


El atributo noreturn es una promesa que le hace al compilador sobre su función.

Si regresa de tal función, el comportamiento no está definido, pero esto no significa que un compilador sensato le permitirá alterar completamente el estado de la aplicación al eliminar la declaración ret , especialmente porque el compilador a menudo incluso puede deducir que un retorno es de hecho posible.

Sin embargo, si escribes esto:

noreturn void func(void) { printf("func/n"); } int main(void) { func(); some_other_func(); }

entonces es perfectamente razonable que el compilador elimine el some_other_func completo, si así lo desea.


Los especificadores de funciones en C son una pista para el compilador, el grado de aceptación está definido por la implementación.

En primer lugar, _Noreturn function specifier (o, noreturn , using <stdnoreturn.h> ) es una pista para el compilador sobre una promesa teórica hecha por el programador de que esta función nunca volverá. Basado en esta promesa, el compilador puede tomar ciertas decisiones, realizar algunas optimizaciones para la generación de código.

IIRC, si una función especificada con el especificador de función noreturn eventualmente regresa a su llamador,

  • mediante el uso de una declaración de return explícita
  • al llegar al final del cuerpo de la función

El comportamiento es indefinido . NO DEBE regresar de la función.

Para que quede claro, el uso del especificador de función noreturn no detiene el retorno de un formulario de función a su llamador. Es una promesa hecha por el programador al compilador para permitirle un mayor grado de libertad para generar código optimizado.

Ahora, en caso de que hicieras una promesa antes y después, elige violar esto, el resultado es UB. Se recomienda a los compiladores, pero no es obligatorio, que _Noreturn advertencias cuando una función _Noreturn parece ser capaz de regresar a la persona que llama.

De acuerdo con el capítulo §6.7.4, C11 , párrafo 8

Una función declarada con un especificador de función _Noreturn no volverá a su llamador.

y, el párrafo 12, (¡¡¡ Note los comentarios !! )

EXAMPLE 2 _Noreturn void f () { abort(); // ok } _Noreturn void g (int i) { // causes undefined behavior if i <= 0 if (i > 0) abort(); }

Para C++ , el comportamiento es bastante similar. Citando del capítulo §7.6.4, C++14 , párrafo 2 ( énfasis mío )

Si se llama a una función f donde f se declaró previamente con el atributo noreturn y f finalmente regresa, el comportamiento es indefinido. [Nota: La función puede terminar lanzando una excepción. —Nota final]

[Nota: Se recomienda a las implementaciones que emitan una advertencia si una función marcada [[noreturn]] puede volver. —Nota final]

3 [Ejemplo:

[[ noreturn ]] void f() { throw "error"; // OK } [[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0 if (i > 0) throw "positive"; }

—Ejemplo]


la función sin retorno no guarda los registros en la entrada ya que no es necesario. Facilita las optimizaciones. Ideal para la rutina del planificador, por ejemplo.

Vea el ejemplo aquí: https://godbolt.org/g/2N3THC y descubra la diferencia


noreturn es una promesa. Le está diciendo al compilador: "Puede o no ser obvio, pero sé, según la forma en que escribí el código, que esta función nunca volverá". De esa manera, el compilador puede evitar configurar los mecanismos que permitirían que la función regrese correctamente. Dejar de lado esos mecanismos podría permitir al compilador generar código más eficiente.

¿Cómo puede una función no volver? Un ejemplo sería si llamara a exit() lugar.

Pero si le promete al compilador que su función no regresará, y el compilador no hace arreglos para que sea posible que la función regrese correctamente, y luego va y escribe una función que devuelve, ¿qué se supone que debe hacer el compilador? ¿hacer? Básicamente tiene tres posibilidades:

  1. Sea "amable" con usted y descubra una forma de hacer que la función regrese correctamente de todos modos.
  2. Emite un código que, cuando la función vuelve incorrectamente, se bloquea o se comporta de manera arbitrariamente impredecible.
  3. Darle un mensaje de advertencia o error que indique que incumplió su promesa.

El compilador puede hacer 1, 2, 3 o alguna combinación.

Si esto suena como un comportamiento indefinido, es porque lo es.

La conclusión, en la programación como en la vida real, es: no hagas promesas que no puedas cumplir. Alguien más podría haber tomado decisiones basadas en su promesa, y pueden ocurrir cosas malas si luego rompe su promesa.


ret simplemente significa que la función devuelve el control a la persona que llama. Entonces, main call func , la CPU ejecuta la función y luego, con ret , la CPU continúa la ejecución de main .

Editar

Entonces, this noreturn no hace que la función no regrese en absoluto, es solo un especificador que le dice al compilador que el código de esta función está escrito de tal manera que la función no volverá . Entonces, lo que debe hacer aquí es asegurarse de que esta función realmente no devuelva el control a la persona que llama. Por ejemplo, podría llamar a exit dentro de él.

Además, dado lo que he leído sobre este especificador, parece que para asegurarse de que la función no vuelva a su punto de invocación, uno debe llamar a otra función de no noreturn dentro de él y asegurarse de que esta última siempre se ejecute (en para evitar un comportamiento indefinido) y no causa UB en sí.


TL: DR: Es una optimización perdida por gcc .

noreturn es una promesa al compilador de que la función no regresará. Esto permite optimizaciones, y es útil especialmente en casos en los que es difícil para el compilador probar que un ciclo nunca saldrá, o de lo contrario probar que no hay una ruta a través de una función que regresa.

GCC ya optimiza main para caerse al final de la función si regresa func() , incluso con el valor predeterminado -O0 (nivel mínimo de optimización) que parece que usó.

El resultado para func() sí mismo podría considerarse una optimización perdida; podría omitir todo después de la llamada a la función (ya que la llamada no se devuelve es la única forma en que la función en sí puede ser no noreturn ). No es un gran ejemplo, ya que printf es una función C estándar que se sabe que devuelve normalmente (a menos que setvbuf para que stdout proporcione un búfer que se segfault)

Vamos a usar una función diferente que el compilador no conoce.

void ext(void); //static int foo; _Noreturn void func(int *p, int a) { ext(); *p = a; // using function args after a function call foo = 1; // requires save/restore of registers } void bar() { func(&foo, 3); }

( Código + asm x86-64 en el explorador del compilador Godbolt ) .

La salida de gcc7.2 para bar() es interesante. Incluye func() y elimina el almacén muerto foo=3 , dejando solo:

bar: sub rsp, 8 ## align the stack call ext mov DWORD PTR foo[rip], 1 ## fall off the end

Gcc todavía asume que ext() va a volver, de lo contrario podría haber llamado ext() con jmp ext . Pero gcc no llama a noreturn funciones de retorno de noreturn , porque eso gcc.gnu.org/bugzilla/show_bug.cgi?id=55747#c4 de noreturn para cosas como abort() . Aparentemente, alinearlos está bien, sin embargo.

Gcc podría haberse optimizado al omitir también la tienda mov después de la call . Si ext regresa, el programa está alojado, por lo que no tiene sentido generar nada de ese código. Clang hace esa optimización en bar() / main() .

func sí es más interesante y una mayor optimización perdida .

gcc y clang emiten casi lo mismo:

func: push rbp # save some call-preserved regs push rbx mov ebp, esi # save function args for after ext() mov rbx, rdi sub rsp, 8 # align the stack before a call call ext mov DWORD PTR [rbx], ebp # *p = a; mov DWORD PTR foo[rip], 1 # foo = 1 add rsp, 8 pop rbx # restore call-preserved regs pop rbp ret

Esta función podría suponer que no regresa, y usar rbx y rbp sin guardarlos / restaurarlos.

Gcc para ARM32 realmente hace eso, pero aún así emite instrucciones para regresar de manera limpia. Por lo tanto, una función de no retorno que realmente regresa en ARM32 romperá la ABI y causará problemas difíciles de depurar en la persona que llama o más tarde. (El comportamiento indefinido lo permite, pero al menos es un problema de calidad de implementación: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158 ).

Esta es una optimización útil en los casos en que gcc no puede probar si una función regresa o no. (Sin embargo, es obviamente perjudicial cuando la función simplemente regresa. Gcc advierte cuando está seguro de que una función de retorno no regresa). Otras arquitecturas de destino gcc no hacen esto; Esa es también una optimización perdida.

Pero gcc no llega lo suficientemente lejos: optimizar también la instrucción de devolución (o reemplazarla con una instrucción ilegal) ahorraría el tamaño del código y garantizaría una falla ruidosa en lugar de una corrupción silenciosa.

Y si va a optimizar el ret , tiene sentido optimizar todo lo que solo se necesita si la función regresa.

Por lo tanto, func() podría compilarse para :

sub rsp, 8 call ext # *p = a; and so on assumed to never happen ud2 # optional: illegal insn instead of fall-through

Cualquier otra instrucción presente es una optimización perdida. Si ext se declara noreturn , eso es exactamente lo que obtenemos.

Se puede suponer que cualquier bloque básico que termine con un retorno nunca se alcanzará.