以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 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, 进一步回溯整个堆栈.

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

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

  1. Python Unittest-根据不同测试环境跳过用例详解

    Python Unittest-根据不同测试环境跳过用例详解 本文章会讲述以下几个内容: 1.Unittest 如何跳过用例 2.如何使用sys.argv 3.自动化测试项目中如何一套代码多套环境运行 ...

  2. Linux用户、权限及改变文件所有者及文件所属组多例详解 附python代码

    https://blog.csdn.net/hanhanwanghaha宝藏女孩 欢迎您的关注! 欢迎关注微信公众号:宝藏女孩的成长日记 如有转载,请注明出处(如不注明,盗者必究) Linux用户.权 ...

  3. 必过SafetyNet!以MIUI开发版系统为例详解Android设备通过SafetyNet校验方法

    必过SafetyNet!以MIUI开发版系统为例详解Android设备通过SafetyNet校验方法 作者 梓沐啊_(KylinDemons) 版权声明 Copyright © 2021 KylinD ...

  4. STM32H750 更好用的CANFD 用例详解

    目录 前言 Message RAM分配 STM32工程搭建 串口配置 100us定时器 FDCAN配置 Bus-Off处理 新消息接收处理 发送处理 使用Xavier配合测试一下 完整工程下载 关于用 ...

  5. C/C++趣味编程经典100例详解

    更新记录   2019-08-27 28题 重写     C/C++语言经典.实用.趣味程序设计编程百例精解 1.绘制余弦曲线 在屏幕上用"*"显示0~360度的余弦函数cos(x ...

  6. 【数据结构】共享栈详解 判断共享栈满条件栈顶指针变化详解记忆方法例题

    摘要:简单易懂,详细地介绍共享栈概念,指针,判断共享栈栈满条件以及记忆方法等 目录 共享栈概念 栈顶指针&变化详解 栈顶指针种类的记忆方法 判断栈满条件 判断栈满条件的记忆方法 例题 解题思路 ...

  7. 常量池详解(含栈、堆、方法区简析)

    1 位置分布图 2 内存区域类型 寄存器:最快的存储区, 由编译器根据需求进行分配,我们在程序中无法控制: 堆:存放所有new出来的对象: 栈:存放基本类型的变量数据和对象的引用,但对象本身不存放在栈 ...

  8. 一起学DNS系列(十)图、例详解DNS递归和迭代查询原理及过程 (1)

    上节中提到了一些有关递归查询的内容,但说的很少,也很笼统,本节将会从原理和实例两方面入手分析DNS的递归以及迭代查询. 在此之前,我们需要了解一些背景知识,以便于更好的理解今天的主题内容. 在互联网中 ...

  9. python discover()没有加载测试用例_对python_discover方法遍历所有执行的用例详解

    当我们写了一个单个py的测试文件时直接运行就ok了,但当我们有很多很多个这样的py时,难道要一个一个的点击来运行吗,当然不是.我们可以通过discover方法来找到所有的用例. 下面直接举例说明dis ...

最新文章

  1. 构造 ---- D. AB Graph(偶数和3远环的关系)
  2. nginx 停止服务方法
  3. C#将图像文件压缩为AVI文件播放
  4. hellocharts-android开源图表库(效果非常好)
  5. Ubuntu16.04 安装RabbitMQ
  6. 用于matplotlib对齐很有用的算法,可用于面试笔试
  7. explain 之 type
  8. arraylist 初始化_ArrayList实现原理(JDK1.8)
  9. centos 7 升级/安装 git 2.7.3
  10. pxe装机原理_linux PXE装机详解(非常详细,小白专用)
  11. 我从小米裸辞后进Shopee了...
  12. bzoj1833: [ZJOI2010]count 数字计数(数位dp)
  13. php计算面积,PHP中长方形的面积怎么求
  14. 第21章 DHCP
  15. PHP函数计算中英文字符串长度的方法
  16. Java实习生常规技术面试题每日十题Java基础(五)
  17. 最常访问的几个技术网站
  18. Android设备虚拟摄像头技术实现
  19. python提示IndentationError: unexpected indent错误
  20. 【初学者必看】vlc实现的rtsp服务器及转储H264文件

热门文章

  1. Ubuntu 14.04 64位上配置JDK操作步骤
  2. Caffe源码中Pooling Layer文件分析
  3. VS2013中Image Watch插件的使用(OpenCV)
  4. gb50243-2016通风与空调工程施工质量验收规范_《通风与空调工程施工质量验收规范》GB50243-2016 重点解读...
  5. st7789v tft 驱动电路_图解宁波博信出品的KEEWAY摩托车数字仪表盘,附测绘的电路图...
  6. co88 sap 实际结算_SAP中有关差异的一些概念
  7. Java项目:星际争霸游戏(java+swing+awt界面编程+IO输入输出流+socket+udp网络通信)
  8. python元组转字典_python中怎么将元组、字典转化为列表
  9. node.js 出现cannot find module ‘xxx‘ 解决办法
  10. js取一定范围内的随机整数