嵌入式Linux——應用除錯:使用者態列印段錯誤資訊
簡介:
很多時候我們會遇到段錯誤:segmentation fault,而段錯誤有時是由核心引起的,有時是由應用程式引起的。在核心態時,發生段錯誤時會列印oops資訊,但是在使用者態時,發生段錯誤卻只會列印segmentation fault而並不會列印其他的資訊。所以本文主要介紹在使用者態時,通過修改核心設定和新增啟動引數來列印引發segmentation fault的資訊。
Linux核心:linux-2.6.22.6
所用開發板:JZ2440 V3(S3C2440A)
宣告:
本文是看完韋東山老師視訊並結合其他網友文章所寫,文中引用其他網友文章內容的位置我會標明。希望我的文章對你有所幫助。
segmentation fault:
儲存器區塊錯誤(英語:Segmentation fault,經常被縮寫為segfault),又譯為儲存器段錯誤,也稱訪問許可權衝突(access violation),是一種程式錯誤。它會出現在當程式企圖訪問CPU無法定址的儲存器區塊時。當錯誤發生時,硬體會通知作業系統產生了儲存器訪問許可權衝突的狀況。作業系統通常會產生核心轉儲檔案(core dump)以方便程式設計師進行除錯。通常該錯誤是由於呼叫一個地址,而該地址為空(NULL)所造成的,例如連結串列中呼叫一個未分配地址的空連結串列單元的元素。陣列訪問越界也可能產生這個錯誤。
發生segmentation fault時,MMU 產生記憶體保護異常 GPF(異常號 13)時,異常處理程式傳送相應訊號 SIGSEGV,SIGSEGV 的預設訊號處理程式終止程序執行。如下圖:
核心程式碼:
介紹了segmentation fault,那麼我們下面就要了解一下在核心中segmentation fault由什麼引起。我們知道當發生段錯誤時異常處理函式會發送SIGSEGV訊號來結束該程序,那麼我們就要看看在哪裡定義與SIGSEGV訊號相關的函式。我們去核心中搜“SIGSEGV”,找到:arch\arm\mm\fault.c中的fsr_info結構體:
static struct fsr_info { int (*fn)(unsigned long addr, unsigned int fsr, struct pt_regs *regs); //呼叫函式 int sig; //訊號 int code; const char *name; //錯誤名稱 } fsr_info[] = { /* * The following are the standard ARMv3 and ARMv4 aborts. ARMv5 * defines these to be "precise" aborts. */ { do_bad, SIGSEGV, 0, "vector exception" }, { do_bad, SIGILL, BUS_ADRALN, "alignment exception" }, { do_bad, SIGKILL, 0, "terminal exception" }, { do_bad, SIGILL, BUS_ADRALN, "alignment exception" }, { do_bad, SIGBUS, 0, "external abort on linefetch" }, { do_translation_fault, SIGSEGV, SEGV_MAPERR, "section translation fault" }, { do_bad, SIGBUS, 0, "external abort on linefetch" }, { do_page_fault, SIGSEGV, SEGV_MAPERR, "page translation fault" }, { do_bad, SIGBUS, 0, "external abort on non-linefetch" }, { do_bad, SIGSEGV, SEGV_ACCERR, "section domain fault" }, { do_bad, SIGBUS, 0, "external abort on non-linefetch" }, { do_bad, SIGSEGV, SEGV_ACCERR, "page domain fault" }, { do_bad, SIGBUS, 0, "external abort on translation" }, { do_sect_fault, SIGSEGV, SEGV_ACCERR, "section permission fault" }, { do_bad, SIGBUS, 0, "external abort on translation" }, { do_page_fault, SIGSEGV, SEGV_ACCERR, "page permission fault" }, };
在fsr_info中大多數是呼叫do_bad函式,而do_bad函式其實就是簡單的返回1,並不做其他的處理:
static int do_bad(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
return 1;
}
而下面我們主要分析的是:
"section translation fault" : do_translation_fault段轉換錯誤,即找不到一級頁表
"page translation fault" : do_page_fault頁表錯誤,即線性地址無效,沒有對應的實體地址
"section permission fault" : do_sect_fault 段許可權錯誤,即二級頁表許可權錯誤
"page permission fault" : do_page_fault頁許可權錯誤
do_translation_fault函式:轉化錯誤,一級頁表中不含有一個有效地址值。
static int do_translation_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
unsigned int index;
pgd_t *pgd, *pgd_k;
pmd_t *pmd, *pmd_k;
/* 如果是使用者空間地址,呼叫do_page_fault,轉入和頁表錯誤、頁許可權錯誤同樣的處理流程。 */
if (addr < TASK_SIZE)
return do_page_fault(addr, fsr, regs);
index = pgd_index(addr);
/*
* 如果是核心空間地址,會判斷該地址對應的二級頁表指標是否在init_mm中。
* 如果在init_mm裡面,那麼複製該二級頁表指標到當前程序的一級頁表;否則,呼叫do_bad_area處理(可能會呼叫到fixup)
*/
pgd = cpu_get_pgd() + index;
pgd_k = init_mm.pgd + index;
if (pgd_none(*pgd_k))
goto bad_area;
if (!pgd_present(*pgd))
set_pgd(pgd, *pgd_k);
pmd_k = pmd_offset(pgd_k, addr);
pmd = pmd_offset(pgd, addr);
if (pmd_none(*pmd_k))
goto bad_area;
copy_pmd(pmd, pmd_k);
return 0;
bad_area:
do_bad_area(addr, fsr, regs);
return 0;
}
do_page_fault函式:
do_page_fault完成了真正的物理頁面分配工作,另外棧擴充套件、mmap的支援等也都在這裡。對於物理頁面的分配,會呼叫到do_anonymous_page->。。。-> __rmqueue,__rmqueue中實現了物理頁面分配的夥伴演算法。
如果當前沒有足夠物理頁面供記憶體分配,即分配失敗:
核心模式下的abort會呼叫__do_kernel_fault,這與段許可權錯誤中的處理一樣。
使用者模式下,會呼叫do_group_exit退出該任務所屬的程序。
使用者程式申請記憶體空間時,如果庫函式本身的記憶體池不能滿足分配,會呼叫brk系統呼叫向系統申請擴大堆空間。但此時擴大的只是線性空間,直到真正使用到那塊線性空間時,系統才會通過data abort分配物理頁面。所以,malloc返回不為NULL只能說明得到了線性空間的資源,真正實體記憶體分配失敗時,程序還是會以資源不足為由,直接退出。
do_sect_fault函式:
static int do_sect_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
do_bad_area(addr, fsr, regs);
return 0;
}
do_sect_fault函式直接呼叫do_bad_area作處理,並返回0。
我們看到上面的函式都呼叫了do_bad_area,那麼我們看看在do_bad_area函式裡做了什麼:
void do_bad_area(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
struct task_struct *tsk = current;
struct mm_struct *mm = tsk->active_mm;
/*
* 判斷是在使用者態還是核心態
*/
if (user_mode(regs))
__do_user_fault(tsk, addr, fsr, SIGSEGV, SEGV_MAPERR, regs);
else
__do_kernel_fault(mm, addr, fsr, regs);
}
從上面可以看出,這裡主要是判斷在使用者態還是在核心態,在使用者態就呼叫__do_user_fault函式,而在核心態就呼叫:__do_kernel_fault函式。而user_mode巨集為:
#define user_mode(regs) \
(((regs)->ARM_cpsr & 0xf) == 0)
從中可以看出,通過當前狀態暫存器的值與0xf做與運算。
M[4:0] | 處理器模式 | ARM模式可訪問的暫存器 | THUMB模式可訪問的暫存器 |
0b10000 | 使用者模式 | PC,CPSR,R0~R14 | PC,CPSR,R0~R7,LR,SP |
0b10001 | FIQ模式 | PC,CPSR,SPSR_fiq,R14_fiq~R8_fiq,R0~R7 | PC,CPSR,SPSR_fiq,LR_fiq,SP_fiq,R0~R7 |
0b10010 | IRQ模式 | PC,CPSR,SPSR_irq,R14_irq~R13_irq,R0~R12 | PC,CPSR,SPSR_irq,LR_irq,SP_irq,R0~R7 |
0b10011 | 管理模式 | PC,CPSR,SPSR_svc,R14_svc~R13_svc,R0~R12 | PC,CPSR,SPSR_svc,LR_svc,SP_svc,R0~R7 |
0b10111 | 中止模式 | PC,CPSR,SPSR_abt,R14_abt~R13_abt,R0~R12 | PC,CPSR,SPSR_abt,LR_abt,SP_abt,R0~R7 |
0b11011 | 未定義模式 | PC,CPSR,SPSR_und,R14_und~R13_und,R0~R12 | PC,CPSR,SPSR_und,LR_und,SP_und,R0~R7 |
0b11111 | 系統模式 | PC,CPSR,R0~R14 | PC,CPSR,LR,SP,R0~R74 |
從上面知道只有使用者模式當前狀態暫存器的值與0xf做與運算的值為0,而其他模式時都不為0 。
在核心態時:
由於我們有核心態的oops資訊,所以我們先分析在核心態時的函式,然後我們再分析在使用者態時的函式就會好分析一些。
static void __do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
/*
* 如果可以修復這個錯誤,我們就修復他,並返回
*/
if (fixup_exception(regs))
return;
/*
* 如果不能修復,結束程序,列印oops資訊
*/
bust_spinlocks(1);
printk(KERN_ALERT
"Unable to handle kernel %s at virtual address %08lx\n",
(addr < PAGE_SIZE) ? "NULL pointer dereference" :
"paging request", addr);
show_pte(mm, addr);
die("Oops", regs, fsr);
bust_spinlocks(0);
do_exit(SIGKILL);
}
這裡我們主要分析當不能修復時,列印的oops資訊。我想大家看到:
printk(KERN_ALERT
"Unable to handle kernel %s at virtual address %08lx\n",
(addr < PAGE_SIZE) ? "NULL pointer dereference" :
"paging request", addr);
是不是很熟悉啊,在我們核心的oops資訊中為:
Unable to handle kernel paging request at virtual address 56000050
而show_pte函式則是列印在mm中頁表與地址的關係:
void show_pte(struct mm_struct *mm, unsigned long addr)
{
pgd_t *pgd;
printk(KERN_ALERT "pgd = %p\n", mm->pgd);
pgd = pgd_offset(mm, addr);
printk(KERN_ALERT "[%08lx] *pgd=%08lx", addr, pgd_val(*pgd));
······
printk("\n");
}
而對應的列印資訊為:
pgd = c3edc000
[56000050] *pgd=00000000
而die函式則列印與oops和暫存器相關的資訊:
int die(const char * str, struct pt_regs * fp, long err)
{
static int die_counter;
int nl = 0;
console_verbose();
spin_lock_irq(&die_lock);
printk("Oops: %s, sig: %ld [#%d]\n", str, err, ++die_counter);
if (nl)
printk("\n");
show_regs(fp);
spin_unlock_irq(&die_lock);
do_exit(err);
}
對應的資訊為:
Internal error: Oops: 5 [#1]
Modules linked in: first_drv
CPU: 0 Not tainted (2.6.22.6 #1)
PC is at first_drv_open+0x18/0x3c [first_drv]
LR is at chrdev_open+0x14c/0x164
pc : [<bf000018>] lr : [<c008d888>] psr: a0000013
sp : c3ed5e88 ip : c3ed5e98 fp : c3ed5e94
r10: 00000000 r9 : c3ed4000 r8 : c049a300
r7 : 00000000 r6 : 00000000 r5 : c3e700c0 r4 : c06a4540
r3 : bf000000 r2 : 56000050 r1 : bf000964 r0 : 00000000
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user
Control: c000717f Table: 33edc000 DAC: 00000015
Process firstdrvtest (pid: 783, stack limit = 0xc3ed4258)
Stack: (0xc3ed5e88 to 0xc3ed6000)
5e80: c3ed5ebc c3ed5e98 c008d888 bf000010 00000000 c049a300
5ea0: c3e700c0 c008d73c c0474f20 c3e79724 c3ed5ee4 c3ed5ec0 c0089e48 c008d74c
5ec0: c049a300 c3ed5f04 00000003 ffffff9c c002c044 c3cf4000 c3ed5efc c3ed5ee8
5ee0: c0089f64 c0089d58 00000000 00000002 c3ed5f68 c3ed5f00 c0089fb8 c0089f40
5f00: c3ed5f04 c3e79724 c0474f20 00000000 00000000 c3edd000 00000101 00000001
5f20: 00000000 c3ed4000 c046de08 c046de00 ffffffe8 c3cf4000 c3ed5f68 c3ed5f48
5f40: c008a16c c009fc70 00000003 00000000 c049a300 00000002 bed00edc c3ed5f94
5f60: c3ed5f6c c008a2f4 c0089f88 00008520 bed00ed4 0000860c 00008670 00000005
5f80: c002c044 4013365c c3ed5fa4 c3ed5f98 c008a3a8 c008a2b0 00000000 c3ed5fa8
5fa0: c002bea0 c008a394 bed00ed4 0000860c 00008720 00000002 bed00edc 00000001
5fc0: bed00ed4 0000860c 00008670 00000001 00008520 00000000 4013365c bed00ea8
5fe0: 00000000 bed00e84 0000266c 400c98e0 60000010 00008720 4021a2cc 4021a2dc
Backtrace:
[<bf000000>] (first_drv_open+0x0/0x3c [first_drv]) from [<c008d888>] (chrdev_open+0x14c/0x164)
[<c008d73c>] (chrdev_open+0x0/0x164) from [<c0089e48>] (__dentry_open+0x100/0x1e8)
r8:c3e79724 r7:c0474f20 r6:c008d73c r5:c3e700c0 r4:c049a300
[<c0089d48>] (__dentry_open+0x0/0x1e8) from [<c0089f64>] (nameidata_to_filp+0x34/0x48)
[<c0089f30>] (nameidata_to_filp+0x0/0x48) from [<c0089fb8>] (do_filp_open+0x40/0x48)
r4:00000002
[<c0089f78>] (do_filp_open+0x0/0x48) from [<c008a2f4>] (do_sys_open+0x54/0xe4)
r5:bed00edc r4:00000002
[<c008a2a0>] (do_sys_open+0x0/0xe4) from [<c008a3a8>] (sys_open+0x24/0x28)
[<c008a384>] (sys_open+0x0/0x28) from [<c002bea0>] (ret_fast_syscall+0x0/0x2c)
Code: e24cb004 e59f1024 e3a00000 e5912000 (e5923000)
在使用者態時:
有了對核心態的介紹,現在我們講使用者態,大家可能就更好理解了。
static void __do_user_fault(struct task_struct *tsk, unsigned long addr,
unsigned int fsr, unsigned int sig, int code,
struct pt_regs *regs)
{
struct siginfo si;
#ifdef CONFIG_DEBUG_USER
if (user_debug & UDBG_SEGV) {
printk(KERN_DEBUG "%s: unhandled page fault (%d) at 0x%08lx, code 0x%03x\n",
tsk->comm, sig, addr, fsr);
show_pte(tsk->mm, addr);
show_regs(regs);
}
#endif
tsk->thread.address = addr;
tsk->thread.error_code = fsr;
tsk->thread.trap_no = 14;
si.si_signo = sig;
si.si_errno = 0;
si.si_code = code;
si.si_addr = (void __user *)addr;
force_sig_info(sig, &si, tsk);
}
從上面程式碼看,在核心態時錯誤列印的程式碼主要在:
#ifdef CONFIG_DEBUG_USER
if (user_debug & UDBG_SEGV) {
printk(KERN_DEBUG "%s: unhandled page fault (%d) at 0x%08lx, code 0x%03x\n",
tsk->comm, sig, addr, fsr);
show_pte(tsk->mm, addr);
show_regs(regs);
}
#endif
所以我們要想打印出使用者態的錯誤資訊要滿足兩個條件:
1. 定義CONFIG_DEBUG_USER
2. 滿足條件:user_debug & UDBG_SEGV 不為0
我們先看第一個條件:定義CONFIG_DEBUG_USER,這裡我們有兩種方法
1. 直接在這個檔案中定義CONFIG_DEBUG_USER,或者直積去掉這個預編譯判斷。但是這樣會修改核心程式碼,為我們以後使用其他模組時編譯核心帶來麻煩。
2. 在make menuconfig時將這個選項選中。
具體做法為:
1. 在make menuconfig中搜DEBUG_USER,然後按著他指示的路徑去設定。
2. 在kernel hacking選項中將[*]Verbose user fault messages 選中。
下面我們看第二個條件,這裡是設定user_debug & UDBG_SEGV不為0,那麼我們就要看看user_debug是在哪裡設定了,我們在核心中搜user_debug,發現在arch\arm\kernel\traps.c中:
__setup("user_debug=", user_debug_setup);
而這個就是要我們在uboot的bootargs中加上user_debug=XXX選項來為user_debug賦值,關於__setup的設定在:嵌入式Linux——printk:printk列印機制分析中有介紹。而至於XXX具體等於多少我們就要看UDBG_SEGV的值了。
#define UDBG_UNDEFINED (1 << 0) //未定義
#define UDBG_SYSCALL (1 << 1) //非法系統呼叫
#define UDBG_BADABORT (1 << 2) //資料終止
#define UDBG_SEGV (1 << 3) //非法訪問
#define UDBG_BUS (1 << 4) //訪問無效匯流排
這裡為了方便我們直接將user_debug設為0xff。所以我們要在bootargs中加入user_debug=0xff語句,而其他的選項不變。這樣我們就可以列印核心的段錯誤資訊了。
測試:
這裡我們在測試程式中故意引入一個空指標錯誤,測試程式為:
#include <stdio.h>
void c(int *p)
{
*p = 0x12;
}
void b(int *p)
{
c(p);
}
void a(int *p)
{
b(p);
}
void a2(int *p)
{
c(p);
}
int main(int argc,char **argv)
{
int a;
int *p = NULL;
a2(&a);
printf(" a = 0x%x \n",a);
a(p); //這裡為會引發空指標錯誤
return 0;
}
然後打印出的資訊為:
# ./debug_test
a = 0x12
pgd = c3e78000
[00000000] *pgd=306e9031, *pte=00000000, *ppte=00000000
程序號和程序:
Pid: 777, comm: debug_test
CPU號和核心
CPU: 0 Not tainted (2.6.22.6 #10)
暫存器值
PC is at 0x84ac
LR is at 0x84d0
pc : [<000084ac>] lr : [<000084d0>] psr: 60000010
sp : bed78e60 ip : bed78e74 fp : bed78e70
r10: 4013365c r9 : 00000000 r8 : 00008514
r7 : 00000001 r6 : 000085cc r5 : 00008568 r4 : bed78ee4
r3 : 00000012 r2 : 00000000 r1 : 00001000 r0 : 00000000
ARM狀態暫存器值
Flags: nZCv IRQs on FIQs on Mode USER_32 Segment user
Control: c000717f Table: 33e78000 DAC: 00000015
回溯資訊
[<c002cd1c>] (show_regs+0x0/0x4c) from [<c0031a98>] (__do_user_fault+0x5c/0xa4)
r4:c04c80c0
[<c0031a3c>] (__do_user_fault+0x0/0xa4) from [<c0031d38>] (do_page_fault+0x1dc/0x20c)
r7:c00271e0 r6:c3c8da04 r5:c04c80c0 r4:ffffffec
[<c0031b5c>] (do_page_fault+0x0/0x20c) from [<c002b224>] (do_DataAbort+0x3c/0xa0)
[<c002b1e8>] (do_DataAbort+0x0/0xa0) from [<c002be48>] (ret_from_exception+0x0/0x10)
Exception stack(0xc3cf9fb0 to 0xc3cf9ff8)
9fa0: 00000000 00001000 00000000 00000012
9fc0: bed78ee4 00008568 000085cc 00000001 00008514 00000000 4013365c bed78e70
9fe0: bed78e74 bed78e60 000084d0 000084ac 60000010 ffffffff
r8:00008514 r7:00000001 r6:000085cc r5:00008568 r4:c039c028
Segmentation fault
通過上面資訊我們就可以定位出具體是哪裡出了問題了。只不過這裡我們要反彙編的是測試程式而不是驅動程式。
列印棧資訊:
雖然我們上面已經有了回溯資訊,但是我們還是不知道具體棧中的資訊。而棧中的資訊有時候對我們定位錯誤位置是很有幫助的。所以我們要想辦法將棧中的資訊打印出來。而我們知道現在程式碼所處的空間為核心空間,所以要想將使用者空間的棧資訊打印出來就需要呼叫copy_from_user函式來將棧資訊傳遞到核心空間。同時我們需要在我們編寫的函式中有pt_regs結構體,因為只有這樣我們才能得到當前執行緒的暫存器值。所以我們要在__do_user_fault函式的#ifdef CONFIG_DEBUG_USER下加程式碼:
unsigned long ret;
unsigned long val;
int i = 0;
while(i<1024){
if(copy_from_user(&val,(const void __user *)(regs->ARM_sp+i*4),4)){
break;
}
printk("%08x ",val);
if(i%8 == 0)
printk("\n");
}
printk("\n end of stack \n");
然後我們重新編譯核心,並測試上面的程式。我們得到下面的列印資訊:
# ./debug_test
a = 0x12
STACK :
00000000 bee18e84 bee18e74 000084d0 000084a0 00000000 bee18e98 bee18e88
000084f0 000084c4 00000000 bee18eb8 bee18e9c 00008554 000084e4 00000000
00000012 bee18ee4 00000001 00000000 bee18ebc 40034f14 00008524 00000000
00000000 0000839c 00000000 00000000 4001d594 000083c4 000085cc 4000c02c
bee18ee4 bee18f8f 00000000 bee18f9c bee18fa6 bee18fad bee18fb8 bee18fdb
bee18fe9 00000000 00000010 00000003 00000006 00001000 00000011 00000064
00000003 00008034 00000004 00000020 00000005 00000006 00000007 40000000
00000008 00000000 00000009 0000839c 0000000b 00000000 0000000c 00000000
0000000d 00000000 0000000e 00000000 00000017 00000000 0000000f bee18f8b
00000000 00000000 76000000 2e006c34 6265642f 745f6775 00747365 52455355
6f6f723d 4f480074 2f3d454d 52455400 74763d4d 00323031 48544150 62732f3d
2f3a6e69 2f727375 6e696273 69622f3a 752f3a6e 622f7273 53006e69 4c4c4548
69622f3d 68732f6e 44575000 2e002f3d 6265642f 745f6775 00747365 00000000
END of STACK
pgd = c3cf8000
[00000000] *pgd=3000a031, *pte=00000000, *ppte=00000000
Pid: 776, comm: debug_test
CPU: 0 Not tainted (2.6.22.6 #11)
PC is at 0x84ac
LR is at 0x84d0
pc : [<000084ac>] lr : [<000084d0>] psr: 60000010
sp : bee18e60 ip : bee18e74 fp : bee18e70
r10: 4013365c r9 : 00000000 r8 : 00008514
r7 : 00000001 r6 : 000085cc r5 : 00008568 r4 : bee18ee4
r3 : 00000012 r2 : 00000000 r1 : 00001000 r0 : 00000000
Flags: nZCv IRQs on FIQs on Mode USER_32 Segment user
Control: c000717f Table: 33cf8000 DAC: 00000015
[<c002cd1c>] (show_regs+0x0/0x4c) from [<c0031b28>] (__do_user_fault+0xec/0x144)
r4:c04967e0
[<c0031a3c>] (__do_user_fault+0x0/0x144) from [<c0031dd8>] (do_page_fault+0x1dc/0x20c)
[<c0031bfc>] (do_page_fault+0x0/0x20c) from [<c002b224>] (do_DataAbort+0x3c/0xa0)
[<c002b1e8>] (do_DataAbort+0x0/0xa0) from [<c002be48>] (ret_from_exception+0x0/0x10)
Exception stack(0xc0721fb0 to 0xc0721ff8)
1fa0: 00000000 00001000 00000000 00000012
1fc0: bee18ee4 00008568 000085cc 00000001 00008514 00000000 4013365c bee18e70
1fe0: bee18e74 bee18e60 000084d0 000084ac 60000010 ffffffff
r8:00008514 r7:00000001 r6:000085cc r5:00008568 r4:c039c028
Segmentation fault
參考文章: