【Linux 源码】内核态到用户态

文章目录

    • 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版本,因为这是linusintel 开发手册最纯粹的编写,也就可以理解为照着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芯片阵脚置为高电频,并对应的中断程序位置为高电频,当中断检测周期检测到后,会把CPUINTR阵脚置为高电频,通过后续的检测,来执行中断程序,执行完后通过指定的中断返回指令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指令类似。在中断处理程序的同个特权级,当执行来自中断/异常处理器的返回时,处理器会做如下动作:

  1. 将CS和EIP寄存器恢复到中断或异常之前的值。
  2. 恢复eflags寄存器。
  3. 适当的增加栈针偏移量。
  4. 恢复中断程序的执行。
3.4.2 不同特权级

​ 如果产生了切换,就需要额外存储原先的栈的地址信息。

🐯volume 1 - 6.5 INTERRUPTS AND EXCEPTIONS:栈切换的步骤流程(特权级不同)在这里插入图片描述

当处理完后,需要返回到原先的执行代码

🐯volume 1 - 6.5 INTERRUPTS AND EXCEPTIONS:不同特权级,栈切换,返回的处理

执行一个特权级别不同的返回,处理器会有如下动作:

  1. 执行权限校验。
  2. 将CS和EIP寄存器恢复到中断或异常之前的值。
  3. 恢复eflags寄存器。
  4. 将SS和ESP寄存器恢复到中断或异常之前的值,从而导致堆栈切换回被中断过程的堆栈。
  5. 恢复被中断的程序。

3.5 中断流程

​ 那既然发生了中断,那intel 开发手册是如何规定的中断运转流程呢?看下下面这张图。

在这里插入图片描述

CPU首先正在运行当前程序,中断检测周期定期通知,CPU当执行完一个原子性的操作后,然后根据中断检测周期来的提醒,检测CPUINTR阵脚,和IF标志位(后面会说),进行决定是否运行中断程序,当产生中断后,会有中断程序被执行,那这些程序是如何产生的?又怎么找到的呢?

intel 开发手册规定了18个预定义的中断和异常,224用户定义的中断,这些预定义的程序的入口在IDT表里里。在IDT里的每个中断和异常被一个向量数字所标识。

目录位置:🐯volume 1 - 6.5 INTERRUPTS AND EXCEPTIONS

在这里插入图片描述

​ 有CPUOS自己定义的中断程序,通过IDT来寻找对应的程序,简简单单的一句话,解决了2个疑问。那下来我来介绍下如何执行中断流程代码的步骤

3.6 定位中断程序

​ 这里会涉及很多概念,如GDTSegmenet SelectorSegment 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 中断流程步骤总结

来梳理下流程:

  1. 首先8259A芯片会产生一个中断标志,然后阵脚INT置为高电频,对应的中断向量位置置为高电频。或者int指令进行中断
  2. 中断检测周期发现后,将CPU的INTR置为1,当CPU执行完最后一个指令(IF,ID,EXEC,MEM,WB)时,开始判断是否可以中断
  3. 检查eflagsif标志位,是否是被清除的,是的话,不能中断
  4. CPU和8259A芯片连接的数据总线,解码获取到中断向量(与就是中断的程序位置)
  5. 获取到后,和IDTR(base addr + offset)找到对应IDT表,专表专用,通过中断向量找到对应的IDT描述符
  6. 进行特权级检查,是否发生栈切换,(CPL和中断程序的dpl判断),然后压入对应的SS,SP,ELFAGS,CS,IP,ERROR CODE等信息
  7. 通过中断描述符段选择子找到对应的GDT里对应的各种段信息
  8. 然后开始执行code segment中断程序
  9. 执行后,通过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,%%eax
              pushl $0x17                  // ss
              pushl %%eax                  // esp
              pushfl                       // eflags
              pushl $0x0f                  // cs
              pushl $1f                    // eip
              iret                         // Interrupt return 

             1:      movl $0x17,%%eax   // 
                      movw %%ax,%%ds    // 数据段
                      movw %%ax,%%es    // 扩展段
                      movw %%ax,%%fs    // fs
                      movw %%ax,%%gs    // gs
              ::: // 无输出,无输入
             "ax" // 保存到eax里
)

那为什么ss0x17,这个也是对着手册中的段选择子的规则写的,那我们来看下

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 4096

union 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 state
				   15,			// long counter
				   15, 		    // long priority
					
/* signals */		0,			 // long signal
				   {{},},		// struct sigaction sigaction[32];
				   0, 			// long blocked /* bitmap of masked signals */
					
