• 深入剖析怎么分析进程crash问题(上)


    1. 概述

            平台:linux/armv7

    2.问题

            以编程中经常发生程序段错误为例,要全方面理解段错误发生时候硬件和操作系统背后的逻辑,我们尝试思考如下几个核心问题:

    • cpu怎么知道段错误,段错误之后cpu做了哪些逻辑?
    • 异常发生后,怎么保存硬件上下文?
    • 异常发生后,怎么跳转到对应的处理程序?
    • 如果注册了signal handler发生段错误后打印crash现场的寄存器,从发生到段错误,到回调到应用进程的signal handler中间流程怎样的?这其中涉及一个有趣的问题就是怎么从内核态调用了用户态的signal handler,又自动跳转回内核执行的?

    3. 段错误的本质

      对于arm linux内核来讲,应用程序的段错误核心是一种“异常”,ARM平台定义了如下几种异常,异常向量表中定义每种异常的处理程序,如下:

            段错误发生的时候最终会调用到异常向量表中data abort对应的处理程序。

    4. 段错误发生时的硬件逻辑

         arm异常发生时硬件的动作在armv7 B1.8.3 章节有详细的描述,总结如下:

    1. 根据异常类型决定需要进入的mode。
    2. CPSR寄存器保存到相应mode对应的SPSR寄存器。
    3. 更新CPSR进入特定的模式(mode),注意:切入特定模式之后,像sp寄存器就会切入到特定模式的sp寄存器(banking register概念)。
    4. 将返回地址(PC)自动保存到对应模式的LR寄存器中。
    5. PC寄存器设置成相应的异常向量表中的对应的代码,跳转到相应的异常处理程序执行,进入到软件(OS)控制流程中。
    6. 处理完异常后,恢复异常发生前的处理器状态,即将对应模式SPSR寄存器中的数据复制到CPSR寄存器中。
    7. 设置程序计数器PC,使其指向中断发生前要执行的指令,即将对应模式的LR寄存器中的数据复制到PC寄存器中。

     细节:CPU怎么跳转到异常处理程序?

            核心cpu需要知道异常向量表的地址,这个不同的芯片平台可以有不同配置,针对arm linux一般配置在0xFFFF0000地址处,这个可以参考armv7芯片手册的B1.8.1,CP15协处理器的SCTLR寄存器可以控制具体的地址。

    5. 段错误发生时的软件(OS)逻辑

            上面"段错误发生时的硬件逻辑"第5步开始跳转到OS软件的逻辑,对于os有了解的都知道,异常发生时最重要的步骤是保存硬件上下文。这点arm和x86逻辑并不相同,x86硬件做了更多的逻辑,arm可能基于功耗的考虑,没有设计那么复杂的硬件逻辑,保存上下文完全通过软件实现。硬件上下文(即各种寄存器)保存在内核栈中(arm芯片有要求不同mode使用不同的stack,linux也遵循了这个逻辑)。

    5.1 中断向量表

    1. arch/arm/kernel/entry-armv.S
    2. .L__vectors_start:
    3. W(b) vector_rst
    4. W(b) vector_und
    5. W(ldr) pc, .L__vectors_start + 0x1000
    6. W(b) vector_pabt
    7. W(b) vector_dabt
    8. W(b) vector_addrexcptn
    9. W(b) vector_irq
    10. W(b) vector_fiq

    可以看到汇编代码中定义了armv7芯片手册中定义的各种异常处理函数,本文的段错误发生会跳转到vector_dabt函数,这个函数定义是通过vector_stub宏定义:

    1. /*
    2. * Data abort dispatcher
    3. * Enter in ABT mode, spsr = USR CPSR, lr = USR PC
    4. */
    5. vector_stub dabt, ABT_MODE, 8
    6. .long __dabt_usr @ 0 (USR_26 / USR_32)
    7. .long __dabt_invalid @ 1 (FIQ_26 / FIQ_32)
    8. .long __dabt_invalid @ 2 (IRQ_26 / IRQ_32)
    9. .long __dabt_svc @ 3 (SVC_26 / SVC_32)
    10. .long __dabt_invalid @ 4
    11. .long __dabt_invalid @ 5
    12. .long __dabt_invalid @ 6
    13. .long __dabt_invalid @ 7
    14. .long __dabt_invalid @ 8
    15. .long __dabt_invalid @ 9
    16. .long __dabt_invalid @ a
    17. .long __dabt_invalid @ b
    18. .long __dabt_invalid @ c
    19. .long __dabt_invalid @ d
    20. .long __dabt_invalid @ e
    21. .long __dabt_invalid @ f

    vector_stub宏定义了vector_dabt函数,函数体后面定义了16个入口,有效的入口是0和3索引处的__dabt_usr和__dabt_svc,用户空间发生dabt异常跳转到__dabt_usr函数,内核空间发生dabt异常跳转到__dabt_svc,如何实现这种跳转要需要分析vector_stub宏实现代码:

    1. /*
    2. * Vector stubs.
    3. *
    4. * This code is copied to 0xffff1000 so we can use branches in the
    5. * vectors, rather than ldr's. Note that this code must not exceed
    6. * a page size.
    7. *
    8. * Common stub entry macro:
    9. * Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC
    10. *
    11. * SP points to a minimal amount of processor-private memory, the address
    12. * of which is copied into r0 for the mode specific abort handler.
    13. */
    14. .macro vector_stub, name, mode, correction=0
    15. .align 5
    16. vector_\name:
    17. .if \correction
    18. sub lr, lr, #\correction
    19. .endif
    20. @
    21. @ Save r0, lr_<exception> (parent PC) and spsr_<exception>
    22. @ (parent CPSR)
    23. @
    24. stmia sp, {r0, lr} @ save r0, lr
    25. mrs lr, spsr
    26. str lr, [sp, #8] @ save spsr
    27. @
    28. @ Prepare for SVC32 mode. IRQs remain disabled.
    29. @
    30. mrs r0, cpsr
    31. eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
    32. msr spsr_cxsf, r0
    33. @
    34. @ the branch table must immediately follow this code
    35. @
    36. //获取异常之前处于哪个模式mode. 0:user 3: svc
    37. and lr, lr, #0x0f
    38. THUMB( adr r0, 1f )
    39. THUMB( ldr lr, [r0, lr, lsl #2] )
    40. //内核栈sp赋值给r0
    41. mov r0, sp
    42. //lr = pc + lr * 4,根据mode值算出来要跳转的入口,比如用户空间跳转到__dabt_usr
    43. ARM( ldr lr, [pc, lr, lsl #2] )
    44. movs pc, lr @ branch to handler in SVC mode
    45. ENDPROC(vector_\name)
    46. .align 2
    47. @ handler addresses follow this label
    48. 1:
    49. .endm

     5.2 __dabt_user

    1. __dabt_usr:
    2. //保存硬件上下文,即各种寄存器
    3. usr_entry uaccess=0
    4. kuser_cmpxchg_check
    5. sp赋值给r2,即给do_DataAbort第三个参数pt_regs传参
    6. mov r2, sp
    7. //内部实现跳转到CPU_DABORT_HANDLER,即v7_early_abort
    8. dabt_helper
    9. b ret_from_exception
    10. UNWIND(.fnend )
    11. ENDPROC(__dabt_usr)

     5.3 v7_early_abort

    1. #include <linux/linkage.h>
    2. #include <asm/assembler.h>
    3. /*
    4. * Function: v7_early_abort
    5. *
    6. * Params : r2 = pt_regs
    7. * : r4 = aborted context pc
    8. * : r5 = aborted context psr
    9. *
    10. * Returns : r4 - r11, r13 preserved
    11. *
    12. * Purpose : obtain information about current aborted instruction.
    13. */
    14. .align 5
    15. ENTRY(v7_early_abort)
    16. //armv7架构中cp15写处理的c5和c6寄存器保存了fsr和far
    17. mrc p15, 0, r1, c5, c0, 0 @ get FSR
    18. mrc p15, 0, r0, c6, c0, 0 @ get FAR
    19. uaccess_disable ip @ disable userspace access
    20. b do_DataAbort
    21. ENDPROC(v7_early_abort)
    • FSR : 失效状态寄存器(Data Fault Status Register)
    • FAR:失效地址寄存器(Data Fault Address Register)

        FSR寄存器记录发生存储失效的相关信息,包括存储访问所属域和存储访问类型,FAR记录访存失效的虚拟地址。参数r0 r1 r2准备好之后跳转到do_DataAbort执行。

    5.4 do_DataAbort

    1. /*
    2. * Dispatch a data abort to the relevant handler.
    3. */
    4. asmlinkage void __exception
    5. do_DataAbort(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
    6. {
    7. struct thread_info *thread = current_thread_info();
    8. const struct fsr_info *inf = fsr_info + fsr_fs(fsr);
    9. struct siginfo info;
    10. if (!user_mode(regs)) {
    11. thread->cpu_excp++;
    12. if (thread->cpu_excp == 1) {
    13. thread->regs_on_excp = (void *)regs;
    14. aee_excp_regs = (void *)regs;
    15. }
    16. /*
    17. * NoteXXX: The data abort exception may happen twice
    18. * when calling probe_kernel_address() in which.
    19. * __copy_from_user_inatomic() is used and the
    20. * fixup table lookup may be performed.
    21. * Check if the nested panic happens via
    22. * (cpu_excp >= 3).
    23. */
    24. if (thread->cpu_excp >= 3)
    25. aee_stop_nested_panic(regs);
    26. }
    27. if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs)) {
    28. if (!user_mode(regs))
    29. thread->cpu_excp--;
    30. return;
    31. }
    32. pr_alert("Unhandled fault: %s (0x%03x) at 0x%08lx\n",
    33. inf->name, fsr, addr);
    34. show_pte(current->mm, addr);
    35. info.si_signo = inf->sig;
    36. info.si_errno = 0;
    37. info.si_code = inf->code;
    38. info.si_addr = (void __user *)addr;
    39. arm_notify_die("", regs, &info, fsr, 0);
    40. }

    根据fsr寄存器信息得到fsr_info结构体,该结构体描述一个异常状态对应的处理逻辑,结构体定义如下:

    1. struct fsr_info {
    2. int (*fn)(unsigned long addr, unsigned int fsr, struct pt_regs *regs);
    3. int sig;
    4. int code;
    5. const char *name;
    6. };

    name是失效状态的名称,sig是处理失效时Linux内核发送的信号,fn表示修复失效状态的处理函数。结构体的具体实现如下:

    1. static struct fsr_info fsr_info[] = {
    2. /*
    3. * The following are the standard ARMv3 and ARMv4 aborts. ARMv5
    4. * defines these to be "precise" aborts.
    5. */
    6. { do_bad, SIGSEGV, 0, "vector exception" },
    7. { do_bad, SIGBUS, BUS_ADRALN, "alignment exception" },
    8. { do_bad, SIGKILL, 0, "terminal exception" },
    9. { do_bad, SIGBUS, BUS_ADRALN, "alignment exception" },
    10. { do_bad, SIGBUS, 0, "external abort on linefetch" },
    11. { do_translation_fault, SIGSEGV, SEGV_MAPERR, "section translation fault" },
    12. { do_bad, SIGBUS, 0, "external abort on linefetch" },
    13. { do_page_fault, SIGSEGV, SEGV_MAPERR, "page translation fault" },
    14. { do_bad, SIGBUS, 0, "external abort on non-linefetch" },
    15. { do_bad, SIGSEGV, SEGV_ACCERR, "section domain fault" },
    16. { do_bad, SIGBUS, 0, "external abort on non-linefetch" },
    17. { do_bad, SIGSEGV, SEGV_ACCERR, "page domain fault" },
    18. { do_bad, SIGBUS, 0, "external abort on translation" },
    19. { do_sect_fault, SIGSEGV, SEGV_ACCERR, "section permission fault" },
    20. { do_bad, SIGBUS, 0, "external abort on translation" },
    21. { do_page_fault, SIGSEGV, SEGV_ACCERR, "page permission fault" },
    22. ...
    23. /*

     针对本文举例的段错误,对应的处理函数是do_translation_fault函数。该函数中最后会给进程发送段错误信号。

    参考文章:

    linux arm内核栈切换,(转)ARM Linux中断发生时内核堆栈切换 - CodeAntenna   

  • 相关阅读:
    wustctf2020_name_your_cat
    vue 放大镜(简易)
    测试平台项目部署一(手动部署)
    成为年薪200W的云原生工程师,需要做什么?
    HTML 之 块级元素、行内元素和行内块元素之间的嵌套规则
    第二章 进程的描述与控制
    Android全新UI框架之Compose状态管理与重组
    中科大遭钓鱼邮件攻击了?3500名师生中招
    抓包工具mitmprox
    Yakit工具篇:端口探测和指纹扫描的配置和使用
  • 原文地址:https://blog.csdn.net/GetNextWindow/article/details/126365481