如何查看內核的調用棧

如何查看函數的調用棧

也就是根據當前cpu內寄存器的值,如何反推函數的調用棧,這種調試方法在內核調試過程中是很常見的,也算是常用的技巧。這個在追查死機等問題,可以用的上。

(1)通過ejtag來讀取當前pc的地址以及sp,ra寄存器的信息

首先,我們的機器起來以後,我們用ejtag調試工具查看一下當前的寄存器的內容如下:

cpu0 -set
#set
zero:0x0 at:0x5400cce1 v0:0xffffffff818d0000 v1:0x1 
a0:0x0 a1:0x140000000000000 a2:0x0 a3:0x1 
t0:0x0 t1:0x30 t2:0x20 t3:0x2f72657070617773 
t4:0x0 t5:0xffffffff80f33d70 t6:0x0 t7:0x1 
s0:0xffffffff80f30010 s1:0xffffffff80f50000 s2:0xffffffff80f30000 s3:0xffffffff80f55838 
s4:0xffffffff81810000 s5:0x1 s6:0xffffffff81810000 s7:0xffffffff80e2ee28 
t8:0x0 t9:0xffffffff80216438 k0:0xffffffff80f33e00 k1:0xffffffff80f33e00 
gp:0xffffffff80f30000 sp:0xffffffff80f33e00 s8:0x0 ra:0xffffffff802b855c 
status:0x5400cce1 lo:0x58028e44 hi:0x0 badvaddr:0xfff19a8000 
cause:0x10000000 pc:0xffffffff80214760 epc:0xffffffff80214760

根據上面的值可以看出當前pc的地址也就是pc寄存器:pc:0xffffffff80214760這個地址。這個地址是屬於內核態的地址。然後我們在ejtag下反彙編一下這地址沒看看具體執行的是什麼指令。反彙編如下:

cpu0 -disas 0xffffffff80214760 0x10
#disas 0xffffffff80214760 0x10
0xffffffff80214760: 03e00008 jr      ra
0xffffffff80214764: 00000000 nop     
0xffffffff80214768: 00000000 nop     
0xffffffff8021476c: 00000000 nop     
0xffffffff80214770: 00000000 nop     
0xffffffff80214774: 00000000 nop     
0xffffffff80214778: 00000000 nop     
0xffffffff8021477c: 00000000 nop     
0xffffffff80214780: 403a7000 dmfc0   k0,C0_EPC
0xffffffff80214784: 3c1b8021 lui     k1,0x8021    # 32801
0xffffffff80214788: 677b4740 daddiu  k1,k1,0x4740 # 18240
0xffffffff8021478c: 375a001f ori     k0,k0,0x1f   # 31
0xffffffff80214790: 3b5a001f xori    k0,k0,0x1f   # 31
0xffffffff80214794: 175b0002 bne     k0,k1,802147a0# 0x802147a0
0xffffffff80214798: 00000000 nop     
0xffffffff8021479c: 40ba7000 dmtc0   k0,C0_EPC

如上所示,我們這裏反彙編了16條指令,地址從0xffffffff80214760開始。那麼這個時候,我們需要將心在運行的內核vmlinux文件拷貝出來,用編譯內核的反彙編工具,反彙編內核。使用
mips64el-redhat-linux-objdump -D vmlinux > k.log來反彙編內核內核代碼並輸出到k.log文件中。查看當前的地址是不是這條指令,如果是說明我們反彙編的內核就是我們現在運行的內核,否則就要檢查內核或者編譯器是否正確。
下面是我反彙編的內核片段:
在這裏插入圖片描述
如上圖所示,圖片中0xffffffff80214760地址剛好就是我們咋ejtag下反匯編出來的代碼指令,說明我們現在反彙編的內核和運行的內核完全一致。

(2)通過sp地址來回溯函數的調用關係

