2010年7月22日星期四

解讀Valgrind的訊息(1)

C/C++是一個沒有garbage collection的語言,動態呼叫所佔用的記憶體必須自行處理,一旦處置不當便會產生memory leakage及dangling pointer等各種問題,Memory leakage若非嚴重是有機會不影響程式的運作,而dangling pointer在大多情況下也可以很容易地用debugger找出來(不保證就是了)。

可是若發生memory的invalid read/write問題如heap block overrun之類,用debugger也不一定找得出來,錯誤的程序不會立即造成問題,就像武林高手用內功震傷對手後潛勁不會立即發作,而是隨時間加深傷勢,忽然間就爆發出來。

很多時程式會在不固定的地方crash,又或者在不同的機器上出現不同的表現,更常見是Debugger環境下執行及實際執行的結果有所出入,實際執行時是在A的地方出錯,但在debugger下卻在B的地方停下來,解決這些問題往往令人抓狂,這也是C/C++語言令人恐懼的原因之一。

這時候就要祭出終極武器Valgrind來幫你疏通經脈,這是一個有關於記憶體運用的除錯器、尋找洩漏、效能評估的工具,它能監控程式一切的運作,令記憶體有關的問題無所遁形。

不過呢,越是強大的工具就往往越難操作,Valgrind正是屬於這類,主要有二個問題

1)大量的假陽性的報告
很多時候都會回報大量的錯誤報告,但實際上都是對程式沒有影響的

2) 訊息的說明不足、難以理解
以前Valgrind的說明書是很簡短的,現在好了很多,但訊息並不一定直接告訢你問題的成因,只能大概告訢你這裏有些錯誤,加上有(1)提到的問題,怎樣找出有用的訊息及正確地解讀就要看閣下的功力。
這二星期不幸地都要祭出valgrind除錯,剛好有些能公開的代碼,所以就試試解說其中一項訊息,以後如果還遇上的話可以再說多一點,所以文章會分幾部份,但什麼時候有下一集就不得而知了。

這次會說的訊息是:

Address 0x8015be4 is 4 bytes inside a block of size 52 free'd

問題程式:PenPen (一個繪圖程式)
平台:N900
繪圖庫:Qt
取得代碼: bzr branch -r 29 lp:penpen

症狀:

1) 在進行了Refactor及增加一些功能後,在任一幅圖畫上繪畫,然後開啟另一幅畫,這時候會segfault

2) 如果光是觀看圖畫,不修改,則無論重覆多少次打開及關閉也沒有問題

3) 在GDB裏會見到是以下代碼出錯:
class QGraphicsSceneFindItemBspTreeVisitor : public QGraphicsSceneBspTreeVisitor
{

    QList *foundItems;
    bool onlyTopLevelItems;

    void visit(QList *items)
    {
        for (int i = 0; i < items->size(); ++i) {
        QGraphicsItem *item = items->at(i);
        if (onlyTopLevelItems && item->d_ptr->parent) // 在這裏出錯,存取item導至segfault
            item = item->topLevelItem();
            if (!item->d_func()->itemDiscovered && item->d_ptr->visible)     {
                item->d_func()->itemDiscovered = 1;
            foundItems->prepend(item);
            }
    }
    }

};
4) 看stack trackback會發現程式並沒有直接呼叫過QGraphicsSceneFindItemBspTreeVisitor::visit(),是經event loop去呼叫的,這是Qt的內部function。

初步診斷:

  1. 因為在Refactor前沒有問題,應該不是Qt的bug
  2. 因為沒有直接呼叫有關的function,是因為不當的API呼叫程序的可能性不高
  3. 已檢查過refactor前後的代碼修改,不覺得有出錯的地方,因為程式在自己沒有觸及的地方segfault,故認為記憶體的invalid read、write的嫌疑最大,就算嫌疑較低,若能排除這個可能性亦有助於除錯,所以決定使用valgrind。
我所使用的指令

valgrind --error-limit=no --tool=memcheck --leak-check=full --show-reachable=yes --track-origins=yes --freelist-vol=62914560 ./penpen 2>&1 | tee log

整份報告有3萬行,不過大部份都是memory leakage有關的,其中要留意的實際只有12項報告,很快就找出了相關的:

