• Linux ARMv8 异常向量表


    http://blog.chinaunix.net/uid-69947851-id-5830546.html

    本章接着《Linux内核启动》部分讲解,我们知道了在进入start_kernel之前,通过指令adr_l   x8, vectors;msr vbar_el1, x8设置了异常向量表,那么异常向量表的结构是怎么样的呢?在armv8中,每个异常的 向量地址不再是4字节,而是0x80字节,可以放更多的代码在向量表里面,因此

    点击(此处)折叠或打开

    1. ENTRY(vectors)
    2.     kernel_ventry 1, sync_invalid // Synchronous EL1t
    3.     kernel_ventry 1, irq_invalid // IRQ EL1t
    4.     kernel_ventry 1, fiq_invalid // FIQ EL1t
    5.     kernel_ventry 1, error_invalid // Error EL1t
    6.     kernel_ventry 1, sync // Synchronous EL1h
    7.     kernel_ventry 1, irq // IRQ EL1h
    8.     kernel_ventry 1, fiq_invalid // FIQ EL1h
    9.     kernel_ventry 1, error // Error EL1h
    10.     kernel_ventry 0, sync // Synchronous 64-bit EL0
    11.     kernel_ventry 0, irq // IRQ 64-bit EL0
    12.     kernel_ventry 0, fiq_invalid // FIQ 64-bit EL0
    13.     kernel_ventry 0, error // Error 64-bit EL0
    14. #ifdef CONFIG_COMPAT
    15.     kernel_ventry 0, sync_compat, 32 // Synchronous 32-bit EL0
    16.     kernel_ventry 0, irq_compat, 32 // IRQ 32-bit EL0
    17.     kernel_ventry 0, fiq_invalid_compat, 32 // FIQ 32-bit EL0
    18.     kernel_ventry 0, error_compat, 32 // Error 32-bit EL0
    19. #else
    20.     kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
    21.     kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
    22.     kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
    23.     kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
    24. #endif
    25. END(vectors)

    从上图可以了解到一条kernel_ventry 为一个异常,但是kernel_ventry的展开需要对齐到0x80,不够的部分用nop填充。通过上图,我们可以知道armv8有4张向量表,每张向量表有4中异常:同步异常、irq异常、fiq异常、系统错误异常,而4张表分别对应:
    1、发生中断时,异常等级不发生变化,并且不管怎么异常模式,sp只用SP_EL0
    2、发生中断时,异常等级不发生变化,并且sp用对应异常私有的SP_ELx
    3、发生中断时,异常等级发生变化,这种情况一般是用户态向内核态发生迁移,当前表示64位用户态向64位内核态发生迁移
    4、发生中断时,异常等级发生变化,这种情况一般是用户态向内核态发生迁移,当前表示32位用户态向64位/32位内核发生迁移

    下面我们来看看kernel_ventry的实现:

    点击(此处)折叠或打开

    1. .macro kernel_ventry, el, label, regsize = 64
    2.     .align 7
    3.     sub sp, sp, #S_FRAME_SIZE // 将sp预留一个fram_size, 这个size 就是struct pt_regs的大小
    4. #ifdef CONFIG_VMAP_STACK
    5.     ....这里省略掉检查栈溢出的代码
    6. #endif
    7.     b el\()\el\()_\label    // 跳转到对应级别的异常处理函数, kernel_entry 1, irq为el1_irq
    8. .endm

    对于向量表vectors中的kernel_ventry 1, irq ,  则 b el\()\el\()_\label跳转到el1_irq函数。 其中1表示的是从哪个异常模式产生的,比如是User->kernel就是0. , kernel->kernel就是1. 下面接着看el1_irq实现, el1_irq是内核运行期间产生了中断:

    点击(此处)折叠或打开

    1. el1_irq:
    2.     kernel_entry 1    // 跟进入C函数需要压栈的道理一样, 这里进入内核空间,需要保存寄存器到pt_regs,也就是前面kernel_ventry  sp预留的空间当中。
    3.     enable_da_f
    4.     irq_handler   // 中断处理函数
    5. #ifdef CONFIG_PREEMPT
    6.     ldr w24, [tsk, #TSK_TI_PREEMPT] // get preempt count
    7.     cbnz w24, 1f // preempt count != 0
    8.     ldr x0, [tsk, #TSK_TI_FLAGS] // get flags
    9.     tbz x0, #TIF_NEED_RESCHED, 1f // needs rescheduling?
    10.     bl el1_preempt     // 支持内核抢占,会在这里判断是否需要调度到新的进程。
    11. 1:
    12. #endif
    13.     kernel_exit 1  // 这里是kernel_entry 1的逆向过程,弹出栈,就是还原寄存器
    14. ENDPROC(el1_irq)
    15. el1_preempt:  // 内核抢占
    16.     mov x24, lr  // 保存lr寄存器
    17. 1:  bl  preempt_schedule_irq        // irq en/disable is done inside
    18.     ldr x0, [tsk, #TSK_TI_FLAGS]    //获取新进程的标志TI_FLAGS
    19.     tbnz    x0, #TIF_NEED_RESCHED, 1b   // 如果还有需要调度的进程,继续抢占
    20.     ret x24

    下面看看kernel_entry的实现:

    点击(此处)折叠或打开

    1. .macro kernel_entry, el, regsize = 64
    2.     /* 这里用stp指令将x0-x29保存到预留的栈中,保存顺序为下面结构体顺序
    3.     struct pt_regs {
    4.         union {
    5.              struct user_pt_regs user_regs;
    6.              struct {
    7.                  u64 regs[31];
    8.                  u64 sp;
    9.                  u64 pc;
    10.                  u64 pstate;
    11.              };
    12.         };
    13.      u64 orig_x0;
    14.     #ifdef __AARCH64EB__
    15.      u32 unused2;
    16.      s32 syscallno;
    17.     #else
    18.      s32 syscallno;
    19.      u32 unused2;
    20.     #endif
    21.      u64 orig_addr_limit;
    22.     u64 unused; // maintain 16 byte alignment
    23.     u64 stackframe[2];
    24.     }
    25.    */
    26.     stp x0, x1, [sp, #16 * 0]  ->pt_regs.regs[0] 和pt_regs.regs[1]
    27.     stp x2, x3, [sp, #16 * 1]  // 以此类推
    28.     stp x4, x5, [sp, #16 * 2]
    29.     stp x6, x7, [sp, #16 * 3]
    30.     stp x8, x9, [sp, #16 * 4]
    31.     stp x10, x11, [sp, #16 * 5]
    32.     stp x12, x13, [sp, #16 * 6]
    33.     stp x14, x15, [sp, #16 * 7]
    34.     stp x16, x17, [sp, #16 * 8]
    35.     stp x18, x19, [sp, #16 * 9]
    36.     stp x20, x21, [sp, #16 * 10]
    37.     stp x22, x23, [sp, #16 * 11]
    38.     stp x24, x25, [sp, #16 * 12]
    39.     stp x26, x27, [sp, #16 * 13]
    40.     stp x28, x29, [sp, #16 * 14]
    41.     //如果el为0 表示从用户态产生的异常
    42.     .if \el == 0
    43.     clear_gp_regs   // 清除 x0-x29寄存器
    44.     mrs x21, sp_el0 // 将用户态的sp指针保存到x21寄存器
    45.     ldr_this_cpu tsk, __entry_task, x20 // 从当前per_cpu读取当前的task_struct地址
    46.     ldr x19, [tsk, #TSK_TI_FLAGS] // 获取task->flag标记
    47.     disable_step_tsk x19, x20 // exceptions when scheduling.
    48.     .else
    49.     // 从内核状态产生的异常
    50.     add x21, sp, #S_FRAME_SIZE  // X21保存压入pt_regs数据之前的栈地址,也就是异常时,内核的栈地址
    51.     get_thread_info tsk   // 这里是从sp_el0从获取到当前task_struct结构,在启动篇看到,内核状态的时候,sp_el0用于保存内核的task_struct结构,用户态的时候, 这个sp_el0是用户态的sp
    52.     /* 保存task's original addr_limit 然后设置USER_DS */
    53.     ldr x20, [tsk, #TSK_TI_ADDR_LIMIT]
    54.     str x20, [sp, #S_ORIG_ADDR_LIMIT]
    55.     mov x20, #USER_DS
    56.     str x20, [tsk, #TSK_TI_ADDR_LIMIT]
    57.     .endif /* \el == 0 */
    58.     // x22保存异常地址
    59.     mrs x22, elr_el1
    60.     // x23保存程序状态寄存器
    61.     mrs x23, spsr_el1
    62.     stp lr, x21, [sp, #S_LR] // 将lr和sp保存到pt_regs->x[30], pt_rets->sp
    63.     // 如果发生在el1模式,则将x29和异常地址保存到pt_regs->stackframe
    64.     .if \el == 0
    65.     stp xzr, xzr, [sp, #S_STACKFRAME]
    66.     .else
    67.     stp x29, x22, [sp, #S_STACKFRAME]
    68.     .endif
    69.     add x29, sp, #S_STACKFRAME
    70.     stp x22, x23, [sp, #S_PC] // 将异常和程序状态 保存到pt_regs->pstate和pt__regs->pc
    71.     // 如果是el0->el1发了变迁, 那么将当前的task_struct给sp_el0保存
    72.     .if \el == 0
    73.     msr sp_el0, tsk
    74.     .endif
    75.     /*
    76.      * Registers that may be useful after this macro is invoked:
    77.      *
    78.      * x21 - aborted SP
    79.      * x22 - aborted PC
    80.      * x23 - aborted PSTATE
    81.     */
    82. .endm

    irq_handler为中断实现函数,具体实现如下:

    点击(此处)折叠或打开

    1. .macro irq_handler
    2.     ldr_l x1, handle_arch_irq   // 将handle_arch_irq的地址放入x1寄存器
    3.     mov x0, sp
    4.     irq_stack_entry  // 中断入口, 这里主要是切换成中断栈
    5.     blr x1          // 跳转到handle_arch_irq函数运行,这个函数是gic驱动加载的时候设置的,否则是invilid
    6.     irq_stack_exit
    7. .endm
    8. //C语言设置回调函数
    9. int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
    10. {
    11.     if (handle_arch_irq)
    12.         return -EBUSY;
    13.     handle_arch_irq = handle_irq;
    14.     return 0;
    15. }
       

    点击(此处)折叠或打开

    1. .macro irq_stack_entry
    2.     mov x19, sp // 保存当前sp到x19
    3.     /*
    4.      * 判断当前栈是不是中断栈, 如果是任务栈,就从per_cpu中读取中断栈地址,并切换到中断栈
    5.      */
    6.     ldr x25, [tsk, TSK_STACK]
    7.     eor x25, x25, x19
    8.     and x25, x25, #~(THREAD_SIZE - 1)
    9.     cbnz x25, 9998f
    10.     ldr_this_cpu x25, irq_stack_ptr, x26  // 读取per_cpu的irq_stack_ptr
    11.     mov x26, #IRQ_STACK_SIZE
    12.     add x26, x25, x26
    13.     /* 切换到中断栈 */
    14.     mov sp, x26
    15. 9998:
    16. .endm

    如果是用户态发生中断异常,则进入el0_irq, 实现如下:

    点击(此处)折叠或打开

    1. el0_irq:
    2.     kernel_entry 0   // 和el1_irq一样,只是这传入的是0表示 用户态发生异常
    3.     enable_da_f
    4.     ct_user_exit 
    5.     irq_handler  // 中断回调
    6.     b ret_to_user  // 中断返回
    7. ENDPROC(el0_irq)

    点击(此处)折叠或打开

    1. work_pending:
    2.     mov x0, sp // 'regs'
    3.     bl do_notify_resume     // 用户抢占调度和处理信号
    4.     ldr x1, [tsk, #TSK_TI_FLAGS] // re-check for single-step
    5.     b finish_ret_to_user
    6. ret_to_user:
    7.     disable_daif
    8.     ldr x1, [tsk, #TSK_TI_FLAGS]
    9.     and x2, x1, #_TIF_WORK_MASK
    10.     cbnz x2, work_pending   // 判断是否有信号或者任务挂起
    11. finish_ret_to_user:
    12.     enable_step_tsk x1, x2
    13.     kernel_exit 0           // 恢复栈
    14. ENDPROC(ret_to_user)

    do_notify_resume函数用于用户抢占和信号处理, 实现大概如下:

    点击(此处)折叠或打开

    1. asmlinkage void do_notify_resume(struct pt_regs *regs,
    2.                  unsigned long thread_flags)
    3. {
    4.     trace_hardirqs_off();
    5.     do {
    6.         /* Check valid user FS if needed */
    7.         addr_limit_user_check();
    8.         if (thread_flags & _TIF_NEED_RESCHED) {
    9.             /* Unmask Debug and SError for the next task */
    10.             local_daif_restore(DAIF_PROCCTX_NOIRQ);
    11.             schedule();   // 重新调度新的进程
    12.         } else {
    13.             local_daif_restore(DAIF_PROCCTX);
    14.             if (thread_flags & _TIF_UPROBE)
    15.                 uprobe_notify_resume(regs);
    16.             if (thread_flags & _TIF_SIGPENDING)
    17.                 do_signal(regs);   // 信号处理
    18.             if (thread_flags & _TIF_NOTIFY_RESUME) {
    19.                 clear_thread_flag(TIF_NOTIFY_RESUME);
    20.                 tracehook_notify_resume(regs);  // 工作队列
    21.                 rseq_handle_notify_resume(NULL, regs);
    22.             }
    23.             if (thread_flags & _TIF_FOREIGN_FPSTATE)
    24.                 fpsimd_restore_current_state();
    25.         }
    26.         local_daif_mask();
    27.         thread_flags = READ_ONCE(current_thread_info()->flags);
    28.     } while (thread_flags & _TIF_WORK_MASK);
    29. }

    至于el1_sync同步异常(包含系统调用,数据异常,指令异常等)这里就不多做说明了, 原理是一样的,如
    1、数据异常:el0/1_sync->el1_da->do_mem_abort->do_page_fault.
    2、系统调用:el0_sync->el0_svc->el0_svc_handler->el0_svc_common(__NR_syscalls, sys_call_table)->invoke_syscall

  • 相关阅读:
    技术管理进阶——什么是影响力
    自动化测试 — selenium + Java
    2.4 信道复用技术
    Spring 场景下突破 pebble 模板注入限制
    MySQL的enum类型的踩坑记录
    python使用matplotlib可视化subplots子图、subplots绘制子图并为可视化的多个子图设置共享的X轴
    多态语法,析构多态
    算法—5、最长回文子串
    Redis 缓存过期淘汰策略
    动态规划 -背包问题-详解
  • 原文地址:https://blog.csdn.net/RopenYuan/article/details/133920491