下面我們就通過sp地址也就是函數的棧來查看函數的調用層次關係,還是回到上面的ejtag set出來的寄存器值,sp的值爲
sp:0xffffffff80f33e00,也就是說當前函數棧指針的位置在ffffffff80f33e00,那麼這個位置是屬於哪個函數的棧呢,我們就要看0xffffffff80214760這個指令所在的函數,查看反彙編代碼發現,這個函數在__r4k_wait這個函數內,但是這個函數進來的時候卻沒有操作棧指針,也就是說這個函數是一個內聯函數,其本身沒有棧,自身的代碼被編譯到了其調用位置的函數內的。那麼這個時候我們要找誰調用這個函數了,就要看反彙編的代碼,看看這個函數中ra的值位於哪個函數內。ra寄存器存放的是調用這個函數的地址跳轉指令的嚇一跳指令的地址。這時查看ra寄存器的值爲 ra:0xffffffff802b855c ,這時我們查看ffffffff802b855c這個地址的指令,其代碼如下:

 193036 fffffffff802b8530:_8e2258d4 _lw__v0,22740(s1)
 193037 ffffffff802b8534:_14400026 _bnez__v0,ffffffff802b85d0 <cpu_startup_entry+0x168>
 193038 ffffffff802b8538:_00000000 _nop
 193039 ffffffff802b853c:_de020000 _ld__v0,0(s0)
 193040 ffffffff802b8540:_30420004 _andi__v0,v0,0x4
 193041 ffffffff802b8544:_1440002a _bnez__v0,ffffffff802b85f0 <cpu_startup_entry+0x188>
 193042 ffffffff802b8548:_00000000 _nop
 193043 ffffffff802b854c:_0c0c05ac _jal_ffffffff803016b0 <rcu_idle_enter>
 193044 ffffffff802b8550:_00000000 _nop
 193045 ffffffff802b8554:_0c0858d0 _jal_ffffffff80216340 <arch_cpu_idle>
 193046 ffffffff802b8558:_00000000 _nop
 193047 ffffffff802b855c:_40026000 _mfc0__v0,c0_status                                                                                                                                        
 193048 ffffffff802b8560:_00021000 _sll_v0,v0,0x0
 193049 ffffffff802b8564:_30420001 _andi__v0,v0,0x1

根據上面的代碼可知,fffffff802b855c:_40026000 _mfc0__v0,c0_status 就是這條指令,其上兩條指令就是jal_ffffffff80216340 <arch_cpu_idle>和nop指令。(龍芯平臺每一條跳轉指令的下一條指令必須值nop)。這個時候也就是說從這個函數向上推最後一次將地址給ra的指令也就是上面的代碼al_ffffffff80216340 <arch_cpu_idle>這條指令。這個跳轉指令執行時跳轉到對應的函數內,於此同時將下一條指令的地址給ra,供返回使用。也就是說_r4_wait肯定是從函數這個函數就是函數arch_cpu_idle內調用進去的。繼續向上看代碼這段代碼位於函數<cpu_startup_entry>也就是說當前的函數棧應該是arch_cpu_idle,但是查看arch_cpu_idle反彙編的代碼發現:這個函數也沒有操作棧指針,也就是其使用的還是調用它函數的棧。所以這個當前sp的值是函數是<cpu_startup_entry>函數的棧,arch_cpu_idle和__r4k_wait都共用這個函數棧,具體爲什麼應該是編譯器的問題。
arch_cpu_idle的彙編代碼如下:

23067 ffffffff80216340 <arch_cpu_idle>:                                                                                                                                                     
  23068 ffffffff80216340:_3c02818d _lui_v0,0x818d
  23069 ffffffff80216344:_dc599ca0 _ld__t9,-25440(v0)
  23070 ffffffff80216348:_13200003 _beqz__t9,ffffffff80216358 <arch_cpu_idle+0x18>
  23071 ffffffff8021634c:_00000000 _nop
  23072 ffffffff80216350:_03200008 _jr__t9
  23073 ffffffff80216354:_00000000 _nop
  23074 ffffffff80216358:_40016000 _mfc0__at,c0_status
  23075 ffffffff8021635c:_3421001f _ori_at,at,0x1f
  23076 ffffffff80216360:_3821001e _xori__at,at,0x1e
  23077 ffffffff80216364:_40816000 _mtc0__at,c0_status
  23078 ffffffff80216368:_00000040 _ssnop
  23079 ffffffff8021636c:_00000040 _ssnop
  23080 ffffffff80216370:_03e00008 _jr__ra
  23081 ffffffff80216374:_00000040 _ssnop

從源碼的調用關係上可知:(源碼如下)

void cpu_startup_entry(enum cpuhp_state state)
{
__/*
__ * This #ifdef needs to die, but it's too late in the cycle to
__ * make this generic (arm and sh have never invoked the canary
__ * init for the non boot cpus!). Will be fixed in 3.11
__ */
#ifdef CONFIG_X86
__/*
__ * If we're the non-boot CPU, nothing set the stack canary up
__ * for us. The boot CPU already has it initialized but no harm
__ * in doing it again. This is a good place for updating it, as
__ * we wont ever return from this function (so the invalid
__ * canaries already on the stack wont ever trigger).
__ */
__boot_init_stack_canary();
#endif
__arch_cpu_idle_prepare();
__cpu_idle_loop();                                                                                                                                                                            
}

