Linux内核堆栈实现分析 save_stack_trace

1 内核线程

Linux 内核为每个线程分配THREAD_SIZE(16k)的栈空间, 在每个堆栈的顶部放着struct thread_info 结构体,用来保存线程相关信息.
 

其中有几个重要变量:

Preempt_count :

此变量分为四部分

0-7bit :当前进程是否能抢占的标志

8-15bit:softirq  使能标志

16-23bit :hardirq 使能标志

24bit:PREEMPT_ACTIVE标志位(原子上下文标志位??)

Task:  进程相关的结构,包含更加丰富的信息

Cpu_context :cpu 寄存器值,这个应该是当前进程被切换时,保留下来的线程执行场景.

struct thread_info {

    unsignedlong     flags;     /* low level flags */

    int        preempt_count;    /* 0 => preemptable, <0 => bug */

    mm_segment_t      addr_limit;   /* address limit */

    structtask_struct   *task;     /* main task structure */

    __u32         cpu;       /*cpu */

    structcpu_context_save  cpu_context;  /* cpu context */

 ……………………….

}

获取当前线程/进程

只需要获得当前sp指针,然后进行16k字节对齐即可找到thread_info结构

register unsignedlong sp asm ("sp");

return (structthread_info *)(sp & ~(THREAD_SIZE - 1));

内核提供的相关函数结构

 static inline struct thread_info*current_thread_info(void)

     以及current宏用于获取当前进程结构体.

c 语言中获取cpsr寄存器

static inline unsigned longarch_local_cpsr_save(void)

{

   unsignedlong flags ;

   asmvolatile( "   mrs %0, cpsr   @ arch_local_irq_save\n"

      :: "r" (flags): "memory", "cc");

   returnflags;

}

在ARM64中,如果定义了CONFIG_THREAD_INFO_IN_TASK 中,task_struct中直接会包含thread_info,这是thread_info就没有在线程的栈空间了,ARM64在进程切换时使用sp_el0来保存当前进程的task_struct。

这current = (struct stask_sturct *)sp_el0, 这是与之前不同的地方.

2 函数调用时stack frame

每一个进程都有自己的栈。考虑进程执行时发生函数调用的场景,母函数和子函数使用的是同一个栈,在通常的情况下,并不需要区分母函数和子函数分别使用了栈的哪个部分。但是,当需要在执行过程中对函数调用进行backtrace的时候,这一信息就很重要了。

简单的说,stack frame就是一个函数所使用的stack的一部分,所有函数的stack frame串起来就组成了一个完整的栈。stack frame的两个边界分别由FP和SP来限定。

通过FP指针就可以找出所有的backtrace过程

在程序执行过程中(通常是发生了某种意外情况而需要进行调试),通过SP和FP所限定的 stackframe,就可以得到母函数的SP和FP,从而得到母函数的stack frame(PC,LR,SP,FP会在函数调用的第一时间压栈),以此追溯,即可得到所有函数的调用顺序。

要内核支持FP指针必须打开CONFIG_FRAME_POINTER配置

3内核save_stack_trace分析

voidsave_stack_trace(struct stack_trace *trace)

{

save_stack_trace_tsk(current, trace);

}

接着分析:

voidsave_stack_trace_tsk(struct task_struct *tsk, struct stack_trace *trace)

{

struct stack_trace_data data;

struct stackframe frame;

 

data.trace = trace;

data.skip = trace->skip;//设置需要忽视的调用级数,一般设置为0

 

if (tsk != current) {

#ifdefCONFIG_SMP

           /*

            * What guarantees do we have here that 'tsk'is not

            * running on another CPU?  For now, ignore it as we

            * can't guarantee we won't explode.

            */

//如果不是保存当前cpu上的当前进程,那么是很难确定tsk进程寄存器的值的,

也许时刻都在变化.

           if (trace->nr_entries <trace->max_entries)

                    trace->entries[trace->nr_entries++]= ULONG_MAX;

           return;

#else //在单cpu时,tsk进程已经被切换,可以追溯backtrace

           data.no_sched_functions = 1;

           frame.fp = thread_saved_fp(tsk);

           frame.sp = thread_saved_sp(tsk);

           frame.lr = 0;              /* recovered from the stack */

           frame.pc = thread_saved_pc(tsk);

#endif

} else {

           register unsigned long current_sp asm("sp");//通过sp获取当前堆栈指针

   //__builtin_frame_address(0)返回当前函数的FP指针

 //__builtin_return_address(0)返回当前函数的返回地址(LR)

           data.no_sched_functions = 0;

           frame.fp = (unsignedlong)__builtin_frame_address(0);

           frame.sp = current_sp;

           frame.lr = (unsignedlong)__builtin_return_address(0);

           frame.pc = (unsignedlong)save_stack_trace_tsk;//通过函数名获取到当前pc

}

 

walk_stackframe(&frame, save_trace,&data);//进行堆栈遍历

if (trace->nr_entries <trace->max_entries)

           trace->entries[trace->nr_entries++]= ULONG_MAX;

}

