操作系统第三篇

第三章 加载操作系统

  前面已经讲过,只要电源打开,CPU就要不停的执行程序,所以要不停的喂给它程序,计算机系统才能正常运行。当电源一打开时,CPU 首先执行的BIOS。BIOS 自检后把软盘镜像上的第一个扇区,也就是 MBR 读入到内存的0x07c00 处,然后跳转到 MBR 继续执行。当 CPU 执行 MBR 时,第一篇的结果只是在屏幕上显示了“Hello world!”字符串,由于再没有要执行的程序,MBR 只好死循环,以保证给 CPU 喂足程序。
  MBR 实际的职责是加载操作系统。加载操作系统是指把操作系统从外存读入到内存中,由于不是简单的拷贝,因此只能说加载。只有让 MBR 加载了操作系统,然后让 CPU 去执行操作系统,计算机系统才能继续正常运行。本章将讲述 MBR 如何加载操作系统,内容主要包括可执行文件格式、最简单的 myos、优雅的写入 myos 以及加载 myos 等知识。

3.1 可执行文件格式

  计算机能处理的所有数据只有数值、文字、声音和图像 4 种类型,所有的数据最终都是以文件的形式来存储和使用的。
  所谓文件格式,就是不同类型的数据用二进制数在文件中表示时的具体规定。比如在 windows 下你打开记事本,输入 abc 三个字符后存盘退出。然后在十六进制编辑器中打开该文件,你会发现该文件里也确确实实只有 3 个字节,就是 abc 三个字符的 ASCII 码。在该文件名上点击右键,查看属性,你会发现该文件大小为 3 个字节,占用空间 4KB。空间占用 4KB 是因为虽然文件内容只有 3 字节,但保存在磁盘上要占用一个基本存储单位,4KB 说明该版本 windows 设置了磁盘的基本存储单位是 4KB。因此我们可以说,扩展名为 txt 的文本文件没有格式,没有格式也算是一种格式。同样的,在 word里输入 abc 三个字母并存盘退出,你会发现它的大小是 11.3KB,占用空间 12KB。用十六进制编辑器打开该文件,你几乎找不到 abc 三个字符在哪里。这说明 word 文档与 txt 文本文件的文件格式是完全不一样的。那么 word 文档的文件格式具体是什么样的呢?还有,音频文件、视频文件、数据库文件、可执行文件等等,它们的文件格式分别都是怎样的呢?搞懂了这些,你就会从骨子里知道如何达到自己的目标。
  我们的 myos 要用可执行文件这种文件格式,那么可执行文件的格式应该是什么样子的?
  简单说,可执行文件就是能被 CPU 执行的文件。既然 CPU 只会执行机器指令,那么可执行文件里面应该保存的就是许许多多的机器指令了,这些机器指令如何保存在可执行文件里面,就是可执行文件的格式。
  txt 文件可以用记事本来打开,word 文档只能用 word 打开,mp4 文件只能用 mp4 播放器打开。可见,不同格式的文件应该由不同的工具软件去解释,去处理。那么可执行文件该由哪个软件来解释处理呢?由于可执行文件只有放在内存中才能被 CPU 执行,因此可执行文件的格式应该由把可执行文件放入内存的那个软件去解释,去处理,这个软件就是操作系统。因此,可执行文件的格式是与操作系统紧密联系的。
  理论上,我们自己编写的 myos 要由 MBR 加载到内存中再执行,因此 myos 的格式应该由 MBR 规定。但 MBR 也是我们自己写的,换句话说,myos 这个可执行文件的格式该由我们自己去规定。假定我们规定了一种保存 myos 的文件格式,那么由谁来产生这种格式的myos 呢?由于 myos 由汇编语言和 C 语言编写,因此我们就要编写一个汇编器和 C 语言编译器,从而把 myos 汇编和编译成我们自己规定的格式,也就是在编写 myos 之前要先写一个自己的汇编器和编译器。
  也许,写一个汇编器和编译器比 myos 本身还要难呢!所以,我们放弃自己编写汇编器和编译器,转而深入了解已有的可执行文件格式,然后为我所用。
  在这里我给出几种可执行文件格式,有纯二进制格式,COFF 格式,PE 格式,ELF 格式。
  纯二进制格式是指可执行文件中只有代码和数据,再没有任何其他辅助的信息。mbr.bin就是纯二进制格式,文件从第一个字节开始就是指令代码,没有像后述其他格式那样的各种头部信息。
  COFF(Common Object File Format),通用对象文件格式,是由 UNIX 操作系统首先提出并且使用的目标文件格式规范,其目的是为了解决 UNIX 可执行文件 a.out 缺乏可扩展性的问题,这种文件格式主要用于目标文件。
  PE(Portable Executable),可移植的执行体,是 windows 在 COFF 基础上制定的可执行文件的格式,也就是说 PE 是 COFF 的一个变种。
  ELF(Executable and Linking Format),可执行与可链接格式,是 UNIX 系统实验室(USL)在 COFF 基础上开发的可执行文件的格式,也就是说 ELF 也是 COFF 的一个变种。ELF 也是 Linux 主要的可执行文件格式。

