Home

Ninja-Segfault

Montag, 7. Februar 2011 | Autor:

Letzte Woche hatte ich einen Bug, der mich 4 Stun­den und die ent­spre­chende Menge Ner­ven gekos­tet hat. Der Ori­gi­nal­code ist ziem­lich undurch­sich­tig, des­we­gen habe ich eine kleine, kom­pi­lier­bare 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, warum 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>

Diese Mel­dung ist noch rela­tiv aus­sa­ge­kräf­tig - der ursprüng­li­che Code aller­dings zeigte diese 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 Stelle 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änge 3 defi­niert. Dann möchte man das dritte Ele­ment anzei­gen - da aber die Zäh­lung bei Null beginnt, greift man mit [3] fälsch­li­cher­weise auf das vierte 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­stelle, die uns nicht gehört.

Ursa­che

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 Klasse Blubb - diese erzeugt in ihrem Kon­struk­tor mit new2 einen vec­tor und spei­chert den Poin­ter, der auf die­sen zeigt. Mit new erzeugte Objekte 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ürde 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 nahezu allen Con­tai­nern4.

Wenn man jetzt eine Weile 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 meine erste 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! Beide Kopien ent­hal­ten den­sel­ben Poin­ter zur liste.

Wenn nun das Ende der Funk­tion main erreicht ist, wer­den alle in main defi­nier­ten Objekte zer­stört, d.h. deren Destruk­tor auf­ge­ru­fen. Diese Objekte sind 1. stapel, 2. alle Objekte 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 Adresse 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 obige 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ösungs­mög­lich­kei­ten

Man könnte den­ken, das Pro­blem ließe sich mit fol­gen­der Vari­ante 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­lose Objekt dann wie­der gelöscht (und delete liste; aus­ge­führt). Es ist genau das selbe 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 einige Jahre. 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-Constructor defi­niert haben. Des­halb defi­niert sich C++ selbst einen. Die­ser kopiert ein­fach die ganze Klasse, 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!

Diese Art der Kopie nennt man Shallow-Copy. Das ist der Grund, wes­halb beide 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 Shallow-Copy heißt Deep-Copy. Deep bedeu­tet hier, dass auch Daten­struk­tu­ren, für die in der Klasse 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-Constructor schrei­ben. Zum Bei­spiel sowas7:

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

2. Kopie­ren 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­adresse. Auch „Zei­ger” genannt.
  2. für mehr Infor­ma­tio­nen bezüg­lich dyna­mi­scher Spei­cher­al­lo­ka­tion mit new, siehe 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 intrusive Con­tai­nern aus z.B. Boost
  5. Muss aber nicht. Lei­der, denn so kann der Bug lange unent­deckt blei­ben.
  6. malloc/free sind noch aus der C-Welt. In C++ ist die Spei­cher­al­lo­ka­tion mit­tels new/delete übli­cher.
  7. Ich über­nehme keine Garan­tie für Kor­rekt­heit. Beim Defi­nie­ren von Copy-Construktoren oder Assignment-Operatoren gibt es eine Reihe Fall­stri­cke, mit denen ich mich nicht im Detail befasst habe.

Tags » , «

Trackback: Trackback-URL |  Feed zum Beitrag: RSS 2.0
Thema: Sezierte C++-Käfer

Diesen Beitrag kommentieren.

2 Kommentare

  1. 1
    der_Karl 

    Du hät­test es mal bes­ser als „Wer meint, die STL zu beherr­schen“. Ich hab die letz­ten vier Jahre in der Firma mit unse­rem eige­nen Frame­work rum­gefri­ckelt. Da sind Copy-Konstruktoren obli­ga­to­risch, eben genau wegen sol­cher Bugs. Dem­ent­spre­chend bin ich es gar nicht mehr gewohnt, keine Feh­ler­mel­dung um die Ohren gehauen zu bekom­men, wenn ich kei­nen Copy-Konstruktor implementiere. ;)

    Ein­ge­grenzt bekom­men hatte ich den Feh­ler zwar trotz­dem, aller­dings war die Feh­ler­mel­dung, die mir der gdb unter OSX an den Kopf warf bei wei­tem nicht so aus­sa­ge­kräf­tig, wie die hier von dir ange­ge­bene. Außer­dem merke ich so lang­sam, dass ich ein­roste. Ich sollte echt drin­gend mal wie­der was coden. *g*

  2. 2
    Nico 

    Ich glaube C++ ist so umfang­reich - irgendwo rostet’s immer ;)

Kommentar abgeben