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

相关推荐
城南云小白10 分钟前
Linux网络服务只iptables防火墙工具
linux·服务器·网络
从心归零11 分钟前
sshj使用代理连接服务器
java·服务器·sshj
咩咩大主教12 分钟前
C++基于select和epoll的TCP服务器
linux·服务器·c语言·开发语言·c++·tcp/ip·io多路复用
羌俊恩17 分钟前
视频服务器:GB28181网络视频协议
服务器·网络·音视频
Zww089139 分钟前
docker部署个人网页导航
运维·docker·容器
Flying_Fish_roe39 分钟前
linux-网络管理-网络配置
linux·网络·php
运维小白。。41 分钟前
Nginx 反向代理
运维·服务器·nginx·http
PeterJXL42 分钟前
Docker-compose:管理多个容器
运维·docker·容器
FuLLovers42 分钟前
2024-09-13 冯诺依曼体系结构 OS管理 进程
linux·开发语言
海王正在撒网1 小时前
用 Docker 部署 Seafile 社区版
运维·docker·容器