La función no llamada en el código se llama en tiempo de ejecución
¿Cómo puede llamar el siguiente programa never_called
si nunca
¿llamada en código?
#include <cstdio>
static void never_called()
{
std::puts("formatting hard disk drive!");
}
static void (*foo)() = nullptr;
void set_foo()
{
foo = never_called;
}
int main()
{
foo();
}
Esto difiere de un compilador a otro. Compilar con Clang con
optimizaciones en, la función never_called
se ejecuta en tiempo de ejecución.
$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!
Compilando con GCC, sin embargo, este código simplemente se bloquea:
$ g++ -std=c++17 -O3 a.cpp && ./a.out
Segmentation fault (core dumped)
Versión de compiladores:
$ clang --version
clang version 5.0.0 (tags/RELEASE_500/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
$ gcc --version
gcc (GCC) 7.2.1 20171128
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
2 answers
El programa contiene un comportamiento indefinido, como desreferenciar un puntero nulo
(es decir, llamando a foo()
en main sin asignarle una dirección válida
de antemano) es UB, por lo tanto, no hay requisitos impuestos por la norma.
Ejecutar never_called
en tiempo de ejecución es una situación válida perfecta cuando
el comportamiento indefinido ha sido golpeado, es tan válido como simplemente estrellarse (como
cuando se compila con GCC). Vale, ¿pero por qué Clang hace eso? Si
compilar con optimizaciones desactivadas, el programa ya no salida
"formatear la unidad de disco duro", y simplemente se bloqueará:
$ clang++ -std=c++17 -O0 a.cpp && ./a.out
Segmentation fault (core dumped)
El código generado para esta versión es el siguiente:
main: # @main
push rbp
mov rbp, rsp
call qword ptr [foo]
xor eax, eax
pop rbp
ret
Intenta hacer una llamada a una función a la que foo
apunta, y como foo
se inicializa con nullptr
(o si no tenía ninguna inicialización,
este seguiría siendo el caso), su valor es cero. Aquí, indefinido
comportamiento ha sido golpeado, por lo que cualquier cosa puede suceder en absoluto y el programa
se vuelve inútil. Normalmente, hacer una llamada a tal inválido dirección
resultados en errores de falla de segmentación, de ahí el mensaje que recibimos cuando
ejecutando el programa.
Ahora examinemos el mismo programa pero compilándolo con optimizaciones en:{[46]]}
$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!
El código generado para esta versión es el siguiente:
set_foo(): # @set_foo()
ret
main: # @main
push rax
mov edi, .L.str
call puts
xor eax, eax
pop rcx
ret
.L.str:
.asciz "formatting hard disk drive!"
Curiosamente, de alguna manera las optimizaciones modificaron el programa para que
main
llama a std::puts
directamente. ¿Pero por qué Clang hizo eso? Y por qué es
set_foo
compilado en una única instrucción ret
?
Volvamos a la estándar (N4660, específicamente) por un momento. Lo ¿dice sobre comportamiento indefinido?
3.27 comportamiento indefinido [defns.undefined]
Comportamiento para el cual este documento no impone requisitos{[46]]}
[Nota: Se puede esperar un comportamiento indefinido cuando este documento omite cualquier definición explícita de comportamiento o cuando un programa utiliza una construir o datos erróneos. Rangos de comportamiento indefinidos permitidos de ignorando completamente la situación con resultados impredecibles, para comportarse durante la traducción o la ejecución del programa de una manera documentada característica del medio ambiente (con o sin la emisión de un diagnóstico), para terminar una traducción o ejecución (con el emisión de un mensaje de diagnóstico). Muchas construcciones erróneas del programa no engendrar un comportamiento indefinido; se requiere que sean diagnosticados. Evaluación de una expresión constante nunca exhibe el comportamiento explícitamente especificado como undefined ([expr.const]). - nota final]
Énfasis mío.
Un programa que exhibe un comportamiento indefinido se vuelve inútil, ya que todo
lo ha hecho hasta ahora y lo hará aún más no tiene sentido si contiene
datos o construcciones erróneas. Con eso en mente, recuerda que
los compiladores pueden ignorar completamente para el caso cuando el comportamiento indefinido
se golpea, y esto en realidad se utiliza como hechos descubiertos al optimizar un
programa. Para instancia, una construcción como x + 1 > x
(donde x
es un entero con signo) se compilará a
true, incluso si el valor de x
es desconocido en tiempo de compilación. Razonamiento
es que el compilador quiere optimizar para casos válidos, y el único
la manera de que esa construcción sea válida es si no activa la aritmética
desbordamiento (es decir, si x != std::numeric_limits<decltype(x)>::max()
). Este
es un nuevo hecho aprendido en el optimizador. Basado en eso, la construcción es
demostrado ser siempre cierto.
Nota: esta misma optimización no puede ocurre para enteros sin signo, porque desbordar uno no es UB. Es decir, el compilador necesita mantener la expresión tal como está, porque podría tener una evaluación diferente cuando se desborda (unsigned es módulo 2N, donde N es número de bits). Optimizarlo para enteros sin signo sería incompleto con el estándar (gracias aschepler.)
Esto es útil ya que permite toneladas de optimizaciones para patear
in . Tan
lejos, tan bueno, pero lo que sucede si x
mantiene su ¿valor máximo en tiempo de ejecución?
Bueno, ese es un comportamiento indefinido, así que no tiene sentido tratar de razonar sobre
esto, como cualquier cosa puede suceder y la norma no impone requisitos.
Ahora tenemos suficiente información para examinar mejor su defectuoso programa. Ya sabemos que acceder a un puntero nulo es indefinido comportamiento, y eso es lo que está causando el comportamiento divertido en tiempo de ejecución. Así que vamos a tratar de entender por qué Clang (o técnicamente LLVM) optimizado el programa de la forma en que hacer.
static void (*foo)() = nullptr;
static void never_called()
{
std::puts("formatting hard disk drive!");
}
void set_foo()
{
foo = never_called;
}
int main()
{
foo();
}
Recuerde que es posible llamar set_foo
antes de la entrada main
comienza a ejecutarse. Por ejemplo, cuando se declara una variable de nivel superior,
puede llamarlo mientras inicializa el valor de esa variable:
void set_foo();
int x = (set_foo(), 42);
Si escribe este fragmento antes de main
, el programa no
más largo exhibe un comportamiento indefinido, y el mensaje " formatear duro
unidad de disco!se muestra " , con optimizaciones activadas o desactivadas.
Entonces, ¿cuál es la única forma en que este programa es válido? Hay esto set_foo
función que asigna la dirección de never_called
a foo
, por lo que podríamos
encuentra algo aquí. Tenga en cuenta que foo
está marcado como static
, lo que significa que
tiene enlace interno y no se puede acceder desde fuera de esta traducción
unidad. En contraste, la función set_foo
tiene enlace externo, y puede
se puede acceder desde el exterior. Si otra unidad de traducción contiene un fragmento
al igual que el anterior, este programa se vuelve válido.
Genial, pero no hay nadie llamando set_foo
desde fuera. Aunque este
es el hecho, el optimizador ve que la única manera de que este programa
be valid is if set_foo
is called before main
, otherwise it's
sólo un comportamiento indefinido. Ese es un nuevo hecho aprendido, y asume set_foo
de hecho se llama. Basado en ese nuevo conocimiento, otras optimizaciones que
patear puede aprovecharlo.
Por ejemplo, cuando constante
plegado es
aplicado, ve que la construcción foo()
solo es válida si foo
se puede inicializar correctamente. La única manera de que eso suceda es si set_foo
se llama fuera de esta unidad de traducción, así que foo = never_called
.
Eliminación de código muerto y optimización interprocedural podría descubrir que si foo == never_called
, entonces el código dentro de set_foo
es innecesario,
así que se transforma en una única instrucción ret
.
Optimización de expansión inline
ve que foo == never_called
, por lo que la llamada a foo
puede ser reemplazada
con su cuerpo. Al final, terminamos con algo como esto:
set_foo():
ret
main:
mov edi, .L.str
call puts
xor eax, eax
ret
.L.str:
.asciz "formatting hard disk drive!"
Que es algo equivalente a la salida de Clang con optimizaciones activadas. Por supuesto, lo que Clang realmente hizo puede (y podría) ser diferente, pero las optimizaciones son, sin embargo, capaces de llegar a la misma conclusión.
Examinando la salida de GCC con optimizaciones activadas, parece que no se molestó en investigar:
.LC0:
.string "formatting hard disk drive!"
never_called():
mov edi, OFFSET FLAT:.LC0
jmp puts
set_foo():
mov QWORD PTR foo[rip], OFFSET FLAT:never_called()
ret
main:
sub rsp, 8
call [QWORD PTR foo[rip]]
xor eax, eax
add rsp, 8
ret
Ejecutar ese programa resulta en un bloqueo (error de segmentación), pero si llama a set_foo
en otra unidad de traducción antes de que main obtenga ejecutado, entonces este programa ya no exhibe un comportamiento indefinido.
Todo esto puede cambiar locamente a medida que se diseñan más y más optimizaciones, así que no confíe en la suposición de que su compilador se encargará del código que contiene un comportamiento indefinido, también podría arruinarlo (¡y formatear su disco duro de verdad!)
Te recomiendo que leas Lo que todo programador de C debe saber sobre el Comportamiento Indefinido y Una Guía para Undefined Comportamiento en C y C++, ambas series de artículos son muy informativas y podrían ayudarlo a comprender el estado del arte.
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
2018-03-03 11:53:15
A menos que una implementación especifique el efecto de intentar invocar un puntero de función nulo, podría comportarse como una llamada a código arbitrario. Tal código arbitrario podría perfectamente comportarse como una llamada a la función " foo ()". Mientras que el Anexo L del Estándar C invitaría a las implementaciones a distinguir entre "UB crítica" y "UB no crítica", y algunas implementaciones de C++ podrían aplicar una distinción similar, una invocación de un puntero de función no válido sería UB crítica en cualquier caso.
Tenga en cuenta que la situación en esta pregunta es muy diferente de, por ejemplo,
unsigned short q;
unsigned hey(void)
{
if (q < 50000)
do_something();
return q*q;
}
En esta última situación, un compilador que no afirma ser "analizable" podría reconocer que el código se invocará si q es mayor que 46.340 cuando la ejecución alcanza la instrucción return
, y por lo tanto también podría invocar do_something()
incondicionalmente. Si bien el anexo L está mal redactado, parecería que la intención sería prohibir tales "optimizaciones". En el caso de llamar a una función no válida puntero, sin embargo, incluso el código generado directamente en la mayoría de las plataformas podría tener un comportamiento arbitrario.
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
2018-01-03 02:19:33