[授權發表]代碼測試、調試與優化小結

by falcon [email protected] of TinyLab.org
2008-2-29

最初發表:泰曉科技 – 聚焦嵌入式 Linux,追本溯源,見微知著!
原文鏈接:代碼測試、調試與優化小結
評論說明:爲更好地聚合大家的討論,請到上面原文的評論區回覆。


【注】這是開源書籍《C語言編程透視》第九章,如果您喜歡該書,請關注我們的新浪微博@泰曉科技

代碼測試、調試與優化小結

前言

代碼寫完以後往往要做測試(或驗證)、調試,可能還要優化。

  • 關於測試(或驗證)

通常對應着兩個英文單詞verification和validation,在資料1中有關於這個的定義和一些深入的討論,在資料2中,很多人給出了自己的看法。但是正如資料2提到的:

“The differences between verification and validation are unimportant except to the theorist; practitioners use the term V&V to refer to all of the activities that are aimed at making sure the software will function as required.”

所以,無論測試(或驗證)目的都是爲了讓軟件的功能能夠達到需求。測試和驗證通常會通過一些形式化(貌似可以簡單地認爲有數學根據的)或者非形式化的方法去驗證程序的功能是否達到要求。

  • 關於調試

而調試對應英文debug,debug叫“驅除害蟲”,也許一個軟件的功能達到了要求,但是可能會在測試或者是正常運行時出現異常,因此需要處理它們。

  • 關於優化

debug是爲了保證程序的正確性,之後就需要考慮程序的執行效率,對於存儲資源受限的嵌入式系統,程序的大小也可能是優化的對象。

很多理論性的東西實在沒有研究過,暫且不說吧。這裏只是想把一些需要動手實踐的東西先且記錄和總結一下,另外很多工具在這裏都有提到和羅列,包括Linux內核調試相關的方法和工具。關於更詳細更深入的內容還是建議直接看後面的參考資料爲妙。

下面的所有演示在如下環境下進行:

$ uname -a
Linux falcon 2.6.22-14-generic #1 SMP Tue Feb 12 07:42:25 UTC 2008 i686 GNU/Linux
$ echo $SHELL
/bin/bash
$ /bin/bash --version | grep bash
GNU bash, version 3.2.25(1)-release (i486-pc-linux-gnu)
$ gcc --version | grep gcc
gcc (GCC) 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)
$ cat /proc/cpuinfo | grep "model name"
model name      : Intel(R) Pentium(R) 4 CPU 2.80GHz

代碼測試

代碼測試有很多方面,例如運行時間、函數調用關係圖、代碼覆蓋度、性能測試(profiling)、內存訪問越界(segmentation fault)、緩衝區溢出(stack smashing合法地進行非法的內存訪問?所以很危險)、內存泄露(memory leak)等。

測試程序的運行時間 time

Shell提供了內置命令time用於測試程序的執行時間,默認顯示結果包括三部分:實際花費時間(real time)、用戶空間花費時間(user time)和內核空間花費時間(kernel time)。

$ time pstree 2>&1 >/dev/null

real    0m0.024s
user    0m0.008s
sys     0m0.004s

time命令給出了程序本身的運行時間。這個測試原理非常簡單,就是在程序運行(通過system函數執行)前後記錄了系統時間(用times函數),然後進行求差就可以。如果程序運行時間很短,運行一次看不到效果,可以考慮採用測試紙片厚度的方法進行測試,類似把很多紙張疊到一起來測試紙張厚度一樣,我們可以讓程序運行很多次。

如果程序運行時間太長,執行效率很低,那麼得考慮程序內部各個部分的執行情況,從而對代碼進行可能的優化。具體可能會考慮到這兩點:

對於C語言程序而言,一個比較宏觀的層次性的輪廓(profile)是函數調用圖、函數內部的條件分支構成的語句塊,然後就是具體的語句。把握好這樣一個輪廓後,就可以有針對性地去關注程序的各個部分,包括哪些函數、哪些分支、哪些語句最值得關注(執行次數越多越值得優化,術語叫hotspots)。

