完成一个简单的时间片轮转多道程序内核代码

王康 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

wps6539.tmp

分别是1 存储程序计算机工作模型,cpu执行程序的基础流程;

2 函数调用堆栈:各种寄存器和存储主要是为了指令的传取值,通过eip,esp,eax,ebp和程序内存的分区,搭配push pop call return leave等一系列指令完成函数调用操作。

3 中断:多道批程序!

在复习一下上一讲的几个重要指令:

先leave再ret(恢复栈帧后恢复指令序列)

call压栈指令序列

enter用于进入函数时候,压入上一个ebp,同时两者指向新的栈帧开头(所以都是先call然后进入调用函数内部再保存上一个ebp)

wps6549.tmp

1,函数调用堆栈

wps6579.tmp

甚至提供了leave enter来简洁指令

3:用eax保存返回值,如果eax返回是个内存地址,内存地址可以指向返回数据(引用)

编译器不同,可能反汇编有些差异(不同指令序列可以实现相同功能)

所以仍需分析一下函数调用堆栈机制:

wps657A.tmp

wps657B.tmp

程序比较简单,cs代码段就是相同的,所以可省略这一部分。

以上都是连续的指令流,但如何从一个进程此程序如何调到另一个进程程序?

中断,cpu内部工作(不仅是把esp ebp压栈)

深入理解函数调用堆栈的工作机制:

wps658C.tmp

前边加入enter建立堆栈框架,和leave ret拆除。搭配call和ret完成一次调用执行流。

wps658D.tmp

执行刚刚进入函数体时的状态:wps658E.tmp

退出:mov ebp,esp清除栈,pop ebp恢复原来状态wps658F.tmp

举例分析:

wps6590.tmp

wps6591.tmp

wps65A2.tmp

wps65A3.tmp

调用call之前,push了两个。为什么不直接push y反而变址寻址0Xfffffff8(%ebp)呢?

因为建立当前栈框架时候就把局部变量放到了内部,就可以用一个变址寻址找到。

call之后发现ebp和x之间隔了一个cs:ip的值所以是+8,+c

最后因为当前参数还在栈框架中,所以之前准备的要清理掉

wps65A4.tmp

保存返回值用eax放到利用变址寻址找到的局部变量z中。

wps65A5.tmp

可能跟printf右结合顺序有关,所以先push y后x,z

最后,调用printf机制相同。

wps65A6.tmp

局部变量的变址寻址,

sub $0x18,esp在堆栈中预留一段空间来分配局部变量(这也是为什么c变量声明要放在头部,这样可以直接全部放入;现在编译器优化后可以扫描整个直接放入,所以不强调局部变量必须放在头部)

char c=’a’是把ASCII码放入

总结:

wps65A7.tmp

1 从main开始执行,系统自动建立的main堆栈;

2 调用p1时候:

wps65A8.tmp

3,p1 return回到初始状态;执行p2同p1,之后p2 ret就整个回到main中

(同时每次会把eax结果赋值给局部变量)

三级调用:

wps65A9.tmp

wps65AA.tmp

逐级压入,逐级退出

2,借助linux内核部分源代码模拟存储程序计算机工作模型及时钟中断

之前介绍了x86汇编和函数调用堆栈,现在利用mykernel实验模拟计算机硬件平台

虚拟一个x86cpu,使用linux源代码把cpu配置好开始执行我们的程序。

中断:中断信号发生时候,cpu和内核代码把当前esp eip ebp压入内核堆栈,然后把eip指向中断处理程序入口,执行中断处理程序!

(本次实验模拟时钟中断:每隔一段时间时钟中断发生一次,进而实现时间片轮转)

3,实验:

wps65AB.tmp

qemu把内核加载进去。

系统唯一进程mymain.c和时钟中断myinterrupter.c-------------只要在mymain.c基础上继续写进程描述PCB和进程链表管理等代码,在myinterrupt.c的基础上完成进程切换代码,一个可运行的小OS kernel就完成了。

wps65BB.tmp

wps65BC.tmp

mymain.c:

之前都是硬件准备的工作,在这里是操作系统启动,执行代码是:

wps65BD.tmp

