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

相关推荐
荼靡6037 分钟前
shell(三)
linux·服务器·数据库
zym大哥大16 分钟前
Linux的权限
linux·服务器
Stark-C19 分钟前
功能齐全,支持协作 | Docker部署一款支持多人共享的私密浏览器『n.eko』
运维·docker·容器
嘟嘟Listing39 分钟前
设置jenkins时区记录
运维·jenkins
嘟嘟Listing40 分钟前
jenkins docker记录
java·运维·jenkins
伴野星辰41 分钟前
小乌龟TortoiseGit 安装和语言包选择
linux·运维·服务器
枫叶丹41 小时前
【在Linux世界中追寻伟大的One Piece】多线程(一)
java·linux·运维
残念ing1 小时前
【Linux】—简单实现一个shell(myshell)
linux·运维·服务器
明月心9521 小时前
linux mount nfs开机自动挂载远程目录
linux·运维·服务器
Ray55051 小时前
bridge-multicast-igmpsnooping
linux·服务器·网络