在工作中关于 Linux 系统中的堆栈问题一直是工程师们很头疼的问题,尤其出现一些新的版本,下面就与大家一起分享以下 Linux0.11 系统中堆栈的使用方法。
一、系统引导初始化临时使用的堆栈。
二、进入保护模式后提供内核程序始化使用的堆栈,该堆栈也是后来任务0使用的用户态堆栈。
三、每个任务通过系统调用,执行内核程序时使用的堆栈,称之为任务的内核态堆栈,每个任务都有自己独立的内核态堆栈。
四、任务在用户态执行的堆栈,位于任务(进程 )逻辑地址空间近末端处。
使用多个栈或在不同情况下使用不同栈的主要原因
首先,由于从实模式进入保护模式,使得 CPU 对内存寻址访问方式发生了变化,因此需要重新设置堆栈区域;其次,为了解决不同CPU特权级共享使用堆栈带来的保护问题,执行0级的内核代码和执行3级的用户代码需要使用不同的栈。当一个任务进入内核态运行时,就会使用其TSS段中给出的特权级0的堆栈指针tss.ss0.tss.esp0,即内核栈,原用户栈指针会保存在内核栈中,而当从内核态返回用户态时,就会恢复使用用户态的堆栈。
以下分别说明。
开机初始化时(bootsect.s,setup.s)
当bootsect代码被 ROM B IOS 引导加载到物理内存0x7c00处时,并没有设置堆栈段,程序也没有使用堆栈,直到bootsect被移动到0x9000:0处时,才把堆栈段 寄存器 SS设置为0x9000,堆栈指针esp寄存器设置为0xff00,所以堆栈堆栈在0x9000:0xff00处(boot/bootsect.s L61,62)setup.s也使用这个堆栈
进入保护模式时候(head.s,L31)
此时堆栈段被设置为内核数据段(0x10),堆栈指针esp设置成指向user_stack数组(sched.c L67~72)的顶端,保留了1页内存作为堆栈使用。
初始化时(m ai n.c)
在执行move_to_user_mode()代码把控制权移交给任务0之前,系统一直使用上述堆栈,而在执行过move_to_user_mode()之后,main.c的代码被“切换”成任务0中执行。通过执行fork()系统调用,main.c中的init()将在任务1中执行,并使用任务1的堆栈,而main()本身则在被“切换”成为任务0后,仍热继续使用上述内核程序自己的堆栈作为任务0的用户态堆栈。
任务的堆栈
每个任务都有两个堆栈,分别用于用户态和内核态程序的执行,并且分别称为用户态堆栈和内核态堆栈。
除了处于不同CPU特权级中,这两个堆栈之间的主要区别在于任务的内核态堆栈很小,所保存的数据最多不能超过4096个字节,而任务的用户态堆栈却可以在用户的64MB空间中延伸在用户态运行时每个任务(除了任务0和任务1)有自己的64MB地址空间,当一个任务(进程)刚被创建时,它的用户态堆栈指针被设置在其地址空间的靠近末端部分,应用程序在用户态下运行时就一直使用这个堆栈,实际物理地址内存则由CPU分页机制确定。
在内核态运行时
每个任务有其自己的内核态堆栈,用于任务在内核代码中执行期间。其所在的线性地址中位置由该任务TSS段中ss0和esp0两个字段指定,任务内核态堆栈被设置在位于其任务数据结构所在页面的末端,即于任务的任务数据结构(task_struct)放在同一页面中,参见kernel/fork.c L93
p->tss.esp0 = PAGE_SIZE + (long)p;
p->tss.ss0 = 0x10
*为什么从主存区申请得来的用于保存任务数据结构的一页内存也能被设置成内核数据段中的数据呢?就是说tss.ss0为什么可以是0x10?
用户内核态仍然属于内核数据空间,在head.s中设置内核代码段和数据段的描述符,段长度都设置成了16MB,这个长度值是Linux0.11内核所能支持的最大物理内存长度(head.s,110开始的注释),所以,内核代码可以寻址到整个物理内存范围中的任何位置,当然也包括主存区,每当任务执行内核程序而需要使用其内核栈时,CPU就会利用TSS结构把它的内核态堆栈设置成由tss.ss0和tss.esp0这两个值构成任务0(空闲进程idle)和任务1(初始化进程init)的堆栈任务0和任务1的代码段和数据段相同,限长都是640KB,但它们被映射到不同的线性地址空间,任务0的段基址从线性地址0开始,而任务1的段基址从64MB开始,但他们全部映射到物理地址0~640KB范围中,这个地址也就是内核代码和基本数据所存放的地方,在执行了move_to_user_mode()后,任务0和任务1的内核态堆栈分别位于各自任务数据结构所在页面的末端,而任务0的用户态堆栈就是前面进入保护模式后使用的堆栈,即user_stack[]数组的位置,由于任务1在创建时复制了任务0的用户堆栈,所以刚开始时任务0和任务1共享使用同一个用户堆栈空间,但是当任务1开始运行时,写时复制机制会为任务1另行分配主存区页面作为堆栈空间使用,只有到这个时候,任务1才开始使用自己独立的用户堆栈内存页面,因此任务0的堆栈需要在任务1实际开始使用之前保持干净,即任务0此时不能使用堆栈,以确保复制的堆栈页面中不含任务0的数据。