最近linux內核的中斷部分,總是被書裏的棧弄暈,一會兒內核棧,一會兒用戶棧的……很是崩潰,在網上google了一下 找了一篇不錯的文章拿來分享。
5.8 Linux 系統中堆棧的使用方法
本節內容概要描述了Linux內核從開機引導到系統正常運行過程中對堆棧的使用方式。這部分內容的說明與內核代碼關係比較密切,可以先跳過。在開始閱讀相應代碼時再回來仔細研究。
Linux 0.12系統中共使用了4種堆棧。第1種是系統引導初始化時臨時使用的堆棧;第2種是進入保護模式之後提供內核程序初始化使用的堆棧,位於內核代碼地址空間固定位置處。該堆棧也是後來任務0使用的用戶態堆棧;第3種是每個任務通過系統調用,執行內核程序時使用的堆棧,我們稱之爲任務的內核態堆棧。每個任務都有自己獨立的內核態堆棧;第4種是任務在用戶態執行的堆棧,位於任務(進程)邏輯地址空間近末端處。
使用多個棧或在不同情況下使用不同棧的主要原因有兩個。首先是由於從實模式進入保護模式,使得CPU對內存尋址訪問方式發生了變化,因此需要重新調整設置棧區域。另外,爲了解決不同CPU特權級共享使用堆棧帶來的保護問題,執行0級的內核代碼和執行3級的用戶代碼需要使用不同的棧。當一個任務進入內核態運行時,就會使用其TSS段中給出的特權級0的堆棧指針tss.ss0、tss.esp0,即內核棧。原用戶棧指針會被保存在內核棧中。而當從內核態返回用戶態時,就會恢復使用用戶態的堆棧。下面分別對它們進行說明。
5.8.1 初始化階段
(1)開機初始化時(bootsect.S,setup.s)
當bootsect代碼被ROM BIOS引導加載到物理內存0x7c00處時,並沒有設置堆棧段,當然程序也沒有使用堆棧。直到bootsect被移動到0x9000:0處時,才把堆棧段寄存器SS設置爲0x9000,堆棧指針esp寄存器設置爲0xff00,即堆棧頂端在0x9000:0xff00處,參見boot/bootsect.s第61、62行。setup.s程序中也沿用了bootsect中設置的堆棧段。這就是系統初始化時臨時使用的堆棧。
(2)進入保護模式時(head.s)
從esp設置成指向user_stack數組的頂端(參見user_stack數組定義在sched.c的67~23。此時該堆棧是內核程序自己使用的堆棧。其中給出的地址是大約值,它們與編譯時的實際設置參數有關。這些地址位置是從編譯內核時生成的system.map文件中查到的。
(3)初始化時(main.c)
在init/main.c程序中,在執行move_to_user_mode()代碼把控制權移交給任務0之前,系統一直使用上述堆棧。而在執行過move_to_user_mode()之後,main.c的代碼被“切換”成任務0中執行。通過執行fork()系統調用,main.c中的init()將在任務1中執行,並使用任務1的堆棧。而main()本身則在被“切換”成爲任務0後,仍然繼續使用上述內核程序自己的堆棧作爲任務0的用戶態堆棧。關於任務0所使用堆棧的詳細描述見後面說明。
每個任務都有兩個堆棧,分別用於用戶態和內核態程序的執行,並且分別稱爲用戶態堆棧和內核態堆棧。除了處於不同CPU特權級中,這兩個堆棧之間的主要區別在於任務的內核態堆棧很小,所保存的數據量最多不能超過4096 – 任務數據結構塊個字節,大約爲3KB。而任務的用戶態堆棧卻可以在用戶的64MB空間內延伸。
(1)在用戶態運行時
每個任務(除了任務0和任務1)有自己的64MB地址空間。當一個任務(進程)剛被創建時,它的用戶態堆棧指針被設置在其地址空間的靠近末端(64MB頂端)部分。實際上末端部分還要包括執行程序的參數和環境變量,然後纔是用戶堆棧空間,如圖5-24所示。應用程序在用戶態下運行時就一直使用這個堆棧。堆棧實際使用的物理內存則由CPU分頁機制確定。由於Linux實現了寫時複製功能(Copy on Write),因此在進程被創建後,若該進程及其父進程都沒有使用堆棧,則兩者共享同一堆棧對應的物理內存頁面。只有當其中一個進程執行堆棧寫操作(如push操作)時內核內存管理程序纔會爲寫操作進程分配新的內存頁面。而進程0和進程1的用戶堆棧比較特殊,見後面說明。
(2)在內核態運行時
每個任務都有自己的內核態堆棧,用於任務在內核代碼中執行期間。其所在線性地址中的位置由該任務TSS段中ss0和esp0兩個字段指定。ss0是任務內核態堆棧的段選擇符,esp0是堆棧棧底指針。因此每當任務從用戶代碼轉移進入內核代碼中執行時,任務的內核態棧總是空的。任務內核態堆棧被設置在位於其任務數據結構所在頁面的末端,即與任務的任務數據結構(task_struct)放在同一頁面內。這是在建立新任務時,fork()程序在任務tss段的內核級堆棧字段(tss.esp0和tss.ss0)中設置的,參見kernel/fork.c,92行:
p->tss.esp0 = PAGE_SIZE + (long)p;
p->tss.ss0 = 0x10;
其中,p是新任務的任務數據結構指針,tss是任務狀態段結構。內核爲新任務申請內存用作保存其task_struct結構數據,而tss結構(段)是task_struct中的一個字段。該任務的內核堆棧段值tss.ss0也被設置成爲0x10(即內核數據段選擇符),而tss.esp0則指向保存task_struct結構頁面的末端。如圖5-25所示。實際上tss.esp0被設置成指向該頁面(外)上一字節處(圖中堆棧底處)。這是因爲Intel CPU執行堆棧操作時是先遞減堆棧指針esp值,然後在esp指針處保存入棧內容。
爲什麼從主內存區申請得來的用於保存任務數據結構的一頁內存也能被設置成內核數據段中的數據呢,即tss.ss0爲什麼能被設置成0x10呢?這是因爲用戶內核態棧仍然屬於內核數據空間。我們可以從內核代碼段的長度範圍來說明。在head.s程序的末端,分別設置了內核代碼段和數據段的描述符,段長度都被設置成了16MB。這個長度值是Linux 0.12內核所能支持的最大物理內存長度(參見head.s,110行開始的註釋)。因此,內核代碼可以尋址到整個物理內存範圍中的任何位置,當然也包括主內存區。每當任務執行內核程序而需要使用其內核棧時,CPU就會利用TSS結構把它的內核態堆棧設置成由tss.ss0和tss.esp0這兩個值構成。在任務切換時,老任務的內核棧指針esp0不會被保存。對CPU來講,這兩個值是隻讀的。因此每當一個任務進入內核態執行時,其內核態堆棧總是空的。
(3)任務0和任務1的堆棧
任務0(空閒進程idle)和任務1(初始化進程init)的堆棧比較特殊,需要特別予以說明。任務0和任務1的代碼段和數據段相同,限長也都是640KB,但它們被映射到不同的線性地址範圍中。任務0的段基地址從線性地址0開始,而任務1的段基地址從64MB開始。但是它們全都映射到物理地址0~640KB範圍中。這個地址範圍也就是內核代碼和基本數據所存放的地方。在執行了move_to_user_mode()之後,任務0和任務1的內核態堆棧分別位於各自任務數據結構所在頁面的末端,而任務0的用戶態堆棧就是前面進入保護模式後所使用的堆棧,即sched.c的user_stack[]數組的位置。由於任務1在創建時複製了任務0的用戶堆棧,因此剛開始時任務0和任務1共享使用同一個用戶堆棧空間。但是當任務1開始運行時,由於任務1映射到user_stack[]處的頁表項被設置成只讀,使得任務1在執行堆棧操作時將會引起寫頁面異常,從而內核會使用寫時複製機制(關於寫時複製技術的說明請參見第13章)爲任務1另行分配主內存區頁面作爲堆棧空間使用。只有到此時,任務1纔開始使用自己獨立的用戶堆棧內存頁面。因此任務0的堆棧需要在任務1實際開始使用之前保持“乾淨”,即任務0此時不能使用堆棧,以確保複製的堆棧頁面中不含有任務0的數據。
任務0的內核態堆棧是在其人工設置的初始化任務數據結構中指定的,而它的用戶態堆棧是在執行move_to_user_mode()時,在模擬iret返回之前的堆棧中設置的,參見圖5-22所示。我們知道,當進行特權級會發生變化的控制權轉移時,目的代碼會使用新特權級的堆棧,而原特權級代碼堆棧指針將保留在新堆棧中。因此這裏先把任務0用戶堆棧指針壓入當前處於特權級0的堆棧中,同時把代碼指針也壓入堆棧,然後執行IRET指令即可實現把控制權從特權級0的代碼轉移到特權級3的任務0代碼中。在這個人工設置內容的堆棧中,原esp值被設置成仍然是user_stack中原來的位置值,而原ss段選擇符被設置成0x17,即設置成用戶態局部表LDT中的數據段選擇符。然後把任務0代碼段選擇符0x0f壓入堆棧作爲棧中原CS段的選擇符,把下一條指令的指針作爲原EIP壓入堆棧。這樣,通過執行IRET指令即可“返回”到任務0的代碼中繼續執行了。
在<span times="" new="" roman',="" 'serif'"="" lang="EN-US" style="padding: 0px; margin: 0px;">Linux 0.12系統中,所有中斷服務程序都屬於內核代碼。如果一箇中斷產生時任務正在用戶代碼中執行,那麼該中斷就會引起CPU特權級從3級到0級的變化,此時CPU就會進行用戶態堆棧到內核態堆棧的切換操作。CPU會從當前任務的任務狀態段TSS中取得新堆棧的段選擇符和偏移值。因爲中斷服務程序在內核中,屬於0級特權級代碼,所以48位的內核態堆棧指針會從TSS的ss0和esp0字段中獲得。在定位了新堆棧(內核態堆棧)之後,CPU就會首先把原用戶態堆棧指針ss和esp壓入內核態堆棧,隨後把標誌寄存器eflags的內容和返回位置cs、eip壓入內核態堆棧。
內核的系統調用是一個軟件中斷,因此任務調用系統調用時就會進入內核並執行內核中的中斷服務代碼。此時內核代碼就會使用該任務的內核態堆棧進行操作。同樣,當進入內核程序時,由於特權級別發生了改變(從用戶態轉到內核態),用戶態堆棧的堆棧段和堆棧指針以及eflags會被保存在任務的內核態堆棧中。而在執行iret退出內核程序返回到用戶程序時,將恢復用戶態的堆棧和eflags。這個過程如圖5-26所示。
如果一個任務正在內核態中運行,那麼若CPU響應中斷就不再需要進行堆棧切換操作,因爲此時該任務運行的內核代碼已經在使用內核態堆棧,並且不涉及優先級別的變化,所以CPU僅把eflags和中斷返回指針cs、eip壓入當前內核態堆棧,然後執行中斷服務過程。
查看進程在內核中的調用棧小工具
編譯內核時打開調試選項:
General setup--------->Configure standard kernel features
-------->
Load all symbols for debugging/kksymbols
Include all symbols in kallsyms
工具的使用很簡單:
#echo pid > /proc/show_stack
#cat /proc/show_stack
調用棧的顯示代碼是從內核函數show_stack改寫得來
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/sched.h>
#include <asm/uaccess.h>
#include <linux/fs.h>
#include <linux/stat.h>
#include <linux/proc_fs.h>
#include <linux/kallsyms.h>
MODULE_LICENSE("BSD/GPL");
MODULE_AUTHOR("SanLongCai");
static int show_pid = 1;
module_param(show_pid, int, 0);
static int sstack_read_proc(char *page, char **start, off_t offset, int count, int
*eof, void *data);
static int myatoi(const char *buf, unsigned int count)
{
int i;
int res;
if(! buf || count == 0)
return 0;
res = 0;
i = 0;
while(i < count && buf[i]) {
if(buf[i] < '0' || buf[i] > '9') {
printk(KERN_INFO "the buf:%s conn't convert to int
", buf);
return 0;
}
res = res * 10 + buf[i] - '0';
i++;
}
return res;
}
static int sstack_show(void)
{
char *page;
if(!(page = (char *)__get_free_page(GFP_KERNEL)))
return -ENOMEM;
sstack_read_proc(page, NULL, 0, 0, NULL, NULL);
printk(KERN_INFO "%s
", page);
free_page((unsigned long)page);
//show_stack(task, NULL); //show_stack must be export by kernel
return 0;
}
static inline int valid_stack_ptr(struct thread_info *tinfo, void *p, unsigned size)
{
return p > (void *)tinfo &&
p <= (void *)tinfo + THREAD_SIZE - size;
}
struct stack_frame {
struct stack_frame *next_frame;
unsigned long return_address;
};
static void show_address_symbol(unsigned long address, char *page, int *len)
{
char buffer[KSYM_SYMBOL_LEN];
*len += sprintf(page + (*len), " [<%08lx>]", address);
sprint_symbol(buffer, (unsigned long)__builtin_extract_return_addr((void
*)address));
*len += sprintf(page + (*len), " %s
", buffer);
}
static unsigned long show_context_stack(struct thread_info *tinfo, unsigned long
*stack, unsigned long bp,
char *page, int *len)
{
struct stack_frame *frame = (struct stack_frame *)bp;
while (valid_stack_ptr(tinfo, stack, sizeof(*stack))) {
unsigned long addr;
addr = *stack;
if (kernel_text_address(addr)) {
if ((unsigned long) stack == bp + 4) {
frame = frame->next_frame;
bp = (unsigned long) frame;
show_address_symbol(addr, page, len);
} else {
if(bp == 0)
show_address_symbol(addr, page, len);
}
}
stack++;
}
return bp;
}
static int sstack_read_proc(char *page, char **start, off_t offset, int count, int
*eof, void *data)
{
int len = 0;
unsigned long bp;
unsigned long *stack;
struct task_struct *task;
len += sprintf(page, "Call Trace:
");
if((task = find_task_by_pid(show_pid)) == NULL)
{
len += sprintf(page + len, "Process %d not found!!
", show_pid);
return len;
}
stack = (unsigned long *)task->thread.sp;
bp = *stack;
while(1) {
struct thread_info *context;
context = (struct thread_info *) ((unsigned long)stack & (~(THREAD_SIZE -
1)));
bp = show_context_stack(context, stack, bp, page, &len);
stack = (unsigned long *)context->previous_esp;
if(! stack)
break;
}
len += sprintf(page + len, "Call Trace End.
");
len += sprintf(page + len, "==================================
");
return len;
}
static int sstack_write_proc(struct file *file, const char *buffer, unsigned long
count, void *data)
{
char buf[16];
int new_pid;
if(count > 16) {
printk(KERN_INFO "the count is too large");
return -EINVAL;
}
if(copy_from_user(buf, buffer, count)) {
return -EFAULT;
}
if(buf[count - 1] < '0' || buf[count - 1] > '9')
buf[count -1] = '';
if(new_pid = myatoi(buf, 16))
{
int ret;
show_pid = new_pid;
if(ret = sstack_show())
return ret;
return count;
}
else
return -EINVAL;
}
static void sstack_create_proc(void)
{
struct proc_dir_entry *res = create_proc_entry("show_stack", S_IRUGO | S_IWUGO,
NULL);
if(res) {
res->read_proc = sstack_read_proc;
res->write_proc = sstack_write_proc;
}
else
printk(KERN_INFO "create the proc failure
");
}
static int __init sstack_init(void)
{
printk(KERN_INFO "sstack_init
");
sstack_create_proc();
return 0;
}
static void sstack_exit(void)
{
printk(KERN_INFO "sstack_exit
");
remove_proc_entry("show_stack", NULL);
}
module_init(sstack_init);
module_exit(sstack_exit);
死鎖後,導出線程函數調用棧
很多時候,內核oops還是很好處理的,因爲可以看到當時的函數調用棧。objdump -DS vmlinux,配合epc(程序指針)可以定位情況發生時的代碼位置。有些調試器,支持斷點地址設置,可以直接顯示問題發生時的代碼位置。
然而,當系統陷入某種死鎖狀態。又比如應用程序進行的系統調用不能退出等等。這種情況下,顯示當前所有線程的當前函數調用棧就有很大的幫助作用了。
我們可以通過外部觸發事件,比如按鍵,終端輸入。或者在程序代碼中設定定時器。在事件處理程序中,顯示當前所有線程或者關心的線程的函數調用棧。
if(task == current)
{
dump_stack();
}else
{
regs.regs[29] = task->thread.reg29;
regs.regs[31] = task->thread.reg31;
regs.cp0_epc = 0;
show_backtrace(task, ®s);
}
簡單實現dump_stack
0.首先確保你能寫個內核模塊:打印"hello kernel"
如果熟悉dump_stack的話,完全可以繞開此文,或者自己去看dump_stack代碼實現之。
1.dump_stack是什麼
經常調試內核一定對這個函數不陌生,因爲我們大多數人調試內核的時候都受這個函數的
折磨,不信,那麼我們調用下這個函數看看(隨意寫個內核模塊調用dump_stack(),插入內核),
我們來看看輸出:
Pid: 9982, comm: insmod Not tainted 2.6.31.5-127.fc12.i686.PAE #1
Call Trace:
[<f7e98008>] init+0x8/0xc [hello]
[<c040305b>] do_one_initcall+0x51/0x13f
[<c0462e2f>] sys_init_module+0xac/0x1bd
[<c0408f7b>] sysenter_do_call+0x12/0x28
看到輸出,大家一定很熟悉, 沒見過類似輸出的,一定沒把kernel搞崩過.
(來我教你:*(int *)NULL = 0xdead;)
其實不見得每次內核崩潰都會調用dump_stack,但是看到dump_stack的我們不應該被嚇到,
反而應該高興:內核在臨掛前還喘口氣給我們提示了寶貴的調試信息
2.構造dump_stack第一句
有的人已經不耐煩我這嘮叨,自己開始查看代碼了,但是爲了滿足我們小小的虛榮,看懂還不行,
自己也要來寫個玩玩,不能老被dump_stack欺負阿。
我們看看dump_stack裏面第一句代碼:
printk("Pid: %d, comm: %.20s %s %s %.*s/n",
current->pid, current->comm, print_tainted(),
init_utsname()->release,
(int)strcspn(init_utsname()->version, " "),
init_utsname()->version);
這裏就不解釋printk每個參數了,代碼本身就自解釋了,剩下的請google,
對於不太理解print_tainted,請看下此函數實現的源碼上方的註釋:)
3.構造dump_stack的call trace
1)先來句printk("Call Trace:/n"),
2)接下來就神奇了,當初我就覺得能將函數執行流打印出來實在是很神奇,內核到底用了什麼方法呢?
先不說,我們來看一句簡單的代碼:
printk("[<%p>] %pS/n", &printk, &printk);
觀察輸出:
[<c0776cf4>] printk+0x0/0x1c
你發現,這個輸出結果和dump_stack輸出的部分驚人的相似,但是我們傳給printk的參數是確確實實的
地址值,原來prink自己能轉換地址到相應的函數名,只要用參數"%pS"就可以了。
我相信看到這裏的人,估計已經走開自己去實現dump_stack玩了。
但是輸出也可能是:
[<c0776cf4>] c0776cf4S
如果你不幸看到這個,那麼你還是升級下內核吧,或者仔細閱讀下dump_stack代碼,完全靠自己去實現
下,那麼你收穫一定會遠超出這篇文章。
printk之所以能夠識別函數地址,靠的是kallsyms子系統的幫助,
用過類似grep -w "printk" /proc/kallsyms命令的人,一定要好好謝謝這個子系統,
多虧它我們才能從內核導出symbol
深入研究kallsyms就靠大家了。
3)在剛剛的驚喜後,我們回到正題,怎麼用printk把當前的執行流打印出來?
這裏用到x86中堆棧對函數調用的幫助,詳細信息請google,我們要知道一點:每次函數調用時候,
都會將函數的返回地址(調用函數指令的下一句指令的地址)壓入堆棧,已備函數返回時。
我們就可以靠這個返回地址來幫助打印函數執行流。
但是這個地址並不是一個函數的準確地址呀?
%pS需要的參數不一定是準確的函數地址,在函數內部任意指令地址都可以,這就解釋了輸出形式是
"printk+0x0/0x1c",0x0表示參數地址相對於printk地址的偏移,0x1c表示printk函數大小。
你可以嘗試下:printk("%pS/n", &printk + 1);
並且如果函數屬於某個模塊,還會在輸出後面加上模塊名稱,類似:" [<f8cd40a5>] exit+0xd/0xf [hello]"
這裏知道地址在堆棧裏,那麼怎麼取堆棧呢?
其實很簡單:
int stack_pointer;
我們只要取臨時變量地址值 &stack_pointer 就可以了(也可以用內聯彙編取esp值),然後只要循環遍歷堆棧上所有值,然後判斷該值是否在
內核代碼段空間內,如果是那麼就用%pS輸出。
那麼堆棧的結束地址是什麼呢?
就是當前進程的內核態堆棧段,不懂的話請google,一定要搞清除這個。
這裏我們記堆棧底爲:
bottom = (unsigned int)current_thread_info() + THREAD_SIZE;
但是怎麼判斷地址值是否在內核代碼段呢?
我們可以用kernel_text_address這個函數就可以了,但是很不幸的是此函數內核沒有導出,我們不能使用,
那麼我們就自己實現個kernel_text_address吧,但是更不幸的是此函數內部實現所依賴的變量_etext等也沒有
被內核導出,其實我也沒想到很好的方法,索性就用個笨辦法:
手動找出此函數內核中的地址,
# grep kernel_text_address /proc/kallsyms
c044f107 T kernel_text_address
在代碼中通過地址值調用kernel_text_address
int (*kernel_text_addressp)(unsigned int) = (int (*)(unsigned int))0xc044f107;
4)給出個較爲完整的代碼(在本機上寫的:2.6.31.5-127.fc12.i686.PAE, 虛擬機上文檔寫的麻煩)
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/utsname.h>
/*
* change the value to real addr of kernel_text_address :
* grep -w kernel_text_address /proc/kallsyms
*/
static int (*kernel_text_addressp)(unsigned int) = (int (*)(unsigned int))0xc044f107;
void my_dump_stack(void)
{
unsigned int stack;
unsigned int bottom;
unsigned int addr;
printk("Pid: %d, comm: %.20s %s %s %.*s/n",
current->pid, current->comm, print_tainted(),
init_utsname()->release,
(int)strcspn(init_utsname()->version, " "),
init_utsname()->version);
printk("Call Trace:/n");
/* get stack point */
stack = (unsigned int)&stack;
/* get stack bottom point */
bottom = (unsigned int)current_thread_info() + THREAD_SIZE;
for (; stack < bottom; stack += 4) {
addr = *(unsigned int *)stack;
if (kernel_text_addressp(addr))
printk(" [<%p>] %pS/n", (void *)addr, (void *)addr);
}
}
static int __init init(void)
{
/* test */
my_dump_stack();
return 0;
}
static void __exit exit(void)
{
/* test */
my_dump_stack();
}
MODULE_LICENSE("GPL");
module_init(init);
module_exit(exit);
3.改進
1)
如果你還沒厭煩的話,這裏有個改進的地方。
你會發現內核中的dump_stack會又類似如下輸出:
[<c04cf4e6>] ? path_put+0x1a/0x1d
這裏有個問號:這個表示堆棧中有確有此值(某個函數內部地址),但是並不代表此函數被
執行,也許這個值是個臨時變量寄存在堆棧中。
要區分這個很容易,只要比較地址值所在堆棧的位置是否緊貼當前函數棧空間底的上方,
可以利用ebp(記錄當前堆棧底)指針值來作比較,代碼就留給大家寫了。
2)
x86 64 實現要比x86 32複雜(見內核註釋):
/*
* x86-64 can have up to three kernel stacks:
* process stack
* interrupt stack
* severe exception (double fault, nmi, stack fault, debug, mce) hardware stack
*/
4.後記
你可能認爲作者在忽悠你,這就整一個dump_stack註釋的文章呀,貫上了寫dump_stack的頭銜!
我只有一句話:
Just for fun!