Home

Ninja-Segfault

Montag, 7. Februar 2011 | Autor:

Letz­te Woche hat­te ich einen Bug, der mich 4 Stun­den und die ent­spre­chen­de Men­ge Ner­ven gekos­tet hat. Der Ori­gi­nal­code ist ziem­lich undurch­sich­tig, des­we­gen habe ich eine klei­ne, kom­pi­lier­ba­re Demo geschrieben:

#include <vector>
#include <stack>

class Blubb
{
	public:
		std::vector<int> * liste;
		Blubb() {
			liste = new std::vector<int>(10);
		}
		~Blubb() {
			delete liste;
		}
};

int main()
{
	std::stack<Blubb> stapel;
	Blubb blip;
	stapel.push(blip);
	return 0;
}

Wer meint, C++ zu beherr­schen, darf jetzt grü­beln, war­um die­ser Code einen Seg­fault wirft. Viel Spaß dabei! 😉

Ansons­ten füh­ren wir die­sen Code ein­fach mal aus:

nico@linux:~/code> ./a.out
*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x0000000000606060 ***
======= Backtrace: =========
/lib64/libc.so.6(+0x73226)[0x7fb32a25c226]
/lib64/libc.so.6(cfree+0x6c)[0x7fb32a260fcc]
./a.out[0x400a83]
./a.out[0x4026d2]
./a.out[0x40240c]
./a.out[0x402031]
./a.out[0x401cb3]
./a.out[0x4016a6]
./a.out[0x40105f]
./a.out[0x400c1e]
./a.out[0x400aa2]
./a.out[0x400930]
/lib64/libc.so.6(__libc_start_main+0xfd)[0x7fb32a207b7d]
./a.out[0x4007f9]
======= Memory map: ========
00400000-00405000 r-xp 00000000 08:07 2105675                            /home/nico/code/a.out
00604000-00605000 r--p 00004000 08:07 2105675                            /home/nico/code/a.out
    ... [diverse Zeilen ausgelassen] ...
7fff829f3000-7fff829f4000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
Aborted
nico@linux:~/code>

Die­se Mel­dung ist noch rela­tiv aus­sa­ge­kräf­tig - der ursprüng­li­che Code aller­dings zeig­te die­se nicht, son­dern rief nur: Seg­fault! Seg­fault bedeu­tet: Wir grei­fen auf Spei­cher zu, auf den wir nicht zugrei­fen dür­fen. Das pas­siert fast aus­schließ­lich aus zwei Gründen:

1. Man greift auf eine Stel­le in einem Array o.Ä. zu, die gar nicht mehr im Array drin liegt. Typi­sches Beispiel:

vector liste(3);
cout << liste[3];

Hier wird ein vec­tor der Län­ge 3 defi­niert. Dann möch­te man das drit­te Ele­ment anzei­gen - da aber die Zäh­lung bei Null beginnt, greift man mit [3] fälsch­li­cher­wei­se auf das vier­te Ele­ment zu.

2. Man hat mit Poin­tern1 rum­ge­spielt und dabei nicht auf­ge­passt und nun zeigt irgend ein Poin­ter an eine Spei­cher­stel­le, die uns nicht gehört.

Ursache

Der Bug im obi­gen Code ist aus der zwei­ten Kate­go­rie. Am bes­ten wir gehen den Code noch­mal durch:

class Blubb
{
	public:
		std::vector<int> * liste;
		Blubb() {
			liste = new std::vector<int>(10);
		}
		~Blubb() {
			delete liste;
		}
};

Wir haben eine Klas­se Blubb - die­se erzeugt in ihrem Kon­struk­tor mit new2 einen vec­tor und spei­chert den Poin­ter, der auf die­sen zeigt. Mit new erzeug­te Objek­te gehen nicht von selbst wie­der weg - wenn wir sie nicht mehr brau­chen, müs­sen wir sie mit delete wie­der löschen, d.h. den Spei­cher­platz, den sie bele­gen, wie­der frei­ge­ben. Das tun wir hier im Destruk­tor. Täten wir’s nicht, wür­de der Vec­tor in alle Ewig­keit Spei­cher­platz bele­gen - das nennt man Spei­cher­leck3 und ist natür­lich zu vermeiden.

Soweit, so gut. Kom­men wir zur main:

int main()
{
	std::stack<Blubb> stapel;
	Blubb blip;
	stapel.push(blip);
	return 0;
}

Wir machen uns hier einen Stack auf, auf den wir ein Blubb-Objekt legen wol­len. Ich habe hier einen Stack genom­men, aber das Pro­blem, das gleich auf­taucht, exis­tiert mit nahe­zu allen Con­tai­nern4.

Wenn man jetzt eine Wei­le rum­spielt und den Feh­ler ein­kreist, merkt man irgend­wann, dass der Seg­fault von unse­rem delete liste; her­rührt. Man merkt dann auch, dass delete liste; zwei­mal(!) auf­ge­ru­fen wird, weil der Desk­truk­tor zwei­mal auf­ge­ru­fen wird!

Hä?!” war auch mei­ne ers­te Reaktion.

Das Pro­blem ist, dass push(blip); das Objekt blip auf den Stack kopiert. Das heißt wir haben nun zwei Kopien von blip! Bei­de Kopien ent­hal­ten den­sel­ben Poin­ter zur liste.

