Wiem, że „niezdefiniowane zachowanie” w C ++ może pozwolić kompilatorowi na zrobienie wszystkiego, co chce. Miałem jednak awarię, która mnie zaskoczyła, ponieważ uznałem, że kod jest wystarczająco bezpieczny.
W takim przypadku prawdziwy problem wystąpił tylko na konkretnej platformie używającej określonego kompilatora i tylko wtedy, gdy włączono optymalizację.
Próbowałem kilku rzeczy, aby odtworzyć problem i maksymalnie go uprościć. Oto fragment funkcji o nazwie Serialize
, która pobierałaby parametr bool i kopiowałaby ciąg true
lub false
do istniejącego bufora docelowego.
Czy ta funkcja byłaby w przeglądzie kodu, nie byłoby sposobu, aby powiedzieć, że w rzeczywistości mogłaby się zawiesić, gdyby parametr bool był wartością niezainicjowaną?
// Zero-filled global buffer of 16 characters
char destBuffer[16];
void Serialize(bool boolValue) {
// Determine which string to print based on boolValue
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
const size_t len = strlen(whichString);
// Copy string into destination buffer, which is zero-filled (thus already null-terminated)
memcpy(destBuffer, whichString, len);
}
Jeśli ten kod jest wykonywany z optymalizacjami clang 5.0.0 +, to może / może ulec awarii.
Oczekiwany operator trójskładnikowy boolValue ? "true" : "false"
wyglądał na wystarczająco bezpieczny dla mnie, zakładałem: „Niezależnie od tego, jaką wartość ma śmieci, boolValue
nie ma to znaczenia, ponieważ i tak będzie to prawda lub fałsz”.
Mam skonfigurowany przykład Eksploratora kompilatora, który pokazuje problem podczas demontażu, tutaj pełny przykład. Uwaga: w celu zlikwidowania problemu kombinacja, którą znalazłem, działała, używając Clang 5.0.0 z optymalizacją -O2.
#include <iostream>
#include <cstring>
// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
bool uninitializedBool;
__attribute__ ((noinline)) // Note: the constructor must be declared noinline to trigger the problem
FStruct() {};
};
char destBuffer[16];
// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
// Determine which string to print depending if 'boolValue' is evaluated as true or false
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
size_t len = strlen(whichString);
memcpy(destBuffer, whichString, len);
}
int main()
{
// Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
FStruct structInstance;
// Output "true" or "false" to stdout
Serialize(structInstance.uninitializedBool);
return 0;
}
Problem powstaje z powodu optymalizatora: był wystarczająco sprytny, aby wywnioskować, że ciągi „prawda” i „fałsz” różnią się tylko długością o 1. Więc zamiast naprawdę obliczać długość, używa wartości samego bool, który powinien technicznie wynosi 0 lub 1 i wygląda następująco:
const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue; // clang clever optimization
Chociaż jest to „sprytne”, że tak powiem, moje pytanie brzmi: czy standard C ++ pozwala kompilatorowi założyć, że bool może mieć tylko wewnętrzną reprezentację liczbową „0” lub „1” i używać go w taki sposób?
Czy jest to przypadek zdefiniowany w implementacji, w którym to przypadku implementacja przyjęła, że wszystkie jej boole będą zawierały zawsze 0 lub 1, a każda inna wartość jest terytorium niezdefiniowanym?
true
” jest regułą dotyczącą operacji boolowskich, w tym „przypisania do bool” (która może domyślnie wywoływaćstatic_cast<bool>()
zależne od specyfiki). Nie jest to jednak wymóg dotyczący wewnętrznej reprezentacjibool
wybranego przez kompilator.Odpowiedzi:
Tak, ISO C ++ pozwala (ale nie wymaga) implementacji, aby dokonać tego wyboru.
Należy jednak pamiętać, że ISO C ++ pozwala kompilatorowi na celowe emitowanie kodu, który ulega awarii (np. Z niedozwoloną instrukcją), jeśli program napotka UB, np. Jako sposób na pomoc w znalezieniu błędów. (Lub dlatego, że jest to DeathStation 9000. Ścisłe przestrzeganie nie wystarcza, aby implementacja C ++ była przydatna w jakimkolwiek rzeczywistym celu). Tak więc ISO C ++ pozwoliłoby kompilatorowi wykonać asm, który się zawiesił (z zupełnie innych powodów) nawet na podobnym kodzie, który czyta niezainicjowany
uint32_t
. Mimo że jest to wymagany typ układu o stałym układzie bez reprezentacji pułapek.To interesujące pytanie o to, jak działają rzeczywiste implementacje, ale pamiętaj, że nawet gdyby odpowiedź była inna, twój kod byłby nadal niebezpieczny, ponieważ nowoczesny C ++ nie jest przenośną wersją języka asemblera.
Kompilujesz dla systemu AB86 x86-64 System V. , który określa, że
bool
jako argument arg w rejestrze jest reprezentowany przez wzorce bitowefalse=0
itrue=1
w 8 niskich bitach rejestru 1 . W pamięcibool
jest typ 1-bajtowy, który ponownie musi mieć wartość całkowitą 0 lub 1.(ABI to zestaw opcji implementacyjnych, na które zgadzają się kompilatory dla tej samej platformy, dzięki czemu mogą tworzyć kod, który wywołuje nawzajem funkcje, w tym rozmiary typów, reguły układu struktur i konwencje wywoływania.)
ISO C ++ tego nie określa, ale ta decyzja ABI jest szeroko rozpowszechniona, ponieważ sprawia, że konwersja bool-> int jest tania (tylko rozszerzenie zerowe) . Nie znam żadnych ABI, które nie pozwalają kompilatorowi przyjąć 0 lub 1
bool
dla dowolnej architektury (nie tylko x86). Pozwala na optymalizacje typu!mybool
zxor eax,1
odwracaniem niskiego bitu: Dowolny możliwy kod, który może odwrócić bit / liczbę całkowitą / bool od 0 do 1 w instrukcji pojedynczego procesora . Lub kompilacjaa&&b
do bitowego AND dlabool
typów. Niektóre kompilatory faktycznie wykorzystują w kompilatorach wartości boolowskie jako 8 bitów. Czy operacje na nich są nieefektywne? .Zasadniczo zasada „tak, jakby” pozwala kompilatorowi korzystać z rzeczy, które są prawdziwe na kompilowanej platformie docelowej , ponieważ wynikiem końcowym będzie kod wykonywalny, który implementuje to samo widoczne z zewnątrz zachowanie, co źródło C ++. (Ze wszystkimi ograniczeniami, które Undefined Behawiior nakłada na to, co jest faktycznie „widoczne z zewnątrz”: nie za pomocą debuggera, ale z innego wątku w dobrze uformowanym / legalnym programie C ++.)
Kompilator jest zdecydowanie możliwość pełnego wykorzystania gwarancji ABI w jego kodzie-gen, i uczynić kod jak znalazłeś który optymalizuje
strlen(whichString)
się5U - boolValue
. (BTW, ta optymalizacja jest dość sprytna, ale może krótkowzroczna vs. rozgałęzienie i inlinememcpy
jako zapasy natychmiastowych danych 2 ).Lub kompilator mógł utworzyć tabelę wskaźników i zindeksować ją wartością całkowitą
bool
, ponownie zakładając, że jest to 0 lub 1. ( Ta możliwość sugeruje odpowiedź @ Barmar ).Twój
__attribute((noinline))
konstruktor z włączoną optymalizacją doprowadził do tego, że clang właśnie ładował bajt ze stosu, aby użyć go jakouninitializedBool
. Wykonana miejsca dla obiektu wmain
zpush rax
(który jest mniejszy i dla rozmaitych powodów o tak skuteczny jaksub rsp, 8
), więc niezależnie od śmieci było w AL na wejściu domain
jest wartością go stosujeuninitializedBool
. Właśnie dlatego masz wartości, które nie były tylko0
.5U - random garbage
może łatwo zawinąć do dużej wartości bez znaku, co prowadzi memcpy do przejścia do niezmapowanej pamięci. Miejsce docelowe znajduje się w pamięci statycznej, a nie na stosie, więc nie zastępujesz adresu zwrotnego ani czegoś takiego.Inne implementacje mógł dokonać różnych wyborów, EG
false=0
itrue=any non-zero value
. Wtedy clang prawdopodobnie nie spowodowałby awarii kodu dla tego konkretnego wystąpienia UB. (Ale nadal byłoby to dozwolone, gdyby chciał). Nie znam żadnych implementacji, które wybierają coś innego, co robi dla x86-64bool
, ale standard C ++ pozwala na wiele rzeczy, których nikt nie chce, a nawet nie chciałby robić sprzęt podobny do obecnych procesorów.ISO C ++ pozostawia nieokreślone, co znajdziesz podczas badania lub modyfikacji reprezentacji obiektowej
bool
. (np. poprzezmemcpy
wpisaniebool
dounsigned char
, co możesz zrobić, ponieważchar*
może alias cokolwiek. Iunsigned char
jest gwarantowana nie mieć bity wypełniające, więc C ++ standardowy sposób formalnie pozwalają HexDump reprezentacje obiektów bez UB. Pointer odlewania skopiować obiekt reprezentacja różni sięchar foo = my_bool
oczywiście od przypisywania , więc booleanizacja na 0 lub 1 nie miałaby miejsca, a otrzymalibyśmy reprezentację surowego obiektu).Już częściowo „ukryte” UB na tej drodze egzekucji z kompilatora z
noinline
. Nawet jeśli nie jest to wbudowane, optymalizacje międzyproceduralne mogą nadal tworzyć wersję funkcji zależną od definicji innej funkcji. (Po pierwsze, clang tworzy plik wykonywalny, a nie uniksową bibliotekę współdzieloną, w której może się zdarzyć interpolacja symboli. Po drugie, definicja wclass{}
definicji, więc wszystkie jednostki tłumaczeniowe muszą mieć tę samą definicję. Tak jak w przypadkuinline
słowa kluczowego.)Tak więc kompilator może emitować po prostu
ret
lubud2
(niedozwoloną instrukcję) jako definicjęmain
, ponieważ ścieżka wykonania rozpoczynająca się u górymain
nieuchronnie napotyka Niezdefiniowane zachowanie.(Który kompilator może zobaczyć w czasie kompilacji, jeśli zdecyduje się podążać ścieżką przez konstruktor inny niż wbudowany).Każdy program, który napotka UB, jest całkowicie niezdefiniowany przez całe swoje istnienie. Ale UB wewnątrz funkcji lub
if()
gałęzi, która nigdy nie działa, nie uszkadza reszty programu. W praktyce oznacza to, że kompilatory mogą podjąć decyzję o wydaniu niedozwolonej instrukcji lub oret
czymkolwiek, lub o braku emisji i wpadnięciu do następnego bloku / funkcji, dla całego podstawowego bloku, który można udowodnić w czasie kompilacji, aby zawierał lub prowadził do UB.GCC i Clang w praktyce nie faktycznie czasami emitować
ud2
na UB, a nie próbując nawet do generowania kodu dla ścieżek realizacji, które nie mają sensu. Lub w przypadkach takich jak wypadnięcie końcavoid
niefunkcji, gcc czasami pomijaret
instrukcję. Jeśli myślałeś, że „moja funkcja po prostu wróci z tym, co zawiera śmieci w RAX”, to jesteś bardzo w błędzie. Nowoczesne kompilatory C ++ nie traktują już języka jako przenośnego języka asemblera. Twój program naprawdę musi być poprawnym językiem C ++, nie przyjmując założeń o tym, jak autonomiczna, nie wstawiona wersja twojej funkcji może wyglądać w asm.Innym zabawnym przykładem jest to, dlaczego niewyrównany dostęp do pamięci mmap czasami nie działa poprawnie na AMD64? . x86 nie ma błędu w niezaangażowanych liczbach całkowitych, prawda? Dlaczego więc źle ustawiony
uint16_t*
byłby problem? Ponieważalignof(uint16_t) == 2
i naruszenie tego założenia doprowadziło do segfaulta podczas auto-wektoryzacji za pomocą SSE2.Zobacz także Co każdy programista C powinien wiedzieć o nieokreślonym zachowaniu # 1/3 , artykuł autora clang.
Kluczowy punkt: jeśli kompilator zauważy UB w czasie kompilacji, może „złamać” (emitować zaskakujący asm) ścieżkę przez twój kod, który powoduje UB, nawet jeśli celuje w ABI, dla którego dowolny wzorzec bitowy jest prawidłową reprezentacją obiektu
bool
.Spodziewaj się całkowitej wrogości wobec wielu błędów popełnianych przez programistę, zwłaszcza rzeczy, o których ostrzegają współczesne kompilatory. Dlatego powinieneś używać
-Wall
i naprawiać ostrzeżenia. C ++ nie jest językiem przyjaznym dla użytkownika, a coś w C ++ może być niebezpieczne, nawet jeśli byłoby bezpieczne w asm na celu, dla którego kompilujesz. (np. podpisane przepełnienie to UB w C ++ i kompilatory zakładają, że tak się nie stanie, nawet przy kompilacji dla uzupełnienia x86 2, chyba że użyjeszclang/gcc -fwrapv
).UB widoczny w czasie kompilacji jest zawsze niebezpieczny i naprawdę trudno jest mieć pewność (z optymalizacją czasu łącza), że naprawdę ukryłeś UB przed kompilatorem, a zatem może uzasadnić rodzaj generowanego asmu.
Nie być zbyt dramatycznym; często kompilatory pozwalają ci uciec od pewnych rzeczy i emitują kod tak, jak tego oczekujesz, nawet jeśli coś jest UB. Ale może będzie to problem w przyszłości, jeśli twórcy kompilatora wdrożą jakąś optymalizację, która uzyska więcej informacji o zakresach wartości (np. Że zmienna jest nieujemna, może umożliwiając optymalizację rozszerzenia znaku do wolnego rozszerzenia zerowego na x86- 64). Na przykład w bieżącym gcc i clang robienie
tmp = a+INT_MIN
nie jest optymalizowanea<0
jako zawsze-fałsz, tylkotmp
to zawsze jest ujemne. (PonieważINT_MIN
+a=INT_MAX
jest ujemny względem celu uzupełnienia 2, ia
nie może być wyższy niż to.)Więc gcc / clang nie wycofuje się obecnie w celu uzyskania informacji o zakresie dla danych wejściowych obliczeń, tylko na podstawie wyników opartych na założeniu braku podpisanego przepełnienia: przykład na Godbolt . Nie wiem, czy taka optymalizacja jest celowo „pomijana” w imię przyjazności dla użytkownika czy co.
Zauważ również, że implementacje (znane również jako kompilatory) mogą definiować zachowanie, które ISO C ++ pozostawia niezdefiniowane . Na przykład wszystkie kompilatory obsługujące elementy wewnętrzne Intela (takie jak
_mm_add_ps(__m128, __m128)
ręczna wektoryzacja SIMD) muszą zezwalać na tworzenie źle wyrównanych wskaźników, które są UB w C ++, nawet jeśli się ich nie lekceważy.__m128i _mm_loadu_si128(const __m128i *)
wykonuje wyrównane obciążenia, przyjmując źle wyrównany__m128i*
argument, a nie avoid*
lubchar*
. Czy `reinterpret_cast`ing między sprzętowym wskaźnikiem wektorowym a odpowiednim typem jest niezdefiniowanym zachowaniem?GNU C / C ++ definiuje również zachowanie przesunięcia w lewo liczby ujemnej ze znakiem (nawet bez
-fwrapv
), niezależnie od normalnych reguł UB z przepełnieniem ze znakiem. ( Jest to UB w ISO C ++ , podczas gdy odpowiednie przesunięcia liczb podpisanych są zdefiniowane w implementacji (logiczne vs. arytmetyka); implementacje dobrej jakości wybierają arytmetykę w HW, która ma arytmetyczne przesunięcia w prawo, ale ISO C ++ nie określa). Jest to udokumentowane w części całkowitej instrukcji GCC , wraz ze zdefiniowaniem zachowania zdefiniowanego w implementacji, że standardy C wymagają implementacji do zdefiniowania w taki czy inny sposób.Zdecydowanie istnieją problemy z jakością implementacji, którymi interesują się twórcy kompilatorów; generalnie nie próbują tworzyć kompilatorów, które są celowo wrogie, ale wykorzystując wszystkie dziury UB w C ++ (z wyjątkiem tych, które zdecydują się zdefiniować) w celu lepszej optymalizacji, czasami mogą być prawie nie do odróżnienia.
Przypis 1 : Górne 56 bitów może być śmieciami, które callee musi zignorować, jak zwykle dla typów węższych niż rejestr.
( Inne Abis zrobić dokonać różnych wyborów tutaj . Niektóre wymagają wąskich typów całkowitych być zerowa lub logowania rozszerzony wypełnić rejestr gdy przekazywane lub powrocie z funkcji, takich jak MIPS64 i PowerPC64. Zobacz ostatni odcinek tej x86-64 odpowiedź który porównuje się z wcześniejszymi wersjami ISA ).
Na przykład osoba dzwoniąca mogła obliczyć
a & 0x01010101
w RDI i użyć jej do czegoś innego, zanim zadzwonibool_func(a&1)
. Osoba dzwoniąca może zoptymalizować,&1
ponieważ już to zrobiła dla niskiego bajtu jako częśćand edi, 0x01010101
i wie, że odbiorca jest zobowiązany do zignorowania wysokich bajtów.Lub jeśli bool jest przekazywany jako trzeci argument, być może program wywołujący optymalizujący rozmiar kodu ładuje go
mov dl, [mem]
zamiastmovzx edx, [mem]
, oszczędzając 1 bajt kosztem fałszywej zależności od starej wartości RDX (lub innego efektu częściowego rejestru, w zależności w modelu procesora). Lub dla pierwszego argumentumov dil, byte [r10]
zamiastmovzx edi, byte [r10]
, ponieważ oba wymagają i tak prefiksu REX.To dlatego dzyń wydzielające
movzx eax, dil
wSerialize
, zamiastsub eax, edi
. (W przypadku argumentów liczb całkowitych clang narusza tę zasadę ABI, zamiast tego w zależności od nieudokumentowanego zachowania gcc i clang do zerowych lub rozszerzających wąskie liczby całkowite do 32 bitów. Czy rozszerzenie znaku lub zera jest wymagane przy dodawaniu 32-bitowego przesunięcia do wskaźnika dla x86-64 ABI? Byłem więc zainteresowany, aby zobaczyć, że to nie robi tego samegobool
.)Przypis 2: Po rozgałęzieniu masz po prostu 4-bajtowy-
mov
natychmiastowy lub 4-bajtowy + 1-bajtowy sklep. Długość jest niejawna w szerokości sklepu + przesunięciach.OTOH, glibc memcpy wykona dwa 4-bajtowe ładunki / sklepy z nakładaniem się, które zależy od długości, więc tak naprawdę kończy się to uwolnieniem całości od warunkowych gałęzi na boolean. Zobacz
L(between_4_7):
blok w memcpy / memmove glibc. Lub przynajmniej, idź tą samą drogą dla jednego z wartości logicznych w gałęzi memcpy, aby wybrać wielkość porcji.W przypadku
mov
wstawiania można użyć 2x -immediate +cmov
i warunkowego przesunięcia lub pozostawić ciąg danych w pamięci.Lub jeśli dostroisz się do Intel Ice Lake ( z funkcją Fast Short REP MOV ), rzeczywisty
rep movsb
może być optymalny. glibcmemcpy
może zacząć używaćrep movsb
do małych procesorów z tą funkcją, oszczędzając wiele rozgałęzień.Narzędzia do wykrywania UB i użycia niezainicjowanych wartości
W gcc i clang można skompilować,
-fsanitize=undefined
aby dodać instrumentację w czasie wykonywania, która będzie ostrzegać lub wykasować błąd na UB, który zdarza się w czasie wykonywania. Nie złapie to jednak zmiennych jednostkowych. (Ponieważ nie zwiększa rozmiarów czcionek, aby zrobić miejsce na „niezainicjowany” bit).Zobacz https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/
Aby znaleźć użycie niezainicjowanych danych, w Clang / LLVM znajduje się Sanitizer adresu i Sanitizer pamięci. https://github.com/google/sanitizers/wiki/MemorySanitizer pokazuje przykłady
clang -fsanitize=memory -fPIE -pie
wykrywania niezainicjowanych odczytów pamięci. Może działać najlepiej, jeśli kompilujesz bez optymalizacji, więc wszystkie odczyty zmiennych w rzeczywistości ładują się z pamięci w asm. Pokazują, że jest używany-O2
w przypadku, gdy obciążenie nie zoptymalizuje się. Sam tego nie próbowałem. (W niektórych przypadkach, np. Nie inicjując akumulatora przed zsumowaniem tablicy, brzęk -O3 wyśle kod sumujący się do rejestru wektorowego, którego nigdy nie zainicjował. Tak więc dzięki optymalizacji możesz mieć przypadek, w którym nie ma odczytu pamięci powiązanego z UB . Ale-fsanitize=memory
zmienia wygenerowany asm i może spowodować sprawdzenie tego.)Powinno to działać w tym przypadku, ponieważ wywołanie glibc
memcpy
zlength
obliczeniem z niezainicjowanej pamięci spowoduje (wewnątrz biblioteki) utworzenie gałęzi opartej nalength
. Gdyby wprowadził w pełni bez rozgałęzioną wersję, która właśnie używałacmov
, indeksował i dwa sklepy, mógł nie działać.Valgrind
memcheck
będzie również szukał tego rodzaju problemu, ponownie nie narzekając, jeśli program po prostu kopiuje niezainicjowane dane. Mówi jednak, że wykryje, kiedy „Skok warunkowy lub ruch zależy od niezainicjowanych wartości (wartości)”, aby spróbować uchwycić wszelkie widoczne z zewnątrz zachowanie, które zależy od niezainicjowanych danych.Być może idea nie oznaczania tylko obciążenia polega na tym, że struktury mogą mieć dopełnianie, a kopiowanie całej struktury (w tym wypełniania) za pomocą szerokiego wczytywania / przechowywania wektorów nie jest błędem, nawet jeśli poszczególne elementy były pisane tylko pojedynczo. Na poziomie asm utracono informacje o tym, co było dopełnieniem i co faktycznie stanowi część wartości.
źródło
Kompilator może założyć, że wartość logiczna przekazana jako argument jest prawidłową wartością logiczną (tj. Taką, która została zainicjowana lub przekonwertowana na
true
lubfalse
).true
Wartość nie musi być taka sama, jak liczba całkowita 1 - Rzeczywiście, nie mogą być różne reprezentacjetrue
ifalse
- ale parametr musi być jakiś ważny przedstawienie jednej z tych dwóch wartości, gdzie „ważne reprezentacja” to implementation- zdefiniowane.Jeśli więc nie uda się zainicjować a
bool
lub jeśli uda się go nadpisać jakimś wskaźnikiem innego typu, wówczas założenia kompilatora będą błędne i nastąpi niezdefiniowane zachowanie. Zostałeś ostrzeżony:źródło
true
Wartość nie musi być taka sama jak liczba całkowita 1” jest niejasnym wprowadzeniem w błąd. Jasne, rzeczywisty wzorzec bitowy może być czymś innym, ale gdy domyślnie jest konwertowany / promowany (jedyny sposób, w jaki można zobaczyć wartość inną niżtrue
/false
),true
jest zawsze1
ifalse
zawsze jest0
. Oczywiście, taki kompilator nie byłby również w stanie skorzystać ze sztuczki, której ten kompilator próbował użyć (biorąc pod uwagę fakt, żebool
faktyczny wzorzec bitów może być tylko0
lub1
), więc jest to trochę nieistotne dla problemu OP.true
do wzorca bitowego1
, jest to jego przywilej. Jeśli wybierze jakiś inny zestaw reprezentacji, to rzeczywiście nie będzie mógł skorzystać z opisanej tutaj optymalizacji. Jeśli wybierze tę konkretną reprezentację, może to zrobić. Musi być tylko wewnętrznie spójny. Państwo może zbadać reprezentacjibool
kopiując go do tablicy bajtów; to nie jest UB (ale jest zdefiniowane w implementacji)bool
wzorca bitowego0
lub1
. Nie ponawiają booleanizacji zabool
każdym razem, gdy czytają go z pamięci (lub rejestru zawierającego funkcję arg). Tak mówi ta odpowiedź. przykłady : gcc4.7 + może zoptymalizowaćreturn a||b
door eax, edi
zwracanej funkcjibool
lub MSVC może zoptymalizowaća&b
dotest cl, dl
. x86test
jest trochę bitoweand
, więc jeślicl=1
idl=2
test ustawi flagi zgodnie zcl&dl = 0
.Sama funkcja jest poprawna, ale w programie testowym instrukcja, która wywołuje funkcję, powoduje niezdefiniowane zachowanie przy użyciu wartości niezainicjowanej zmiennej.
Błąd występuje w funkcji wywołującej i można go wykryć przez przegląd kodu lub analizę statyczną funkcji wywołującej. Za pomocą linku eksploratora kompilatora kompilator gcc 8.2 wykrywa błąd. (Może mógłbyś zgłosić raport o błędzie przeciwko brzęczeniu, że nie znajduje on problemu).
Niezdefiniowane zachowanie oznacza, że wszystko może się zdarzyć, w tym awaria programu kilka linii po zdarzeniu, które wywołało niezdefiniowane zachowanie.
NB Odpowiedź na „Czy niezdefiniowane zachowanie może powodować _____?” jest zawsze „Tak”. To dosłownie definicja niezdefiniowanego zachowania.
źródło
bool
wyzwalacza UB?bool
). Kopiowanie wymaga oceny źródłatrue
,false
inot-a-thing
wartości logicznych.Bool może przechowywać tylko wartości zależne od implementacji używane wewnętrznie dla
true
ifalse
, a wygenerowany kod może założyć, że będzie przechowywał tylko jedną z tych dwóch wartości.Zazwyczaj implementacja użyje liczby całkowitej
0
dlafalse
i1
dlatrue
, aby uprościć konwersje międzybool
iint
, iif (boolvar)
wygenerować taki sam kod jakif (intvar)
. W takim przypadku można sobie wyobrazić, że kod wygenerowany dla trójki w przydziale użyłby tej wartości jako indeksu w tablicy wskaźników do dwóch łańcuchów, tzn. Mógłby zostać przekonwertowany na coś takiego:Jeśli nie
boolValue
jest zainicjowany, może w rzeczywistości zawierać dowolną wartość całkowitą, co może spowodować dostęp poza granicestrings
tablicy.źródło
bool
sięint
ze*(int *)&boolValue
i wydrukować go do celów debugowania, sprawdzić, czy jest coś innego niż0
lub1
kiedy się zawiesi. Jeśli tak jest, to prawie potwierdza teorię, że kompilator optymalizuje wbudowany - jeśli jako tablicę, co wyjaśnia, dlaczego ulega awarii.std::bitset<8>
nie nadaje mi ładnych nazw dla wszystkich moich różnych flag. W zależności od tego, jakie są, może to być ważne.Często podsumowując pytanie, zadajesz pytanie Czy standard C ++ pozwala kompilatorowi założyć, że
bool
może mieć tylko wewnętrzną reprezentację liczbową „0” lub „1” i używać go w taki sposób?Standard nie mówi nic o wewnętrznej reprezentacji
bool
. Określa tylko, co się dzieje, gdy rzutujesz „bool
na”int
(lub odwrotnie). Przeważnie, z powodu tych integralnych konwersji (i faktu, że ludzie polegają na nich dość mocno), kompilator użyje 0 i 1, ale nie musi (chociaż musi przestrzegać ograniczeń dowolnego używanego ABI niższego poziomu ).Tak więc kompilator, gdy widzi a,
bool
ma prawo uznać, że wspomnianybool
zawiera jeden ze wzorów bitowych „true
” lub „false
” i zrobić wszystko, co mu się podoba. Więc jeśli wartościtrue
ifalse
są 1 i 0, odpowiednio, kompilator jest rzeczywiście pozwolił, aby zoptymalizowaćstrlen
do5 - <boolean value>
. Możliwe są inne zabawne zachowania!Jak wielokrotnie tu powtarzano, niezdefiniowane zachowanie ma niezdefiniowane wyniki. Zawierające Ale nie ograniczone do
Zobacz Co każdy programista powinien wiedzieć o nieokreślonym zachowaniu
źródło