對於Linux下的程序而言,程序運行時涉及到的代碼會涵蓋兩個空間,即用戶空間和內核空間。由於這兩個空間涉及到地址空間的隔離,在測試或調試時,可能涉及到兩個空間的工具。前者絕大多數是基於gcc的特定參數和系統的ptrace調用,而後者往往實現爲內核的補丁,它們在原理上可能類似,但實際操作時後者顯然會更麻煩,不過如果你不去hack內核,那麼往往無須關心後者。

函數調用關係圖 calltree

calltree可以非常簡單方便地反應一個項目的函數調用關係圖,雖然諸如gprof這樣的工具也能做到,不過如果僅僅要得到函數調用圖,calltree應該是更好的選擇。如果要產生圖形化的輸出可以使用它的-dot參數。從這裏可以下載到它。

這裏是一份基本用法演示結果:

$ calltree -b -np -m *.c
main:
|   close
|   commitchanges
|   |   err
|   |   |   fprintf
|   |   ferr
|   |   ftruncate
|   |   lseek
|   |   write
|   ferr
|   getmemorysize
|   modifyheaders
|   open
|   printf
|   readelfheader
|   |   err
|   |   |   fprintf
|   |   ferr
|   |   read
|   readphdrtable
|   |   err
|   |   |   fprintf
|   |   ferr
|   |   malloc
|   |   read
|   truncatezeros
|   |   err
|   |   |   fprintf
|   |   ferr
|   |   lseek
|   |   read$ 

這樣一份結果對於“反向工程”應該會很有幫助,它能夠呈現一個程序的大體結構,對於閱讀和分析源代碼來說是一個非常好的選擇。雖然cscope和ctags也能夠提供一個函數調用的“即時”(在編輯vim的過程中進行調用)視圖(view),但是calltree卻給了我們一個宏觀的視圖。

不過這樣一個視圖只涉及到用戶空間的函數,如果想進一步給出內核空間的宏觀視圖,那麼strace,KFT或者Ftrace就可以發揮它們的作用。另外,該視圖也沒有給出庫中的函數,如果要跟蹤呢?需要ltrace工具。

另外發現calltree僅僅給出了一個程序的函數調用視圖,而沒有告訴我們各個函數的執行次數等情況。如果要關注這些呢?我們有gprof。

性能測試工具 gprof & kprof

參考資料3詳細介紹了這個工具的用法,這裏僅挑選其中一個例子來演示。gprof是一個命令行的工具,而KDE桌面環境下的kprof則給出了圖形化的輸出,這裏僅演示前者。

首先來看一段代碼(來自資料3),算Fibonacci數列的,

#include <stdio.h>

int fibonacci(int n);

int main (int argc, char **argv)
{
    int fib;
    int n;

    for (n = 0; n <= 42; n++) {
        fib = fibonacci(n);
        printf("fibonnaci(%d) = %dn", n, fib);
    }

    return 0;
}

int fibonacci(int n)
{
    int fib;

    if (n <= 0) {
        fib = 0;
    } else if (n == 1) {
        fib = 1;
    } else {
        fib = fibonacci(n -1) + fibonacci(n - 2);
    }

    return fib;
}

通過calltree看看這段代碼的視圖,

$ calltree -b -np -m *.c
main:
|   fibonacci
|   |   fibonacci ....
|   printf

可以看出程序主要涉及到一個fibonacci函數,這個函數遞歸調用自己。爲了能夠使用gprof,需要編譯時加上-pg選項,讓gcc加入相應的調試信息以便gprof能夠產生函數執行情況的報告。

$ gcc -pg -o fib fib.c
$ ls
fib  fib.c

運行程序並查看執行時間,

$ time ./fib
fibonnaci(0) = 0
fibonnaci(1) = 1
fibonnaci(2) = 1
fibonnaci(3) = 2
...
fibonnaci(41) = 165580141
fibonnaci(42) = 267914296

real    1m25.746s
user    1m9.952s
sys     0m0.072s
$ ls
fib  fib.c  gmon.out

