Linux 系统中堆栈的使用方法

本节内容概要描述了Linux内核从开机引导到系统正常运行过程中对堆栈的使用方式。这部分内容的说明与内核代码关系比较密切,可以先跳过。在开始阅读相应代码时再回来仔细研究。

Linux 0.12系统中共使用了4种堆栈。第1种是系统引导初始化时临时使用的堆栈;第2种是进入保护模式之后提供内核程序初始化使用的堆栈,位于内核代码地址空间固定位置处。该堆栈也是后来任务0使用的用户态堆栈;第3种是每个任务通过系统调用,执行内核程序时使用的堆栈,我们称之为任务的内核态堆栈。每个任务都有自己独立的内核态堆栈;第4种是任务在用户态执行的堆栈,位于任务(进程)逻辑地址空间近末端处。

使用多个栈或在不同情况下使用不同栈的主要原因有两个。首先是由于从实模式进入保护模式,使得CPU对内存寻址访问方式发生了变化,因此需要重新调整设置栈区域。另外,为了解决不同CPU特权级共享使用堆栈带来的保护问题,执行0级的内核代码和执行3级的用户代码需要使用不同的栈。当一个任务进入内核态运行时,就会使用其TSS段中给出的特权级0的堆栈指针tss.ss0、tss.esp0,即内核栈。原用户栈指针会被保存在内核栈中。而当从内核态返回用户态时,就会恢复使用用户态的堆栈。下面分别对它们进行说明。

5.8.1  初始化阶段

(1)开机初始化时(bootsect.S,setup.s)

当bootsect代码被ROM BIOS引导加载到物理内存0x7c00处时,并没有设置堆栈段,当然程序也没有使用堆栈。直到bootsect被移动到0x9000:0处时,才把堆栈段寄存器SS设置为0x9000,堆栈指针esp寄存器设置为0xff00,即堆栈顶端在0x9000:0xff00处,参见boot/bootsect.s第61、62行。setup.s程序中也沿用了bootsect中设置的堆栈段。这就是系统初始化时临时使用的堆栈。

(2)进入保护模式时(head.s)