3.2 最简单的 myos

  第一篇编写的 MBR 其实并没有引导的功能,只是实现了在屏幕上显示了 Hello World!字符串的功能,这样做的目的只是表明 MBR 顺利地从 BIOS 中取得了控制权,接下来,一切的一切,都从 MBR 开始。本章将改写 MBR,使其具有引导 myos 的功能。
  显然,首先要有 myos,然后才能引导。这里暂时用一个能在屏幕上显示字符串的程序代替 myos(因为真正的 myos 以后才慢慢写呢)。由于原来的 MBR 本身就能在屏幕上显示一个字符串,因此只要复制一份 MBR,然后稍作修改,就能当作 myos 来用。
  现在在 myos 子目录下,把 myos1 子目录复制一份,改名为 myos3。在 myos3 中,把 mbr.asm复制一份,改名为 start.asm,并做如下修改:
  1、删掉一开始的 ORG 语句。因为 MBR 被 BIOS 规定为必须要在内存的 0x07c00 处执行,所以 mbr.asm 中要有 ORG 伪指令设置汇编地址。那么 myos 将加载到内存的什么地方去执呢?这得由我们自己规定。第二章已经讲过,通过对 BIOS 之后内存空间的分析,myos 将加载到内存的 0x10000 处执行,这个地址不管怎么写,段内偏移量都为 0,而 nasm汇编时默认的汇编地址计数器就为 0,因此不需要 ORG 伪指令。当然写上“ORG 0x0000”也行,并且显得更加清楚一些。
  2、第 2、3、4 行用 CS 段寄存器的值初始化 DS 和 ES 段寄存器的值。因为 myos 被加载到内存的 0x10000 处执行,此时 CS 寄存器的值为 0x1000,如果还用 0 初始化其他段寄存器的值,那么执行 LODSB 等传送指令时,取到的字符就在 0x0:偏移量处,而不是正确的0x1000:偏移量处。这点很重要,任何时候,你都要清楚地知道,此时程序在哪里执行,各段寄存器的值是否合适。
  3、第 5、6、7 行初始化堆栈。堆栈的位置还是内存的低端,但是当 start.asm 运行时,加载到 0x0000:0x7c00 到 0x0000:0x7dff 处的 MBR 已经完成了历史使命,因此堆栈的栈底由原来的 0x0000:0x7c00 再往上增加 512 字节,变为 0x0000:0x7e00,也就是把 MBR 所占的区域现在也用作堆栈。当然也可以不这样做,从 0x07c00 往地址低端生长,也就是压栈,到0x00500 处,有将近 30KB 的空间;从 0x07c00 往地址高端生长,也就是出栈,到 myos 加载的 0x10000 处,也有大约 32KB 的空间,有足够的安全边际,也不用在乎区区 512 字节。但这样做了,你就更加清楚,更加通透了,你的地盘你做主,你是操作系统系统的编写者,在掌握基本原理的基础上,一切都由你通过编程说了算。此时的内存空间分配图你画一画行不行呢?
  4、第 8、9 行换了更简洁的指令 LEA 和 LODSB,请你参照第二章的内容做正确理解,这里不再赘述。第 9 到第 15 行调用 BIOS 打印字符的子程序,逐个字符的打印字符串。
  5、第 16、17 行在打印完字符串之后,无事可做,暂停机器,死循环。
  6、把字符串换成“myos is running!”,以区别于 MBR 打印的字符串。
  7、mbr.asm 的最后两行保证汇编后的文件大小一定是 512 字节,即一个扇区,并且保证最后两个字节是标记字节“0x55”和“0xAA”。但这在 start.asm 中不需要,因此删掉最后两行,但如果保留也没什么影响。
  修改后的 start.asm 功能为在屏幕上上显示“myos is running!”字符串后死循环,其汇编代码如下所示:

