首先,栈 (stack) 是一种串列形式的数据结构。这种数据结构的特点是后入先出 (LIFO, Last In First Out),数据只能在串列的一端 (称为:栈顶 top) 进行 推入 (push) 和 弹出 (pop) 操作。根据栈的特点,很容易的想到可以利用数组,来实现这种数据结构。但是本文要讨论的并不是软件层面的栈,而是硬件层面的栈。

进程在创建时,内核会为进程创建一系列的数据结构,其中最重要的就是进程描述task_struct结构。同时内核会为进程创建两个栈,一个是用户栈,一个是内核栈。分别处于用户态和内核态使用的栈。而本文的主线就是内核栈!

内核栈

在每个进程生命周期内,会通过系统调用或者中断进入内核。此时,这些内核代码使用的栈并不是原先用户空间的栈,而这个在内核空间的栈被称为进程内核栈

用户态切到内核态,内核将用户态时的堆栈寄存器的值保存在内核中,这样从内核栈中可以快速切换回用户态。

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
	/*
	 * For reasons of header soup (see current_thread_info()), this
	 * must be the first element of task_struct.
	 */
	struct thread_info		thread_info;
#endif
	unsigned int			__state;

	/* saved state for "spinlock sleepers" */
	/* moved to ANDROID_KABI_USE(1, unsigned int saved_state) */

	/*
	 * This begins the randomizable portion of task_struct. Only
	 * scheduling-critical items should be added above here.
	 */
	randomized_struct_fields_start

	void				*stack;
	refcount_t			usage;
	/* Per task flags (PF_*), defined further below: */
	unsigned int			flags;
    //...
}

此stack指针就是内核栈的指针

而在6.1内核中thread_info直接作为了结构体成员嵌入到task_struct 中,通CONFIG_THREAD_INFO_IN_TASK 启用(我司项目默认启用)

这里也需要标注一下thread_info 在6.1内核中也不再有stack指针了!可能网上的一些文章已经过时了(老内核)。

ARM64 内核通过 sp_el0 寄存器直接存储当前任务的 task_struct 指针(而非栈指针),因此不再需要通过栈指针反向查找 thread_infothread_info 作为 task_struct 的成员,其地址可通过 task_struct 直接计算,无需依赖栈指针

static __always_inline struct task_struct *get_current(void)
{
	unsigned long sp_el0;

	asm ("mrs %0, sp_el0" : "=r" (sp_el0));

	return (struct task_struct *)sp_el0;
}

pt_regs和cpu_context

  • pt_regs 位于内核栈的高端,主要用于在用户内核模式切换时保存用户寄存器。因此,返回用户空间后,执行的第一条指令位于 pt_regs->pc 处。

  • cpu_context 位于 task_struct->thread_struct 中,主要用于保存上下文切换的寄存器。因此,当上下文切换到一个进程后,它的 cpu_context->pc 就会被执行

pt_regs

  • 定义
    pt_regs 存储的是 用户态进程陷入内核态时的寄存器快照(如系统调用、中断、异常发生时)。

  • 用途

    • 保存用户态上下文,用于返回到用户态时恢复现场。

    • 作为系统调用参数传递的载体(如通过 x0-x7 传递参数)。

    • 核心路径:entry.S 中的异常向量表处理逻辑会填充 pt_regs

  • 关键字段(ARM64 示例):

    struct pt_regs {
        u64 regs[31];  // 通用寄存器 x0-x30
        u64 sp;        // 用户态栈指针
        u64 pc;        // 用户态程序计数器(触发异常的地址)
        u64 pstate;    // 处理器状态(如 PSTATE_DAIF 标志)
        u64 orig_x0;   // 原始 x0(用于 syscall 重启)
        // ... 其他架构特定字段
    };

