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宏用于获取当前进程结构体.
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, 这是与之前不同的地方.
每一个进程都有自己的栈。考虑进程执行时发生函数调用的场景,母函数和子函数使用的是同一个栈,在通常的情况下,并不需要区分母函数和子函数分别使用了栈的哪个部分。但是,当需要在执行过程中对函数调用进行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配置
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->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->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;
}
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]);
}
}
内核产生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 : [
[ 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][
[ 103.640863] r4:de48ed80
[ 103.643286]-(0)[232:sh][
[ 103.652321] r7:00000000 r6:d9ec1f78 r5:b79a6c90 r4:c9627e00
[ 103.657847]-(0)[232:sh][
[ 103.666451] r8:b79a6c90 r7:00000000 r6:c9627e00 r5:00000000 r4:00000000
[ 103.673010]-(0)[232:sh][
[ 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