start:  JMP entry
entry:  MOV AX,CS
        MOV DS,AX
        MOV ES,AX
        MOV AX,0
        MOV SS,AX
        MOV SP,0x7e00
        LEA SI,msg
putloop: LODSB
        CMP AL,0
        JE fin
        MOV AH,0x0e
        MOV BX,0x0f
        INT 0x10
        JMP putloop
fin:    HLT
        JMP fin
msg:    DB "myos is running!"
        DB 0x0a,0x0a,0x0a,0

3.3 优雅的写入 myos

  第一篇引导 MBR 时,按照 BIOS 的要求,把 mbr.bin 放在了软盘镜像文件 A.img 的第一个扇区。现在有了最简单的 myos,其可执行文件 start.bin 也需要放到 A.img 中,那么放到 A.img 的什么地方呢?这也该由你自己确定。由于 A.img 此时只有第一个扇区被 mbr.bin占用,其余 2879 个扇区都是空的,因此 start.bin 放到哪里都可以。注意到此时的 start.bin只有 57 个字节,我们就简单的把它放到 A.img 的第二个扇区。作为对比,你可以看一看著名的 Hello World 程序的大小。
  那么怎么放呢?第一篇采取了简单粗暴的方式,即借助于十六进制编辑器把 mbr.bin 复制粘贴到了 A.img 的最前面(还记得吗?),即第一个扇区位置。这里也可以用该方法如法炮制的把 start.bin 放在 A.img 的第二个扇区位置,但是以后随着 myos 的代码增加,再用该方法就很不方便,还容易出错。为此本教材编写了简单的以扇区为单位把一个文件写入另一个文件特定位置的 C 语言程序 writeA.c,其代码如下所示。

#include <stdio.h>
#include <stdlib.h> 
int main(int argc, char **argv)
{
    
    
    FILE *rfd, *wfd;
    char buf[256];
    int offset, bytes, totalbytes=0;
    if (argc != 4){
    
    
        fprintf(stderr, "Usage: %s source target offset\n", argv[0]);
        return -1;
    }
    if((rfd = fopen(argv[1], "rb"))==NULL){
    
    
        printf("Can't open the file %s! \n",argv[1]);
        return -1;
    }
    if((wfd = fopen(argv[2], "rb+"))==NULL){
    
    
        printf("Can't open the file %s! \n",argv[2]);
        return -1;
    }
    offset = atoi(argv[3]);
    offset = (offset - 1) * 512;
    fseek(wfd, offset, SEEK_SET);
    while((bytes = fread(buf, 1, 256, rfd)) > 0){
    
    
        fwrite(buf, 1, bytes, wfd);
        totalbytes += bytes;
    }
    fclose(rfd);
    fclose(wfd);
    printf("%d bytes copied from %s to %s\n",totalbytes, argv[1], argv[2]);
    return 0;
}

  这段 C 语言代码比较简单,它调用了 C 语言有关文件操作的库函数,需要解释的是参数数组。命令行由参数组成,这些参数由空格来分隔,每个参数都被认定为一个字符串。例如命令行:

writeA start.bin A.img 2

就包括四个参数:writea、start.bin、A.img 和 2。当用户输入一个对应于 C 的可执行程序的命令行时,命令解释程序就将命令行解析成参数,并将结果以参数数组的形式传送到程序中去。参数数组(argument array)是一个指向字符串的指针数组,数组的结尾由一个 NULL指针来标识。为了将命令行参数传递到程序中去,需要在 main 函数的参数位置上指定保存参数个数的变量和参数数组的名字,比如上述 writeA.c 程序中的第 3 行 main 函数就写为:

int main(int argc, char **argv ) 

其中整形变量 argc 用于存储命令行参数的个数,argv 是一个指向命令行参数的指针数组,如果在命令行输入上述命令行,则这些参数都会被传递到 writeA 程序中,其中:

        argc=4;共有 4 个字符串参数,包括可执行文件名。
        argv[0]=“writea\0”,其中“\0”是字符串结束符。
        argv[1]=“start.bin\0”
        argv[2]=“A.img\0”
        argv[3]=2\0”
        argv[4]=NULL

