linux驅動程序設計21 Linux設備驅動的調試

本章導讀
“工欲善其事,必先利其器”,爲了方便進行Linux設備驅動的開發和調試,建立良好的開發環境很重
要,還要使用必要的工具軟件以及掌握常用的調試技巧等。
21.1節講解了Linux下調試器GDB的基本用法和技巧。
21.2節講解了Linux內核的調試方法。
21.3~21.10節對21.3節的概述展開了講解,內容有:Linux內核調試用的printk()、BUG_ON()、
WARN_ON()、/proc、Oops、strace、KGDB,以及使用仿真器進行調試的方法。
21.11節講解了Linux應用程序的調試方法,驅動工程師往往需要編寫用戶空間的應用程序以對自身編
寫的驅動進行驗證和測試,因此,掌握應用程序調試方法對驅動工程師而言也是必需的。
21.12節講解了Linux常用的一些穩定性、性能分析和調優工具。
21.1 GDB調試器的用法
21.1.1 GDB的基本用法
GDB是GNU開源組織發佈的一個強大的UNIX下的程序調試工具,GDB主要可幫助工程師完成下面4
個方面的功能。
·啓動程序,可以按照工程師自定義的要求運行程序。
·讓被調試的程序在工程師指定的斷點處停住,斷點可以是條件表達式。
·當程序被停住時,可以檢查此時程序中所發生的事,並追蹤上文。
·動態地改變程序的執行環境。
不管是調試Linux內核空間的驅動還是調試用戶空間的應用程序,都必須掌握GDB的用法。而且,在
調試內核和調試應用程序時使用的GDB命令是完全相同的,下面以代碼清單21.1的應用程序爲例演示
GDB調試器的用法。
代碼清單21.1 GDB調試器用法的演示程序
1int add(int a, int b)
2{
3 return a + b;
4}
5
6main()
7{
8 int sum[10] =
9 {
10 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
11 };
12 int i;
13
14 int array1[10] =
15 {
16 48, 56, 77, 33, 33, 11, 226, 544, 78, 90
17 };
18 int array2[10] =
19 {
20 85, 99, 66, 0x199, 393, 11, 1, 2, 3, 4
21 };
22
23 for (i = 0; i < 10; i++)
24 {
25 sum[i] = add(array1[i], array2[i]);
26 }
27}
使用命令gcc–g gdb_example.c–o gdb_example編譯上述程序,得到包含調試信息的二進制文件
example,執行gdb gdb_example命令進入調試狀態,如下所示:
$ gdb gdb_example
GNU gdb (Ubuntu 7.7-0ubuntu3.1) 7.7
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
(gdb)
1.list命令
在GDB中運行list命令(縮寫l)可以列出代碼,list的具體形式如下。
·list<linenum>,顯示程序第linenum行周圍的源程序,如下所示:
(gdb) list 15
10
11 int array1[10] =
12 {
13 48, 56, 77, 33, 33, 11, 226, 544, 78, 90
14 };
15 int array2[10] =
16 {
17 85, 99, 66, 0x199, 393, 11, 1, 2, 3, 4
18 };
19
·list<function>,顯示函數名爲function的函數的源程序,如下所示:
(gdb) list main
2 {
3 return a + b;
4 }
56
main()
7 {
8 int sum[10];
9 int i;
10
11 int array1[10] =
·list,顯示當前行後面的源程序。
·list-,顯示當前行前面的源程序。
下面演示了使用GDB中的run(縮寫爲r)、break(縮寫爲b)、next(縮寫爲n)命令控制程序的運
行,並使用print(縮寫爲p)命令打印程序中的變量sum的過程:
(gdb) break add
Breakpoint 1 at 0x80482f7: file gdb_example.c, line 3.
(gdb) run
Starting program: /driver_study/gdb_example
Breakpoint 1, add (a=48, b=85) at gdb_example.c:3
warning: Source file is more recent than executable.
3 return a + b;
(gdb) next
4 }
(gdb) next
main () at gdb_example.c:23
23 for (i = 0; i < 10; i++)
(gdb) next
25 sum[i] = add(array1[i], array2[i]);
(gdb) print sum
$1 = {133, 0, 0, 0, 0, 0, 0, 0, 0, 0}
2.run命令
在GDB中,運行程序使用run命令。在程序運行前,我們可以設置如下4方面的工作環境。
(1)程序運行參數
用set args可指定運行時參數,如set args 10 20 30 40 50;用show args命令可以查看設置好的運行參
數。
(2)運行環境
用path<dir>可設定程序的運行路徑;用how paths可查看程序的運行路徑;用set environment
varname[=value]可設置環境變量,如set env USER=baohua;用show environment[varname]則可查看環境變
量。
(3)工作目錄
cd<dir>相當於shell的cd命令,pwd可顯示當前所在的目錄。
(4)程序的輸入輸出
info terminal用於顯示程序用到的終端的模式;在GDB中也可以使用重定向控制程序輸出,如
run>outfile;用tty命令可以指定輸入輸出的終端設備,如tty/dev/ttyS1。
3.break命令
在GDB中用break命令來設置斷點,設置斷點的方法如下。
(1)break<function>
在進入指定函數時停住,在C++中可以使用class::function或function(type,type)格式來指定函數
名。
(2)break<linenum>
在指定行號停住。
(3)break+offset/break-offset。
在當前行號的前面或後面的offset行停住,offiset爲自然數。
(4)break filename:linenum
在源文件filename的linenum行處停住。
(5)break filename:function
在源文件filename的function函數的入口處停住。
(6)break*address
在程序運行的內存地址處停住。
(7)break
break命令沒有參數時,表示在下一條指令處停住。
(8)break…if<condition>
…可以是上述的break<linenum>、break+offset/break–offset中的參數,condition表示條件,在條件成立
時停住。比如在循環體中,可以設置break if i=100,表示當i爲100時停住程序。
查看斷點時,可使用info命令,如info breakpoints[n]、info break[n](n表示斷點號)。
4.單步命令
在調試過程中,next命令用於單步執行,類似於VC++中的step over。next的單步不會進入函數的內
部,與next對應的step(縮寫爲s)命令則在單步執行一個函數時,進入其內部,類似於VC++中的step
into。下面演示了step命令的執行情況,在第23行的add()函數調用處執行step會進入其內部的return
a+b;語句:
(gdb) break 25
Breakpoint 1 at 0x8048362: file gdb_example.c, line 25.
(gdb) run
Starting program: /driver_study/gdb_example
Breakpoint 1, main () at gdb_example.c:25
25 sum[i] = add(array1[i], array2[i]);
(gdb) step
add (a=48, b=85) at gdb_example.c:3
3 return a + b;
單步執行的更復雜用法如下。
(1)step<count>
單步跟蹤,如果有函數調用,則進入該函數(進入函數的前提是,此函數被編譯有debug信息)。step
後面不加count表示一條條地執行,加count表示執行後面的count條指令,然後再停住。
(2)next<count>
單步跟蹤,如果有函數調用,它不會進入該函數。同理,next後面不加count表示一條條地執行,加
count表示執行後面的count條指令,然後再停住。
(3)set step-mode
set step-mode on用於打開step-mode模式,這樣,在進行單步跟蹤(運行step指令)時,若跨越某沒有
調試信息的函數,程序的執行則會在該函數的第一條指令處停住,而不會跳過整個函數。這樣我們可以查
看該函數的機器指令。
(4)finish
運行程序,直到當前函數完成返回,並打印函數返回時的堆棧地址、返回值及參數值等信息。
(5)until(縮寫爲u)
一直在循環體內執行單步而退不出來是一件令人煩惱的事情,用until命令可以運行程序直到退出循環
體。
(6)stepi(縮寫爲si)和nexti(縮寫爲ni)
stepi和nexti用於單步跟蹤一條機器指令。比如,一條C程序代碼有可能由數條機器指令完成,stepi和
nexti可以單步執行機器指令,相反,step和next是C語言級別的命令。
另外,運行display/i$pc命令後,單步跟蹤會在打出程序代碼的同時打出機器指令,即彙編代碼。
5.continue命令
當程序被停住後,可以使用continue命令(縮寫爲c,fg命令同continue命令)恢復程序的運行直到程序
結束,或到達下一個斷點,命令格式爲:
continue [ignore-count]
c [ignore-count]
fg [ignore-count]
ignore-count表示忽略其後多少次斷點。
假設我們設置了函數斷點add(),並觀察i,則在continue過程中,每次遇到add()函數或i發生變
化,程序就會停住,如下所示:
(gdb) continue
Continuing.
Hardware watchpoint 3: i
Old value = 2
New value = 3
0x0804838d in main () at gdb_example.c:23
23 for (i = 0; i < 10; i++)
(gdb) continue
Continuing.
Breakpoint 1, main () at gdb_example.c:25
25 sum[i] = add(array1[i], array2[i]);
(gdb) continue
Continuing.
Hardware watchpoint 3: i
Old value = 3
New value = 4
0x0804838d in main () at gdb_example.c:23
23 for (i = 0; i < 10; i++)
6.print命令
在調試程序時,當程序被停住時,可以使用print命令(縮寫爲p),或是同義命令inspect來查看當前
程序的運行數據。print命令的格式如下:
print <expr>
print /<f> <expr>
<expr>是表達式,也是被調試的程序中的表達式,<f>是輸出的格式,比如,如果要把表達式按十六
進制的格式輸出,那麼就是/x。在表達式中,有幾種GDB所支持的操作符,它們可以用在任何一種語言
中,@是一個和數組有關的操作符,::指定一個在文件或是函數中的變量,{<type>}<addr>表示一個指
向內存地址<addr>的類型爲type的對象。
下面演示了查看sum[]數組的值的過程:
(gdb) print sum
$2 = {133, 155, 0, 0, 0, 0, 0, 0, 0, 0}
(gdb) next
Breakpoint 1, main () at gdb_example.c:25
25 sum[i] = add(array1[i], array2[i]);
(gdb) next
23 for (i = 0; i < 10; i++)
(gdb) print sum
$3 = {133, 155, 143, 0, 0, 0, 0, 0, 0, 0}
當需要查看一段連續內存空間的值時,可以使用GDB的@操作符,@的左邊是第一個內存地址,@的
右邊則是想查看內存的長度。例如如下動態申請的內存:
int *array = (int *) malloc (len * sizeof (int));
在GDB調試過程中這樣顯示這個動態數組的值:
p *array@len
print的輸出格式如下。
·x:按十六進制格式顯示變量。
·d:按十進制格式顯示變量。
·u:按十六進制格式顯示無符號整型。
·o:按八進制格式顯示變量。
·t:按二進制格式顯示變量。
·a:按十六進制格式顯示變量。
·c:按字符格式顯示變量。
·f:按浮點數格式顯示變量。
我們可用display命令設置一些自動顯示的變量,當程序停住時,或是單步跟蹤時,這些變量會自動顯
示。
如果要修改變量,如x的值,可使用如下命令:
print x=4
當用GDB的print查看程序運行時數據時,每一個print都會被GDB記錄下來。GDB會以$1,$2,$3…
這樣的方式爲每一個print命令編號。我們可以使用這個編號訪問以前的表達式,如$1。
7.watch命令
watch一般用來觀察某個表達式(變量也是一種表達式)的值是否有了變化,如果有變化,馬上停止
程序運行。我們有如下幾種方法來設置觀察點。
watch<expr>:爲表達式(變量)expr設置一個觀察點。一旦表達式值有變化時,馬上停止程序運行。
rwatch<expr>:當表達式(變量)expr被讀時,停止程序運行。
awatch<expr>:當表達式(變量)的值被讀或被寫時,停止程序運行。
info watchpoints:列出當前所設置的所有觀察點。
下面演示了觀察i並在連續運行next時一旦發現i變化,i值就會顯示出來的過程:
(gdb) watch i
Hardware watchpoint 3: i
(gdb) next
23 for (i = 0; i < 10; i++)
(gdb) next
Hardware watchpoint 3: i
Old value = 0
New value = 1
0x0804838d in main () at gdb_example.c:23
23 for (i = 0; i < 10; i++)
(gdb) next
Breakpoint 1, main () at gdb_example.c:25
25 sum[i] = add(array1[i], array2[i]);
(gdb) next
23 for (i = 0; i < 10; i++)
(gdb) next
Hardware watchpoint 3: i
Old value = 1
New value = 2
0x0804838d in main () at gdb_example.c:23
23 for (i = 0; i < 10; i++)
8.examine命令
我們可以使用examine命令(縮寫爲x)來查看內存地址中的值。examine命令的語法如下所示:
x/<n/f/u> <addr>
<addr>表示一個內存地址。“x/”後的n、f、u都是可選的參數,n是一個正整數,表示顯示內存的長
度,也就是說從當前地址向後顯示幾個地址的內容;f表示顯示的格式,如果地址所指的是字符串,那麼
格式可以是s,如果地址是指令地址,那麼格式可以是i;u表示從當前地址往後請求的字節數,如果不指
定的話,GDB默認的是4字節。u參數可以被一些字符代替:b表示單字節,h表示雙字節,w表示四字節,
g表示八字節。當我們指定了字節長度後,GDB會從指定的內存地址開始,讀寫指定字節,並把其當作一
個值取出來。n、f、u這3個參數可以一起使用,例如命令x/3uh 0x54320表示從內存地址0x54320開始以雙
字節爲1個單位(h)、16進制方式(u)顯示3個單位(3)的內存。
9.set命令
examine命令用於查看內存,而set命令用於修改內存。它的命令格式是“set*有類型的指針=value”。
比如,下列程序,在用gdb運行起來後,通過Ctrl+C停住。
main()
{
void *p = malloc(16);
while(1);
}
我們可以在運行中用如下命令來修改p指向的內存。
(gdb) set *(unsigned char *)p='h'
(gdb) set *(unsigned char *)(p+1)='e'
(gdb) set *(unsigned char *)(p+2)='l'
(gdb) set *(unsigned char *)(p+3)='l'
(gdb) set *(unsigned char *)(p+4)='o'
看看結果:
(gdb) x/s p
0x804b008: "hello"
也可以直接使用地址常數:
(gdb) p p
$2 = (void *) 0x804b008
(gdb) set *(unsigned char *)0x804b008='w'
(gdb) set *(unsigned char *)0x804b009='o'
(gdb) set *(unsigned char *)0x804b00a='r'
(gdb) set *(unsigned char *)0x804b00b='l'
(gdb) set *(unsigned char *)0x804b00c='d'
(gdb) x/s 0x804b008
0x804b008: "world"
10.jump命令
一般來說,被調試程序會按照程序代碼的運行順序依次執行,但是GDB也提供了亂序執行的功能,
也就是說,GDB可以修改程序的執行順序,從而讓程序隨意跳躍。這個功能可以由GDB的jump命令
jump<linespec>來指定下一條語句的運行點。<linespec>可以是文件的行號,可以是file:line格式,也可以
是+num這種偏移量格式,表示下一條運行語句從哪裏開始。
jump <address>
這裏的<address>是代碼行的內存地址。
注意:jump命令不會改變當前程序棧中的內容,如果使用jump從一個函數跳轉到另一個函數,當跳
轉到的函數運行完返回,進行出棧操作時必然會發生錯誤,這可能會導致意想不到的結果,因此最好只用
jump在同一個函數中進行跳轉。
11.signal命令
使用singal命令,可以產生一個信號量給被調試的程序,如中斷信號Ctrl+C。於是,可以在程序運行
的任意位置處設置斷點,並在該斷點處用GDB產生一個信號量,這種精確地在某處產生信號的方法非常
有利於程序的調試。
signal命令的語法是signal<signal>,UNIX的系統信號量通常爲1~15,因此<signal>的取值也在這個範
圍內。
12.return命令
如果在函數中設置了調試斷點,在斷點後還有語句沒有執行完,這時候我們可以使用return命令強制
函數忽略還沒有執行的語句並返回。
return
return <expression>
上述return命令用於取消當前函數的執行,並立即返回,如果指定了<expression>,那麼該表達式的值
會被作爲函數的返回值。
13.call命令
call命令用於強制調用某函數:
call <expr>
表達式可以是函數,以此達到強制調用函數的目的,它會顯示函數的返回值(如果函數返回值不是
void)。比如在下列程序執行while(1)的時候:
main()
{
void *p = malloc(16);
while(1);
}
我們強制要求其執行strcpy()和printf():
(gdb) call strcpy(p, "hello world")
$3 = 134524936
(gdb) call printf("%s\n", p)
hello world
$4 = 12
14.info命令
info命令可以用來在調試時查看寄存器、斷點、觀察點和信號等信息。要查看寄存器的值,可以使用
如下命令:
info registers (查看除了浮點寄存器以外的寄存器)
info all-registers (查看所有寄存器,包括浮點寄存器)
info registers <regname ...> (查看所指定的寄存器)
要查看斷點信息,可以使用如下命令:
info break要列出當前所設置的所有觀察點,可使用如下命令:
info watchpoints
要查看有哪些信號正在被GDB檢測,可使用如下命令:
info signals
info handle
也可以使用info line命令來查看源代碼在內存中的地址。info line後面可以跟行號、函數名、文件名:行
號、文件名:函數名等多種形式,例如用下面的命令會打印出所指定的源碼在運行時的內存地址:
info line tst.c:func
15.disassemble
disassemble命令用於反彙編,可用它來查看當前執行時的源代碼的機器碼,實際上只是把目前內存中
的指令沖刷出來。下面的示例用於查看函數func的彙編代碼:
(gdb) disassemble func
Dump of assembler code for function func:
0x8048450 <func>: push %ebp
0x8048451 <func+1>: mov %esp,%ebp
0x8048453 <func+3>: sub $0x18,%esp
0x8048456 <func+6>: movl $0x0,0xfffffffc(%ebp)
...
End of assembler dump.
21.1.2 DDD圖形界面調試工具
GDB本身是一種命令行調試工具,但是通過DDD(Data Display Debugger,見
http://www.gnu.org/software/ddd/)可以被圖形界面化。DDD可以作爲GDB、DBX、WDB、Ladebug、
JDB、XDB、Perl Debugger或Python Debugger的可視化圖形前端,其特有的圖形數據顯示功能(Graphical
Data Display)可以把數據結構按照圖形的方式顯示出來。
DDD最初源於1990年Andreas Zeller編寫的VSL結構化語言,後來經過一些程序員的努力,演化成今天
的模樣。DDD的功能非常強大,可以調試用C/C++、Ada、Fortran、Pascal、Modula-2和Modula-3編寫的程
序;能以超文本方式瀏覽源代碼;能夠進行斷點設置、回溯調試和歷史記錄;具有程序在終端運行的仿真
窗口,具備在遠程主機上進行調試的能力;能夠顯示各種數據結構之間的關係,並將數據結構以圖形形式
顯示;具有GDB/DBX/XDB的命令行界面,包括完整的文本編輯、歷史紀錄、搜尋引擎等。
DDD的主界面如圖21.1所示,它和Visual Studio等集成開發環境非常相近,而且DDD包含了Visual
Studio所不包含的部分功能。
圖21.1 DDD的主界面
在設計DDD的時候,設計人員決定把它與GDB之間的耦合度儘量降低。因爲像GDB這樣的開源軟
件,更新的速度比商業軟件快,所以爲了使GDB的變化不會影響到DDD,在DDD中,GDB是作爲獨立的
進程運行的,通過命令行接口與DDD進行交互。
圖21.2顯示了用戶、DDD、GDB和被調試進程之間的關係,DDD和GDB之間的所有通信都是異步進
行的。在DDD中發出的GDB命令都會與一個回調函數相連,放入命令隊列中。這個回調函數在合適的時
間會處理GDB的輸出。例如,如果用戶手動輸入一條GDB的命令,DDD就會把這條命令與顯示GDB輸出
的一個回調函數連起來。一旦GDB命令完成,就會觸發回調函數,GDB的輸出就會顯示在DDD的命令窗
口中。
圖21.2 DDD運行機理
DDD在事件循環時等待用戶輸入和GDB輸出,同時等着GDB進入等待輸入狀態。當GDB可用時,下
一條命令就會從命令隊列中取出,送給GDB。GDB到達的輸出由上次命令的回調函數過程來處理。這種
異步機制避免了DDD在等待GDB輸出時發生阻塞現象,到達的事件可以在任何時間得到處理。
不可否認的是,DDD和GDB的分離使得DDD的運行速度相對來說比較慢,但是這種方法帶來了靈活
性和兼容性的好處。例如,用戶可以把GDB調試器換成其他調試器,如DBX等。另外,GDB和DDD的分
離使得用戶可以在不同的機器上分別運行GDB和DDD。
在DDD中,可以直接在底部的控制檯中輸入GDB命令,也可以通過菜單和鼠標以圖形方式觸發GDB
命令的運行,使用方法甚爲簡單,因此這裏不再贅述。
DDD不僅可用於調試PC上的應用程序,也可調試目標板子,方法是用如下命令啓動DDD(通過-
debugger選項指定一個針對ARM的GDB):
ddd --debugger arm-linux-gnueabihf-gdb <要調試的程序>
除了DDD以外,在Linux環境下,也可以使用廣受歡迎的Eclipse來編寫代碼並進行調試。安裝Eclipse
IDE for C/C++Developer後,在Eclipse中,可以設置Using GDB(DSF)Manual Remote Debugging Launcher
以及ARM的GDB等,如圖21.3所示。
圖21.3 在Eclipse中設置Remote調試模式和GDB
21.2 Linux內核調試
在嵌入式系統中,由於目標機資源有限,因此往往在主機上先編譯好程序,再在目標機上運行。用戶
所有的開發工作都在主機開發環境下完成,包括編碼、編譯、連接、下載和調試等。目標機和主機通過串
口、以太網、仿真器或其他通信手段通信,主機用這些接口控制目標機,調試目標機上的程序。
調試嵌入式Linux內核的方法如下。
1)目標機“插樁”,如打上KGDB補丁,這樣主機上的GDB可與目標機的KGDB通過串口或網口通
信。
2)使用仿真器,仿真器可直接連接目標機的JTAG/BDM,這樣主機的GDB就可以通過與仿真器的通
信來控制目標機。
3)在目標板上通過printk()、Oops、strace等軟件方法進行“觀察”調試,這些方法不具備查看和修
改數據結構、斷點、單步等功能。
21.4~21.7節將對這些調試方法進行一一講解。
不管是目標機“插樁”還是使用仿真器連接目標機JTAG/SWD/BDM,在主機上,調試工具一般都採用
GDB。
GDB可以直接把Linux內核當成一個整體來調試,這個過程實際上可以被QEMU模擬出來。進入本書
配套Ubuntu的/home/baohua/develop/linux/extra目錄下,修改run-nolcd.sh的腳本,將其從
qemu-system-arm -nographic -sd vexpress.img -M vexpress-a9 -m 512M -kernel
zImage -dtb vexpress-v2p-ca9.dtb -smp 4 -append "init=/linuxrc root=/dev/
mmcblk0p1 rw rootwait e arlyprintk console=ttyAMA0" 2>/dev/null
改爲:
qemu-system-arm –s –S -nographic -sd vexpress.img -M vexpress-a9 -m 512M -kernel
zImage -dtb vexpress-v2p-ca9.dtb -smp 4 -append "init=/linuxrc root=/dev/
mmcblk0p1 rw rootwait e arlyprintk console=ttyAMA0" 2>/dev/null
即添加-s–S選項,則會使嵌入式ARM Linux系統等待GDB遠程連入。在終端1運行新的./run-nolcd.sh,
這樣嵌入式ARM Linux的模擬平臺在1234端口偵聽。開一個新的終端2,進入/home/baohua/develop/linux/,
執行如下代碼:
baohua@baohua-VirtualBox:~/develop/linux$ arm-linux-gnueabihf-gdb ./vmlinux
GNU gdb (crosstool-NG linaro-1.13.1-4.8-2013.05 - Linaro GCC 2013.05) 7.6-2013.05
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "--host=i686-build_pc-linux-gnu --target=arm-linux-gnueabihf".
For bug reporting instructions, please see:
<https://bugs.launchpad.net/gcc-linaro>...
Reading symbols from /home/baohua/develop/linux/vmlinux...done.
(gdb)
接下來我們遠程連接127.0.0.1:1234
(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
0x60000000 in ?? ()
設置一個斷點到start_kernel()。
(gdb) b start_kernel
Breakpoint 1 at 0x805fd8ac: file init/main.c, line 490.
繼續運行:
(gdb) c
Continuing.
Breakpoint 1, start_kernel () at init/main.c:490
490 {
(gdb)
斷點停在了內核啓動過程中的start_kernel()函數,這個時候我們按下Ctrl+X,A鍵,可以看到代
碼,如圖21.4所示。
進一步,可以看看jiffies值之類的:
(gdb) p jiffies
$1 = 775612
(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
cpu_v7_do_idle () at arch/arm/mm/proc-v7.S:74
74 ret lr
(gdb) p jiffies
$2 = 775687
(gdb)
圖21.4 GDB調試內核
儘管採用“插樁”和仿真器結合GDB的方式可以查看和修改數據結構、斷點、單步等,而printk()這
種最原始的方法卻應用得更廣泛。
printk()這種方法很原始,但是一般可以解決工程中95%以上的問題。因此具體何時打印,以及打
印什麼東西,需要工程師逐步建立敏銳的嗅覺。加深對內核的認知,深入理解自己正在調試的模塊,這才
是快速解決問題的“王道”。工具只是一個輔助手段,無法代替工程師的思維。
工程師不能抱着得過且過的心態,也不能總是一知半解地進行低水平的重複建設。求知慾望對工程師
技術水平的提升有着最關鍵的作用。
21.3 內核打印信息——printk()
在Linux中,內核打印語句printk()會將內核信息輸出到內核信息緩衝區中,內核緩衝區是在
kernel/printk.c中通過如下語句靜態定義的:
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
內核信息緩衝區是一個環形緩衝區(Ring Buffer),因此,如果塞入的消息過多,則就會將之前的消
息沖刷掉。
printk()定義了8個消息級別,分爲級別0~7,級別越低(數值越大),消息越不重要,第0級是緊急
事件級,第7級是調試級,代碼清單21.2所示爲printk()的級別定義。
代碼清單21.2 printk()的級別定義
1 #define KERN_EMERG "<0>" /* 緊急事件,一般是系統崩潰之前提示的消息 */
2 #define KERN_ALERT "<1>" /* 必須立即採取行動 */
3 #define KERN_CRIT "<2>" /* 臨界狀態,通常涉及嚴重的硬件或軟件操作失敗 */
4 #define KERN_ERR "<3>" /* 用於報告錯誤狀態,設備驅動程序會
5 經常使用KERN_ERR來報告來自硬件的問題 */
6 #define KERN_WARNING "<4>" /* 對可能出現問題的情況進行警告,
7 這類情況通常不會對系統造成嚴重的問題 */
8 #define KERN_NOTICE "<5>" /* 有必要進行提示的正常情形,
9 許多與安全相關的狀況用這個級別進行彙報 */
10#define KERN_INFO "<6>" /* 內核提示性信息,很多驅動程序
11 在啓動的時候,用這個級別打印出它們找到的硬件信息 */
12#define KERN_DEBUG "<7>" /* 用於調試信息 */
通過/proc/sys/kernel/printk文件可以調節printk()的輸出等級,該文件有4個數字值,如下所示。
·控制檯(一般是串口)日誌級別:當前的打印級別,優先級高於該值的消息將被打印至控制檯。
·默認的消息日誌級別:將用該優先級來打印沒有優先級前綴的消息,也就是在直接寫printk(“xxx”)
而不帶打印級別的情況下,會使用該打印級別。
·最低的控制檯日誌級別:控制檯日誌級別可被設置的最小值(一般都是1)。
·默認的控制檯日誌級別:控制檯日誌級別的默認值。
如在Ubuntu PC上,/proc/sys/kernel/printk的值一般如下:
$ cat /proc/sys/kernel/printk
4 4 1 7
而我們通過如下命令可以使得Linux內核的任何printk()都從控制檯輸出:
# echo 8 > /proc/sys/kernel/printk
在默認情況下,DEBUG級別的消息不會從控制檯輸出,我們可以通過在bootargs中設置ignore_loglevel
來忽略打印級別,以保證所有消息都被打印到控制檯。在系統啓動後,用戶還可以通過
寫/sys/module/printk/parameters/ignore_loglevel文件動態來設置是否忽略打印級別。
要注意的是,/proc/sys/kernel/printk並不控制內核消息進入__log_buf的門檻,因此無論消息級別是多
少,都會進入__log_buf中,但是最終只有高於當前打印級別的內核消息纔會從控制檯打印。
用戶可以通過dmesg命令查看內核打印緩衝區,而如果使用dmesg-c命令,則不僅會顯示__log_buf,還
會清除該緩衝區的內容。也可以使用cat/proc/kmsg命令來顯示內核信息。/proc/kmsg是一個“永無休止的文
件”,因此,cat/proc/kmsg的進程只能通過“Ctrl+C”或kill終止。
在設備驅動中,經常需要輸出調試或系統信息,儘管可以直接採用printk(“<7>debug info…\n”)方式
的printk()語句輸出,但是通常可以使用封裝了printk()的更高級的宏,如pr_debug()、
dev_debug()等。代碼清單21.3所示爲pr_debug()和pr_info()的定義。
代碼清單21.3 可替代printk()的宏pr_debug()和pr_info()的定義
1#ifdef DEBUG
2#define pr_debug(fmt,arg...) \
3 printk(KERN_DEBUG fmt,##arg)
4#else
5static inline int _ _attribute_ _ ((format (printf, 1, 2))) pr_debug(const char * fmt, ...)
6{
7 return 0;
8}
9#endif
10
11#define pr_info(fmt,arg...) \
12 printk(KERN_INFO fmt,##arg)
使用pr_xxx()族API的好處是,可以在文件最開頭通過pr_fmt()定義一個打印格式,比如在
kernel/watchdog.c的最開頭通過如下定義可以保證之後watchdog.c調用的所有pr_xxx()打印的消息都自動
帶有“NMI watchdog:”的前綴。
#define pr_fmt(fmt) "NMI watchdog: " fmt
#include <linux/mm.h>
#include <linux/cpu.h>
#include <linux/nmi.h>…
代碼清單21.4所示爲dev_dbg()、dev_err()、dev_info()等的定義,使用dev_xxx()族API打印
的時候,設備名稱會被自動加到打印消息的前頭。
代碼清單21.4 包含設備信息的可替代printk()的宏
1#define dev_printk(level, dev, format, arg...) \
2 printk(level "%s %s: " format , dev_driver_string(dev) , (dev)->bus_id , ## arg)
3
4#ifdef DEBUG
5#define dev_dbg(dev, format, arg...) \
6 dev_printk(KERN_DEBUG , dev , format , ## arg)
7#else
8#define dev_dbg(dev, format, arg...) do { (void)(dev); } while (0)
9#endif
10
11#define dev_err(dev, format, arg...) \
12 dev_printk(KERN_ERR , dev , format , ## arg)
13#define dev_info(dev, format, arg...) \
14 dev_printk(KERN_INFO , dev , format , ## arg)
15#define dev_warn(dev, format, arg...) \
16 dev_printk(KERN_WARNING , dev , format , ## arg)
17#define dev_notice(dev, format, arg...) \
18 dev_printk(KERN_NOTICE , dev , format , ## arg)
在打印信息時,如果想輸出printk()調用所在的函數名,可以使用__func__;如果想輸出其所在代
碼的行號,可以使用__LINE__;想輸出源代碼文件名,可以使用__FILE__。例如drivers/block/sx8.c中的:
#ifdef CARM_NDEBUG
#define assert(expr)
#else
#define assert(expr) \
if(unlikely(!(expr))) { \
printk(KERN_ERR "Assertion failed! %s,%s,%s,line=%d\n", \
#expr, __FILE__, __func__, __LINE__); \
}
#endif
21.4 DEBUG_LL和EARLY_PRINTK
DEBUG_LL對應內核的Kernel low-level debugging功能,EARLY_PRINTK則對應內核中一個早期的控
制臺。爲了在內核的drivers/tty/serial下的控制檯驅動初始化之前支持打印,可以選擇DEBUG_LL和
EARLY_PRINTK這兩個配置選項。另外,也需要在bootargs中設置earlyprintk的選項。
對於LDDD3_vexpress而言,沒有DEBUG_LL和EARLY_PRINTK的時候,我們看到的內核最早的打印
是:
Booting Linux on physical CPU 0x0
Initializing cgroup subsys cpuset
Linux version …
如果我們使能DEBUG_LL和EARLY_PRINTK,選擇如圖21.5所示的“Use PL011UART0at
0x10009000(V2P-CA9core tile)”這個低級別調試口,並在bootargs中設置earlyprintk,則我們看到了更早
的打印信息:
Uncompressing Linux... done, booting the kernel.
圖21.5 選擇低級別調試UART
21.5 使用“/proc”
在Linux系統中,“/proc”文件系統十分有用,它被內核用於向用戶導出信息。“/proc”文件系統是一個
虛擬文件系統,通過它可以在Linux內核空間和用戶空間之間進行通信。在/proc文件系統中,我們可以將
對虛擬文件的讀寫作爲與內核中實體進行通信的一種手段,與普通文件不同的是,這些虛擬文件的內容都
是動態創建的。
“/proc”下的絕大多數文件是隻讀的,以顯示內核信息爲主。但是“/proc”下的文件也並不是完全只讀
的,若節點可寫,還可用於一定的控制或配置目的,例如前面介紹的寫/proc/sys/kernel/printk可以改變
printk()的打印級別。
Linux系統的許多命令本身都是通過分析“/proc”下的文件來完成的,如ps、top、uptime和free等。例
如,free命令通過分析/proc/meminfo文件得到可用內存信息,下面顯示了對應的meminfo文件和free命令的
結果。
1.meminfo文件
[root@localhost proc]# cat meminfo
MemTotal: 29516 kB
MemFree: 1472 kB
Buffers: 4096 kB
Cached: 12648 kB
SwapCached: 0 kB
Active: 14208 kB
Inactive: 8844 kB
HighTotal: 0 kB
HighFree: 0 kB
LowTotal: 29516 kB
LowFree: 1472 kB
SwapTotal: 265064 kB
SwapFree: 265064 kB
Dirty: 20 kB
Writeback: 0 kB
Mapped: 10052 kB
Slab: 3864 kB
CommitLimit: 279820 kB
Committed_AS: 13760 kB
PageTables: 444 kB
VmallocTotal: 999416 kB
VmallocUsed: 560 kB
VmallocChunk: 998580 kB
2. free命令
[root@localhost proc]# free
total used free shared buffers cached
Mem: 29516 28104 1412 0 4100 12700
-/+ buffers/cache: 11304 18212
Swap: 265064 0 265064
在Linux 3.9以及之前的內核版本中,可用如下函數創建“/proc”節點:
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,
struct proc_dir_entry *parent);
struct proc_dir_entry *create_proc_read_entry(const char *name, mode_t mode,
struct proc_dir_entry *base, read_proc_t *read_proc, void * data);
create_proc_entry()函數用於創建“/proc”節點,而create_proc_read_entry()調用
create_proc_entry()創建只讀的“/proc”節點。參數name爲“/proc”節點的名稱,parent/base爲父目錄的節
點,如果爲NULL,則指“/proc”目錄,read_proc是“/proc”節點的讀函數指針。當read()系統調用
在“/proc”文件系統中執行時,它映像到一個數據產生函數,而不是一個數據獲取函數。
下列函數用於創建“/proc”目錄:
struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent);
結合create_proc_entry()和proc_mkdir(),代碼清單21.5中的程序可用於先在/proc下創建一個目錄
procfs_example,而後在該目錄下創建一個文件example_file。
代碼清單21.5 proc_mkdir()和create_proc_entry()函數使用範例
1/* 創建/proc下的目錄 */
2example_dir = proc_mkdir("procfs_example", NULL);
3if (example_dir == NULL) {
4 rv = -ENOMEM;
5 goto out;
6}
7
8example_dir->owner = THIS_MODULE;
9
10/* 創建一個/proc文件 */
11example_file = create_proc_entry("example_file", 0666, example_dir);
12if (example_file == NULL) {
13 rv = -ENOMEM;
14 goto out;
15}
16
17example_file->owner = THIS_MODULE;
18example_file->read_proc = example_file_read;
19example_file->write_proc = example_file_write;
作爲上述函數返回值的proc_dir_entry結構體包含了“/proc”節點的讀函數指針
(read_proc_t*read_proc)、寫函數指針(write_proc_t*write_proc)以及父節點、子節點信息等。
/proc節點的讀寫函數的類型分別爲:
typedef int (read_proc_t)(char *page, char **start, off_t off,
int count, int *eof, void *data);
typedef int (write_proc_t)(struct file *file, const char __user *buffer,
unsigned long count, void *data);
讀函數中page指針指向用於寫入數據的緩衝區,start用於返回實際的數據並寫到內存頁的位置,eof是
用於返回讀結束標誌,offset是讀的偏移,count是要讀的數據長度。start參數比較複雜,對於/proc只包含
簡單數據的情況,通常不需要在讀函數中設置*start,這意味着內核將認爲數據保存在內存頁偏移0的地
方。
寫函數與file_operations中的write()成員函數類似,需要一次從用戶緩衝區到內存空間的複製過程。
在Linux系統中可用如下函數刪除/proc節點:
void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
在Linux系統中已經定義好的可使用的/proc節點宏包括:proc_root_fs(/proc)、
proc_net(/proc/net)、proc_bus(/proc/bus)、proc_root_driver(/proc/driver)等,proc_root_fs實際上就是
NULL。
代碼清單21.6所示爲一個簡單的“/proc”文件系統使用範例,這段代碼在模塊加載函數中創
建/proc/test_dir目錄,並在該目錄中創建/proc/test_dir/test_rw文件節點,在模塊卸載函數中撤銷“/proc”節
點,而/proc/test_dir/test_rw文件中只保存了一個32位的整數。
代碼清單21.6 /proc文件系統使用範例
1#include <linux/module.h>
2#include <linux/kernel.h>
3#include <linux/init.h>
4#include <linux/proc_fs.h>
5
6static unsigned int variable;
7static struct proc_dir_entry *test_dir, *test_entry;
8
9static int test_proc_read(char *buf, char **start, off_t off, int count,
10 int *eof, void *data)
11{
12 unsigned int *ptr_var = data;
13 return sprintf(buf, "%u\n", *ptr_var);
14}
15
16static int test_proc_write(struct file *file, const char *buffer,
17 unsigned long count, void *data)
18{
19 unsigned int *ptr_var = data;
20
21 *ptr_var = simple_strtoul(buffer, NULL, 10);
22
23 return count;
24}
25
26static __init int test_proc_init(void)
27{
28 test_dir = proc_mkdir("test_dir", NULL);
29 if (test_dir) {
30 test_entry = create_proc_entry("test_rw", 0666, test_dir);
31 if (test_entry) {
32 test_entry->nlink = 1;
33 test_entry->data = &variable;
34 test_entry->read_proc = test_proc_read;
35 test_entry->write_proc = test_proc_write;
36 return 0;
37 }
38 }
39
40 return -ENOMEM;
41}
42module_init(test_proc_init);
43
44static __exit void test_proc_cleanup(void)
45{
46 remove_proc_entry("test_rw", test_dir);
47 remove_proc_entry("test_dir", NULL);
48}
49module_exit(test_proc_cleanup);
50
51MODULE_AUTHOR("Barry Song <[email protected]>");
52MODULE_DESCRIPTION("proc exmaple");
53MODULE_LICENSE("GPL v2");
上述代碼第21行調用的simple_strtoul()用於將用戶輸入的字符串轉換爲無符號長整數,第3個參數
10意味着轉化方式是十進制。
編譯上述簡單的proc.c爲proc.ko,運行insmod proc.ko加載該模塊後,“/proc”目錄下將多出一個目錄
test_dir,該目錄下包含一個test_rw,ls–l的結果如下:
$ ls -l /proc/test_dir/test_rw
-rw-rw-rw- 1 root root 0 Aug 16 20:45 /proc/test_dir/test_rw
測試/proc/test_dir/test_rw的讀寫:
$ cat /proc/test_dir/test_rw
0$
echo 111 > /proc/test_dir/test_rw
$ cat /proc/test_dir/test_rw
說明我們上一步執行的寫操作是正確的。
在Linux 3.10及以後的版本中,“/proc”的內核API和實現架構變更較大,create_proc_entry()、
create_proc_read_entry()之類的API都被刪除了,取而代之的是直接使用proc_create()、
proc_create_data()API。同時,也不再存在read_proc()、write_proc()之類的針對proc_dir_entry的成
員函數了,而是直接把file_operations結構體的指針傳入proc_create()或者proc_create_data()函數中,
其原型爲:
static inline struct proc_dir_entry *proc_create(
const char *name, umode_t mode, struct proc_dir_entry *parent,
const struct file_operations *proc_fops);
struct proc_dir_entry *proc_create_data(
const char *name, umode_t mode, struct proc_dir_entry *parent,
const struct file_operations *proc_fops, void *data);
我們把代碼清單21.6的範例改造爲同時支持Linux 3.10以前的內核和Linux3.10以後的內核。改造結果
如代碼清單21.7所示。#if LINUX_VERSION_CODE<KERNEL_VERSION(3,10,0)中的部分是舊版本
的代碼,與21.6相同,所以省略了。
代碼清單21.7 支持Linux 3.10以後內核的/proc文件系統使用範例
1#include <linux/module.h>
2#include <linux/kernel.h>
3#include <linux/init.h>
4#include <linux/version.h>
5#include <linux/proc_fs.h>
6#include <linux/seq_file.h>
7
8static unsigned int variable;
9static struct proc_dir_entry *test_dir, *test_entry;
10
11#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 10, 0)
12...
13#else
14static int test_proc_show(struct seq_file *seq, void *v)
15{
16 unsigned int *ptr_var = seq->private;
17 seq_printf(seq, "%u\n", *ptr_var);
18 return 0;
19}
20
21static ssize_t test_proc_write(struct file *file, const char __user *buffer,
22 size_t count, loff_t *ppos)
23{
24 struct seq_file *seq = file->private_data;
25 unsigned int *ptr_var = seq->private;
26
27 *ptr_var = simple_strtoul(buffer, NULL, 10);
28 return count;
29}
30
31static int test_proc_open(struct inode *inode, struct file *file)
32{
33 return single_open(file, test_proc_show, PDE_DATA(inode));
34}
35
36static const struct file_operations test_proc_fops =
37{
38 .owner = THIS_MODULE,
39 .open = test_proc_open,
40 .read = seq_read,
41 .write = test_proc_write,
42 .llseek = seq_lseek,
43 .release = single_release,
44};
45#endif
46
47static __init int test_proc_init(void)
48{
49 test_dir = proc_mkdir("test_dir", NULL);
50 if (test_dir) {
51#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 10, 0)
52 ...
53#else
54 test_entry = proc_create_data("test_rw",0666, test_dir, &test_proc_fops, &variable);
55 if (test_entry)
56 return 0;
57#endif
58 }
59
60 return -ENOMEM;
61}
62module_init(test_proc_init);
63
64static __exit void test_proc_cleanup(void)
65{
66 remove_proc_entry("test_rw", test_dir);
67 remove_proc_entry("test_dir", NULL);
68}
69module_exit(test_proc_cleanup);
21.6 Oops
當內核出現類似用戶空間的Segmentation Fault時(例如內核訪問一個並不存在的虛擬地址),Oops會
被打印到控制檯和寫入內核log緩衝區。
我們在globalmem.c的globalmem_read()函數中加上下面一行代碼:
} else {
*ppos += count;
ret = count;
*(unsigned int *)0 = 1; /* a kernel panic */
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}
假設這個字符設備對應的設備節點是/dev/globalmem,通過cat/dev/globalmem命令讀設備文件,將得到
如下Oops信息:
# cat /dev/globalmem
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = 9ec08000
[00000000] *pgd=7f733831, *pte=00000000, *ppte=00000000
Internal error: Oops: 817 [#1] SMP ARM
Modules linked in: globalmem
CPU: 0 PID: 609 Comm: cat Not tainted 3.16.0+ #13
task: 9f7d8000 ti: 9f722000 task.ti: 9f722000
PC is at globalmem_read+0xbc/0xcc [globalmem]
LR is at 0x0
pc : [<7f000200>] lr : [<00000000>] psr: 00000013
sp : 9f723f30 ip : 00000000 fp : 00000000
r10: 9f414000 r9 : 00000000 r8 : 00001000
r7 : 00000000 r6 : 00001000 r5 : 00001000 r4 : 00000000
r3 : 00000001 r2 : 00000000 r1 : 00001000 r0 : 7f0003cc
Flags: nzcv IRQs on FIQs on Mode SVC_32 ISA ARM Segment user
Control: 10c53c7d Table: 7ec08059 DAC: 00000015
Process cat (pid: 609, stack limit = 0x9f722240)
Stack: (0x9f723f30 to 0x9f724000)
3f20: 7ed5ff91 9f723f80 00000000 9f79ab40
3f40: 00001000 7ed5eb18 9f723f80 00000000 00000000 800cb114 00000020 9f722000
3f60: 9f5e4628 9f79ab40 9f79ab40 00001000 7ed5eb18 00000000 00000000 800cb2ec
3f80: 00001000 00000000 9f7168c0 00001000 7ed5eb18 00000003 00000003 8000e4e4
3fa0: 9f722000 8000e360 00001000 7ed5eb18 00000003 7ed5eb18 00001000 0000002f
3fc0: 00001000 7ed5eb18 00000003 00000003 7ed5eb18 00000001 00000003 00000000
3fe0: 0015c23c 7ed5eb00 0000f718 00008d8c 60000010 00000003 00000000 00000000
[<7f000200>] (globalmem_read [globalmem]) from [<800cb114>] (vfs_read+0x98/0x13c)
[<800cb114>] (vfs_read) from [<800cb2ec>] (SyS_read+0x44/0x84)
[<800cb2ec>] (SyS_read) from [<8000e360>] (ret_fast_syscall+0x0/0x30)
Code: e1a05008 e2a77000 e1c360f0 e3a03001 (e58c3000)
---[ end trace 5a36d6470da50d02 ]---
Segmentation fault
上述Oops的第一行給出了“原因”,即訪問了NULL pointer。Oops中的PC is at
globalmem_read+0xbc/0xcc這一行代碼也比較關鍵,給出了“事發現場”,即globalmem_read()函數偏移
0xbc字節的指令處。
通過反彙編globalmem.o可以尋找到globalmem_read()函數開頭位置偏移0xbc的指令,反彙編方法如
下:
drivers/char/globalmem$ arm-linux-gnueabihf-objdump -d -S globalmem.o
對應的反彙編代碼如下,global_read()開始於0x144,偏移0xbc的位置爲0x200:
static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,
loff_t * ppos)
{
144: e92d45f0 push {r4, r5, r6, r7, r8, sl, lr}
148: e24dd00c sub sp, sp, #12
unsigned long p = *ppos;
14c: e5934000 ldr r4, [r3]

*ppos += count;
1f4: e2a77000 adc r7, r7, #0
1f8: e1c360f0 strd r6, [r3]
ret = count;
*(unsigned int *)0 = 1; /* a kernel panic */
1fc: e3a03001 mov r3, #1
200: e58c3000 str r3, [ip]
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
204: …
return ret;
}
“str r3,[ip]”是引起Oops的指令。這裏僅僅給出了一個例子,工程實踐中的“事發現場”並不全那麼容
易找到,但方法都是類似的。
21.7 BUG_ON()和WARN_ON()
內核中有許多地方調用類似BUG()的語句,它非常像一個內核運行時的斷言,意味着本來不該執
行到BUG()這條語句,一旦執行即拋出Oops。BUG()的定義爲:
#define BUG() do { \
printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __func__); \
panic("BUG!"); \
} while (0)
其中的panic()定義在kernel/panic.c中,會導致內核崩潰,並打印Oops。比如arch/arm/kernel/dma.c中的
enable_dma()函數:
void enable_dma (unsigned int chan)
{
dma_t *dma = dma_channel(chan);
if (!dma->lock)
goto free_dma;
if (dma->active == 0) {
dma->active = 1;
dma->d_ops->enable(chan, dma);
}
return;
free_dma:
printk(KERN_ERR "dma%d: trying to enable free DMA\n", chan);
BUG();
}
上述代碼的含義是,如果在dma->lock不成立的情況下,驅動直接調用了enable_dma(),實際上意味
着內核的一個bug。
BUG()還有一個變體叫BUG_ON(),它的內部會引用BUG(),形式爲:
#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while (0)
對於BUG_ON()而言,只有當括號內的條件成立的時候,才拋出Oops。比如drivers/char/random.c中
的類似代碼:
static void push_to_pool(struct work_struct *work)
{
struct entropy_store *r = container_of(work, struct entropy_store,
push_work);
BUG_ON(!r);
_xfer_secondary_pool(r, random_read_wakeup_bits/8);
trace_push_to_pool(r->name, r->entropy_count >> ENTROPY_SHIFT,
r->pull->entropy_count >> ENTROPY_SHIFT);
}
除了BUG_ON()外,內核有個稍微弱一些WARN_ON(),在括號中的條件成立的時候,內核會拋
出棧回溯,但是不會panic(),這通常用於內核拋出一個警告,暗示某種不太合理的事情發生了。如在
kernel/locking/mutex-debug.c中的debug_mutex_unlock()函數發現mutex_unlock()的調用者和
mutex_lock()的調用者不是同一個線程的時候或者mutex的owner爲空的時候,會拋出警告信息:
void debug_mutex_unlock(struct mutex *lock)
{
if (likely(debug_locks)) {
DEBUG_LOCKS_WARN_ON(lock->magic != lock);
if (!lock->owner)
DEBUG_LOCKS_WARN_ON(!lock->owner);
else
DEBUG_LOCKS_WARN_ON(lock->owner != current);
DEBUG_LOCKS_WARN_ON(!lock->wait_list.prev && !lock->wait_list.next);
mutex_clear_owner(lock);
}
}
有時候,WARN_ON()也可以作爲一個調試技巧。比如,我們進到內核某個函數後,不知道這個函
數是怎麼一級一級被調用進來的,那可以在該函數中加入一個WARN_ON(1)。
21.8 strace
在Linux系統中,strace是一種相當有效的跟蹤工具,它的主要特點是可以被用來監視系統調用。我們
不僅可以用strace調試一個新開始的程序,也可以調試一個已經在運行的程序(這意味着把strace綁定到一
個已有的PID上)。對於第6章的globalmem字符設備文件,以strace方式運行如代碼清單21.8所示的用戶空
間應用程序globalmem_test,運行的結果如下:
execve("./globalmem_test", ["./globalmem_test"], [/* 24 vars */]) = 0
...
open("/dev/globalmem", O_RDWR) = 3 /* 打開的/dev/globalmem的fd是3 */
ioctl(3, FIBMAP, 0) = 0
read(3, 0xbff17920, 200) = -1 ENXIO (No such device or address)/* 讀取失敗 */
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f04000
write(1, "-1 bytes read from globalmem\n", 29-1 bytes read from globalmem
) = 29 /* 向標準輸出設備(fd爲1)寫入printf中的字符串 */
write(3, "This is a test of globalmem", 27) = 27
write(1, "27 bytes written into globalmem\n", 3227 bytes written into globalmem
) = 32
...
輸出的每一行對應一次Linux系統調用,其格式爲“左邊=右邊”,等號左邊是系統調用的函數名及其參
數,右邊是該調用的返回值。
代碼清單21.8 用戶空間應用程序globalmem_test
1#include ...
2
3#define MEM_CLEAR 0x1
4main()
5{
6 int fd, num, pos;
7 char wr_ch[200] = "This is a test of globalmem";
8 char rd_ch[200];
9 /* 打開/dev/globalmem */
10 fd = open("/dev/globalmem", O_RDWR, S_IRUSR | S_IWUSR);
11 if (fd != -1 ) { /* 清除globalmem */
12 if(ioctl(fd, MEM_CLEAR, 0) < 0)
13 printf("ioctl command failed\n");
14 /* 讀globalmem */
15 num = read(fd, rd_ch, 200);
16 printf("%d bytes read from globalmem\n",num);
17
18 /* 寫globalmem */
19 num = write(fd, wr_ch, strlen(wr_ch));
20 printf("%d bytes written into globalmem\n",num);
21 ...
22 close(fd);
23 }
24}
使用strace雖然無法直接追蹤到設備驅動中的函數,但是足以幫助工程師進行推演,如從
open(“/dev/globalmem”,O_RDWR)=3的返回結果知道/dev/globalmem的fd爲3,之後對fd爲3的文件進行
read()、write()和ioctl()系統調用,最終會使globalmem裏file_operations中的相應函數被調用,通過
系統調用的結果就可以知道驅動中globalmem_read()、globalmem_write()和globalmem_ioctl()的運
行結果。
21.9 KGDB
Linux直接提供了對KGDB的支持,KGDB採用了典型的嵌入式系統“插樁”技巧,一般依賴於串口與調
試主機通信。爲了支持KGDB,串口驅動應該實現純粹的輪詢收發單一字符的成員函數,以供
drivers/tty/serial/kgdboc.c調用,譬如drivers/tty/serial/8250/8250_core.c中的:
static struct uart_ops serial8250_pops = {

#ifdef CONFIG_CONSOLE_POLL
.poll_get_char = serial8250_get_poll_char,
.poll_put_char = serial8250_put_poll_char,
#endif
};
在編譯內核時,運行make ARCH=arm menuconfig時需選擇關於KGDB的編譯項目,如圖21.6所示。
圖21.6 KGDB編譯選項配置
對於目標板而言,需要在bootargs中設置與KGDB對應的串口等信息,如kgdboc=ttyS0,
115200kgdbcon。
如果想一開機內核就直接進入等待GDB連接的調試狀態,可以在bootargs中設置kgdbwait,kgdbwait的
含義是啓動時就等待主機的GDB連接。而若想在內核啓動後進入GDB調試模式,可運行echo
g>/proc/sysrq_trigger命令給內核傳入一個鍵值是g的magic_sysrq。
在調試PC上,依次運行如下命令就可以啓動調試並連接至目標機(假設串口在PC上對應的設備節點
是/dev/ttyS0):
# arm-eabi-gdb ./vmlinux
(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyS0 //連接目標機
(gdb)
之後,在主機上,我們可以使用GDB像調試應用程序一樣調試使能了KGDB的目標機上的內核。
21.10 使用仿真器調試內核
在ARM Linux領域,目前比較主流的是採用ARM DS-5Development Studio方案。ARM DS-5是一個針
對基於Linux的系統和裸機嵌入式系統的專業軟件開發解決方案,它涵蓋了開發的所有階段,從啓動代
碼、內核移植直到應用程序調試、分析。如圖21.7所示,它使用了DSTREAM高性能仿真器(ARM已經停
止更新RVI-RVT2仿真器),在Eclipse內包含了DS-5和DSTREAM的開發插件。
調試主機一般通過網線與DSTREAM仿真器連接,而仿真器則連接與電路板類似的JTAG接口,之後
用DS-5調試器進行調試。DS-5圖形化調試器提供了全面和直觀的調試圖,非常易於調試Linux和裸機程
序,易於查看代碼,進行棧回溯,查看內存、寄存器、表達式、變量,分析內核線程,設置斷點。
圖21.7 DSTREAM仿真器和DS-5開發環境
值得一提的是,DS-5也提供了Streamline Performance Analyzer。ARM Streamline性能分析器(見圖
21.8)爲軟件開發人員提供了一種用來分析和優化在ARM926、ARM11和Cortex-A系列平臺上運行的Linux
和Android系統的直觀方法。使用Streamline,Linux內核中需包含一個gator模塊,用戶空間則需要使能
gatord後臺服務器程序。關於Streamline具體的操作方法可以查看《ARM® DS-5Using ARM Streamline》。
圖21.8 ARMStreamline性能分析器
21.11 應用程序調試
在嵌入式系統中,爲調試Linux應用程序,可在目標板上先運行GDBServer,再讓主機上的GDB與目
標板上的GDBServer通過網口或串口通信。
1.目標板
需要運行如下命令啓動GDBServer:
gdbserver <host_ip>:<port> <app>
<host_ip>:<port>爲主機的IP地址和端口,app是可執行的應用程序名。
當然,也可以用系統中空閒的串口作爲GDB調試器和GDBServer的底層通信手段,如:
gdbserver/dev/ttyS0./tdemo
2.主機
需要先運行如下命令啓動GDB:
arm-eabi-gdb <app>
app與GDBServer的app參數對應。
之後,運行如下命令就可以連接目標板:
target remote <target_ip>:<port>
<target_ip>:<port>爲目標機的IP地址和端口。
如果目標板上的GDBServer使用串口,則在宿主機上GDB也應該使用串口,如:
(gdb)target remote/dev/ttyS1
之後,便可以使用GDB像調試本機上的程序一樣調試目標機上的程序。
3.通過GDB server和ARM GDB調試應用程序
在ARM開發板上放置GDB server,便可以通過目標板與調試PC之間的以太網等調試。要調試的應用
程序的源代碼如下:
/*
* gdb_example.c: program to show how to use arm-linux-gdb
*/
void increase_one(int *data)
{ *data = *data + 1;
}i
nt main(int argc, char *argv[])
{ int dat = 0;
int *p = 0;
increase_one(&dat);
/* program will crash here */
increase_one(p);
return 0;
}
通過debug方式編譯它:
arm-linux-gnueabi-gcc -g -o gdb_example gdb_example.c
將程序下載到目標板後,在目標板上運行:
# gdbserver 192.168.1.20:1234 gdb_example
Process gdb_example created; pid = 1096
Listening on port 1234
其中192.168.1.20爲目標板的3IP,1234爲GDBserver的偵聽端口。
如果目標機是Android系統,且沒有以太網,可以嘗試使用adb forward功能,比如adb forward tcp:
1234tcp:1234是把目標機1234端口與主機1234端口進行轉發。
在主機上運行:
$ arm-eabi-gdb gdb_example…
主機的GDB中運行如下命令以連接目標板:
(gdb) target remote 192.168.1.20:1234
Remote debugging using 192.168.1.20:1234
...
0x400007b0 in ?? ()
如果是Android的adb forward,則上述target remote 192.168.1.20:1234中的IP地址可以去掉,因爲它變
成直接連接本機了,可直接寫成target remote:1234。
運行如下命令將斷點設置在increase_one(&dat);這一行:
(gdb) b gdb_example.c:16
Breakpoint 1 at 0x8390: file gdb_example.c, line 16.
通過c命令繼續運行目標板上的程序,發生斷點:
(gdb) c
Continuing.
...
Breakpoint 1, main (argc=1, argv=0xbead4eb4) at gdb_example.c:16
16increase_one(&dat);
運行n命令執行完increase_one(&dat);再查看dat的值:
(gdb) n
19increase_one(p); (gdb) p dat
$1 = 1
發現dat變成1。繼續運行c命令,由於即將訪問空指針,gdb_example將崩潰:
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x0000834c in increase_one (data=0x0) at gdb_example.c:8
8*data = *data + 1;
我們通過bt命令可以拿到backtrace:
(gdb) bt
#0 0x0000834c in increase_one (data=0x0) at gdb_example.c:8
#1 0x000083a4 in main (argc=1, argv=0xbead4eb4) at gdb_example.c:19
通過info reg命令可以查看當時的寄存器值:
(gdb) info reg
r00x0 0
r10xbead4eb43199028916
r20x1 1
r30x0 0
r40x4001e5e01073866208
r50x0 0
r60x826c33388
r70x0 0
r80x0 0
r90x0 0
r10 0x400250001073893376
r11 0xbead4d443199028548
r12 0xbead4d483199028552
sp 0xbead4d300xbead4d30
lr 0x83a433700
pc 0x834c0x834c <increase_one+24>
fps 0x0 0
cpsr 0x600000101610612752
21.12 Linux性能監控與調優工具
除了保證程序的正確性以外,在項目開發中往往還關心性能和穩定性。這時候,我們往往要對內核、
應用程序或整個系統進行性能優化。在性能優化中常用的手段如下。
1.使用top、vmstat、iostat、sysctl等常用工具
top命令用於顯示處理器的活動狀況。在缺省情況下,顯示佔用CPU最多的任務,並且每隔5s做一次
刷新;vmstat命令用於報告關於內核線程、虛擬內存、磁盤、陷阱和CPU活動的統計信息;iostat命令用於
分析各個磁盤的傳輸閒忙狀況;netstat是用來檢測網絡信息的工具;sar用於收集、報告或者保存系統活動
信息,其中,sar用於顯示數據,sar1和sar2用於收集和保存數據。
sysctl是一個可用於改變正在運行中的Linux系統的接口。用sysctl可以讀取幾百個以上的系統變量,例
如用sysctl–a可讀取所有變量。
sysctl的實現原理是:所有的內核參數在/proc/sys中形成一個樹狀結構,sysctl系統調用的內核函數是
sys_sysctl,匹配項目後,最後的讀寫在do_sysctl_strategy中完成,如
echo "1" > /proc/sys/net/ipv4/ip_forward
就等價於:
sysctl –w net.ipv4.ip_forward ="1"
2.使用高級分析手段,如OProfile、gprof
OProfile可以幫助用戶識別諸如模塊的佔用時間、循環的展開、高速緩存的使用率低、低效的類型轉
換和冗餘操作、錯誤預測轉移等問題。它收集有關處理器事件的信息,其中包括TLB的故障、停機、存儲
器訪問以及緩存命中和未命中的指令的攫取數量。
OProfile支持兩種採樣方式:基於事件的採樣(Event Based)和基於時間的採樣(Time Based)。基於
事件的採樣是OProfile只記錄特定事件(比如L2緩存未命中)的發生次數,當達到用戶設定的定值時
Oprofile就記錄一下(採一個樣)。這種方式需要CPU內部有性能計數器(Performace Counter)。基於時
間的採樣是OProfile藉助OS時鐘中斷的機制,在每個時鐘中斷,OProfile都會記錄一次(採一次樣)。引
入它的目的在於,提供對沒有性能計數器的CPU的支持,其精度相對於基於事件的採樣要低,因爲要藉助
OS時鐘中斷的支持,對於禁用中斷的代碼,OProfile不能對其進行分析。
OProfile在Linux上分兩部分,一個是內核模塊(oprofile.ko),另一個是用戶空間的守護進程
(oprofiled)。前者負責訪問性能計數器或者註冊基於時間採樣的函數,並將採樣值置於內核的緩衝區
內。後者在後臺運行,負責從內核空間收集數據,寫入文件。其運行步驟如下。
1)初始化opcontrol--init
2)配置opcontrol--setup--event=...
3)啓動opcontrol--start
4)運行待分析的程序xxx
5)取出數據
opcontrol--dump
opcontrol--stop
6)分析結果opreport-l./xxx
用GNU gprof可以打印出程序運行中各個函數消耗的時間,以幫助程序員找出衆多函數中耗時最多的
函數;還可產生程序運行時的函數調用關係,包括調用次數,以幫助程序員分析程序的運行流程。
GNU gprof的實現原理:在編譯和鏈接程序的時候(使用-pg編譯和鏈接選項),gcc在應用程序的每
個函數中都加入名爲mcount(_mcount或__mcount,依賴於編譯器或操作系統)的函數,也就是說應用程
序裏的每一個函數都會調用mcount,而mcount會在內存中保存一張函數調用圖,並通過函數調用堆棧的形
式查找子函數和父函數的地址。這張調用圖也保存了所有與函數相關的調用時間、調用次數等的所有信
息。
GNU gprof的基本用法如下。
1)使用-pg編譯和鏈接應用程序。
2)執行應用程序並使它生成供gprof分析的數據。
3)使用gprof程序分析應用程序生成的數據。
3.進行內核跟蹤,如LTTng
LTTng(Linux Trace Toolkit-next generation,官方網站爲http://lttng.org/)是一個用於跟蹤系統詳細運行
狀態和流程的工具,它可以跟蹤記錄系統中的特定事件。這些事件包括:系統調用的進入和退出;陷阱/
中斷(Trap/Irq)的進入和退出;進程調度事件;內核定時器;進程管理相關事件——創建、喚醒、信號
處理等;文件系統相關事件——open/read/write/seek/ioctl等;內存管理相關事件——內存分配/釋放等;其
他IPC/套接字/網絡等事件。而對於這些記錄,我們可以通過圖形的方式經由lttv-gui查看,如圖21.9所示。
4.使用LTP進行壓力測試
LTP(Linux Test Project,官方網站爲http://ltp.sourceforge.net/)是一個由SGI發起並由IBM負責維護的
合作計劃。它的目的是爲開源社區提供測試套件來驗證Linux的可靠性、健壯性和穩定性。它通過壓力測
試來判斷系統的穩定性和可靠性,在工程中我們可使用LTP測試套件對Linux操作系統進行超長時間的測
試,它可進行文件系統壓力測試、硬盤I/O測試、內存管理壓力測試、IPC壓力測試、SCHED測試、命令
功能的驗證測試、系統調用功能的驗證測試等。
圖21.9 LTTng形成的時序圖
5.使用Benchmark評估系統
可用於Linux的Benchmark的包括lmbench、UnixBench、AIM9、Netperf、SSLperf、dbench、Bonnie、
Bonnie++、Iozone、BYTEmark等,它們可用於評估操作系統、網絡、I/O子系統、CPU等的性能,參考網
址http://lbs.sourceforge.net/列出了許多Benchmark工具。
21.13 總結
Linux程序的調試,尤其是內核的調試看起來比較複雜,沒有類似於VC++、Tornado的IDE開發環境,
最常用的調試手段依然是文本方式的GDB。文本方式的GDB調試器功能異常強大,當我們使用習慣後,
就會用得非常自然。
Linux內核驅動的調試方法包括“插樁”、使用仿真器和藉助printk()、Oops、strace等,在大多數情況
下,原始的printk()仍然是最有效的手段。
除了本章介紹的方法外,在驅動的調試中很可能還會藉助其他的硬件或軟件調試工具,如調試USB驅
動最好藉助USB分析儀,用USB分析儀將可捕獲USB通信中的數據包,如同網絡中的Sniffer軟件一樣。

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