其中save_trace主要保存当前pc到数组中.

staticint save_trace(struct stackframe *frame, void *d)

{

struct stack_trace_data *data = d;

struct stack_trace *trace = data->trace;

unsigned long addr = frame->pc;

if (data->no_sched_functions &&in_sched_functions(addr))

           return 0;

if (data->skip) {//如果有设置skip,则会跳过当前调用

           data->skip--;

           return 0;

}

trace->entries[trace->nr_entries++] =addr;//保存当前pc

return trace->nr_entries >=trace->max_entries;

}

接着分析walk_stackframe函数

void notrace walk_stackframe(struct stackframe *frame,

               int (*fn)(struct stackframe *, void *), void *data)

{

while (1) {

           int ret;

 

           if (fn(frame, data))//调用save_trace保存当前pc

                    break;

           ret = unwind_frame(frame);//FP指针移到上一个函数

           if (ret < 0)

                    break;

}

}

 

 

 

intnotrace unwind_frame(struct stackframe *frame)

{

unsigned long high, low;

unsigned long fp = frame->fp;

 

/* only go to a higher address on the stack */

low = frame->sp;//当前堆栈末端

high = ALIGN(low, THREAD_SIZE);//当前堆栈顶端

 

/* check current frame pointer is within bounds*/

if (fp < low + 12 || fp > high - 4)

           return -EINVAL;

 

/* restore the registers from the stack frame*/

由上面的堆栈图可知,这几个指针在堆栈上的存放顺序为

Pc,lr,sp,fp

frame->fp = *(unsigned long *)(fp - 12);

frame->sp = *(unsigned long *)(fp - 8);

frame->pc = *(unsigned long *)(fp - 4);

return 0;

}

 3.1 获取stack实例

 voidaee_get_traces(char *msg)

{

         structstack_trace trace;

         inti;

         intoffset;

         if(trace_entry_ptr == NULL)

                   return;

         memset(trace_entry_ptr,0, MAX_STACK_TRACE_DEPTH * 4);

         trace.entries= trace_entry_ptr;

         /*savebacktraces */

         trace.nr_entries= 0;

         trace.max_entries= 32;//32级调用

         trace.skip= 0;

         save_stack_trace_tsk(current,&trace);

         for(i = 0; i < trace.nr_entries; i++) {//current

                   offset= strlen(msg);

                   //根据pc,通过%pf , %pf就可以打印出函数名

                   snprintf(msg+ offset, KERNEL_REPORT_LENGTH - offset, "[<%p>]%pS\n",

                             (void *)trace.entries[i], (void*)trace.entries[i]);

         }

}

 

4.一种基于内核堆栈的调试方式

 

内核产生oops时,会通过die()和panic()吐出大量当前线程信息,其中最重要为FP和SP指针

由上面知道,通过FP指针可以追溯函数调用过程.在用fp指针分析堆栈数据之前,需要确认系统的堆栈增长方向.大多数栈都是从高地址向低地址增长.

以下为oops信息,在每个FP帧指针处,依次排列着:PC,LR,SP,FP

在堆数据中找出以上四个值,其中

PC用红色标记,表示上一级的PC,

LR用绿色标记,表示上一级函数的返回地址,

FP用蓝色标记,表示上一级函数的帧指针:

 

1.第一级FP帧

第一级FP的地址为0xd9ec1f24,找到1f24的地方.

可以看到,0xd9ec1f24存的值是0xc04eff24,这就是上一级的函数(pc), c01e05c8是上一级

的返回地址(lr),这两个地址可以通过addr2line在vmlinux中找到对于函数名.

Addr2line –e vmlinux 0xc01e05c8 –f

 

找到当前FP帧的四个元素以后,开始找上一级FP帧

2. 第二级FP帧

 从0xd9ec1f24倒退12个字节,得到第二级帧地址为0xd9ec1f44,而0xd9ec1f44的内容为

0xc01e0558,这是pc值,其返回地址为c018be94.依次类推,可以遍历堆栈上的所有FP帧.

直到找到的FP指针为0为止.

