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


He leído esta pregunta sobre el atributo noreturn, que se usa para funciones que no regresan al llamador.

Entonces he hecho un programa en C.

#include <stdio.h>
#include <stdnoreturn.h>

noreturn void func()
{
        printf("noreturn func\n");
}

int main()
{
        func();
}

Y el ensamblado generado del código usando este :

.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é devuelve function func() después de proporcionar el atributo noreturn?

Author: rsp, 2017-08-31

9 answers

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

En primer lugar, _Noreturn function specifier (o, noreturn, usando <stdnoreturn.h>) es una pista para el compilador sobre una promesa teórica hecha por el programador de que esta función nunca regresará. 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 noreturn el especificador de función eventualmente regresa a su llamante, ya sea

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

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

Para dejarlo claro, usar noreturn especificador de función no impide que una forma de función regrese a su llamador. Es una promesa hecha por el programador al compilador para permitirle un cierto grado más de libertad para generar código optimizado.

Ahora, en caso de que haya hecho una promesa antes y después, elija violar esto, el resultado es UB. Se anima a los compiladores, pero no se les exige, a producir advertencias cuando una función _Noreturn parece ser capaz de regresar a su autor de llamada.

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, (Tenga en cuenta 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 el 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 devuelve, el comportamiento es indefinido. [ Nota: La función puede terminar lanzando una excepción. -final nota ]

[ Nota: Se anima a las implementaciones a emitir una advertencia si una función marcada [[noreturn]] podría devolver. -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 final]

 116
Author: Sourav Ghosh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-02 06:21:42

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

Porque escribiste un código que le dijo que lo hiciera.

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 que no sea regresar después de haber llamado printf()?

El estándar C en 6.7.4 Especificadores de función , el párrafo 12 incluye específicamente un ejemplo de una función noreturn que puede en realidad devuelve-y etiqueta el comportamiento como undefined :

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 no volverá nunca". Si violas esa restricción, todo depende de ti.

 45
Author: Andrew Henle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-31 12:46:52

noreturn es una promesa. Le estás diciendo al compilador, " Puede o no ser obvio, pero Yo sé, basado en 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 regresara correctamente. Dejar de lado esos mecanismos podría permitir al compilador generar código más eficiente.

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

Pero si prométele al compilador que tu función no regresará, y el compilador no hace arreglos para que sea posible que la función regrese correctamente, y luego vas y escribes una función que devuelve, ¿qué se supone que debe hacer el compilador? Básicamente tiene tres posibilidades:

  1. Sea "amable" con usted y encuentre una manera de que la función regrese correctamente de todos modos.
  2. Emitir código que, cuando la función devuelve incorrectamente, se bloquea o se comporta de forma arbitraria formas impredecibles.
  3. Darle una advertencia o mensaje de error señalando que usted rompió su promesa.

El compilador podría hacer 1, 2, 3, o alguna combinación.

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

La conclusión, tanto 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 tu promesa, y pueden pasar cosas malas si luego rompes tu promesa.

 25
Author: Steve Summit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-31 18:37:30

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

Si devuelve desde tal función, el comportamiento es indefinido, pero esto no significa que un compilador cuerdo le permitirá ensuciar el estado de la aplicación completamente eliminando la instrucción ret, especialmente porque el compilador a menudo incluso podrá deducir que un retorno es realmente posible.

Sin embargo, si escribes esto:

noreturn void func(void)
{
    printf("func\n");
}

int main(void)
{
    func();
    some_other_func();
}

Entonces es perfectamente razonable para el compilador para eliminar el some_other_func completamente, si se siente así.

 15
Author: Groo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-31 12:48:56

Como otros han mencionado, este es un comportamiento indefinido clásico. Prometiste func no volverías, pero lo hiciste volver de todos modos. Puedes recoger los pedazos cuando eso se rompa.

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

Puede ver esto en la lista de ensamblados: el compilador ha asumido, en main, que func no volverá. Por lo tanto, literalmente borró todo el código después del call func (ver para usted en https://godbolt.org/g/8hW6ZR ). La lista del ensamblado no está truncada, literalmente solo termina después del call func porque el compilador asume que cualquier código después de eso sería inalcanzable. Entonces, cuando func realmente regresa, main va a comenzar a ejecutar cualquier basura que siga a la función main - ya sea relleno, constantes inmediatas o un mar de 00 bytes. Una vez más - mucho comportamiento indefinido.

Esto es transitivo - una función que llama a una función noreturn en todas las rutas de código posibles pueden, en sí, ser asumidas como noreturn.

 11
Author: nneonneo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-31 18:22:24

De acuerdo con este

Si la función declarada _Noreturn retorna, 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, exit(1) al final de la función.

 7
Author: ChrisB,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-31 12:39:20

ret simplemente significa que la función devuelve control al llamador. Así, main no call func, la CPU ejecuta la función, y luego, con ret, la CPU continúa la ejecución de main.

Editar

Por lo tanto, resulta, noreturn no hace quela 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 devolverá. Entonces, lo que debe hacer aquí es asegurarse de que esta función realmente no devuelve el control al destinatario. Por ejemplo, puedes 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 regrese a su punto de invocación, uno debería llamar a otro noreturn función dentro de él y asegúrese de que este último siempre se ejecuta (con el fin de evitar un comportamiento indefinido) y no causa UB sí mismo.

 6
Author: ForceBru,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-31 12:52:23

La función No return no guarda los registros en la entrada ya que no es necesario. Facilita las optimizaciones. Ideal para la rutina del programador, por ejemplo.

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

 6
Author: P__J__,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-31 13:13:17

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 donde es difícil para el compilador probar que un bucle nunca saldrá, o probar que no hay una ruta a través de una función que regrese.

GCC ya optimiza main para que se caiga el final de la función si func() devuelve, incluso con el valor predeterminado -O0 (optimización mínima nivel) que parece que usaste.

La salida para func() en sí podría considerarse una optimización perdida; podría omitir todo después de la llamada a la función (ya que tener la llamada no devuelta es la única forma en que la función en sí puede ser 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 dar stdout un búfer que se segfault?)

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

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 + x86 - 64 asm en el Godbolt compiler explorer .)

La salida Gcc7.2 para bar() es interesante. Se alinea func(), y elimina el foo=3 almacén muerto, 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 regresar, de lo contrario podría haber llamado simplemente ext() con jmp ext. Pero gcc no hace tailcall noreturn funciones, porque pierde información de traza inversa para cosas como abort(). Aparentemente en línea ellos está bien, sin embargo.

Gcc podría haber optimizado omitiendo el almacén mov después del call también. Si ext retorna, el programa es hosed, así que no tiene sentido generar nada de ese código. Clang hace que la optimización en bar() / main().


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

Gcc y clang emiten casi lo mismo: {[37]]}

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 asumir que no devuelve, y usar rbx y rbp sin guardarlos/restaurarlos.

Gcc para ARM32 realmente hace eso, pero aún así emite instrucciones para regresar de otra manera limpiamente. Por lo tanto, una función noreturn que realmente regrese en ARM32 romperá la ABI y causará problemas difíciles de depurar en el llamante o más tarde. (El comportamiento indefinido permite esto, pero es al menos 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 devuelve o no. (Obviamente es perjudicial cuando la función simplemente regresa, sin embargo. Gcc avisa cuando está seguro de que una función noreturn regresa.) Otras arquitecturas de destino gcc no hacen esto; eso también es una optimización perdida.

Pero gcc no va lo suficientemente lejos: optimizar la instrucción de retorno también (o reemplazarla con una instrucción ilegal) ahorraría el tamaño del código y garantizaría un fallo ruidoso en lugar de silencioso corrupción.

Y si vas a optimizar el ret, optimizar todo lo que solo es necesario si la función volverá tiene sentido.

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

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

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

Cualquier bloque básico que termina con un retorno podría suponerse que nunca se alcanzará.

 0
Author: Peter Cordes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-09-25 16:04:04