cpu_context

  • 定义
    cpu_contextstruct thread_struct 的成员,保存 任务切换时需保留的内核态 CPU 寄存器(如任务调度时的上下文)。

  • 用途

    • 在任务切换(context_switch)时保存/恢复内核态执行流。

    • 仅包含任务切换必需的寄存器(如 x19-x30 等被调用者保存的寄存器)。

  • 关键字段(ARM64 示例):

    struct cpu_context {
        u64 x19, x20, x21, x22, x23, x24, x25, x26, x27, x28; // 被调用者保存寄存器
        u64 fp;     // 帧指针(x29)
        u64 lr;     // 链接寄存器(x30)
        u64 sp;     // 内核态栈指针
        u64 tpidr_el0; // 线程 ID 寄存器(用户态 TLS)
    };

核心区别

特性

pt_regs

cpu_context

触发场景

用户态陷入内核态(如 syscall、中断)

内核态任务切换(如进程调度)

保存的寄存器范围

全部用户态可见寄存器(x0-x30, sp, pc)

仅内核态需保存的寄存器(x19-x30, sp)

存储位置

内核栈顶(每个异常入口动态分配)

task_struct->thread.cpu_context

生命周期

临时(仅在异常处理期间有效)

持久(伴随任务的生命周期)

恢复时机