[  98.062716]-(0)[232:sh]Internal error: Oops - BUG: 0 [#1] PREEMPT SMP ARM

[  98.069436]disable aee kernel api

[ 103.154589]-(0)[232:sh]Modules linked in:-(0)[232:sh]

[ 103.159615]-(0)[232:sh]CPU: 0 PID: 232 Comm: sh Tainted: G        W   3.10.35+ #50

[ 103.167194]-(0)[232:sh]task: d92e1000 ti: d9ec0000 task.ti: d9ec0000

[ 103.173481]-(0)[232:sh]PC is at proc_generate_oops_read+0x80/0x94

[ 103.179511]-(0)[232:sh]LR is at 0xa216465

[ 103.183479]-(0)[232:sh]pc : [<c04eff98>]    lr : [<0a216465>]    psr: 60010013

[ 103.183479]sp : d9ec1e88  ip : 00000010 fp : d9ec1f24

[ 103.195709]-(0)[232:sh]r10: 00000000 r9 : 00001000  r8 : b79a6c90

[ 103.201826]-(0)[232:sh]r7 : 00000000 r6 : 00000001  r5 : 00000000  r4 : b79a6c90

[ 103.209235]-(0)[232:sh]r3 : 00000000 r2 : 00000000  r1 : d9ec1e9c  r0 : 00000000

[ 103.216646]-(0)[232:sh]Flags: nZCv IRQs on  FIQs on  Mode SVC_32 ISA ARM 

[ 103.508017]-(0)[232:sh]Process sh (pid: 232, stack limit = 0xd9ec0248)

[ 103.514478]-(0)[232:sh]Stack: (0xd9ec1e88 to 0xd9ec2000)

1e80:                00010000 73706f4f6e654720 74617265 0a216465 d9ec1e00

1ea0: c02b7cd8 00000000 00000000 00000000d9ec1edc 00020000 c9627e08 da5a5920

1ec0: 00000000 00001000 00000000 c9627e00d9ec1f04 d9ec1ee0 c02addb8 c01c74f8

1ee0: 00000000 00000000 c0186344 0000000000000000 00001000 d9ec1f44 d9ec1f08

1f00: c018bd58 c02add20 c9627e00 271ae73fc9627e08 de48ed80 d9ec1f44 d9ec1f28

1f20: c01e05c8c04eff24  c9627e00b79a6c90 d9ec1f78 00000000d9ec1f74 d9ec1f48

1f40: c018be94c01e0558d9ec0000 00000000 d9ec1f7c 00000000 00000000 c9627e00

1f60: 00000000 b79a6c90 d9ec1fa4 d9ec1f78 c018c4b4c018bde8  0000000000000000

1f80: 00000003 00000000 00000000 00000003c000dc44 d9ec0000  00000000 d9ec1fa8

1fa0: c000d9c0c018c474 00000003 00000000 00000003 b79a6c9000001000 ffffff60

1fc0: 00000003 00000000 00000000 00000003b79a6c90 b79a5ce0 b6f65b6c b6f5bde9

1fe0: 00001000 be9865d8 b6f549db b6ecc94060010010 00000003 00000000 00000000

 

[ 103.628283]Backtrace:

 

[ 103.630622]-(0)[232:sh][<c04eff18>](proc_generate_oops_read+0x0/0x94) from [<c01e05c8>](proc_reg_read+0x7c/0x90)

[ 103.640863] r4:de48ed80

[ 103.643286]-(0)[232:sh][<c01e054c>] (proc_reg_read+0x0/0x90) from[<c018be94>] (vfs_read+0xb8/0x148)

[ 103.652321] r7:00000000 r6:d9ec1f78 r5:b79a6c90 r4:c9627e00

[ 103.657847]-(0)[232:sh][<c018bddc>] (vfs_read+0x0/0x148) from[<c018c4b4>] (SyS_read+0x4c/0x7c)

[ 103.666451] r8:b79a6c90 r7:00000000 r6:c9627e00 r5:00000000 r4:00000000

[ 103.673010]-(0)[232:sh][<c018c468>] (SyS_read+0x0/0x7c) from[<c000d9c0>] (ret_fast_syscall+0x0/0x48)

[ 103.682131] r9:d9ec0000 r8:c000dc44 r7:00000003 r6:00000000 r5:00000000

r4:00000003

[ 103.689803]-(0)[232:sh]Code: e3a02010 ebf82143 e3500000 1afffff5 (e7f001f2)

[ 103.696783]-(0)[232:sh]---[ end trace 7ced394a7aba437f ]---

[ 103.702295]-(0)[232:sh]Kernel panic - not syncing: Fatal exception

 

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值