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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM