本章节所有代码托管在miniOS_32
章节任务介绍
任务简介
上一节,我们介绍了程序的基本结构以及程序是如何加载到内存的,同时成功的加载了内核到内存
本节我们将正式书写内核代码
本章的主要任务有:
- 实现内核的单个字符打印
- 实现内核字符串打印功能
前置知识
汇编层间的函数调用
函数调用涉及两个问题
- 参数传递的方式
- 参数传递的顺序
先回答第一个问题,参数传递的方式
首先,我们需要知道,现代编译器在处理函数调用进行参数传递有两种方式
- 寄存器传递
- 栈空间传递
以下是当前的64位操作系统的寄存器概览
- 如果参数的个数小于7个,则采用寄存器传递,因为寄存器传递参数速度较快。这7个寄存器分别是
- rdi,传递第一个参数
- rsi,传递第二个参数
- rdx,传递第三个参数
- rcx,传递第四个参数
- r8,传递第五个参数
- r9,传递第六个参数
- 当参数个数大于等于7个,多余的参数则使用栈空间传递
当然,我们目前要做的是32位操作系统(如果想了解更多有关现代编译器过程调用细节可参看《深入理解计算机系统》),其调用约定还相对没那么多,当参数个数小于等于5个的时候使用寄存器传递,当参数个数大于5的时候使用栈空间传递,以下是我们目前使用的参数传递寄存器约定
- ebx存储第一个参数
- ecx存储第二个参数
- edx存储第三个参数
- esi存储第四个参数
- edi存储第五个参数
接下来,我们回答第二个问题,参数传递的顺序
- 调用者按照逆序存储参数
- 被调用者顺序取出参数
我们以一段代码说明
|
|
以下是调用者的汇编代码,这里为了说明问题用的是栈空间传递参数
|
|
以下是被调用者的汇编代码
|
|
需要知道,call
指令在调用函数时做了两件事
- 将call指令的下一条指令的地址压入栈中,作为函数的返回地址
- 修改eip寄存器(指令寄存器,用于取出指令送给cpu执行)的值为subtract符号的地址
相对的,ret
指令也做了两件事
- 弹出调用者压在栈中的返回地址
- 修改eip寄存器的值为弹出的返回地址
因此,其调用过程如下所示
由此可以看到,计算机正是通过call
和ret
这两条指令完成函数的调用和恢复的,但需要知道的是,从本质上说,这两天指令仅仅只是修改eip寄存器用的,并不能保证恢复环境的时候正确,因此有了以下框架
|
|
这个框架是每个函数都要做的,事实上,在高级语言中
- 函数的左括号对应的就是前两条指令
- 函数的右括号对应的是最后两个指令
c语言与汇编语言的混合调用
如果想了解更多有关函数调用的细节可参考《深入理解计算机系统》第三章的内容,这里也推荐一个网站,供大家学习使用
有了上述知识,我们知道高级语言的底层其实就是汇编语言,因此本质上两者是想通的,接下来我们实战以作说明
|
|
以上是一段简单的使用c语言调用系统函数write
,接下来我们看如果使用汇编语言怎么写
syscall_write.S
|
|
注意,所有的系统调用最底层的入口只有一个,那就是
0x80
号中断,针对不同的功能有不同的功能号,如4号功能代表write
调用,1号功能代表退出,这些功能号像是参数,由操作系统通过eax
传递
编译链接上述代码,即可验证结果
|
|
接下来我们看c语言和汇编语言是如果相互调用的
C_with_S_c.c
|
|
这段代码想必大家都能看懂,c_print函数通过将传入的参数str传递给asm_print函数进行打印,只不过接下来我们将通过汇编实现asm_print函数,同时我们在汇编代码中传递字符串参数给c_print
所以本质上二者的调用关系应该是
- 汇编代码首先传递字符串参数给c_print
- 然后c_print将参数再回传给汇编代码中的asm_print函数
- asm_print函数最终负责将字符串打印出来
C_with_S_S.S
|
|
编译
|
|
链接
|
|
运行
|
|
内核初步完善
代码目录结构
|
|
实现单字符打印
接下来我们将实现内核的第一个功能——单个字符打印,本节字符打印的源代码依旧采用汇编形式
数据类型定义
/lib/std_int.h
|
|
字符打印头文件定义
/lib/kernel/print.h
|
|
字符打印源文件实现
/lib/kernel/print.S
|
|
内核主函数
/kernel/main.c
|
|
编译链接
|
|
运行
仍旧进入bochs安装目录,运行
|
|
结果如下所示
实现字符串打印
字符串打印函数的实现就比较简单了,直接遍历字符串的每一个字符,然后调用上述我们的字符打印函数即可
在/lib/kernel/print.S文件中添加如下代码
|
|
/kernel/main.c文件中测试如下
|
|
编译运行之后结果如下