《操作系统》by李治军 | 实验5.pre - switch_to 汇编代码详解

news2024/11/27 10:31:25

目录

【前言】

一、栈帧的处理

1. 什么是栈帧

2. 为什么要处理栈帧

3. 执行 switch_to 前的内核栈

4. 栈帧处理代码分析

二、PCB 的比较

1. 根据 PCB 判断进程切换与否

2. PCB 比较代码分析

三、PCB 的切换

1. 什么是 PCB 的切换

2. PCB 切换代码分析

四、TSS 内核栈指针的重写

1. 为什么要重写 TSS 中的内核栈

2. 内核栈重写代码分析

五、内核栈的切换

1. 如何完成内核栈切换

2. 内核栈切换代码分析

六、LDT 的切换

1. LDT 切换代码分析

七、用户栈的切换

1. switch_to 退出代码分析


【前言】

       在李治军老师的《操作系统》课程的实验 5(基于内核栈切换的进程切换)中,需要完成 switch_to 函数的汇编代码编写。代码很容易获取,网上的资源非常多,但是拿到了看不懂……

       所以本文章将会对 switch_to 的代码进行逐条分析,希望能帮助理解基于内核栈的进程切换的整体流程。

switch_to() 完整汇编代码:

.align 2
switch_to:
    //因为该汇编函数要在c语言中调用,所以要先在汇编中处理栈帧
	pushl %ebp
	movl %esp,%ebp
	pushl %ecx
	pushl %ebx
	pushl %eax

    //将ebp+8指向的数据(目标进程的PCB)传递给ebx,然后进行判断:
    //如果目标进程的pcb <<等于>> 当前进程的pcb => 不需要进行切换,直接退出函数调用
    //如果目标进程的pcb <<不等于>> 当前进程的pcb => 需要进行切换,直接跳到下面去执行
	movl 8(%ebp),%ebx
	cmpl %ebx,current
	je 1f

    /** 执行到此处,就要进行真正的基于堆栈的进程切换了 **/
	
    // 切换PCB
	movl %ebx,%eax
	xchgl %eax,current
	
	// 重写TSS中内核栈的指针
	movl tss,%ecx
	addl $4096,%ebx
	movl %ebx,ESP0(%ecx)

	// 切换内核栈
	movl %esp,KERNEL_STACK(%eax)
	movl 8(%ebp),%ebx
	movl KERNEL_STACK(%ebx),%esp

	// 切换LDT
	movl 12(%ebp),%ecx
	lldt %cx
    // 切换 LDT 之后
	movl $0x17,%ecx
	mov %cx,%fs
	
    // 这一段先不用管
	cmpl %eax,last_task_used_math
	jne 1f
	clts
	
	// 现在进入新进程的内核栈工作了,所以接下来做的四次弹栈以及ret处理使用的都是新进程内核栈中的东西
1:	popl %eax
	popl %ebx
	popl %ecx
	popl %ebp
	ret

一、栈帧的处理

1. 什么是栈帧

       大多数 CPU 上的程序实现都是通过使用来支持函数调用操作。栈被用来传递函数参数、存储返回地址、临时保存寄存器原有值以备恢复以及用来存储局部数据。单个函数调用操作所使用的栈部分被称为栈帧结构。
       栈帧结构的两端由两个指针来指定:① ebp:用作指针(指向栈帧部);② esp:用作指针(指向栈帧部)。在函数执行过程中,肯定会有数据的入栈和出栈,而栈指针 esp 就会随之移动。因此,函数中对大部分数据的访问都是基于帧指针 ebp 进行的。

>> 强烈推荐先看看这篇文章再继续:栈帧_yxysdcl的博客-CSDN博客

2. 为什么要处理栈帧

       现在我们知道,每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中保存着该函数所需要的各种信息。寄存器 ebp 指向当前栈帧的底部,寄存器 esp 指向当前栈帧的顶部。

       当调用一个函数时,就意味着要创建一个属于这个函数自己的栈帧,而进入该函数后这个栈帧就变成了当前栈帧,所以要让 ebp 指向这个栈帧的底部,让 esp 指向这个栈帧的顶部,同时还要保存上一个函数的栈帧底部和栈帧顶部。