上面僅僅選取了部分執行結果,程序運行了1分多鐘,代碼運行以後產生了一個gmon.out文件,這個文件可以用於gprof產生一個相關的性能報告。

$ gprof  -b ./fib gmon.out
Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total
 time   seconds   seconds    calls  ms/call  ms/call  name
 96.04     14.31    14.31       43   332.80   332.80  fibonacci
  4.59     14.99     0.68                             main

                        Call graph

granularity: each sample hit covers 2 byte(s) for 0.07% of 14.99 seconds

index % time    self  children    called     name
                                                 <spontaneous>
[1]    100.0    0.68   14.31                 main [1]
               14.31    0.00      43/43          fibonacci [2]
-----------------------------------------------
                             2269806252             fibonacci [2]
               14.31    0.00      43/43          main [1]
[2]     95.4   14.31    0.00      43+2269806252 fibonacci [2]
                             2269806252             fibonacci [2]
-----------------------------------------------

Index by function name

   [2] fibonacci               [1] main

從這份結果中可觀察到程序中每個函數的執行次數等情況,從而找出值得修改的函數。在對某些部分修改之後,可以再次比較程序運行時間,查看優化結果。另外,這份結果還包含一個特別有用的東西,那就是程序的動態函數調用情況,即程序運行過程中實際執行過的函數,這和calltree產生的靜態調用樹有所不同,它能夠反應程序在該次執行過程中的函數調用情況。而如果想反應程序運行的某一時刻調用過的函數,可以考慮採用gdb的backtrace命令。

類似測試紙片厚度的方法,gprof也提供了一個統計選項,用於對程序的多次運行結果進行統計。另外,gprof有一個KDE下圖形化接口kprof,這兩部分請參考資料3

對於非KDE環境,可以使用Gprof2Dot把gprof輸出轉換成圖形化結果。

關於dot格式的輸出,也可以可以考慮通過dot命令把結果轉成jpg等格式,例如:

$ dot -Tjpg test.dot -o test.jp

gprof雖然給出了函數級別的執行情況,但是如果想關心具體哪些條件分支被執行到,哪些語句沒有被執行,該怎麼辦?

