Funkcja nie wywołana w kodzie zostanie wywołana w czasie wykonywania
Jak poniższy program może wywołać never_called
Jeśli nigdy
kod?
#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();
}
To różni się w zależności od kompilatora. Kompilowanie z Clang z
optymalizacje na, funkcja never_called
wykonuje w czasie wykonywania.
$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!
Kompilowanie z GCC, jednak ten kod po prostu się zawiesza:
$ g++ -std=c++17 -O3 a.cpp && ./a.out
Segmentation fault (core dumped)
Wersja kompilatora:
$ 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
Program zawiera nieokreślone zachowanie, jako dereferowanie wskaźnika null
(tzn. wywołanie foo()
W main Bez przypisywania do niego poprawnego adresu
wcześniej) jest UB, dlatego norma nie nakłada żadnych wymagań.
Wykonywanie never_called
w czasie wykonywania jest idealną sytuacją, gdy
undefined behavior has been hit, it ' s as valid as just crashing (like
po skompilowaniu z GCC). Dobra, ale dlaczego Clang to robi? Jeśli
skompilować go z wyłączonymi optymalizacjami, program nie będzie już wyjście
"formatowanie dysku twardego" i po prostu się zawiesi:
$ clang++ -std=c++17 -O0 a.cpp && ./a.out
Segmentation fault (core dumped)
Wygenerowany kod dla tej wersji jest następujący:
main: # @main
push rbp
mov rbp, rsp
call qword ptr [foo]
xor eax, eax
pop rbp
ret
Próbuje wywołać funkcję, do której foo
wskazuje, a jako foo
jest inicjalizowana za pomocą nullptr
(lub jeśli nie ma inicjalizacji,
nadal tak będzie), jego wartość wynosi zero. Tutaj, undefined
zachowanie zostało uderzone, więc wszystko może się w ogóle zdarzyć, a program
jest bezużyteczny. Normalnie, wykonanie połączenia do takiego nieważnego adres
skutkuje błędami segmentacji, stąd komunikat, który otrzymujemy, gdy
uruchamianie programu.
Teraz zbadajmy ten sam program, ale skompilujmy go z optymalizacjami na:
$ clang++ -std=c++17 -O3 a.cpp && ./a.out
formatting hard disk drive!
Wygenerowany kod dla tej wersji jest następujący:
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!"
Co ciekawe, w jakiś sposób optymalizacje zmodyfikowały program tak, aby
main
wywołuje std::puts
bezpośrednio. Ale dlaczego Clang to zrobił? I dlaczego jest
set_foo
skompilowane do pojedynczej ret
instrukcji?
Wróćmy do standardowo (konkretnie N4660) na chwilę. Co? mówi o nieokreślonym zachowaniu?
/ Align = "left" /3.27 undefined behavior [defns.undefined]
Zachowanie, dla którego niniejszy dokument nie nakłada żadnych wymagań
[Uwaga: można oczekiwać niezdefiniowanego zachowania , gdy ten dokument pomija Dowolna jednoznaczna definicja zachowania lub , gdy program używa błędnego konstruowania lub błędnych danych. dopuszczalne nieokreślone zakresy zachowań od ignorowanie sytuacji całkowicie z nieprzewidywalnymi skutkami, aby zachowanie podczas tłumaczenia lub wykonanie programu w udokumentowany sposób charakterystyczne dla środowiska (z wydaniem lub bez wydania komunikat diagnostyczny), do zakończenia tłumaczenia lub wykonania (z wydanie komunikatu diagnostycznego). Wiele błędnych konstrukcji programu nie powodują nieokreślonego zachowania; są one wymagane do zdiagnozowania. Ocena wyrażenia stałego nigdy wykazuje zachowanie jawnie specified as undefined ([expr.const]). - Uwaga końcowa]
Program, który wykazuje nieokreślone zachowanie, staje się bezużyteczny, ponieważ wszystko
zrobił to do tej pory i zrobi dalej nie ma znaczenia, jeśli zawiera
błędne dane lub konstrukcje. Mając to na uwadze, pamiętaj, że
Kompilatory mogą całkowicie ignorować w przypadku niezdefiniowanego zachowania
jest trafiony, a to faktycznie jest używane jako odkryte fakty podczas optymalizacji
program. Na przykład, konstrukt taki jak x + 1 > x
(gdzie x
jest liczbą całkowitą podpisaną) zostanie skompilowany do
true, nawet jeśli wartość x
jest nieznana podczas kompilacji. Rozumowanie
jest to, że kompilator chce zoptymalizować dla ważnych przypadków, a jedynym
sposobem na poprawność tego konstruktu jest to, że nie uruchamia arytmetyki
przepełnienie (tj. if x != std::numeric_limits<decltype(x)>::max()
). To
to nowy wyuczony fakt w optymalizatorze. Na tej podstawie konstrukcja jest
/ align = "left" /
Uwaga: ta sama optymalizacja nie może występuje dla niepodpisanych liczb całkowitych, ponieważ przepełnienie nie jest UB. Oznacza to, że kompilator musi zachować wyrażenie w takim stanie, w jakim jest, ponieważ może mieć inną ocenę, gdy przepełnia się (unsigned to moduł 2 N , gdzie N to liczba bitów). Optymalizacja go dla niepodpisanych liczb całkowitych byłaby niezgodna ze standardem (dzięki aschepler.)
Jest to przydatne, ponieważ pozwala na Tony optymalizacji do kopnięcia
w . Więc
daleko, tak dobrze, ale co się stanie, jeśli x
trzyma swój maksymalna wartość w czasie wykonywania?
Cóż, to jest nieokreślone zachowanie, więc nonsensem jest próbować rozumować o
to, jak wszystko może się zdarzyć, a norma nie nakłada żadnych wymagań.
Teraz mamy wystarczająco dużo informacji, aby lepiej zbadać twój błąd program. Wiemy już, że dostęp do wskaźnika null jest niezdefiniowany zachowanie, i to jest przyczyną zabawnego zachowania w czasie pracy. Spróbujmy więc zrozumieć, dlaczego clang (lub technicznie LLVM) zoptymalizowany program w jaki sposób tak.
static void (*foo)() = nullptr;
static void never_called()
{
std::puts("formatting hard disk drive!");
}
void set_foo()
{
foo = never_called;
}
int main()
{
foo();
}
Pamiętaj, że możliwe jest wywołanie set_foo
przed wpisem main
rozpoczyna egzekucję. Na przykład, gdy najwyższy poziom deklaruje zmienną,
można ją wywołać podczas inicjalizacji wartości tej zmiennej:
void set_foo();
int x = (set_foo(), 42);
Jeśli napiszesz ten fragment przed main
, program nie
dłużej wykazuje nieokreślone zachowanie, a komunikat " formatowanie twarde
dysk!" jest wyświetlany z optymalizacjami włączonymi lub wyłączonymi.
Więc jaki jest jedyny sposób, aby ten program ważne? Jest to set_foo
funkcja, która przypisuje adres never_called
do foo
, więc możemy
znajdź coś tutaj. Zauważ, że foo
jest oznaczony jako static
, co oznacza, że
ma wewnętrzne powiązanie i nie można uzyskać dostępu spoza tego tłumaczenia
unit. Natomiast funkcja set_foo
ma powiązania zewnętrzne i może
być dostępne z zewnątrz. Jeśli inna jednostka tłumaczeń zawiera fragment
podobnie jak ten powyżej, wtedy ten program staje się ważny.
Fajne, ale nikt nie dzwoni set_foo
z Na Zewnątrz. Chociaż to
jest fakt, optymalizator widzi, że jedynym sposobem, aby ten program
be valid is if {[15] } is called before main
, otherwise it ' s
tylko nieokreślone zachowanie. Jest to nowy wyuczony fakt i zakłada set_foo
jest w rzeczywistości nazywany. W oparciu o tę nową wiedzę, inne optymalizacje, które
kopnięcie może to wykorzystać.
Na przykład, gdy stała
składanie jest
zastosowany, widzi, że konstrukcja foo()
jest ważna tylko wtedy, gdy foo
może być prawidłowo zainicjowana. Jedynym sposobem na to jest wywołanie {[15] } poza tą jednostką translacji, więc foo = never_called
.
Eliminacja Martwego kodu i optymalizacja interproceduralna może się okazać, że jeśli foo == never_called
, to kod wewnątrz set_foo
jest niepotrzebny,
przekształca się więc w pojedynczą ret
instrukcję.
Inline expansion optymalizacja
widzi, że foo == never_called
, więc wezwanie do foo
można zastąpić
swoim ciałem. W końcu kończymy z czymś takim jak to:
set_foo():
ret
main:
mov edi, .L.str
call puts
xor eax, eax
ret
.L.str:
.asciz "formatting hard disk drive!"
, który jest w pewnym sensie odpowiednikiem wyjścia Clang z włączonymi optymalizacjami. Oczywiście to, co naprawdę zrobił Clang, może (i może) być inne, ale optymalizacje są jednak w stanie osiągnąć ten sam wniosek.
W przeciwieństwie do poprzednich wersji GCC, GCC nie może być używany w grach komputerowych.]}.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
Wykonanie tego programu powoduje awarię (błąd segmentacji), ale jeśli wywołasz set_foo
w innej jednostce tłumaczeniowej, zanim main dostanie wykonanym, wtedy ten program nie wykazuje już niezdefiniowanego zachowania.
Wszystko to może się zmienić szalenie jak coraz więcej optymalizacji są projektowane, więc nie polegaj na założeniu, że Twój kompilator zajmie się kodem zawierającym niezdefiniowanego zachowania, to może po prostu spieprzyć, jak również (i sformatować dysk twardy na serio!)
Polecam przeczytać co każdy programista C powinien wiedzieć o niezdefiniowanym zachowaniu I Przewodnik po Undefined Zachowanie w C i C++, obie serie artykułów są bardzo pouczające i mogą pomóc w zrozumieniu stanu wiedzy.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-03 11:53:15
Jeśli implementacja nie określi efektu wywołania wskaźnika funkcji null, może zachowywać się jak wywołanie dowolnego kodu. Taki arbitralny kod może doskonale zachowywać się jak wywołanie funkcji " foo ()". Podczas gdy Załącznik L do standardu C zachęcałby implementacje do rozróżnienia między "Critical UB" i "non-critical UB" , a niektóre implementacje C++ mogłyby zastosować podobne rozróżnienie, wywołanie nieprawidłowego wskaźnika funkcji byłoby critical ub w dowolnym case.
Zauważ, że sytuacja w tym pytaniu jest bardzo różna od np.
unsigned short q;
unsigned hey(void)
{
if (q < 50000)
do_something();
return q*q;
}
W tej ostatniej sytuacji kompilator, który nie twierdzi, że jest" analizowalny", może rozpoznać, że kod wywoła się, jeśli q jest większe niż 46,340, gdy wykonanie osiągnie return
, a zatem równie dobrze może bezwarunkowo wywoływać do_something()
. Chociaż Załącznik L jest źle napisany, wydaje się, że intencją byłoby zabronienie takich "optymalizacji". W przypadku wywołania nieprawidłowej funkcji wskaźnik, jednak nawet bezpośrednio generowany kod na większości platform może mieć dowolne zachowanie.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-03 02:19:33