3. 执行 switch_to 前的内核栈

       现在我们应该清楚,在执行上面的 switch_to 汇编代码前,当前进程内核栈的情况应该如下图所示(我们以在 schedule() 中调用 switch_to 为例),此时还没有进入 switch_to 函数,所以 ebp 和 esp 应该分别指向 schedule 函数的栈帧底部和栈帧顶部。

4. 栈帧处理代码分析

接下来我们逐条分析 switch_to 中栈帧处理的部分。

pushl %ebp

>> 将 ebp 入栈 <<

       这条指令的目的就是保存 schedule() 的栈帧底部。因为现在进入了 switch_to 函数,也就是说要有 switch_to 自己的栈帧了,所以必须把上面函数的栈帧底部和栈帧顶部保存起来,之后才能修改 ebp 和 esp 的指向。这里不用另外保存栈顶,因为上一个栈帧的顶部就是下一个的栈帧的底部(两栈帧相邻)。ebp 入栈后内核栈变为:(esp 会随着入栈/出栈自动变化)

movl %esp,%ebp

>> 将 esp 中内容传递给 ebp <<

       原来 ebp 指向 schedule 函数的栈帧底部,但这句代码执行完后,ebp 和 esp 就都指向刚刚压入的 ebp 位置,也就是 switch_to 函数的栈帧底部。

pushl %ecx

pushl %ebx

pushl %eax

>> 将 ecx、ebx、eax 依次入栈 <<

       入栈后内核栈如下图所示。可以看到现在 ebp 指向 switch_to() 栈帧底部,esp 指向 switch_to()栈帧顶部,而且上一个函数 schedule 的栈帧底部指针就保存在调用的 switch_to 函数的栈帧底部位置,之后 switch_to 结束时就要通过这个 ebp 返回 schedule。现在栈帧就处理完毕了!

二、PCB 的比较

1. 根据 PCB 判断进程切换与否

① 目标进程的 PCB  =  当前进程的 PCB  =>  无需进行切换,直接退出函数调用

② 目标进程的 PCB    当前进程的 PCB  =>  需要进行切换,接着进行切换操作

2. PCB 比较代码分析

movl 8(%ebp),%ebx

>> 将 ebp 指针 + 8 指向的数据传递给了 ebx 寄存器 <<

       Linux 0.11 内核栈的地址顺序从上往下,是由高到低的。所以 ebp + 8 指向的就是 pnext(目标进程的 PCB ),所以 ebx 现在就存储着 pnext。

cmpl %ebx,current
je 1f

>> 比较 ebx 中的内容和 current <<

ebx 中保存着目标进程的 PCB,current 是当前进程的 PCB。

① 如果两个进程的 PCB 相同,则跳转到 1f 位置处,switch_to 接下来的代码也不用执行,不会进行进程切换。

② 如果两个进程的 PCB 不同,则继续执行 switch_to 接下来的代码进行进程切换。

三、PCB 的切换

1. 什么是 PCB 的切换

       切换 PCB 就是要让 current 切换为目标进程的 PCB。

2. PCB 切换代码分析

movl %ebx,%eax

xchgl %eax,current

>> 将 ebx 中数据置给 eax ,再交换 eax 和 current 的内容 <<

       执行前 ebx 中保存着目标进程的 PCB,current 是当前进程的 PCB。这两句代码执行后,ebx 和 current 都指向目标进程的 PCB,eax 则指向当前进程的 PCB,切换完成!

四、TSS 内核栈指针的重写