代碼覆蓋率測試 gcov & ggcov {#代碼覆蓋率測試-gcov-ggcov}

如果要使用gcov,在編譯時需要加上這兩個選項-fprofile-arcs -ftest-coverage,這裏直接用之前的fib.c做演示。

$ ls fib.c $ gcc -fprofile-arcs -ftest-coverage -o fib fib.c $ ls fib fib.c fib.gcno 運行程序,並通過gcov分析代碼的覆蓋度:

$ ./fib
$ gcov fib.c
File &#39;fib.c&#39;
Lines executed:100.00% of 12
fib.c:creating &#39;fib.c.gcov&#39;

12行代碼100%被執行到,再查看分支情況,

$ gcov -b fib.c
File &#39;fib.c&#39;
Lines executed:100.00% of 12
Branches executed:100.00% of 6
Taken at least once:100.00% of 6
Calls executed:100.00% of 4
fib.c:creating &#39;fib.c.gcov&#39;

發現所有函數,條件分支和語句都被執行到,說明代碼的覆蓋率很高,不過資料3gprof的演示顯示代碼的覆蓋率高並不一定說明代碼的性能就好,因爲那些被覆蓋到的代碼可能能夠被優化成性能更高的代碼。那到底哪些代碼值得被優化呢?執行次數最多的,另外,有些分支雖然都覆蓋到了,但是這個分支的位置可能並不是理想的,如果一個分支的內容被執行的次數很多,那麼把它作爲最後一個分支的話就會浪費很多不必要的比較時間。因此,通過覆蓋率測試,可以嘗試着剔除那些從未執行過的代碼或者把那些執行次數較多的分支移動到較早的條件分支裏頭。通過性能測試,可以找出那些值得優化的函數、分支或者是語句。

如果使用-fprofile-arcs -ftest-coverage參數編譯完代碼,可以接着用-fbranch-probabilities參數對代碼進行編譯,這樣,編譯器就可以對根據代碼的分支測試情況進行優化。

$ wc -c fib
16333 fib
$ ls fib.gcda  #確保fib.gcda已經生成,這個是運行fib後的結果
fib.gcda
$ gcc -fbranch-probabilities -o fib fib.c #再次運行
$ wc -c fib
6604 fib
$ time ./fib
...
real    0m21.686s
user    0m18.477s
sys     0m0.008s

可見代碼量減少了,而且執行效率會有所提高,當然,這個代碼效率的提高可能還跟其他因素有關,比如gcc還優化了一些跟平臺相關的指令。

如果想看看代碼中各行被執行的情況,可以直接看fib.c.gcov文件。這個文件的各列依次表示執行次數、行號和該行的源代碼。次數有三種情況,如果一直沒有執行,那麼用####表示;如果該行是註釋、函數聲明等,用-表示;如果是純粹的代碼行,那麼用執行次數表示。這樣我們就可以直接分析每一行的執行情況。

gcov也有一個圖形化接口ggcov,是基於gtk+的,適合Gnome桌面的用戶。

現在都已經關注到代碼行了,實際上優化代碼的前提是保證代碼的正確性,如果代碼還有很多bug,那麼先要debug。不過下面的這些"bug"用普通的工具確實不太方便,雖然可能,不過這裏還是把它們歸結爲測試的內容,並且這裏剛好承接上gcov部分,gcov能夠測試到每一行的代碼覆蓋情況,而無論是內存訪問越界、緩衝區溢出還是內存泄露,實際上是發生在具體的代碼行上的。

內存訪問越界 catchsegv, libSegFault.so

"segmentation fault"是很頭痛的一個問題,估計“糾纏”過很多人。這裏僅僅演示通過catchsegv腳本測試段錯誤的方法,其他方法見後面相關資料。

catchsegv利用系統動態鏈接的PRELOAD機制(請參考man ld-linux),把庫/lib/libSegFault.so提前load到內存中,然後通過它檢查程序運行過程中的段錯誤。

$ cat test.c
#include <stdio.h>

int main(void)
{
    char str[10];

        sprintf(str, "%s", 111);

        printf("str = %sn", str);
        return 0;
}
$ make test
$ LD_PRELOAD=/lib/libSegFault.so ./test  #等同於catchsegv ./test
*** Segmentation fault
Register dump:

 EAX: 0000006f   EBX: b7eecff4   ECX: 00000003   EDX: 0000006f
 ESI: 0000006f   EDI: 0804851c   EBP: bff9a8a4   ESP: bff9a27c

 EIP: b7e1755b   EFLAGS: 00010206

 CS: 0073   DS: 007b   ES: 007b   FS: 0000   GS: 0033   SS: 007b

 Trap: 0000000e   Error: 00000004   OldMask: 00000000
 ESP/signal: bff9a27c   CR2: 0000006f

Backtrace:
/lib/libSegFault.so[0xb7f0604f]
[0xffffe420]
/lib/tls/i686/cmov/libc.so.6(vsprintf+0x8c)[0xb7e0233c]
/lib/tls/i686/cmov/libc.so.6(sprintf+0x2e)[0xb7ded9be]
./test[0x804842b]
/lib/tls/i686/cmov/libc.so.6(__libc_start_main+0xe0)[0xb7dbd050]
./test[0x8048391]
...

從結果中可以看出,代碼的sprintf有問題。經過檢查發現它把整數當字符串輸出,對於字符串的輸出,需要字符串的地址作爲參數,而這裏的111則剛好被解釋成了字符串的地址,因此sprintf試圖訪問111這個地址,從而發生了非法訪問內存的情況,出現"segmentation fault"。

緩衝區溢出 libsafe.so

緩衝區溢出是指棧溢出(stack smashing),通常發生在對函數內的局部變量進行賦值操作時,超出了該變量的字節長度而引起對棧內原有數據(比如eip,ebp等)的覆蓋,從而引發內存訪問越界,甚至執行非法代碼,導致系統崩潰。關於緩衝區的詳細原理和實例分析見《C語言緩衝區溢出與注入分析》。這裏僅僅演示該資料中提到的一種用於檢查緩衝區溢出的方法,它同樣採用動態鏈接的PRELOAD機制提前裝載一個名叫libsafe.so的庫,可以從這裏獲取它,下載後,再解壓,編譯,得到libsafe.so

下面,演示一個非常簡單的,但可能存在緩衝區溢出的代碼,並演示libsafe.so的用法。

$ cat test.c
$ make test
$ LD_PRELOAD=/path/to/libsafe.so ./test ABCDEFGHIJKLMN
ABCDEFGHIJKLMN
*** stack smashing detected ***: ./test terminated
Aborted (core dumped)

資料7分析到,如果不能夠對緩衝區溢出進行有效的處理,可能會存在很多潛在的危險。雖然libsafe.so採用函數替換的方法能夠進行對這類stack smashing進行一定的保護,但是無法根本解決問題,alert7大蝦在資料10中提出了突破它的辦法,資料1111]提出了另外一種保護機制。

