ARM 架构 dump_stack 实现分析(2.0 调用时序)

本文深入解析Linux内核中堆栈跟踪过程,包括dump_stack、unwind_backtrace、dump_backtrace_entry等核心函数的作用,以及LR寄存器与堆栈保存的重要性。通过代码注释和函数流程图,详细解释了如何获取并打印函数调用栈,同时强调了配置KALLSYMS以查找函数地址对应函数名和偏移量的重要性。

看下具体调用时序:

void dump_stack(void)
{
	dump_backtrace(NULL, NULL);
}


 

dump_stack
    -->dump_backtrace
            -->unwind_backtrace 
                - ->dump_backtrace_entry
                     -->printk(%pS)
                         - ->kallsyms_lookup


 

void unwind_backtrace(struct pt_regs *regs, struct task_struct *tsk)
{
	struct stackframe frame;
        ......
        //得到 FP ,SP ,LR ,及 PC的值
        //其中真正用的是: FP及PC
	if (regs) {
		frame.fp = regs->ARM_fp;
		frame.sp = regs->ARM_sp;
		frame.lr = regs->ARM_lr;
		/* PC might be corrupted, use LR in that case. */
		frame.pc = kernel_text_address(regs->ARM_pc)
			 ? regs->ARM_pc : regs->ARM_lr;
	} 
	while (1) {
		int urc;
		unsigned long where = frame.pc;
                //设置上一帧的FP,PC,SP等值
		urc = unwind_frame(&frame);
		if (urc < 0)
			break;
                //打印出对应函数指针的函数名及偏移量
		dump_backtrace_entry(where, frame.pc, frame.sp - 4);
	}
}


 

// 堆栈的大小,FP指针不能超过此值
#define THREAD_SIZE		8192
int notrace unwind_frame(struct stackframe *frame)
{
	unsigned long high, low;
	unsigned long fp = frame->fp;

	//此时,堆栈地址是一步一步往高地址爬的
	low = frame->sp;
	high = ALIGN(low, THREAD_SIZE);

	//确保帧指针是合法的
	if (fp < (low + 12) || fp + 4 >= high)
		return -EINVAL;
         //从堆栈中取出之前备份的值,设置成上一帧的值
         // 为什么是 -12 -8 - 4
         //因为函数调用前大概会执行:
      // ldm  sp, {fp, sp, pc}   把当前fp, sp, pc的值保存到堆栈中
	frame->fp = *(unsigned long *)(fp - 12);
	frame->sp = *(unsigned long *)(fp - 8);
	frame->pc = *(unsigned long *)(fp - 4);

	return 0;
}



大家可以想想,为什么有了LR寄存器,还要保存pc值在堆栈中。
个人LR保存的是调用函数指令,下面的代码地址,不能反映当时真实的函数调用
配合下面这张函数调用堆栈图,可能会更清晰些:

当然编译内核的时候,要打开 CONFIG_KALLSYMS ,这样才能查找找函数地址对应的函数名及偏移量
void dump_backtrace_entry(unsigned long where, unsigned long from, unsigned long frame)
{
#ifdef CONFIG_KALLSYMS
        //记住%pS是关键  printk 与 普通printf最大的不同
        //惭愧啊,现在才知道此选项
	printk("symbol<%08lx>] (%pS) from [<%08lx>] (%pS)\n", where, (void *)where, from, (void *)from);
#else
	printk("Function entered at [<%08lx>] from [<%08lx>]\n", where, from);
#endif
	if (in_exception_text(where))
		dump_mem("", "Exception stack", frame + 4, frame + 4 + sizeof(struct pt_regs));
}


 

