¿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
dondef
se declaró previamente con el atributonoreturn
yf
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 sí devuelve, ¿qué se supone que debe hacer el compilador? ¿hacer? Básicamente tiene tres posibilidades:
- Sea "amable" con usted y descubra una forma de hacer que la función regrese correctamente de todos modos.
- Emite un código que, cuando la función vuelve incorrectamente, se bloquea o se comporta de manera arbitrariamente impredecible.
- 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á.