/* ec,brk... */		0,			 // exit_code
				   0,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 umask
					NULL,		// 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,那gdtldt是放在哪的呢?还有tss进程的上下文信息的地址在gdt是如何排布的呢?

4.3 TSSLDTGDT表排布

那既然涉及到排布,其实也就是初始化地址的排布,那就在主入口里

void main(void)
{			
	// 前面代码省略....
	sched_init();					// 初始化0号进程的tss,ldt描述符信息
    // 后面代码省略....
}

那我们看看这个代码的实现,这里就进行了ldttss描述符的地址初始化,那我们看看是如何实现的

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手册进行设置的。博主也粘贴了过来,也依次标注了。

TSSLDT是通过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 在gdtidtldt表项中第一个地址是不防值的

所以可以看到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

Type0010

就可得出是栈段

在这里插入图片描述

这也得到了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源码来讲软件中断。(这里主要讲的是内核态到用户态,用户态到内核态需要系统调用,也就是所谓的软件中断)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/956687.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SparkSQL数据模型综合实践

文章目录 1. 实战概述2. 实战步骤2.1 创建数据集2.2 创建数据模型对象2.2.1 创建常量2.2.2 创建加载数据方法2.2.3 创建过滤年龄方法2.2.4 创建平均薪水方法2.2.5 创建主方法2.2.6 查看完整代码 2.3 运行程序&#xff0c;查看结果 3. 实战小结 1. 实战概述 在本次实战中&#…

通过电光晶体的电光效应,实现白光干涉中的电光调制相移原理

通过电光晶体的电光效应&#xff0c;实现白光干涉中的电光调制相移原理&#xff0c;是一个基于物理光学和电光学原理的高级测量技术。以下是对这一原理的详细解释&#xff1a; 一、电光效应与电光晶体 电光效应是指某些材料&#xff08;主要是晶体&#xff09;在外加电场的作…

基于Python的多元医疗知识图谱构建与应用研究(上)

一、引言 1.1 研究背景与意义 在当今数智化时代,医疗数据呈爆发式增长,如何高效管理和利用这些数据,成为提升医疗服务质量的关键。传统医疗数据管理方式存在数据孤岛、信息整合困难等问题,难以满足现代医疗对精准诊断和个性化治疗的需求。知识图谱作为一种知识表示和管理…

logback日志自定义占位符

前言 在大型系统运维中&#xff0c;很大程度上是需要依赖日志的。在java大型web工程中&#xff0c;一般都会使用slf4jlogback这一个组合来实现日志的管理。 logback中很多现成的占位符可以可以直接使用&#xff0c;比如线程号【%t】、时间【%d】、日志等级【%p】&#xff0c;…

嵌入式知识点总结 C/C++ 专题提升(一)-关键字

针对于嵌入式软件杂乱的知识点总结起来&#xff0c;提供给读者学习复习对下述内容的强化。 目录 1.C语言宏中"#“和"##"的用法 1.1.(#)字符串化操作符 1.2.(##)符号连接操作符 2.关键字volatile有什么含意?并举出三个不同的例子? 2.1.并行设备的硬件寄存…

嵌入式Linux驱动开发之platform

关键词&#xff1a;rk3399 嵌入式驱动 Linux platform 前言 前面的嵌入式Linux驱动都是描述从特定的SOC与特定设备之间的直接两两通信。而Linux不是为单一某一SOC结构而设计的操作系统&#xff0c;它可以运行在X86、ARM等多种架构多种SOC平台上&#xff0c;如果驱动程序按照S…

KubeSphere部署安装,接入KubeKey安装的k8s集群

KubeSphere安装接入KubeKey安装的k8s集群 文章目录 KubeSphere安装接入KubeKey安装的k8s集群 一.NFS安装配置1.服务器安装NFS服务2.下载并部署 NFS Subdir External Provisioner1).下载部署文件2).创建 NameSpace3).创建 RBAC 资源4).配置 deployment.yaml5).部署 Storage Clas…

从密码学原理与应用新方向到移动身份认证与实践

相关学习资料放下面啦&#xff01; 记得关注❤️&#xff5e;后续分享更多资料 通过百度网盘分享的文件&#xff1a;从密码学原理与应... 链接https://pan.baidu.com/s/1mHpHkvPuf8DUwReQkoYQlw?pwdGza7 提取码&#xff1a;Gza7 复制这段内容打开「百度网盘APP 即可获取」 记…

Java 特殊文件、 properties文件、xml文件

一. 属性文件.properties 1. #注释 2. 内容都是一些键值对信息&#xff0c;每行都是一个键值对&#xff1b;键不能重复&#xff1b; 3. 属性文件的后缀一般都是properties结尾 4. 使用程序读取properties属性文件里面的数据 (1) Properties&#xff1a;是一个Map集合(键值对集合…

