不同于Linux应用程序的栈能够动态增长,Linux内核栈是固定的,并且比较小,比如Linux 2.6.x内核,在X86 32位架构上一般是4K或8K(在进行内核编译时,Kernel hacking下进行配置,默认8K),而在X86 64位架构上固定为8K。Linux内核会分配一页(4K stack)或两页连续(8K stack)不可交换(non-swappable)内存来作为内核栈使用。Linux 2.4.x内核在X86位架构上,内核栈固定为8K。
当一个进行运行在内核态时(比如通过系统调用),它就将开始使用它自己的内核栈,如果内核栈大小为8K,那么此时的触发的中断处理也将使用这个栈,如果内核栈大小为4K,那么此时的触发的中断处理则将使用单独的内核栈。
由于内核栈大小固定且比较小,很容易出现内核栈溢出的情况,所以不能在内核代码里使用递归调用(除非你非常清楚它递归的层次,但仍建议将递归改为循环,因为谁也不知道将来哪一天递归的层次是否会发生变化),也不建议使用较大或大小未知的栈变量(比如动态数组)等。
由于task_struct和内核栈共用同一块内存区域,所以内核栈溢出最直接的后果就是把task_struct结构体踩坏,在linux下,这个结构体是至关重要的,每一个进程都是由这个task_struct数据结构来定义,它也就是我们通常所说的PCB,它是用来对进程进行控制的唯一手段,也是最有效的手段;但这是kernel 2.4.x的情况,下面代码来之kernel 2.4.37.11:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
到了kernel 2.6.x之后,和内核栈共用同一块内存区域的不再是task_struct,而是结构体thread_info,下面代码来之kernel 2.6.38.8:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
不过由于thread_info结构体的第一个字段就是task_struct指针,所以内核栈溢出的话同样会损坏task_struct,因为此时task指针指向一个不可预知的地址,相应的task_struct结构体各个字段数据当然也就都是垃圾数据了。
看图示更直观:
一旦内核栈溢出,间接导致task_struct结构体里的数据异常,那么就会导致系统处于一种不稳定状态(当然,一般情况也就是宕机):
当在查宕机问题时,如果定位到是由于thread_info结构体或task_struct结构体里数据异常导致(比如引起系统宕机的指令是访问task_struct结构体变量的某个字段),那么就要优先考虑是否由内核栈下溢引起宕机。对于栈的保护,Linux内核提供了一系列选项,比如DEBUG_STACKOVERFLOW、CC_STACKPROTECTOR等,但这些都只是辅助手段,要如何定位到更具体的位置呢?内核函数对栈空间的预留和应用层没有什么两样,同样是移动esp或rsp,所以我们可以先找出所有这些预留的地方,看哪个地方预留得最多,也就是该函数占用的栈空间最多,那么就是最有可能引发内核栈溢出的地方。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
可以看到,一个地方(kernel_stack_init函数)的栈占去$0x400(有几个局部变量是直接使用的寄存器,所以才没有消耗栈),而另外一个地方(just_copy_half函数)的栈占去%rax是个不定值,这就需要继续看对应的汇编来进行分析(因为数组是动态数组),这只是个示例,如果在真实内核代码中有这样的函数,那是相当危险的。更多细节不说,相关汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
|
上面提到Linux 2.6.x内核在X86 32位架构上可以配置内核栈大小(在进行内核编译时,Kernel hacking下进行配置,默认8K,配置之后对应的宏为CONFIG_4KSTACKS),具体生效代码可以看:
LXR / The Linux Cross Reference
1 2 3 4 5 6 |
|
通过CONFIG_4KSTACKS宏来定义THREAD_SIZE大小,但是在2.6.37后的内核代码里都已经找不到这个定义了:
LXR / The Linux Cross Reference
LXR / The Linux Cross Reference
LXR / The Linux Cross Reference
后来一查才知道CONFIG_4KSTACKS已经被移除了:https://lkml.org/lkml/2010/6/29/107,好吧,继续8K内核栈。