文章目录
- 1. 由来
- 2. 流程图
- 3. 中断
- 3.1 概念
- 3.2 8259A芯片
- 3.4 中断时的栈处理
- 3.4.1 相同特权级
- 3.4.2 不同特权级
- 3.5 中断流程
- 3.6 定位中断程序
- 3.7 中断流程步骤总结
- 4. 源码
- 4.1 `move_to_user_mode`
- 4.2 0号进程
- 4.3 `TSS`和`LDT`在`GDT`表排布
- 4.4 ldt中的0x17栈段
- 5. 总结
1. 由来
首先需要强调下,博主这边涉及的linux内核源码是0.11
版本,因为这是linus
对intel 开发手册
最纯粹的编写,也就可以理解为照着intel 开发手册
写的,后面的小节会体现出来。intel 开发手册
是对CPU
的一些规则制定,所以读者们也不要心生畏惧,就比如你要写一个JAVA
程序,是不是首先要熟悉它的语法规则,然后通过这些规则进行编写自己目标程序。而intel 开发手册
就类似于这个概念,而linux
实现的源码就是在此基础上进行的程序实现。(PS: intel规定的ISA规则 -》机器码 -》 汇编语言 -》C语言)
在聊内核态和用户态之前,我觉得读者对这2个概念都会恐慌,退缩。即使知道它们的概念,也不知道是个什么,也就知道内核态是OS,用户态是应用程序,而不理解为什么需要这么切换,这么切换的目的又是什么,不急,我这边逐一讲解。这里会涉及大篇幅的前置知识介绍,这些知识都通了,读者们也就顺理成章理解了内核态和用户态的神秘面纱的背后是什么。
接下来我会介绍下他们的由来,对于CPU
来说,所有程序都是二进制的各种组合,而CPU
本身通过自己指定的二进制规则,来识别程序的二进制,达到程序的执行。操作系统本身也是一个程序,应用程序也是一个程序,所以对于CPU
来说,都是一视同仁的。而CPU
本身是来驱动硬件,并不能让所有程序都能根据CPU
的硬件控制规则,来控制硬件,这会造成极度的不安全。所以CPU
产生了特权级的概念,也可以类理解为我们日常开发的权限系统
,特权级的小知识可以看我之前写的博客【intel 开发手册】特权级。当需要特权级后进行甄别不同特权级的代码,是否可以运行。每个特权级都有自己的栈空间,来存储自己程序信息。为什么每个特权级都有自己的栈,用来隔阂不同程序间的敏感数据。那这时就会引申出OS
为最高特权级,会有自己的栈,那么应用程序为最低特权级,也有自己的栈。那么比如现在我执行的是内核OS的代码,那么如何切换到应用程序的栈空间呢?这里会涉及到CPU
给定的规则中断规则,来执行切换,来完成内核态到用户态切换的效果。
以上就是本文章的核心,整个文章也会因此展开详细的解说。
2. 流程图
3. 中断
3.1 概念
什么是中断?为什么需要这个概念?
拿一个生活的例子举例,比如你是一个JAVA CODER
正在编写需求,这时一个电话打来了,你是否需要停下手头的代码任务,来看谁打来的电话,并接听。那如果你没有这个中断动作,一直执行程序编写,那你的电话岂不是是会被打爆。CPU
也同样如此,比如CPU
执行一个程序时,通过中断检测周期来检查是否发生中断,有中断后,执行中断程序,执行完后,在继续执程序,也就是所谓的不同任务切换。那说到这,中断的背景和概念也是出来了,那我通过专业点的术语来描绘下这个过程吧。
中断是通过8259A
芯片阵脚置为高电频,并对应的中断程序位置为高电频,当中断检测周期检测到后,会把CPU
的INTR
阵脚置为高电频,通过后续的检测,来执行中断程序,执行完后通过指定的中断返回指令iret
来进行返回,让CPU
继续执行代码。
这里博主主要讲述下中断流程,而流程的原理来源于intel 开发手册
,我们通过图解来更深入理解下流程
3.2 8259A芯片
中断的原理,就是围绕8259A
芯片展开和软件中断,这里博主主要讲下硬件中断,这个芯片名称大家看到后不要退缩,博主当时学的时候,就担心因为有很多未知的芯片都需要去记,就很苦恼,也会产生畏惧,但发现这些都是无用的畏惧。因为当你学一个,才用一个,未来是未来学,不要把未来的苦恼搬到现在,让自己临阵退缩。而是研究一个东西,就研究到底,形成自己的知识树。
该图取自《Linux 内核完全注释》,8259A
芯片左面有一列的中断源,芯片INT
引脚连接着CPU
的引脚INTR
。一共2个8259A
,读者会好奇为什么是2个,不是更多,因为当有1个8259A
芯片时,中断源只能有8个,不够用,所以级联的方式级联了另外一个8259A
。
PC
机通过8259A
芯片的设置,周期性发出中断信号。也就是中断检测周期,CPU
通过接收到中断检测周期后,检查INTR
是否被8259A
置为高电频,然后通过数据总线解码后获取中断向量,在通过中断向量执行对应的中断程序。
3.4 中断时的栈处理
开头说了不同特权级,有着不同的栈,那么中断也是同理,中断属于内核代码,拥有最高权限,那如果执行的代码权限相同,会在当前执行成的栈上进行,不同的特权级,会产生切换。
3.4.1 相同特权级
当不切换时,在原先的栈继续执行,需要保存原先执行代码的地址信息,错误码,状态标志位。为什么需要这些信息?首先但你执行完中断代码,需要找到原先的代码继续执行,产生中断时并且告诉CPU,我在中断中,对应的中断信息是什么,也就是错误码。
状态标志位用来记录当前执行的中断程序是否可以再次嵌套中断,为什么中断后还可以中断呢?因为中断有2种类型,一种是陷阱门,另一种是中断门,如果是中断门,就会清除状态标志位IF
,来告诉CPU
不允许被中断(但有时会有不可屏蔽中断,如切断电源)。
🐯volume 1 - 6.5 INTERRUPTS AND EXCEPTIONS:栈未切换的步骤流程(特权级相同)
当处理完后,需要返回到原先的执行代码
🐯volume 1 - 6.5 INTERRUPTS AND EXCEPTIONS:同一个特权级,栈未切换,返回的处理
来自中断/异常处理器返回,被
IRET
指令发起。除了它会存储中断程序的eflags
内容,IRET
指令和far ret
指令类似。在中断处理程序的同个特权级,当执行来自中断/异常处理器的返回时,处理器会做如下动作:
- 将CS和EIP寄存器恢复到中断或异常之前的值。
- 恢复
eflags
寄存器。- 适当的增加栈针偏移量。
- 恢复中断程序的执行。
3.4.2 不同特权级
如果产生了切换,就需要额外存储原先的栈的地址信息。
🐯volume 1 - 6.5 INTERRUPTS AND EXCEPTIONS:栈切换的步骤流程(特权级不同)
当处理完后,需要返回到原先的执行代码
🐯volume 1 - 6.5 INTERRUPTS AND EXCEPTIONS:不同特权级,栈切换,返回的处理
执行一个特权级别不同的返回,处理器会有如下动作:
- 执行权限校验。
- 将CS和EIP寄存器恢复到中断或异常之前的值。
- 恢复
eflags
寄存器。- 将SS和ESP寄存器恢复到中断或异常之前的值,从而导致堆栈切换回被中断过程的堆栈。
- 恢复被中断的程序。
3.5 中断流程
那既然发生了中断,那intel 开发手册
是如何规定的中断运转流程呢?看下下面这张图。
CPU
首先正在运行当前程序,中断检测周期定期通知,CPU
当执行完一个原子性的操作后,然后根据中断检测周期来的提醒,检测CPU
的INTR
阵脚,和IF
标志位(后面会说),进行决定是否运行中断程序,当产生中断后,会有中断程序被执行,那这些程序是如何产生的?又怎么找到的呢?
intel 开发手册
规定了18个预定义的中断和异常,224用户定义的中断,这些预定义的程序的入口在IDT
表里里。在IDT
里的每个中断和异常被一个向量数字所标识。
目录位置:🐯volume 1 - 6.5 INTERRUPTS AND EXCEPTIONS
有CPU
和OS
自己定义的中断程序,通过IDT
来寻找对应的程序,简简单单的一句话,解决了2个疑问。那下来我来介绍下如何执行中断流程代码的步骤
3.6 定位中断程序
这里会涉及很多概念,如GDT
,Segmenet Selector
,Segment Descriptor
概念,读者也不用慌,博主也写了一篇文章专门对这些概念的介绍【intel 开发手册】GDT相关概念。建议读者先大致了解下,然后继续这一节的流程。
通过intel 开发手册
提供的图直接讲解,IDT
是中断描述符表,存储一些信息,那如何找到这张表的地址,那就需要一个寄存器,也就是IDTR
中断寄存器来存储这个表地址和相关信息。用通俗易懂的话讲,你要存数据,就需要一个表,而表是存储在硬盘上的,那如何找到这张表,就需要知道这个表在硬盘存在哪。
所以下图也是这个目的,通过IDTR
存储着IDT
表的地址,IDT
表存储着中断门程序的地址和特权级等一些信息。、
那就梳理一下流程:通过IDTR
寄存器里的IDT Base Addr
定位IDT
表的首地址,IDT limit
限制表的大小。
目录:🐯volume 3 - 7.1 Relationship of the IDTR and IDT
既然找到中断程序门描述符了,那就需要寻找对应的中断的程序了,那就会有一个疑问,为什么不直接存程序呢,而是要多一个步骤,因为涉及中断权限校验,看你是否有权限。
通过之前说的,8259A
芯片会有一个中断源的引脚产生高电频,CPU
通过数据总线解码(也有int
中断指令),得到这个高电频的位置,也就是中断向量,那么我们就可以通过IDT
和这个Interrupt Vector
来定位中断程序在IDT
表中的位置信息,定位到后,开始权限校验,来获取段选择子,和offset
从而定位GDT
表里的Code Segment
的位置,从而获取到最终的中断代码
目录:🐯volume 3 - 7.3 Interrupt Procedure Call
3.7 中断流程步骤总结
来梳理下流程:
- 首先8259A芯片会产生一个中断标志,然后阵脚
INT
置为高电频,对应的中断向量位置置为高电频。或者int
指令进行中断 - 中断检测周期发现后,将CPU的
INTR
置为1,当CPU执行完最后一个指令(IF,ID,EXEC,MEM,WB)时,开始判断是否可以中断 - 检查
eflags
的if
标志位,是否是被清除的,是的话,不能中断 - CPU和8259A芯片连接的数据总线,解码获取到中断向量(与就是中断的程序位置)
- 获取到后,和
IDTR
(base addr + offset)找到对应IDT
表,专表专用,通过中断向量找到对应的IDT
描述符 - 进行特权级检查,是否发生栈切换,(CPL和中断程序的dpl判断),然后压入对应的
SS,SP,ELFAGS,CS,IP,ERROR CODE
等信息 - 通过中断描述符段选择子找到对应的
GDT
里对应的各种段信息 - 然后开始执行
code segment
中断程序 - 执行后,通过
iret
,通过压入栈的SS,SP
信息返回压入的栈地址
4. 源码
需要再次声明,linux
使用的是0.11
版本源码,如果小伙伴不知道怎么阅读源码,可以看Source Insight 读取源码使用入门。
linux
的内核态到用户态的切换,就是利用中断的特权级切换时,产生栈切换,把用户态的代码段信息压入栈中,当iret
返回时,就顺利的切换到了指定的用户程序代码。下面进行下讲解。
在入口函数,就有内核态切换用户态的源码,那我们先从主函数的源码开始
4.1 move_to_user_mode
这个代码在main.c
里,这是linux
的执行入口。博主把不相关代码省略了,这样看着也比较清爽。
我们这边通过主函数的执行代码,可以看到move_to_user_mode
,从代码名称也可看出,是切换为用户模式,那我们看下这个api的详细执行代码。
void main(void)
{ // 前面代码省略....sched_init(); // 初始化0号进程的tss,ldt描述符信息// 中间代码省略...move_to_user_mode(); // SS,SP,EFLAGS,SS,SP初始化,并iret切换栈,cpl=3// 切换到了cpl=3,就不能直接调用内核函数,需要系统调用(可类比B/S架构,规定协议,然后调用接口,接受返回结果)// 需要找到对应的TSS,LDT来开始执行代码// 而他俩的位置:NULL,CS(内核代码段),DS(内核数据段),NULL,TSS0,LDT0(这个排列规则在sched.h写了FIRST_TSS_ENTRY的注释,给出)if (!fork()) { /* we count on this going ok */init();}for(;;) pause();
}
它的实现是内联汇编,汇编的语法比较简单,这里linux
的实现是c,用的gnuc
套件,所以是AT&T
的语法,是从左到右逻辑。由于它是使用intel
提供的中断机制,那么就需要遵守iret
返回时的数据信息,也就是上面[3.4.2 不同特权级](#3.4.2 不同特权级)的讲述
大家也可以通过代码和对照图能看出,它压入的顺序,和上面讲述的图片压入的信息是一样的
ss:栈段选择子
esp:栈顶指针
eflags:状态
cs:状态寄存器段选择子
eip:相对代码段位置的下一个偏移指针
#define move_to_user_mode()
__asm__ (movl %%esp,%%eaxpushl $0x17 // sspushl %%eax // esppushfl // eflagspushl $0x0f // cspushl $1f // eipiret // Interrupt return 1: movl $0x17,%%eax // movw %%ax,%%ds // 数据段movw %%ax,%%es // 扩展段movw %%ax,%%fs // fsmovw %%ax,%%gs // gs::: // 无输出,无输入"ax" // 保存到eax里
)
那为什么ss
是0x17
,这个也是对着手册中的段选择子的规则写的,那我们来看下
0x17
->0000_0000_0001_0111
再通过图中的对比,可以得出如下信息
- rpl = 3,
- ldt
- 在
ldt
表里的位置index=2只不过它在
ldt
表项放在第2个位置索引,后面讲INIT_TASK
时,会讲ldt
表项的排布
当它执行到iret
时,依次弹出,当弹出1f
时,会执行下面的1:
后面的代码,进行数据段等的初始化。然后弹出对应的代码段,指定的用户态的栈段,这样就顺利切换到了用户态的代码了。
但如果指明是ldt
类型的段选择子,会存在ldt
表里,那对应ldt
表里的下表为什么是2呢?在这节,通过上面的铺垫就已经讲完了内核态到用户态的切换,下面的几节是深入一下相关知识,读者可自行选择。
4.2 0号进程
linux
执行进程,它的进程管理是一个数组,更多进程的详解,可看我之前的博客【Linux 源码】进程。那肯定会有一个内核进程先执行,才能做后续的操作,所以我们看下这个0号进程是怎么初始化的ldt
就可以了。那我们就在source insight
找下task_struct
,可以看到它的0号索引存放的是一个task_struct
的地址指针,也就是第一个进程。
🐯那我们看看这里面的ini_task
都做了什么
当我们看它的实现,其实是一个union
的结构体,那我们先看下这个结构体定义了什么,它定义了2个属性,union
会把这个结构体里占用内存最大的空间,作为结构体的总大小,所以也就是4kb的栈大小,而在下面进行的INI_TASK
的内容,就放入到了这个栈中。我也通过图片展示了下
#define PAGE_SIZE 4096union task_union {struct task_struct task;char stack[PAGE_SIZE];
};static union task_union init_task = {INIT_TASK,};
🐯既然知道了是存储了一个栈上的结构体,那这个INIT_TASK
里面的实现又是什么呢,是怎么定义task_struct
各种属性呢,那我们点进去看下
点进去是各种眼花缭乱的数据,但博主进行一一标注,因为它是相当于给task_struct
的各个属性赋值,就包含上面我们提及到的ldt
,可进行观看下,博主也把task_struct
对应的属性代码也放置过来
/** Entry into gdt where to find first TSS. 0-nul, 1-cs, 2-ds, 3-syscall* 4-TSS0, 5-LDT0, 6-TSS1 etc ...*/
#define FIRST_TSS_ENTRY 4
// 5
#define FIRST_LDT_ENTRY (FIRST_TSS_ENTRY+1)
// 0 + 5 * 2^3 = 40 -> ldt0的首地址
#define _LDT(n) ((((unsigned long) n)<<4)+(FIRST_LDT_ENTRY<<3))/** INIT_TASK is used to set up the first task table, touch at* your own risk!. Base=0, limit=0x9ffff (=640kB)*/
#define INIT_TASK
/* state etc */ { 0, // long state15, // long counter15, // long priority/* signals */ 0, // long signal{{},}, // struct sigaction sigaction[32];0, // long blocked /* bitmap of masked signals *//* ec,brk... */ 0, // exit_code0,0,0,0,0, // unsigned long start_code,end_code,end_data,brk,start_stack;/* pid etc.. */ 0,-1,0,0,0, // long pid,father,pgrp,session,leader;/* uid etc */ 0,0,0, // unsigned short uid,euid,suid;0,0,0, // unsigned short gid,egid,sgid;/* alarm */ 0, // long alarm;0,0,0,0,0, // long utime,stime,cutime,cstime,start_time;/* math */ 0, // unsigned short used_math;/* fs info */ -1, // int tty; /* -1 if no tty, so it must be signed */0022, // unsigned short umaskNULL, // struct m_inode * pwd;NULL, // struct m_inode * root;NULL, // struct m_inode * executable;0, // unsigned long close_on_exec;/* filp */ {NULL,}, // struct file * filp[NR_OPEN];// struct desc_struct ldt[3];{ {0,0},
/* ldt */ {0x9f,0xc0fa00}, {0x9f,0xc0f200}, },
/*tss*/ {0, // back_link; /* 16 high bits zero */PAGE_SIZE+(long)&init_task, // esp0;0x10, // ss0; /* 16 high bits zero */0, // esp1;0, // ss1; /* 16 high bits zero */0, // esp2;0, // ss2; /* 16 high bits zero */(long)&pg_dir, // cr3; 0, // eip; 0, // eflags; 0,0,0,0, // eax,ecx,edx,ebx; 0, // esp;0, // ebp;0, // esi;0, // edi;0x17, // es; /* 16 high bits zero */0x17, // cs; /* 16 high bits zero */0x17, // ss; /* 16 high bits zero */0x17, // ds; /* 16 high bits zero */0x17, // fs; /* 16 high bits zero */0x17, // gs; /* 16 high bits zero */_LDT(0), // ldt; /* 16 high bits zero */0x80000000, // trace_bitmap; /* bits: trace 0, bitmap 16-31 */{} // i387_struct i387;},
}
struct task_struct {
/* these are hardcoded - don't touch */long state; /* -1 unrunnable, 0 runnable, >0 stopped */long counter;long priority;long signal;struct sigaction sigaction[32];long blocked; /* bitmap of masked signals */
/* various fields */int exit_code;unsigned long start_code,end_code,end_data,brk,start_stack;long pid,father,pgrp,session,leader;unsigned short uid,euid,suid;unsigned short gid,egid,sgid;long alarm;long utime,stime,cutime,cstime,start_time;unsigned short used_math;
/* file system info */int tty; /* -1 if no tty, so it must be signed */unsigned short umask;struct m_inode * pwd;struct m_inode * root;struct m_inode * executable;unsigned long close_on_exec;struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */struct desc_struct ldt[3];
/* tss for this task */struct tss_struct tss;
};struct tss_struct {long back_link; /* 16 high bits zero */long esp0;long ss0; /* 16 high bits zero */long esp1;long ss1; /* 16 high bits zero */long esp2;long ss2; /* 16 high bits zero */long cr3;long eip;long eflags;long eax,ecx,edx,ebx;long esp;long ebp;long esi;long edi;long es; /* 16 high bits zero */long cs; /* 16 high bits zero */long ss; /* 16 high bits zero */long ds; /* 16 high bits zero */long fs; /* 16 high bits zero */long gs; /* 16 high bits zero */long ldt; /* 16 high bits zero */long trace_bitmap; /* bits: trace 0, bitmap 16-31 */struct i387_struct i387;
};
看到这小伙伴,就好奇了,task_struct
为啥这么多属性,因为它是严格遵守intel手册的属性顺序排布的,也就是下面这张图。
在上面初始化我们就看到了ss
初始化就是0x17
,这就对应上了之前讲的move_to_user_mode
里的0x17
的由来。
那既然有了local desciptor
相关,但执行代码的流程,是通过段选择子找到GDT
,然后根据段选择子来判断是ldt,gdt
,那gdt
里ldt
是放在哪的呢?还有tss
进程的上下文信息的地址在gdt
是如何排布的呢?
4.3 TSS
和LDT
在GDT
表排布
那既然涉及到排布,其实也就是初始化地址的排布,那就在主入口里
void main(void)
{ // 前面代码省略....sched_init(); // 初始化0号进程的tss,ldt描述符信息// 后面代码省略....
}
那我们看看这个代码的实现,这里就进行了ldt
和tss
描述符的地址初始化,那我们看看是如何实现的
void sched_init(void)
{int i;struct desc_struct * p;if (sizeof(struct sigaction) != 16)panic("Struct sigaction MUST be 16 bytes");set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));// 后面代码略....
}
点进去后,又是内联汇编,发现设置了怎么多的属性,那这些属性,也不外乎是根据intel
手册进行设置的。博主也粘贴了过来,也依次标注了。
TSS
和LDT
是通过TI
标志位来判断的
🐯volume 3 - 9.2.2 TSS Descriptor:intel开发手册原话
An attempt to access a TSS using a segment selector with its TI flag set (which indicates the current LDT)
TI
标志位设置的话,那么这个描述符是LDT
描述符
0x89 -> 1(p) 00(dpl) 0 1001(type)
:tss
0x82 -> 1(p) 00(dpl) 0 0010(type)
:ldt
// 初始化tss,ldt描述符
// n后面加的是偏移量,单位是byte
#define _set_tssldt_desc(n,addr,type)
__asm__ (// 处理TSS Descriptor下面的0 ~ 31"movw $104,%1 // *(n) ,放入104,也就是0~15的SegmentLimit"movw %%ax,%2 // 将0号进程的task.tss地址的低16位,放入*(n+2),也就是16~31的Base Address,(2 * 8 = 16bit)"rorl $16,%%eax // 处理TSS Descriptor 上面的0 ~ 31"movb %%al,%3 // 0 ~ 7的base addr"movb $type ",%4 // 8 ~ 16,放入0x89 , 0x89 -> 1(p) 00(dpl) 0 1001(type)"movb $0x00,%5 // 16 ~ 24放入0"movb %%ah,%6 // 24 ~ 31 放入base addr"rorl $16,%%eax::"a(addr), // 0"m(*(n)), // 1"m(*(n+2)), // 2"m(*(n+4)), // 3 "m(*(n+5)), // 4"m(*(n+6)), // 5"m(*(n+7)) // 6
)// n为tss首地址
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),((i)(addr)),"0x89")set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),((i)(addr)),"0x82")
看到这,基本上是掌握了linux
的用户态从浅入深的所有过程。很多晦涩的点,需要读者多琢磨,因为需要很多前置知识,才能看懂这段源码,当你拥有了这些前置知识GDT,TSS,LDT,interrupt
等等,看这个源码在研究研究语法,就可看出是照着intel
开发手册写的。
4.4 ldt中的0x17栈段
上面提到了0x17栈段解析,那现在需要知道为什么在ldt
表里的索引下标是2。那就要看看ldt
表里都初始化了什么,在上面的[4.2 0号进程](# 4.2 0号进程)讲解了各类属性的初始化的值,这里就有ldt
表数据的初始化
intel 在
gdt
,idt
,ldt
表项中第一个地址是不防值的所以可以看到0索引下标无值
// struct desc_struct ldt[3];{ {0,0},
/* ldt */ {0x9f,0xc0fa00}, // cs 0x0f {0x9f,0xc0f200}, // ss 0x17},
0x17
就是对应的是ldt[2]
,也就是{0x9f,0xc0f200}
,这个的初始化规则,是通过intel
规定的Segment Descriptor
的规则,来写的
将其转化为2进制(intel是小端序)
将其转化为2进制(intel是小端序)
0x9f,0xc0f200
0000 0000 1100 0000 1111 0010 0000 0000
0000 0000 0000 0000 0000 0000 1001 1111能得到的
S
标志位是描述符类型1
Type
:0010
就可得出是栈段
这也得到了0x17
的验证。cs
的验证同理
5. 总结
内核态和用户态,就是特权级切换,那为什么特权级切换,就有对应的内核态和用户态呢?因为intel规定,每个特权级都有自己的栈信息,相当于你是R0
的内核态特权级,有自己的特权级栈,应用程序也是如此。如果他俩都在一个栈,可以遍历敏感信息,一个特权级,那么直接可以像OS操控CPU了
对于CPU来说所有的程序,不管是OS,还是应用程序,对于它来说,都是程序,所以CPU规定了特权级,来隔阂不同的程序代码权限,所以OS程序就是最高特权级的程序了,来通过CPU提供了指令,来控制硬件,而应用程序只能通过OS提供的API来有限的控制CPU硬件,如IO输入输出等。
所以每个特权级都有自己的栈,那如何切换栈呢?切换到我用户态的栈里?这里intel 开发手册
提供了中断规则,不同的特权级就可以切换栈,所以linux 0.11
利用这个特性,来进行切换栈信息,并压入指定的应用程序的栈地址,也就是用户态代码,当执行完中断时,通过iret
指令依次弹出信息,当弹出SS,SP
找到linux 0.11
指定的栈地址,就完成了内核态到用户态切换
虽然这3段话总结起来简单,但里面涉及的知识,博主研究了很久,才慢慢串通。就比如上面的中断流程涉及的8259A
芯片,Segment selector
对应的机器码的属性信息等等。
博主这里主要讲了CPU中断场景的硬件中断,没有讲软件方式的INT n
中断,后续我会再出2篇文章讲中断,一篇是intel
中断的知识,一篇是结合linux
源码来讲软件中断。(这里主要讲的是内核态到用户态,用户态到内核态需要系统调用,也就是所谓的软件中断)