简介:
很多时候我们会遇到段错误:
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”,找到:archarmmmfault.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
",
(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
",
(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
", mm->pgd);
pgd = pgd_offset(mm, addr);
printk(KERN_ALERT "[%08lx] *pgd=%08lx", addr, pgd_val(*pgd));
······
printk("
");
}
而对应的打印信息为:
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]
", str, err, ++die_counter);
if (nl)
printk("
");
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 : [] lr : [] 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:
[] (first_drv_open+0x0/0x3c [first_drv]) from [] (chrdev_open+0x14c/0x164)
[] (chrdev_open+0x0/0x164) from [] (__dentry_open+0x100/0x1e8)
r8:c3e79724 r7:c0474f20 r6:c008d73c r5:c3e700c0 r4:c049a300
[] (__dentry_open+0x0/0x1e8) from [] (nameidata_to_filp+0x34/0x48)
[] (nameidata_to_filp+0x0/0x48) from [] (do_filp_open+0x40/0x48)
r4:00000002
[] (do_filp_open+0x0/0x48) from [] (do_sys_open+0x54/0xe4)
r5:bed00edc r4:00000002
[] (do_sys_open+0x0/0xe4) from [] (sys_open+0x24/0x28)
[] (sys_open+0x0/0x28) from [] (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
",
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
",
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,发现在archarmkernel raps.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
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
",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
回溯信息
[] (show_regs+0x0/0x4c) from [] (__do_user_fault+0x5c/0xa4)
r4:c04c80c0
[] (__do_user_fault+0x0/0xa4) from [] (do_page_fault+0x1dc/0x20c)
r7:c00271e0 r6:c3c8da04 r5:c04c80c0 r4:ffffffec
[] (do_page_fault+0x0/0x20c) from [] (do_DataAbort+0x3c/0xa0)
[] (do_DataAbort+0x0/0xa0) from [] (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("
");
}
printk("
end of stack
");
然后我们重新编译内核,并测试上面的程序。我们得到下面的打印信息:
# ./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
[] (show_regs+0x0/0x4c) from [] (__do_user_fault+0xec/0x144)
r4:c04967e0
[] (__do_user_fault+0x0/0x144) from [] (do_page_fault+0x1dc/0x20c)
[] (do_page_fault+0x0/0x20c) from [] (do_DataAbort+0x3c/0xa0)
[] (do_DataAbort+0x0/0xa0) from [] (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
好了写到这里就写完了,而具体的利用oops信息进行错误定位和错误分析,同时使用栈信息回溯函数调用关系的方法可以看前面的文章:
嵌入式Linux——oops:根据oops信息,找到错误的产生位置以及函数的调用关系
参考文章:
详谈 UNIX 环境进程异常退出存储器区块错误关于SIGSEGV错误及处理方法ARM Linux异常处理之data abort打印更多的用户态段错误信息41.Linux应用调试-修改内核来打印用户态的oops