1. 为什么要重写 TSS 中的内核栈

       执行 INT 0x80 中断之后,进程进入内核,要先找到内核栈的位置。而系统需要根据一些硬件寄存器(TR)知道这个哪个进程,以及该进程对应的内核栈在哪里。同时还会将用户态下的 SS:ESP、CS:EIP、EFLAGS 都压入内核栈中保存下来。

       也就是说,要先找到当前进程的内核栈,才能从用户栈切换到内核栈。而找到内核栈还得依靠 TR 指向的当前 TSS。虽然此时不再使用 TSS 进行进程切换,但是 Intel 的中断处理机制还是要保持,因为中断机制就是通过 TR 指向的 TSS 来找到当前进程的内核栈,并自动将用户栈等相关信息压入对应内核栈。所以每个进程仍然需要一个TSS,这样系统才能并通过 TSS 中的内核栈指针 esp0 找到当前进程的内核栈。

       这里采用的方案是让所有进程共用一个TSS(即 0 号进程的TSS),并且这个 TSS 指向当前进程。在 sched.c 中定义的全局变量  struct tss_struct *tss = &(init_task.task.tss);  就是 0 号进程的 TSS,所有的进程都共用这个 TSS,任务切换时再发生变化。

这个唯一的 TSS 的目的就是:在中断处理时,帮助 CPU 找到当前进程的内核栈的位置

2. 内核栈重写代码分析

movl tss,%ecx

>> 将 tss(当前进程的 TSS)赋给 ecx 寄存器 <<

所以现在 ecx 也保存了当前进程的 TSS。

addl $4096,%ebx

>> ebx + 4096 <<

       ebx 本来指向目标进程的 PCB,执行该指令后,ebx 就指向目标进程的内核栈。Linux 0.11 中进程的 PCB内核栈同一页内存上(即一块 4KB 大小的内存)。其中 PCB 位于这页内存的低地址,内核栈位于这页内存的高地址。也就是说,低地址空间 base 用来存放进程的 PCB,而 base + PAGE_SIZE 则作为该进程的内核栈的栈底。

       为什么偏移量是 4096 ?因为 4096 = 4KB = 一页内存大小,所以 ebx 加 4096 就可以得到内核栈的地址。

movl %ebx,ESP0(%ecx)

>> 将 ebx 中内容(目标进程的内核栈地址)复制到 ecx + ESP0 指向的位置 <<

       ecx 指向当前进程的 TSS,而 ESP0 = 4。我们再看 tss_struct 的定义,发现偏移为 4 的地方就是 TSS 中的内核栈指针 esp0,所以 ecx + ESP0 对应位置就是 tss 中的内核栈指针 esp0。将目标进程的内核栈地址赋给 esp0,实现了 tss 的重写。

五、内核栈的切换

1. 如何完成内核栈切换

       上一步只是修改了 tss 中的内核栈指针 esp0,帮助 CPU 找到当前进程的内核栈以进行相关操作,但并没有实际切换内核栈,因为 esp 还是指向的当前进程的内核栈栈顶。

       完成内核栈的切换非常简单,就是将寄存器 esp(内核栈使用到当前情况时的栈顶位置)的值保存到当前进程 PCB 中的对应位置,再从目标进程 PCB 中的对应位置取出保存的内核栈栈顶放入 esp 寄存器中。这样处理完后,再通过 esp 使用内核栈时使用的就是目标进程的内核栈了。

2. 内核栈切换代码分析

movl %esp,KERNEL_STACK(%eax)

>> 将 esp 中内容保存到 eax + KERNEL_STACK 位置 <<

       eax 指向当前进程的 PCB(如果忘了可以返回 “PCB 的切换” 去看看),而 KERNEL_STACK 的数值没有明确定义,因为 Linux 0.11 中 PCB 的定义里并没有保存内核栈指针这个域(kernelstack),所以需要我们自己找位置加上这个定义,而宏 KERNEL_STACK 就是我们添加的那个位置。添加位置不同,KERNEL_STACK 的值也会不同。

       但是不管 KERNEL_STACK 的值是什么,eax + KERNEL_STACK 就是当前进程的 PCB 中对应存储内核栈指针的位置。所以这条指令实现了将寄存器 esp(内核栈使用到当前情况时的栈顶位置)的值保存到当前进程 PCB 中的对应位置。

movl 8(%ebp),%ebx

>> 将 ebp 指针 + 8 指向的数据传递给了 ebx 寄存器 <<

ebp + 8 指向的还是 pnext(目标进程的 PCB ),所以现在 ebx 存储着目标进程的 PCB。

movl KERNEL_STACK(%ebx),%esp

