内核栈

在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先用户空间中的栈,而是一个内核空间的栈,这个称作进程的“内核栈”。

内核栈主要用于进程陷入内核时使用的栈,主要涉及进程切换时保存用户态进程信息(寄存器值,一部分硬件上下文等),以及进程在内核执行时分配空间使用。

进程是动态实体,其生命周期范围很大。因此内核必须能够同时处理很多进程,并把进程描述符放在动态内存中,而不是放在永久分配给内核的内存区。

So,对每个进程,Linux都把两个不同的数据结构紧凑的存放在一个单独为进程分配的存储区域内,如下:

  1. 线程描述符:thread_info结构,与进程描述符相关,见下图。
  2. 内核栈:内核态的进程堆栈,见下图。

值得注意的是,这块存储区域通常为8192字节(两个页框),并且这8K空间占据连续的两个页框。内核控制路径使用很少的栈,因此内核栈比较小。
这里写图片描述
图中的数据结构均是存放在内核地址空间中,task_struct为进程描述符,也是存放在内核地址空间中,是内核对进程进行管理所使用的数据结构,其中包含了进程几乎所有信息。

内核栈的结构比较精巧,内核使用一个联合体定义内核栈:

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[2048];
};

其中thread_info中存放了进程/线程(内核不大区分进程与线程)的一些数据,其中包括指向task_struct结构的指针。数组stack即内核栈,stack占据8K/4K(依配置不同)空间,是这个联合体的主要部分。

thread_info结构从0x015fa000地址开始往上存放,内核栈从0x015fc000地址往下存放。因此内核很容易从esp寄存器的值获得当前在CPU上正在运行进程的thread_info结构的地址。事实上,如果thread_union大小为8K,则内核屏蔽掉esp的低13位就可以获得thread_info结构的基地址。

这样,一个实际的内核栈的结构将如下图所示。由于栈总是由高地址向低地址延伸的,所以栈底位于thread_union联合体的最末端,而thread_info结构则位于thread_union联合体的开始处,而且所占用的空间比较少。只要不出现内核栈特别大的极端情况,栈与thread_info可以互不干扰。该图为内核地址空间示意图。
这里写图片描述

用户栈

用户栈就是应用程序直接使用的栈。如下图所示,它位于应用程序的用户进程空间的最顶端。

当用户程序逐级调用函数时,用户栈从高地址向低地址方向扩展,每次增加一个栈帧,一个栈帧中存放的是函数的参数、返回地址和局部变量等,所以栈帧的长度是不定的。

用户栈的栈底靠近进程空间的上边缘,但一般不会刚好对齐到边缘,出于安全考虑,会在栈底与进程上边缘之间插入一段随机大小的隔离区。这样,程序在每次运行时,栈的位置都不同,这样黑客就不大容易利用基于栈的安全漏洞来实施攻击。

用户栈的伸缩对于应用程序来说是透明的,应用程序不需要自己去管理栈,这是操作系统提供的功能。应用程序在刚刚启动的时候(由fork()系统调用复制出新的进程),新的进程其实并不占有任何栈的空间。当应用程序中调用了函数需要压栈时,会触发一个page fault,内核在处理这个异常里会发现进程需要新的栈空间,于是建立新的VMA并映射内存给用户栈。
这里写图片描述

Logo

更多推荐