日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > linux >内容正文

linux

linux驱动程序设计21 Linux设备驱动的调试

發(fā)布時間:2023/12/8 linux 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 linux驱动程序设计21 Linux设备驱动的调试 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本章導(dǎo)讀
“工欲善其事,必先利其器”,為了方便進(jìn)行Linux設(shè)備驅(qū)動的開發(fā)和調(diào)試,建立良好的開發(fā)環(huán)境很重
要,還要使用必要的工具軟件以及掌握常用的調(diào)試技巧等。
21.1節(jié)講解了Linux下調(diào)試器GDB的基本用法和技巧。
21.2節(jié)講解了Linux內(nèi)核的調(diào)試方法。
21.3~21.10節(jié)對21.3節(jié)的概述展開了講解,內(nèi)容有:Linux內(nèi)核調(diào)試用的printk()、BUG_ON()、
WARN_ON()、/proc、Oops、strace、KGDB,以及使用仿真器進(jìn)行調(diào)試的方法。
21.11節(jié)講解了Linux應(yīng)用程序的調(diào)試方法,驅(qū)動工程師往往需要編寫用戶空間的應(yīng)用程序以對自身編
寫的驅(qū)動進(jìn)行驗證和測試,因此,掌握應(yīng)用程序調(diào)試方法對驅(qū)動工程師而言也是必需的。
21.12節(jié)講解了Linux常用的一些穩(wěn)定性、性能分析和調(diào)優(yōu)工具。
21.1 GDB調(diào)試器的用法
21.1.1 GDB的基本用法
GDB是GNU開源組織發(fā)布的一個強(qiáng)大的UNIX下的程序調(diào)試工具,GDB主要可幫助工程師完成下面4
個方面的功能。
·啟動程序,可以按照工程師自定義的要求運(yùn)行程序。
·讓被調(diào)試的程序在工程師指定的斷點(diǎn)處停住,斷點(diǎn)可以是條件表達(dá)式。
·當(dāng)程序被停住時,可以檢查此時程序中所發(fā)生的事,并追蹤上文。
·動態(tài)地改變程序的執(zhí)行環(huán)境。
不管是調(diào)試Linux內(nèi)核空間的驅(qū)動還是調(diào)試用戶空間的應(yīng)用程序,都必須掌握GDB的用法。而且,在
調(diào)試內(nèi)核和調(diào)試應(yīng)用程序時使用的GDB命令是完全相同的,下面以代碼清單21.1的應(yīng)用程序為例演示
GDB調(diào)試器的用法。
代碼清單21.1 GDB調(diào)試器用法的演示程序
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編譯上述程序,得到包含調(diào)試信息的二進(jìn)制文件
example,執(zhí)行g(shù)db gdb_example命令進(jìn)入調(diào)試狀態(tài),如下所示:
$ 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中運(yùn)行l(wèi)ist命令(縮寫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>,顯示函數(shù)名為function的函數(shù)的源程序,如下所示:
(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,顯示當(dāng)前行后面的源程序。
·list-,顯示當(dāng)前行前面的源程序。
下面演示了使用GDB中的run(縮寫為r)、break(縮寫為b)、next(縮寫為n)命令控制程序的運(yù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中,運(yùn)行程序使用run命令。在程序運(yùn)行前,我們可以設(shè)置如下4方面的工作環(huán)境。
(1)程序運(yùn)行參數(shù)
用set args可指定運(yùn)行時參數(shù),如set args 10 20 30 40 50;用show args命令可以查看設(shè)置好的運(yùn)行參
數(shù)。
(2)運(yùn)行環(huán)境
用path<dir>可設(shè)定程序的運(yùn)行路徑;用how paths可查看程序的運(yùn)行路徑;用set environment
varname[=value]可設(shè)置環(huán)境變量,如set env USER=baohua;用show environment[varname]則可查看環(huán)境變
量。
(3)工作目錄
cd<dir>相當(dāng)于shell的cd命令,pwd可顯示當(dāng)前所在的目錄。
(4)程序的輸入輸出
info terminal用于顯示程序用到的終端的模式;在GDB中也可以使用重定向控制程序輸出,如
run>outfile;用tty命令可以指定輸入輸出的終端設(shè)備,如tty/dev/ttyS1。
3.break命令
在GDB中用break命令來設(shè)置斷點(diǎn),設(shè)置斷點(diǎn)的方法如下。
(1)break<function>
在進(jìn)入指定函數(shù)時停住,在C++中可以使用class::function或function(type,type)格式來指定函數(shù)
名。
(2)break<linenum>
在指定行號停住。
(3)break+offset/break-offset。
在當(dāng)前行號的前面或后面的offset行停住,offiset為自然數(shù)。
(4)break filename:linenum
在源文件filename的linenum行處停住。
(5)break filename:function
在源文件filename的function函數(shù)的入口處停住。
(6)break*address
在程序運(yùn)行的內(nèi)存地址處停住。
(7)break
break命令沒有參數(shù)時,表示在下一條指令處停住。
(8)break…if<condition>
…可以是上述的break<linenum>、break+offset/break–offset中的參數(shù),condition表示條件,在條件成立
時停住。比如在循環(huán)體中,可以設(shè)置break if i=100,表示當(dāng)i為100時停住程序。
查看斷點(diǎn)時,可使用info命令,如info breakpoints[n]、info break[n](n表示斷點(diǎn)號)。
4.單步命令
在調(diào)試過程中,next命令用于單步執(zhí)行,類似于VC++中的step over。next的單步不會進(jìn)入函數(shù)的內(nèi)
部,與next對應(yīng)的step(縮寫為s)命令則在單步執(zhí)行一個函數(shù)時,進(jìn)入其內(nèi)部,類似于VC++中的step
into。下面演示了step命令的執(zhí)行情況,在第23行的add()函數(shù)調(diào)用處執(zhí)行step會進(jìn)入其內(nèi)部的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;
單步執(zhí)行的更復(fù)雜用法如下。
(1)step<count>
單步跟蹤,如果有函數(shù)調(diào)用,則進(jìn)入該函數(shù)(進(jìn)入函數(shù)的前提是,此函數(shù)被編譯有debug信息)。step
后面不加count表示一條條地執(zhí)行,加count表示執(zhí)行后面的count條指令,然后再停住。
(2)next<count>
單步跟蹤,如果有函數(shù)調(diào)用,它不會進(jìn)入該函數(shù)。同理,next后面不加count表示一條條地執(zhí)行,加
count表示執(zhí)行后面的count條指令,然后再停住。
(3)set step-mode
set step-mode on用于打開step-mode模式,這樣,在進(jìn)行單步跟蹤(運(yùn)行step指令)時,若跨越某沒有
調(diào)試信息的函數(shù),程序的執(zhí)行則會在該函數(shù)的第一條指令處停住,而不會跳過整個函數(shù)。這樣我們可以查
看該函數(shù)的機(jī)器指令。
(4)finish
運(yùn)行程序,直到當(dāng)前函數(shù)完成返回,并打印函數(shù)返回時的堆棧地址、返回值及參數(shù)值等信息。
(5)until(縮寫為u)
一直在循環(huán)體內(nèi)執(zhí)行單步而退不出來是一件令人煩惱的事情,用until命令可以運(yùn)行程序直到退出循環(huán)
體。
(6)stepi(縮寫為si)和nexti(縮寫為ni)
stepi和nexti用于單步跟蹤一條機(jī)器指令。比如,一條C程序代碼有可能由數(shù)條機(jī)器指令完成,stepi和
nexti可以單步執(zhí)行機(jī)器指令,相反,step和next是C語言級別的命令。
另外,運(yùn)行display/i$pc命令后,單步跟蹤會在打出程序代碼的同時打出機(jī)器指令,即匯編代碼。
5.continue命令
當(dāng)程序被停住后,可以使用continue命令(縮寫為c,fg命令同continue命令)恢復(fù)程序的運(yùn)行直到程序
結(jié)束,或到達(dá)下一個斷點(diǎn),命令格式為:
continue [ignore-count]
c [ignore-count]
fg [ignore-count]
ignore-count表示忽略其后多少次斷點(diǎn)。
假設(shè)我們設(shè)置了函數(shù)斷點(diǎn)add(),并觀察i,則在continue過程中,每次遇到add()函數(shù)或i發(fā)生變
化,程序就會停住,如下所示:
(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命令
在調(diào)試程序時,當(dāng)程序被停住時,可以使用print命令(縮寫為p),或是同義命令inspect來查看當(dāng)前
程序的運(yùn)行數(shù)據(jù)。print命令的格式如下:
print <expr>
print /<f> <expr>
<expr>是表達(dá)式,也是被調(diào)試的程序中的表達(dá)式,<f>是輸出的格式,比如,如果要把表達(dá)式按十六
進(jìn)制的格式輸出,那么就是/x。在表達(dá)式中,有幾種GDB所支持的操作符,它們可以用在任何一種語言
中,@是一個和數(shù)組有關(guān)的操作符,::指定一個在文件或是函數(shù)中的變量,{<type>}<addr>表示一個指
向內(nèi)存地址<addr>的類型為type的對象。
下面演示了查看sum[]數(shù)組的值的過程:
(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}
當(dāng)需要查看一段連續(xù)內(nèi)存空間的值時,可以使用GDB的@操作符,@的左邊是第一個內(nèi)存地址,@的
右邊則是想查看內(nèi)存的長度。例如如下動態(tài)申請的內(nèi)存:
int *array = (int *) malloc (len * sizeof (int));
在GDB調(diào)試過程中這樣顯示這個動態(tài)數(shù)組的值:
p *array@len
print的輸出格式如下。
·x:按十六進(jìn)制格式顯示變量。
·d:按十進(jìn)制格式顯示變量。
·u:按十六進(jìn)制格式顯示無符號整型。
·o:按八進(jìn)制格式顯示變量。
·t:按二進(jìn)制格式顯示變量。
·a:按十六進(jìn)制格式顯示變量。
·c:按字符格式顯示變量。
·f:按浮點(diǎn)數(shù)格式顯示變量。
我們可用display命令設(shè)置一些自動顯示的變量,當(dāng)程序停住時,或是單步跟蹤時,這些變量會自動顯
示。
如果要修改變量,如x的值,可使用如下命令:
print x=4
當(dāng)用GDB的print查看程序運(yùn)行時數(shù)據(jù)時,每一個print都會被GDB記錄下來。GDB會以$1,$2,$3…
這樣的方式為每一個print命令編號。我們可以使用這個編號訪問以前的表達(dá)式,如$1。
7.watch命令
watch一般用來觀察某個表達(dá)式(變量也是一種表達(dá)式)的值是否有了變化,如果有變化,馬上停止
程序運(yùn)行。我們有如下幾種方法來設(shè)置觀察點(diǎn)。
watch<expr>:為表達(dá)式(變量)expr設(shè)置一個觀察點(diǎn)。一旦表達(dá)式值有變化時,馬上停止程序運(yùn)行。
rwatch<expr>:當(dāng)表達(dá)式(變量)expr被讀時,停止程序運(yùn)行。
awatch<expr>:當(dāng)表達(dá)式(變量)的值被讀或被寫時,停止程序運(yùn)行。
info watchpoints:列出當(dāng)前所設(shè)置的所有觀察點(diǎn)。
下面演示了觀察i并在連續(xù)運(yùn)行next時一旦發(fā)現(xiàn)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)來查看內(nèi)存地址中的值。examine命令的語法如下所示:
x/<n/f/u> <addr>
<addr>表示一個內(nèi)存地址。“x/”后的n、f、u都是可選的參數(shù),n是一個正整數(shù),表示顯示內(nèi)存的長
度,也就是說從當(dāng)前地址向后顯示幾個地址的內(nèi)容;f表示顯示的格式,如果地址所指的是字符串,那么
格式可以是s,如果地址是指令地址,那么格式可以是i;u表示從當(dāng)前地址往后請求的字節(jié)數(shù),如果不指
定的話,GDB默認(rèn)的是4字節(jié)。u參數(shù)可以被一些字符代替:b表示單字節(jié),h表示雙字節(jié),w表示四字節(jié),
g表示八字節(jié)。當(dāng)我們指定了字節(jié)長度后,GDB會從指定的內(nèi)存地址開始,讀寫指定字節(jié),并把其當(dāng)作一
個值取出來。n、f、u這3個參數(shù)可以一起使用,例如命令x/3uh 0x54320表示從內(nèi)存地址0x54320開始以雙
字節(jié)為1個單位(h)、16進(jìn)制方式(u)顯示3個單位(3)的內(nèi)存。
9.set命令
examine命令用于查看內(nèi)存,而set命令用于修改內(nèi)存。它的命令格式是“set*有類型的指針=value”。
比如,下列程序,在用gdb運(yùn)行起來后,通過Ctrl+C停住。
main()
{
void *p = malloc(16);
while(1);
}
我們可以在運(yùn)行中用如下命令來修改p指向的內(nèi)存。
(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'
看看結(jié)果:
(gdb) x/s p
0x804b008: "hello"
也可以直接使用地址常數(shù):
(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命令
一般來說,被調(diào)試程序會按照程序代碼的運(yùn)行順序依次執(zhí)行,但是GDB也提供了亂序執(zhí)行的功能,
也就是說,GDB可以修改程序的執(zhí)行順序,從而讓程序隨意跳躍。這個功能可以由GDB的jump命令
jump<linespec>來指定下一條語句的運(yùn)行點(diǎn)。<linespec>可以是文件的行號,可以是file:line格式,也可以
是+num這種偏移量格式,表示下一條運(yùn)行語句從哪里開始。
jump <address>
這里的<address>是代碼行的內(nèi)存地址。
注意:jump命令不會改變當(dāng)前程序棧中的內(nèi)容,如果使用jump從一個函數(shù)跳轉(zhuǎn)到另一個函數(shù),當(dāng)跳
轉(zhuǎn)到的函數(shù)運(yùn)行完返回,進(jìn)行出棧操作時必然會發(fā)生錯誤,這可能會導(dǎo)致意想不到的結(jié)果,因此最好只用
jump在同一個函數(shù)中進(jìn)行跳轉(zhuǎn)。
11.signal命令
使用singal命令,可以產(chǎn)生一個信號量給被調(diào)試的程序,如中斷信號Ctrl+C。于是,可以在程序運(yùn)行
的任意位置處設(shè)置斷點(diǎn),并在該斷點(diǎn)處用GDB產(chǎn)生一個信號量,這種精確地在某處產(chǎn)生信號的方法非常
有利于程序的調(diào)試。
signal命令的語法是signal<signal>,UNIX的系統(tǒng)信號量通常為1~15,因此<signal>的取值也在這個范
圍內(nèi)。
12.return命令
如果在函數(shù)中設(shè)置了調(diào)試斷點(diǎn),在斷點(diǎn)后還有語句沒有執(zhí)行完,這時候我們可以使用return命令強(qiáng)制
函數(shù)忽略還沒有執(zhí)行的語句并返回。
return
return <expression>
上述return命令用于取消當(dāng)前函數(shù)的執(zhí)行,并立即返回,如果指定了<expression>,那么該表達(dá)式的值
會被作為函數(shù)的返回值。
13.call命令
call命令用于強(qiáng)制調(diào)用某函數(shù):
call <expr>
表達(dá)式可以是函數(shù),以此達(dá)到強(qiáng)制調(diào)用函數(shù)的目的,它會顯示函數(shù)的返回值(如果函數(shù)返回值不是
void)。比如在下列程序執(zhí)行while(1)的時候:
main()
{
void *p = malloc(16);
while(1);
}
我們強(qiáng)制要求其執(zhí)行strcpy()和printf():
(gdb) call strcpy(p, "hello world")
$3 = 134524936
(gdb) call printf("%s\n", p)
hello world
$4 = 12
14.info命令
info命令可以用來在調(diào)試時查看寄存器、斷點(diǎn)、觀察點(diǎn)和信號等信息。要查看寄存器的值,可以使用
如下命令:
info registers (查看除了浮點(diǎn)寄存器以外的寄存器)
info all-registers (查看所有寄存器,包括浮點(diǎn)寄存器)
info registers <regname ...> (查看所指定的寄存器)
要查看斷點(diǎn)信息,可以使用如下命令:
info break要列出當(dāng)前所設(shè)置的所有觀察點(diǎn),可使用如下命令:
info watchpoints
要查看有哪些信號正在被GDB檢測,可使用如下命令:
info signals
info handle
也可以使用info line命令來查看源代碼在內(nèi)存中的地址。info line后面可以跟行號、函數(shù)名、文件名:行
號、文件名:函數(shù)名等多種形式,例如用下面的命令會打印出所指定的源碼在運(yùn)行時的內(nèi)存地址:
info line tst.c:func
15.disassemble
disassemble命令用于反匯編,可用它來查看當(dāng)前執(zhí)行時的源代碼的機(jī)器碼,實(shí)際上只是把目前內(nèi)存中
的指令沖刷出來。下面的示例用于查看函數(shù)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圖形界面調(diào)試工具
GDB本身是一種命令行調(diào)試工具,但是通過DDD(Data Display Debugger,見
http://www.gnu.org/software/ddd/)可以被圖形界面化。DDD可以作為GDB、DBX、WDB、Ladebug、
JDB、XDB、Perl Debugger或Python Debugger的可視化圖形前端,其特有的圖形數(shù)據(jù)顯示功能(Graphical
Data Display)可以把數(shù)據(jù)結(jié)構(gòu)按照圖形的方式顯示出來。
DDD最初源于1990年Andreas Zeller編寫的VSL結(jié)構(gòu)化語言,后來經(jīng)過一些程序員的努力,演化成今天
的模樣。DDD的功能非常強(qiáng)大,可以調(diào)試用C/C++、Ada、Fortran、Pascal、Modula-2和Modula-3編寫的程
序;能以超文本方式瀏覽源代碼;能夠進(jìn)行斷點(diǎn)設(shè)置、回溯調(diào)試和歷史記錄;具有程序在終端運(yùn)行的仿真
窗口,具備在遠(yuǎn)程主機(jī)上進(jìn)行調(diào)試的能力;能夠顯示各種數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系,并將數(shù)據(jù)結(jié)構(gòu)以圖形形式
顯示;具有GDB/DBX/XDB的命令行界面,包括完整的文本編輯、歷史紀(jì)錄、搜尋引擎等。
DDD的主界面如圖21.1所示,它和Visual Studio等集成開發(fā)環(huán)境非常相近,而且DDD包含了Visual
Studio所不包含的部分功能。
圖21.1 DDD的主界面
在設(shè)計DDD的時候,設(shè)計人員決定把它與GDB之間的耦合度盡量降低。因為像GDB這樣的開源軟
件,更新的速度比商業(yè)軟件快,所以為了使GDB的變化不會影響到DDD,在DDD中,GDB是作為獨(dú)立的
進(jìn)程運(yùn)行的,通過命令行接口與DDD進(jìn)行交互。
圖21.2顯示了用戶、DDD、GDB和被調(diào)試進(jìn)程之間的關(guān)系,DDD和GDB之間的所有通信都是異步進(jìn)
行的。在DDD中發(fā)出的GDB命令都會與一個回調(diào)函數(shù)相連,放入命令隊列中。這個回調(diào)函數(shù)在合適的時
間會處理GDB的輸出。例如,如果用戶手動輸入一條GDB的命令,DDD就會把這條命令與顯示GDB輸出
的一個回調(diào)函數(shù)連起來。一旦GDB命令完成,就會觸發(fā)回調(diào)函數(shù),GDB的輸出就會顯示在DDD的命令窗
口中。
圖21.2 DDD運(yùn)行機(jī)理
DDD在事件循環(huán)時等待用戶輸入和GDB輸出,同時等著GDB進(jìn)入等待輸入狀態(tài)。當(dāng)GDB可用時,下
一條命令就會從命令隊列中取出,送給GDB。GDB到達(dá)的輸出由上次命令的回調(diào)函數(shù)過程來處理。這種
異步機(jī)制避免了DDD在等待GDB輸出時發(fā)生阻塞現(xiàn)象,到達(dá)的事件可以在任何時間得到處理。
不可否認(rèn)的是,DDD和GDB的分離使得DDD的運(yùn)行速度相對來說比較慢,但是這種方法帶來了靈活
性和兼容性的好處。例如,用戶可以把GDB調(diào)試器換成其他調(diào)試器,如DBX等。另外,GDB和DDD的分
離使得用戶可以在不同的機(jī)器上分別運(yùn)行GDB和DDD。
在DDD中,可以直接在底部的控制臺中輸入GDB命令,也可以通過菜單和鼠標(biāo)以圖形方式觸發(fā)GDB
命令的運(yùn)行,使用方法甚為簡單,因此這里不再贅述。
DDD不僅可用于調(diào)試PC上的應(yīng)用程序,也可調(diào)試目標(biāo)板子,方法是用如下命令啟動DDD(通過-
debugger選項指定一個針對ARM的GDB):
ddd --debugger arm-linux-gnueabihf-gdb <要調(diào)試的程序>
除了DDD以外,在Linux環(huán)境下,也可以使用廣受歡迎的Eclipse來編寫代碼并進(jìn)行調(diào)試。安裝Eclipse
IDE for C/C++Developer后,在Eclipse中,可以設(shè)置Using GDB(DSF)Manual Remote Debugging Launcher
以及ARM的GDB等,如圖21.3所示。
圖21.3 在Eclipse中設(shè)置Remote調(diào)試模式和GDB
21.2 Linux內(nèi)核調(diào)試
在嵌入式系統(tǒng)中,由于目標(biāo)機(jī)資源有限,因此往往在主機(jī)上先編譯好程序,再在目標(biāo)機(jī)上運(yùn)行。用戶
所有的開發(fā)工作都在主機(jī)開發(fā)環(huán)境下完成,包括編碼、編譯、連接、下載和調(diào)試等。目標(biāo)機(jī)和主機(jī)通過串
口、以太網(wǎng)、仿真器或其他通信手段通信,主機(jī)用這些接口控制目標(biāo)機(jī),調(diào)試目標(biāo)機(jī)上的程序。
調(diào)試嵌入式Linux內(nèi)核的方法如下。
1)目標(biāo)機(jī)“插樁”,如打上KGDB補(bǔ)丁,這樣主機(jī)上的GDB可與目標(biāo)機(jī)的KGDB通過串口或網(wǎng)口通
信。
2)使用仿真器,仿真器可直接連接目標(biāo)機(jī)的JTAG/BDM,這樣主機(jī)的GDB就可以通過與仿真器的通
信來控制目標(biāo)機(jī)。
3)在目標(biāo)板上通過printk()、Oops、strace等軟件方法進(jìn)行“觀察”調(diào)試,這些方法不具備查看和修
改數(shù)據(jù)結(jié)構(gòu)、斷點(diǎn)、單步等功能。
21.4~21.7節(jié)將對這些調(diào)試方法進(jìn)行一一講解。
不管是目標(biāo)機(jī)“插樁”還是使用仿真器連接目標(biāo)機(jī)JTAG/SWD/BDM,在主機(jī)上,調(diào)試工具一般都采用
GDB。
GDB可以直接把Linux內(nèi)核當(dāng)成一個整體來調(diào)試,這個過程實(shí)際上可以被QEMU模擬出來。進(jìn)入本書
配套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系統(tǒng)等待GDB遠(yuǎn)程連入。在終端1運(yùn)行新的./run-nolcd.sh,
這樣嵌入式ARM Linux的模擬平臺在1234端口偵聽。開一個新的終端2,進(jìn)入/home/baohua/develop/linux/,
執(zhí)行如下代碼:
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)
接下來我們遠(yuǎn)程連接127.0.0.1:1234
(gdb) target remote 127.0.0.1:1234
Remote debugging using 127.0.0.1:1234
0x60000000 in ?? ()
設(shè)置一個斷點(diǎn)到start_kernel()。
(gdb) b start_kernel
Breakpoint 1 at 0x805fd8ac: file init/main.c, line 490.
繼續(xù)運(yùn)行:
(gdb) c
Continuing.
Breakpoint 1, start_kernel () at init/main.c:490
490 {
(gdb)
斷點(diǎn)停在了內(nèi)核啟動過程中的start_kernel()函數(shù),這個時候我們按下Ctrl+X,A鍵,可以看到代
碼,如圖21.4所示。
進(jìn)一步,可以看看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調(diào)試內(nèi)核
盡管采用“插樁”和仿真器結(jié)合GDB的方式可以查看和修改數(shù)據(jù)結(jié)構(gòu)、斷點(diǎn)、單步等,而printk()這
種最原始的方法卻應(yīng)用得更廣泛。
printk()這種方法很原始,但是一般可以解決工程中95%以上的問題。因此具體何時打印,以及打
印什么東西,需要工程師逐步建立敏銳的嗅覺。加深對內(nèi)核的認(rèn)知,深入理解自己正在調(diào)試的模塊,這才
是快速解決問題的“王道”。工具只是一個輔助手段,無法代替工程師的思維。
工程師不能抱著得過且過的心態(tài),也不能總是一知半解地進(jìn)行低水平的重復(fù)建設(shè)。求知欲望對工程師
技術(shù)水平的提升有著最關(guān)鍵的作用。
21.3 內(nèi)核打印信息——printk()
在Linux中,內(nèi)核打印語句printk()會將內(nèi)核信息輸出到內(nèi)核信息緩沖區(qū)中,內(nèi)核緩沖區(qū)是在
kernel/printk.c中通過如下語句靜態(tài)定義的:
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
內(nèi)核信息緩沖區(qū)是一個環(huán)形緩沖區(qū)(Ring Buffer),因此,如果塞入的消息過多,則就會將之前的消
息沖刷掉。
printk()定義了8個消息級別,分為級別0~7,級別越低(數(shù)值越大),消息越不重要,第0級是緊急
事件級,第7級是調(diào)試級,代碼清單21.2所示為printk()的級別定義。
代碼清單21.2 printk()的級別定義
1 #define KERN_EMERG "<0>" /* 緊急事件,一般是系統(tǒng)崩潰之前提示的消息 */
2 #define KERN_ALERT "<1>" /* 必須立即采取行動 */
3 #define KERN_CRIT "<2>" /* 臨界狀態(tài),通常涉及嚴(yán)重的硬件或軟件操作失敗 */
4 #define KERN_ERR "<3>" /* 用于報告錯誤狀態(tài),設(shè)備驅(qū)動程序會
5 經(jīng)常使用KERN_ERR來報告來自硬件的問題 */
6 #define KERN_WARNING "<4>" /* 對可能出現(xiàn)問題的情況進(jìn)行警告,
7 這類情況通常不會對系統(tǒng)造成嚴(yán)重的問題 */
8 #define KERN_NOTICE "<5>" /* 有必要進(jìn)行提示的正常情形,
9 許多與安全相關(guān)的狀況用這個級別進(jìn)行匯報 */
10#define KERN_INFO "<6>" /* 內(nèi)核提示性信息,很多驅(qū)動程序
11 在啟動的時候,用這個級別打印出它們找到的硬件信息 */
12#define KERN_DEBUG "<7>" /* 用于調(diào)試信息 */
通過/proc/sys/kernel/printk文件可以調(diào)節(jié)printk()的輸出等級,該文件有4個數(shù)字值,如下所示。
·控制臺(一般是串口)日志級別:當(dāng)前的打印級別,優(yōu)先級高于該值的消息將被打印至控制臺。
·默認(rèn)的消息日志級別:將用該優(yōu)先級來打印沒有優(yōu)先級前綴的消息,也就是在直接寫printk(“xxx”)
而不帶打印級別的情況下,會使用該打印級別。
·最低的控制臺日志級別:控制臺日志級別可被設(shè)置的最小值(一般都是1)。
·默認(rèn)的控制臺日志級別:控制臺日志級別的默認(rèn)值。
如在Ubuntu PC上,/proc/sys/kernel/printk的值一般如下:
$ cat /proc/sys/kernel/printk
4 4 1 7
而我們通過如下命令可以使得Linux內(nèi)核的任何printk()都從控制臺輸出:
# echo 8 > /proc/sys/kernel/printk
在默認(rèn)情況下,DEBUG級別的消息不會從控制臺輸出,我們可以通過在bootargs中設(shè)置ignore_loglevel
來忽略打印級別,以保證所有消息都被打印到控制臺。在系統(tǒng)啟動后,用戶還可以通過
寫/sys/module/printk/parameters/ignore_loglevel文件動態(tài)來設(shè)置是否忽略打印級別。
要注意的是,/proc/sys/kernel/printk并不控制內(nèi)核消息進(jìn)入__log_buf的門檻,因此無論消息級別是多
少,都會進(jìn)入__log_buf中,但是最終只有高于當(dāng)前打印級別的內(nèi)核消息才會從控制臺打印。
用戶可以通過dmesg命令查看內(nèi)核打印緩沖區(qū),而如果使用dmesg-c命令,則不僅會顯示__log_buf,還
會清除該緩沖區(qū)的內(nèi)容。也可以使用cat/proc/kmsg命令來顯示內(nèi)核信息。/proc/kmsg是一個“永無休止的文
件”,因此,cat/proc/kmsg的進(jìn)程只能通過“Ctrl+C”或kill終止。
在設(shè)備驅(qū)動中,經(jīng)常需要輸出調(diào)試或系統(tǒng)信息,盡管可以直接采用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調(diào)用的所有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打印
的時候,設(shè)備名稱會被自動加到打印消息的前頭。
代碼清單21.4 包含設(shè)備信息的可替代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()調(diào)用所在的函數(shù)名,可以使用__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對應(yīng)內(nèi)核的Kernel low-level debugging功能,EARLY_PRINTK則對應(yīng)內(nèi)核中一個早期的控
制臺。為了在內(nèi)核的drivers/tty/serial下的控制臺驅(qū)動初始化之前支持打印,可以選擇DEBUG_LL和
EARLY_PRINTK這兩個配置選項。另外,也需要在bootargs中設(shè)置earlyprintk的選項。
對于LDDD3_vexpress而言,沒有DEBUG_LL和EARLY_PRINTK的時候,我們看到的內(nèi)核最早的打印
是:
Booting Linux on physical CPU 0x0
Initializing cgroup subsys cpuset
Linux version …
如果我們使能DEBUG_LL和EARLY_PRINTK,選擇如圖21.5所示的“Use PL011UART0at
0x10009000(V2P-CA9core tile)”這個低級別調(diào)試口,并在bootargs中設(shè)置earlyprintk,則我們看到了更早
的打印信息:
Uncompressing Linux... done, booting the kernel.
圖21.5 選擇低級別調(diào)試UART
21.5 使用“/proc”
在Linux系統(tǒng)中,“/proc”文件系統(tǒng)十分有用,它被內(nèi)核用于向用戶導(dǎo)出信息。“/proc”文件系統(tǒng)是一個
虛擬文件系統(tǒng),通過它可以在Linux內(nèi)核空間和用戶空間之間進(jìn)行通信。在/proc文件系統(tǒng)中,我們可以將
對虛擬文件的讀寫作為與內(nèi)核中實(shí)體進(jìn)行通信的一種手段,與普通文件不同的是,這些虛擬文件的內(nèi)容都
是動態(tài)創(chuàng)建的。
“/proc”下的絕大多數(shù)文件是只讀的,以顯示內(nèi)核信息為主。但是“/proc”下的文件也并不是完全只讀
的,若節(jié)點(diǎn)可寫,還可用于一定的控制或配置目的,例如前面介紹的寫/proc/sys/kernel/printk可以改變
printk()的打印級別。
Linux系統(tǒng)的許多命令本身都是通過分析“/proc”下的文件來完成的,如ps、top、uptime和free等。例
如,free命令通過分析/proc/meminfo文件得到可用內(nèi)存信息,下面顯示了對應(yīng)的meminfo文件和free命令的
結(jié)果。
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以及之前的內(nèi)核版本中,可用如下函數(shù)創(chuàng)建“/proc”節(jié)點(diǎn):
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()函數(shù)用于創(chuàng)建“/proc”節(jié)點(diǎn),而create_proc_read_entry()調(diào)用
create_proc_entry()創(chuàng)建只讀的“/proc”節(jié)點(diǎn)。參數(shù)name為“/proc”節(jié)點(diǎn)的名稱,parent/base為父目錄的節(jié)
點(diǎn),如果為NULL,則指“/proc”目錄,read_proc是“/proc”節(jié)點(diǎn)的讀函數(shù)指針。當(dāng)read()系統(tǒng)調(diào)用
在“/proc”文件系統(tǒng)中執(zhí)行時,它映像到一個數(shù)據(jù)產(chǎn)生函數(shù),而不是一個數(shù)據(jù)獲取函數(shù)。
下列函數(shù)用于創(chuàng)建“/proc”目錄:
struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent);
結(jié)合create_proc_entry()和proc_mkdir(),代碼清單21.5中的程序可用于先在/proc下創(chuàng)建一個目錄
procfs_example,而后在該目錄下創(chuàng)建一個文件example_file。
代碼清單21.5 proc_mkdir()和create_proc_entry()函數(shù)使用范例
1/* 創(chuàng)建/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/* 創(chuàng)建一個/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;
作為上述函數(shù)返回值的proc_dir_entry結(jié)構(gòu)體包含了“/proc”節(jié)點(diǎn)的讀函數(shù)指針
(read_proc_t*read_proc)、寫函數(shù)指針(write_proc_t*write_proc)以及父節(jié)點(diǎn)、子節(jié)點(diǎn)信息等。
/proc節(jié)點(diǎn)的讀寫函數(shù)的類型分別為:
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);
讀函數(shù)中page指針指向用于寫入數(shù)據(jù)的緩沖區(qū),start用于返回實(shí)際的數(shù)據(jù)并寫到內(nèi)存頁的位置,eof是
用于返回讀結(jié)束標(biāo)志,offset是讀的偏移,count是要讀的數(shù)據(jù)長度。start參數(shù)比較復(fù)雜,對于/proc只包含
簡單數(shù)據(jù)的情況,通常不需要在讀函數(shù)中設(shè)置*start,這意味著內(nèi)核將認(rèn)為數(shù)據(jù)保存在內(nèi)存頁偏移0的地
方。
寫函數(shù)與file_operations中的write()成員函數(shù)類似,需要一次從用戶緩沖區(qū)到內(nèi)存空間的復(fù)制過程。
在Linux系統(tǒng)中可用如下函數(shù)刪除/proc節(jié)點(diǎn):
void remove_proc_entry(const char *name, struct proc_dir_entry *parent);
在Linux系統(tǒng)中已經(jīng)定義好的可使用的/proc節(jié)點(diǎn)宏包括:proc_root_fs(/proc)、
proc_net(/proc/net)、proc_bus(/proc/bus)、proc_root_driver(/proc/driver)等,proc_root_fs實(shí)際上就是
NULL。
代碼清單21.6所示為一個簡單的“/proc”文件系統(tǒng)使用范例,這段代碼在模塊加載函數(shù)中創(chuàng)
建/proc/test_dir目錄,并在該目錄中創(chuàng)建/proc/test_dir/test_rw文件節(jié)點(diǎn),在模塊卸載函數(shù)中撤銷“/proc”節(jié)
點(diǎn),而/proc/test_dir/test_rw文件中只保存了一個32位的整數(shù)。
代碼清單21.6 /proc文件系統(tǒng)使用范例
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 <baohua@kernel.org>");
52MODULE_DESCRIPTION("proc exmaple");
53MODULE_LICENSE("GPL v2");
上述代碼第21行調(diào)用的simple_strtoul()用于將用戶輸入的字符串轉(zhuǎn)換為無符號長整數(shù),第3個參數(shù)
10意味著轉(zhuǎn)化方式是十進(jìn)制。
編譯上述簡單的proc.c為proc.ko,運(yùn)行insmod proc.ko加載該模塊后,“/proc”目錄下將多出一個目錄
test_dir,該目錄下包含一個test_rw,ls–l的結(jié)果如下:
$ 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
說明我們上一步執(zhí)行的寫操作是正確的。
在Linux 3.10及以后的版本中,“/proc”的內(nèi)核API和實(shí)現(xiàn)架構(gòu)變更較大,create_proc_entry()、
create_proc_read_entry()之類的API都被刪除了,取而代之的是直接使用proc_create()、
proc_create_data()API。同時,也不再存在read_proc()、write_proc()之類的針對proc_dir_entry的成
員函數(shù)了,而是直接把file_operations結(jié)構(gòu)體的指針傳入proc_create()或者proc_create_data()函數(shù)中,
其原型為:
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以前的內(nèi)核和Linux3.10以后的內(nèi)核。改造結(jié)果
如代碼清單21.7所示。#if LINUX_VERSION_CODE<KERNEL_VERSION(3,10,0)中的部分是舊版本
的代碼,與21.6相同,所以省略了。
代碼清單21.7 支持Linux 3.10以后內(nèi)核的/proc文件系統(tǒng)使用范例
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
當(dāng)內(nèi)核出現(xiàn)類似用戶空間的Segmentation Fault時(例如內(nèi)核訪問一個并不存在的虛擬地址),Oops會
被打印到控制臺和寫入內(nèi)核log緩沖區(qū)。
我們在globalmem.c的globalmem_read()函數(shù)中加上下面一行代碼:
} else {
*ppos += count;
ret = count;
*(unsigned int *)0 = 1; /* a kernel panic */
printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
}
假設(shè)這個字符設(shè)備對應(yīng)的設(shè)備節(jié)點(diǎn)是/dev/globalmem,通過cat/dev/globalmem命令讀設(shè)備文件,將得到
如下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這一行代碼也比較關(guān)鍵,給出了“事發(fā)現(xiàn)場”,即globalmem_read()函數(shù)偏移
0xbc字節(jié)的指令處。
通過反匯編globalmem.o可以尋找到globalmem_read()函數(shù)開頭位置偏移0xbc的指令,反匯編方法如
下:
drivers/char/globalmem$ arm-linux-gnueabihf-objdump -d -S globalmem.o
對應(yīng)的反匯編代碼如下,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的指令。這里僅僅給出了一個例子,工程實(shí)踐中的“事發(fā)現(xiàn)場”并不全那么容
易找到,但方法都是類似的。
21.7 BUG_ON()和WARN_ON()
內(nèi)核中有許多地方調(diào)用類似BUG()的語句,它非常像一個內(nèi)核運(yùn)行時的斷言,意味著本來不該執(zhí)
行到BUG()這條語句,一旦執(zhí)行即拋出Oops。BUG()的定義為:
#define BUG() do { \
printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __func__); \
panic("BUG!"); \
} while (0)
其中的panic()定義在kernel/panic.c中,會導(dǎo)致內(nèi)核崩潰,并打印Oops。比如arch/arm/kernel/dma.c中的
enable_dma()函數(shù):
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不成立的情況下,驅(qū)動直接調(diào)用了enable_dma(),實(shí)際上意味
著內(nèi)核的一個bug。
BUG()還有一個變體叫BUG_ON(),它的內(nèi)部會引用BUG(),形式為:
#define BUG_ON(condition) do { if (unlikely(condition)) BUG(); } while (0)
對于BUG_ON()而言,只有當(dāng)括號內(nèi)的條件成立的時候,才拋出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()外,內(nèi)核有個稍微弱一些WARN_ON(),在括號中的條件成立的時候,內(nèi)核會拋
出棧回溯,但是不會panic(),這通常用于內(nèi)核拋出一個警告,暗示某種不太合理的事情發(fā)生了。如在
kernel/locking/mutex-debug.c中的debug_mutex_unlock()函數(shù)發(fā)現(xiàn)mutex_unlock()的調(diào)用者和
mutex_lock()的調(diào)用者不是同一個線程的時候或者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()也可以作為一個調(diào)試技巧。比如,我們進(jìn)到內(nèi)核某個函數(shù)后,不知道這個函
數(shù)是怎么一級一級被調(diào)用進(jìn)來的,那可以在該函數(shù)中加入一個WARN_ON(1)。
21.8 strace
在Linux系統(tǒng)中,strace是一種相當(dāng)有效的跟蹤工具,它的主要特點(diǎn)是可以被用來監(jiān)視系統(tǒng)調(diào)用。我們
不僅可以用strace調(diào)試一個新開始的程序,也可以調(diào)試一個已經(jīng)在運(yùn)行的程序(這意味著把strace綁定到一
個已有的PID上)。對于第6章的globalmem字符設(shè)備文件,以strace方式運(yùn)行如代碼清單21.8所示的用戶空
間應(yīng)用程序globalmem_test,運(yùn)行的結(jié)果如下:
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 /* 向標(biāo)準(zhǔn)輸出設(shè)備(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
...
輸出的每一行對應(yīng)一次Linux系統(tǒng)調(diào)用,其格式為“左邊=右邊”,等號左邊是系統(tǒng)調(diào)用的函數(shù)名及其參
數(shù),右邊是該調(diào)用的返回值。
代碼清單21.8 用戶空間應(yīng)用程序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雖然無法直接追蹤到設(shè)備驅(qū)動中的函數(shù),但是足以幫助工程師進(jìn)行推演,如從
open(“/dev/globalmem”,O_RDWR)=3的返回結(jié)果知道/dev/globalmem的fd為3,之后對fd為3的文件進(jìn)行
read()、write()和ioctl()系統(tǒng)調(diào)用,最終會使globalmem里file_operations中的相應(yīng)函數(shù)被調(diào)用,通過
系統(tǒng)調(diào)用的結(jié)果就可以知道驅(qū)動中g(shù)lobalmem_read()、globalmem_write()和globalmem_ioctl()的運(yùn)
行結(jié)果。
21.9 KGDB
Linux直接提供了對KGDB的支持,KGDB采用了典型的嵌入式系統(tǒng)“插樁”技巧,一般依賴于串口與調(diào)
試主機(jī)通信。為了支持KGDB,串口驅(qū)動應(yīng)該實(shí)現(xiàn)純粹的輪詢收發(fā)單一字符的成員函數(shù),以供
drivers/tty/serial/kgdboc.c調(diào)用,譬如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
};
在編譯內(nèi)核時,運(yùn)行make ARCH=arm menuconfig時需選擇關(guān)于KGDB的編譯項目,如圖21.6所示。
圖21.6 KGDB編譯選項配置
對于目標(biāo)板而言,需要在bootargs中設(shè)置與KGDB對應(yīng)的串口等信息,如kgdboc=ttyS0,
115200kgdbcon。
如果想一開機(jī)內(nèi)核就直接進(jìn)入等待GDB連接的調(diào)試狀態(tài),可以在bootargs中設(shè)置kgdbwait,kgdbwait的
含義是啟動時就等待主機(jī)的GDB連接。而若想在內(nèi)核啟動后進(jìn)入GDB調(diào)試模式,可運(yùn)行echo
g>/proc/sysrq_trigger命令給內(nèi)核傳入一個鍵值是g的magic_sysrq。
在調(diào)試PC上,依次運(yùn)行如下命令就可以啟動調(diào)試并連接至目標(biāo)機(jī)(假設(shè)串口在PC上對應(yīng)的設(shè)備節(jié)點(diǎn)
是/dev/ttyS0):
# arm-eabi-gdb ./vmlinux
(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyS0 //連接目標(biāo)機(jī)
(gdb)
之后,在主機(jī)上,我們可以使用GDB像調(diào)試應(yīng)用程序一樣調(diào)試使能了KGDB的目標(biāo)機(jī)上的內(nèi)核。
21.10 使用仿真器調(diào)試內(nèi)核
在ARM Linux領(lǐng)域,目前比較主流的是采用ARM DS-5Development Studio方案。ARM DS-5是一個針
對基于Linux的系統(tǒng)和裸機(jī)嵌入式系統(tǒng)的專業(yè)軟件開發(fā)解決方案,它涵蓋了開發(fā)的所有階段,從啟動代
碼、內(nèi)核移植直到應(yīng)用程序調(diào)試、分析。如圖21.7所示,它使用了DSTREAM高性能仿真器(ARM已經(jīng)停
止更新RVI-RVT2仿真器),在Eclipse內(nèi)包含了DS-5和DSTREAM的開發(fā)插件。
調(diào)試主機(jī)一般通過網(wǎng)線與DSTREAM仿真器連接,而仿真器則連接與電路板類似的JTAG接口,之后
用DS-5調(diào)試器進(jìn)行調(diào)試。DS-5圖形化調(diào)試器提供了全面和直觀的調(diào)試圖,非常易于調(diào)試Linux和裸機(jī)程
序,易于查看代碼,進(jìn)行棧回溯,查看內(nèi)存、寄存器、表達(dá)式、變量,分析內(nèi)核線程,設(shè)置斷點(diǎn)。
圖21.7 DSTREAM仿真器和DS-5開發(fā)環(huán)境
值得一提的是,DS-5也提供了Streamline Performance Analyzer。ARM Streamline性能分析器(見圖
21.8)為軟件開發(fā)人員提供了一種用來分析和優(yōu)化在ARM926、ARM11和Cortex-A系列平臺上運(yùn)行的Linux
和Android系統(tǒng)的直觀方法。使用Streamline,Linux內(nèi)核中需包含一個gator模塊,用戶空間則需要使能
gatord后臺服務(wù)器程序。關(guān)于Streamline具體的操作方法可以查看《ARM? DS-5Using ARM Streamline》。
圖21.8 ARMStreamline性能分析器
21.11 應(yīng)用程序調(diào)試
在嵌入式系統(tǒng)中,為調(diào)試Linux應(yīng)用程序,可在目標(biāo)板上先運(yùn)行GDBServer,再讓主機(jī)上的GDB與目
標(biāo)板上的GDBServer通過網(wǎng)口或串口通信。
1.目標(biāo)板
需要運(yùn)行如下命令啟動GDBServer:
gdbserver <host_ip>:<port> <app>
<host_ip>:<port>為主機(jī)的IP地址和端口,app是可執(zhí)行的應(yīng)用程序名。
當(dāng)然,也可以用系統(tǒng)中空閑的串口作為GDB調(diào)試器和GDBServer的底層通信手段,如:
gdbserver/dev/ttyS0./tdemo
2.主機(jī)
需要先運(yùn)行如下命令啟動GDB:
arm-eabi-gdb <app>
app與GDBServer的app參數(shù)對應(yīng)。
之后,運(yùn)行如下命令就可以連接目標(biāo)板:
target remote <target_ip>:<port>
<target_ip>:<port>為目標(biāo)機(jī)的IP地址和端口。
如果目標(biāo)板上的GDBServer使用串口,則在宿主機(jī)上GDB也應(yīng)該使用串口,如:
(gdb)target remote/dev/ttyS1
之后,便可以使用GDB像調(diào)試本機(jī)上的程序一樣調(diào)試目標(biāo)機(jī)上的程序。
3.通過GDB server和ARM GDB調(diào)試應(yīng)用程序
在ARM開發(fā)板上放置GDB server,便可以通過目標(biāo)板與調(diào)試PC之間的以太網(wǎng)等調(diào)試。要調(diào)試的應(yīng)用
程序的源代碼如下:
/*
* 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
將程序下載到目標(biāo)板后,在目標(biāo)板上運(yùn)行:
# gdbserver 192.168.1.20:1234 gdb_example
Process gdb_example created; pid = 1096
Listening on port 1234
其中192.168.1.20為目標(biāo)板的3IP,1234為GDBserver的偵聽端口。
如果目標(biāo)機(jī)是Android系統(tǒng),且沒有以太網(wǎng),可以嘗試使用adb forward功能,比如adb forward tcp:
1234tcp:1234是把目標(biāo)機(jī)1234端口與主機(jī)1234端口進(jìn)行轉(zhuǎn)發(fā)。
在主機(jī)上運(yùn)行:
$ arm-eabi-gdb gdb_example…
主機(jī)的GDB中運(yùn)行如下命令以連接目標(biāo)板:
(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地址可以去掉,因為它變
成直接連接本機(jī)了,可直接寫成target remote:1234。
運(yùn)行如下命令將斷點(diǎn)設(shè)置在increase_one(&dat);這一行:
(gdb) b gdb_example.c:16
Breakpoint 1 at 0x8390: file gdb_example.c, line 16.
通過c命令繼續(xù)運(yùn)行目標(biāo)板上的程序,發(fā)生斷點(diǎn):
(gdb) c
Continuing.
...
Breakpoint 1, main (argc=1, argv=0xbead4eb4) at gdb_example.c:16
16increase_one(&dat);
運(yùn)行n命令執(zhí)行完increase_one(&dat);再查看dat的值:
(gdb) n
19increase_one(p); (gdb) p dat
$1 = 1
發(fā)現(xiàn)dat變成1。繼續(xù)運(yùn)行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命令可以查看當(dāng)時的寄存器值:
(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性能監(jiān)控與調(diào)優(yōu)工具
除了保證程序的正確性以外,在項目開發(fā)中往往還關(guān)心性能和穩(wěn)定性。這時候,我們往往要對內(nèi)核、
應(yīng)用程序或整個系統(tǒng)進(jìn)行性能優(yōu)化。在性能優(yōu)化中常用的手段如下。
1.使用top、vmstat、iostat、sysctl等常用工具
top命令用于顯示處理器的活動狀況。在缺省情況下,顯示占用CPU最多的任務(wù),并且每隔5s做一次
刷新;vmstat命令用于報告關(guān)于內(nèi)核線程、虛擬內(nèi)存、磁盤、陷阱和CPU活動的統(tǒng)計信息;iostat命令用于
分析各個磁盤的傳輸閑忙狀況;netstat是用來檢測網(wǎng)絡(luò)信息的工具;sar用于收集、報告或者保存系統(tǒng)活動
信息,其中,sar用于顯示數(shù)據(jù),sar1和sar2用于收集和保存數(shù)據(jù)。
sysctl是一個可用于改變正在運(yùn)行中的Linux系統(tǒng)的接口。用sysctl可以讀取幾百個以上的系統(tǒng)變量,例
如用sysctl–a可讀取所有變量。
sysctl的實(shí)現(xiàn)原理是:所有的內(nèi)核參數(shù)在/proc/sys中形成一個樹狀結(jié)構(gòu),sysctl系統(tǒng)調(diào)用的內(nèi)核函數(shù)是
sys_sysctl,匹配項目后,最后的讀寫在do_sysctl_strategy中完成,如
echo "1" > /proc/sys/net/ipv4/ip_forward
就等價于:
sysctl –w net.ipv4.ip_forward ="1"
2.使用高級分析手段,如OProfile、gprof
OProfile可以幫助用戶識別諸如模塊的占用時間、循環(huán)的展開、高速緩存的使用率低、低效的類型轉(zhuǎn)
換和冗余操作、錯誤預(yù)測轉(zhuǎn)移等問題。它收集有關(guān)處理器事件的信息,其中包括TLB的故障、停機(jī)、存儲
器訪問以及緩存命中和未命中的指令的攫取數(shù)量。
OProfile支持兩種采樣方式:基于事件的采樣(Event Based)和基于時間的采樣(Time Based)。基于
事件的采樣是OProfile只記錄特定事件(比如L2緩存未命中)的發(fā)生次數(shù),當(dāng)達(dá)到用戶設(shè)定的定值時
Oprofile就記錄一下(采一個樣)。這種方式需要CPU內(nèi)部有性能計數(shù)器(Performace Counter)。基于時
間的采樣是OProfile借助OS時鐘中斷的機(jī)制,在每個時鐘中斷,OProfile都會記錄一次(采一次樣)。引
入它的目的在于,提供對沒有性能計數(shù)器的CPU的支持,其精度相對于基于事件的采樣要低,因為要借助
OS時鐘中斷的支持,對于禁用中斷的代碼,OProfile不能對其進(jìn)行分析。
OProfile在Linux上分兩部分,一個是內(nèi)核模塊(oprofile.ko),另一個是用戶空間的守護(hù)進(jìn)程
(oprofiled)。前者負(fù)責(zé)訪問性能計數(shù)器或者注冊基于時間采樣的函數(shù),并將采樣值置于內(nèi)核的緩沖區(qū)
內(nèi)。后者在后臺運(yùn)行,負(fù)責(zé)從內(nèi)核空間收集數(shù)據(jù),寫入文件。其運(yùn)行步驟如下。
1)初始化opcontrol--init
2)配置opcontrol--setup--event=...
3)啟動opcontrol--start
4)運(yùn)行待分析的程序xxx
5)取出數(shù)據(jù)
opcontrol--dump
opcontrol--stop
6)分析結(jié)果opreport-l./xxx
用GNU gprof可以打印出程序運(yùn)行中各個函數(shù)消耗的時間,以幫助程序員找出眾多函數(shù)中耗時最多的
函數(shù);還可產(chǎn)生程序運(yùn)行時的函數(shù)調(diào)用關(guān)系,包括調(diào)用次數(shù),以幫助程序員分析程序的運(yùn)行流程。
GNU gprof的實(shí)現(xiàn)原理:在編譯和鏈接程序的時候(使用-pg編譯和鏈接選項),gcc在應(yīng)用程序的每
個函數(shù)中都加入名為mcount(_mcount或__mcount,依賴于編譯器或操作系統(tǒng))的函數(shù),也就是說應(yīng)用程
序里的每一個函數(shù)都會調(diào)用mcount,而mcount會在內(nèi)存中保存一張函數(shù)調(diào)用圖,并通過函數(shù)調(diào)用堆棧的形
式查找子函數(shù)和父函數(shù)的地址。這張調(diào)用圖也保存了所有與函數(shù)相關(guān)的調(diào)用時間、調(diào)用次數(shù)等的所有信
息。
GNU gprof的基本用法如下。
1)使用-pg編譯和鏈接應(yīng)用程序。
2)執(zhí)行應(yīng)用程序并使它生成供gprof分析的數(shù)據(jù)。
3)使用gprof程序分析應(yīng)用程序生成的數(shù)據(jù)。
3.進(jìn)行內(nèi)核跟蹤,如LTTng
LTTng(Linux Trace Toolkit-next generation,官方網(wǎng)站為http://lttng.org/)是一個用于跟蹤系統(tǒng)詳細(xì)運(yùn)行
狀態(tài)和流程的工具,它可以跟蹤記錄系統(tǒng)中的特定事件。這些事件包括:系統(tǒng)調(diào)用的進(jìn)入和退出;陷阱/
中斷(Trap/Irq)的進(jìn)入和退出;進(jìn)程調(diào)度事件;內(nèi)核定時器;進(jìn)程管理相關(guān)事件——創(chuàng)建、喚醒、信號
處理等;文件系統(tǒng)相關(guān)事件——open/read/write/seek/ioctl等;內(nèi)存管理相關(guān)事件——內(nèi)存分配/釋放等;其
他IPC/套接字/網(wǎng)絡(luò)等事件。而對于這些記錄,我們可以通過圖形的方式經(jīng)由lttv-gui查看,如圖21.9所示。
4.使用LTP進(jìn)行壓力測試
LTP(Linux Test Project,官方網(wǎng)站為http://ltp.sourceforge.net/)是一個由SGI發(fā)起并由IBM負(fù)責(zé)維護(hù)的
合作計劃。它的目的是為開源社區(qū)提供測試套件來驗證Linux的可靠性、健壯性和穩(wěn)定性。它通過壓力測
試來判斷系統(tǒng)的穩(wěn)定性和可靠性,在工程中我們可使用LTP測試套件對Linux操作系統(tǒng)進(jìn)行超長時間的測
試,它可進(jìn)行文件系統(tǒng)壓力測試、硬盤I/O測試、內(nèi)存管理壓力測試、IPC壓力測試、SCHED測試、命令
功能的驗證測試、系統(tǒng)調(diào)用功能的驗證測試等。
圖21.9 LTTng形成的時序圖
5.使用Benchmark評估系統(tǒng)
可用于Linux的Benchmark的包括lmbench、UnixBench、AIM9、Netperf、SSLperf、dbench、Bonnie、
Bonnie++、Iozone、BYTEmark等,它們可用于評估操作系統(tǒng)、網(wǎng)絡(luò)、I/O子系統(tǒng)、CPU等的性能,參考網(wǎng)
址http://lbs.sourceforge.net/列出了許多Benchmark工具。
21.13 總結(jié)
Linux程序的調(diào)試,尤其是內(nèi)核的調(diào)試看起來比較復(fù)雜,沒有類似于VC++、Tornado的IDE開發(fā)環(huán)境,
最常用的調(diào)試手段依然是文本方式的GDB。文本方式的GDB調(diào)試器功能異常強(qiáng)大,當(dāng)我們使用習(xí)慣后,
就會用得非常自然。
Linux內(nèi)核驅(qū)動的調(diào)試方法包括“插樁”、使用仿真器和借助printk()、Oops、strace等,在大多數(shù)情況
下,原始的printk()仍然是最有效的手段。
除了本章介紹的方法外,在驅(qū)動的調(diào)試中很可能還會借助其他的硬件或軟件調(diào)試工具,如調(diào)試USB驅(qū)
動最好借助USB分析儀,用USB分析儀將可捕獲USB通信中的數(shù)據(jù)包,如同網(wǎng)絡(luò)中的Sniffer軟件一樣。

總結(jié)

以上是生活随笔為你收集整理的linux驱动程序设计21 Linux设备驱动的调试的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。