如何通过oops信息调试用户应用程序

参考文献:韦东山视频

  • 要解决的问题:在开发板上运行程序时,如果遇到段错误,提示信息仅仅如下提示:
# ./test_file
Segmentation fault

并没有像之前我们在调试内核驱动程序时那样,完整显示oops信息:

Unable to handle kernel paging request at virtual address 56000050
      //无法处理内核页面请求的虚拟地址56000050
pgd = c3850000
[56000050] *pgd=00000000
Internal error: Oops: 5 [#1]
        //内部错误oops
Modules linked in: 26th_segmentfault
        //表示内部错误发生在26th_segmentfault.ko驱动模块里
CPU: 0    Not tainted  (2.6.22.6 #2)
PC is at first_drv_open+0x78/0x12c [26th_segmentfault]
        //PC值:程序运行成功的最后一次地址,位于first_drv_open()函数里,偏移值0x78,该函数总大小0x12c
LR is at 0xc0365ed8             //LR值

/*发生错误时的各个寄存器值*/
pc : [<bf000078>]    lr : [<c0365ed8>]    psr: 80000013
sp : c3fcbe80  ip : c0365ed8  fp : c3fcbe94
r10: 00000000  r9 : c3fca000  r8 : c04df960
r7 : 00000000  r6 : 00000000  r5 : bf000de4  r4 : 00000000
r3 : 00000000  r2 : 56000050  r1 : 00000001  r0 : 00000052

Flags: Nzcv  IRQs on  FIQs on  Mode SVC_32  Segment user
Control: c000717f  Table: 33850000  DAC: 00000015
Process 26th_segmentfau (pid: 813, stack limit = 0xc3fca258)
            //发生错误时,进程名称为26th_segmentfault

Stack: (0xc3fcbe80 to 0xc3fcc000)        //栈信息,从栈底0xc3fcbe80到栈顶0xc3fcc000
be80: c06d7660 c3e880c0 c3fcbebc c3fcbe98 c008d888 bf000010 00000000 c04df960
bea0: c3e880c0 c008d73c c0474e20 c3fb9534 c3fcbee4 c3fcbec0 c0089e48 c008d74c
bec0: c04df960 c3fcbf04 00000003 ffffff9c c002c044 c380a000 c3fcbefc c3fcbee8
bee0: c0089f64 c0089d58 00000000 00000002 c3fcbf68 c3fcbf00 c0089fb8 c0089f40
bf00: c3fcbf04 c3fb9534 c0474e20 00000000 00000000 c3851000 00000101 00000001
bf20: 00000000 c3fca000 c04c90a8 c04c90a0 ffffffe8 c380a000 c3fcbf68 c3fcbf48
bf40: c008a16c c009fc70 00000003 00000000 c04df960 00000002 be84ce38 c3fcbf94
bf60: c3fcbf6c c008a2f4 c0089f88 00008588 be84ce84 00008718 0000877c 00000005
bf80: c002c044 4013365c c3fcbfa4 c3fcbf98 c008a3a8 c008a2b0 00000000 c3fcbfa8
bfa0: c002bea0 c008a394 be84ce84 00008718 be84ce30 00000002 be84ce38 be84ce30
bfc0: be84ce84 00008718 0000877c 00000003 00008588 00000000 4013365c be84ce58
bfe0: 00000000 be84ce28 0000266c 400c98e0 60000010 be84ce30 30002031 30002431

Backtrace:                                        //回溯信息
[<bf000000>] (first_drv_open+0x0/0x12c [26th_segmentfault]) from [<c008d888>] (chrdev_open+0x14c/0x164)
 r5:c3e880c0 r4:c06d7660
[<c008d73c>] (chrdev_open+0x0/0x164) from [<c0089e48>] (__dentry_open+0x100/0x1e8)
 r8:c3fb9534 r7:c0474e20 r6:c008d73c r5:c3e880c0 r4:c04df960
[<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:be84ce38 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: bf000094 bf0000b4 bf0000d4 e5952000 (e5923000)

Segmentation fault
  • 所以接下来的目标就是,让我们在开发板上运行应用程序出错时,也能像调试内核程序那样打印出oops信息

1、分析内核程序的oops信息怎么打印出来的

  • 思路: 定位错误提示信息所在函数及其调用关系

  • 在内核源码中搜索错误提示信息:Unable to handle kernel

  • cd linux2.6.22/
    grep "Unable to handle kernel" * -nR
    

image-20210826222615024

  • 发现__do_kernel_fault()被do_bad_area()调用

image-20210826223802963

  • do_bad_area()函数:从字面上分析,表示代码执行到错误段位置

    ​ 其中user_mode(regs)函数,通过判断CPSR寄存器若是用户模式则返回0,否则返回正数.

    ​ 所以我们上一章的错误的应用程序便会调用__do_user_fault()函数

  • 查看__do_user_fault()函数

image-20210826224004017]

  • 分析语句得知:
    • 只有当在内核配置(只要宏是以"CONFIG_"开头,都是与配置相关)中设置了CONFIG_DEBUG_USER宏,才会打印提示消息!
    • 只有user_debug & UDBG_SEGV为真,才会打印提示信息!

2、设置必备条件

  • 1.在内核配置中,设置CONFIG_DEBUG_USER宏:

    • 运行make menuconfig,执行/DEBUG_USER命令,搜索”DEBUG_USER”
    • image-20210826224721897]
    • 依次进入:Kernel hacking-> Verbose user fault messages ,设置为Y,并重新烧内核
  • 2.设置user_debug和UDBG_SEGV宏

    • 查看user_debug定义

      • 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRnlwBNU-1629993196771)
      • 结论:当uboot传递进来的命令行字符里含有user_debug=时,便会调用user_debug_setup()->get_option(),最终会将"user_debug="后面带的字符串提取给user_debug变量。比如:当命令行字符里含有"user_debug=0xff"时,则user_debug变量等于0xff。
    • 查看UDBG_SEGV定义

      • #define UDBG_UNDEFINED  (1 << 0)        //用户态的代码出现未定义指令(UNDEFINED)
        #define UDBG_SYSCALL (1 << 1)           //用户态系统调用已过时(SYSCALL)     
        #define UDBG_BADABORT    (1 << 2)       //用户态数据错误已中止(BADABORT) 
        #define UDBG_SEGV     (1 << 3)         //用户态的代码出现段错误(SEGV)
        #define UDBG_BUS       (1 << 4)        //用户态访问忙(BUS)
        
      • 结论:只需要将user_debug设为0xff上面的所有条件就都成立。比如:当用户态的代码出现未定义指令时,由于user_debug最低位=1,所以才打印出oops

    • 总结论:启动开发板后,按任意键进入uboot。在uboot启动参数里添加: “user_debug=0xff”

      • set bootargs noinitrd root=/dev/nfs nfsroot=192.168.1.100:/home/leon/nfs_root/first_fs init=/linuxrc ip=192.168.1.50:192.168.1.102:192.168.1.1:255.255.255.0::eth0:off console=ttySAC0,115200 user_debug=0xff
        

