完成一个简单的时间片轮转多道程序内核代码
王康 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
分别是1 存储程序计算机工作模型,cpu执行程序的基础流程;
2 函数调用堆栈:各种寄存器和存储主要是为了指令的传取值,通过eip,esp,eax,ebp和程序内存的分区,搭配push pop call return leave等一系列指令完成函数调用操作。
3 中断:多道批程序!
在复习一下上一讲的几个重要指令:
先leave再ret(恢复栈帧后恢复指令序列)
call压栈指令序列
enter用于进入函数时候,压入上一个ebp,同时两者指向新的栈帧开头(所以都是先call然后进入调用函数内部再保存上一个ebp)
1,函数调用堆栈
甚至提供了leave enter来简洁指令
3:用eax保存返回值,如果eax返回是个内存地址,内存地址可以指向返回数据(引用)
编译器不同,可能反汇编有些差异(不同指令序列可以实现相同功能)
所以仍需分析一下函数调用堆栈机制:
程序比较简单,cs代码段就是相同的,所以可省略这一部分。
以上都是连续的指令流,但如何从一个进程此程序如何调到另一个进程程序?
中断,cpu内部工作(不仅是把esp ebp压栈)
深入理解函数调用堆栈的工作机制:
前边加入enter建立堆栈框架,和leave ret拆除。搭配call和ret完成一次调用执行流。
退出:mov ebp,esp清除栈,pop ebp恢复原来状态
举例分析:
调用call之前,push了两个。为什么不直接push y反而变址寻址0Xfffffff8(%ebp)呢?
因为建立当前栈框架时候就把局部变量放到了内部,就可以用一个变址寻址找到。
call之后发现ebp和x之间隔了一个cs:ip的值所以是+8,+c
最后因为当前参数还在栈框架中,所以之前准备的要清理掉
保存返回值用eax放到利用变址寻址找到的局部变量z中。
可能跟printf右结合顺序有关,所以先push y后x,z
最后,调用printf机制相同。
局部变量的变址寻址,
sub $0x18,esp在堆栈中预留一段空间来分配局部变量(这也是为什么c变量声明要放在头部,这样可以直接全部放入;现在编译器优化后可以扫描整个直接放入,所以不强调局部变量必须放在头部)
char c=’a’是把ASCII码放入
总结:
1 从main开始执行,系统自动建立的main堆栈;
2 调用p1时候:
3,p1 return回到初始状态;执行p2同p1,之后p2 ret就整个回到main中
(同时每次会把eax结果赋值给局部变量)
三级调用:
逐级压入,逐级退出
2,借助linux内核部分源代码模拟存储程序计算机工作模型及时钟中断
之前介绍了x86汇编和函数调用堆栈,现在利用mykernel实验模拟计算机硬件平台
虚拟一个x86cpu,使用linux源代码把cpu配置好开始执行我们的程序。
中断:中断信号发生时候,cpu和内核代码把当前esp eip ebp压入内核堆栈,然后把eip指向中断处理程序入口,执行中断处理程序!
(本次实验模拟时钟中断:每隔一段时间时钟中断发生一次,进而实现时间片轮转)
3,实验:
qemu把内核加载进去。
系统唯一进程mymain.c和时钟中断myinterrupter.c-------------只要在mymain.c基础上继续写进程描述PCB和进程链表管理等代码,在myinterrupt.c的基础上完成进程切换代码,一个可运行的小OS kernel就完成了。
mymain.c:
之前都是硬件准备的工作,在这里是操作系统启动,执行代码是:
每循环10万次打印一次
myinterrupt.c:
因为linux已经完成获取进入时钟中断代码,我们只要每次中断发生时候实际中断处理动作即可
4,在mykernel基础上构造一个简单的操作系统
嵌入方法语法:
_asm_( .. )相当于c语言代码,整体可以看做一个函数。
输出部分输入部分类似函数堆栈的参数,例如ret是一个输出部分。
%%为了转义字符,结尾\n\t
%1指输出输入部分(第一个输出输入是0开始)即:”c”(val1),
这里c代表ecx寄存器即用ecx存储val1的值。之后把val1 + eax放入eax
%2是edx(val2)
最后放入%0就是val3,”m”就是内存变量而非寄存器;类似(%eax)
在输入输出部分的限定符;
r让编译器自动选择一个通用寄存器放入
= 表示只写的
+ 读写类型
最后eax是破坏描述部分,即这段代码可能破坏eax
分析:1 把eax赋值为0 2 把eax放入m即temp变量 3 再把r即input=1放入eax
4 把eax赋值给output 所以流程是交换了值(中间记得输入输出有自动取局部变量值过程)
构造一个简单内核:实验二
编译如下文的c文件,完成后make再执行一次
代码分析:
mypcb.h定义了进程控制块,实际在linux叫tasks_struct
thread存储eip esp
定义了进程管理相关的数据结构:
char stack[]内核堆栈
task_entry指定入口
调度器
mymain.c:
声明了一个task数组,是否需要调度标志
从init_mykernel开始分析:
0正在运行状态;
入口为my_process,实际是mystartkernel
堆栈栈底是定义的stack。
next还是指向他自己,然后fork很多其他进程
把0号进程状态都copy过来,用自己的堆栈
新fork进程加入进程列表尾部
之后启动0号进程;
之后用了嵌入式汇编:
把thread.sp放入esp,当前栈空直接push esp相当于push ebp
再把ip压栈,ret是把这个ip pop最后popl ebp
0号进程做的工作就是my_process:其他进程也是
1千万次输出1次,
这是一种主动调度机制(即代码中if(my_need_sched == 1)),调度完从my_schedule(0开始继续执行
---另外一种是抢占式调度
myinterrupt.c:如何调度
最后有个时间计数
时钟中断发生1千次,并且标志不为1就设置为1.
my_schedule出错处理,之后是:
把当前进程下一个进程赋值给next,如果当前进程正在执行,就进行进程切换:
保存ebp esp赋值给sp
把下一个进程sp放入esp
pushl 3 :把下一个进程eip push进来
如果进程是新进程还没有执行过,就用else处理
置为运行时状态,作为当前正在执行进程
保存ebp esp,restore eip esp ebp,把当前进程入口保存,ret
对“操作系统是如何工作的”理解:
操作系统内核有一个起始位置,从这个起始位置开始执行。在执行了一些初始化操作,比如进程的状态设置,各个进程的栈的空间的分配后,将CPU分配给第一个进程,开始执行第一个进程,然后通过一定的调度算法度,比如时间片轮转,在一个时间片后,发生中断,第一个进程被阻塞,在完成保存现场后将CPU分配给下一个进程,执行下一个进程。这样,操作系统就完成了基本的进程调度的功能。