由于内核栈空间非常有限通常只有 8KB 或 16 KB它保存的内容必须精简且至高无上。如果把内核栈比作进程在内核态工作时的“随身笔记本”那么这个笔记本的每一页都记录着进程在处理系统调用或中断时的关键信息。内核栈包含哪些东西 内核栈保存内容的“三层结构”我们可以按照从**栈底高地址到栈顶低地址**的顺序把保存的内容分为三个部分1. 寄存器快照pt_regs 结构体这是进程从用户态刚刚进入内核态时由硬件和内核代码第一时间保存下来的“第一现场”。用户态寄存器包括 RAX, RBX, RCX, RDX, RSI, RDI, RBP 以及 R8-R15。用户栈指针RSP (用户态的栈顶在哪)。指令指针RIP (用户态程序运行到哪一行被中断了)。状态标志EFLAGS。段寄存器CS, SS 等。作用当内核处理完工作准备回到用户态时就靠这些数据来“还原”程序让程序感觉自己从未被中断过。2. 函数调用链路内核函数栈帧 (Stack Frames)一旦进入内核开始执行内核函数如 sys_read - vfs_read - ext4_read每进入一个函数栈就会向下生长局部变量你在内核函数里定义的变量如 int i;, char temp[64];。函数返回地址当前函数执行完后应该跳回到上一层函数的哪一行。函数参数如果参数太多超过 6 个多余的参数会通过栈传递。作用支撑内核代码的正常运行和层级调用。3. 线程元数据thread_info (取决于架构)在较旧的 Linux 内核版本中内核栈的最底部地址最低处存放着一个非常关键的结构体 thread_info。包含内容当前进程的 task_struct 指针、CPU 标志位、抢占计数器等。现代内核的变化为了防止栈溢出破坏这个结构现在的 x86 架构通常将 thread_info 移入了 task_struct 中或者使用 per-cpu 变量存储栈底变得更加纯粹。内存站布局图示----------------------- --- 栈起始位置 (高地址 / Stack Base)| SS / RSP | || RFLAGS / CS | | 这部分是 pt_regs| RIP / ERR | | 记录进入内核瞬间的 CPU 状态| 通用寄存器 (RAX...) | |-----------------------| 函数 A 的局部变量 | || 函数 A 的返回地址 | | 内核函数运行时产生的| 函数 B 的局部变量 | | 不断向下生长的栈帧 (Frames)| ... | |----------------------- --- 当前 RSP 指针 (低地址 / Stack Top)| || 剩余空闲空间 || |-----------------------| [thread_info 结构] | --- 栈底 (有些架构在此存放元数据)-----------------------问为什么要搞“一程一栈”为什么不能所有进程共用一个内核栈或者让进程直接用用户栈隔离性Security如果内核在用户栈里运行恶意程序可以修改栈上的返回地址等内核函数一结束就跳到黑客的代码里去了。并发性Concurrency如果多个进程同时陷入内核比如多核 CPU它们必须有各自独立的栈来保存自己的执行状态否则数据就全乱了。可靠性Reliability内核栈大小固定如 8KB这强制要求内核开发者不能写递归太深的代码保证了内核的稳定性。在多核 CPU 的现代 Linux 中如果一个硬件中断发生了它一定会用当前进程的内核栈吗答案是不一定。为了减小进程内核栈的压力x86 架构现在通常会有独立的“硬中断栈” (Hard-IRQ Stack)。当硬中断发生时CPU 会切换到这个每 CPU 专用的独立栈上运行。只有系统调用软中断才会雷打不动地占用进程自己的内核栈。为什么不能存大对象了解了内核栈存什么你就能明白为什么内核编程有两条铁律严禁定义大数组如果你在内核函数里写char buffer[4096];直接就占掉了一半甚至全部的栈空间。一旦后续有中断嵌套ESP栈指针就会越界踩坏其他内存导致系统瞬间崩溃Panic。严禁深递归每递归一层就要压入一个返回地址和一组局部变量。在只有 8KB 的空间里稍微深一点的递归就会导致“栈溢出”。内核栈一般是多大为什么不大一些在 Linux 内核开发中内核栈Kernel Stack的大小是一个经过精确权衡的“生存空间”。 内核栈一般有多大在现代主流的 Linux 系统中如 x86_64 架构内核栈的大小通常是16 KB(4 个连续的物理页即 $2^2 \times 4\text{ KB}$)而在较早的 32 位系统或某些嵌入式架构如 ARM上常见的是8 KB($2^1 \times 4\text{ KB}$)。内核开发者极其吝啬理由如下1. 内存碎片的压力 (Physical Continuity)内核栈要求在物理内存上必须是连续的由伙伴系统分配。如果每个进程的内核栈很大比如 64KB当系统运行成千上万个进程时内核很难在物理内存中找到那么多连续的大块空闲区域。8KB/16KB是一个甜点位既能满足大多数需求又容易从伙伴系统中申请到。2. 内存总量的消耗Linux 支持成千上万个并发进程/线程。假设有 10,000 个线程每个内核栈 16KB总共消耗约160 MB的常驻物理内存。如果增加到 1MB则需要10 GB内存。对于嵌入式设备或高并发服务器来说这简直是灾难。3. 强制代码质量强制“瘦身”这是一个工程上的“负反馈”机制。内核栈小意味着开发者不能在内核里定义巨大的局部变量不能写多层递归。这迫使内核开发者必须使用kmalloc申请堆空间或者优化算法。正如 Linux 开发者常说的“如果你在内核里需要超过 8KB 的栈那一定是你的代码写得烂。”如何防范内核栈溢出影子栈与溢出检测由于空间太小栈溢出Stack Overflow 是内核崩溃的头号杀手。内核引入了以下机制来保护VMAP_STACK (虚拟映射栈)现代内核4.9 版本后默认开启。它使用 vmalloc 区域来分配栈。它的天才之处在于在栈的末尾设置了一个死区Guard Page。一旦 ESP 指针越界踩到这个死区MMU 会立刻触发一个无法恢复的异常内核直接 Panic。这比让 ESP 悄悄踩坏邻居的数据导致难以排查的 Bug要安全得多。中断的“搭便车”问题在早期的 Linux 中硬中断处理程序会直接“借用”当前进程的内核栈。如果此时进程已经用掉了 7KB来个中断再用 2KB栈就爆了。现在的解决办法现在的内核为每个 CPU 准备了独立的硬中断栈 (Hardirq Stack)。中断发生时CPU 会从进程栈跳到这个专用栈从而释放了进程内核栈的压力。PS每个进程在内核中都有一个独立的内核栈它不在用户态的地址空间里而是存在于内核管理的内存区域中。在 Linux 中内核栈并不像用户栈那样通过 malloc 或 mmap 分配它是伴随着进程描述符 task_struct 一起创建的。物理位置位于内核页表的直接映射区低端内存。逻辑位置在早期的 Linux 内核中内核栈和 thread_info 结构体紧挨着共同占用一个或两个物理页通常是 8KB 或 16KB。现代内核变化在 x86_64 架构的现代内核中为了安全防止溢出攻击内核栈已经通过 vmap 分配在虚拟连续但物理离散的内存区域中且不再与 thread_info 紧挨着。历史布局x86 32位常用过去内核分配 8KB2个页的空间。栈顶向下增长而 thread_info 结构体位于这个块的最底部。栈顶 (High Address)执行内核代码时的起始 SP堆栈指针。栈底 (Low Address)存放 thread_info它包含一个指向 task_struct 的指针。巧妙的设计当时内核通过屏蔽掉 SP 的低位例如 sp ~(8192-1)就能快速定位到 thread_info进而找到当前运行进程的 task_struct。这就是 current 宏的由来。现代布局x86_64 / VMAP_STACK现在的内核出于稳定性考虑默认开启了 CONFIG_VMAP_STACK内核栈拥有独立的虚拟内存区域。栈的末尾通常有一个“空洞”Guard Page如果内核栈溢出会立即触发页错误Page Fault而不是悄悄修改掉隔壁进程的数据。