现在我们再来解释这段 C 语言代码。第 8-10 行判断,如果运行该程序时命令行参数写得不对,就给出一个提示信息,然后程序直接返回;第 12-14 行以只读方式打开第一个文件(start.bin);第 16-18 行以读写方式打开第二个文件(A.img);第 20-22 行计算并设置以字节为单位的读写位置;第 23-25 行从 start.bin 文件中读出数据并写入 A.img 文件中指定的位置;第 29 行打印执行结果。
  上述命令行的含义是把start.bin文件的内容写入到A.img文件中第2个扇区开始的地方。在记事本中输入并保存 writeA.c 源程序到 myos3 子目录。打开 cmd 命令行界面,参照第一章的步骤,改变当前目录为 myos3,然后用下面的命令先编译 writeA.c:

gcc writeA.c -o writeA.exe

该命令表示编译 writeA.c,输出的可执行文件名为 writeA.exe。关于 gcc 更详细的使用方法,我们第四章讲解。再用以下命令把 start.bin 优雅的写入 A.img 中第 2 个扇区上:

writeA myos.bin A.img 2

注意:命令行上文件名大小写不敏感,但带“-”的选项大小写敏感,比如“-o”不能写成“-O”,“-f”不能写成“-F”等等。具体操作命令如下图所示。

3.4 使 MBR 具有加载功能

  现在,最简单的 myos 操作系统已经写好了,文件名是 start.bin,并把它保存在了 A.img的第 2 个扇区上,接下来就要运行 myos。怎么运行 myos?首先要把 myos 从 A.img 上加载到内存中,然后让 CPU 去执行它。那么谁来加载 myos 呢?当然只能是 MBR。因此本节将在原mbr.asm 的基础上增加加载 myos 的汇编代码,使其具有加载功能。
  首先,MBR 如何从 A.img 文件中读出 start.bin?由于 A.img 在虚拟机中表示的是软盘镜像,因此 MBR 在虚拟机中运行的时候只能以访问软盘的方式读写 A.img,这需要用到 BIOS提供的读写磁盘的子程序,该子程序的名字叫 INT 0x13。这里需要解释一下。操作系统的目的主要有两个:
  一是管理计算机系统的资源;
  二是为用户提供方便的接口。操作系统为用户提供方便的接口体现在两个方面:一是操作界面的方便;二是编程的方便。编程的方便是指操作系统提供了许多功能子程序,用户利用这些子程序编写程序就会方便很多。除了操作系统提供的各种功能子程序之外,各高级语言的编译器也提供了许多子程序,比如 C 语言的 printf 函数等。把操作系统提供的功能子程序和高级语言提供的各种子程序统称为应用程序接口(Application Programming Interface,API),但二者还是有区别的。操作系统提供的功能子程序在执行状态、返回机制和程序结构上都有特殊的规定,因此称之为系统调用;高级语言提供的各种子程序一般都以目标文件的形式封装在一个库文件中,因此称之为库函数。BIOS 提供的各种功能子程序当然是系统调用了,只是是以 INT 指令的机制来提供的,
  因此也常被称为多少号中断。有关 INT 指令的执行机制第七章再详细讲解。现在再回到 INT 0x13,该子程序提供对磁盘操作的各种功能,调用时由 AH 寄存器指出具体的子功能号。各子功能号及其具体功能描述如下表所示。
