学会如何造轮子

本文是我学习在学堂在线上由清华大学开设的操作系统课程的相关笔记,你可以通过下面的链接找到课程相关内容:

学堂在线:http://www.xuetangx.com/courses/course-v1:TsinghuaX+30240243X+sp/about

课程维基:http://os.cs.tsinghua.edu.cn/oscourse/OS2019spring

课程实验、练习:https://github.com/chyyuu/os_course_info

“实验楼”在线实验环境:http://www.shiyanlou.com/courses/221

ucore 使用了 C 和汇编语言来编写代码,因此需要掌握一定的汇编基础,才能更好的完成课程。

汇编语言是低级语言,直接描述、控制 CPU 的运行。

汇编语言是什么

CPU 只负责计算,输入一条指令,它就能运行一次,然后停下来,等待下一条指令,这些指令都是二进制的,称为操作码(opcode)。编译器的作用在于将高级语言写好的程序翻译成操作码。

汇编语言就是二进制指令的文本形式,与指令是一一对应的关系。

环境搭建(MacOS)

此处有坑

需要安装 gcc 和 nasm,gcc 是编译器,Mac 已经自带了,nasm 是 x86 架构的汇编与反汇编工具,看网上说 Mac 也已经带了,但我没找到,所以需要通过 brew 安装一下:

brew install nasm

查看 nasm 版本:

nasm -v

可以先新建一个 test.s文件:

global _main

_main:
    mov rax, 0
    ret

然后编译:

nasm -f macho64 test.s
gcc -o test test.o

运行:

./test

可以尝试一下把代码里的返回值打印出来:

./test ; echo $?

寄存器

CPU 本身只负责运算,不存储数据,数据一般存储于内存之中,CPU 要用的时候就去内存读写数据,但 CPU 的运行速度远高于内存的读写速度,因此 CPU 一般都自带一级缓存和二级缓存。

但 CPU 缓存的速度仍不够快,而且缓存内的地址不固定,CPU 需要花费时间去寻址。因此,除缓存外,CPU 还自带了寄存器(register),用来存储最常用的数据。

寄存器不依靠地址区分数据,而依靠名称。

寄存器的种类

X86 CPU 只有8个寄存器(第二节笔记

32 位 CPU 的寄存器大小是 4 个字节

内存模型:Heap

程序运行时,操作系统会给它分配一段内存,用来存储程序和运行产生的数据。这段内存有起始地址和结束地址。

程序运行过程中,对于动态的内存占用请求,系统会从预先分配的内存中,划出一部分给用户,划分规则是从起始位置开始。

这种因用户主动请求而划分出来的内存区域,叫做 Heap(堆)。

Heap 的一个重要特点是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

内存模型:Stack

除 Heap 以外,其他内存占用叫做 Stack(栈)。Stack 是由于函数运行而临时占用的内存区域。

Stack 由内存区域的结束地址开始,从高位向地位分配。

CPU 指令

example.c

int add_a_and_b(int a, int b) {
   return a + b;
}

int main() {
   return add_a_and_b(2, 3);
}

可以用 gcc 将上面的程序转换成汇编语言。

gcc -S example.c

(自己转换出来大概率会很不同,这取决于你的 CPU)

example.s经过简化以后,大概是下面的样子。

_add_a_and_b:
   push   %ebx
   mov    %eax, [%esp+8] 
   mov    %ebx, [%esp+12]
   add    %eax, %ebx 
   pop    %ebx 
   ret  

_main:
   push   3
   push   2
   call   _add_a_and_b 
   add    %esp, 8
   ret

上面的代码,每一方就是 CPU 执行一次操作。

push   %ebx

上面这一行代码,push是 CPU 指令,%edx是该指令要用到的运算子。一个 CPU 指令可以有 0 到多个运算子。

push 指令

push 指令用于将运算子放入 Stack

call 指令

call 指令用来调用函数

call   _add_a_and_b

上面的代码表示调用add_a_and_b函数

mov 指令

mov    %eax, [%esp+8] 

mov 指令用于将一个值写入某个寄存器

add 指令

add    %eax, %ebx

add 指令用于将两个运算子相加,并将结果写入第一个运算子。

pop 指令

pop 用于取出 Stack 最近写入的值(最低地址),并将这个值写入运算子指定的位置。

pop    %ebx

ret指令

ret 用于终止当前函数的执行,将运行权交还上层函数。

参考资料

http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html

https://www.jianshu.com/p/552f37d3c9b0

https://zhuanlan.zhihu.com/c_144694924