从esp设置成指向user_stack数组的顶端(参见user_stack数组定义在sched.c的67~23。此时该堆栈是内核程序自己使用的堆栈。其中给出的地址是大约值,它们与编译时的实际设置参数有关。这些地址位置是从编译内核时生成的system.map文件中查到的。

图5-23  刚进入保护模式时内核使用的堆栈示意图

(3)初始化时(main.c)

在init/main.c程序中,在执行move_to_user_mode()代码把控制权移交给任务0之前,系统一直使用上述堆栈。而在执行过move_to_user_mode()之后,main.c的代码被“切换”成任务0中执行。通过执行fork()系统调用,main.c中的init()将在任务1中执行,并使用任务1的堆栈。而main()本身则在被“切换”成为任务0后,仍然继续使用上述内核程序自己的堆栈作为任务0的用户态堆栈。关于任务0所使用堆栈的详细描述见后面说明。

5.8.2  任务的堆栈

每个任务都有两个堆栈,分别用于用户态和内核态程序的执行,并且分别称为用户态堆栈和内核态堆栈。除了处于不同CPU特权级中,这两个堆栈之间的主要区别在于任务的内核态堆栈很小,所保存的数据量最多不能超过4096 – 任务数据结构块个字节,大约为3KB。而任务的用户态堆栈却可以在用户的64MB空间内延伸。

(1)在用户态运行时

每个任务(除了任务0和任务1)有自己的64MB地址空间。当一个任务(进程)刚被创建时,它的用户态堆栈指针被设置在其地址空间的靠近末端(64MB顶端)部分。实际上末端部分还要包括执行程序的参数和环境变量,然后才是用户堆栈空间,如图5-24所示。应用程序在用户态下运行时就一直使用这个堆栈。堆栈实际使用的物理内存则由CPU分页机制确定。由于Linux实现了写时复制功能(Copy on Write),因此在进程被创建后,若该进程及其父进程都没有使用堆栈,则两者共享同一堆栈对应的物理内存页面。只有当其中一个进程执行堆栈写操作(如push操作)时内核内存管理程序才会为写操作进程分配新的内存页面。而进程0和进程1的用户堆栈比较特殊,见后面说明。

图5-24  逻辑空间中的用户态堆栈

(2)在内核态运行时

每个任务都有自己的内核态堆栈,用于任务在内核代码中执行期间。其所在线性地址中的位置由该任务TSS段中ss0和esp0两个字段指定。ss0是任务内核态堆栈的段选择符,esp0是堆栈栈底指针。因此每当任务从用户代码转移进入内核代码中执行时,任务的内核态栈总是空的。任务内核态堆栈被设置在位于其任务数据结构所在页面的末端,即与任务的任务数据结构(task_struct)放在同一页面内。这是在建立新任务时,fork()程序在任务tss段的内核级堆栈字段(tss.esp0和tss.ss0)中设置的,参见kernel/fork.c,92行:

p->tss.esp0 = PAGE_SIZE + (long)p;

p->tss.ss0 = 0x10;

其中,p是新任务的任务数据结构指针,tss是任务状态段结构。内核为新任务申请内存用作保存其task_struct结构数据,而tss结构(段)是task_struct中的一个字段。该任务的内核堆栈段值tss.ss0也被设置成为0x10(即内核数据段选择符),而tss.esp0则指向保存task_struct结构页面的末端。如图5-25所示。实际上tss.esp0被设置成指向该页面(外)上一字节处(图中堆栈底处)。这是因为Intel CPU执行堆栈操作时是先递减堆栈指针esp值,然后在esp指针处保存入栈内容。

图5-25  进程的内核态堆栈示意图

为什么从主内存区申请得来的用于保存任务数据结构的一页内存也能被设置成内核数据段中的数据呢,即tss.ss0为什么能被设置成0x10呢?这是因为用户内核态栈仍然属于内核数据空间。我们可以从内核代码段的长度范围来说明。在head.s程序的末端,分别设置了内核代码段和数据段的描述符,段长度都被设置成了16MB。这个长度值是Linux 0.12内核所能支持的最大物理内存长度(参见head.s,110行开始的注释)。因此,内核代码可以寻址到整个物理内存范围中的任何位置,当然也包括主内存区。每当任务执行内核程序而需要使用其内核栈时,CPU就会利用TSS结构把它的内核态堆栈设置成由tss.ss0和tss.esp0这两个值构成。在任务切换时,老任务的内核栈指针esp0不会被保存。对CPU来讲,这两个值是只读的。因此每当一个任务进入内核态执行时,其内核态堆栈总是空的。

(3)任务0和任务1的堆栈

任务0(空闲进程idle)和任务1(初始化进程init)的堆栈比较特殊,需要特别予以说明。任务0和任务1的代码段和数据段相同,限长也都是640KB,但它们被映射到不同的线性地址范围中。任务0的段基地址从线性地址0开始,而任务1的段基地址从64MB开始。但是它们全都映射到物理地址0~640KB范围中。这个地址范围也就是内核代码和基本数据所存放的地方。在执行了move_to_user_mode()之后,任务0和任务1的内核态堆栈分别位于各自任务数据结构所在页面的末端,而任务0的用户态堆栈就是前面进入保护模式后所使用的堆栈,即sched.c的user_stack[]数组的位置。由于任务1在创建时复制了任务0的用户堆栈,因此刚开始时任务0和任务1共享使用同一个用户堆栈空间。但是当任务1开始运行时,由于任务1映射到user_stack[]处的页表项被设置成只读,使得任务1在执行堆栈操作时将会引起写页面异常,从而内核会使用写时复制机制(关于写时复制技术的说明请参见第13章)为任务1另行分配主内存区页面作为堆栈空间使用。只有到此时,任务1才开始使用自己独立的用户堆栈内存页面。因此任务0的堆栈需要在任务1实际开始使用之前保持“干净”,即任务0此时不能使用堆栈,以确保复制的堆栈页面中不含有任务0的数据。

任务0的内核态堆栈是在其人工设置的初始化任务数据结构中指定的,而它的用户态堆栈是在执行move_to_user_mode()时,在模拟iret返回之前的堆栈中设置的,参见图5-22所示。我们知道,当进行特权级会发生变化的控制权转移时,目的代码会使用新特权级的堆栈,而原特权级代码堆栈指针将保留在新堆栈中。因此这里先把任务0用户堆栈指针压入当前处于特权级0的堆栈中,同时把代码指针也压入堆栈,然后执行IRET指令即可实现把控制权从特权级0的代码转移到特权级3的任务0代码中。在这个人工设置内容的堆栈中,原esp值被设置成仍然是user_stack中原来的位置值,而原ss段选择符被设置成0x17,即设置成用户态局部表LDT中的数据段选择符。然后把任务0代码段选择符0x0f压入堆栈作为栈中原CS段的选择符,把下一条指令的指针作为原EIP压入堆栈。这样,通过执行IRET指令即可“返回”到任务0的代码中继续执行了。

5.8.3  任务内核态堆栈与用户态堆栈之间的切换

在Linux 0.12系统中,所有中断服务程序都属于内核代码。如果一个中断产生时任务正在用户代码中执行,那么该中断就会引起CPU特权级从3级到0级的变化,此时CPU就会进行用户态堆栈到内核态堆栈的切换操作。CPU会从当前任务的任务状态段TSS中取得新堆栈的段选择符和偏移值。因为中断服务程序在内核中,属于0级特权级代码,所以48位的内核态堆栈指针会从TSS的ss0和esp0字段中获得。在定位了新堆栈(内核态堆栈)之后,CPU就会首先把原用户态堆栈指针ss和esp压入内核态堆栈,随后把标志寄存器eflags的内容和返回位置cs、eip压入内核态堆栈。

内核的系统调用是一个软件中断,因此任务调用系统调用时就会进入内核并执行内核中的中断服务代码。此时内核代码就会使用该任务的内核态堆栈进行操作。同样,当进入内核程序时,由于特权级别发生了改变(从用户态转到内核态),用户态堆栈的堆栈段和堆栈指针以及eflags会被保存在任务的内核态堆栈中。而在执行iret退出内核程序返回到用户程序时,将恢复用户态的堆栈和eflags。这个过程如图5-26所示。

图5-26  内核态和用户态堆栈的切换

如果一个任务正在内核态中运行,那么若CPU响应中断就不再需要进行堆栈切换操作,因为此时该任务运行的内核代码已经在使用内核态堆栈,并且不涉及优先级别的变化,所以CPU仅把eflags和中断返回指针cs、eip压入当前内核态堆栈,然后执行中断服务过程。

时间: 2024-07-31 12:01:17

Linux 系统中堆栈的使用方法的相关文章

虚拟机上在Linux系统中安装JDK的方法

1.   mkdir /soft      (创建一个目录,用于存放安装软件 ) 2.     cd /soft      (切换到soft目录 ) 3. rz(从windows中选择安装包) 4.rmp  -ivh jdk-7u45-linux-x64.rpm   (安装JDK) 5.find / -name 'jdk' 6.vi /etc/profile  (编辑环境变量)  在profile的最后写下面的地址 export JAVA_HOME=/usr/java/jdk1.7.0_45ex

学习Linux系统中命令的简单方法

如果说如何快速学习.了解Linux的话,我的答案是学命令.背命令!为何呢?对于一名新手来说,去学习Linux的思想.了解Linux的架构.明白Linux中"一切皆文件"概念虽然说是没有错,是对的.但是个人认为去学习这些"高大上"的东西不是一时半会的事儿,它需要一定的时间和经验去沉淀才能掌握.那么如何最快速了解Linux并使用呢?我依然觉得学命令.背命令,掌握命令是比较笨但却是比较快的方式. 我开始学习Linux的时候,问了前辈:我入门Linux需要掌握哪些命令呢?前

老男孩教育每日一题-2017年5月12日-磁盘知识点:linux系统中LVM配置实现方法?

1.题目 2.参考答案 01:将一个或多个物理分区创建为一个PV # pvcreate /dev/sdb{1,2} Physical volume "/dev/sdb1" successfully created Physical volume "/dev/sdb2" successfully created # pvs          #<- 查看系统中的PV信息 PV                 VG   Fmt      Attr     PSiz

Linux0.11系统中堆栈的使用方法

Linux 0.11系统中共使用了四种堆栈 一.系统引导初始化临时使用的堆栈. 二.进入保护模式后提供内核程序始化使用的堆栈,该堆栈也是后来任务0使用的用户态堆栈. 三.每个任务通过系统调用,执行内核程序时使用的堆栈,称之为任务的内核态堆栈,每个任务都有自己独立的内核态堆栈. 四.任务在用户态执行的堆栈,位于任务(进程 )逻辑地址空间近末端处使用多个栈或在不同情况下使用不同栈的主要原因. 一.由于从实模式进入保护模式,使得CPU对内存寻址访问方式发生了变化,因此需要重新设置堆栈区域. 二.为了解

Linux系统中修改/etc/profile文件的方法

在Linux系统中etc/profile文件一般是不能更改的,想要更改etc/profile文件就要用一些特殊的技巧进行Linux文件修改.本文就来介绍一下Linux系统中修改/etc/profile文件的方法: etc/profile文件是只读的,直接用vi或gedit打开修改后是无法保存的.要修改profile,需要取得root权限,(使用gedit编辑) $sudo gedit /etc/profile 或者 $sudo -s $gedit /etc/profile 这样打开profile

每天进步一点点——Linux系统中的异常堆栈跟踪简单实现

转载请说明出处:http://blog.csdn.net/cywosp/article/details/25002201 在Linux中做C/C++开发经常会遇到一些不可预知的问题导致程序崩溃,同时崩溃后也没留下任何代码运行痕迹,因此,堆栈跟踪技术就显得非要重要了.本文将简单介绍Linux中C/C++程序运行时堆栈获取,首先来看backtrace系列函数--使用范围适合于没有安装GDB或者想要快速理清楚函数调用顺序的情况 ,头文件execinfo.h int backtrace (void **

Linux系统中vi编辑器的使用方法入门

vi编辑器是Linux系统下标准的编辑器.而且不逊色于其他任何最新的编辑器.可是会用的有多少呢.下面介绍一下vi编辑器的简单用法和部分命令.让你在Linux系统中畅行无阻. 基本上vi可以分为三种状态,分别是命令模式(command mode).插入模式(Insert mode)和底行模式(last line mode). 1) 命令行模式command mode) 控制屏幕光标的移动,字符.字或行的删除,移动复制某区段及进入Insert mode下,或者到 last line mode.  

