操作系统是如何工作的
1. 小结:计算机是怎样工作的
三个法宝
存储程序计算机、函数调用堆栈、中断机制
两把宝剑
中断上下文、进程上下文的切换
2. 堆栈
- 堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间。
- 函数条用框架
- 传递参数
- 保存返回地址
- 提供局部变量空间...
堆栈相关寄存器:
esp:堆栈指针——指向系统栈最上面一个栈帧的栈顶
ebp: 基址指针——指向系统栈最上面一个栈帧的底部
cs:eip:指令寄存器——指向下一条等待执行的指令地址- 堆栈相关操作:
- push :栈顶地址减少4个字节
- pop: 栈顶地址增加4个字节
- call:将当前cs:eip值压栈,cs:eip指向被调函数入口地址。
- ret:从栈顶弹出原来保存在此的cs:eip值,放入cs:eip中。
leave:当调用函数调用时,一般都有这两条指令
pushl %ebp
和movl %esp,%ebp
,leave是这两条指令的反操作。
3. 函数调用约定
函数调用约定 | 参数传递顺序 | 负责清理参数占用的堆栈 |
---|---|---|
__pascal | 从左到右 | 调用者 |
__stdcall | 从右到左 | 被调函数 |
__cdecl | 从右到左 | 调用者 |
调用函数的代码和被调函数必须采用相同的函数的调用约定,程序才能正常运行。
Windows中C/C++程序的缺省函数调用约定是__cdecl
linux中gcc默认用的规则是__stdcall
4、C代码中嵌入汇编代码的写法
__asm__(汇编语句模板: 输出部分: 输入部分: 破坏描述部分)
汇编语句模板
汇编语句模板由汇编语句序列组成,语句之间使用“;”、“\n”或“\n\t”分开。指令中的操作数可以使用占位符引用C语言变量,操作数占位符最多10个,名称如下:%0,%1,…,%9。
输出部分
输出部分描述输出操作数,不同的操作数描述符之间用逗号格开,每个操作数描述符由限定字符串和C语言变量组成。
输入部分
输入部分描述输入操作数,不同的操作数描述符之间使用逗号格开,每个操作数描述符由限定字符串和C语言表达式或者C语言变量组成。
破坏描述部分
破坏描述符用于通知编译器我们使用了哪些寄存器或内存,由逗号格开的字符串组成,每个字符串描述一种情况,一般是寄存器名;除寄存器外还有“memory”。
限制字符
限制字符有很多种,有些是与特定体系结构相关,它们的作用是指示编译器如何处理其后的C语言变量与指令操作数之间的关系。
常用限制字符 | ||
---|---|---|
分类 | 限定符 | 描述 |
通用寄存器 | “a” | 将输入变量放入eax |
“b” | 将输入变量放入ebx | |
“c” | 将输入变量放入ecx | |
“d” | 将输入变量放入edx | |
“s” | 将输入变量放入esi | |
“d” | 将输入变量放入edi | |
“q” | 将输入变量放入eax,ebx,ecx,edx中的一个 | |
“r” | 将输入变量放入通用寄存器,也就是eax,ebx,ecx,edx,esi,edi中的一个 | |
“A” | 把eax和edx合成一个64 位的寄存器(use long longs) | |
内存 | “m” | 内存变量 |
“o” | 操作数为内存变量,但是其寻址方式是偏移量类型,也即是基址寻址,或者是基址加变址寻址 | |
“V” | 操作数为内存变量,但寻址方式不是偏移量类型 | |
“ ” | 操作数为内存变量,但寻址方式为自动增量 | |
“p” | 操作数是一个合法的内存地址(指针) | |
寄存器或内存 | “g” | 将输入变量放入eax,ebx,ecx,edx中的一个或者作为内存变量 |
“X” | 操作数可以是任何类型 | |
立即数 | “I” | 0-31之间的立即数(用于32位移位指令) |
“J” | 0-63之间的立即数(用于64位移位指令) | |
“N” | 0-255之间的立即数(用于out指令) | |
“n” | 立即数 | |
“p” | 立即数,有些系统不支持除字以外的立即数,这些系统应该使用“n”而不是“i” | |
匹配 | & | 该输出操作数不能使用过和输入操作数相同的寄存器 |
操作数类型 | “=” | 操作数在指令中是只写的(输出操作数) |
“+” | 操作数在指令中是读写类型的(输入输出操作数) | |
浮点数 | “f” | 浮点寄存器 |
“t” | 第一个浮点寄存器 | |
“u” | 第二个浮点寄存器 | |
“G” | 标准的80387浮点常数 | |
其它 | % | 该操作数可以和下一个操作数交换位置 |
# | 部分注释,从该字符到其后的逗号之间所有字母被忽略 | |
* | 表示如果选用寄存器,则其后的字母被忽略 |
5.mykernel实验
cd LinuxKernel/linux-3.9.4
qemu -kernel arch/x86/boot/bzImage
然后cd mykernel 可以看到qemu窗口输出的内容的代码mymain.c和myinterrupt.c
进程的启动
/*mymain.c*/
/* start process 0 by task[0] */
pid = 0;
my_current_task = &task[pid];
asm volatile(
"movl %1,%%esp\n\t" /* 将进程的sp赋给esp寄存器 */
"pushl %1\n\t" /* ebp入栈:因为在这里栈为空,esp=ebp,所以push的%1就是esp就是ebp。*/
"pushl %0\n\t" /* 进程入口ip入栈 */
"ret\n\t" /* 把进程入口ip赋给eip,即从这之后0号进程启动。*/
"popl %%ebp\n\t"
:
: "c" (task[pid].thread.ip),"d" (task[pid].thread.sp) /* input c or d mean %ecx/%edx*/
);
进程的切换:
(1).下一个进程next->state == 0 即正在执行时
/*myinterrupt.c*/
//两个正在运行的进程之间做进程上下文切换
if(next->state == 0)
/* state值的含义:-1表示没有执行,0表示正在执行,>0表示停止,这里为0,即进程正在执行 */
{
/* 以下是进程切换关键代码 */
asm volatile(
"pushl %%ebp\n\t" /* 把当前进程的ebp保存*/
"movl %%esp,%0\n\t" /* 把当前进程的esp赋值到sp中保存下来*/
"movl %2,%%esp\n\t" /* 把下一个进程的sp放到esp中*/
"movl $1f,%1\n\t" /* 把eip保存起来,$1f指接下来的标号1:的位置*/
"pushl %3\n\t" /*把下一个进程的eip保存起来*/
"ret\n\t" /* 还原eip */
"1:\t" /* 标号1,下一进程从此开始 */
"popl %%ebp\n\t"
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
}
(2).进程是一个新进程,还从未执行过
/*myinterrupt.c*/
/*这段代码是当进程从未执行过时,所执行的动作,即启动一个进程
next->state = 0; /* 首先要把进程置为运行时状态,作为当前正在执行的进程 */
my_current_task = next;
printk(KERN_NOTICE ">>>switch %d to %d<<<\n",prev->pid,next->pid);
/* 进程切换时的提示,从当前进程切换至下一进程*/
asm volatile( //混合编程
"pushl %%ebp\n\t" /* 保存ebp */
"movl %%esp,%0\n\t" /* 保存esp */
"movl %2,%%esp\n\t" /* 将下一进程的sp存入esp */
"movl %2,%%ebp\n\t" /* 将下一进程的bp存入ebp,因为栈空,所以esp和ebp指向同一位置 */
"movl $1f,%1\n\t" /* 保存eip */
"pushl %3\n\t" /*保存当前进程入口 */
"ret\n\t" /* 还原eip */
: "=m" (prev->thread.sp),"=m" (prev->thread.ip)
: "m" (next->thread.sp),"m" (next->thread.ip)
);
实验截图