內存泄露 Memwatch, Valgrind, mtrace

堆棧通常會被弄在一起叫,不過這兩個名詞卻是指進程的內存映像中的兩個不同的部分,棧(stack)用於函數的參數傳遞、局部變量的存儲等,是系統自動分配和回收的;而堆(heap)則是用戶通過malloc等方式申請而且需要用戶自己通過free釋放的,如果申請的內存沒有釋放,那麼將導致內存泄露,進而可能導致堆的空間被用盡;而如果已經釋放的內存再次被釋放(double-free)則也會出現非法操作。(如果要真正理解堆和棧的區別,需要理解進程的內存映像,請參考《C語言緩衝區溢出與注入分析》)

這裏演示通過Memwatch來檢測程序中可能存在內存泄露,可以從這裏下載到這個工具。 使用這個工具的方式很簡單,只要把它鏈接(ld)到可執行文件中去,並在編譯時加上兩個宏開關-DMEMWATCH -DMW_STDIO。這裏演示一個簡單的例子。

$ cat test.c
#include <stdlib.h>
#include <stdio.h>
#include "memwatch.h"

int main(void)
{
    char *ptr1;
    char *ptr2;

    ptr1 = malloc(512);
    ptr2 = malloc(512);

    ptr2 = ptr1;
    free(ptr2);
    free(ptr1);
}
$ gcc -DMEMWATCH -DMW_STDIO test.c memwatch.c -o test
$ cat memwatch.log
============= MEMWATCH 2.71 Copyright (C) 1992-1999 Johan Lindh =============

Started at Sat Mar  1 07:34:33 2008

Modes: __STDC__ 32-bit mwDWORD==(unsigned long)
mwROUNDALLOC==4 sizeof(mwData)==32 mwDataSize==32

double-free: <4> test.c(15), 0x80517e4 was freed from test.c(14)

Stopped at Sat Mar  1 07:34:33 2008

unfreed: <2> test.c(11), 512 bytes at 0x8051a14         {FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE FE ................}

Memory usage statistics (global):
 N)umber of allocations made: 2
 L)argest memory usage      : 1024
 T)otal of all alloc() calls: 1024
 U)nfreed bytes totals      : 512

通過測試,可以看到有一個512字節的空間沒有被釋放,而另外512字節空間卻被連續釋放兩次(double-free)。valgrind和mtrace也可以做類似的工作,請參考資料45和mtrace的手冊。

這裏有用mtrace檢查緩衝區溢出的一個例子,請參考。

代碼調試

調試的方法很多,調試往往要跟蹤代碼的運行狀態,printf是最基本的辦法,然後呢?靜態調試方法有哪些,非交互的呢?非實時的有哪些?實時的呢?用於調試內核的方法有哪些?有哪些可以用來調試彙編代碼呢?

靜態調試:printf + gcc -D(打印程序中的變量)

利用gcc的宏定義開關(-D)和printf函數可以跟蹤程序中某個位置的狀態,這個狀態包括當前一些變量和寄存器的值。調試時需要用-D開關進行編譯,在正式發佈程序時則可把-D開關去掉。這樣做比單純用printf方便很多,它可以避免清理調試代碼以及由此帶來的代碼誤刪除等問題。