>> 将 ebx + KERNEL_STACK 位置的内容保存到 esp 中 <<

       ebx + KERNEL_STACK 位置的内容就是目标进程的 PCB 中对应存储内核栈指针的位置,所以这条指令实现了从目标进程 PCB 中的对应位置取出保存好的内核栈栈顶放入 esp 寄存器中,现在的 esp 就指向新进程的内核栈栈顶了。到此正式完成了内核栈的切换!

六、LDT 的切换

1. LDT 切换代码分析

movl 12(%ebp), %ecx

>> 将 ebp 指针 + 12 指向的数据传递给了 ecx 寄存器 <<

       当前内核栈中数据如下图所示,注意现在 ebp 指向的还是原来进程的内核栈,但 esp 指向的是新进程的内核栈。所以 ebp + 12 指向的就是 _LDT(next)(目标进程的 LDT ),这条指令就是负责取出对应 LDT(next) 的那个参数,这里暂时不用深入理解。

lldt %cx

       这条指令负责修改 LDTR 寄存器。一旦完成了修改,下一个进程在执行用户态程序时使用的映射表就是自己的 LDT 表了,地址空间就实现了分离。

movl $0x17,%ecx

mov %cx,%fs

       这两条指令在 LDT 切换完成之后,作用是重新取一下段寄存器 fs 的值。这两句指令必须要加、且必须出现在切换完 LDT 之后。因为 fs 的作用——通过 fs 访问进程的用户态内存,而 LDT 切换完成就意味着切换了分配给进程的用户态内存地址空间,所以前一个 fs 指向的是上一个进程的用户态内存,而现在需要执行下一个进程的用户态内存,所以需要用这两条指令来重取 fs。

七、用户栈的切换

1. switch_to 退出代码分析

cmpl %eax,last_task_used_math
jne 1f
clts

       在 PCB、内核栈和 LDT 切换完成之后还有上面这段代码,我们暂时忽略不管,直接来到最后的 4 条出栈指令和 ret :

popl %eax
popl %ebx
popl %ecx
popl %ebp
ret

上面的指令执行前内核栈情况如图所示:

       可以看出进程 A 和进程 B 的内核栈整体结构是差不多的。因为任何一个进程进入内核,用户态下的 SS:ESP、CS:EIP、EFLAGS 都会被自动压入内核栈中,所以内核栈栈底的内容都是该进程用户态下的相关信息(SS ~ CS)。而进程需要切换时通过 schedule() 进行调度找到目标进程,schedule() 退出前又调用了 switch_to 进行进程切换,这段过程对每个进程都是一样。所以从调用 switch_to 开始,_LDT(next) ~ eax 就依次入栈,每个进程都如此。

       回到这 5 条指令,此时已经切换了PCB、内核栈、LDT,剩用户栈还没有切换。esp 指向新进程的内核栈栈顶,但 ebp 还指向原来进程的内核栈。经过前 3 条出栈指令后,内核栈中情况如图所示:

popl %eax
popl %ebx
popl %ecx

       此时 esp 指向的当前进程内核栈栈顶的 ebp 是什么?就是当前进程——进程 B 上一次调用 schedule() 时 schedule 的栈帧底部!所以现在继续执行下一条指令:

popl %ebp

       执行后内核栈情况如图所示,也就是回到了进程 B 停止时调用 schedule 时的状态。

现在执行最后一条指令:

ret

       这条指令就是 switch_to 的返回指令,执行之后就会弹出 schedule() 的  }  并执行,而这个 是 schedule() 的返回指令。之后在内核中运行一段代码后,就会退出内核(因为系统调用只是进入内核溜达一圈)。进入内核通过 INT 0x80,退出内核就通过 iret,这 iret 指令就实现用户栈的切换。iret 会执行一系列的操作,其中源 CS ~ 源 SS 也会出栈,而 SS、SP 指向用户栈,就切换到了进程 B 的用户栈,接下来就从进程 B 停止时的位置开始继续往下执行。至此就完成了进程 A 到进程 B 的全部切换。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ChatGPT再起争端,如何应对未来的机器挑战?速来学习解决方法!

