ARM Linux系統(tǒng)中的用戶棧與內核棧
用戶棧
用戶棧就是應用程序直接使用的棧。如下圖所示,它位于應用程序的用戶進程空間的最頂端。
本文引用地址:http://www.ex-cimer.com/article/201611/317854.htm當用戶程序逐級調用函數(shù)時,用戶棧從高地址向低地址方向擴展,每次增加一個棧幀,一個棧幀中存放的是函數(shù)的參數(shù)、返回地址和局部變量等,所以棧幀的長度是不定的。
用戶棧的棧底靠近進程空間的上邊緣,但一般不會剛好對齊到邊緣,出于安全考慮,會在棧底與進程上邊緣之間插入一段隨機大小的隔離區(qū)。這樣,程序在每次運行時,棧的位置都不同,這樣黑客就不大容易利用基于棧的安全漏洞來實施攻擊。
用戶棧的伸縮對于應用程序來說是透明的,應用程序不需要自己去管理棧,這是操作系統(tǒng)提供的功能。應用程序在剛剛啟動的時候(由fork()系統(tǒng)調用復制出新的進程),新的進程其實并不占有任何棧的空間。當應用程序中調用了函數(shù)需要壓棧時,會觸發(fā)一個page fault,內核在處理這個異常里會發(fā)現(xiàn)進程需要新的??臻g,于是建立新的VMA并映射內存給用戶棧。
內核棧
內核棧對于應用程序是不可見的,因為它位于內核空間中。在應用程序執(zhí)行過程中,如果發(fā)生異常、中斷或系統(tǒng)調用的話,應用程序會被暫停,系統(tǒng)進入內核態(tài),轉去執(zhí)行異常響應等代碼,這個時候所使用的棧就是內核棧。
與用戶棧相比,內核棧的尺寸要小得多。在32位Linux系統(tǒng)上,用戶棧最多可以擴展到64M,但內核棧最多也只有8K字節(jié),而且有時為了提高內存利用率還常常把內核棧配置成4K。其實即使是只有4K,在絕大多數(shù)情況下也仍然是夠用的,因為這里只是給內核代碼使用的,棧不會很大。
每個進程在內核空間中都擁有一個對應的內核棧,而且這個棧是在進程fork的時候就預留出來的。以下是創(chuàng)建內核棧的代碼(Kernel 2.6.35 版本):
[c]static struct task_struct *dup_task_struct(struct task_struct *orig){struct task_struct *tsk;struct thread_info *ti;......ti = alloc_thread_info(tsk);......tsk->stack = ti;......}[/c]
內核棧的結構比較精巧,內核使用一個聯(lián)合體來定義內核棧:
[c]union thread_union {struct thread_info thread_info;unsigned long stack[THREAD_SIZE/sizeof(long)];};[/c]
其中thread_info中存放了進程/線程(內核不大區(qū)分進程與線程)的一些數(shù)據(jù),其中包括指向task_struct結構的指針。數(shù)組stack即內核棧,stack占據(jù)8K/4K(依配置不同)空間,是這個聯(lián)合體的主要部分。
這樣,一個實際的內核棧的結構將如下圖所示。由于棧總是由高地址向低地址延伸的,所以棧底位于thread_union聯(lián)合體的最末端,而thread_info結構則位于thread_union聯(lián)合體的開始處,而且所占用的空間比較少。只要不出現(xiàn)內核棧特別大的極端情況,棧與thread_info可以互不干擾。
為什么要設計成這樣的結構呢?原因就在于,使用這種結構可以在系統(tǒng)進入內核態(tài)時很方便地取得當前進程的信息。如果不用這種方式的話,取得task_struct將是一個比較麻煩的事情。
不管系統(tǒng)因為什么原因進入內核態(tài),最后都要切換到SVC模式做主要的異常處理。在進入SVC模式時,SP/R13寄存器所指向的位置就正好是當前進程的內核棧。通過簡單的對齊操作,就可以拿到thread_union即thread_info結構的指針,從中又可以得到最重要的task_struct的指針,這個進程的所有信息就都有了。
以下兩個函數(shù)即分別用于從SP寄存器取得當前進程的thread_info,以及進一步取得task_struct結構的內容。
[c]static inline struct thread_info *current_thread_info(void){register unsigned long sp asm ("sp");return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));}static inline struct task_struct *get_current(void){return current_thread_info()->task;}[/c]
關于SP寄存器,這里有一個問題值得澄清一下,前面提到“在進入SVC模式時,SP/R13寄存器所指向的位置就正好是當前進程的內核棧”,原因是什么呢?
在當前進程即當前被異?;蛑袛嗨鶗和5倪@個進程,是在上一次發(fā)生進程調度(schedule())的時候被調入的,當時在“上下文切換”(context_switch())完成的時候,當前這個進程可以說已經(jīng)被調入了CPU,系統(tǒng)當時所處的模式也是SVC模式。當進程高度完成,CPU從SVC模式切換到USR模式時候,SVC模式下的SP寄存器已經(jīng)指向了當前進程的內核棧。所以當再次切換到SVC模式時,進程還是這個進程,SP也還是指向這個內核棧。
其實ARM處理器的每一種模式下都有自己獨立的SP/R13寄存器。當CPU在不同的模式間切換的時候所看到的寄存器內容都是不同的。Linux對于各種模式的使用策略是:SVC和USR兩種模式是可以穩(wěn)定工作的模式;在其它的模式下都是不穩(wěn)定的,會盡快切換到穩(wěn)定的模式去工作。在SVC模式下,SP寄存器總是指向內核棧;在USR模式下,SP寄存器總是指向用戶棧;那么,其它模式下,SP又指向哪里呢?
其它模式下,Linux對于SP寄存器的維護很簡單。在系統(tǒng)啟動階段,cpu_init()函數(shù)會被調用,其中有對其它模式下SP寄存器的初始化操作:
[c]struct stack {u32 irq[3];u32 abt[3];u32 und[3];} ____cacheline_aligned;static struct stack stacks[NR_CPUS];void cpu_init(void){unsigned int cpu = smp_processor_id();struct stack *stk = &stacks[cpu];__asm__ ("msr cpsr_c, %1nt""add r14, %0, %2nt""mov sp, r14nt""msr cpsr_c, %3nt""add r14, %0, %4nt""mov sp, r14nt""msr cpsr_c, %5nt""add r14, %0, %6nt""mov sp, r14nt""msr cpsr_c, %7":: "r" (stk),PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),"I" (offsetof(struct stack, irq[0])),PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),"I" (offsetof(struct stack, abt[0])),PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE),"I" (offsetof(struct stack, und[0])),PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE): "r14");}[/c]
可以看到,Linux為IRQABTUND3種模式的SP寄存器指定了相應的棧,但是這個棧很小很小,只有12個字節(jié)。但這已經(jīng)足夠了,在中斷處理最初那部分相應模式的代碼vector_XXX中,linux只保存r0, lr和spsr三個32位的數(shù)據(jù),這正好需要12個字節(jié)。
評論