操作系统是如何工作的

1. 小结:计算机是怎样工作的

三个法宝

存储程序计算机、函数调用堆栈、中断机制

两把宝剑

中断上下文、进程上下文的切换

2. 堆栈

  • 堆栈是C语言程序运行时必须的一个记录调用路径和参数的空间。

    • 函数条用框架
    • 传递参数
    • 保存返回地址
    • 提供局部变量空间...
  • 堆栈相关寄存器:

    esp:堆栈指针——指向系统栈最上面一个栈帧的栈顶
    ebp: 基址指针——指向系统栈最上面一个栈帧的底部
    cs:eip:指令寄存器——指向下一条等待执行的指令地址
  • 堆栈相关操作:
    • push :栈顶地址减少4个字节
    • pop: 栈顶地址增加4个字节
    • call:将当前cs:eip值压栈,cs:eip指向被调函数入口地址。
    • ret:从栈顶弹出原来保存在此的cs:eip值,放入cs:eip中。
    • leave:当调用函数调用时,一般都有这两条指令pushl %ebpmovl %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实验

  1. cd LinuxKernel/linux-3.9.4
  2. 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)
);
  • 实验截图

20135302魏静静Linux内核分析第二周学习总结-LMLPHP

05-08 08:17