操作系统启动实验
简单的操作系统
汇编代码
; hello-os
; TAB=4
ORG 0x7c00 ; 指明程序装载地址
; 标准FAT12格式软盘专用的代码 Stand FAT12 format floppy code
JMP entry
DB 0x90
DB "HELLOIPL" ; 启动扇区名称(8字节)
DW 512 ; 每个扇区(sector)大小(必须512字节)
DB 1 ; 簇(cluster)大小(必须为1个扇区)
DW 1 ; FAT起始位置(一般为第一个扇区)
DB 2 ; FAT个数(必须为2)
DW 224 ; 根目录大小(一般为224项)
DW 2880 ; 该磁盘大小(必须为2880扇区1440*1024/512)
DB 0xf0 ; 磁盘类型(必须为0xf0)
DW 9 ; FAT的长度(必??9扇区)
DW 18 ; 一个磁道(track)有几个扇区(必须为18)
DW 2 ; 磁头数(必??2)
DD 0 ; 不使用分区,必须是0
DD 2880 ; 重写一次磁盘大小
DB 0,0,0x29 ; 意义不明(固定)
DD 0xffffffff ; (可能是)卷标号码
DB "HELLO-OS " ; 磁盘的名称(必须为11字?,不足填空格)
DB "FAT12 " ; 磁盘格式名称(必??8字?,不足填空格)
RESB 18 ; 先空出18字节
; 程序主体
entry:
MOV AX,0 ; 初始化寄存器
MOV SS,AX
MOV SP,0x7c00
MOV DS,AX
MOV ES,AX
MOV SI,msg
putloop:
MOV AL,[SI]
ADD SI,1 ; 给SI加1
CMP AL,0
JE fin
MOV AH,0x0e ; 显示一个文字
MOV BX,15 ; 指定字符颜色
INT 0x10 ; 调用显卡BIOS
JMP putloop
fin:
HLT ; 让CPU停止,等待指令
JMP fin ; 无限循环
msg:
DB 0x0a, 0x0a ; 换行两次
DB "hello, world"
DB 0x0a ; 换行
DB "hello MYOS,K1T000"
DB 0x0a ; 换行
DB 0
RESB 0x7dfe-$ ; 填写0x00直到0x001fe
DB 0x55, 0xaa
利用工具构建
makefile
- Makefile 的功能
Makefile
是一种用于自动化构建过程的脚本文件,通常与 make
工具一起使用。在开发中,它定义了如何编译和链接代码,如何生成目标文件,如何处理依赖关系等。通过 make
命令,开发人员可以根据 Makefile
中的指令自动执行任务,比如编译源代码、创建可执行文件、生成镜像文件等。
具体来说,Makefile
包含:
- 目标(target):如
helloos.img
、helloos.bin
等生成的文件。 - 规则(rule):定义如何生成目标文件的步骤。
- 依赖关系(dependency):指定哪些文件或目标是当前目标的依赖。
- 命令(command):用于生成目标文件的具体操作。
采用汇编
- 为什么用汇编语言写操作系统?
写操作系统时,选择使用汇编语言通常有以下几个原因:
(1) 操作系统对硬件的直接控制
操作系统,尤其是引导程序、内核的早期部分,需要直接控制硬件资源(如 CPU、内存、硬盘等)。汇编语言提供了极低级别的控制,使你能够精确地操作硬件,比如设置中断向量、控制内存映射等。
在操作系统启动的早期阶段(引导加载程序),需要与硬件打交道,这时 C 语言的抽象就不再合适,汇编提供的直接访问硬件的能力是必要的。
(2) 性能和效率
汇编语言是一种低级语言,通常比高级语言(如 C 语言)更高效。对于内核的某些关键部分,使用汇编可以实现非常高效的操作,如上下文切换、任务调度等。
(3) 学习和理解底层原理
通过汇编语言写操作系统的早期部分,你可以深入了解计算机的底层架构,包括 CPU 寄存器、栈操作、中断机制等。这对理解操作系统的工作原理非常有帮助。
- 为什么不直接用 C 语言写操作系统?
虽然 C 语言在编写操作系统时非常流行,但它并不是完全适合于操作系统所有部分的语言。操作系统的早期阶段(如引导加载程序)确实需要使用汇编语言。以下是一些原因,解释为什么 C 语言通常不会在操作系统的整个开发过程中取代汇编语言:
(1) 操作系统的启动过程
在操作系统启动的初期阶段,CPU 和硬件设备的控制非常直接。你必须配置中断、设置段寄存器、初始化内存等,这些操作 C 语言通常做不到。C 语言对硬件的抽象非常强,它需要操作系统提供的运行时环境来支持,而在操作系统的引导加载过程中(也就是 bootloader
阶段),这些环境尚未建立。
(2) C 语言依赖于运行时库
C 语言虽然非常接近硬件,但它依赖于操作系统的运行时库(例如标准库、系统调用等),这些库在操作系统本身没有完成之前无法使用。操作系统内核的开发必须考虑到这些库在编译时没有定义的情况,因此,直接使用 C 语言的执行环境并不适合操作系统的最初开发。
(3) 操作系统设计中的低级任务
有些操作系统的设计任务需要处理直接与硬件相关的低级任务。例如,操作系统的中断处理、设备驱动、任务调度等,需要非常精确的控制和高效的代码。这些任务用汇编语言编写时更为简单和高效,而在 C 语言中,编译器的优化有时会使代码变得不够高效,特别是与硬件交互时。
(4) 更好的移植性和可维护性
尽管操作系统开发时可能会用汇编语言,但现代操作系统的核心部分通常还是会用 C 语言开发。因为 C 语言具备较好的移植性、可维护性和易读性,尤其是相对于汇编语言。汇编语言的代码是与硬件架构紧密耦合的,因此难以移植,而 C 语言能够在不同的硬件平台上进行相对容易的移植。
关于 Make
的使用
Make
是一个自动化工具,主要用于自动化编译和构建过程。它可以让你通过一个 Makefile
文件定义如何从源代码生成目标文件(如 .o
文件、可执行文件等)。让我们来看一下如何使用 Make
来编译一个汇编语言编写的操作系统。
基本的 Makefile
结构
一个简单的 Makefile
包含以下几个基本部分:
- 目标(target):你希望生成的文件,比如
.o
文件、最终的可执行文件。 - 依赖(dependency):生成目标所需要的源文件。
- 规则(rule):从依赖生成目标的命令。
假设你有一个简单的操作系统源码,包含以下文件:
boot.asm
:引导程序(使用汇编语言编写)kernel.asm
:操作系统内核(使用汇编语言编写)link.ld
:链接脚本,用于将生成的目标文件链接成最终的可执行镜像
make
工具不仅仅用于编译操作系统项目,它实际上是一个非常通用的自动化构建工具,广泛应用于各种编程项目中,无论是 C 语言、C++、汇编语言,还是其他语言的项目。它的核心功能是通过 Makefile
自动化地管理源代码的编译、链接等过程,使得项目的构建变得更加高效和可重复。
make
的应用场景
make
工具可以用于编译许多类型的项目,常见的包括:
- C/C++ 项目:这是
make
最常见的应用场景,尤其是在大型项目中,可以帮助管理多个源文件的编译与链接。 - Python 项目:虽然 Python 本身不需要编译,但
make
可以用于自动化测试、打包、清理等任务。 - Java 项目:尽管 Maven 或 Gradle 是更常用的构建工具,但
make
也可以用于编译 Java 源文件。 - 汇编语言项目:正如你在操作系统开发中看到的,
make
也可以用来自动化汇编语言文件的编译过程。 - 其他脚本语言:对于 Bash 脚本、Ruby 脚本等,
make
可以用来执行特定任务,如运行测试、部署应用等。
编译
make file 或者直接 nask.exe helloos.nas helloos.img
2、虚拟机启动操作系统
Step1:启动VMware,创建一个新的虚拟机MyOS(注意加上软盘驱动器);
Step2:设置从软盘镜像文章中启动系统,文件为刚做好的helloos.img
Step3:启动该虚拟机
系统调用
Linux系统中,用户程序可以通过隐式调用或显示调用使用系统调用。
隐式调用——用户通过封装了系统调用的API使用系统调用
显式调用——使用syscall()函数实现调用,int syscall(int number,…),number是系统调用号
基础
syscall
并不仅仅用于获取进程 ID(PID),它是用于直接与操作系统内核交互的一种方式,允许你调用任何支持的系统调用。syscall
函数是一个低级接口,通过它你可以直接发起操作系统的系统调用。
syscall
的基本用法
syscall
函数是一个系统调用接口,允许你通过系统调用号来调用操作系统内核提供的底层功能。它的基本签名通常如下:
c
long syscall(long number, ...);
number
是系统调用的编号,也就是操作系统为每个系统调用分配的一个唯一的数字(如SYS_getpid
、SYS_read
等)。- 后面的
...
是系统调用的参数,具体取决于你要调用的系统调用。
syscall
支持的系统调用
syscall
函数是通用的,可以用来调用几乎所有的系统调用,不仅仅是 getpid
。以下是一些常见的可以通过 syscall
调用的操作系统系统调用:
-
进程管理相关的系统调用:
SYS_getpid
:获取当前进程的 PID(进程 ID)。SYS_fork
:创建一个新的进程(子进程)。SYS_exit
:使当前进程退出。SYS_getppid
:获取父进程的 PID。SYS_waitpid
:等待子进程的状态改变。
-
文件操作相关的系统调用:
SYS_open
:打开文件。SYS_read
:从文件描述符中读取数据。SYS_write
:向文件描述符写入数据。SYS_close
:关闭文件描述符。SYS_lseek
:设置文件指针的位置。
-
内存管理相关的系统调用:
SYS_brk
:改变数据段的结束地址。SYS_mmap
:内存映射文件或设备到进程的虚拟内存。SYS_munmap
:解除内存映射。
-
信号和中断相关的系统调用:
SYS_kill
:发送信号给进程。SYS_signal
:设置信号处理程序。
-
网络操作相关的系统调用:
SYS_socket
:创建一个新的网络套接字。SYS_connect
:连接到远程主机。SYS_sendto
:向指定的地址发送数据。SYS_recvfrom
:从指定的地址接收数据。
-
时间和日期相关的系统调用:
SYS_time
:获取系统的当前时间。SYS_gettimeofday
:获取当前时间戳。
隐式调用实际上是指别人已经为你封装好了底层的功能,你不需要手动调用底层的系统调用(如 syscall
),而是通过更高级的接口来实现相同的功能。这种封装不仅让你在编程时更加方便,也提高了代码的可读性和可维护性。
libc
libc
(C 语言标准库,lib
是 “library” 的缩写)是一个包含 C 语言标准函数的库。它为开发者提供了许多标准功能,诸如输入输出(I/O)、内存管理、字符串操作、数学函数等。在类Unix 操作系统中,libc
通常是一个包含系统调用封装的共享库。
实验
#include <syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main (viod) {
long ID1, ID2;
ID1=syscall(SYS_getpid); /*显式系统调用*/
printf(“syscall(SYS_getpid)=%ld\n”, ID1);
ID2=getpid(); /*使用libc封装的隐式系统调用*/
printf(“getpid()=%ld\n”, ID2);
return(0);
}