$ cat test.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    int i = 0;

#ifdef DEBUG
        printf("i = %dn", i);

        int t;
        __asm__ __volatile__ ("movl %%ebp, %0;":"=r"(t)::"%ebp");
        printf("ebp = 0x%xn", t);
#endif

        _exit(0);
}
$ gcc -DDEBUG -g -o test test.c
$ ./test
i = 0
ebp = 0xbfb56d98

上面演示瞭如何跟蹤普通變量和寄存器變量的辦法。跟蹤寄存器變量採用了內聯彙編。

不過,這種方式不夠靈活,我們無法“即時”獲取程序的執行狀態,而gdb等交互式調試工具不僅解決了這樣的問題,而且通過把調試器拆分成調試服務器和調試客戶端適應了嵌入式系統的調試,另外,通過預先設置斷點以及斷點處需要收集的程序狀態信息解決了交互式調試不適應實時調試的問題。

交互式的調試(動態調試)

包括gdb(支持本地和遠程)/ald(彙編指令級別的調試)

嵌入式系統調試方法 gdbserver/gdb

估計大家已經非常熟悉GDB(Gnu DeBugger)了,所以這裏並不介紹常規的gdb用法,而是介紹它的服務器/客戶(gdbserver/gdb)調試方式。這種方式非常適合嵌入式系統的調試,爲什麼呢?先來看看這個:

$ wc -c /usr/bin/gdbserver
56000 /usr/bin/gdbserver
$ which gdb
/usr/bin/gdb
$ wc -c /usr/bin/gdb
2557324 /usr/bin/gdb
$ echo "(2557324-56000)/2557324"  | bc -l
.97810210986171482377

gdb比gdbserver大了將近97%,如果把整個gdb搬到存儲空間受限的嵌入式系統中是很不合適的,不過僅僅5K左右的gdbserver即使在只有8M Flash卡的嵌入式系統中也都足夠了。所以在嵌入式開發中,我們通常先在本地主機上交叉編譯好gdbserver/gdb。

如果是初次使用這種方法,可能會遇到麻煩,而麻煩通常發生在交叉編譯gdb和gdbserver時。在編譯gdbserver/gdb前,需要配置(./configure)兩個重要的選項:

  • –host,指定gdb/gdbserver本身的運行平臺,
  • –target,指定gdb/gdbserver調試的代碼所運行的平臺,

關於運行平臺,通過$MACHTYPE環境變量就可獲得,對於gdbserver,因爲要把它複製到嵌入式目標系統上,並且用它來調試目標平臺上的代碼,因此需要把–host和–target都設置成目標平臺;而gdb因爲還是運行在本地主機上,但是需要用它調試目標系統上的代碼,所以需要把–target設置成目標平臺。

編譯完以後就是調試,調試時需要把程序交叉編譯好,並把二進制文件複製一份到目標系統上,並在本地需要保留一份源代碼文件。調試過程大體如下,首先在目標系統上啓動調試服務器:

$ gdbserver :port /path/to/binary_file
...

然後在本地主機上啓動gdb客戶端鏈接到gdb調試服務器,(gdbserver_ipaddress是目標系統的IP地址,如果目標系統不支持網絡,那麼可以採用串口的方式,具體看手冊)

$ gdb
...
(gdb) target remote gdbserver_ipaddress:2345
...

其他調試過程和普通的gdb調試過程類似。

彙編代碼的調試 ald

用gdb調試彙編代碼貌似會比較麻煩,不過有人正是因爲這個原因而開發了一個專門的彙編代碼調試器,名字就叫做assembly language debugger,簡稱ald,你可以從這裏下載到。

下載後,解壓編譯,我們來調試一個程序看看。

這裏是一段非常簡短的彙編代碼:

.global _start
_start:
        popl %ecx
        popl %ecx
        popl %ecx
        movb $10,12(%ecx)
        xorl %edx, %edx
        movb $13, %dl
        xorl %eax, %eax
        movb $4, %al
        xorl %ebx, %ebx
        int $0x80
        xorl %eax, %eax
        incl %eax       
        int $0x80

