以SIGSEGV爲例詳解信號處理(與棧回溯)

以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 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, 進一步回溯整個堆棧.

相關文章
相關標籤/搜索