Když vám váš program padá, tak je potřeba jej odladit a nalézt příčinu. Při ladění jste se již mohli setkat se situací, kdy program padá v metodě, která byla volaná přes nulový ukazatel. Přišlo vám však divné, že k pádu dojde až v těle metody, nikoliv již při volání metody. Popřípadě volání metody přes nulový ukazatel projde bez pádu. V dnešním příspěvku se dozvíte, proč k této situaci může dojít.
Začněme příkladem
Vezměte si následující kód:
#include <iostream> class A { public: void foo() { std::cerr << "foo called\n"; } void bar() { std::cerr << "bar called\n"; i = 5; } private: int i; }; int main() { A *a = nullptr; // nebo 0 či NULL v C++98 a->foo(); a->bar(); }
Když jej přeložíme a spustíme, dostaneme takovýto výstup (na použitém standardu nezáleží):
$ g++ -std=c++14 -pedantic -o prog prog.cpp && ./prog foo called bar called Segmentation fault (core dumped)
Při ladění zjistíte, že pád nastal až na řádku i = 5;
. Otázka je, proč volání foo()
a bar()
prošlo až tak daleko, když byly tyto dvě metody volány na nulovém ukazateli? Proč to nespadlo už při volání metody foo()
?
Nedefinované chování
Volání metody přes nulový ukazatel na objekt je v C++ nedefinované chování. Pokud by vás zajímalo zdůvodnění s odkazy na normu, mrkněte např. zde. Tedy přísně řečeno, kód uvedený výše se může chovat libovolně, protože obsahuje konstrukci s nedefinovaným chováním.
Proč to ale většinou dopadne takto?
Když tedy přejdeme ono nedefinované chování, tak se naskýtá otázka, proč to obvykle dopadne tak, jak to dopadlo v našem příkladu. Důvodem je, že překladače onen kód přeloží typicky takto:
struct _A { int i; }; void _ZN1A3fooEv(_A *this) { // A::foo() // [..] výpis "foo called" } void _ZN1A3barEv(_A *this) { // A::bar() // [..] výpis "bar called" this->i = 5; // přesněji (*this).i = 5 } int main() { A *a = 0; _ZN1A3fooEv(a); _ZN1A3barEv(a); }
K samotné dereferenci tedy poprvé dojde až na řádku this->i = 5;
, což je důvodem, proč program spadne až tam. Že je tomu opravdu tak lze vidět z výstupu disassembleru (přeložte si program s ladicími informacemi, tj. s přepínačem -g
a mrkněte na výstup z objdump -S prog
).
Co se stane, pokud je metoda virtuální?
Jak si možná pamatujete z knih či kurzů C++, pokud třída obsahuje virtuální metodu, tak je ke každému objektu přiřazen ukazatel na tabulku virtuálních metod. Je to z toho důvodu, aby překladač mohl zaručit, že se za běhu programu zavolá korektní metoda v případech, kdy při překladu není zřejmé, jaký je typ skutečného objektu, na který daný ukazatel/reference odkazuje. Onen ukazatel se typicky nachází buď před daty objektu, nebo až za nimi.
Dejme tomu, že náš původní kód změníme takto:
virtual void foo() { // nyní se jedná o virtuální metodu std::cerr << "foo called\n"; }
Pokud nedojde k optimalizaci při překladu, tak překladač typicky vygeneruje něco takového (umístění ukazatele na tabulku virtuálních metod se liší překladač od překladače):
struct _A { /* Interní typ VTABLE */ *_vtable; int i; } /* Interní typ VTABLE */ _VTABLE_A[1] = { _ZN1A3fooEv // A::foo() } // ... A *a = 0; a->_vtable[0](a); // či přesněji (*a)._vtable[0](a);
V tomto případě dojde k pádu programu již při volání metody foo()
z důvodu přístupu k tabulce virtuálních metod a program tak nic nevypíše. Opět se ale jedná o nedefinované chování a záleží na konkrétním překladači.
Jinak, v oné tabulce virtuálních metod se toho obecně může nacházet více, např. ukazatel na příslušnou type_info
strukturu pro využití při RTTI (detaily).
Ten první příklad je pěkná
Ten první příklad je pěkná ukázka toho, kdy by se hodil ubsan (gcc 4.9+):
Re: Ten první příklad je pěkná
Díky za info! O tomto detektoru jsem neslyšel.
Začali jsme ho psát pro GCC
Začali jsme ho psát pro GCC 4.9, ale hodně nových věcí/optimalizací bude až v GCC 5. Něco jsem o tom psal v blogpostu tady: http://developerblog.redhat.com/2014/10/16/gcc-undefined-behavior-sanit… - snad to někomu přijde vhod.
Re: Začali jsme ho psát pro GCC
Jo jo, ten příspěvek na blogu jsem nedávno četl a UBSAN zkoušel. Parádní věcička. Jsem zvědavý na GCC 5.