在Linux系统中文件(资源)和用户的管理

一个可执行二进制程序,被加载到内存,被内核调度到CPU上运行,这时候,就表现了一个进程.也可以说进程是程序的一个实例,是程序的动态表现. 在 Linux 系统中进程(process)是有属主的,也就是该进程以哪个用户的身份运行的.大家都知道,程序有输入和输出,也称这为程序IO.如果我们程序数据输入是磁盘.如,Web 服务器,接收用户的请求之后,把网页数据从磁盘中读入加工之后再把数据响应给用户.如果,发起Web 服务进程的用户没有读取该用户请求网页文件的权限.则无法响应用户的请求了.所以,文件(资

Linux系统中“动态库”和“静态库”那点事儿 /etc/ld.so.conf 动态库的后缀为*.so 静态库的后缀为 libxxx.a ldconfig 目录名

Linux系统中“动态库”和“静态库”那点事儿 /etc/ld.so.conf  动态库的后缀为*.so  静态库的后缀为 libxxx.a   ldconfig   目录名 转载自:http://blog.chinaunix.net/uid-23069658-id-3142046.html 今天我们主要来说说Linux系统下基于动态库(.so)和静态(.a)的程序那些猫腻.在这之前,我们需要了解一下源代码到可执行程序之间到底发生了什么神奇而美妙的事情. 在Linux操作系统中,普遍使用ELF格