個人簡介
lccz(龍城赤子),資深嵌入式開發者,愛好Linux內核相關技術。個人CSDN博客:wwwyue1985。
最近在調試設備時,遇到了一個偶發的開機死機問題。通過查看輸出日志,發現內核報告了oops錯誤,如下所示(中間省略了部分日志,以......代替)。
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格式文件系統的存儲卡時,內核出現了空指針訪問問題,最終導致內核奔潰并輸出oops。因為之前沒有遇到過這個問題,且最近硬件更換了讀卡器,存儲卡也更新換代了,從之前的100MB/s換到了120MB/s,所以,最初懷疑問題可能是因為更換讀卡器或(和)存儲卡導致的。但是,硬件和卡的變更到底是如何影響并導致上述oops錯誤的,這其中的細節并不清楚。好在堆棧信息比較明確,異常時,PC指針指向了這個位置:exfat_fill_super+0xc8/0x4cc (PC is at exfat_fill_super+0xc8/0x4cc [exfat])。那我們就順藤摸瓜,看看這個位置對應的代碼是什么。
首先,在工程中搜索exfat_fill_super這個函數,了解其位置和關聯模塊。一番操作下來,發現這個函數在第三方開源庫exfat中。這個庫提供了exfat文件系統掛載的支持,并被編譯為ko庫文件,在系統啟動時insmod到系統中。
其次,我們看看問題日志中,PC指針指向的代碼具體是哪一行?因為日志中只提示在exfat_fill_super這個函數的0xc8偏移處,為了準確找到這個位置,我們需要借助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這一行(也告訴我們對應的匯編在0x9670處,后面會用到):
2301opts->fs_fmask=opts->fs_dmask=current->fs->umask;
但是,比較煩人的是,這行代碼是連續賦值,并且都使用到了指針,所以并不能一下就確定問題到底在那一個賦值上產生。不過,不著急,我們先看看這行代碼做了什么。按照C語言的規則,連續賦值是從右到左執行,所以先執行的應該是:
opts->fs_dmask = current->fs->umask;
執行這行代碼時,需要先確定current->fs,再確定fs->umask,最后,將結果給opts->fs_dmask。所以,就這一處賦值而言,就有三個可能的疑點。
先看第一個current->fs。這里current是一個宏,用于獲取當前線程的任務結構體(這里又隱藏一個指針)。
對于當前arm平臺,線程信息是通過堆棧寄存器獲取的。
static inline struct thread_info *current_thread_info(void)
{
register unsigned long sp asm ("sp");
return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}
從上面代碼,進一步的得知,線程信息是堆棧寄存器通過位運算獲得的。這里的THREAD_SIZE定義如下:
這是一個跟頁面大小相關的量。在當前系統中,PAGE_SIZE為4KB大小,所以THREAD_SIZE為8KB大小,也即0x2000,一共14位。減去1,就是1FFFF,取反就是0b’0000(第一個0占1bit,其余為4bit),然后參與“與”運算。這一連串的運算,總結為一句話,就是將給定的棧指針地址的低13位與0進行與運算,即將棧指針低13位清零。
這就是說內核線程結構體是在當前棧8KB對齊的低地址處。這是內核在設計時故意安排的,可以提高查找效率。我們來看這個指針獲取是否存在空指針訪問的問題:
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的位置應該是cdf7c000,從上面的日志中也可以看到ti是cdf7c000,所以這個位置不會是空指針的位置。
這里的task是thread_info結構體的一個子域,如下
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有沒有可能是一個空指針呢?上面oosp的日志也給出了,
task: ced40e40,所以,task也不為空。
這樣,current就指代了這里的task,一個不為空的地址。所以我們再看
current->fs
這里的fs是task_struct結構體的一個子域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;
......
};
從上面的定義,可以看到,它是跟文件系統相關的一個結構體。分析到這里時,考慮到問題所在函數為exfat_fill_super,看名字似乎是填充文件系統超級快的操作,加之測試部門反饋,問題出現后,格式化存儲卡就會恢復,所以我懷疑,會不會是因為更換讀卡器和存儲卡,導致讀取超級塊信息有誤,才使得文件系統相關訪問出現空指針,并報告oops。
為了驗證這一想法,我將上述連續賦值的這行代碼(即前述問題所在的2301行代碼)進行拆分,分為多條語句,然后在每一個指針使用點添加日志,以便在問題出現時,輸出問題到底在哪個指針上。另外,為了盡可能保留環境,在問題出現后,采取軟重啟設備,并通過重新配置uboot參數,讓內核通過nfs掛載根文件系統,這樣就可以替換之前的ko庫文件來測試了。
奇怪的是,每次替換后,問題就不出現了。這一現象似乎打破了之前的猜測,感覺問題又偏向軟件一側了。在這種取巧的打印方案沒有取得效果后,我決定直接分析匯編代碼,看看問題出現時,空指針到底落在了哪里。反匯編目標文件,結合gdb報告的位置(前面已提到)和oops中報告的指令內容
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寄存器指示的內存地址,偏移12位置后的兩個字節,加載到r3寄存器中。這里r3指示的內存地址是什么呢?根據oops中給出的信息,是00000000,加上12,就是地址0000000C,所以oops報告
Unable to handle kernel NULL pointer dereference at virtual address 0000000c
結合C代碼及問題點前后的匯編代碼,直觀感覺,這里的12應該是一個結構體中某一個子域的偏移,找到這個偏移對應的域,那么就可以確定是在哪一個賦值上出現了空指針。
回到C代碼,問題代碼行前后有好幾個結構體使用,為了快速確定偏移,我選擇參考內核container_of宏,定義一個找偏移的宏
通過這個宏,快速找到每一個元素在結構體中的偏移。當然,也可以通過看代碼來確定,但是沒有這種方法來得快。就是通過這個操作,引出了問題的最終原因。我們繼續。
添加獲取偏移的日志后,得到的相關偏移信息如下:
task_offset=12, fs_offset=904, umask_offset=12, fs_fmask=8, fs_dmask=10
這里的12、904、12、、8、10似乎跟匯編有隱隱的對應關系。但是這里的904跟776沒有什么關系。我決定再看看添加日志后目標文件的反匯編代碼,如下:
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]
因為此時代碼被修改,所以只能大概判斷之前問題所在的匯編范圍。從上面可以看出,這一次匯編里的數值跟打印出來的偏移對應上了。根據這次的偏移,結合匯編,基本可以確定,之前出問題的匯編對應的就是C代碼中的fs->umask這個語句
因為fs為空,所以再去獲取umask,就會報空指針異常。那問題來了,為啥fs會變空呢?有經驗的讀者,此時可能已經猜出問題的原因了。
我們看到,之前代碼反匯編后,fs的偏移是776,添加日志重新編譯后,反匯編成了904。雖然添加日志,導致代碼被修改,但是并不影響這個偏移,所以,這里的fs偏移可能就是問題所在。對于偏移變化,我考慮了三個因素,分別進行了驗證:
1 是ko庫文件因為flash壞塊或其他原因,導致二進制文件部分bit翻轉。實際驗證后,排除了這個原因。
2 是ko庫針對不同平臺編譯的,放置錯誤導致。實際驗證后,這個原因也排除了。
3 是當前添加日志后所編譯ko庫,其依賴的內核配置跟之前編譯ko庫依賴的內核配置相比有更新,也就是內核配置發生了變化(內核版本本身是一致的)。這種情況最常見的就是對內核進行了menuconfig操作。檢查fs所在的task_struct結構體,發現其中有很多ifdef,不過都不曾配置過,倒是有一個perf相關的CONFIG_PERF_EVENTS,由于調測性能所需,是后來新配置的。但是這個配置選項在fs結構體后面(見前面task_struct結構體),按理說是不影響fs在整個結構體中偏移的。考慮到task_struct結構體里面包含了很多子結構體,不排除上述perf配置影響了fs前面的某些子結構體而導致fs自己的偏移發生變化。
說了這么多,那么到底是不是呢,驗證一下就知道了。關閉上述選項,重新編譯內核,之后再編譯exfat,查看匯編,發現偏移回到了776。Yes,問題就是這里了。最終原因就是內核更新了,但是ko沒有更新,導致二者不匹配(舊的ko庫從776偏移找fs,但是在新內核中,fs的偏移已經成了904),產生了潛在的問題。
問題原因最終是找到了,但是問題產生的過程,其實更值得引起注意:ko庫因為也是在內核空間運行,所以需要跟kernel版本匹配起來,做版本一致管理。進一步的,不僅僅是嵌入式領域,桌面端也同樣的,如果系統中加載了ko庫,當更新kernel時,就需要考慮對ko庫的影響。二者需要統一起來看待和管理。
原文標題:一個內核oops問題的分析及解決
文章出處:【微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
-
嵌入式
+關注
關注
5072文章
19026瀏覽量
303523 -
內核
+關注
關注
3文章
1366瀏覽量
40236 -
Oops
+關注
關注
0文章
4瀏覽量
3305
原文標題:一個內核oops問題的分析及解決
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論