unwind.c
// SPDX-License-Identifier: GPL-2.0-only /* * arch/arm/kernel/unwind.c * * Copyright (C) 2008 ARM Limited * * Stack unwinding support for ARM * * An ARM EABI version of gcc is required to generate the unwind * tables. For information about the structure of the unwind tables, * see "Exception Handling ABI for the ARM Architecture" at: * * http://infocenter.arm.com/help/topic/com.arm.doc.subset.swdev.abi/index.html */ #include <stdio.h> #include <stdint.h> #include "unwind.h" // /* Dummy functions to avoid linker complaints */ // void __aeabi_unwind_cpp_pr0(void) // { // }; // EXPORT_SYMBOL(__aeabi_unwind_cpp_pr0); // void __aeabi_unwind_cpp_pr1(void) // { // }; // EXPORT_SYMBOL(__aeabi_unwind_cpp_pr1); // void __aeabi_unwind_cpp_pr2(void) // { // }; // EXPORT_SYMBOL(__aeabi_unwind_cpp_pr2); #ifdef CONFIG_THUMB2_KERNEL #define frame_pointer(regs) (regs)->ARM_r7 #else #define frame_pointer(regs) (regs)->ARM_fp #endif #define ALIGN_UN(x, a) (((x) + (a) - 1) & ~((a) - 1)) static __always_inline void arm_get_current_stackframe(struct pt_regs *regs, struct stackframe *frame) { frame->fp = frame_pointer(regs); frame->sp = regs->ARM_sp; frame->lr = regs->ARM_lr; frame->pc = regs->ARM_pc; } extern const struct unwind_idx __start_unwind_idx[]; static const struct unwind_idx *__origin_unwind_idx; extern const struct unwind_idx __stop_unwind_idx[]; /* Convert a prel31 symbol to an absolute address */ #define prel31_to_addr(ptr) \ ({ \ /* sign-extend to 32 bits */ \ long offset = (((long)*(ptr)) << 1) >> 1; \ (unsigned long)(ptr) + offset; \ }) void dump_backtrace_entry(unsigned long where, unsigned long from, unsigned long frame) { unsigned long end = frame + 4 + sizeof(struct pt_regs); rt_kprintf("Function entered at [<%08lx>] from [<%08lx>]\n", where, from); } /* * Binary search in the unwind index. The entries are * guaranteed to be sorted in ascending order by the linker. * * start = first entry * origin = first entry with positive offset (or stop if there is no such entry) * stop - 1 = last entry */ static const struct unwind_idx *search_index(unsigned long addr, const struct unwind_idx *start, const struct unwind_idx *origin, const struct unwind_idx *stop) { unsigned long addr_prel31; pr_debug("%s(%08lx, %p, %p, %p)\n", __func__, addr, start, origin, stop); /* * only search in the section with the matching sign. This way the * prel31 numbers can be compared as unsigned longs. */ if (addr < (unsigned long)start) /* negative offsets: [start; origin) */ stop = origin; else /* positive offsets: [origin; stop) */ start = origin; /* prel31 for address relavive to start */ addr_prel31 = (addr - (unsigned long)start) & 0x7fffffff; while (start < stop - 1) { const struct unwind_idx *mid = start + ((stop - start) >> 1); /* * As addr_prel31 is relative to start an offset is needed to * make it relative to mid. */ if (addr_prel31 - ((unsigned long)mid - (unsigned long)start) < mid->addr_offset) stop = mid; else { /* keep addr_prel31 relative to start */ addr_prel31 -= ((unsigned long)mid - (unsigned long)start); start = mid; } } if ((start->addr_offset <= addr_prel31)) return start; else { pr_warn("unwind: Unknown symbol address %08lx\n", addr); return NULL; } } static const struct unwind_idx *unwind_find_origin( const struct unwind_idx *start, const struct unwind_idx *stop) { pr_debug("%s(%p, %p)\n", __func__, start, stop); while (start < stop) { const struct unwind_idx *mid = start + ((stop - start) >> 1); if (mid->addr_offset >= 0x40000000) /* negative offset */ start = mid + 1; else /* positive offset */ stop = mid; } pr_debug("%s -> %p\n", __func__, stop); return stop; } static const struct unwind_idx *unwind_find_idx(unsigned long addr) { const struct unwind_idx *idx = NULL; unsigned long flags; pr_debug("%s(%08lx)\n", __func__, addr); __origin_unwind_idx = unwind_find_origin(__start_unwind_idx, __stop_unwind_idx); /* main unwind table */ idx = search_index(addr, __start_unwind_idx, __origin_unwind_idx, __stop_unwind_idx); pr_debug("%s: idx = %p\n", __func__, idx); return idx; } static unsigned long unwind_get_byte(struct unwind_ctrl_block *ctrl) { unsigned long ret; if (ctrl->entries <= 0) { pr_warn("unwind: Corrupt unwind table\n"); return 0; } ret = (*ctrl->insn >> (ctrl->byte * 8)) & 0xff; if (ctrl->byte == 0) { ctrl->insn++; ctrl->entries--; ctrl->byte = 3; } else ctrl->byte--; return ret; } /* Before poping a register check whether it is feasible or not */ static int unwind_pop_register(struct unwind_ctrl_block *ctrl, unsigned long **vsp, unsigned int reg) { if ((ctrl->check_each_pop)) if (*vsp >= (unsigned long *)ctrl->sp_high) return -URC_FAILURE; rt_kprintf("%p: %p %p\n", ctrl->vrs[SP] , *vsp , *(*vsp)); ctrl->vrs[reg] = *(*vsp)++; rt_kprintf("%p: %p %p , %d %p\n", vsp , *vsp, **vsp ,reg ,ctrl->vrs[reg]); return URC_OK; } /* Helper functions to execute the instructions */ static int unwind_exec_pop_subset_r4_to_r13(struct unwind_ctrl_block *ctrl, unsigned long mask) { unsigned long *vsp = (unsigned long *)ctrl->vrs[SP]; int load_sp, reg = 4; load_sp = mask & (1 << (13 - 4)); while (mask) { if (mask & 1) if (unwind_pop_register(ctrl, &vsp, reg)) return -URC_FAILURE; mask >>= 1; reg++; } if (!load_sp) ctrl->vrs[SP] = (unsigned long)vsp; return URC_OK; } static int unwind_exec_pop_r4_to_rN(struct unwind_ctrl_block *ctrl, unsigned long insn) { unsigned long *vsp = (unsigned long *)ctrl->vrs[SP]; int reg; /* pop R4-R[4+bbb] */ for (reg = 4; reg <= 4 + (insn & 7); reg++) if (unwind_pop_register(ctrl, &vsp, reg)) return -URC_FAILURE; if (insn & 0x8) if (unwind_pop_register(ctrl, &vsp, 14)) return -URC_FAILURE; ctrl->vrs[SP] = (unsigned long)vsp; return URC_OK; } static int unwind_exec_pop_subset_r0_to_r3(struct unwind_ctrl_block *ctrl, unsigned long mask) { unsigned long *vsp = (unsigned long *)ctrl->vrs[SP]; int reg = 0; /* pop R0-R3 according to mask */ while (mask) { if (mask & 1) if (unwind_pop_register(ctrl, &vsp, reg)) return -URC_FAILURE; mask >>= 1; reg++; } ctrl->vrs[SP] = (unsigned long)vsp; return URC_OK; } /* * Execute the current unwind instruction. */ static int unwind_exec_insn(struct unwind_ctrl_block *ctrl) { unsigned long insn = unwind_get_byte(ctrl); int ret = URC_OK; pr_debug("%s: insn = %08lx\n", __func__, insn); if ((insn & 0xc0) == 0x00) ctrl->vrs[SP] += ((insn & 0x3f) << 2) + 4; else if ((insn & 0xc0) == 0x40) ctrl->vrs[SP] -= ((insn & 0x3f) << 2) + 4; else if ((insn & 0xf0) == 0x80) { unsigned long mask; insn = (insn << 8) | unwind_get_byte(ctrl); mask = insn & 0x0fff; if (mask == 0) { pr_warn("unwind: 'Refuse to unwind' instruction %04lx\n", insn); return -URC_FAILURE; } ret = unwind_exec_pop_subset_r4_to_r13(ctrl, mask); if (ret) goto error; } else if ((insn & 0xf0) == 0x90 && (insn & 0x0d) != 0x0d) ctrl->vrs[SP] = ctrl->vrs[insn & 0x0f]; else if ((insn & 0xf0) == 0xa0) { ret = unwind_exec_pop_r4_to_rN(ctrl, insn); if (ret) goto error; } else if (insn == 0xb0) { if (ctrl->vrs[PC] == 0) ctrl->vrs[PC] = ctrl->vrs[LR]; /* no further processing */ ctrl->entries = 0; } else if (insn == 0xb1) { unsigned long mask = unwind_get_byte(ctrl); if (mask == 0 || mask & 0xf0) { pr_warn("unwind: Spare encoding %04lx\n", (insn << 8) | mask); return -URC_FAILURE; } ret = unwind_exec_pop_subset_r0_to_r3(ctrl, mask); if (ret) goto error; } else if (insn == 0xb2) { unsigned long uleb128 = unwind_get_byte(ctrl); ctrl->vrs[SP] += 0x204 + (uleb128 << 2); } else { pr_warn("unwind: Unhandled instruction %02lx\n", insn); return -URC_FAILURE; } pr_debug("%s: fp = %08lx sp = %08lx lr = %08lx pc = %08lx\n", __func__, ctrl->vrs[FP], ctrl->vrs[SP], ctrl->vrs[LR], ctrl->vrs[PC]); error: return ret; } /* * Unwind a single frame starting with *sp for the symbol at *pc. It * updates the *pc and *sp with the new values. */ int unwind_frame(struct stackframe *frame) { unsigned long low; const struct unwind_idx *idx; struct unwind_ctrl_block ctrl; rt_thread_t current_thread = rt_thread_self(); /* store the highest address on the stack to avoid crossing it*/ low = frame->sp; ctrl.sp_high = ALIGN_UN(low, current_thread->stack_size); pr_debug("%s(pc = %08lx lr = %08lx sp = %08lx)\n", __func__, frame->pc, frame->lr, frame->sp); idx = unwind_find_idx(frame->pc); if (!idx) { pr_warn("unwind: Index not found %08lx\n", frame->pc); return -URC_FAILURE; } ctrl.vrs[FP] = frame->fp; ctrl.vrs[SP] = frame->sp; ctrl.vrs[LR] = frame->lr; ctrl.vrs[PC] = 0; if (idx->insn == 1) /* can't unwind */ return -URC_FAILURE; else if ((idx->insn & 0x80000000) == 0) /* prel31 to the unwind table */ ctrl.insn = (unsigned long *)prel31_to_addr(&idx->insn); else if ((idx->insn & 0xff000000) == 0x80000000) /* only personality routine 0 supported in the index */ ctrl.insn = &idx->insn; else { pr_warn("unwind: Unsupported personality routine %08lx in the index at %p\n", idx->insn, idx); return -URC_FAILURE; } /* check the personality routine */ if ((*ctrl.insn & 0xff000000) == 0x80000000) { ctrl.byte = 2; ctrl.entries = 1; } else if ((*ctrl.insn & 0xff000000) == 0x81000000) { ctrl.byte = 1; ctrl.entries = 1 + ((*ctrl.insn & 0x00ff0000) >> 16); } else { pr_warn("unwind: Unsupported personality routine %08lx at %p\n", *ctrl.insn, ctrl.insn); return -URC_FAILURE; } ctrl.check_each_pop = 0; while (ctrl.entries > 0) { int urc; if ((ctrl.sp_high - ctrl.vrs[SP]) < sizeof(ctrl.vrs)) ctrl.check_each_pop = 1; urc = unwind_exec_insn(&ctrl); if (urc < 0) return urc; if (ctrl.vrs[SP] < low || ctrl.vrs[SP] >= ctrl.sp_high) return -URC_FAILURE; } if (ctrl.vrs[PC] == 0) ctrl.vrs[PC] = ctrl.vrs[LR]; /* check for infinite loop */ if (frame->pc == ctrl.vrs[PC] && frame->sp == ctrl.vrs[SP]) return -URC_FAILURE; frame->fp = ctrl.vrs[FP]; frame->sp = ctrl.vrs[SP]; frame->lr = ctrl.vrs[LR]; frame->pc = ctrl.vrs[PC]; return URC_OK; } void unwind_backtrace(struct pt_regs *regs) { struct stackframe frame; pr_debug("%s(regs = %p)\n", __func__, regs); if (regs) { arm_get_current_stackframe(regs, &frame); /* PC might be corrupted, use LR in that case. */ // if (!kernel_text_address(regs->ARM_pc)) // frame.pc = regs->ARM_lr; } else { rt_kprintf("resg is null ,%s",__func__); } while (1) { int urc; unsigned long where = frame.pc; urc = unwind_frame(&frame); if (urc < 0) break; dump_backtrace_entry(where, frame.pc, frame.sp - 4); } } // struct unwind_table *unwind_table_add(unsigned long start, unsigned long size, // unsigned long text_addr, // unsigned long text_size) // { // unsigned long flags; // struct unwind_table *tab = malloc(sizeof(*tab)); // pr_debug("%s(%08lx, %08lx, %08lx, %08lx)\n", __func__, start, size, // text_addr, text_size); // if (!tab) // return tab; // tab->start = (const struct unwind_idx *)start; // tab->stop = (const struct unwind_idx *)(start + size); // tab->origin = unwind_find_origin(tab->start, tab->stop); // tab->begin_addr = text_addr; // tab->end_addr = text_addr + text_size; // list_add_tail(&tab->list, &unwind_tables); // return tab; // } // void unwind_table_del(struct unwind_table *tab) // { // unsigned long flags; // if (!tab) // return; // list_del(&tab->list); // free(tab); // }
unwind.h
/* SPDX-License-Identifier: GPL-2.0-only */ /* * arch/arm/include/asm/unwind.h * * Copyright (C) 2008 ARM Limited */ #ifndef __ASM_UNWIND_H #define __ASM_UNWIND_H #include <rtthread.h> #ifndef __ASSEMBLY__ /* Unwind reason code according the the ARM EABI documents */ enum unwind_reason_code { URC_OK = 0, /* operation completed successfully */ URC_CONTINUE_UNWIND = 8, URC_FAILURE = 9 /* unspecified failure of some kind */ }; struct unwind_idx { unsigned long addr_offset; unsigned long insn; }; struct stackframe { /* * FP member should hold R7 when CONFIG_THUMB2_KERNEL is enabled * and R11 otherwise. */ unsigned long fp; unsigned long sp; unsigned long lr; unsigned long pc; }; struct unwind_ctrl_block { unsigned long vrs[16]; /* virtual register set */ const unsigned long *insn; /* pointer to the current instructions word */ unsigned long sp_high; /* highest value of sp allowed */ /* * 1 : check for stack overflow for each register pop. * 0 : save overhead if there is plenty of stack remaining. */ int check_each_pop; int entries; /* number of entries left to interpret */ int byte; /* current byte number in the instructions word */ }; enum regs { #ifdef CONFIG_THUMB2_KERNEL FP = 7, #else FP = 11, #endif SP = 13, LR = 14, PC = 15 }; #define pr_debug(...) rt_kprintf(__VA_ARGS__) #define pr_warn(...) rt_kprintf(__VA_ARGS__) struct pt_regs { unsigned long uregs[17]; }; #define ARM_cpsr uregs[16] #define ARM_pc uregs[15] #define ARM_lr uregs[14] #define ARM_sp uregs[13] #define ARM_ip uregs[12] #define ARM_fp uregs[11] #define ARM_r10 uregs[10] #define ARM_r9 uregs[9] #define ARM_r8 uregs[8] #define ARM_r7 uregs[7] #define ARM_r6 uregs[6] #define ARM_r5 uregs[5] #define ARM_r4 uregs[4] #define ARM_r3 uregs[3] #define ARM_r2 uregs[2] #define ARM_r1 uregs[1] #define ARM_r0 uregs[0] // struct unwind_table { // struct list_head list; // const struct unwind_idx *start; // const struct unwind_idx *origin; // const struct unwind_idx *stop; // unsigned long begin_addr; // unsigned long end_addr; // }; // extern struct unwind_table *unwind_table_add(unsigned long start, // unsigned long size, // unsigned long text_addr, // unsigned long text_size); // extern void unwind_table_del(struct unwind_table *tab); extern void unwind_backtrace(struct pt_regs *regs); #endif /* !__ASSEMBLY__ */ #ifdef CONFIG_ARM_UNWIND #define UNWIND(code...) code #else #define UNWIND(code...) #endif #endif /* __ASM_UNWIND_H */
1 当内核某处陷入死循环,有时运行sysrq的内核线程栈回溯功能可以排查,但并不适用所用情况,笔者实际项目遇到过。最后是在系统定时钟中断函数,对死循环线程栈回溯20多级终于找到死循环的函数。
2 当应用程序段错误,内核捕捉到崩溃,对崩溃的应用空间进程/线程栈回溯,像内核栈回溯一样,打印应用段错误进程/线程的层层函数调用关系。虽然运用core文件分析或者gdb也很简便排查应用崩溃问题,但是对于不容易复现、测试部偶先的、客户现场偶先的,这二者就很难发挥作用。还有就是如果崩溃发生在C库中,CPU的pc和lr(arm架构)寄存器指向的函数指令在C库的用户空间,很难找到应用的代码哪里调用了C库的函数。arm架构网上能找到应用层栈回溯的例子,但是编译较麻烦,代码并不容易理解,况且mips能在应用层实现吗?还是在内核实现应用程序栈回溯比较方便。
3 应用程序发生double free,运用内核的栈回溯功能,找到应用代码哪里发生了double free。double free是C库层发现并截获该事件,然后向当前进程/线程发送SIGABRT进程终止信号,后续就是内核强制清理该进程/线程。double free比应用程序段错误更麻烦,后者内核还会打印出错进程/线程名字、pid、pc和lr寄存器值,double free这些打印全没有。笔者做过的一个项目,发布前,遇到一例double free崩溃问题,极难复现,当初要是把double free内核对出问题进程/线程栈回溯的功能做进内核,就能找到出问题的应用函数了。
4 当应用程序出现锁死问题,对应用所有线程栈回溯,分析每个线程的函数执行流程,对查找锁死问题有帮助。
2 栈回溯的原理解释
2.1 基于fp栈帧寄存器形式的栈回溯
2.2 unwind 形式的栈回溯
这个unwind段中存储着跟函数入栈相关的关键数据。当函数执行入栈指令后,在unwind段会保存跟入栈指令一一对应的编码数据,
当函数执行入栈指令后,在unwind段会保存跟入栈指令一一对应的编码数据,根据这些编码数据,就能计算出当前函数栈大小和cpu的哪些寄存器入栈了,在栈中什么位置。当栈回溯时,首先根据当前函数中的指令地址,就可以计算出函数unwind段的地址,然后从unwind段取出跟入栈有关的编码数据,根据这些编码数据就能计算出当前函数栈的大小以及入栈时lr寄存器数据在栈中的存储地址。这样就可以找到lr寄存器数据,就是当前函数返回地址,也就是上一级函数的指令地址。此时sp一般指向的函数栈顶,sp+函数栈大小就是上一级函数的栈顶。这样就完成了一次栈回溯,并且知道了上一级函数的指令地址和栈顶地址,按照同样的方法就能对上一级函数栈回溯,类推就能实现整个栈回溯流程。为了方便理解,下方举一个实际调试的示例。该示例中首先列出栈回溯过程每个函数unwind段的编码数据和栈数据。
读者想研究清楚的话,可以阅读内核arm架构unwind_frame函数实现流程,其中最核心的是在unwind_exec_insn函数,根据0xa8,0xb0这些跟函数入栈过程有关的编码数据,分析入栈过程的详细信息,计算出函数lr寄存器保存在栈中的地址和上一级函数的栈顶地址。
这就表示match_dev_by_uuid函数在unwind段编码数据是0x808ab0b0,0xc0008af8是该函数指令首地址。其中有用的是0xa8 ,表示pop {r4,r14}出栈指令,0xb0表示unwind段结束。
2.3 fp和unwind形式栈回溯的比较
上文介绍了两种常用的栈回溯形式的基本原理,并辅助了例子说明。基于fp寄存器的栈回溯和unwind形式的栈回溯,各有优点和缺点。fp形式的栈回溯,基于APCS规范,入栈过程必须要将pc、lr、fp等4个寄存器入栈(其实没必要这样做,只需把lr和fp入栈),并且消耗的入栈指令要多(除了入栈pc、lr、fp等4个寄存器,还得将栈底地址保存到fp),同时还浪费了寄存器,至少fp寄存器是浪费了,不能参与指令数据运算,CPU寄存器是很宝贵的,多一个对加快指令数据运算是有积极意义的。而unwind形式的栈回溯,就没有这些缺点,仅仅只是将入栈相关的指令的编码保存到unwind段中,不用把无关的寄存器保存到栈中,也不用浪费fp寄存器。unwind形式栈回溯是有缺点的,首先栈回溯的速度肯定比fp形式栈回溯慢,理解难度要比fp形式大很多,并且,站在开发者角度,使用前还得对每个入栈指令编码,这都是需要工作量的。但是站在使用者角度,这些缺点影响并不大,所以现在有很多arm32系统用的是unwind形式的栈回溯。
3 linux内核栈回溯的原理
当内核崩溃,将会执行异常处理程序,这里以mips架构为例,崩溃函数执行流程是:
do_page_fault()->die()->show_registers()->show_stacktrace()->show_backtrace()
栈回溯的过程就是在show_backtrace()函数,arm架构最终是在dump_backtrace()函数,内核崩溃处理流程与mips不同。arm架构栈回溯过程相对来说更简单,首先讲解arm架构的栈回溯过程。
不同内核版本,内核代码有差异,本内核版本3.10.104
3.1.1 内核源码分析
如果读者对上一节的演示理解的话,理解下方的源码就比较容易。
内核崩溃时,产生异常,内核的异常处理程序自动将崩溃时的CPU寄存器存入struct pt_regs结构体,并传入该函数,相关代码不再列出。这样栈回溯的关键环节就是红色标注的代码,先对frame.fp,frame.sp,frame.pc赋值。下方进入while循环,先执行unwind_frame(&frame) 找出崩溃过程的每个函数中的汇编指令地址,存入frame.pc(第一次while循环是直接where = frame.pc赋值,这就是当前崩溃函数的崩溃指令地址),下次循环存入where变量,再传入dump_backtrace_entry函数,在该函数中打印诸如[<c016c873>] chrdev_open+0x12/0x4B1 的字符串。
arch/arm64/kernel/stacktrace.c
4 linux内核栈回溯的应用
文章最开头说过,笔者在实际项目开发过程,已经总结出了3个内核栈回溯的应用:
1 应用程序崩溃,像内核栈回溯一样打印整个崩溃过程,应用函数的调用关系
2 应用程序发生double free,像内核栈回溯一样打印double free过程,应用函数的调用关系
3 内核陷入死循环,sysrq的内核线程栈回溯功能无法发挥作用时,在系统定时钟中断函数中对卡死线程栈回溯,找出卡死位置
下文逐一讲解。
4.1 应用程序崩溃栈回溯
笔者在研究过内核栈回溯功能后,不禁发问,为什么不能用同样的方法对应用程序的崩溃栈回溯呢?不管是内核空间,应用空间,程序的指令是一样的,无非是地址有差异,函数入栈出栈原理是一样的。栈回溯的入口,arm架构是获取崩溃线程/进程的pc、fp、lr寄存器值,mips架构是获取pc、ra、sp寄存器值,有了这些值就能按照各自的回溯规律,实现栈回溯。从理论上来说,完全是可以实现的。
4.1 .1 arm架构应用程序栈回溯的实现
当应用程序发生崩溃,与内核一样,系统自动将崩溃时所有的CPU寄存器存入struct pt_regs结构,一般崩溃入口函数是do_page_fault,又因为是应用程序崩溃,所以是__do_user_fault函数,这里直接分析__do_user_fault。
unwind_idx 和 eh_frame 5. rt-thread下的移植 将linux unwind.c unwind.h 关于内核参数解释的所有代码注释掉,跟tsk有关的也注释掉 rtconfig.py 编译参数加funwind-tables DEVICE = ' -Wall -mcpu=cortex-a9 -mfpu=vfpv3 -ftree-vectorize -mfloat-abi=softfp -ffunction-sections -funwind-tables' ld文件 增加unwind段 . = ALIGN(4);
.ARM.unwind_idx : {
__start_unwind_idx = .;
*(.ARM.exidx*)
__stop_unwind_idx = .;
}
.ARM.unwind_tab : {
__start_unwind_tab = .;
*(.ARM.extab*)
__stop_unwind_tab = .;
}
.ARM.exidx : {
__exidx_start = .;
*(.ARM.exidx* .gnu.linkonce.armexidx.*)
__exidx_end = .;
}
插入 trap.c 异常处理函数中
标签:__,unwind,idx,ctrl,Linux,unsigned,long,回溯,冬之焱 From: https://www.cnblogs.com/ycjstudy/p/18253670