06-27 14:30:48.756 17487 17487 D UsbDeviceConnectionJNI: close 06-27 14:30:48.756 17487 17487 I MultiCameraClient: disconnect device name/pid/vid:/dev/bus/usb/005/097&8841&7119 06-27 14:30:48.757 17487 17487 D ActivityStackUtils: remove stack: CusCarmerActivity 06-27 14:30:48.757 17487 17487 I CusCarmerActivity22: onDestroy==end 06-27 14:30:48.761 443 516 W InputManager-JNI: Input channel object '8e49df8 com.example.simplecarmer/com.example.simplecarmer.CusCarmerActivity (client)' was disposed without first being removed with the input manager! 06-27 14:30:48.772 17487 17487 I CusCarmerActivity22: CusCarmerActivity stateCLOSED==null 06-27 14:30:48.773 23868 23898 D BufferPoolAccessor2.0: bufferpool2 0xb400007d2ad603d8 : 5(19200 size) total buffers - 4(15360 size) used buffers - 812541/812546 (recycle/alloc) - 2285/1625082 (fetch/transfer) 06-27 14:30:48.798 17487 26435 I RenderManager: camera render frame rate is 7 fps-->gl_render 06-27 14:30:48.804 376 23907 D BufferPoolAccessor2.0: bufferpool2 0xb4000070ae124038 : 4(96000 size) total buffers - 2(48000 size) used buffers - 812540/812544 (recycle/alloc) - 117/812542 (fetch/transfer) 06-27 14:30:48.809 17487 26436 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x780d80e690 in tid 26436 (camera-17510058), pid 17487 (le.simplecarmer) 06-27 14:30:48.930 26448 26448 I crash_dump64: obtaining output fd from tombstoned, type: kDebuggerdTombstoneProto 06-27 14:30:48.933 210 210 I tombstoned: received crash request for pid 26436 06-27 14:30:48.935 26448 26448 I crash_dump64: performing dump of process 17487 (target tid = 26436) 06-27 14:30:49.080 26448 26448 W unwind : Failed to initialize DEX file support: dlopen failed: library "libdexfile.so" not found 06-27 14:30:49.193 26448 26448 I crash_dump64: type=1400 audit(0.0:1451): avc: denied { open } for path="/dev/__properties__/u:object_r:hwservicemanager_prop:s0" dev="tmpfs" ino=11849 scontext=u:r:crash_dump:s0:c512,c768 tcontext=u:object_r:hwservicemanager_prop:s0 tclass=file permissive=1 app=com.example.simplecarmer 06-27 14:30:49.193 26448 26448 I crash_dump64: type=1400 audit(0.0:1452): avc: denied { getattr } for path="/dev/__properties__/u:object_r:hwservicemanager_prop:s0" dev="tmpfs" ino=11849 scontext=u:r:crash_dump:s0:c512,c768 tcontext=u:object_r:hwservicemanager_prop:s0 tclass=file permissive=1 app=com.example.simplecarmer 06-27 14:30:49.193 26448 26448 I crash_dump64: type=1400 audit(0.0:1453): avc: denied { map } for path="/dev/__properties__/u:object_r:hwservicemanager_prop:s0" dev="tmpfs" ino=11849 scontext=u:r:crash_dump:s0:c512,c768 tcontext=u:object_r:hwservicemanager_prop:s0 tclass=file permissive=1 app=com.example.simplecarmer 06-27 14:30:49.197 26448 26448 I crash_dump64: type=1400 audit(0.0:1454): avc: denied { open } for path="/dev/__properties__/u:object_r:vendor_default_prop:s0" dev="tmpfs" ino=11955 scontext=u:r:crash_dump:s0:c512,c768 tcontext=u:object_r:vendor_default_prop:s0 tclass=file permissive=1 app=com.example.simplecarmer 06-27 14:30:49.197 26448 26448 I crash_dump64: type=1400 audit(0.0:1455): avc: denied { getattr } for path="/dev/__properties__/u:object_r:vendor_default_prop:s0" dev="tmpfs" ino=11955 scontext=u:r:crash_dump:s0:c512,c768 tcontext=u:object_r:vendor_default_prop:s0 tclass=file permissive=1 app=com.example.simplecarmer 06-27 14:30:49.197 26448 26448 I crash_dump64: type=1400 audit(0.0:1456): avc: denied { map } for path="/dev/__properties__/u:object_r:vendor_default_prop:s0" dev="tmpfs" ino=11955 scontext=u:r:crash_dump:s0:c512,c768 tcontext=u:object_r:vendor_default_prop:s0 tclass=file permissive=1 app=com.example.simplecarmer 06-27 14:30:49.196 0 0 W audit : audit_lost=260 audit_rate_limit=5 audit_backlog_limit=64 06-27 14:30:49.196 0 0 E audit : rate limit exceeded 06-27 14:30:49.368 0 0 I logd : logdr: UID=10088 GID=10088 PID=26448 n tail=0 logMask=8 pid=17487 start=0ns deadline=0ns 06-27 14:30:49.376 0 0 I logd : logdr: UID=10088 GID=10088 PID=26448 n tail=0 logMask=1 pid=17487 start=0ns deadline=0ns
06-28
<think>我们讨论的是Linux内核栈溢出时如何利用dump_stack()分析函数调用链。根据引用[2]和引用[3],我们可以得到以下关键点: 1. **内核栈溢出检测机制**:当内核配置了`CONFIG_DEBUG_STACKOVERFLOW`时,内核会在任务栈的底部(或顶部,取决于架构)设置一个特殊的魔数(canary值)。如果这个值被改变,说明发生了栈溢出,此时内核会触发栈溢出处理。 2. **dump_stack()的作用**:当检测到栈溢出时,内核会调用`dump_stack()`函数,该函数会打印出当前的函数调用链(即堆栈回溯信息),帮助开发者定位问题。3.**如何启用**:需要在内核配置中启用以下选项:-`CONFIG_DEBUG_STACKOVERFLOW`:启用栈溢出检测。- `CONFIG_FRAME_POINTER`(引用[2]):启用帧指针,这样`dump_stack()`可以得到更准确的调用链。4.**分析步骤**:-在发生栈溢出时,内核会打印出调用栈信息(通过`dump_stack()`)。-开发者可以通过这些信息查看是从哪个函数开始,调用链是怎样的,从而定位导致栈溢出的函数。 5. **示例**:引用[3]虽然展示的是用户空间的栈溢出(在GDB中使用`bt`命令查看调用栈),但在内核空间类似,只是获取方式不同(通过内核日志)。 现在,我们具体说明如何在Linux内核栈溢出时利用`dump_stack()`分析函数调用链:###步骤1:配置内核以支持栈溢出检测和帧指针在编译内核前,确保以下配置被启用:```kconfigCONFIG_DEBUG_STACKOVERFLOW=yCONFIG_FRAME_POINTER=y```可以通过`makemenuconfig`或直接修改`.config`文件来设置:- `Kernel hacking` ->`DebugOops,Lockupsand Hangs`-> `[*]Detectstack overflows`(即`CONFIG_DEBUG_STACKOVERFLOW`) -`Kernel hacking` ->`Compile-timechecks andcompiler options` ->`[*] Compile thekernel withframe pointers`(即`CONFIG_FRAME_POINTER`)###步骤2:触发栈溢出并获取调用链当内核任务栈溢出时,内核会检测到魔数被破坏,然后调用`dump_stack()`。该函数会将当前任务的调用链打印到内核日志(如通过`printk`)。例如,日志中可能出现类似以下的信息: ```------------[ cuthere ]------------kernel stackoverflow in: funcA+0x20/0x40CPU:0 PID:123 Comm: test_program Tainted:GW5.10.0#1Call Trace: [<ffffffff81234567>] funcA+0x20/0x40[<ffffffff81234567>]funcB+0x30/0x50[<ffffffff81234567>]funcC+0x40/0x60...更多调用... ```###步骤3:分析调用链从调用链(Call Trace)中,我们可以从上到下(或从下到上,取决于架构)查看函数调用的顺序。最顶部的函数(在x86上通常是最后调用的函数)就是导致栈溢出的最近函数。我们需要检查这些函数中是否有大量的栈分配(如大数组、深度递归等)。 ###示例:模拟栈溢出并分析假设我们在内核模块中编写了一个递归函数导致栈溢出: ```c#include <linux/module.h> #include<linux/kernel.h>static voidrecursive_func(int depth) {charbuffer[1024];//每次递归分配1KB栈空间if (depth >0) {recursive_func(depth -1);} }static int__inittest_init(void){printk(KERN_INFO "Starting stackoverflow test\n");recursive_func(20); //递归20次,约20KB,超过默认内核栈大小(通常8KB或16KB)return0;} staticvoid __exit test_exit(void){printk(KERN_INFO "Module unloaded\n"); }module_init(test_init); module_exit(test_exit);MODULE_LICENSE("GPL");``` 当加载此模块时,会触发栈溢出,内核会检测到并打印调用链。我们通过`dmesg`查看内核日志,可以看到类似:``` [123.456789] Startingstack overflowtest[123.456790] kernelstack overflowin:recursive_func+0x20/0x30[test_module][123.456791] CPU:0 PID:123 Comm: insmod Tainted:GO5.10.0 #1[123.456792] CallTrace:[123.456793][<ffffffffc0001000>] recursive_func+0x20/0x30 [test_module] [123.456794][<ffffffffc0001020>] recursive_func+0x40/0x30 [test_module][123.456795][<ffffffffc0001020>] recursive_func+0x40/0x30 [test_module] ...重复多次 ...[123.456796][<ffffffffc0001020>]recursive_func+0x40/0x30[test_module][123.456797][<ffffffffc0001040>]test_init+0x20/0x1000 [test_module] [123.456798][<ffffffff810022a7>]do_one_initcall+0x57/0x1e0...其他内核初始化函数 ...``` 从调用链中,我们看到`recursive_func`被递归调用了多次,最终导致栈溢出。 ###注意事项1. **栈大小**:内核栈大小有限(通常8KB或16KB),因此应避免在栈上分配大数组或深度递归。 2. **调试符号**:为了调用链中的函数名可读,需要确保内核或模块编译时带有调试符号(`CONFIG_DEBUG_INFO`)。3.**实时性**:栈溢出可能导致系统不稳定,因此有时可能无法完整打印调用链。在这种情况下,可以结合`kdump`和`crash`工具分析vmcore(引用[1]提到的coredump机制在内核层面的应用)。###相关问题1.如何配置Linux内核以捕获栈溢出?2.内核栈大小是多少?如何调整?3.除了`dump_stack()`,还有哪些内核调试手段可以分析栈溢出?4.用户态程序栈溢出和内核栈溢出有何异同?5.如何在内核模块中安全地使用递归?通过以上步骤,我们可以利用`dump_stack()`在内核栈溢出时获取函数调用链,从而定位问题代码。</think>### Linux 内核栈溢出时利用 `dump_stack()` 分析函数调用链指南 #### 一、核心原理与配置 1. **栈溢出检测机制** - 内核通过 `CONFIG_DEBUG_STACKOVERFLOW` 启用栈溢出保护[^2] - 在栈底设置 "金丝雀值" (canary),当该值被覆盖时触发溢出检测 - 溢出时会强制切换到安全栈(Overflow Stack)执行诊断代码 2. **关键配置选项** 编译内核时需启用: ```kconfig CONFIG_DEBUG_STACKOVERFLOW=y # 启用栈溢出检测 CONFIG_FRAME_POINTER=y # 启用帧指针(确保完整调用链)[^2] ``` #### 二、`dump_stack()` 工作流程 当检测到栈溢出时,内核自动触发: ```mermaid graph LR A[栈溢出发生] --> B[检测金丝雀值损坏] B --> C[切换到 Overflow Stack] C --> D[调用 dump_stack()] D --> E[打印函数调用链到内核日志] E --> F[安全终止进程] ``` #### 三、实战分析步骤 1. **触发栈溢出** 示例代码(内核模块): ```c #include <linux/module.h> void recursive_func(int depth) { char buffer[1024]; // 大局部变量消耗栈空间 if (depth > 0) recursive_func(depth - 1); // 深度递归 } static int __init test_init(void) { recursive_func(20); // 触发溢出 return 0; } module_init(test_init); ``` 2. **获取调用链信息** 内核日志会输出类似内容: ``` [ 123.456789] Kernel stack overflow in process: test_module/pid:789 [ 123.456790] Call Trace: [ 123.456791] [<ffffffff81234567>] recursive_func+0x20/0x40 [ 123.456792] [<ffffffff81234589>] recursive_func+0x42/0x40 [ 123.456793] [<ffffffff81234589>] recursive_func+0x42/0x40 [ 123.456794] [<ffffffff810022a7>] test_init+0x57/0x80 [ 123.456795] [<ffffffff81001234>] do_one_initcall+0x44/0x1d0 ``` 3. **符号解析与定位** 使用 `addr2line` 工具解析地址: ```bash addr2line -e vmlinux 0x81234567 # 解析 recursive_func 位置 addr2line -e vmlinux 0x810022a7 # 解析 test_init 位置 ``` 输出示例: ``` /path/to/kernel/source/file.c:123 # 精确到源码行号 ``` #### 四、关键分析技巧 1. **调用链解读** - 从下往上读:最底部是初始调用点(`test_init`) - 从上往下读:最顶部是崩溃点(最近的递归调用) - 重复出现的函数:表明递归深度(如示例中的 `recursive_func`) 2. **栈消耗分析** 计算单次调用栈消耗: ```c sizeof(buffer) + 函数调用开销 ≈ 1024 + 32 = 1056 bytes ``` 总消耗 = 递归深度 × 单次消耗(示例中 20 × 1056 ≈ 20KB) 3. **结合 GDB 调试** 加载内核镜像: ```bash gdb vmlinux (gdb) list *recursive_func+0x20 # 查看对应汇编代码 (gdb) disas recursive_func # 反汇编函数 ``` #### 五、典型错误模式识别 | 模式 | 原因 | 解决方案 | |------|------|----------| | 重复同一函数 | 无限递归 | 添加终止条件 | | 栈分配大数组 | 局部变量过大 | 改用动态分配 | | 中断嵌套过深 | 中断处理耗时 | 优化中断处理 | #### 六、高级调试技巧 1. **栈跟踪增强** 启用 `CONFIG_STACKTRACE` 获取更详细信息: ```c dump_stack(); print_stack_trace(current->stack_trace, 0); // 增强版堆栈 ``` 2. **动态探测** 在可疑函数插入探测点: ```c #include <linux/stacktrace.h> void debug_func() { unsigned long entries[32]; struct stack_trace trace = { .entries = entries, .max_entries = ARRAY_SIZE(entries), }; save_stack_trace(&trace); print_stack_trace(&trace, 0); } ``` 3. **性能监控** 使用 `CONFIG_DEBUG_STACK_USAGE` 跟踪栈使用峰值: ```bash cat /proc/<pid>/stack # 查看实时栈使用 ``` #### 七、常见问题解决 1. **调用链不完整** - 确保启用 `CONFIG_FRAME_POINTER`[^2] - 检查编译器优化级别(-O0 调试更准确) 2. **符号解析失败** - 保存内核编译时的 `System.map` 文件 - 使用 `vmlinux-with-debuginfo` 镜像 3. **日志被冲刷** - 增大内核日志缓冲区: ```bash dmesg -n 8 # 提高日志级别 ``` - 使用 `klogd` 实时保存日志 > 案例:某驱动模块因 8KB 局部数组导致栈溢出,通过 `dump_stack()` 定位到 `ioctl` 处理函数中的数组声明行[^3]。 --- ### 相关问题 1. 如何配置内核使 `dump_stack()` 包含更多调试信息? 2. 用户态程序栈溢出(如 `__stack_chk_fail`)与内核态有何差异?[^4] 3. 如何在内核模块中主动触发 `dump_stack()` 进行调试? 4. ARM 架构与 x86 架构的栈回溯实现有何区别? 5. 如何通过 `perf` 工具监控内核栈使用情况?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值