這裏面編譯器將函數cpu_idle_loop();直接以內聯函數的形式來編譯的,並不是以函數調用的形式。下面看一下cpu_idle_loop()這個函數:

static void cpu_idle_loop(void)
{                                                                                                                                                                                             
__while (1) {
____/*
____ * If the arch has a polling bit, we maintain an invariant:
____ *
____ * Our polling bit is clear if we're not scheduled (i.e. if
____ * rq->curr != rq->idle).  This means that, if rq->idle has
____ * the polling bit set, then setting need_resched is
____ * guaranteed to cause the cpu to reschedule.
____ */

______current_set_polling();
____quiet_vmstat();
____tick_nohz_idle_enter();

____while (!need_resched()) {
______check_pgt_cache();
______rmb();

______if (cpu_is_offline(smp_processor_id()))
________arch_cpu_idle_dead();

______local_irq_disable();
______arch_cpu_idle_enter();

______/*
______ * In poll mode we reenable interrupts and spin.
______ *
______ * Also if we detected in the wakeup from idle
______ * path that the tick broadcast device expired
______ * for us, we don't want to go deep idle as we
______ * know that the IPI is going to arrive right
______ * away
______ */
______if (cpu_idle_force_poll || tick_check_broadcast_expired()) {
________cpu_idle_poll();
______} else {
________if (!current_clr_polling_and_test()) {
__________stop_critical_timings();
__________rcu_idle_enter();
__________arch_cpu_idle();
__________WARN_ON_ONCE(irqs_disabled());
__________rcu_idle_exit();
__________start_critical_timings();
________} else {
__________local_irq_enable();
________}
__________current_set_polling();
______}
______arch_cpu_idle_exit();
____}
____tick_nohz_idle_exit();
______current_clr_polling();

____/*
____ * We promise to call sched_ttwu_pending and reschedule
____ * if need_resched is set while polling is set.  That
____ * means that clearing polling needs to be visible
____ * before doing these things.
____ */
____smp_mb__after_atomic();

____sched_ttwu_pending();
____schedule_preempt_disabled();
__}
}

cpu_idle_loop這個函數又調用了很多函數,其中arch_cpu_idle函數函數調用了__r4_wait()函數
其代碼如下:

void arch_cpu_idle(void)
{
__smtc_idle_hook();
__if (cpu_wait)
____cpu_wait();
__else
____local_irq_enable();
}

這裏面cpu_wait就是上面的__r4k_wait,所以到這裏我們弄明白了,__r4k_wait是從哪個函數裏面調用過來的。也就是從cpu_startup_entry函數調用過來的。那麼誰又調用的這個函數呢?這個時候就需要看函數的棧內的信息了。

(3)如何找誰調用的cpu_startup_entry
根據這個函數的反彙編代碼,代碼入口如下:

192985 ffffffff802b8468 <cpu_startup_entry>:
 192986 ffffffff802b8468:_67bdffb0 _daddiu__sp,sp,-80
 192987 ffffffff802b846c:_ffb70040 _sd__s7,64(sp)                                                                                                                                             
 192988 ffffffff802b8470:_ffb60038 _sd__s6,56(sp)
 192989 ffffffff802b8474:_ffb50030 _sd__s5,48(sp)
 192990 ffffffff802b8478:_ffb40028 _sd__s4,40(sp)
 192991 ffffffff802b847c:_ffb30020 _sd__s3,32(sp)
 192992 ffffffff802b8480:_ffb20018 _sd__s2,24(sp)
 192993 ffffffff802b8484:_ffb10010 _sd__s1,16(sp)
 192994 ffffffff802b8488:_ffb00008 _sd__s0,8(sp)
 192995 ffffffff802b848c:_ffbf0048 _sd__ra,72(sp)
 192996 ffffffff802b8490:_0c0ae0d8 _jal_ffffffff802b8360 <arch_cpu_idle_prepare>
 192997 ffffffff802b8494:_3c148181 _lui_s4,0x8181
 192998 ffffffff802b8498:_3c0280c9 _lui_v0,0x80c9
 192999 ffffffff802b849c:_3c1780e3 _lui_s7,0x80e3
 193000 ffffffff802b84a0:_dc539568 _ld__s3,-27288(v0)
 193001 ffffffff802b84a4:_66f7ee28 _daddiu__s7,s7,-4568
 193002 ffffffff802b84a8:_67900010 _daddiu__s0,gp,16
 193003 ffffffff802b84ac:_0380902d _move__s2,gp
 193004 ffffffff802b84b0:_3c1180f5 _lui_s1,0x80f5
 193005 ffffffff802b84b4:_0280b02d _move__s6,s4
 193006 ffffffff802b84b8:_24150001 _li__s5,1
 193007 ffffffff802b84bc:_0c0d8852 _jal_ffffffff80362148 <quiet_vmstat>
 193008 ffffffff802b84c0:_00000000 _nop
 193009 ffffffff802b84c4:_0c0b1104 _jal_ffffffff802c4410 <tick_nohz_idle_enter>
 193010 ffffffff802b84c8:_00000000 _nop