==4948== Invalid read of size 4
==4948== at 0x48F8850: QGraphicsSceneFindItemBspTreeVisitor::visit(QList*) (qgraphicsscene_bsp.cpp:79)
==4948== by 0x48F6C21: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:247)
==4948== by 0x48F6C97: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:261)
==4948== by 0x48F6C5A: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:252)
==4948== by 0x48F6C97: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:261)
==4948== by 0x48F6C5A: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:252)
==4948== by 0x48F6C97: QGraphicsSceneBspTree::climbTree(QGraphicsSceneBspTreeVisitor*, QRectF const&, int) const (qgraphicsscene_bsp.cpp:261)
==4948== by 0x48F6E7E: QGraphicsSceneBspTree::items(QRectF const&, bool) const (qgraphicsscene_bsp.cpp:154)
==4948== by 0x48FAA0F: QGraphicsSceneBspTreeIndexPrivate::estimateItems(QRectF const&, Qt::SortOrder, bool) (qgraphicsscenebsptreeindex.cpp:387)
==4948== by 0x48FAC01: QGraphicsSceneBspTreeIndex::estimateTopLevelItems(QRectF const&, Qt::SortOrder) const (qgraphicsscenebsptreeindex.cpp:540)
==4948== by 0x48E9FD7: QGraphicsScenePrivate::drawItems(QPainter*, QTransform const*, QRegion*, QWidget*) (qgraphicsscene.cpp:4617)
==4948== by 0x490AC9D: QGraphicsView::paintEvent(QPaintEvent*) (qgraphicsview.cpp:3393)
==4948== Address 0x8015be4 is 4 bytes inside a block of size 52 free'd
==4948== at 0x402387B: operator delete(void*) (vg_replace_malloc.c:387)
==4948== by 0x8064A05: GraphicsStroke::~GraphicsStroke() (graphicsstroke.h:10)
==4948== by 0x806773A: SketchPaper::clear() (paper.cpp:107)
==4948== by 0x80577E3: StandardPage::clear() (page.cpp:88)
==4948== by 0x8057BF0: StandardPage::load(int) (page.cpp:33)
==4948== by 0x806A6F5: PageEditorWindow::open(int) (pageeditorwindow.cpp:83)
==4948== by 0x80538FC: Maemo5Application::openPage(int) (maemo5application.cpp:100)
==4948== by 0x807179D: Maemo5Application::qt_metacall(QMetaObject::Call, int, void**) (moc_maemo5application.cpp:79)
==4948== by 0x4D1EADA: QMetaObject::metacall(QObject*, QMetaObject::Call, int, void**) (qmetaobject.cpp:237)
==4948== by 0x4D2C5A6: QMetaObject::activate(QObject*, QMetaObject const*, int, void**) (qobject.cpp:3285)
==4948== by 0x8070B6F: PageViewerWindow::requestOpenPage(int) (moc_pageviewerwindow.cpp:101)
==4948== by 0x80688C6: PageViewerWindow::on_listView_activated(QModelIndex) (pageviewerwindow.cpp:65)

4948是指process的PID,在排除call stack以後,實際的錯誤只有二項:

  1. Invalid read of size 4
  2. Address 0x8015be4 is 4 bytes inside a block of size 52 free'd
第一項是指valgrind認為這裏存取了不當的記憶體地址,這種訊息是最常發生的,而大部份都是誤判,而這裏可以排除這個可能性,因為與gdb的資訊一致。

另外留意一件事,這裏雖然說size 4,但實際所讀取的量不是這個數字也會當成是4,這與memory alignment有關,理由不多說,總之不要深究是否真的讀了4 bytes,也不要因此判斷是否正確。

至於第2項,相信會令人很費解,首先我們要知道,2)其實是在說1)的成因,QGraphicsSceneFindItemBspTreeVisitor::visit錯誤地讀取了記憶體,而這段記憶體在2)的時候發生了一件事。

看一看 2)的call stack:

==4948== Address 0x8015be4 is 4 bytes inside a block of size 52 free'd
==4948== at 0x402387B: operator delete(void*) (vg_replace_malloc.c:387)
==4948== by 0x8064A05: GraphicsStroke::~GraphicsStroke() (graphicsstroke.h:10)
==4948== by 0x806773A: SketchPaper::clear() (paper.cpp:107)

這段訊息要這樣理解

  • a) QGraphicsSceneFindItemBspTreeVisitor::visit嘗試存取0x8015be4這個地址,但這個地址是不能讀取的,正是segfault的原兇
  • b) 0x8015be4這個地址是曾經正確的,那時候有一塊連續52 bytes的記憶體是可以讀寫的,而0x8015be4就在這段記憶體中
  • c) Call stack告訢了你,這段記憶體是在什麼時候被釋放的,那是在paper.cpp的107行發生的。
換言之,0x8015be4是一個dangling pointer,指向了一個已被釋放的空間 - 你就是真兇!(若有什麼說話是最想在現實裹說一次的,相信這是no.1的首選)

那應該很簡單解決的。

但事實上卻不是那回事……因為QGraphicsSceneFindItemBspTreeVisitor::visit()不是由PenPen直接呼叫的,是處理user interaction時被叫喚的,那個GraphicsStroke物件為什麼在paper.cpp:107被釋放後,Qt仍保留了一個 reference呢?

到了這階段就不能靠valgrind幫忙了,valgrind己經成功地排除了我的代碼中有直接做出dangling pointer的可能,餘下的只能翻Qt的說明,再Google了數十遍文章,總算知道原來是Qt的bug...

147478

Crash in QGraphicsSceneBspTree when adding items in response to a mouse press and adjusting them when moving.

Resolution: Always call prepareGeometryChange() before changing the item's geometry. Otherwise, the scene's index will fall out of sync, and the behavior is undefined.

如果prepareGeometryChange()這個function不是依某個次序喚叫,就會令Indexing有*可能*出錯(不一定發生),結果它保留了一份己釋放的reference(dangling pointer),換言之最初的診斷1)及2)是正確的…… 囧

在Refactor前沒有問題,僅僅是因為好彩沒有撞上這個問題…… 後來把prepareGeometryChange()的次序改一改就讓問題解決了。

結論

我認為這個bug的除錯難度屬於中等偏高,在除錯前要先理解到底是自己的代碼導至segfault、還是由library簡接造成,前者只要熟用gdb/valgrind便能快速地驗證,不過熟用valgrind本身就要一定的功力了。

到發現是Qt的bug,若非在google上找到相關的文章,就必需花費大量時間去驗證,要對Qt很熟識才有可能做到。我找到其他類似的bug report,本身要reproduce這個bug已經要花了很多時間(遞交者說要先要弄20k個instance... Orz),而且TrollTech至今仍未解決,要自己弄出解決方法並不輕鬆。

沒有留言:

Creative Commons License
本網誌Ben Lau製作,以共享創意署名-非商業性-相同方式共享 3.0 香港 授權條款釋出。