鞏 琛 蔡 文
(上海師范大學信息與機電工程學院 上海 200234)
?
基于ARM的Linux驅動調試技術研究
鞏琛蔡文
(上海師范大學信息與機電工程學院上海 200234)
在ARM上進行Linux驅動移植時,要對Linux內核代碼進行修改、刪減或添加,但這樣做在運行時可能會遇到很多意想不到的錯誤,這時就需要去調試代碼以找到出錯的原因和位置。針對這一需要,提出并實現兩種新的調試技術:第一種構造一個打印函數,把添加的打印信息單獨存儲,然后借助proc文件系統將其輸出,實現了外加打印信息與內核自身打印信息的分離,使查找更加方便;其次利用系統時鐘中斷永不停息的特性確定系統僵死的位置。通過實驗表明,該技術能快速有效地找到死循環的位置,省去了大量查找和分析代碼的工作。
Linux調試proc文件系統系統時鐘中斷
如今,以ARM為核心處理器,搭載Linux操作系統的嵌入式產品越來越多,應用場景越來越廣泛[1]。但由于硬件平臺的多樣性,并沒有一套通用的Linux操作系統。開發相應的嵌入式產品時,需要工程師根據硬件平臺外圍設備的特性對Linux內核源碼進行增減。這樣一個移植過程中會出現各種各樣的bug,此時就需要開發者運用各種調試手段去找到出錯的地方和原因。本文基于Linux+ARM的實驗平臺(Linux選取2.6.31.14的版本;ARM架構芯片采用三星公司的s3c2440;BootLoader采用U-boot,根文件系統用Busybox創建[2,3]),提出并實現兩種新的驅動調試技術。文中涉及到的交叉開發方面的知識不作詳述。
1.1整體思想概述
在調試Linux驅動時在程序里用printk()添加打印語句是最常用的方法[4]。首先,Linux內核會在內核空間分配一段靜態緩沖區,作為顯示用的空間。然后調用sprintf,格式化顯示字符串,最后調用tty_write向終端進行信息的顯示。執行dmesg命令,就可把緩沖區里面的打印信息重新顯示在硬件終端上。dmesg命令的實質是打開/proc/kmsg文件。
Linux內核源碼多用printk()打印信息,而開發人員在調試驅動時也常常會用printk()添加調試信息。當開發者想回頭查看自己添加的打印信息時,用簡單的cat命令去打開 /proc/kmsg文件即可。但此時顯示的是與內核打印信息混雜在一起的結果,查找起來很不方便。cat命令的原理是先執行open系統調用打開某個文件,然后再在一個while()里用read系統調用循環地讀取該文件里面的內容,然后把內容顯示到硬件輸出端。cat命令的機制原理不是本文的重點,這里只是簡單介紹一下,便于后面代碼的理解。
基于此問題,本文構造出一個私有打印函數pvt_printk(),把自己添加的打印語句單獨存儲到一段緩沖區pvt_buf里面去。然后利用內核的proc機制創建一個與kmsg類似的文件pvt_kmsg。當去執行cat/proc/pvt_kmsg時,可把pvt_buf緩沖區里面的數據顯示出來。
1.2proc文件系統
本調試技術用到了Linux內核的proc虛擬文件系統,它是一種在用戶態檢查內核狀態的機制。里面的內容是內核動態生成的。它的存在主要是想通過這樣一種渠道把內核的一些狀態信息反饋給用戶,讓用戶能夠了解到內核運行的一些狀況[5]。
Linux內核當中使用structproc_dir_entry結構體來表示和描述/proc目錄下的一個文件或者目錄。如下:
structproc_dir_entry
{
…… ……
conststructfile_operations*proc_fops;
…… ……
}
這個結構里面成員比較多,其他的不用太關心,這里著重介紹file_operations這個結構體,它與對proc文件的具體操作相對應[5]。Linux內核當中是用create_proc_entry這個函數去創建一個proc文件,返回值為一個proc_dir_entry結構體指針。
structproc_dir_entry*create_proc_entry(constchar
*name,mode_tmode,structproc_dir_entry*parent)
name: 要創建的文件名。
mode: 要創建的文件屬性。
parent: 這個文件的父目錄。如果為NULL,便直接在proc的根目錄下面去創建文件。
file_operations這個結構體的成員也很多,本文只用到open、read。
structfile_operations
{
……;
ssize_t(*read) (structfile*,char__user*,size_t,loff_t*);
ssize_t(*write) (structfile*,constchar__user*,size_t,loff_t*);
int(*open) (structinode*,structfile*);
……;
}
當應用程序(本實驗就是cat命令)對一個proc文件執行open、read、write系統調用時[6],程序會通過SWI指令,最終調用到與此文件相對應的file_operations結構體里面的open、read、write函數。整個過程類似于一個典型的字符型設備驅動[7],/proc目錄下的文件有點像字符型驅動里面的設備節點。
1.3實現過程及步驟
創建proc-printk.c文件作為該實驗的驅動源碼文件。經前面分析,用到proc文件的描述結構體proc_dir_entry和創建函數create_proc_entry(),所以程序除了添加字符型驅動所需的頭文件,還需要包含Linux/proc_fs.h。
在驅動初始化函數里利用create_proc_entry()去創建一個proc_dir_entry結構體指針pvt_entry,并將其設為全局變量。如果創建成功,把file_operations結構體proc_pvt_entry_operations的指針賦給pvt_entry所指結構體里面的proc_fops成員。
structproc_dir_entry*pvt_entry;
staticintpvt_kmsg_init(void)
{
pvt_entry=create_proc_entry("pvt_kmsg",S_IRUSR,NULL);
if(pvt_entry)
pvt_entry->proc_fops=&proc_pvt_kmsg_operations;
return0;
}
S_IRUSR是Linux內核定義的宏,作用是讓創建的文件的屬性為只讀。
staticvoidpvt_kmsg_exit(void)
{
remove_proc_entry("pvt_kmsg",NULL);/*在出口函數做相應的移除*/
}
module_init(pvt_kmsg_init);
module_exit(pvt_kmsg_exit);
MODULE_LICENSE("GPL");
接著構造并填充file_operations結構體proc_pvt_kmsg_operations。
structfile_operationsproc_pvt_kmsg_operations=
{
.read=pvt_kmsg_read,
.open=pvt_kmsg_open,
};
先實現pvt_kmsg_read函數,pvt_kmsg_open后面再說。
staticssize_tpvt_kmsg_read(structfile*file,char__user*buf,size_tcount,loff_t*ppos)
{
inti,ret;
charc;
//文件以非阻塞方式打開并且pvt_buf為空,立刻返回錯誤
if((file->f_flags&O_NONBLOCK) &&empty())
return-EAGAIN;
//如果文件以阻塞方式打開又為空,用wait_event_interruptible()讓此進程在queue等待隊列上睡眠[8]
//如果文件以阻塞方式打開有數據,用__put_user把數據讀到用戶空間
if(!wait_event_interruptible(queue,!empty()))
{
for(i=0;i //count是要讀的字節數 { if(read_pvt_buf(&c)) { ret=__put_user(c,buf); //返回值:0表成功,-EFAULT表錯誤 buf++; } } } returnret; } 上述代碼用等待隊列來實現阻塞操作,所以需在文件開頭用宏DECLARE_QUEUEEUE_HEAD(queue)去定義并初始化一個等待隊列queue。 定義一個私有的存儲區域staticcharpvt_buf[2048],對它的存貯讀取方式采用數組環形緩沖區。在本設計里只有自定義的打印函數pvt_printk()往里寫數據,只有cat應用程序從里讀數據。在這樣僅有一個讀用戶和一個寫用戶的情況下,使用環形緩沖區的好處是可以不用添加互斥[9]保護機制就能保證數據的正確性。下面是對環形緩沖區空、滿判斷函數和讀寫函數的實現。 staticcharpvt_buf[2048]; staticintread_p; staticintwrite_p= 0; staticintreadstart_p=0; //每次讀的起始位置 staticintempty() { if(read_p==write_p)return1; elsereturn0; } staticintfull() { if((write_p+ 1)%2048 ==readstart_p)return1; elsereturn0; } staticvoidwrite_pvt_buf(charc) { /*如果數據滿了移動一位讀的起始位,丟棄一個數據 */ if(full()) readstart_p= (readstart_p+ 1) % 2048; pvt_buf[write_p] =c; write_p= (write_p+ 1) % 2048; wake_up_interruptible(&queue); //喚醒進程 } staticintread_pvt_buf(char*p) { if(empty())return0; *p=pvt_buf[read_p]; read_p= (read_p+1) % 2048; return1; } 接著實現私有打印函數pvt_printk。這里借助于內核提供的vsnprintf函數的存儲功能進行改寫。intvsnprintf(char*buf,size_tsize,constchar*fmt,va_listargs)把fmt指向的打印內容拷貝到*buf里面。 但為了實現環形緩沖區的順序寫入,這里不能直接將打印信息拷貝到pvt_buf。而是需先把打印信息存儲到這個臨時緩沖區temp[2048],再把臨時緩沖區的信息拷貝到pvt_buf。 staticchartemp[2048]; intpvt_printk(constchar*fmt,…) { va_listargs; intj,k= 0; va_start(args,fmt); /*把打印信息放到臨時緩沖區*/ j=vsnprintf(temp,INT_MAX,fmt,args); va_end(args); /*把臨時緩沖區的數據放到環形緩沖區pvt_buf里 */ while(k write_pvt_buf(temp[k]); k++; } returnj; } 到此最初的構想已經可以實現了,但是這樣做只能cat一次。因為在cat一次后讀位置已經移動到最后面沒有數據的地方。所以為了實現多次可讀,得在每次cat調用open的時候把讀位置調整到起始的地方。 staticintpvt_kmsg_open(structinode*inode,structfile*file) { read_p=readstart_p; return0; } 用EXPORT_SYMBOL(pvt_printk)將pvt_printk函數導出,供全部內核文件使用。 簡單測試:在xx驅動源碼里先用externintpvt_printk(constchar*fmt,…);聲明,然后在合適位置添加pvt_printk(“abcdefg”),將此驅動和proc-printk.c都編譯進內核,把新內核燒寫到ARM開發板并啟動。在開發板的串口終端下先運行與xx驅動對應的應用程序test1,再去cat創建的proc文件pvt_kmsg,就可以看到添加的信息。如圖1所示。 圖1 第一種調試方法實驗結果 2.1原理 當驅動程序某個地方不小心寫入了死循環的代碼。那么在運行相應app的時候Linux系統就會出現僵死的狀態。如果在多進程的狀況話,直接追查代碼找尋僵死點那將顯得非常困難。 但Linux時鐘中斷確是永遠都在發生的,進程的僵死是不會屏蔽掉系統時鐘中斷的。就像人一樣,即便是在睡覺,心臟總是在跳動的。 Linux系統下的中斷處理過程如圖2所示。t是時間軸,一個進程在運行著,發生中斷時,先保存現場,再執行中斷函數,最后回復現場[10],只不過系統時鐘中斷是周而復始的這樣執行。 圖2 中斷發生過程 保存現場其實就是保存各個寄存器的值。其中pc寄存器(ARM里由r15寄存器充當)就保存了執行中的驅動程序被打斷處的地址。僵死狀態時,進程會重復執行同一段代碼,這樣pc值會在一個很小的范圍里變動。 本調試技術就是借助系統時鐘中斷永不停息這一特性,在中斷入口函數里添加一些打印語句,把保存的pc值打印出來,再經過分析判斷定位出僵死的大致位置了。 在本實驗平臺的Linux系統當中,一發生中斷,CPU就強制跳到0xffff0018處(此地址根據CPU的架構不同而不同)執行異常向量表里面的bvector_irq+stubs_offset。其中“stubs_offset”用來重定位跳轉的位置[11],這條匯編指令是去跳轉執行vector_irq這個函數,在這個函數里面保存了一些現場,并經過復雜的匯編代碼后會調用到asm_do_IRQ(archarmkernelIrq.c)這個函數。它是中斷處理函數的總入口[12],在它內部經過層層的函數調用后,會最終執行到時鐘中斷處理函數s3c2410_timer_interrupt()(archarmplat-s3c24xx ime.c)。選擇在s3c2410_timer_interrupt()或asm_do_IRQ()里加打印信息都可以,這里選擇asm_do_IRQ(),因為在asm_do_IRQ()的傳入參數中有一個pt_regs結構體,它作用就是保存發生中斷時的現場。pt_regs結構體里面定義了一個長度為18的數組uregs,其中uregs[15]保存的就是pc寄存器的值,正好可以利用它把pc值打印出來。但這里需注意一點,中斷保存的pc值其實是實際指令地址+4。 asmlinkagevoid__exceptionasm_do_IRQ(unsignedintirq,structpt_regs*regs) structpt_regs{ longuregs[18]; }; …… #defineARM_pcuregs[15] …… 2.2實現步驟 先在一個正確的驅動程序里加入一段死循環代碼。本文在mydrive.c這個驅動源碼的讀函數mydrive_read()里加一句for(;;)。 staticssize_tmydrive_read(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos) {…… for(;;); …… } 然后把mydrive.c編譯成模塊下載到開發板(這里把驅動程序編譯成模塊,直接編譯進內核的情況比較簡單,后面會敘述)。insmodmydrive.ko后,執行于此驅動相對應的測試程序mytest,就會看到系統完全卡死了。 在asm_do_IRQ()函數里添加如下一段代碼,把僵死進程號和pc值打印出來。 staticpid_tpid; //之前進程號 staticintcount=0; If(irq== 30) //系統時鐘中斷用的是定時器4,對應的中斷號是30 { //若當前進程不等于之前進程,計數值清零 if(pid!=current->pid) { pid=current->pid; count= 0; } //當前進程等于之前的進程,計數值累加 else count++; if(count==15*HZ)若15秒內都是同一個進程 { count= 0; printk(“PID=%d,name=%s,pc=%08x
”,current->pid,current->comm,regs->uregs[15]); } } 在Linux內核里面每一個進程都由一個task_struct結構體來表示[13,14],里面包含了進程的相關屬性和信息。其中pid_tpid表示進程號;charcomm表示進程的名字。 current是一個全局宏,用來獲取表示當前進程的task_struct結構體指針。HZ也是一個宏定義,表示時鐘中斷發生的頻率,即一秒發生中斷的次數。 把修改的內核源碼在Linux服務器下編譯后下載到開發板并啟動。然后裝載驅動模塊mydrive.ko并運行與之相應的測試程序mytest,如圖3所示。 圖3執行步驟 系統僵死,等待15秒左右之后打印出僵死進程號、僵死進程名、 pc 值。如圖4所示。 圖4 僵死進程相關信息 從打印信息可知是763號進程mytest出現了僵死,從而可以知道問題出在與mytest對應的驅動程序mydrive。在開發板的串口終端下打開system.map(里面是內核的地址空間),發現bf000068不在其中,說明僵死驅動mydrive是個外加驅動模塊,這也與實際操作相符。 至于死循環代碼的具體位置還得用pc值去反推。在Linux2.6版本內核中引入了kallsyms,kallsyms抽取了內核用到的所有函數地址(全局的、靜態的)和非棧數據變量地址,生成一個數據塊,作為只讀數據鏈接進kernelimage。當然外加驅動模塊的地址也在其中重啟開發板,在終端下執行: insmodmydrive.ko cat/proc/kallsyms 在里面尋找地址bf000068,就找到與bf000068相近的一條bf000000。 bf000000tmydrive_open[mydrive] 在Linux服務器下把mydrive.ko模塊反匯編arm-Linux-cbjdump-Dmydrive.ko>mydrive.dis 打開mydrv.dis,找到有mydrive_open的那一行00000000 00000000 …… 64:ebfffffebl64 68:ea00001fbf8 …… 前面說過,中斷保存的pc值其實是實際指令地址+4,所以0x00000064才是中斷函數執行前保存的真正地址,也即僵死的位置。再看對應的名字mydrive_read+0x3c,可知具體代碼在mydrive_read()函數入口地址偏移0x3c。至此回到源碼文件mydrive.c的mydrive_read()函數便可找到具體的僵死處。這正好與之前故意添加的死循環for(;;)的位置相一致。比照著相應的匯編指令[16]bl64:永遠跳轉到64。這也剛好和死循環的C語言是相吻合的。 這里的僵死點在外加的驅動模塊中,如果僵死點在內核里,就會發現打印出來的pc值在system.map文件所列出的地址范圍里面。這時只需要把使用的Linux內核文件反匯編,在里面找到pc-4地址所在那一行代碼,自然就是僵死點的位置。 第一種調試方法中將驅動源碼文件proc-printk.c編譯進了內核。當然為了裝卸載方便,也可以將其編譯成模塊。在第二種調試方法中,為了測試需要,故意在驅動文件里添加for(;;)語句,造成程序僵死在一點,只打印出一個pc值。而在實際中,死循環的代碼可能是一段,這時用上述方法在一段時間內打印出來的pc值可能會不同。但這不要緊,因為此時的pc值雖不同,但分布密集。程序員只須分析這幾個pc值指定的匯編代碼就可確定僵死的大致位置。 [1] 霍玲玲,王世君,徐曉卉,等.嵌入式Linux系統的設計與實現[J]. 計算機技術與發展,2014,24(5):87-89. [2] 馮開林,劉春艷,韓東旭.基于S3C2440平臺搭建Linux環境[J]. 通信技術,2013,46(11):120-124. [3] 付陽.基于ARM9的嵌入式Linux移植和驅動程序設計[D].武漢:華中科技大學,2012. [4] 韋東山.嵌入式Linux應用開發完全手冊[M].北京:人民郵電出版社,2008. [5] 趙付強,李允俊,宮彥磊.Proc文件系統的研究與應用[J]. 計算機系統應用,2013,22(1):87-90. [6] 郭銳.基于覆蓋測試的Linux內核裁剪[D].太原:中北大學,2014. [7] 宋寶華.Linux設備驅動開發詳解[M].北京:人民郵電出版社,2010. [8] 王維,李濤,韓俊剛. 一種多線程輕核機器中進程管理的硬件實現[J].電子技術應用,2013,29(3):40-43. [9] 唐富強,于鴻洋,張萍.Linux下通用線程池的改進與實現[J].計算機工程與應用,2012,48(28):77-83. [10] 周峰,胡軍山,朱宗玖.基于CK810LINUX3.0內核的移植實現[J].計算機應用與軟件,2014,31(1):252-255,267. [11] 鄭強.Linux驅動開發入門與實戰[M].北京:清華大學出版社,2011. [12] 毛德操,胡希明.Linux內核源代碼情景分析[M].杭州:浙江大學出版社,2001. [13] 楊興強,劉翔鵬,劉毅.Linux進程狀態演化過程的圖形學表示[J].系統仿真學報,2013,25(10):2444-2448. [14] 龍飛.嵌入式Linux系統內核實時性研究[D].沈陽:沈陽工業大學,2012. [15] 奚琪,曾勇軍,王清賢,等.一種動靜結合的代碼反匯編框架[J].小型微型計算機系統,2013(10):2251-2255. [16] 黃奉孝,高艷華,張學軍.基于嵌入式構件的編程語言融合技術研究[J].計算機工程與設計,2012,33(11):4138-4141. RESEARCHONARM-BASEDLINUXDRIVERDEBUGGINGTECHNOLOGY GongChenCaiWen (School of Information,Mechanical and Electrical Engineering,Shanghai Normal University,Shanghai 200234,China) WhenperformingLinuxdrivertransplantationonARM,itisneededtomodify,deleteoraddLinuxkernelcodes,butwhichmayresultinmanyunexpectederrorsatruntime.Atthistimetodebugcodessoastofindthecauseandpositionoftheerrorarenecessary.Forthisrequirement,weproposeandimplementtwonewdebuggingtechniques.Thefirstoneistoconstructaprintingfunctiontostoreadditionalprintinformationinabufferseparately,andtooutputitwiththehelpofprocfilesystem.Itachievestheseparationbetweentheadditionalprintinformationandtheprintinformationofthekernelitself,andmakesthesearchmoreconvenient.Thesecondoneistodeterminethepositionofsystemdeadbymakinguseoftheunceasingcharacteristicofsystemclockinterrupt.Itisshownthroughexperimentthatthistechniquefindsthepositionofendlessloopquickly,andsavesalargeamountofcodesearchandanalysiswork. LinuxDebugProcfilesystemSystemclockinterrupt 2014-08-26。鞏琛,碩士生,主研領域:嵌入式系統與通信控制系統。蔡文,副教授。 TP314 ADOI:10.3969/j.issn.1000-386x.2016.03.054
2 利用內核時鐘中斷確定僵死位置



3 結 語