根據上面的代碼可知,函數開始部分就將ra寄存器的值保存在了sp偏移72字節的位置處(ffffffff802b848c:_ffbf0048 _sd__ra,72(sp)),這時我們使用ejtag來dump棧的數據;sp這時爲0xffffffff80f33e00,操作如下:

cpu0 -d8 0xffffffff80f33e00 10
#d8 0xffffffff80f33e00 10
ffffffff80f33e00: 0000000000000000 0000000000000001 ................
ffffffff80f33e10: ffffffff818c0000 ffffffff81870000 ................
ffffffff80f33e20: ffffffff818776a0 ffffffff818c0000 .v..............
ffffffff80f33e30: 0000000000000000 0000000000000000 ................
ffffffff80f33e40: 0000000000000000 ffffffff81820cf0 ................

根據上面的數據可知,ra的值就是ffffffff81820cf0,然後用這個地址去查看反彙編的代碼,看看位於什麼函數內。操作如下:

ffffffff81820cb8:_3c0280f5 _lui_v0,0x80f5
5017119 ffffffff81820cbc:_00042238 _dsll__a0,a0,0x8
5017120 ffffffff81820cc0:_64424680 _daddiu__v0,v0,18048
5017121 ffffffff81820cc4:_0082102d _daddu_v0,a0,v0
5017122 ffffffff81820cc8:_0c60a573 _jal_ffffffff818295cc <check_bugs32>
5017123 ffffffff81820ccc:_ac430010 _sw__v1,16(v0)
5017124 ffffffff81820cd0:_0c60b1e7 _jal_ffffffff8182c79c <check_bugs64>
5017125 ffffffff81820cd4:_00000000 _nop
5017126 ffffffff81820cd8:_0c61728f _jal_ffffffff8185ca3c <acpi_early_init>
5017127 ffffffff81820cdc:_00000000 _nop
5017128 ffffffff81820ce0:_0c0937d0 _jal_ffffffff8024df40 <efi_enabled>
5017129 ffffffff81820ce4:_24040003 _li__a0,3
5017130 ffffffff81820ce8:_0c31b0e4 _jal_ffffffff80c6c390 <rest_init>
5017131 ffffffff81820cec:_00000000 _nop
5017132 ffffffff81820cf0:_dfbf0038 _ld__ra,56(sp)
5017133 ffffffff81820cf4:_dfb40030 _ld__s4,48(sp)
5017134 ffffffff81820cf8:_dfb30028 _ld__s3,40(sp)
5017135 ffffffff81820cfc:_dfb20020 _ld__s2,32(sp)
5017136 ffffffff81820d00:_dfb10018 _ld__s1,24(sp)
5017137 ffffffff81820d04:_dfb00010 _ld__s0,16(sp)
5017138 ffffffff81820d08:_03e00008 _jr__ra
5017139 ffffffff81820d0c:_67bd0040 _daddiu__sp,sp,64

ffffffff81820cf0這個地址的指令就是ld__ra,56(sp),那麼他的前兩條指令就是jal_ffffffff80c6c390 <rest_init>和nop,因此可知,cpu_startup_entry這個函數就是從rest_init這個函數內調用過去的,那麼我們看一下源碼來確認一下:
源碼如下:

static noinline void __init_refok rest_init(void)
{                                                                                                                                                                                             
__int pid;

__rcu_scheduler_starting();
__/*
__ * We need to spawn init first so that it obtains pid 1, however
__ * the init task will end up wanting to create kthreads, which, if
__ * we schedule it before we create kthreadd, will OOPS.
__ */
__kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
__numa_default_policy();
__pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
__rcu_read_lock();
__kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
__rcu_read_unlock();
__complete(&kthreadd_done);

__/*
__ * The boot idle thread must execute schedule()
__ * at least once to get things moving:
__ */
__init_idle_bootup_task(current);
__schedule_preempt_disabled();
__/* Call into cpu_idle with preempt disabled */
__cpu_startup_entry(CPUHP_ONLINE);
}

從上面的代碼可知,確實函數rest_init 調用了函數cpu_startup_entry這個函數。

(4)如何找誰調用了函數rest_init

起始這裏有兩種方法,一種是看反彙編代碼,看這個地址ffffffff81820cf0位於什麼函數內,那麼這個函數就是調用rest_init的地址
操作如下:

ffffffff81820cd4:_00000000 _nop
5017126 ffffffff81820cd8:_0c61728f _jal_ffffffff8185ca3c <acpi_early_init>
5017127 ffffffff81820cdc:_00000000 _nop
5017128 ffffffff81820ce0:_0c0937d0 _jal_ffffffff8024df40 <efi_enabled>
5017129 ffffffff81820ce4:_24040003 _li__a0,3
5017130 ffffffff81820ce8:_0c31b0e4 _jal_ffffffff80c6c390 <rest_init>
5017131 ffffffff81820cec:_00000000 _nop
5017132 ffffffff81820cf0:_dfbf0038 _ld__ra,56(sp)
5017133 ffffffff81820cf4:_dfb40030 _ld__s4,48(sp)
5017134 ffffffff81820cf8:_dfb30028 _ld__s3,40(sp)
5017135 ffffffff81820cfc:_dfb20020 _ld__s2,32(sp)
5017136 ffffffff81820d00:_dfb10018 _ld__s1,24(sp)
5017137 ffffffff81820d04:_dfb00010 _ld__s0,16(sp)
5017138 ffffffff81820d08:_03e00008 _jr__ra
5017139 ffffffff81820d0c:_67bd0040 _daddiu__sp,sp,64

繼續向上追得出,這個函數是start_kernel,也就是從start_kernel中調用過來的。另外一種辦法就是打印函數棧,上面我們打印的函數棧是函數cpu_startup_entry的函數棧,那麼函數rest_init的棧怎麼找呢,就是在原來的棧的基地址0xffffffff80f33e00加上棧的大小72個字節之後,就是ffffffff80f33e50開始,那麼他的棧多大呢?就要看彙編代碼函數rest_init,其代碼入下:

ffffffff80c6c390 <rest_init>:                                                                                                                                                         
2796460 ffffffff80c6c390:_67bdffe0 _daddiu__sp,sp,-32
2796461 ffffffff80c6c394:_ffbf0018 _sd__ra,24(sp)
2796462 ffffffff80c6c398:_0c0bfe70 _jal_ffffffff802ff9c0 <rcu_scheduler_starting>
2796463 ffffffff80c6c39c:_00000000 _nop
2796464 ffffffff80c6c3a0:_3c0480c7 _lui_a0,0x80c7
2796465 ffffffff80c6c3a4:_6484c428 _daddiu__a0,a0,-15320
2796466 ffffffff80c6c3a8:_0000282d _move__a1,zero
2796467 ffffffff80c6c3ac:_0c094cc0 _jal_ffffffff80253300 <kernel_thread>
2796468 ffffffff80c6c3b0:_24060a00 _li__a2,2560
2796469 ffffffff80c6c3b4:_0c0e6086 _jal_ffffffff80398218 <numa_default_policy>
2796470 ffffffff80c6c3b8:_00000000 _nop
2796471 ffffffff80c6c3bc:_3c048028 _lui_a0,0x8028

從上面的代碼可知,rest_init的棧的帶下爲32字節,所以我們就找到了rest_init的棧,這裏沒有dump棧的內容,因爲環境被同事破壞了,所以只能將原理了,然後從棧的偏移24字節的地方就是存放的ra的值,也就是誰調用rest_init的地址。也就是start_kernel的地址。這裏我明就找到了整個函數的調用關係即
start_kernel–>rest_init–>cpu_startup_entry–>cpu_idle_loop–>arch_cpu_idle–>cpu_wait(_r4_wait)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章