Wenn nun das Ende der Funk­ti­on main erreicht ist, wer­den alle in main defi­nier­ten Objek­te zer­stört, d.h. deren Destruk­tor auf­ge­ru­fen. Die­se Objek­te sind 1. stapel, 2. alle Objek­te auf dem Sta­pel 3. blip.

Das ist ein Pro­blem, denn sowohl bei der Zer­stö­rung von blip, als auch der Zer­stö­rung der Kopie, wird delete liste; auf­ge­ru­fen. Beim ers­ten Mal ist noch alles in Ord­nung: Der Spei­cher­platz an der in liste gespei­cher­ten Adres­se wird frei­ge­ge­ben. Beim zwei­ten Mal ver­sucht delete erneut, den sel­ben Spei­cher frei­zu­ge­ben, der uns nun aber gar nicht mehr gehört! Das ist nicht in Ord­nung und kann zu Feh­ler­mel­dun­gen und Abstür­zen füh­ren5.

Nun ist auch die obi­ge Feh­ler­mel­dung klar:

*** glibc detected *** ./a.out: double free or corruption (fasttop): 0x0000000000606060 ***

Ers­te­res ist der Fall: dou­ble free - hier in Form eines dop­pel­ten delete6.

Lösungsmöglichkeiten

Man könn­te den­ken, das Pro­blem lie­ße sich mit fol­gen­der Vari­an­te umgehen:

int main()
{
	std::stack<Blubb> stapel;
	stapel.push(Blubb());
	return 0;
}

Tat­säch­lich aber wer­den auch hier zwei Kopien erzeugt. Zunächst wird mit Blubb() ein, wenn auch namen­lo­ses, Objekt erzeugt. Davon wird ganz regu­lär eine Kopie erstellt und das namen­lo­se Objekt dann wie­der gelöscht (und delete liste; aus­ge­führt). Es ist genau das sel­be Spiel - wir gewin­nen nichts dabei.

Natür­lich ist es nicht sinn­voll, extra ein namen­lo­ses Objekt anzu­le­gen, nur damit es kopiert und dann gleich wie­der gelöscht wird. Das kos­tet schließ­lich alles Per­for­mance. In Zukunft soll C++ daher ein paar Mecha­nis­men erhal­ten, die das ele­gan­ter lösen. Aber bis die­ser neue C++-Standard namens C++0x da ist, ver­ge­hen sicher noch eini­ge Jah­re. Der GCC ent­hält zwar schon ein paar der ange­dach­ten Fea­tures, aber noch nichts, was uns hier wei­ter­hel­fen würde.

Copy-Constructor

Das Kern­pro­blem an der gan­zen Sache ist, dass wir kei­nen eige­nen Copy-Con­s­truc­tor defi­niert haben. Des­halb defi­niert sich C++ selbst einen. Die­ser kopiert ein­fach die gan­ze Klas­se, was an sich gut klingt - aber was pas­siert mit unse­rem Poin­ter liste? Der wird kopiert. Was pas­siert mit dem Vec­tor, auf den der Poin­ter zeigt? Nichts! C++ fasst die­sen Vec­tor nicht an - es wird per default wirk­lich nur der Poin­ter kopiert!

Die­se Art der Kopie nennt man Shal­low-Copy. Das ist der Grund, wes­halb bei­de Kopien auf den sel­ben Vec­tor zeigen.

Wie löst man das Pro­blem? Es gibt zwei Möglichkeiten:

1. Deep Copy

Das Gegen­teil von Shal­low-Copy heißt Deep-Copy. Deep bedeu­tet hier, dass auch Daten­struk­tu­ren, für die in der Klas­se ledig­lich ein Poin­ter exis­tiert, mit­ko­piert wer­den. Das muss man manu­ell machen, das heißt, man muss einen Copy-Con­s­truc­tor schrei­ben. Zum Bei­spiel sowas7:

Blubb(const Blubb & original)	{
	liste = new std::vector(*original.liste);
}

2. Kopieren verbieten

Man kann das Kopie­ren auch schlicht ver­bie­ten! Dazu defi­niert man ein­fach einen private Copy-Constructor:

private:
Blubb(const Blubb & original)	{
	liste = new std::vector(*original.liste);
}

Ob das sinn­voll ist, kommt aller­dings auf das Design des Pro­gramms an.

  1. Varia­blen, die selbst kei­nen Inhalt ent­hal­ten, son­dern eine Spei­cher­adres­se. Auch „Zei­ger” genannt.
  2. für mehr Infor­ma­tio­nen bezüg­lich dyna­mi­scher Spei­cher­al­lo­ka­ti­on mit new, sie­he cplusplus.com/doc/tutorial/dynamic
  3. In die­sem klei­nen Code­stück macht es natür­lich nicht viel aus - aber es ist ja nur ein Bei­spiel!
  4. Außer intru­si­ve Con­tai­nern aus z.B. Boost
  5. Muss aber nicht. Lei­der, denn so kann der Bug lan­ge unent­deckt blei­ben.
  6. malloc/free sind noch aus der C-Welt. In C++ ist die Spei­cher­al­lo­ka­ti­on mit­tels new/delete übli­cher.
  7. Ich über­neh­me kei­ne Garan­tie für Kor­rekt­heit. Beim Defi­nie­ren von Copy-Con­struk­to­ren oder Assign­ment-Ope­ra­to­ren gibt es eine Rei­he Fall­stri­cke, mit denen ich mich nicht im Detail befasst habe.
Tags » , «

Trackback: