文章目錄
- 前言
- 一、dump_stack函數使用
- 二、dump_stack函數源碼解析
- 2.1 show_stack
- 2.2 show_stack_log_lvl
- 2.3 show_trace_log_lvl
- 2.4 dump_trace
- 2.5 print_context_stack
- 參考資料
前言
Linux x86_64
centos7
Linux:3.10.0
一、dump_stack函數使用
dump_stack函數用于打印當前任務的信息以及其堆棧跟蹤,能夠用來回溯打印調用棧信息。
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>void noinline func_c(void)
{dump_stack();
}void noinline func_b(void)
{func_c();
}void noinline func_a(void)
{func_b();
}//內核模塊初始化函數
static int __init lkm_init(void)
{func_a();return 0;
}//內核模塊退出函數
static void __exit lkm_exit(void)
{printk("Goodbye\n");
}module_init(lkm_init);
module_exit(lkm_exit);MODULE_LICENSE("GPL");
這里加了noinline修飾,否則會被優化成 inline 函數。
[1109990.858938] Call Trace:
[1109990.858952] [<ffffffff8e781340>] dump_stack+0x19/0x1b
[1109990.858960] [<ffffffffc0a3700e>] func_c+0xe/0x10 [helloworld]
[1109990.858968] [<ffffffffc0a3701e>] func_b+0xe/0x10 [helloworld]
[1109990.858974] [<ffffffffc0a3702e>] func_a+0xe/0x10 [helloworld]
[1109990.858981] [<ffffffffc0153009>] lkm_init+0x9/0x1000 [helloworld]
[1109990.858990] [<ffffffff8e00210a>] do_one_initcall+0xba/0x240
[1109990.858999] [<ffffffff8e11e45a>] load_module+0x271a/0x2bb0
[1109990.859007] [<ffffffff8e3b4290>] ? ddebug_proc_write+0x100/0x100
[1109990.859016] [<ffffffff8e119fe3>] ? copy_module_from_fd.isra.44+0x53/0x150
[1109990.859024] [<ffffffff8e11ead6>] SyS_finit_module+0xa6/0xd0
[1109990.859033] [<ffffffff8e793f92>] system_call_fastpath+0x25/0x2a
Linux dump_stack 函數原理:
棧幀如下如圖所示:callee的RBP寄存器的值保存caller的RBP寄存器地址,可以看作每個棧幀用單鏈表連接。
// linux-3.10/arch/x86/include/asm/stacktrace.h/* The form of the top of the frame on the stack */
struct stack_frame {struct stack_frame *next_frame;unsigned long return_address;
};
幀指針起到了歷史上的作用。幀指針是一個寄存器,它始終包含著上一個堆棧指針的值。在 x86_64 架構中,通常使用的寄存器是 RBP。
由于幀指針寄存器的存在,堆棧現在成為了一個“堆棧幀”的鏈表,我們可以一直沿著鏈表向前遍歷到開頭。在任何時刻,我們只需查看當前幀指針寄存器的值,就可以獲得先前的 RSP 值。由于先前的 RSP 值恰好是存儲先前幀指針的位置,因此這就是一系列指針沿著堆棧向上爬行的過程。
通過遍歷堆棧幀鏈表,我們可以逐個獲取每個函數的返回地址、參數和局部變量等信息。這樣,我們就可以按順序打印每個函數的名稱,實現堆棧跟蹤。
幀指針寄存器的存在使得堆棧幀之間形成了鏈式結構,使得在堆棧跟蹤過程中可以方便地從當前幀指針寄存器獲取前一個堆棧幀的位置。通過這種方式,我們可以沿著堆棧鏈表一直向上遍歷,獲取所有函數的信息。
幀指針寄存器的使用使得堆棧跟蹤變得更加直觀和可靠,因為它提供了一種可靠的方式來遍歷堆棧幀鏈表。但是需要注意的是,某些情況下,編譯器可能會對幀指針進行優化或省略,因此在特定的編譯器優化設置下,幀指針可能不可用或不準確。
centos 7 配置了CONFIG_FRAME_POINTER選項:
# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_FRAME_POINTER
CONFIG_FRAME_POINTER=y
(1)基于Frame Pointer - fp寄存器的棧回溯:
優點:棧回溯比較快,理解簡單。相對較簡單:基于Frame Pointer寄存器的棧回溯通常比解析unwind節更簡單直接。
缺點:gcc添加了優化選項 -O 就會省略掉省略基指針。這樣就不能都通過這種形式進行棧回溯了。
-fomit-frame-pointer編譯標志進行優化:避免將%rbp用作棧幀指針,把FP當作一個通用寄存器,這樣就提供了一個額外的通用寄存器,提高程序運行效率。
(1)func_c RBP寄存器的值存放了父函數func_b的RBP寄存器的地址。其返回地址 = func_cRBP寄存器地址+8。
(2)對函數func_b的RBP寄存器的地址取值獲取func_b RBP寄存器的值,func_b RBP寄存器的值存放了父函數func_a的RBP寄存器的地址。其返回地址 = func_bRBP寄存器地址+8。
(3)對函數func_a的RBP寄存器的地址取值獲取func_a RBP寄存器的值,func_a RBP寄存器的值存放了父函數lkm_init的RBP寄存器的地址。其返回地址 = func_aRBP寄存器地址+8。
這樣一步步回溯就可以獲取整個調用棧。
在 x86_64 架構中 rbp 指向當前棧幀的起始位置,這個位置保存著舊的 rbp的值。我們可以看到在舊的 rbp 保存的位置上方保存著返回地址(rbp + 8)。這個返回地址是調用者函數中 call 指令的下一條指令的地址,子函數執行完成后會返回,舊的 rbp 首先出棧并賦值給 rbp 寄存器,同時返回地址也要出棧并賦值給 pc。
上面的過程可以遞歸的用于多層函數調用上。
我們可以將 dump_stack 函數的棧幀看做 Current frame,當前 pc 的值保存的是 dump_stack 中的某條指令的地址,內核先根據這個地址查詢 符號表 獲取到 dump_stack 函數的名稱與當前指令先相對于 dump_stack 函數起始位置的偏移量,然后通過訪問 rbp 寄存器指向的舊 rbp 的值來獲取到調用 dump_stack 函數的棧幀指針的值,有了這個值就可以不斷的回溯上方的棧幀,一個棧幀就是一個調用層次。
同時返回地址的位置就在舊的 rbp 存儲位置的上方,根據這樣的特點 dump_stack 也就能回溯不同調用層次中返回地址的值。根據返回地址就可以獲取到返回地址的上一條調用語句的地址,對該地址進行尋址,獲取到指令的編碼,就能夠獲取到調用函數的入口地址。這里可以使用如下公式:
call 指令調用函數的地址 = call 指令碼后面的偏移量 + 返回地址
這之后使用入口地址查詢 System-map 獲取到函數的名稱,同時計算出返回地址相對于函數入口的偏移量就準備好了打印的內容,調用打印函數打印信息,每個棧幀用單鏈表連接,然后繼續重復這一過程直到找不到一個合法的棧幀為止。
二、dump_stack函數源碼解析
centos 7 配置了CONFIG_FRAME_POINTER選項:
# cat /boot/config-3.10.0-1160.el7.x86_64 | grep CONFIG_FRAME_POINTER
CONFIG_FRAME_POINTER=y
// linux-3.10/lib/dump_stack.c/*** dump_stack - dump the current task information and its stack trace** Architectures can override this implementation by implementing its own.*/
void dump_stack(void)
{dump_stack_print_info(KERN_DEFAULT);show_stack(NULL, NULL);
}
EXPORT_SYMBOL(dump_stack);
dump_stack()-->show_stack()-->show_stack_log_lvl()-->show_trace_log_lvl()-->dump_trace()-->print_context_stack()
2.1 show_stack
// linux-3.10/arch/x86/include/asm/stacktrace.h#define STACKSLOTS_PER_LINE 4
#define get_bp(bp) asm("movq %%rbp, %0" : "=r" (bp) :)#ifdef CONFIG_FRAME_POINTER
static inline unsigned long
stack_frame(struct task_struct *task, struct pt_regs *regs)
{unsigned long bp;if (regs)return regs->bp;if (task == current) {/* Grab bp right from our regs */get_bp(bp);return bp;}/* bp is the last reg pushed by switch_to */return *(unsigned long *)task->thread.sp;
}
get_bp(bp)是一個宏定義,使用匯編語句獲取當前函數的基址寄存器(rbp)的值,并將其保存在bp變量中。
stack_frame是一個內聯函數,用于獲取給定任務的棧幀指針。
(1)如果傳入的regs參數非空,說明已經提供了寄存器上下文(pt_regs結構),則直接返回其中的基址寄存器(bp)的值。
(2)如果給定的任務結構體指針與當前任務相同(current表示當前任務),則直接使用get_bp宏獲取當前函數的基址寄存器的值(rbp),并將其作為棧幀指針返回。
(3)如果以上條件都不滿足,則假設bp是由switch_to函數推入的最后一個寄存器,從給定任務的線程結構體中獲取棧指針(sp)所指向的地址,并將其解釋為unsigned long類型的指針,以獲取棧幀指針。
void show_stack(struct task_struct *task, unsigned long *sp)
{unsigned long bp = 0;unsigned long stack;/** Stack frames below this one aren't interesting. Don't show them* if we're printing for %current.*/if (!sp && (!task || task == current)) {sp = &stack;bp = stack_frame(current, NULL);}show_stack_log_lvl(task, NULL, sp, bp, "");
}
該函數用于打印給定任務的堆棧跟蹤信息。
2.2 show_stack_log_lvl
// linux-3.10/arch/x86/kernel/dumpstack_64.cvoid
show_stack_log_lvl(struct task_struct *task, struct pt_regs *regs,unsigned long *sp, unsigned long bp, char *log_lvl)
{unsigned long *irq_stack_end;unsigned long *irq_stack;unsigned long *stack;int cpu;int i;preempt_disable();cpu = smp_processor_id();irq_stack_end = (unsigned long *)(per_cpu(irq_stack_ptr, cpu));irq_stack = (unsigned long *)(per_cpu(irq_stack_ptr, cpu) - IRQ_STACK_SIZE);/** Debugging aid: "show_stack(NULL, NULL);" prints the* back trace for this cpu:*/if (sp == NULL) {if (task)sp = (unsigned long *)task->thread.sp;elsesp = (unsigned long *)&sp;}stack = sp;for (i = 0; i < kstack_depth_to_print; i++) {if (stack >= irq_stack && stack <= irq_stack_end) {if (stack == irq_stack_end) {stack = (unsigned long *) (irq_stack_end[-1]);pr_cont(" <EOI> ");}} else {if (((long) stack & (THREAD_SIZE-1)) == 0)break;}if (i && ((i % STACKSLOTS_PER_LINE) == 0))pr_cont("\n");pr_cont(" %016lx", *stack++);touch_nmi_watchdog();}preempt_enable();pr_cont("\n");show_trace_log_lvl(task, regs, sp, bp, log_lvl);
}
show_stack_log_lvl函數用于打印給定任務的堆棧跟蹤信息,并在日志級別上進行控制。
函數首先定義了一些局部變量,包括irq_stack_end、irq_stack、stack、cpu和i。
然后,禁用搶占(preempt_disable)并獲取當前處理器的 ID(smp_processor_id)。
irq_stack_end表示中斷堆棧的結束地址,irq_stack表示中斷堆棧的起始地址(通過per_cpu宏和irq_stack_ptr變量計算得到)。
接下來,通過一系列條件判斷,確定要打印的堆棧跟蹤信息。
如果給定的sp參數為空,表示需要打印當前任務的堆棧跟蹤信息。根據是否提供了任務結構體指針(task),確定要使用的棧指針(sp)。如果提供了任務結構體指針,則使用任務的線程結構體中的棧指針;否則,使用當前函數的棧指針。
接下來,通過循環遍歷堆棧,打印堆棧上的地址。在遍歷過程中,通過一系列條件判斷確定是否處于中斷堆棧范圍內,并在特定情況下打印(End of Interrupt)標記。如果堆棧地址與線程棧的大小(THREAD_SIZE)對齊,則表示已經遍歷到了棧的底部,循環結束。
在每次打印堆棧地址后,調用touch_nmi_watchdog函數,用于觸發非屏蔽中斷(NMI)看門狗,以確保系統不會因為長時間占用CPU而被認為是死鎖。
最后,啟用搶占(preempt_enable),打印換行符,然后調用show_trace_log_lvl函數,將任務結構體指針、寄存器上下文、棧指針、棧幀指針和日志級別作為參數傳遞,繼續打印堆棧跟蹤信息。
2.3 show_trace_log_lvl
// linux-3.10/arch/x86/kernel/dumpstack.cvoid
show_trace_log_lvl(struct task_struct *task, struct pt_regs *regs,unsigned long *stack, unsigned long bp, char *log_lvl)
{printk("%sCall Trace:\n", log_lvl);dump_trace(task, regs, stack, bp, &print_trace_ops, log_lvl);
}
Call Trace:
[1119269.645012] [<ffffffff8e781340>] dump_stack+0x19/0x1b
[1119269.645021] [<ffffffffc0a5000e>] func_c+0xe/0x10 [helloworld]
[1119269.645028] [<ffffffffc0a5001e>] func_b+0xe/0x10 [helloworld]
[1119269.645034] [<ffffffffc0a5002e>] func_a+0xe/0x10 [helloworld]
[1119269.645041] [<ffffffffc0153009>] lkm_init+0x9/0x1000 [helloworld]
[1119269.645049] [<ffffffff8e00210a>] do_one_initcall+0xba/0x240
[1119269.645059] [<ffffffff8e11e45a>] load_module+0x271a/0x2bb0
[1119269.645066] [<ffffffff8e3b4290>] ? ddebug_proc_write+0x100/0x100
[1119269.645075] [<ffffffff8e119fe3>] ? copy_module_from_fd.isra.44+0x53/0x150
[1119269.645083] [<ffffffff8e11ead6>] SyS_finit_module+0xa6/0xd0
[1119269.645093] [<ffffffff8e793f92>] system_call_fastpath+0x25/0x2a
2.4 dump_trace
(1)
// linux-3.10/arch/x86/kernel/dumpstack_64.c/** x86-64 can have up to three kernel stacks:* process stack* interrupt stack* severe exception (double fault, nmi, stack fault, debug, mce) hardware stack*/void dump_trace(struct task_struct *task, struct pt_regs *regs,unsigned long *stack, unsigned long bp,const struct stacktrace_ops *ops, void *data)
{const unsigned cpu = get_cpu();unsigned long *irq_stack_end =(unsigned long *)per_cpu(irq_stack_ptr, cpu);unsigned used = 0;struct thread_info *tinfo;int graph = 0;unsigned long dummy;if (!task)task = current;if (!stack) {if (regs)stack = (unsigned long *)regs->sp;else if (task != current)stack = (unsigned long *)task->thread.sp;elsestack = &dummy;}if (!bp)bp = stack_frame(task, regs);/** Print function call entries in all stacks, starting at the* current stack address. If the stacks consist of nested* exceptions*/tinfo = task_thread_info(task);for (;;) {char *id;unsigned long *estack_end;estack_end = in_exception_stack(cpu, (unsigned long)stack,&used, &id);if (estack_end) {if (ops->stack(data, id) < 0)break;bp = ops->walk_stack(tinfo, stack, bp, ops,data, estack_end, &graph);ops->stack(data, "<EOE>");/** We link to the next stack via the* second-to-last pointer (index -2 to end) in the* exception stack:*/stack = (unsigned long *) estack_end[-2];continue;}if (irq_stack_end) {unsigned long *irq_stack;irq_stack = irq_stack_end -(IRQ_STACK_SIZE - 64) / sizeof(*irq_stack);if (in_irq_stack(stack, irq_stack, irq_stack_end)) {if (ops->stack(data, "IRQ") < 0)break;bp = ops->walk_stack(tinfo, stack, bp,ops, data, irq_stack_end, &graph);/** We link to the next stack (which would be* the process stack normally) the last* pointer (index -1 to end) in the IRQ stack:*/stack = (unsigned long *) (irq_stack_end[-1]);irq_stack_end = NULL;ops->stack(data, "EOI");continue;}}break;}/** This handles the process stack:*/bp = ops->walk_stack(tinfo, stack, bp, ops, data, NULL, &graph);put_cpu();
}
EXPORT_SYMBOL(dump_trace);
dump_trace函數用于在給定任務的堆棧上進行跟蹤,并通過提供的回調函數執行相應的操作。
函數首先定義了一些局部變量,包括cpu、irq_stack_end、used、tinfo和graph,以及一個dummy變量。
然后,根據情況,確定要跟蹤的任務和堆棧的起始地址。如果沒有給定任務,則默認使用當前任務。如果沒有給定堆棧地址,則根據情況選擇使用寄存器上下文的棧指針、任務的線程結構體中的棧指針,或者一個臨時變量作為棧指針。
接下來,如果沒有給定基指針(bp),則通過調用stack_frame函數計算基指針。
在一個無限循環中,函數根據堆棧的類型進行處理。首先,通過調用in_exception_stack函數檢查堆棧是否屬于異常堆棧(如雙重故障、NMI、堆棧故障、調試、MCE等),并獲取異常堆棧的結束地址(estack_end)以及用于標識堆棧的字符串(id)。
如果堆棧屬于異常堆棧(接上文)
如果堆棧屬于異常堆棧,將調用回調函數ops->stack(data, id)打印堆棧標識符,并通過調用ops->walk_stack函數執行堆棧的遍歷操作。然后,再次調用ops->stack(data, “”)打印異常堆棧的結束標識符。之后,通過異常堆棧的倒數第二個指針(索引為-2)獲取下一個堆棧的起始地址,并繼續下一輪循環。
如果堆棧不屬于異常堆棧,將檢查是否存在中斷堆棧(IRQ stack)。如果存在中斷堆棧,將通過調用in_irq_stack函數判斷當前堆棧是否屬于中斷堆棧,并獲取中斷堆棧的起始地址。如果當前堆棧屬于中斷堆棧,則與異常堆棧類似,調用回調函數打印中斷標識符,并通過ops->walk_stack函數執行中斷堆棧的遍歷操作。然后,通過中斷堆棧的最后一個指針(索引為-1)獲取下一個堆棧的起始地址,并繼續下一輪循環。
如果既不是異常堆棧也不是中斷堆棧,表示已經遍歷完所有堆棧,退出循環。
最后,通過調用ops->walk_stack函數處理進程堆棧,并完成整個跟蹤過程。最后,調用put_cpu()釋放當前CPU的引用計數。
該函數使用了一些其他函數和數據結構,例如task_thread_info函數用于獲取線程信息,stack_frame函數用于計算基指針,in_exception_stack和in_irq_stack函數用于判斷堆棧類型。回調函數ops->stack用于打印堆棧標識符,回調函數ops->walk_stack用于執行堆棧的遍歷操作。
(2)
// linux-3.10/arch/x86/include/asm/stacktrace.h/* Generic stack tracer with callbacks */struct stacktrace_ops {void (*address)(void *data, unsigned long address, int reliable);/* On negative return stop dumping */int (*stack)(void *data, char *name);walk_stack_t walk_stack;
};
/** x86-64 can have up to three kernel stacks:* process stack* interrupt stack* severe exception (double fault, nmi, stack fault, debug, mce) hardware stack*/static inline int valid_stack_ptr(struct thread_info *tinfo,void *p, unsigned int size, void *end)
{void *t = tinfo;if (end) {if (p < end && p >= (end-THREAD_SIZE))return 1;elsereturn 0;}return p > t && p < t + THREAD_SIZE - size;
}unsigned long
print_context_stack(struct thread_info *tinfo,unsigned long *stack, unsigned long bp,const struct stacktrace_ops *ops, void *data,unsigned long *end, int *graph)
{struct stack_frame *frame = (struct stack_frame *)bp;while (valid_stack_ptr(tinfo, stack, sizeof(*stack), end)) {unsigned long addr;addr = *stack;if (__kernel_text_address(addr)) {if ((unsigned long) stack == bp + sizeof(long)) {ops->address(data, addr, 1);frame = frame->next_frame;bp = (unsigned long) frame;} else {ops->address(data, addr, 0);}print_ftrace_graph_addr(addr, data, ops, tinfo, graph);}stack++;}return bp;
}
EXPORT_SYMBOL_GPL(print_context_stack);static int print_trace_stack(void *data, char *name)
{printk("%s <%s> ", (char *)data, name);return 0;
}void printk_address(unsigned long address, int reliable)
{pr_cont(" [<%p>] %s%pB\n",(void *)address, reliable ? "" : "? ", (void *)address);
}/** Print one address/symbol entries per line.*/
static void print_trace_address(void *data, unsigned long addr, int reliable)
{touch_nmi_watchdog();printk(data);printk_address(addr, reliable);
}static const struct stacktrace_ops print_trace_ops = {.stack = print_trace_stack,.address = print_trace_address,.walk_stack = print_context_stack,
};
2.5 print_context_stack
/* The form of the top of the frame on the stack */
struct stack_frame {struct stack_frame *next_frame;unsigned long return_address;
};
/** x86-64 can have up to three kernel stacks:* process stack* interrupt stack* severe exception (double fault, nmi, stack fault, debug, mce) hardware stack*/static inline int valid_stack_ptr(struct thread_info *tinfo,void *p, unsigned int size, void *end)
{void *t = tinfo;if (end) {if (p < end && p >= (end-THREAD_SIZE))return 1;elsereturn 0;}return p > t && p < t + THREAD_SIZE - size;
}unsigned long
print_context_stack(struct thread_info *tinfo,unsigned long *stack, unsigned long bp,const struct stacktrace_ops *ops, void *data,unsigned long *end, int *graph)
{struct stack_frame *frame = (struct stack_frame *)bp;while (valid_stack_ptr(tinfo, stack, sizeof(*stack), end)) {unsigned long addr;addr = *stack;if (__kernel_text_address(addr)) {if ((unsigned long) stack == bp + sizeof(long)) {ops->address(data, addr, 1);frame = frame->next_frame;bp = (unsigned long) frame;} else {ops->address(data, addr, 0);}print_ftrace_graph_addr(addr, data, ops, tinfo, graph);}stack++;}return bp;
}
EXPORT_SYMBOL_GPL(print_context_stack);
print_context_stack函數用于在給定線程的堆棧上打印函數調用的地址,并通過提供的回調函數執行相應的操作。
函數首先定義了局部變量frame,它是一個指向struct stack_frame類型的指針,用于表示幀結構。
然后,使用一個循環遍歷堆棧中的每個地址。在每次循環迭代中,函數檢查堆棧指針是否有效,并獲取當前堆棧指針處的地址。
如果地址屬于內核文本空間(通過__kernel_text_address函數判斷),則進行以下操作:
- 如果當前堆棧指針等于基指針加上一個long大小,表示該地址是當前函數調用的返回地址。在這種情況下,將調用回調函數ops->address(data, addr, 1)打印地址,并更新幀結構和基指針,使其指向上一幀的基指針。
- 如果當前堆棧指針不等于基指針加上一個long大小,表示該地址是普通的函數調用地址。在這種情況下,將調用回調函數ops->address(data, addr, 0)打印地址。
- 最后,調用print_ftrace_graph_addr函數打印與地址相關的ftrace圖形信息。
在每次循環迭代后,將堆棧指針指向下一個地址。
最后,函數返回更新后的基指針。
參考資料
Linux 3.10.0
https://blogs.oracle.com/linux/post/unwinding-stack-frame-pointers-and-orc
https://blog.csdn.net/Longyu_wlz/article/details/103327538