AI孙燕姿事件 前些日子&#xff0c;“AI孙燕姿”一度火爆出圈。其实就是有网友使用人工智能技术&#xff0c;利用神似歌星孙燕姿的声音翻唱了不少歌手的代表作&#xff0c;并上传到社交平台上&#xff0c;而其翻唱作品的逼真程度是“甚至换气声都可以模仿”。随着“AI孙燕姿”翻…

你还不会AVL树吗?

AVL树 AVL树概念AVL树的插入结点定义插入流程左单旋右单旋左右双旋右左双旋 验证AVL树 AVL树概念 &#x1f680;AVL树是一颗平衡的二叉搜索树&#xff0c;所谓平衡是指左右子树的高度差的绝对值不超过1。所以一颗AVL树&#xff08;如果不是空树&#xff09;有以下性质&#xf…

2023 年的 Web Worker 项目实践

目录 前言 引入 Web Worker Worker 实践 Worker 到底有多难用 类库调研 有类库加持的 worker 现状 向着舒适无感的 worker 编写前进 1. 抽取依赖&#xff0c;管理编译和更新&#xff1a; 2. 定义公共调用函数&#xff0c;引入所打包的依赖并串联流程&#xff1a; 3. …

两种方法绘制笑脸(需要用到canvas标签)

两种方法绘制笑脸&#xff08;需要用到canvas标签&#xff09; 方法一&#xff1a; <!DOCTYPE html> <html><head lang"en"><meta charset"utf-8"><title>绘制笑脸-方法一</title></head><body><canv…

二开项目权限应用全流程

