如何查看内核的调用栈

如何查看函数的调用栈

也就是根据当前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)

©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页