1、寄存器
1)每条指令读取后eip自增
2)指令的长度可能不同
3)通过call,ret,jmp等指令可以修改eip的值
此处只简单介绍最主要的几个寄存器。
esp:堆栈顶指针,寄存器中存放栈顶地址。
ebp:堆栈基指针,寄存器中存放栈底地址。
eip:指令指针,寄存器中存放着下一条指令的地址。
eax:累加器。
CS:代码段寄存器。
DS:数据段寄存器。
SS:堆栈段寄存器
2、从C语言到可执行程序
1).c的C语言文件通过gcc编译生成.asm的汇编语言文件
2).asm的汇编文件通过gas汇编生成.o的目标文件(obj文件,可链接)
3)再通过链接生成可执行文件,装载到内存当中进行执行。
3、函数的堆栈框架
函数调用使用call指令。
1)call xxx
在call之前,会先把函数需要的参数压栈。
执行 call时,会将当前的eip压栈,然后eip的值为xxx的入口地址。
2)进入xxx时
pushl %ebp
movl %esp, %ebp
初始化栈堆,进行栈对齐。
3)退出xxx时
movl %ebp,%esp
popl %ebp
ret
恢复调用函数前的现场。
4、用户态和内核态
在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种cpu执行级别就对应着内核态。
而在相应的低执行级别下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动。
eg:intel x86 cpu有4中执行级别0-3,Linux只使用其中的0级和3级分别来表示内核态和用户态。
cs寄存器的最低两位表示了当前代码的特权级
CPU每条指令的读取都是通过cs:eip这两个寄存器,其中cs是代码段选择寄存器,eip是偏移量寄存器。
上述判断由硬件完成。
一般来说在Linux中,地址空间是一个显著的标志:0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问。(此处的地址空间为逻辑地址,而非物理地址。)
5、系统调用
操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用。
意义:把用户从底层的硬件编程中解放出来,极大地提高了系统安全性,使用户程序具有可移植性。
应用编程接口(API)和系统调用是不一样的。API只是一个函数定义,系统调用通过软中断向内核发出一个明确的请求。
Libc库定义的一些封装例程(唯一的目的就是发布系统调用),一般每个系统调用对应一个封装例程,库再用封装例程定义出给用户的API。所以,不是每个API都对应一个特定的系统调用,API可能直接提供用户态的服务,一个单独的API可能调用多个系统调用,不同的API可能调用了同一个系统调用。大部分的封装例程返回一个整数,其值的含义依赖于对应的系统调用,返回值-1一般表示内核不能满足进行的请求。Libc中定义的errno变量包含了特定的出错码。
当用户态进行调用一个系统调用的时候,CPU切换到内核态并开始执行一个内核函数。Linux中是通过执行int $0x80来执行一个系统调用的。这条汇编指令产生向量为128的编译异常。
内核实现了很多不同的系统调用,但是进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数,一般用eax寄存器。
6、进程、轻量级进程、线程和内核线程
CPU(单CPU系统)是一条指令一条指令执行的。
进程是执行上下文、定义一个执行流,有特别权限的进程管理其他进程,也就是分配CPU执行时间。
轻量级进程是为了支持多线程(一个进程中有多个执行流)而使多个进程共享一些资源,从操作系统的角度看没有线程,全部通过进程来分配资源并进行调度管理,但从多线程应用开发者的角度来看,它似乎在同一个进程中创建了多个执行流(线程)。
内核线程并不是线程,而是一个特殊一点的进程,仅工作在内核态,一般是一些服务程序为了避免内核态和用户态切换的开销,而普通进程一般根据需要通过系统调用在内核态和用户态之间反复切换。
为了管理进程,内核必须对每个进程进行清晰的描述。进程描述符提供了内核所需了解的进程信息。
进程描述符:
进程状态转换图:
7、进程的切换:
为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并回复以前挂起的某个进程的执行。
进程上下文,包含用户地址空间(程序代码、数据、用户堆栈等),控制信息(进程描述符、内核堆栈等),硬件上下文(主要是寄存器)。
schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换(prev和next)。
分析switch_to宏,什么时候next进程真正开始执行?
首先在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
然后将prev的内核堆栈指针ebp存入prev->thread.esp中。
把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中
将popl指令所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度
通过jmp指令(而不是call指令)转入一个函数__switch_to()
恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行
当__switch_to正常返回时,发生了什么事情?
切换了内核堆栈和硬件上下文。
__switch_to是怎么传参的?
switch_to(prev,next,last), 可以看到last就是prev
调用方法如下:进程A->进程B,switch_to(A,B,A)主要有三个参数:
输入参数两个:prev:切换前的进程 next:切换后的进程
输出参数一个:last:切换前进程 注意这三个变量都是局部变量,
在系统栈中,所以切换到另一进程后变量的值不会改变。
进程a切换b之前,eax的值为prev,也就是a,edx的值为next,也就是b,ebx的值为prev,也就是a
当不考虑第三个参数时,从c切换成a,内核栈切换成a的栈,这时a中的prev和nexxt
分别指向a和b,进程c的引用丢失了。
这时第三个参数就派上用场了。
c切换进程a后,将c存入eax中,切换到a后,由于输出部"=a" (last)会将eax的值
写入last中,也就是prev中,所以此时prev和next的值就是c和b了。
8、操作系统架构和执行过程
操作系统:内核(进城管理、进程调度、进程间通讯机制、内存管理、中断异常处理、文件系统、I/O系统、网络部分)+其他程序(库函数、shell程序、系统程序等)
操作系统的目的:与硬件交互,管理硬件资源。为用户程序提供一个良好的执行环境。
总结:
通过本次课程的学习,我更加深入的了解Linux系统工作过程,了解了进程的创建与调度,了解了Linux的启动过程,了解了中断是什么,中断是如何发生的以以及cpu相关的操作,文件系统的制作与挂载。由孟老师和李老师深入浅出的讲解,带领我一步步的认识Linux操作系统,多谢老师在疫情期间坚持不懈的讲课,我才能通过老师的讲课得到这么多的收获。