二开项目-权限应用全流程(人力资源类) addRoutes基本使用** 格式 **router.addRoutes([路由配置对象])**或者:this.$router.addRoutes([路由配置对象]) 改造代码 1 .在router/index.js中的路由配置中删除动态路由的部分 export const constantRoutes [{path: /login,comp…

【ChatGPT】如何入门GPT并快速follow当前的大语言模型LLM进展?

如何入门GPT并快速follow当前的大语言模型LLM进展? 自从去年chatGPT悄悄发布,OpenAI发布的GPT系列工作也变得炙手可热,而基于此,各家公司/实验室百家争鸣,纷纷发布自己的工作,可以说每天都有新的进展。 在当前的情况下,要如何入门GPT系列生成模型,并快速跟进SOTA进展…

SpringMVC一站式学习,分分钟钟让你上手

文章目录 一、SpringMVC1.1 引言1.2 MVC架构1.2.1 概念1.2.2 好处 二、开发流程2.1 导入依赖2.2 配置核心(前端)控制器2.3 后端控制器2.4 配置文件2.5 访问 三、接收请求参数3.1 基本类型参数3.2 实体收参【重点】3.3 数组收参3.4 集合收参 【了解】3.5 路径参数3.6 中文乱码 四…

突破障碍:数字化如何改变对外劳务行业

有没有一份工作是又高薪又能学英语又能环游世界&#xff1f;在小红书上一搜&#xff0c;就发现许多年轻人曾经有过“国际邮轮”工作的经历&#xff0c;打卡全球100城市、全方面的英文口语环境、一觉起来就是一个新的国家...而且还能赚钱&#xff1f;听上去真是令人向往的生活&a…

PoseiSwap 参赛,参与斯坦福、Nautilus Chain等联合主办的 Hackathon 活动

近日&#xff0c;由 Stanford Blockchain Accelerator、Zebec Protocol、 Nautilus Chain、Rootz Lab 共同主办的“ Boundless Hackathon Stanford ” 主题的黑客松活动&#xff0c;目前已接受报名。该活动旨在帮助更多的优质开发者参与到 Web3 世界的发展中&#xff0c;推动链…

自动化测试框架、Python面向对象以及POM设计模型简介

目录 1 自动化测试框架概述 2 自动化测试框架需要的环境 3 自动化测试框架设计思想&#xff1a;Python面向对象 4 自动化测试框架设计思想&#xff1a;POM&#xff08;Page Object Model&#xff09;页面对象模型 1 自动化测试框架概述 所谓的框架其实就是一个解决问题…

如何在华为OD机试中获得满分?Java实现【去除多余空格】一文详解!

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Java华为OD机试真题(2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述4. Java算法源码5. 测试6.解题思路1. 题目描述 去除文本多余空格,但不…

MOTOTRBO CPS2.0安装与写频流程

一、安装MOTOTRBO CPS2.0写频软件 安装MOTOTRBO CPS2.0写频软件&#xff0c;选择安装软件的电脑系统必须WIN7以上 1.解压CPS2_2.21.61.0.zip至当前文件内 2. 双击MOTOTRBO_CPS_2.0.exe安装文件 3. 选择安装语言中文&#xff08;简体&#xff09;&#xff0c;点击确定 4.点击下一…

SW质量属性

1. 覆盖质量属性&#xff1a;指派质量、质量中心和惯性张量的值以覆写所计算的值。 2. 质量属性内容&#xff1a; 密度质量体积曲面区域质量中心惯性主轴惯性矩和产品准则 在图形区域中&#xff0c;单色三重轴指示了模型的主轴和质量中心。 三色参考 3D 三重轴将显示在原点 …

Windows10如何快速安装虚拟机! Hyper-V

您可以在 Windows 10 上使用 Hyper-V 来创建虚拟机。Hyper-V 是 Microsoft 提供的虚拟化软件。您可以按照以下步骤在 Windows 10 上安装 Hyper-V 虚拟机&#xff1a; 1. 打开 Hyper-V 管理器&#xff0c;您可以按下 Windows 键并键入 “Hyper-V 管理器”。如果没有Hyper-V这个选…

汇编寄存器认识

1.8086CPU的16个寄存器: 8086CPU所有寄存器都16位: 通用寄存器: 存放一般性数据: 包括 数据寄存器 , 指针寄存器, 索引寄存器 (AX,BX,CX,DX,BP,SP,SI,DI) 数据寄存器: AX,BX,CX,DX AX: AX(Accumulator Register) &#xff1a;累加寄存器&#xff0c;主要用于输入/输出和大…

【JavaSE】Java基础语法(十七)

文章目录 1. final2. 代码块2.1 代码块概述2.2 代码块分类 1. final fianl关键字的作用 final代表最终的意思&#xff0c;可以修饰成员方法&#xff0c;成员变量&#xff0c;类 final修饰类、方法、变量的效果 fianl修饰类&#xff1a;该类不能被继承&#xff08;不能有子类&a…

【EHub_tx1_A200】Ubuntu18.04 + ROS-Melodic/ROS2-Elequent + 速腾 RS-Helios_16P雷达 评测

大家好&#xff0c;我是虎哥&#xff0c;之前使用了很多单线激光雷达&#xff0c;这几年&#xff0c;3D激光雷达国产化后&#xff0c;逐步已经降价很多&#xff0c;3D激光雷达对于大环境导航&#xff0c;无人驾驶辅助导航&#xff0c;都有很多优势。经过逐步的筛选&#xff0c;…

性能测试知多少---性能分析与调优的原理

最近一直纠结性能分析与调优如何下手&#xff0c;先从硬件开始&#xff0c;还是先从代码或数据库。从操作系统&#xff08;CPU调度&#xff0c;内存管理&#xff0c;进程调度&#xff0c;磁盘I/O&#xff09;、网络、协议&#xff08;HTTP&#xff0c; TCP/IP &#xff09;&…

JavaEE——自主实现计时器

文章目录 一、认识定时器二、自主实现定时器1.明确定时器的内核原理2.定时器框架搭建3.优先级队列中的比较问题4.“忙等”问题5. 代码中随机调度的问题 三、整体代码罗列 一、认识定时器 什么是定时器 定时器是我们在日常的软件开发中很重要的一个组件。类似于闹钟&#xff0c…

毫米波雷达数据采集

目录 1.数据采集方式2.分析数据格式3. 解读原始数据4.Bin文件格式 1.数据采集方式 数据采集有两种方式&#xff1a; 方式一&#xff1a;使用SDK中包含的Capture Demo&#xff1a; how to save raw data from the Capture Demo using Code Composer Studio(CCS) 在CCS中通过…