内核中堆栈溢出检测之VMAP_STACK特性

在Linux-4.14之前,Linux内核栈都是位于线性映射区,该区域对应的虚拟地址和物理地址具有一个固定的偏移,并且是在系统刚启动是进行过pre-mapped,因此内核在使用时不需要另外做页表映射。

在2016年的时候内核引入了vmap_stack机制,它是采用vmalloc申请的内存作为内核栈的一种机制。只需要使能 CONFIG_VMAP_STACK 配置选项即可打开该功能。这个功能带来了如下一些优势:

1.利用vmalloc的guard page增强了栈溢出检测能力
2.减少了内存碎片化

当然除了有这些优势,同时也对系统中带来了一些兼容性的问题,不过这些都是可以解决,比如:
使能了CONFIG_VMAP_STACK,那么将使用vmalloc申请的内存作为内核栈,这些内存在物理上可能是不连续的,这就会对一些驱动的DMA操作造成影响,因为一些DMA设备要求数据传输时的物理地址是连续的,如果申请了栈上的内存作为传输数据的缓冲区,那么就会遇到问题。

栈溢出检测

栈溢出检测功能,我们可以利用guard page来判断是否发生了栈溢出错误,但是对于传统的内核栈,guard page就会占用更多的物理内存,而对于vmap_stack则不然,因为guard page对应的虚拟地址可以不做任何映射,这样可以利用MMU的特性来检测栈溢出,也能够节省更多的物理内存。vmap_stack特性不用再特意实现guard page,因为vmalloc本身就自带这种guard page溢出检测功能,vmap_stack利用vmalloc申请内存因此也就带有了该功能。

反碎片

对于内核栈是每个进程都会有各自独立的内核栈,当系统中不断创建和销毁进程时,如果内核栈存在于线性映射区,那么内存也就是越来越趋于碎片化。而使用了vmalloc申请内存作为内核栈,则可以在一定程度上减轻内存碎片化,因为本身vmalloc就是把物理不连续的内存页映射到虚拟地址连续的空间内。

thread_info和stack的关系

提到stack内核栈,很多人会想到它和thread_info结构体的关系,比如在ARM32平台上有一个联合体:

union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
    struct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASK
    struct thread_info thread_info;
#endif
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

简化之后为:

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

因此利用这个关系,我们可以通过sp寄存器获取到对应的stack所在的地址,然后在转换到对应的thread_info结构体,而该结构体中保存的有当前进程的task_struct结构体指针。这就是current的实现原理。

而对于ARM64来说,一般会使能 CONFIG_THREAD_INFO_IN_TASK 这个宏,该宏会导致这个联合体结构发生变化,此时thread_info已经不和stack有什么关系了。此时thread_info是存在于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
    void                *stack;

那么这时的current怎么找到当前运行的进程呢?实际上这里的实现已经和ARM32不一样了,对于ARM64平台,记录当前进程的task_struct地址是利用sp0_el1寄存器,当内核执行进程切换时会把当前要运行的进程task_struct地址记录到该寄存器中。因此我们current查找task_struct时也是很简单了,不用通过sp和thread_info去定位了。

看到这里,会想到另一个问题:task_struct这里的stack和前面的thread_union中的stack有什么不同,该怎么理解呢?

task_struct结构体中一直都存在一个stack成员,它是用来记录每个进程的内核栈起始地址的,内核为每个进程都分配内核栈空间,并记录在此。而每个进程的thread_union中的stack实际上也是同一个stack地址,只不过我们定义一个联合体的目的,是为了方便我们把进程的thread_info结构体保存进stack的最低地址处。当然对于使能了 CONFIG_THREAD_INFO_IN_TASK 的系统,我们完全可以不必关注这个联合体。

说了这么多,至于VMAP_STACK,和上述的唯一差异点就是使用了vmalloc来分配内存并赋值给stack成员,它和thread_info也没有太大关系。


参考链接:
https://lwn.net/Articles/692208/
https://zhuanlan.zhihu.com/p/84591715

  • 2
    点赞
  • 5
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:猿与汪的秘密 设计师:我叫白小胖 返回首页
评论

打赏作者

程序猿Ricky的日常干货

你的鼓励将是我最大的动力!

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值