個(gè)人簡介
lccz(龍城赤子),資深嵌入式開發(fā)者,愛好Linux內(nèi)核相關(guān)技術(shù)。個(gè)人CSDN博客:wwwyue1985。
最近在調(diào)試設(shè)備時(shí),遇到了一個(gè)偶發(fā)的開機(jī)死機(jī)問題。通過查看輸出日志,發(fā)現(xiàn)內(nèi)核報(bào)告了oops錯(cuò)誤,如下所示(中間省略了部分日志,以......代替)。
Unable to handle kernel NULL pointer dereference at virtual address 0000000c
pgd = cdd90000
*pgd=8df4d831, *pte=00000000, *ppte=00000000
Internal error: Oops: 17 [#1] SMP ARM
CPU: 0 PID: 206 Comm: mount Tainted: P O 3.18.20 #4
task: ced40e40 ti: cdf7c000 task.ti: cdf7c000
PC is at exfat_fill_super+0xc8/0x4cc [exfat]
LR is at exfat_fill_super+0x48/0x4cc [exfat]
pc : [
] lr : [ ] psr: a0080013 sp : cdf7de48 ip : ffffffff fp : c0744a30
r10: 00000001 r9 : bf652dac r8 : 00008000
r7 : cdf80000 r6 : cf302000 r5 : cdf85000 r4 : cdf41000
r3 : 00000000 r2 : cdf85104 r1 : 00000003 r0 : 000001b5
Flags: NzCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment user
Control: 10c5387d Table: 8dd9006a DAC: 00000015
SP: 0xcdf7ddc8:
ddc8 cfa70880 fffffffc 0000000b cf17f800 cf4ea000 cf17f600 00000000 cfdee780
dde8 bf64b670 a0080013 ffffffff cdf7de34 00008000 c0012e18 000001b5 00000003
......
Process mount (pid: 206, stack limit = 0xcdf7c238)
Stack: (0xcdf7de48 to 0xcdf7e000)
de40: 00000001 cdf41000 cdf7deb0 cf17f60c 00000001 00008000
de60: cdf41000 cdf7c038 c0744a30 c0264164 bf652db4 cdf7de84 3b9aca00 00000004
de80: cf4ea6c0 00000083 cf4ea734 cf302000 cf4ea6c0 00000083 00008000 cdf41000
......
dfc0: 01197040 01197040 be9fff49 00000015 be9fff31 00008000 00000000 00000000
dfe0: b6e3d2e0 be9ffaf8 0007ebec b6e3d2f0 60080010 be9fff49 00000000 00000000
(exfat_fill_super [exfat]) from [
] (mount_bdev+0x168/0x190) (mount_bdev) from [
] (exfat_fs_mount+0x18/0x20 [exfat]) (exfat_fs_mount [exfat]) from [
] (mount_fs+0x14/0xcc) (mount_fs) from [
] (vfs_kern_mount+0x4c/0x104) (vfs_kern_mount) from [
] (do_mount+0x194/0xb54) (do_mount) from [
] (SyS_mount+0x74/0xa0) (SyS_mount) from [
] (ret_fast_syscall+0x0/0x38) Code: e5851108 e3a01003 e593300c e5933308 (e1d330bc)
從上述日志信息中,初步可以看出,在掛載exfat格式文件系統(tǒng)的存儲卡時(shí),內(nèi)核出現(xiàn)了空指針訪問問題,最終導(dǎo)致內(nèi)核奔潰并輸出oops。因?yàn)橹皼]有遇到過這個(gè)問題,且最近硬件更換了讀卡器,存儲卡也更新?lián)Q代了,從之前的100MB/s換到了120MB/s,所以,最初懷疑問題可能是因?yàn)楦鼡Q讀卡器或(和)存儲卡導(dǎo)致的。但是,硬件和卡的變更到底是如何影響并導(dǎo)致上述oops錯(cuò)誤的,這其中的細(xì)節(jié)并不清楚。好在堆棧信息比較明確,異常時(shí),PC指針指向了這個(gè)位置:exfat_fill_super+0xc8/0x4cc (PC is at exfat_fill_super+0xc8/0x4cc [exfat])。那我們就順藤摸瓜,看看這個(gè)位置對應(yīng)的代碼是什么。
首先,在工程中搜索exfat_fill_super這個(gè)函數(shù),了解其位置和關(guān)聯(lián)模塊。一番操作下來,發(fā)現(xiàn)這個(gè)函數(shù)在第三方開源庫exfat中。這個(gè)庫提供了exfat文件系統(tǒng)掛載的支持,并被編譯為ko庫文件,在系統(tǒng)啟動(dòng)時(shí)insmod到系統(tǒng)中。
其次,我們看看問題日志中,PC指針指向的代碼具體是哪一行?因?yàn)槿罩局兄惶崾驹?/span>exfat_fill_super這個(gè)函數(shù)的0xc8偏移處,為了準(zhǔn)確找到這個(gè)位置,我們需要借助gdb,如下所示:
l exfat_fill_super
&exfat_dentry_ops; =
}
#endif
static int exfat_fill_super(struct super_block *sb, void *data, int silent)
{
struct inode *root_inode = NULL;
struct exfat_sb_info *sbi;
int debug, ret;
long error;
l *exfat_fill_super+0xc8
0x9670 is at ./exfat-nofuse-master/exfat_super.c:2301.
int option;
char *iocharset;
current_uid(); =
current_gid(); =
opts->fs_dmask = current->fs->umask; =
(unsigned short) -1; =
exfat_default_codepage; =
exfat_default_iocharset; =
0; =
可以看到,gdb告訴我們,0xc8偏移在2301這一行(也告訴我們對應(yīng)的匯編在0x9670處,后面會用到):
2301opts->fs_fmask=opts->fs_dmask=current->fs->umask;
但是,比較煩人的是,這行代碼是連續(xù)賦值,并且都使用到了指針,所以并不能一下就確定問題到底在那一個(gè)賦值上產(chǎn)生。不過,不著急,我們先看看這行代碼做了什么。按照C語言的規(guī)則,連續(xù)賦值是從右到左執(zhí)行,所以先執(zhí)行的應(yīng)該是:
opts->fs_dmask = current->fs->umask;
執(zhí)行這行代碼時(shí),需要先確定current->fs,再確定fs->umask,最后,將結(jié)果給opts->fs_dmask。所以,就這一處賦值而言,就有三個(gè)可能的疑點(diǎn)。
先看第一個(gè)current->fs。這里current是一個(gè)宏,用于獲取當(dāng)前線程的任務(wù)結(jié)構(gòu)體(這里又隱藏一個(gè)指針)。
對于當(dāng)前arm平臺,線程信息是通過堆棧寄存器獲取的。
static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}
從上面代碼,進(jìn)一步的得知,線程信息是堆棧寄存器通過位運(yùn)算獲得的。這里的THREAD_SIZE定義如下:
這是一個(gè)跟頁面大小相關(guān)的量。在當(dāng)前系統(tǒng)中,PAGE_SIZE為4KB大小,所以THREAD_SIZE為8KB大小,也即0x2000,一共14位。減去1,就是1FFFF,取反就是0b’0000(第一個(gè)0占1bit,其余為4bit),然后參與“與”運(yùn)算。這一連串的運(yùn)算,總結(jié)為一句話,就是將給定的棧指針地址的低13位與0進(jìn)行與運(yùn)算,即將棧指針低13位清零。
這就是說內(nèi)核線程結(jié)構(gòu)體是在當(dāng)前棧8KB對齊的低地址處。這是內(nèi)核在設(shè)計(jì)時(shí)故意安排的,可以提高查找效率。我們來看這個(gè)指針獲取是否存在空指針訪問的問題:
task
回到最開始的日志中,部分信息如下
task: ced40e40 ti: cdf7c000 task.ti: cdf7c000
PC is at exfat_fill_super+0xc8/0x4cc [exfat]
LR is at exfat_fill_super+0x48/0x4cc [exfat]
pc : [
] lr : [ ] psr: a0080013 sp : cdf7de48 ip : ffffffff fp : c0744a30
其中,sp在cdf7de48,所以thread_info的位置應(yīng)該是cdf7c000,從上面的日志中也可以看到ti是cdf7c000,所以這個(gè)位置不會是空指針的位置。
這里的task是thread_info結(jié)構(gòu)體的一個(gè)子域,如下
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
mm_segment_t addr_limit; /* address limit */
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
那么,task有沒有可能是一個(gè)空指針呢?上面oosp的日志也給出了,
task: ced40e40,所以,task也不為空。
這樣,current就指代了這里的task,一個(gè)不為空的地址。所以我們再看
current->fs
這里的fs是task_struct結(jié)構(gòu)體的一個(gè)子域struct fs_struct *fs;(部分字段省略)
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
......
/* CPU-specific state of this task */
struct thread_struct thread;
/* filesystem information */
struct fs_struct *fs;
/* open file information */
struct files_struct *files;
/* namespaces */
struct nsproxy *nsproxy;
......
struct perf_event_context *perf_event_ctxp[perf_nr_task_contexts];
struct mutex perf_event_mutex;
struct list_head perf_event_list;
unsigned long preempt_disable_ip;
......
};
從上面的定義,可以看到,它是跟文件系統(tǒng)相關(guān)的一個(gè)結(jié)構(gòu)體。分析到這里時(shí),考慮到問題所在函數(shù)為exfat_fill_super,看名字似乎是填充文件系統(tǒng)超級快的操作,加之測試部門反饋,問題出現(xiàn)后,格式化存儲卡就會恢復(fù),所以我懷疑,會不會是因?yàn)楦鼡Q讀卡器和存儲卡,導(dǎo)致讀取超級塊信息有誤,才使得文件系統(tǒng)相關(guān)訪問出現(xiàn)空指針,并報(bào)告oops。
為了驗(yàn)證這一想法,我將上述連續(xù)賦值的這行代碼(即前述問題所在的2301行代碼)進(jìn)行拆分,分為多條語句,然后在每一個(gè)指針使用點(diǎn)添加日志,以便在問題出現(xiàn)時(shí),輸出問題到底在哪個(gè)指針上。另外,為了盡可能保留環(huán)境,在問題出現(xiàn)后,采取軟重啟設(shè)備,并通過重新配置uboot參數(shù),讓內(nèi)核通過nfs掛載根文件系統(tǒng),這樣就可以替換之前的ko庫文件來測試了。
奇怪的是,每次替換后,問題就不出現(xiàn)了。這一現(xiàn)象似乎打破了之前的猜測,感覺問題又偏向軟件一側(cè)了。在這種取巧的打印方案沒有取得效果后,我決定直接分析匯編代碼,看看問題出現(xiàn)時(shí),空指針到底落在了哪里。反匯編目標(biāo)文件,結(jié)合gdb報(bào)告的位置(前面已提到)和oops中報(bào)告的指令內(nèi)容
Code: e5851108 e3a01003 e593300c e5933308 (e1d330bc)
確定問題就在下面匯編中9670這一行:
9660: e5851108 str r1, [r5, #264] ; 0x108
9664: e3a01003 mov r1, #3
9668: e593300c ldr r3, [r3, #12]
966c: e5933308 ldr r3, [r3, #776] ; 0x308
9670: e1d330bc ldrh r3, [r3, #12]
9674: e1c2c0bc strh ip,
9678: e1c200be strh r0,
967c: e1c230ba strh r3,
9680: e1c230b8 strh r3,
這是一條加載指令,即將r3寄存器指示的內(nèi)存地址,偏移12位置后的兩個(gè)字節(jié),加載到r3寄存器中。這里r3指示的內(nèi)存地址是什么呢?根據(jù)oops中給出的信息,是00000000,加上12,就是地址0000000C,所以oops報(bào)告
Unable to handle kernel NULL pointer dereference at virtual address 0000000c
結(jié)合C代碼及問題點(diǎn)前后的匯編代碼,直觀感覺,這里的12應(yīng)該是一個(gè)結(jié)構(gòu)體中某一個(gè)子域的偏移,找到這個(gè)偏移對應(yīng)的域,那么就可以確定是在哪一個(gè)賦值上出現(xiàn)了空指針。
回到C代碼,問題代碼行前后有好幾個(gè)結(jié)構(gòu)體使用,為了快速確定偏移,我選擇參考內(nèi)核container_of宏,定義一個(gè)找偏移的宏
通過這個(gè)宏,快速找到每一個(gè)元素在結(jié)構(gòu)體中的偏移。當(dāng)然,也可以通過看代碼來確定,但是沒有這種方法來得快。就是通過這個(gè)操作,引出了問題的最終原因。我們繼續(xù)。
添加獲取偏移的日志后,得到的相關(guān)偏移信息如下:
task_offset=12, fs_offset=904, umask_offset=12, fs_fmask=8, fs_dmask=10
這里的12、904、12、、8、10似乎跟匯編有隱隱的對應(yīng)關(guān)系。但是這里的904跟776沒有什么關(guān)系。我決定再看看添加日志后目標(biāo)文件的反匯編代碼,如下:
97b8: e3a0b000 mov fp, #0
97bc: e3a0207b mov r2, #123 ; 0x7b
97c0: e3000000 movw r0, #0
97c4: e300a000 movw sl, #0
97c8: e5933388 ldr r3, [r3, #904] ; 0x388
97cc: e3400000 movt r0, #0
97d0: e340a000 movt sl, #0
97d4: e1d330bc ldrh r3, [r3, #12]
97d8: e1c930ba strh r3, [r9, #10]
97dc: e1c930b8 strh r3, [r9, #8]
97e0: e5cb2000 strb r2, [fp]
97e4: e595300c ldr r3, [r5, #12]
因?yàn)榇藭r(shí)代碼被修改,所以只能大概判斷之前問題所在的匯編范圍。從上面可以看出,這一次匯編里的數(shù)值跟打印出來的偏移對應(yīng)上了。根據(jù)這次的偏移,結(jié)合匯編,基本可以確定,之前出問題的匯編對應(yīng)的就是C代碼中的fs->umask這個(gè)語句
因?yàn)閒s為空,所以再去獲取umask,就會報(bào)空指針異常。那問題來了,為啥fs會變空呢?有經(jīng)驗(yàn)的讀者,此時(shí)可能已經(jīng)猜出問題的原因了。
我們看到,之前代碼反匯編后,fs的偏移是776,添加日志重新編譯后,反匯編成了904。雖然添加日志,導(dǎo)致代碼被修改,但是并不影響這個(gè)偏移,所以,這里的fs偏移可能就是問題所在。對于偏移變化,我考慮了三個(gè)因素,分別進(jìn)行了驗(yàn)證:
1 是ko庫文件因?yàn)?span style="line-height:24px;font-family:Calibri;">flash壞塊或其他原因,導(dǎo)致二進(jìn)制文件部分bit翻轉(zhuǎn)。實(shí)際驗(yàn)證后,排除了這個(gè)原因。
2 是ko庫針對不同平臺編譯的,放置錯(cuò)誤導(dǎo)致。實(shí)際驗(yàn)證后,這個(gè)原因也排除了。
3 是當(dāng)前添加日志后所編譯ko庫,其依賴的內(nèi)核配置跟之前編譯ko庫依賴的內(nèi)核配置相比有更新,也就是內(nèi)核配置發(fā)生了變化(內(nèi)核版本本身是一致的)。這種情況最常見的就是對內(nèi)核進(jìn)行了menuconfig操作。檢查fs所在的task_struct結(jié)構(gòu)體,發(fā)現(xiàn)其中有很多ifdef,不過都不曾配置過,倒是有一個(gè)perf相關(guān)的CONFIG_PERF_EVENTS,由于調(diào)測性能所需,是后來新配置的。但是這個(gè)配置選項(xiàng)在fs結(jié)構(gòu)體后面(見前面task_struct結(jié)構(gòu)體),按理說是不影響fs在整個(gè)結(jié)構(gòu)體中偏移的??紤]到task_struct結(jié)構(gòu)體里面包含了很多子結(jié)構(gòu)體,不排除上述perf配置影響了fs前面的某些子結(jié)構(gòu)體而導(dǎo)致fs自己的偏移發(fā)生變化。
說了這么多,那么到底是不是呢,驗(yàn)證一下就知道了。關(guān)閉上述選項(xiàng),重新編譯內(nèi)核,之后再編譯exfat,查看匯編,發(fā)現(xiàn)偏移回到了776。Yes,問題就是這里了。最終原因就是內(nèi)核更新了,但是ko沒有更新,導(dǎo)致二者不匹配(舊的ko庫從776偏移找fs,但是在新內(nèi)核中,fs的偏移已經(jīng)成了904),產(chǎn)生了潛在的問題。
問題原因最終是找到了,但是問題產(chǎn)生的過程,其實(shí)更值得引起注意:ko庫因?yàn)橐彩窃趦?nèi)核空間運(yùn)行,所以需要跟kernel版本匹配起來,做版本一致管理。進(jìn)一步的,不僅僅是嵌入式領(lǐng)域,桌面端也同樣的,如果系統(tǒng)中加載了ko庫,當(dāng)更新kernel時(shí),就需要考慮對ko庫的影響。二者需要統(tǒng)一起來看待和管理。
原文標(biāo)題:一個(gè)內(nèi)核oops問題的分析及解決
文章出處:【微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
-
嵌入式
+關(guān)注
關(guān)注
5059文章
18973瀏覽量
302039 -
內(nèi)核
+關(guān)注
關(guān)注
3文章
1360瀏覽量
40185 -
Oops
+關(guān)注
關(guān)注
0文章
4瀏覽量
3303
原文標(biāo)題:一個(gè)內(nèi)核oops問題的分析及解決
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論