​​
表详细列出磁盘操作的各种子功能,只是想让你了解 INT 0x13 的强大功能。不准备详解各种功能,需要时你可以在百度很方便的查阅到相关使用细节,这里只介绍用到的读扇区功能。
  当用 0x02 号子功能读取磁盘扇区时,需要指定的参数包括:
  1、读哪个磁盘:用 DL 寄存器标识。0x00 表示软盘,0x80 表示硬盘;
  2、从什么地方开始读:软盘地址采用“柱面,磁头,扇区”的基本格式,用 CH 寄存器向子程序传递柱面号,用 DH 寄存器传递磁头号,用 CL 寄存器传递扇区号。由于 myos在 A.img 的第二个扇区,因此 CH=0,DH=0,CL=2;
  3、总共读多少个扇区:AL 传递要读出的扇区数。由于 start.bin 只有 57 个字节,因AL=1;
  4、读出的内容放在什么地方:ES:BX 指出缓冲区的首地址。前面已经规划好,myos将加载到内存 0x10000 处,因此 ES=0x1000,BX=0x0000。
  这样,通过 INT 0x13 的 0x02 号子功能,就能以读扇区的方法把 start.bin 从软盘镜像上读入到内存中。等以后有了文件系统,就要以读文件的方式加载操作系统了。注意虽然读了1 个扇区,但 start.bin 只占用了该扇区最开始的 57 个字节。
  其次,start.bin 加载结束后,如何跳转到正确位置去执行?这里再啰嗦几句。电源打开,CPU 执行 BIOS,地址大概在 0xFxxx:0xxxxx 处,即在内存地址的高端。当 BIOS 引导了MBR 后,会自动跳转到 0x0000:0x7c00 处执行 MBR,即内存地址的低端。当 MBR 加载了start.bin 之后,就应该跳转到 start.bin 的可执行代码处继续执行。前面已经说了 start.bin 是纯二进制格式的,也就是 start.bin 文件的偏移量 0 处就是第一条指令,既然 start.bin 加载到了0x10000 处,那就直接跳转到 0x10000 处执行即可。如果 start.bin 不是纯二进制格式,而是ELF 格式,那么把 start.bin 加载到 0x10000 处时,就不能直接跳转到 0x10000 处执行,因为此时该位置是 ELF 文件头,而不是第一条指令。具体如何跳转呢?可直接用段间跳转指令,也可以利用堆栈和段间返回指令,以下 5 条指令与 JMP 0x1000:0x0000 指令作用相同:

        MOV AX,0x1000
        PUSH AX
        MOV AX,0x0000
        PUSH AX
        RETF

按以上说明修改 mbr.asm。原来的 mbr.asm 第 17-18 行,由于打印完字符串后无事可做,所以死循环。现在从该位置修改,加载 start.bin 后跳转到 start.bin 处继续执行。修改后的mbr.asm 如下所示:

ORG 0x7c00
start: JMP entry
entry: MOV AX,0
MOV SS,AX
MOV DS,AX
MOV ES,AX
MOV SP,0x7c00
MOV SI,msg
putloop: MOV AL,[SI]
ADD SI,1
CMP AL,0
JE fin
MOV AH,0x0e
MOV BX,0x0f
INT 0x10
JMP putloop
fin: MOV AX,0x1000 ;
MOV ES,AX ;
MOV BX,0 ; 读到内存 0x10000 处
MOV AH,0x02 ; AH=0x02,读磁盘
MOV DL,0x00 ; A 驱动器
MOV CH,0 ; 柱面 0
MOV DH,0 ; 磁头 0
MOV CL,2 ; 扇区 2
MOV AL,1 ; 共读 1 个扇区
INT 0x13 ; 调用 BIOS
MOV AX,0x1000
PUSH AX
MOV AX,0x0
PUSH AX
RETF
msg: DB "hello, world!",0x0a,0x0a,0x0a
marker: TIMES (0x01fe-marker+start) DB 0
DB 0x55, 0xaa

重新汇编 mbr.asm 为 mbr.bin,然后将 mbr.bin 优雅的写入 A.img 的第一个扇区。具体操作如下所示:

最后在虚拟机中测试加载的操作系统。打开 VMware Player,选择 myos 虚拟机,点击右下角的“编辑虚拟机设置”。把软盘的镜像文件选择为 myos2 下的 A.img,然后播放该虚拟机。如果一切正确,将出现如下图所示的界面:

(本文代码只有换行,没有回车,该截图是我新截的。不过,回车换行都可以搜的)
  注意,这里“hello,world!”是 mbr.asm 显示的,“myos is running!”是 start.asm 显示的。至此,我们已经实现了操作系统的最基本框架,由开机的 BIOS 到 MBR 再到 myos。接下来将不断丰富 myos,当 myos 比较大时,就要修改 MBR,因为再不能只加载一个扇区了。

作业:
1、用另外一种方式跳转到 myos.bin。
2、使 mbr.bin 只具有加载功能。

猜你喜欢

转载自blog.csdn.net/weixin_59357453/article/details/129899313
今日推荐