每循环10万次打印一次

myinterrupt.c:

wps65BE.tmp

因为linux已经完成获取进入时钟中断代码,我们只要每次中断发生时候实际中断处理动作即可

4,在mykernel基础上构造一个简单的操作系统

wps65BF.tmp

嵌入方法语法:

_asm_(  ..  )相当于c语言代码,整体可以看做一个函数。

输出部分输入部分类似函数堆栈的参数,例如ret是一个输出部分。

wps65C0.tmp

%%为了转义字符,结尾\n\t

%1指输出输入部分(第一个输出输入是0开始)即:”c”(val1),

这里c代表ecx寄存器即用ecx存储val1的值。之后把val1 + eax放入eax

%2是edx(val2)

最后放入%0就是val3,”m”就是内存变量而非寄存器;类似(%eax)

wps65C1.tmp

在输入输出部分的限定符;

r让编译器自动选择一个通用寄存器放入

= 表示只写的

+ 读写类型

wps65C2.tmp

最后eax是破坏描述部分,即这段代码可能破坏eax

分析:1 把eax赋值为0  2 把eax放入m即temp变量  3  再把r即input=1放入eax

4 把eax赋值给output 所以流程是交换了值(中间记得输入输出有自动取局部变量值过程)

构造一个简单内核:实验二

wps65C3.tmp

wps65C4.tmp

编译如下文的c文件,完成后make再执行一次

wps65C5.tmpwps65D6.tmp

代码分析:

mypcb.h定义了进程控制块,实际在linux叫tasks_struct

thread存储eip esp

wps65D7.tmp

wps65D8.tmp

定义了进程管理相关的数据结构:

char stack[]内核堆栈

task_entry指定入口

wps65D9.tmp

调度器

mymain.c:

wps65DA.tmp

wps65DB.tmp

声明了一个task数组,是否需要调度标志

从init_mykernel开始分析:

wps65DC.tmp

0正在运行状态;

入口为my_process,实际是mystartkernel

wps65DD.tmp

堆栈栈底是定义的stack。

next还是指向他自己,然后fork很多其他进程

wps65DE.tmp

把0号进程状态都copy过来,用自己的堆栈

新fork进程加入进程列表尾部

wps65DF.tmp

之后启动0号进程;

wps65E0.tmp

之后用了嵌入式汇编:

把thread.sp放入esp,当前栈空直接push esp相当于push ebp

再把ip压栈,ret是把这个ip pop最后popl ebp

wps65E1.tmp

0号进程做的工作就是my_process:其他进程也是

1千万次输出1次,

wps65E2.tmp

这是一种主动调度机制(即代码中if(my_need_sched == 1)),调度完从my_schedule(0开始继续执行

---另外一种是抢占式调度

myinterrupt.c:如何调度

wps65E3.tmp

最后有个时间计数

wps65F3.tmp

时钟中断发生1千次,并且标志不为1就设置为1.

wps65F4.tmp

my_schedule出错处理,之后是:

把当前进程下一个进程赋值给next,如果当前进程正在执行,就进行进程切换:

wps65F5.tmp

保存ebp esp赋值给sp

把下一个进程sp放入esp

wps65F6.tmp保存eip

pushl 3 :把下一个进程eip push进来

wps65F7.tmp

如果进程是新进程还没有执行过,就用else处理

wps65F8.tmp

置为运行时状态,作为当前正在执行进程

保存ebp esp,restore eip esp ebp,把当前进程入口保存,ret

wps65F9.tmpwps65FA.tmp

 

对“操作系统是如何工作的”理解:

操作系统内核有一个起始位置,从这个起始位置开始执行。在执行了一些初始化操作,比如进程的状态设置,各个进程的栈的空间的分配后,将CPU分配给第一个进程,开始执行第一个进程,然后通过一定的调度算法度,比如时间片轮转,在一个时间片后,发生中断,第一个进程被阻塞,在完成保存现场后将CPU分配给下一个进程,执行下一个进程。这样,操作系统就完成了基本的进程调度的功能。

posted on 2017-02-24 22:03  wk2016just  阅读(704)  评论(0编辑  收藏  举报