3、启动内核、运行待调试程序、测试是否有oops信息打印

image-20210826230500910]

  • 测试结果分析:只打印了各个寄存器值,以及函数调用关系,而没有stack栈信息!

4、修改内核,打印栈信息

4.1 分析内核查找字符串”Stack: “的调用关系

  • 在内核源码中搜索Stack

  • cd linux2.6.22/arch/arm
    grep "Stack: " * -nR
    

    image-20210826231310648]

    发现字符串位于linux2.6.22/arch/arm/kernel/traps.c文件的第229行:

image-20210826231310648

  • 分析结论:如果不是用户模式的话,dump_mem函数就从regs->ARM_sp中打印出栈信息,即该函数通过sp寄存器里存的栈地址,每打印一个栈地址里的32位数据, 栈地址便加4(一个地址存8位,所以加4)。
  • 思路:仿照dump_mem函数原理,在我们应用程序调用的__do_user_fault()里打印出栈信息。

4.2 解决方案

  • 修改应用程序调用的__do_user_fault()

    • static void  __do_user_fault(struct task_struct *tsk, unsigned long addr,unsigned int fsr, unsignedint sig, int code,struct pt_regs *regs)
      {
             struct siginfo si;
          
          /*Added next 2 lines*/
             unsigned long val ;
             int i=0;
          
      #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);
                 
                  /*Added fllowed lines till to line 27*/
                  printk("Stack: \n");
                  while(i<1024){
                  /* copy_from_user()只是用来检测该地址是否有效,如有效,便获取地址数据,否则break */
                     if(copy_from_user(&val, (const void __user *)(regs->ARM_sp+i*4), 4))
                     break;
                      printk("%08x ",val);    //打印数据
                      i++;
                      if(i%8==0)
                      printk("\n");
                  }
                  printk("\n END of Stack\n");
                 
             }
      #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);
      }
      

4.3 重新编译内核、烧录、测试

  • 编译内核
cd linux2.6.22/
make uImage
  • 重启开发板,下载新内核
tftp 30000000 uImage
bootm 30000000
  • 开发板上运行测试程序,观察结果

image-20210826233513007]

5、使用oops信息(栈信息)分析调试

5.1 主机上编译得到调试程序的反汇编文件

arm-linux-objdump -D test_debug > test_debug.dis

5.2 通过oops信息定位出错位置

  • 查看oops信息,发现”PC is at 0x84ac“,即提示出错语句的地址是0x84ac
  • 打开反汇编文件,通过该0x84ac地址,找到出错的汇编语句
  • image-20210826234134452
  • 出错原因:将r3值0x12存入r2值代表的地址中。而r2等于0x00,0x00是个非法地址。

5.3 通过oops栈信息确定函数调用过程

具体分析方法,见我博客文章《如何根据栈信息确定函数调用过程》

  • 在分析过程中,发现main()函数的返回地址为40034f14,但反汇编里没有该地址。原因是我们在编译该程序是采用的是动态链接库。只有采用静态链接库的方法,才会在反汇编文件中找到main函数的调用函数:
arm-linux-gcc -o -static  test_debug test_debug.c
arm-linux-objdump -D test_debug > test_debug.dis
  • 最终, 找到main()函数的返回地址在__lobc_start_main()里

所以函数出错时的调用过程为

 __lobc_start_main()->
    main()->
    A()->
        B()->
            C()  //将0x12(r3)放入地址0x00(r2)中
相关推荐

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值