以SIGSEGV为例详解信号处理(与栈回溯)

以SIGSEGV为例详解信号处理(与栈回溯)
信号是内核提供的向用户态进程发送信息的机制, 常见的有使用SIGUSR1唤醒用户进程执行子程序或发生段错误时使用SIGSEGV保存用户错误现场. 本文以SIGSEGV为例, 详细分析信号使用方法, 内核信号的发送与接收机制.

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;

    //该task阻塞的信号
    sigset_t blocked, real_blocked;
    sigset_t saved_sigmask;
    //该task挂起信号的结构体
    struct sigpending pending;

    ......
};

struct sighand_struct {
    atomic_t count;
    //保存信号处理句柄的数组
    struct k_sigaction action[_NSIG];
    //自旋锁, 不仅保护该结构同时还保护task_struct.signal
    spinlock_t siglock;
    wait_queue_head_t signalfd_wqh;
};

/**
 * signal_struct自身没有锁
 * 因为一个共享的signal_struct往往对饮一个共享的sighand_struct
 * 即使用sighand_struct的锁是signal_struct的超集
 *
**/
struct signal_struct {
    ......

    //进程的信号挂起队列, 与task_struct.pending区别是所有线程共享
    struct sigpending shared_pending;

    ......
};

//描述挂起信号的结构体
//成员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 2mov 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, 进一步回溯整个堆栈.

 

转载于:https://www.cnblogs.com/Five100Miles/p/8458732.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
SIGSEGV (Segmentation fault)是一种常见的错误信号,它通常发生在程序试图访问无效的内存地址或者试图访问没有相应物理内存的地址时。这种错误通常产生于以下几种情况: 1. 缓冲区溢出:当指针引用超过其范围时,导致错误。这通常是因为指针引用了一个超出范围的缓冲区而导致的。 2. 溢出:当程序使用的空间超出了系统默认的大小时,就会发生溢出错误。 3. 非法文件访问:在某些系统中,对于禁止对文件进行操作的情况下,尝试对文件进行操作会导致此错误。 此外,SIGSEGVSIGBUS信号之间也有一些区别。SIGBUS(总线错误)表示指针所指的地址是有效地址,但是总线无法正常使用该指针。通常是由于未对齐的数据访问引起的。而SIGSEGV(段错误)表示指针所指的地址是无效的,即没有与该地址对应的物理内存。 当遇到SIGSEGV错误时,可以通过以下方法来查找错误的根源: 1. 使用gdb调试工具:编译时使用gcc -g选项,运行程序之后等待coredump生成,然后可以使用gdb来查看调用,定位错误。 2. 使用strace工具:运行程序时使用strace命令,它可以显示程序在执行过程中的系统调用,可以帮助找到出错的系统调用。 总结起来,SIGSEGV (Segmentation fault)是一种常见的错误信号,通常发生在程序试图访问无效的内存地址或者试图访问没有相应物理内存的地址时。可以通过使用调试工具如gdb和strace来定位错误。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值