抽象设计如何提升用户体验?

抽象设计在网页设计中可以通过多种方式提升用户体验&#xff0c;以下是具体的应用和作用&#xff1a; 一、增强视觉吸引力 视觉冲击力&#xff1a;抽象元素往往具有强烈的视觉冲击力&#xff0c;能够迅速吸引用户的注意力。通过大胆的色彩、不寻常的形状和丰富的纹理&#xff…

MATLAB中while循环例子,for循环嵌套例子

while循环例子 for循环解决斐波那契数列 for循环嵌套例子 注意最后都有 e n d end end

行人识别检测数据集,yolo格式,PASICAL VOC XML,COCO JSON,darknet等格式的标注都支持,准确识别率可达99.5%

作者简介&#xff1a; 高科&#xff0c;先后在 IBM PlatformComputing从事网格计算&#xff0c;淘米网&#xff0c;网易从事游戏服务器开发&#xff0c;拥有丰富的C&#xff0c;go等语言开发经验&#xff0c;mysql&#xff0c;mongo&#xff0c;redis等数据库&#xff0c;设计模…

【vitePress】基于github快速添加评论功能(giscus)

一.添加评论插件 使用giscus来做vitepress 的评论模块&#xff0c;使用也非常的简单&#xff0c;具体可以参考&#xff1a;giscus 文档&#xff0c;首先安装giscus npm i giscus/vue 二.giscus操作 打开giscus 文档&#xff0c;如下图所示&#xff0c;填入你的 github 用户…

JAVA使用自定义注解,在项目中实现EXCEL文件的导出

首先定义一个注解 Retention(RetentionPolicy.RUNTIME) Target(ElementType.FIELD) public interface Excel {/*** 导出时在excel中排序*/int sort() default Integer.MAX_VALUE;/*** 导出到Excel中的名字.*/String name() default "";/*** 首行字段的批注*/String …

有限元分析学习——Anasys Workbanch第一阶段笔记(14)静定与超静定问题、约束类型介绍、简支梁挠度求解和自定义材料库建立

目录 0 序言 1 静定与超静定问题 2 Workbranch中Supports介绍 3 简支梁挠度的有限元求解 4 自定义材料库建立 0 序言 静定与超静定问题、约束类型介绍、简支梁挠度求解和自定义材料库建立(内容对应视频22到24课)。 1 静定与超静定问题 在有限元分析中&#xff0c;不同的…

领域算法 - 大数据处理

大数据处理 文章目录 大数据处理一&#xff1a;hash分流二&#xff1a;双层桶1&#xff1a;什么是双层桶2&#xff1a;双层桶案例 三&#xff1a;外排序1&#xff1a;经典问题2&#xff1a;位图排序法3&#xff1a;多路归并排序 四&#xff1a;bitMap1&#xff1a;添加 -> 异…

以太网实战AD采集上传上位机——FPGA学习笔记27

一、设计目标 使用FPGA实现AD模块驱动采集模拟电压&#xff0c;通过以太网上传到电脑上位机。 二、框架设计 数据位宽转换模块&#xff08;ad_10bit_to_16bit&#xff09;&#xff1a;为了方便数据传输&#xff0c;数据位宽转换模块实现了将十位的 AD 数据转换成十六位&#…

JavaWeb 快速入门 javaScript(预测爆文) | 019

今日推荐语 人经常推翻自己&#xff0c;经常不同意昨天的自己&#xff0c;这也是常态。——纪静蓉 日期 学习内容 打卡编号2025年01月20日JavaWeb快速入门javaScript019 前言 哈喽&#xff0c;我是菜鸟阿康。 今天大概学习了下 js 的的基础知识&#xff0c;js …

[c语言日寄]内存初阶:大端字节序和小端字节序

【作者主页】siy2333 【专栏介绍】⌈c语言日寄⌋&#xff1a;这是一个专注于C语言刷题的专栏&#xff0c;精选题目&#xff0c;搭配详细题解、拓展算法。从基础语法到复杂算法&#xff0c;题目涉及的知识点全面覆盖&#xff0c;助力你系统提升。无论你是初学者&#xff0c;还是…

【MySQL】数据库-图书管理系统(CC++实现)

一.预期功能 该图书管理系统设计提供基本的设计模版&#xff0c;涉及数据库的增删查改等操作&#xff0c;包含登录功能&#xff0c;图书管理功能&#xff0c;图书借阅功能&#xff0c;用户管理功能等基础功能&#xff0c;详细功能查看以下菜单表&#xff0c;共包含三个菜单&am…