彙編、鏈接、運行:

$ as -o test.o test.s
$ ld -o test test.o
$ ./test "Hello World"
Hello World

查看程序的入口地址:

$ readelf -h test | grep Entry
  Entry point address:               0x8048054

接着用ald調試:

$ ald test
ald> display
Address 0x8048054 added to step display list
ald> n
eax = 0x00000000 ebx = 0x00000000 ecx = 0x00000001 edx = 0x00000000
esp = 0xBFBFDEB4 ebp = 0x00000000 esi = 0x00000000 edi = 0x00000000
ds  = 0x007B es  = 0x007B fs  = 0x0000 gs  = 0x0000
ss  = 0x007B cs  = 0x0073 eip = 0x08048055 eflags = 0x00200292 

Flags: AF SF IF ID 

Dumping 64 bytes of memory starting at 0x08048054 in hex
08048054:  59 59 59 C6 41 0C 0A 31 D2 B2 0D 31 C0 B0 04 31    YYY.A..1...1...1
08048064:  DB CD 80 31 C0 40 CD 80 00 2E 73 79 6D 74 61 62    [email protected]
08048074:  00 2E 73 74 72 74 61 62 00 2E 73 68 73 74 72 74    ..strtab..shstrt
08048084:  61 62 00 2E 74 65 78 74 00 00 00 00 00 00 00 00    ab..text........

08048055                      59                   pop ecx

可見ald在啓動時就已經運行了被它調試的test程序,並且進入了程序的入口0x8048054,緊接着單步執行時,就執行了程序的第一條指令popl ecx。

ald的命令很少,而且跟gdb很類似,比如這個幾個命令用法和名字都類似 help,next,continue,set args,break,file,quit,disassemble,enable,disable等。名字不太一樣但功能對等的有:examine對x, enter 對 set variable {int}地址=數據。

需要提到的是:Linux下的調試器包括上面的gdb和ald,以及strace等都用到了Linux系統提供的ptrace()系統調用,這個調用爲用戶訪問內存映像提供了便利,如果想自己寫一個調試器或者想hack一下gdb和ald,那麼好好閱讀資料12man ptrace吧。

如果確實需要用gdb調試彙編,可以參考:

實時調試:gdb tracepoint

對於程序狀態受時間影響的程序,用上述普通的設置斷點的交互式調試方法並不合適,因爲這種方式將由於交互時產生的通信延遲和用戶輸入命令的時延而完全改變程序的行爲。所以gdb提出了一種方法以便預先設置斷點以及在斷點處需要獲取的程序狀態,從而讓調試器自動執行斷點處的動作,獲取程序的狀態,從而避免在斷點處出現人機交互產生時延改變程序的行爲。

這種方法叫tracepoints(對應breakpoint),它在gdb的user manual裏頭有詳細的說明,不過在gdb的官方發行版中至今都沒有對它的實現。儘管如此,我們還是可以使用它,因爲有其他組織做了相關的工作,並以補丁的方式發佈它。這個補丁可以從這裏獲取。

獲取這個補丁以後,要做的就是把它patch到對應的gdb版本中,然後就是編譯。因爲tracepoints只定義在調試服務器和調試客戶端這種方式中,因此在這個實現中也是這樣,如果想用它,同樣需要編譯gdbserver和gdb,並類似嵌入式系統中的調試方法一樣調試它。

編譯好以後通過參考資料就可以使用它。

調試內核

雖然這裏並不會演示如何去hack內核,但是相關的工具還是需要簡單提到的,這個資料列出了絕大部分用於內核調試的工具,這些對你hack內核應該會有幫助的。

代碼優化

這部分暫時沒有準備足夠的素材,有待進一步完善。

暫且先提到兩個比較重要的工具,一個是oprofile,另外一個是perf。

實際上呢?“代碼測試”部分介紹的很多工具是爲代碼優化服務的,更多具體的細節請參考後續資料,自己做實驗吧。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章