异常返回到用户态(eret

任务切换后恢复执行

关联与协作

(1) 任务调度时的协作
  • 当发生任务切换时:

    1. 保存当前任务上下文:将当前任务的 x19-x30sp 等保存到 task_struct->thread.cpu_context

    2. 恢复新任务上下文:从新任务的 cpu_context 加载寄存器,切换到新任务的内核栈。

    3. 返回到用户态(若新任务从用户态被切换出去):通过 pt_regs 恢复用户态寄存器(如 pcx0-x7)。

(2) 系统调用流程示例
  1. 用户态触发 syscall

    • CPU 自动保存 pcpstateelr_el1spsr_el1,跳转到内核异常向量。

  2. 构造 pt_regs

    • 内核在栈顶分配 pt_regs,保存用户态的 x0-x30sppcpstate 等。

  3. 执行系统调用

    • 使用 pt_regs->x0-x7 获取参数,处理完成后将返回值写入 pt_regs->x0

  4. 返回用户态

    • pt_regs 恢复用户态寄存器,通过 eret 指令返回到用户态 pc

(3) 中断上下文切换
  • 若中断发生时当前任务处于用户态:

    • 保存用户态上下文到 pt_regs,中断处理完毕后恢复。

  • 若中断发生时当前任务处于内核态:

    • 仅保存必要的内核态寄存器(如 cpu_context),无需完整 pt_regs

sp_el0里存的是什么?

通过get_current函数我们可以直接获得当前task_struct ,所以sp_el0里肯定存的是这个!那什么时候存储的呢?

//kernel_platform/common/arch/arm64/kernel/process.c

__notrace_funcgraph __sched
struct task_struct *__switch_to(struct task_struct *prev,
				struct task_struct *next)
{
	struct task_struct *last;

	fpsimd_thread_switch(next);
	tls_thread_switch(next);
	hw_breakpoint_thread_switch(next);
	contextidr_thread_switch(next);
	entry_task_switch(next);
	ssbs_thread_switch(next);
	erratum_1418040_thread_switch(next);
	ptrauth_thread_switch_user(next);

	/*
	 *  vendor hook is needed before the dsb(),
	 *  because MPAM is related to cache maintenance.
	 */
	trace_android_vh_mpam_set(prev, next);

	/*
	 * Complete any pending TLB or cache maintenance on this CPU in case
	 * the thread migrates to a different CPU.
	 * This full barrier is also required by the membarrier system
	 * call.
	 */
	dsb(ish);

	/*
	 * MTE thread switching must happen after the DSB above to ensure that
	 * any asynchronous tag check faults have been logged in the TFSR*_EL1
	 * registers.
	 */
	mte_thread_switch(next);
	/* avoid expensive SCTLR_EL1 accesses if no change */
	if (prev->thread.sctlr_user != next->thread.sctlr_user)
		update_sctlr_el1(next->thread.sctlr_user);

	trace_android_vh_is_fpsimd_save(prev, next);

	/* the actual thread switch */
	last = cpu_switch_to(prev, next); //进程切换

	return last;
}

上面是arm64进程切换时会执行的一个函__switch_to ,最后会调cpu_switch_to

// kernel_platform/common/arch/arm64/kernel/entry.S

SYM_FUNC_START(cpu_switch_to)
	mov	x10, #THREAD_CPU_CONTEXT
	add	x8, x0, x10
	mov	x9, sp
	stp	x19, x20, [x8], #16		// store callee-saved registers
	stp	x21, x22, [x8], #16
	stp	x23, x24, [x8], #16
	stp	x25, x26, [x8], #16
	stp	x27, x28, [x8], #16
	stp	x29, x9, [x8], #16
	str	lr, [x8]
	add	x8, x1, x10
	ldp	x19, x20, [x8], #16		// restore callee-saved registers
	ldp	x21, x22, [x8], #16
	ldp	x23, x24, [x8], #16
	ldp	x25, x26, [x8], #16
	ldp	x27, x28, [x8], #16
	ldp	x29, x9, [x8], #16
	ldr	lr, [x8]
	mov	sp, x9
	msr	sp_el0, x1
	ptrauth_keys_install_kernel x1, x8, x9, x10
	scs_save x0
	scs_load_current
	ret
SYM_FUNC_END(cpu_switch_to)
NOKPROBE(cpu_switch_to)

这段汇编的意思是将prev进程的x19到x29, x9, lr寄存器都存储在内核堆栈中,然后将next进程的x19-x29, x9, lr寄存器从堆栈中恢复。

我们关心msr sp_el0, x1。其中x1就是next进程的struct task_struct结构。则sp_el0存储的是当前进程的task_struct结构。

PS: 为啥x1是next指针?因为next指针是__switch_to函数的第二个参数,被保存在x1里

栈的配置

// kernel_platform/common/arch/arm64/include/asm/memory.h

#define MIN_THREAD_SHIFT	(14 + KASAN_THREAD_SHIFT)

/*
 * VMAP'd stacks are allocated at page granularity, so we must ensure that such
 * stacks are a multiple of page size.
 */
#if defined(CONFIG_VMAP_STACK) && (MIN_THREAD_SHIFT < PAGE_SHIFT)
#define THREAD_SHIFT		PAGE_SHIFT
#else
#define THREAD_SHIFT		MIN_THREAD_SHIFT
#endif

#if THREAD_SHIFT >= PAGE_SHIFT
#define THREAD_SIZE_ORDER	(THREAD_SHIFT - PAGE_SHIFT)
#endif

#define THREAD_SIZE		(UL(1) << THREAD_SHIFT)

从上面的定义我们可以获知:

  1. MIN_THREAD_SHIFT 在没有打开Kasan的情况下为14

  2. THREAD_SIZE_ORDER 是衡量栈大小占用几个页,值THREAD_SHIFT-PAGE_SHIFT

  3. THREAD_SIZE 为 1<<THREAD_SHIFT,即16K,占用4个4k的page

  4. IRQ_STACK_SIZE 同样为16K

通过task_struct找到stack

static __always_inline void *task_stack_page(const struct task_struct *task)
{
	return task->stack;
}

通过task_struct找到pt_regs


#define task_pt_regs(p) \
	((struct pt_regs *)(THREAD_SIZE + task_stack_page(p)) - 1)

所以我们可以通过task_struct,就可以轻松得到内核栈和内核寄存器

内核栈的作用

栈作用可以从两个方面体现:函数调用多任务支持

函数调用

我们知道一个函数调用有以下三个基本过程:

  • 调用参数的传入

  • 局部变量的空间管理

  • 函数返回

函数的调用必须是高效的,而数据存放在 CPU通用寄存器 或者 RAM 内存 中无疑是最好的选择。以传递调用参数为例,我们可以选择使用 CPU通用寄存器 来存放参数。但是通用寄存器的数目都是有限的,当出现函数嵌套调用时,子函数再次使用原有的通用寄存器必然会导致冲突。因此如果想用它来传递参数,那在调用子函数前,就必须先 保存原有寄存器的值,然后当子函数退出的时候再 恢复原有寄存器的值 。

函数的调用参数数目一般都相对少,因此通用寄存器是可以满足一定需求的。但是局部变量的数目和占用空间都是比较大的,再依赖有限的通用寄存器未免强人所难,因此我们可以采用某些 RAM 内存区域来存储局部变量。但是存储在哪里合适?既不能让函数嵌套调用的时候有冲突,又要注重效率。

这种情况下,栈无疑提供很好的解决办法。

  1. 对于通用寄存器传参的冲突,我们可以再调用子函数前,将通用寄存器临时压入栈中;在子函数调用完毕后,在将已保存的寄存器再弹出恢复回来。

  2. 局部变量的空间申请,也只需要向下移动下栈顶指针;将栈顶指针向回移动,即可就可完成局部变量的空间释放

  3. 对于函数的返回,也只需要在调用子函数前,将返回地址压入栈中,待子函数调用结束后,将函数返回地址弹出给 PC 指针,即完成了函数调用的返回

上述函数调用的三个基本过程,就演变记录一个栈指针的过程。每次函数调用的时候,都配套一个栈指针。即使循环嵌套调用函数,只要对应函数栈指针是不同的,也不会出现冲突。

函数栈帧 (Stack Frame) 函数调用经常是嵌套的,在同一时刻,栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等

多任务支持

栈的意义还不只是函数调用,有了它的存在,才能构建出操作系统的多任务模式。我们以 main 函数调用为例,main 函数包含一个无限循环体,循环体中先调用 A 函数,再调用 B 函数。

void B()
{
    return;
}

void A()
{
    B();
}

void main()
{
    while(1)
    {
        A();
    }
}

试想在单处理器情况下,程序将永远停留在此 main 函数中。即使有另外一个任务在等待状态,程序是没法从此 main 函数里面跳转到另一个任务。因为如果是函数调用关系,本质上还是属于 main 函数的任务中,不能算多任务切换。此刻的 main 函数任务本身其实和它的栈绑定在了一起,无论如何嵌套调用函数,栈指针都在本栈范围内移动。

由此可以看出一个任务可以利用以下信息来表征:

  1. main 函数体代码

  2. main 函数栈指针

  3. 当前 CPU 寄存器信息

假如我们可以保存以上信息,则完全可以强制让出 CPU 去处理其他任务。只要将来想继续执行此 main 任务的时候,把上面的信息恢复回去即可。有了这样的先决条件,多任务就有了存在的基础,也可以看出栈存在的另一个意义。在多任务模式下,当调度程序认为有必要进行任务切换的话,只需保存任务的信息(即上面说的三个内容)。恢复另一个任务的状态,然后跳转到上次运行的位置,就可以恢复运行了。

可见每个任务都有自己的栈空间,正是有了独立的栈空间,为了代码重用,不同的任务甚至可以混用任务的函数体本身,例如可以一个main函数有两个任务实例。至此之后的操作系统的框架也形成了,譬如任务在调用 sleep() 等待的时候,可以主动让出 CPU 给别的任务使用,或者分时操作系统任务在时间片用完是也会被迫的让出 CPU。不论是哪种方法,只要想办法切换任务的上下文空间,切换栈即可。

栈回溯

在稳定性死机问题分析中,栈回溯是一个经常会遇到的问题!一般情况下工具会自动把栈回溯好,但是在一些情况下,我们需要手动回溯栈!

这里已一个案例作为演示:

[103516.808566][T10675] ------------[ cut here ]------------
[103516.808579][T10675] kernel BUG at include/linux/swapops.h:548!
[103516.808597][T10675] Internal error: Oops - BUG: 00000000f2000800 [#1] PREEMPT SMP
[103516.808693][T10675] Dumping ftrace buffer:
[103516.808710][T10675]    (ftrace buffer empty)

[103516.809484][T10675] Hardware name: Qualcomm Technologies, Inc. Spring QRD (DT)
[103516.809488][T10675] pstate: 60400005 (nZCv daif +PAN -UAO -TCO -DIT -SSBS BTYPE=--)
[103516.809494][T10675] pc : unmap_page_range+0x8dc/0x8ec
[103516.809510][T10675] lr : unmap_page_range+0x5a8/0x8ec
[103516.809515][T10675] sp : ffffffc0427f38d0
[103516.809518][T10675] x29: ffffffc0427f3950 x28: fffffffe09cd5880 x27: ffffff81b13a5dc0
[103516.809527][T10675] x26: ffffffc0427f3938 x25: 00000071a66fb000 x24: ffffff80d9d547d8
[103516.809535][T10675] x23: 00000071a67cd000 x22: ffffff8093052e10 x21: 0000000017062de0
[103516.809542][T10675] x20: ffffffc0427f39f8 x19: ffffffc0427f3ad8 x18: ffffffc01f247058
[103516.809550][T10675] x17: ffffff8159c53400 x16: ffffff8159c53418 x15: 0000000000000001
[103516.809557][T10675] x14: ffffffc0095b0d70 x13: 0000000000000002 x12: fffffffe00000000
[103516.809564][T10675] x11: 000000000000013e x10: fffffffe03c18b08 x9 : 4000000000000034
[103516.809571][T10675] x8 : fffffffe03c18b40 x7 : 0000000000000001 x6 : fffffffdef7defe8
[103516.809579][T10675] x5 : 000000000000748e x4 : ffffffc0427f39f8 x3 : 00000071a67cd000
[103516.809586][T10675] x2 : 0000000000001000 x1 : fffffffe09cd5880 x0 : 0000000000000000
[103516.809593][T10675] Call trace:
[103516.809597][T10675]  unmap_page_range+0x8dc/0x8ec
[103516.809604][T10675]  unmap_vmas+0xf4/0x164
[103516.809609][T10675]  exit_mmap+0x104/0x4c8
[103516.809616][T10675]  __mmput+0x38/0x150
[103516.809624][T10675]  mmput+0x38/0xe4
[103516.809629][T10675]  do_exit+0x260/0xa98
[103516.809635][T10675]  do_group_exit+0x70/0x9c
[103516.809640][T10675]  get_signal+0x75c/0x874
[103516.809645][T10675]  do_notify_resume+0x114/0x24b8
[103516.809653][T10675]  el0_svc+0x64/0x90
[103516.809662][T10675]  el0t_64_sync_handler+0x68/0xb4
[103516.809666][T10675]  el0t_64_sync+0x1a4/0x1a8

死在unmap_page_range+0x8dc/0x8ec

-> 使用bt指令获取backtrace

crash> bt
PID: 10675    TASK: ffffff81b13a5dc0  CPU: 2    COMMAND: "binder:29838_8"
bt: WARNING: cannot determine starting stack frame for task ffffff81b13a5dc0

无法显示(这是因为在cpu上running的任务的调用栈)

但是已知

x29 0xffffffc0427f33b0 ---> FP

x30 0xffffffc008fb9698 ---> LR

pc 0xffffffc008fb9678 ---> PC

  1. 根据当前FP里存的是上一栈帧的FP,当前FP+8存的是上一个栈帧的LR

    crash> rd -x 0xffffffc0427f33b0 0x40
    ffffffc0427f33b0:  ffffffc0427f33e0 ffffffc008fb9754
    ffffffc0427f33c0:  00000000000006ea 00000000000005c4
    ffffffc0427f33d0:  00000000000004ec ffffffffffffd8f1
    ffffffc0427f33e0:  ffffffc0427f3460 ffffffc001343890
  2. 可以得到上一个栈帧FP: ffffffc0427f33e0 ,上一个栈帧LR: ffffffc008fb9754

    dis -l ffffffc008fb97
    crash> dis -l ffffffc008fb9754
    out/android14-6.1/common/arch/arm64/lib/delay.c: 56
    0xffffffc008fb9754 <__const_udelay+40>: ldp     x29, x30, [sp], #16
  3. 继续朝前推导栈帧

    crash> rd -x  ffffffc0427f33e0 0x20
    ffffffc0427f33e0:  ffffffc0427f3460 ffffffc001343890

    可以得到栈帧FP: ffffffc0427f3460 LR: ffffffc001343890

    crash> dis -l ffffffc001343890
    0xffffffc001343890 <qcom_wdt_trigger_bite+1060>:        adds    x19, x19, #0x1
  1. 继续朝前推导

    crash> rd -x ffffffc0427f3460 0x2
    ffffffc0427f3460:  ffffffc0427f34c0 ffffffc0013442c0

    可以得到栈帧FP: ffffffc0427f34c0 LR: ffffffc0013442c0

    crash> dis -l ffffffc0013442c0
    0xffffffc0013442c0 <restart_wdog_handler+52>:   b       0xffffffc0013442a0 <restart_wdog_handler+20>
  2. 前一个栈帧

    crash> rd -x ffffffc0427f34c0 0x2
    ffffffc0427f34c0:  ffffffc0427f34d0 ffffffc0080e3118

    可以得到栈帧FP: ffffffc0427f34d0 LR: ffffffc0080e3118

    crash> dis -l ffffffc0080e3118
    out/android14-6.1/common/kernel/notifier.c: 87
    0xffffffc0080e3118 <atomic_notifier_call_chain+104>:    mov     w21, w0
  3. crash> rd -x ffffffc0427f34d0 0x2
    ffffffc0427f34d0:  ffffffc0427f3510 ffffffc0080e5604
    
    crash> dis -l ffffffc0080e5604
    out/android14-6.1/common/kernel/reboot.c: 230
    0xffffffc0080e5604 <do_kernel_restart+36>:      ldp     x29, x30, [sp], #16

可以得到栈帧FP: ffffffc0427f3510 LR: ffffffc0080e3118

  1. crash> rd -x ffffffc0427f3510 0x2
    ffffffc0427f3510:  ffffffc0427f3520 ffffffc0080198dc
    
    crash> dis -l ffffffc0080198dc
    out/android14-6.1/common/arch/arm64/kernel/process.c: 147
    0xffffffc0080198dc <machine_restart+72>:        adrp    x0, 0xffffffc0094df000

可以得到栈帧FP: ffffffc0427f3520 LR: ffffffc0080198dc

  1. crash> rd -x ffffffc0427f3520 0x2
    ffffffc0427f3520:  ffffffc0427f3540 ffffffc0080e5404
    
    crash> dis -l ffffffc0080e5404
    out/android14-6.1/common/kernel/reboot.c: 82
    0xffffffc0080e5404 <emergency_restart+40>:      ldp     x29, x30, [sp], #16

可以得到栈帧FP: ffffffc0427f3540 LR: ffffffc0080e5404

  1. crash> rd -x ffffffc0427f3540 0x2
    ffffffc0427f3540:  ffffffc0427f35d0 ffffffc008fe755c
    
    crash> dis -l ffffffc008fe755c
    out/android14-6.1/common/kernel/panic.c: 440
    0xffffffc008fe755c <panic+692>: adrp    x0, 0xffffffc00959a000

可以得到栈帧FP: ffffffc0427f35d0 LR: ffffffc008fe755c

  1. crash> rd -x  ffffffc0427f35d0 0x2
    ffffffc0427f35d0:  ffffffc0427f3670 ffffffc008023114
    
    crash> dis -l ffffffc008023114
    out/android14-6.1/common/arch/arm64/kernel/traps.c: 240
    0xffffffc008023114 <die+656>:   mov     w0, #0xb                        // #11

可以得到栈帧FP: ffffffc0427f3670 LR: ffffffc008023114

  1. crash> rd -x  ffffffc0427f3670 0x2
    ffffffc0427f3670:  ffffffc0427f36b0 ffffffc00802451c
    
    crash> dis -l ffffffc00802451c
    out/android14-6.1/common/arch/arm64/kernel/traps.c: 363
    0xffffffc00802451c <bug_handler+72>:    ldp     x9, x8, [x19, #256]

可以得到栈帧FP: ffffffc0427f36b0 LR: ffffffc00802451c

  1. crash> rd -x ffffffc0427f36b0 0x2
    ffffffc0427f36b0:  ffffffc0427f36d0 ffffffc0080166c0
    
    crash> dis -l  ffffffc0080166c0
    out/android14-6.1/common/arch/arm64/kernel/debug-monitors.c: 332
    0xffffffc0080166c0 <brk_handler+148>:   cbz     w0, 0xffffffc00801670c <brk_handler+224>

可以得到栈帧FP: ffffffc0427f36d0 LR: ffffffc0080166c0

  1. crash> rd -x ffffffc0427f36d0 0x2
    ffffffc0427f36d0:  ffffffc0427f3700 ffffffc00803be60
    
    crash> dis -l ffffffc00803be60
    out/android14-6.1/common/arch/arm64/mm/fault.c: 954
    0xffffffc00803be60 <do_debug_exception+164>:    cbz     w0, 0xffffffc00803be84 <do_debug_exception+200>

可以得到栈帧FP: ffffffc0427f3700 LR: ffffffc00803be60

  1. crash> rd -x ffffffc0427f3700 0x2
    ffffffc0427f3700:  ffffffc0427f3740 ffffffc008fee644
    
    crash> dis -l ffffffc008fee644
    out/android14-6.1/common/arch/arm64/kernel/entry-common.c: 411
    0xffffffc008fee644 <el1_dbg+88>:        bl      0xffffffc008fef88c <arm64_exit_el1_dbg>

可以得到栈帧FP: ffffffc0427f3740 LR: ffffffc008fee644

  1. crash> rd -x ffffffc0427f3740 0x2
    ffffffc0427f3740:  ffffffc0427f3770 ffffffc008fee448
    
    crash> dis -l ffffffc008fee448
    out/android14-6.1/common/arch/arm64/kernel/entry-common.c: 450
    0xffffffc008fee448 <el1h_64_sync_handler+60>:   b       0xffffffc008fee484 <el1h_64_sync_handler+120>

可以得到栈帧FP: ffffffc0427f3770 LR: ffffffc008fee448

  1. crash> rd -x ffffffc0427f3770 0x2
    ffffffc0427f3770:  ffffffc0427f38b0 ffffffc008011298
    
    crash> dis -l ffffffc008011298
    out/android14-6.1/common/arch/arm64/kernel/entry.S: 580
    0xffffffc008011298 <el1h_64_sync+104>:  b       0xffffffc008012130 <ret_to_kernel>

可以得到栈帧FP: ffffffc0427f38b0 LR: ffffffc008011298

  1. crash> rd -x ffffffc0427f38b0 0x2
    ffffffc0427f38b0:  ffffffc0427f3950 ffffffc00830a454
    
    crash> dis -l  ffffffc00830a454
    out/android14-6.1/common/mm/memory.c: 1670
    0xffffffc00830a454 <unmap_page_range+2268>:     brk     #0x800

可以得到栈帧FP: ffffffc0427f3950 LR: ffffffc00830a454

到了这儿我们起始就已经推导到dmesg中显示的死机的地方:unmap_page_range+0x8dc/0x8ec

0x8dc == 2268

证明我们推导的是正确的!

  1. 再朝前推导一次

crash> rd -x ffffffc0427f3950 0x2
ffffffc0427f3950:  ffffffc0427f3a40 ffffffc00830a558

crash> dis -l ffffffc00830a558
out/android14-6.1/common/mm/memory.c: 1755
0xffffffc00830a558 <unmap_vmas+244>:    add     x0, sp, #0x8

可以得到栈帧FP: ffffffc0427f3a40 LR: ffffffc00830a558

此时处于unmap_vmas+0xf4处,0xf4 == 244

unmap_vmas+0xf4/0x164

关于栈回溯部分,详细可以查看OPPO内核工匠出的一篇文章,本站也转载了:crash实战:手把手教你使用crash分析内核dump

参考资料

crash实战:手把手教你使用crash分析内核dump

https://wenboshen.org/posts/2015-12-18-kernel-stack

https://blog.csdn.net/u012489236/article/details/116614606

https://cloud.tencent.com/developer/article/2168474