以SIGSEGV为例详解信号处理(与栈回溯)
信号是内核提供的向用户态进程发送信息的机制, 常见的有使用SIGUSR1唤醒用户进程执行子程序或发生段错误时使用SIGSEGV保存用户错误现场. 本文以SIGSEGV为例, 详细分析信号使用方法, 内核信号的发送与接收机制. linux
1. 信号处理例程
如下是一个SiGEGV处理例程, 主程序注册一个信号量并建立一个线程, 线程中故意访问空指针, 引起段错误. 在信号回调中会回溯堆栈, 保存出错的地址.
回溯堆栈的原理在分析完整个信号处理流程后再分析, 首先咱们先来分析如何使用信号. sigaction()用于向内核注册一个信号(参数1), 使用参数2(若是非空)做为注册信号的回调, 内核会将以前的信号回调返回在参数3中(若是非空). 若是父进程或程序以前阻塞了该信号则需先调用sigprocmask()取消阻塞.
在回调处理结束时需手动退出进程(exit()), 不然内核会不断触发该信号(从新执行异常指令再次引发崩溃), glibc对SIGSEGV有默认的回调, 因此默认状况下也会正常退出. 数组
1 #include <string.h> 2 #include <signal.h> 3 #include <stdio.h> 4 #include <unistd.h> 5 #include <pthread.h> 6 #define POPCNT(data) do { \ 7 data = (data & 0x55555555) + ((data >> 1) & 0x55555555); \ 8 data = (data & 0x33333333) + ((data >> 2) & 0x33333333); \ 9 data = (data & 0x0F0F0F0F) + ((data >> 4) & 0x0F0F0F0F); \ 10 data = (data & 0x00FF00FF) + ((data >> 8) & 0x00FF00FF); \ 11 data = (data & 0x0000FFFF) + ((data >> 16) & 0x0000FFFF); \ 12 } while (0); 13 /** 14 * we only calculate sp decrease which is static confirm in compile time 15 * that is sub immediate & push instruction(and return when we find push) 16 * 17 **/ 18 void backtrace_stack(unsigned int **pppc, unsigned int **ppsp) 19 { 20 unsigned int *ppc_last = *pppc; 21 unsigned int *psp = *ppsp; 22 unsigned int decrease = 0; 23 int i; 24 enum 25 { 26 INS_SUB_IMM = 0, 27 INS_STM1, 28 INS_STR_LR, 29 INS_STR_FP, 30 INS_BUTT 31 }; 32 //see ARM reference manual for more detail 33 struct ins_map 34 { 35 unsigned int mask; 36 unsigned int ins; 37 }; 38 struct ins_map map[INS_BUTT] = 39 { 40 {0xFFEFF000, 0xE24DD000}, 41 {0xFFFF4000, 0xE92D4000}, 42 {0xFFFFFFFF, 0xE52DE004}, 43 {0xFFFFFFFF, 0xE52DB004}, 44 }; 45 again: 46 ppc_last--; 47 for (i = 0; i < INS_BUTT; i++) 48 { 49 if (map[i].ins == (*ppc_last &map[i].mask)) 50 { 51 break; 52 } 53 } 54 switch (i) 55 { 56 case INS_SUB_IMM: 57 //sub sp, sp, imm 58 decrease = (*ppc_last & 0xFF) << ((32 - 2 * (*ppc_last & 0xF00)) % 32); 59 psp += decrease / sizeof(unsigned int); 60 break; 61 case INS_STM1: 62 //push lr, ... 63 decrease = *ppc_last & 0xFFFF; 64 POPCNT(decrease); 65 psp += decrease; 66 *pppc = *(psp - 1); 67 *ppsp = psp; 68 return; 69 case INS_STR_LR: 70 //push lr 71 psp += 1; 72 *pppc = *(psp - 1); 73 *ppsp = psp; 74 return; 75 case INS_STR_FP: 76 //push fp 77 psp += 1; 78 *ppsp = psp; 79 return; 80 default: 81 break; 82 } 83 goto again; 84 } 85 /** 86 * process stack when catch a sigsegv: 87 * ------------ stack top 88 * | ...... 89 * | fault addr sp position when memory fault happen 90 * | sigframe kernel use to resotre context DO NOT MODIFY(same to data) 91 * | siginfo glibc push this struct into stack(same to siginfo) 92 * | current sp sp position when enter signal handle 93 * 94 **/ 95 void sighandle(int sig, siginfo_t *siginfo, void *data) 96 { 97 //data point to sigframe which is not seen to user 98 //search struct ucontext in kernel for more detail 99 unsigned int *psp = ((unsigned int *)data) + 21; 100 unsigned int *plr = ((unsigned int *)data) + 22; 101 unsigned int *ppc = ((unsigned int *)data) + 23; 102 unsigned int pc_val[5] = {0}; 103 unsigned int sp_val[5] = {0}; 104 char **ppstr; 105 int i; 106 107 printf("get signal %u addr %x\n", siginfo->si_signo, siginfo->si_addr); 108 pc_val[0] = *ppc; 109 sp_val[0] = *psp; 110 for (i = 1; i < 4; i++) 111 { 112 pc_val[i] = pc_val[i - 1]; 113 sp_val[i] = sp_val[i - 1]; 114 backtrace_stack((unsigned int **)(&pc_val[i]), (unsigned int **)(&sp_val[i])); 115 /** 116 * for subroutine use push {fp} instruction, we can't get it's caller pc 117 * so we use last lr as pc and hope program won't push {fp} twice 118 * 119 **/ 120 if (pc_val[i] == pc_val[i - 1]) 121 { 122 pc_val[i] = *plr; 123 } 124 pc_val[i] -= 4; 125 } 126 ppstr = backtrace_symbols((void **)pc_val, 5); 127 for (i = 0; i < 5; i++) 128 { 129 printf("%u: pc[0x%08x] sp[0x%08x] %s\n", i, pc_val[i], sp_val[i], ppstr[i]); 130 } 131 exit(1); 132 } 133 void fault_func3() 134 { 135 int *p = NULL; 136 *p = 1; 137 } 138 void fault_func2() 139 { 140 int a = 0x5678; 141 fault_func3(); 142 return; 143 } 144 void fault_func1(void *pvoid) 145 { 146 int a = 0x1234; 147 fault_func2(); 148 return; 149 } 150 int main(int argc, char *argv[]) 151 { 152 struct sigaction sigact; 153 int *p = NULL; 154 memset(&sigact, 0, sizeof(struct sigaction)); 155 sigact.sa_sigaction = sighandle; 156 sigact.sa_flags = SA_SIGINFO | SA_RESTART; 157 sigaction(SIGSEGV, &sigact, NULL); 158 getc(stdin); 159 pthread_t thread; 160 pthread_create(&thread, NULL, fault_func1, NULL); 161 while (1) 162 { 163 ; 164 } 165 return 0; 166 }
2. 内核信号量数据结构与系统调用
虽然用户调用的sig*接口都是glibc的接口, 但实际上glibc仍是经过系统调用实现的.
与信号量相关的数据结构有:
task_struct(负责保存信号处理句柄, 阻塞与挂起的信号队列)
sighand_struct(每一个信号处理句柄, 保护信号的自旋锁)
signal_struct(信号量结构, 大部分参数都在该结构中)
sigpending(挂起队列, 用于索引挂起的信号)
做为一种信息传递机制, 信号量代码自己并不复杂, 即便是信号发送接口__send_signal()(分析见下). 数据结构
struct task_struct {
...... 架构
struct signal_struct *signal;
//信号处理句柄, 包括每一个信号的action, 锁与等待队列
struct sighand_struct *sighand; app
//该task阻塞的信号
sigset_t blocked, real_blocked;
sigset_t saved_sigmask;
//该task挂起信号的结构体
struct sigpending pending; ide
......
}; 函数
struct sighand_struct {
atomic_t count;
//保存信号处理句柄的数组
struct k_sigaction action[_NSIG];
//自旋锁, 不只保护该结构同时还保护task_struct.signal
spinlock_t siglock;
wait_queue_head_t signalfd_wqh;
}; fetch
/**
* signal_struct自身没有锁
* 由于一个共享的signal_struct每每对饮一个共享的sighand_struct
* 即便用sighand_struct的锁是signal_struct的超集
*
**/
struct signal_struct {
...... ui
//进程的信号挂起队列, 与task_struct.pending区别是全部线程共享
struct sigpending shared_pending; this
......
};
//描述挂起信号的结构体
//成员list为进程全部挂起信号的双线链表的头
//成员signal为进程挂起信号量的位图, 挂起的信号对应的位置位
struct sigpending {
//sigqueue链表头
struct list_head list;
//当前挂起的信号量位图
sigset_t signal;
};
//描述一个挂起信号的结构体
struct sigqueue {
//sigqueue链表节点
struct list_head list;
int flags;
//该挂起信号的信息
siginfo_t info;
struct user_struct *user;
};
//描述信号相关信息的结构体
typedef struct siginfo {
int si_signo;
int si_errno;
int si_code;
......
} __ARCH_SI_ATTRIBUTES siginfo_t;
1 /** 2 * 定义见kernel/signal.c 3 * 获取或修改拦截的信号 4 * @how: 为SIG_BLOCK / SIG_UNBLOCK / SIG_SETMASK的一种 5 * @nset: 若是非空为增长或移除的信号 6 * @oset: 若是非空为以前的信号 7 * note: sigprocmask系统调用任务很简单, 用新值修改current->blocked并将旧值传回用户态 8 * 调用set_current_blocked中会先剔除SIGKILL与SIGSTOP, 用户传递这两个值是无效的 9 * 以后还会判断task是否已经pending及是否有线程, 若是有还需对每一个线程单独处理 10 * 11 **/ 12 SYSCALL_DEFINE3(sigprocmask, int, how, \ 13 old_sigset_t __user *, nset, \ 14 old_sigset_t __user *, oset); 15 /** 16 * 定义见kernel/signal.c 17 * 获取或修改拦截信号的action 18 * @sig: 为拦截的信号 19 * @act: 若是非空为信号sig的action 20 * @oact: 若是非空为返回以前信号sig的action 21 * note: 若是传入未定义信号或SIGKILL与SIGSTOP会直接返回EINVAL 22 * 若是act非空则将其赋值给进程task_struct.sighand->action[i]中 23 * 而后检测所拦截的信号是否挂起, 若是有挂起则将其从队列中删除 24 * 25 **/ 26 SYSCALL_DEFINE3(sigaction, int, sig, \ 27 const struct old_sigaction __user *, act, \ 28 struct old_sigaction __user *, oact); 29 /** 30 * 定义见kernel/signal.c 31 * 如下两接口为发送信号的接口, 实际调用send_signal 32 * send_signal()调用__send_signal 33 * 34 **/ 35 int do_send_sig_info(int sig, struct siginfo *info, \ 36 struct task_struct *p, bool group); 37 int __group_send_sig_info(int sig, \ 38 struct siginfo *info, struct task_struct *p);
1 /** 2 * 定义见kernel/signal.c 3 * 实际发送信号的函数, 本接口未加锁, 需外部保证锁 4 * 5 **/ 6 static int __send_signal(int sig, struct siginfo *info, \ 7 struct task_struct *t, int group, int from_ancestor_ns) 8 { 9 //检测是否已锁, 此处使用sighand的锁是由于sighand_struct与signal_struct每每一一对应 10 assert_spin_locked(&t->sighand->siglock); 11 //调用prepare_signal判断信号是否须要发送及作其它准备状况 12 //主要是处理SIGSTOP/SIGCONT, 对于SIGCONT当即发生, 对于SIGSTOP则不是马上中止 13 //1. 对于即将退出的进程, 除SIGKILL外都不发送信号 14 //2. 若是是中止信号, 需先将进程挂起的SIGCONT移出挂起队列 15 //3. 若是是SIGCONT信号, 需先将全部中止信号都移出挂起队列同时清除线程标记位 16 //4. 判断信号是否须要忽略, 阻塞的信号不忽略, 忽略处理句柄为空与内核认为须要忽略信号 17 if (!prepare_signal(sig, t, from_ancestor_ns || (info == SEND_SIG_FORCED))) 18 goto ret; 19 pending = group &t->signal->shared_pending : &t->pending; 20 //对于已挂起信号再也不处理, 确保每种信号在队列中仅存在一个 21 if (legacy_queue(pending, sig)) 22 goto ret; 23 //对于内核内部信号如SIGSTOP或SIGKILL走捷径 24 if (info == SEND_SIG_FORCED) 25 goto out_set; 26 //实时信号必须经过sigqueue或其它实时机制入队列 27 //但考虑到内存不足时kill不容许失败因此保证至少一个信号能够传递 28 if (sig < SIGRTMIN) 29 override_rlimit = (is_si_special(info) || info->si_code >= 0); 30 else 31 override_rlimit = 0; 32 q = __sigqueue_alloc(sig, t, \ 33 GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE, override_rlimit); 34 if (q) { 35 list_add_tail(&q->list, &pending->list); 36 switch ((unsigned long) info) { 37 case (unsigned long) SEND_SIG_NOINFO: 38 q->info.si_signo = sig; 39 q->info.si_errno = 0; 40 q->info.si_code = SI_USER; 41 q->info.si_pid = task_tgid_nr_ns(current, task_active_pid_ns(t)); 42 q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid()); 43 break; 44 case (unsigned long) SEND_SIG_PRIV: 45 q->info.si_signo = sig; 46 q->info.si_errno = 0; 47 q->info.si_code = SI_KERNEL; 48 q->info.si_pid = 0; 49 q->info.si_uid = 0; 50 break; 51 default: 52 copy_siginfo(&q->info, info); 53 if (from_ancestor_ns) 54 q->info.si_pid = 0; 55 break; 56 } 57 userns_fixup_signal_uid(&q->info, t); 58 } else if (!is_si_special(info)) { 59 if (sig >= SIGRTMIN && info->si_code != SI_USER) { 60 //信号队列溢出, 放弃 61 result = TRACE_SIGNAL_OVERFLOW_FAIL; 62 ret = -EAGAIN; 63 goto ret; 64 } else { 65 //继续传递信号, 但info信息丢失 66 result = TRACE_SIGNAL_LOSE_INFO; 67 } 68 } 69 out_set: 70 signalfd_notify(t, sig); 71 //挂起队列位图对应位置位 72 sigaddset(&pending->signal, sig); 73 complete_signal(sig, t, group); 74 ret: 75 //跟踪信号生成, 该接口直接搜索不存在 76 //在include/trace/events/signal.h中宏定义 77 //其中TRACE_EVENT定义见include/linux/tracepoint.h 78 trace_signal_generate(sig, info, t, group, result); 79 return ret; 80 } 81 static void complete_signal(int sig, struct task_struct *p, int group) 82 { 83 //寻找可唤醒的线程 84 //若是信号阻塞, 进程处于退出状态, task处于中止或跟踪状态无需信号 85 //若是信号为SIGKILL, task必须接收该信号 86 //若是task运行在当前cpu上或task无信号挂起也接收信号 87 if (wants_signal(sig, p)) 88 t = p; 89 else if (!group || thread_group_empty(p)) 90 /* 91 * There is just one thread and it does not need to be woken. 92 * It will dequeue unblocked signals before it runs again. 93 */ 94 //仅一个线程无需唤醒, 自动在运行前去除未阻塞信号 95 return; 96 else { 97 t = signal->curr_target; 98 while (!wants_signal(sig, t)) { 99 t = next_thread(t); 100 if (t == signal->curr_target) 101 //遍历全部线程, 没有线程须要唤醒 102 return; 103 } 104 signal->curr_target = t; 105 } 106 //寻找可杀死的线程 107 if (sig_fatal(p, sig) && 108 !(signal->flags & (SIGNAL_UNKILLABLE | SIGNAL_GROUP_EXIT)) && 109 !sigismember(&t->real_blocked, sig) && 110 (sig == SIGKILL || !t->ptrace)) { 111 //唤醒整个线程组 112 if (!sig_kernel_coredump(sig)) { 113 signal->flags = SIGNAL_GROUP_EXIT; 114 signal->group_exit_code = sig; 115 signal->group_stop_count = 0; 116 t = p; 117 do { 118 task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK); 119 sigaddset(&t->pending.signal, SIGKILL); 120 signal_wake_up(t, 1); 121 } while_each_thread(p, t); 122 return; 123 } 124 } 125 //唤醒线程去队列中获取信号 126 signal_wake_up(t, sig == SIGKILL); 127 }
3. 信号处理流程
信号处理涉及内核最底层代码, 需了解芯片架构在内各种知识, 相对晦涩难懂.
通常对现代芯片而言当进程访问一个非法地址后MMU会修改寄存器引发内核进入异常, 在异常处理时内核会分辨非法地址产生的缘由(是真的非法地址仍是没有映射页表)并做出不一样处理. 对于处理失败的状况内核在异常处理结束时会向引发异常的task发送SIGSEGV, 在异常结束后执行调度时会首先判断该task是否有挂起信号, 若是存在则执行信号处理. 信号处理的复杂之处主要在于内核须要调用用户态程序并在程序结束后恢复内核现场. 接下来咱们以Hi3536(ARMv7)平台具体分析信号处理流程(使用3.10内核).
arm一共有7种异常处理模式, reset, und, swi, pabt, dabt, irq, fiq(reference manual A2-13).
其中与内存访问相关的有两种prefetch abort与data abort, 前者为取指令异常, 后者为数据异常.
异常向量表定义在arch/arm/kernel/entry-armv.S, __stubs_start到__stubs_end即整个异常向量表.
在内核初始化时调用early_trap_init拷贝向量表(低地址空间是用户态, 因此需搬移到0xFFFF0000).
向量表中每类异常的起始地址都是vector_stub宏, 后面跟着不一样异常向量处理函数.
以dabt为例, 先看下该宏:
1 .macro vector_stub, name, mode, correction=0 2 .align 5 3 vector_\name: 4 .if \correction 5 sub lr, lr, #\correction 6 .endif 7 @ 8 @ Save r0, lr_<exception> (parent PC) and spsr_<exception> 9 @ (parent CPSR) 10 @ 11 stmia sp, {r0, lr} @ save r0, lr 12 mrs lr, spsr 13 str lr, [sp, #8] @ save spsr 14 @ 15 @ Prepare for SVC32 mode. IRQs remain disabled. 16 @ 17 mrs r0, cpsr 18 eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE) 19 msr spsr_cxsf, r0 20 @ 21 @ the branch table must immediately follow this code 22 @ 23 and lr, lr, #0x0f 24 THUMB(adr r0, 1f) 25 THUMB(ldr lr, [r0, lr, lsl #2]) 26 mov r0, sp 27 ARM( ldr lr, [pc, lr, lsl #2]) 28 movs pc, lr @ branch to handler in SVC mode 29 ENDPROC(vector_\name)
进入异常后第一件事是保存异常模式下寄存器(若是发生嵌套异常又不保存寄存器则没法恢复异常环境).
即保存lr_<exception>与spsr_<exception>, 因为使用r0传递sp还需保存r0, 将cpsr设置为svc模式.
保存现场后第二件事是跳转到对应的异常处理函数, 因为未定义THUMB2_KERNEL, 内核所有使用ARM指令.
经过读cpsr寄存器低4位得知(经过mrs读取到lr中再位与0xF)进入异常前的运行模式.
异常向量表是连续的4字节数组, 紧跟在该代码后, 经过pc + mode * 4获得异常向量地址.
仍以dabt为例, 用户访问空指针引发abort异常, 用户模式mode bits为0, 此时即ldr lr, [pc].
因为arm架构三级流水线, pc领先实际执行两个指令, 即lr为__dabt_usr, 最后跳转到__dabt_usr执行.
若是内核访问空指针引发abort异常, 内核模式mode bits为3, 即跳转到__dabt_svc:
1 vector_stub dabt, ABT_MODE, 8 2 .long __dabt_usr @ 0 (USR_26 / USR_32) 3 .long __dabt_invalid @ 1 (FIQ_26 / FIQ_32) 4 .long __dabt_invalid @ 2 (IRQ_26 / IRQ_32) 5 .long __dabt_svc @ 3 (SVC_26 / SVC_32)
接下来进入具体异常处理函数, 咱们以__dabt_usr为例具体分析.
1 __dabt_usr: 2 usr_entry 3 kuser_cmpxchg_check 4 mov r2, sp 5 dabt_helper 6 b ret_from_exception 7 UNWIND(.fnend) 8 ENDPROC(__dabt_usr)
进入异常处理函数后第一件事是保存现场, 以前已保存了部分寄存器, usr_entry用来保存所有寄存器.
1 .macro usr_entry 2 UNWIND(.fnstart) 3 UNWIND(.cantunwind) @ don't unwind the user space 4 sub sp, sp, #S_FRAME_SIZE 5 ARM( stmib sp, {r1 - r12}) 6 THUMB( stmia sp, {r0 - r12}) 7 ldmia r0, {r3 - r5} 8 add r0, sp, #S_PC @ here for interlock avoidance 9 mov r6, #-1 10 str r3, [sp] @ save the "real" r0 copied 11 @ from the exception stack 12 @ 13 @ We are now ready to fill in the remaining blanks on the stack: 14 @ 15 @ r4 - lr_<exception>, already fixed up for correct return/restart 16 @ r5 - spsr_<exception> 17 @ r6 - orig_r0 (see pt_regs definition in ptrace.h) 18 @ 19 @ Also, separately save sp_usr and lr_usr 20 @ 21 stmia r0, {r4 - r6} 22 ARM( stmdb r0, {sp, lr}^) 23 THUMB( store_user_sp_lr r0, r1, S_SP - S_PC) 24 @ 25 @ Enable the alignment trap while in kernel mode 26 @ 27 alignment_trap r0 28 @ 29 @ Clear FP to mark the first stack frame 30 @ 31 zero_fp 32 #ifdef CONFIG_IRQSOFF_TRACER 33 bl trace_hardirqs_off 34 #endif 35 ct_user_exit save = 0 36 .endm
首先将r1-r12压栈, 注意此处没有使用push而是sp先减小再使用stmib反向压栈.
缘由是这些寄存器后面将以pt_regs形式访问, 数组排列是从低到高, 与栈增加相反.
另外r0, pc, cpsr, orig_r0是压栈传入的, 缘由分别以下.
r0需做为栈地址参数传入异常处理函数, 其原始值被修改, 因此经过栈传入.
因为pt_regs是指用户异常现场, pc与cpsr应保存异常发生时值, 但进入异常时使用影子寄存器.
因此使用压栈的lr_<exception>与spsr_<exception>(reference manual A2-13).
最后orig_r0是什么鬼? 想不清楚它的用处.
保存完用户现场后开始真正异常处理, dabt_helper的注释是调用指定的abort handler.
1 .macro dabt_helper 2 @ 3 @ Call the processor-specific abort handler: 4 @ 5 @ r2 - pt_regs 6 @ r4 - aborted context pc 7 @ r5 - aborted context psr 8 @ 9 @ The abort handler must return the aborted address in r0, and 10 @ the fault status register in r1. r9 must be preserved. 11 @ 12 #ifdef MULTI_DABORT 13 ldr ip, .LCprocfns 14 mov lr, pc 15 ldr pc, [ip, #PROCESSOR_DABT_FUNC] 16 #else 17 bl CPU_DABORT_HANDLER 18 #endif 19 .endm 20 #ifdef MULTI_DABORT 21 .LCprocfns: 22 .word processor 23 #endif
其中pt_regs保存在r2中, abort时的pc指针保存在r4中, abort时的cpsr保存在r5中.
handler返回时abort地址保存在r0中, 错误状态寄存器(fsr)保存在r1中, r9保留.
宏MULTI_DABORT定义见arch/arm/include/asm/glue-df.h, 由不一样架构决定, ARMv7架构定义了该宏.
对于定义MULTI_DABORT宏的架构, ldr pc, [ip, #PROCESSOR_DABT_FUNC]是跳转的关键.
.LCprocfns段存放的是全局变量processor, 其定义在arch/arm/include/asm/proc-fns.h.
PROCESSOR_DABT_FUNC定义见arch/arm/kernel/asm-offsets.c, 即指向processor._data_abort.
.
全局变量processor是如何初始化的? 答案见setup_processor(defined in arch/arm/kernel/setup.c).
在setup_processor中会调用lookup_processor_type(defined in arch/arm/kernel/head-common.S):
1 ENTRY(lookup_processor_type) 2 stmfd sp!, {r4 - r6, r9, lr} 3 mov r9, r0 4 bl __lookup_processor_type 5 mov r0, r5 6 ldmfd sp!, {r4 - r6, r9, pc} 7 ENDPROC(lookup_processor_type) 8 __lookup_processor_type: 9 adr r3, __lookup_processor_type_data 10 ldmia r3, {r4 - r6} 11 sub r3, r3, r4 @ get offset between virt&phys 12 add r5, r5, r3 @ convert virt addresses to 13 add r6, r6, r3 @ physical address space 14 1: ldmia r5, {r3, r4} @ value, mask 15 and r4, r4, r9 @ mask wanted bits 16 teq r3, r4 17 beq 2f 18 add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) 19 cmp r5, r6 20 blo 1b 21 mov r5, #0 @ unknown processor 22 2: mov pc, lr 23 ENDPROC(__lookup_processor_type)
__lookup_processor_type的注释解释了代码意图: 从CP15读取处理器id并从连接时创建的数组中查找.
因为此时未开启MMU所以没法使用绝对地址索引proc_info, 需根据偏移来计算.
lookup_processor_type首先将cpuid保存在r9, 而后获取程序装载地址的偏移.
__lookup_processor_type_data是数据段对象, 其包含两个数据__proc_info_begin与__proc_info_end.
经过arch/arm/kernel/vmlinux.lds.S能够得知该地址区间保存.proc.info.init数据.
r3是编译时的程序地址, r4是运行时的实际地址.
r3与r4相减即无MMU时程序加载地址相对程序文件地址的偏移.
r5与r6分别为__lookup_processor_type_data数据段的起始地址与结束地址.
将r5地址前两个成员(cpu_val与cpu_mask)保存在r3与r4, 将其与cpuid比较, 若是符合则跳出循环.
若是不符合则取r5下一个元素地址与r6比较, 溢出说明数组越界r5设为0, 不然重复上一步比较.
在分析了processor的初始化后, 咱们再来看下.proc.info.init数组是如何定义的.
此处代码与架构强相关, 每一个芯片都有差别, 仅以基于ARMv7架构为例:
1 .macro __v7_proc initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0, proc_fns = v7_processor_functions 2 ALT_SMP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \ 3 PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags) 4 ALT_UP(.long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \ 5 PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags) 6 .long PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \ 7 PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags 8 W(b) \initfunc 9 .long cpu_arch_nam 10 .long cpu_elf_name 11 .long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \ 12 HWCAP_EDSP | HWCAP_TLS | \hwcaps 13 .long cpu_v7_name 14 .long \proc_fns 15 .long v7wbi_tlb_fns 16 .long v6_user_fns 17 .long v7_cache_fns 18 .endm
宏__v7_proc(defined in arch/arm/mm/proc-v7.S)做用是生成一个struct proc_info_list实例.
在arch/arm/mm/proc-v7.S中有多个用该宏定义的实例, 这些实例都放在.proc.info.init段中.
每一个实例对应一类芯片, __v7_proc_info是大部分ARMv7处理器对应的struct proc_info_list的实例.
__v7_proc_info的processor成员是v7_processor_functions, 再来看看该成员.
直接搜索该名字找不到定义的, 由于它是经过宏定义的生成的(烦不烦- -!).
1 .macro define_processor_functions name:req, dabort:req, pabort:req, nommu=0, suspend=0 2 .type \name\()_processor_functions, #object 3 .align 2 4 ENTRY(\name\()_processor_functions) 5 .word \dabort 6 .word \pabort 7 .word cpu_\name\()_proc_init 8 .word cpu_\name\()_proc_fin 9 .word cpu_\name\()_reset 10 .word cpu_\name\()_do_idle 11 .word cpu_\name\()_dcache_clean_area 12 .word cpu_\name\()_switch_mm 13 .if \nommu 14 .word 0 15 .else 16 .word cpu_\name\()_set_pte_ext 17 .endif 18 .if \suspend 19 .word cpu_\name\()_suspend_size 20 #ifdef CONFIG_PM_SLEEP 21 .word cpu_\name\()_do_suspend 22 .word cpu_\name\()_do_resume 23 #else 24 .word 0 25 .word 0 26 #endif 27 .else 28 .word 0 29 .word 0 30 .word 0 31 .endif 32 .size \name\()_processor_functions, . - \name\()_processor_functions 33 .endm 34 define_processor_functions v7, dabort=v7_early_abort, pabort=v7_pabort, suspend=1
宏define_processor_functions(defined in arch/arm/mm/proc-macro.S).
该宏做用是生成一个struct processor实例, 联系对该宏的调用终于能够摸索出咱们想要的回调了.
在lookup_processor_type返回后r0保存着proc_info_list地址, 对ARMv7架构而言.
返回的proc_info_list为__v7_proc_info(defined in arch/arm/mm/proc-v7.S).
其processor成员为v7_processor_functions, 它是由宏展开的, 其_data_abort成员为v7_early_abort.
再来看v7_early_abort(defined in arch/arm/mm/abort-ev7.S):
1 ENTRY(v7_early_abort) 2 /* 3 * The effect of data aborts on on the exclusive access monitor are 4 * UNPREDICTABLE. Do a CLREX to clear the state 5 */ 6 clrex 7 mrc p15, 0, r1, c5, c0, 0 @ get FSR 8 mrc p15, 0, r0, c6, c0, 0 @ get FAR 9 /* 10 * V6 code adjusts the returned DFSR. 11 * New designs should not need to patch up faults. 12 */ 13 #if defined(CONFIG_VERIFY_PERMISSION_FAULT) 14 /* 15 * Detect erroneous permission failures and fix 16 */ 17 ldr r3, =0x40d @ On permission fault 18 and r3, r1, r3 19 cmp r3, #0x0d 20 bne do_DataAbort 21 mcr p15, 0, r0, c7, c8, 0 @ Retranslate FAR 22 isb 23 mrc p15, 0, ip, c7, c4, 0 @ Read the PAR 24 and r3, ip, #0x7b @ On translation fault 25 cmp r3, #0x0b 26 bne do_DataAbort 27 bic r1, r1, #0xf @ Fix up FSR FS[5:0] 28 and ip, ip, #0x7e 29 orr r1, r1, ip, LSR #1 30 #endif 31 b do_DataAbort 32 ENDPROC(v7_early_abort)
v7_early_abort很简单, 先对FSR与FAR的处理(reference manual B3-18), 而后调用do_DataAbort.
使用r0保存FAR(fault address register), 使用r1保存FSR(fault status register), 后面会用到.
1 asmlinkage void __exception 2 do_DataAbort(unsigned long addr, unsigned int fsr, struct pt_regs *regs) 3 { 4 const struct fsr_info *inf = fsr_info + fsr_fs(fsr); 5 struct siginfo info; 6 if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs)) 7 return; 8 printk(KERN_ALERT "Unhandled fault: %s (0x%03x) at 0x%08lx\n", 9 inf->name, fsr, addr); 10 info.si_signo = inf->sig; 11 info.si_errno = 0; 12 info.si_code = inf->code; 13 info.si_addr = (void __user *)addr; 14 arm_notify_die("", regs, &info, fsr, 0); 15 } 16 struct fsr_info { 17 int (*fn)(unsigned long addr, unsigned int fsr, struct pt_regs *regs); 18 int sig; 19 int code; 20 const char *name; 21 }; 22 /* FSR definition */ 23 #ifdef CONFIG_ARM_LPAE 24 #include "fsr-3level.c" 25 #else 26 #include "fsr-2level.c" 27 #endif
do_DataAbort也很简单, 调用fsr_info数组某个元素的回调, 返回后根据结果向进程发送信号.
因为未开启ARM_LPAE(ARM large page support), 此处使用fsr-2level.c的数组(太大了不拷贝).
.
以page fault为例, 调用do_page_fault, 当找不到页表时会调用__do_user_fault向用户进程发送信号.
回到__dabt_usr, 在abort handler返回后调用ret_from_exception退出异常.
1 ENTRY(ret_from_exception) 2 UNWIND(.fnstart) 3 UNWIND(.cantunwind) 4 get_thread_info tsk 5 mov why, #0 6 b ret_to_user 7 UNWIND(.fnend) 8 ENDPROC(__pabt_usr) 9 ENDPROC(ret_from_exception) 10 ENTRY(ret_to_user) 11 ret_slow_syscall: 12 disable_irq @ disable interrupts 13 ENTRY(ret_to_user_from_irq) 14 ldr r1, [tsk, #TI_FLAGS] 15 tst r1, #_TIF_WORK_MASK 16 bne work_pending 17 no_work_pending: 18 asm_trace_hardirqs_on 19 /* perform architecture specific actions before user return */ 20 arch_ret_to_user r1, lr 21 ct_user_enter save = 0 22 restore_user_regs fast = 0, offset = 0 23 ENDPROC(ret_to_user_from_irq) 24 ENDPROC(ret_to_user)
ret_to_user首先会关中断, 检查thread_info->flags.
如发现须要调度的标记执行work_pending(defined in arch/arm/kernel/entry-common.S).
1 work_pending: 2 mov r0, sp @ 'regs' 3 mov r2, why @ 'syscall' 4 bl do_work_pending 5 cmp r0, #0 6 beq no_work_pending 7 movlt scno, #(__NR_restart_syscall - __NR_SYSCALL_BASE) 8 ldmia sp, {r0 - r6} @ have to reload r0 - r6 9 b local_restart @ ... and off we go
do_work_pending(defined in arch/arm/kernel/signal.c)的做用是判断是否须要调度或信号处理:
1 asmlinkage int do_work_pending(struct pt_regs *regs, \ 2 unsigned int thread_flags, int syscall); 3 { 4 do { 5 /** 6 * ret_to_user_from_irq中已将r1赋值为thread_info->flags, 即此处thread_flags 7 * 一样regs值为态sp, syscall值为why 8 * thread_flags可能有多个位置位, 按顺序依次处理 9 * 10 **/ 11 if (likely(thread_flags & _TIF_NEED_RESCHED)) { 12 schedule(); 13 } else { 14 /** 15 * 若是CPSR模式位不在用户态, 即以前程序就工做在内核态 16 * 被高优先级的任务抢占(好比系统调用时被中断打断) 17 * 那么此时直接返回继续以前任务 18 * 19 **/ 20 if (unlikely(!user_mode(regs))) 21 return 0; 22 local_irq_enable(); 23 /** 24 * 判断是否有信号挂起 25 * 该标记位在signal_wake_up_state与recalc_sigpending_tsk设置 26 * 27 **/ 28 if (thread_flags & _TIF_SIGPENDING) { 29 //do_signal(defined in arch/arm/kernel/signal.c)定义见下 30 int restart = do_signal(regs, syscall); 31 if (unlikely(restart)) { 32 //处理失败直接返回, 不调用回调 33 return restart; 34 } 35 syscall = 0; 36 } else { 37 clear_thread_flag(TIF_NOTIFY_RESUME); 38 tracehook_notify_resume(regs); 39 } 40 } 41 local_irq_disable(); 42 thread_flags = current_thread_info()->flags; 43 } while (thread_flags & _TIF_WORK_MASK); 44 return 0; 45 }
do_signal做用是处理挂起信号, 保存内核寄存器状态, 为内核执行用户态回调作准备.
保存数据的缘由: 内核态与用户态共用一套寄存器.
当用户回调返回时内核寄存器状态已被破坏, 所以须要在用户态保存内核寄存器状态.
1 static int do_signal(struct pt_regs *regs, int syscall) 2 { 3 ...... 4 /** 5 * 实际调用get_signal_to_deliver(defined in kernel/signal.c) 6 * get_signal_to_deliver中调用dequeue_signal先从task_struct->pending获取信号 7 * 获取失败再从task_struct->signal->shared_pending获取信号 8 * 还有不少判断, 先忽略 9 * 10 **/ 11 if (get_signal(&ksig)) { 12 /** 13 * 在执行信号回调句柄前准备工做, 在用户态栈保存内核数据 14 * handle_signal实际调用setup_frame或setup_rt_frame(若是为rt信号) 15 * 以setup_frame为例: 16 * 1. 首先调用get_sigframe获取用户态栈地址, 对齐并确承认写 17 * 注意sigframe结构体的排布, 在用户态获取lr时会用到该结构 18 * 2. 设置uc.uc_flags为0x5a3c3c5a 19 * 3. 调用setup_sigframe填充sigframe结构 20 * 4. 调用setup_return设置回调接口返回(设置pt_regs) 21 * 注意此时pt_regs仍在栈上: 22 * pt_regs->pc设置为信号回调句柄 23 * pt_regs->r0设置为signo 24 * pt_regs->lr被修改成retcode 25 * pt_regs->sp被修改成frame(frame是结构体起始地址, 与栈方向相反, 因此是栈底!) 26 * 在栈帧创建后调用signal_setup_done恢复阻塞的信号 27 * 28 **/ 29 handle_signal(&ksig, regs); 30 } 31 ...... 32 }
回到work_pending, 当do_work_pending返回时会检查函数返回值(r0).
若是返回成功则跳转到no_work_pending标签, 此时开始准备进入用户态.
其中arch_ret_to_user宏是架构相关宏, ARM上无定义; ct_user_enter是跟踪上下文宏, 忽略.
重点在restore_user_regs(defined in arch/arm/kernel/entry-header.S).
1 .macro restore_user_regs, fast = 0, offset = 0 2 clrex @ clear the exclusive monitor 3 mov r2, sp 4 load_user_sp_lr r2, r3, \offset + S_SP @ calling sp, lr 5 ldr r1, [sp, #\offset + S_PSR] @ get calling cpsr 6 ldr lr, [sp, #\offset + S_PC] @ get pc 7 add sp, sp, #\offset + S_SP 8 msr spsr_cxsf, r1 @ save in spsr_svc 9 .if \fast 10 ldmdb sp, {r1 - r12} @ get calling r1 - r12 11 .else 12 ldmdb sp, {r0 - r12} @ get calling r0 - r12 13 .endif 14 add sp, sp, #S_FRAME_SIZE - S_SP 15 movs pc, lr @ return & move spsr_svc into cpsr 16 .endm 17 .macro load_user_sp_lr, rd, rtemp, offset = 0 18 mrs \rtemp, cpsr 19 eor \rtemp, \rtemp, #(SVC_MODE ^ SYSTEM_MODE) 20 msr cpsr_c, \rtemp @ switch to the SYS mode 21 ldr sp, [\rd, #\offset] @ load sp_usr 22 ldr lr, [\rd, #\offset + 4] @ load lr_usr 23 eor \rtemp, \rtemp, #(SVC_MODE ^ SYSTEM_MODE) 24 msr cpsr_c, \rtemp @ switch back to the SVC mode 25 .endm
clrex用于清除本地cpu独占访问某块内存区域的标记.
S_SP定义见arch/arm/kernel/asm-offsets.c, 是ARM_sp在pt_regs的偏移.
对sp与lr的保存需额外切换到系统模式后处理, 是由于SVC模式下使用sp_svc与lr_svc.
而系统模式与用户模式使用同一套寄存器, 仅权限不一样.
再根据是否为fast_path恢复用户寄存器, 同时恢复sp(此处sp为SVC模式的sp).
最后将lr拷贝给pc, 此指令会自动恢复cpsr, 不要问我为何reference manual就是这么写的.
至此开始用户子程的执行.
4. 用户进程回溯堆栈 回到第一部分, 如何在信号回调中回溯堆栈? 回顾以前的流程, 当用户进程访问非法地址时当即触发异常, 程序跳转到异常向量, 处理器模式进入异常模式使用异常模式下sp与lr, 当执行完异常处理后cpu恢复到特权模式处理, 此时使用特权模式下sp与lr, 为保证程序在执行完信号回调后能正常恢复特权模式现场, 须要在用户态保存现场, 即do_signal中的sigframe(在用户态即信号回调的参数3), 回到用户态进程还须要入栈一个siginfo结构, 所以用户进程栈结构为: 栈顶 ... 异常发生时栈地址 sigframe siginfo 信号回调地址 经过sigframe咱们能够获取异常发生时寄存器列表, 即获取异常时sp, pc, lr, 进一步回溯整个堆栈.