操作系统开发(3)——操作系统的Hello world

发布于 2022-08-03  23 次阅读


上一章搭建了在进行操作系统开发时所需要的环境,这一章我们会第一次接触到汇编,并且写一个能在VMware虚拟机上运行的“Hello world”。

命名

开发操作系统,最重要的当然是给操作系统取个名字。
那么就叫KNOS好啦。 :diana_yeah:

我们按下电源键时,计算机在干什么?

学过计算机或者了解过计算机的同学可能会知道,程序运行前会把需要使用到的文件读入内存中,然后从内存中运行程序。至于为什么需要先将文件载入到内存,而非直接从存储设备中执行,主要有几点原因:

  • 首先是存储设备的种类多、操作难以统一,现代操作系统的常用外部存储有:硬盘(固态硬盘、机械硬盘)、U盘、存储卡、光盘(部分有特殊要求的地方会用得比较多)等等。这些不同的设备都有各自不同的操作方式,所以如果是直接操作外部存储设备,就可能出现各种出现不兼容的情况;
  • 其次,类似硬盘的存储设备都被称为“低速设备”,而内存的读写速度一般来说要比外部存储设备高几十甚至几百倍,所以使用内存能够大大提升程序运行效率;
  • 最后是刚才所说的那些设备(硬盘、U盘之类的)都是属于外部存储设备,CPU没办法直接访问到这些设备,CPU如果要从外部存储设备中读取数据,需要向硬盘控制器之类的控制设备发送指令,然后由相关的控制器与存储设备所交互。

我们可以把上述步骤分成两个部分:
1) 从硬盘中读取所需文件存放到内存;
2) 将CPU的cs:ip寄存器指向程序的起始地址;

实际上,操作系统在启动前也是在执行这个类似的步骤。

谁来把系统从硬盘“搬”到内存中?

程序在运行时,是由操作系统的“某个模块”将其从硬盘加载到内存中的(当然,这个模块我们后续也会着手开发)。但是操作系统是谁来从硬盘加载到内存中的呢?

答案是BIOS。

BIOS(Base Input & Output System),翻译成中文大概是“基本输入输出系统”。顾名思义,该系统是负责进行最基本的输入输出工作的。

在计算机上电启动以后,首先会进行自检。很多计算机在刚按下电源键后,风扇会转的特别厉害,随后减弱,这个过程其实就是在自检。在比较老的计算机主板上面会有一个蜂鸣器,如果自检出现问题,则会按照相应的规则鸣响,不过现在的计算机基本上都没有预装蜂鸣器了,有些是只留了一个接口,可以自行安装,有些甚至连接口都取消了。

自检完成以后,BIOS就会开始进行它的工作。在启动系统之前,BIOS会根据设置来选择启动设备(也就是操作系统所在的存储设备)。完成启动设备的选择后,BIOS会读取存储设备的第0磁头的第0磁道的第1扇区,然后检查该扇区是否以0x55和0xaa两字节作为结尾。如果是以这两个字节作为结尾,则BIOS会认为这个扇区为引导扇区(Boot Sector),然后把这一个扇区的数据读入内存地址0x7c00处。(之所以是这个地址,而不是0x0000,是因为0号地址还有其他作用,后面会提到),然后CPU的cs:ip会指向这个地址,并开始执行这段程序。

Boot引导程序

上一节提到,计算机启动后会将系统盘的第一个扇区存到内存中,本文操作系统初期的启动盘介质是3.5英寸软盘,其一个扇区只有512字节(实际上正常情况下,大多存储设备的一个扇区都是512字节),其容量较小,没有办法存储操作系统的所有功能,所以我们只会在其中存储文件系统和一些用于加载真正的系统文件的程序,以及一些日志打印程序。这一段程序的主要作用就是引导操作系统的进一步工作,所以这一段程序就被成为Boot引导程序。

其他一些更复杂的操作(如载入系统文件到内存、初始化部分硬件设备等)就由Loader来进行,这一部分在后面会详细说明。

Hello world

接下来,让我们动手实现一段简单的引导程序,然后在VMware里面跑一下,先把效果给做出来。这一次我们的目标,就是在开机时屏幕上显示黑底白字的“Start booting KNOS”即可。

根据前面几节的描述,我们知道,大概操作分这么几步:编写代码->编译成二进制文件->将二进制文件写入镜像第1扇区->将镜像以软盘形式挂载到虚拟机->启动虚拟机。那么我们一步一步地来解决吧。

代码

首先是编写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
    org 0x7c00

BaseOfStack     equ     0x7c00

    jmp short start             ; 跳转到启动代码

; 启动代码
start:
    ; 初始化
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, BaseOfStack

; 在屏幕上显示信息
    ; 显示start booting KNOS
    ; 清屏(ah=0x06, al=0x00)
    mov ah, 06h     ; int 10中断,使用0x06 ah功能号时,按照指定范围滚动窗口
    mov al, 00h     ; al为0时表示进行清屏操作
    int 10h

    ; 设置光标位置
    mov ah, 02h     ; int 10h的02h功能号设定光标位置
    mov dx, 0000h   ; dh为列数,dl为行数
    mov bh, 00h     ; 页码
    int 10h

    ; 显示start booting KNOS
    mov ah, 13h     ; 显示字符串
    mov al, 01h     ; 完成显示后将光标移动至字符串末尾
    mov bh, 00h     ; 页码(第0页)
    mov bl, 0fh     ; 字符属性(白色高亮,黑色背景不闪烁)
    mov dh, 00h     ; 游标行号
    mov dl, 00h     ; 游标列号
    mov cx, 18      ; 字符串长度
    push    ax
    mov ax, ds
    mov es, ax
    pop ax
    mov bp, StartBootMessage        ; es:bp 指向字符串地址
    int 10h

; 栈
StartBootMessage:   db  "Start booting KNOS"

; 填充至512字节,并设置启动扇区识别符
    times   510 - ($ - $$)  db  0
    dw  0xaa55

简单介绍一下上面出现的代码内容,首先是org 0x7c00这个是一个伪指令,它表示这段程序处于内存0x7c00处,之前我们说过了,计算机上电以后,BIOS会将引导扇区复制到这个位置,所以我们必须使用org指令指明其所在位置,如果没有这条指令,编译器会认为程序的起始地址为0x0000,这样会影响到绝对地址寻址。

第二条指令为BaseOfStack equ 0x7c00,equ是一条赋值的指令,这里的意思就是让标识符BaseOfStack的值等于0x7c00,equ指令并不会给标识符分配内存空间,且标识符不能与其他符号同名(可以理解为变成语言的变量不能和保留字相同)。同时,标识符也不能被重新定义。代码里的BaseOfStack是为了给栈指针提供栈基址。

再往下就是jmp short start,jmp short指令用跳转,其跳转范围与short描述一致,即两个字节,这里的意思就是跳转到start:处执行(也就是下面我们要说的那条)。

再往下就进入了启动代码,启动代码的开始部分使用start:进行标识,紧接在这条命令之上就是跳转到这个位置的指令,那么可能有同学要问:“既然都挨在一起,为啥还要单独写一条指令来进行跳转呢,这不是脱裤子放屁吗? :bella_?: :bella_?: :bella_?: ”实际上,这一版的代码只是为了最快得到显示效果,在后续的迭代更新中,还会在这个扇区里构建FAT12文件系统,其部分内容就写在跳转指令和start标签之间,所以这里就先把跳转写上去了,反正早晚都要写的嘛。 :ava_zzz:

下面几行是初始化寄存器相关的代码,意思就是将cs寄存器的段基地址设置到ds、es、ss等寄存器中,最后再设置sp栈指针寄存器。

继续向下看,后面几段代码都是屏幕信息显示相关的内容了,简单来说就是设置各种参数,然后通过BIOS中断功能进行调用,各个参数和功能号在注释里写得都比较清楚了,如果还是有不懂的同学,可以去网上搜索相关资料。

需要单独介绍的可能就只有int 10h,首先在汇编语言当中,16进制数有两种表示方法,一种是0x0000(以0x开头),一种是0000h(以h结尾),当然,这两种表示方法上面的代码都有用到。这里的int指令表示调用中断功能。中断功能可以简单理解为BIOS中预置的一些函数功能,这些功能在实模式下可供我们直接使用。具体有关nasm的详细的内容,可以在这里找到。当然,也可以自行在网上搜索和阅读相关资料。

完成屏幕设置相关代码后,来到了StartBootMessage: db "Start booting KNOS"这条指令,其实严格来说,这是两条指令,或者说一个标签加一条伪指令,只是我把它们写到了一起,这一句的意思是在栈上使用一段内存控件来存放Start booting KNOS这个字符串内容,其中,每个字符会占用1个字节(DB中的B指的是Byte,即字节,也有其他长度的指令,后续用到再介绍)。然后StartBootMessage指向了这个字符串的起始地址,标签的用法和上面的start:是一样的。这一段字符串就是我们要显示的字符串。

最后是times 510 - ($ - $$) db 0dw 0xaa55这两条指令。还记得我们之前说过,BIOS识别扇区是否为引导扇区,是以0x55和0xaa这两个字节为准吗,这一段代码就是来实现在510和511这两个字节处填写0x55和0xaa魔法数的。其中第一句表示使用0,以db(字节,8位)为单位填充0,直到第510字节,第二句是以dw(字,16位,用法上与db差不多只是宽度不同)填充0xaa55。这里之所以是0xaa55而非0x55aa,是因为intel的CPU使用小端存储,按照这样的顺序填充,在二进制文件中的顺序才是0x55、0xaa。

完成这些代码的编写后,将其保存为boot.asm,并保存在bootloader文件夹下。

编译

在介绍编译之前,我们要先了解一个非常好用的工具:make。

虽然我们现在只需要编译一个文件(boot.asm),直接使用nasm boot.asm指令即可,但是在系统逐渐完善的过程当中,不可能把所有代码全部写进一个文件里面。所以我们面临着一个问题:需要同时编译编译几十个甚至上百个文件,这个时候,我们就需要使用到make这个特别方便的自动化编译工具了。

这部分的具体内容我已经写到操作系统开发番外篇(1)——makefile中了,如果对make工具没有任何经验的同学可以去看看,会用make的就可以直接跳过了。

下面是makefile的内容,直接在/bootloader/文件夹下新建一个名叫Makefile的文件,然后将下面这些内容写入其中即可:

1
2
3
4
5
6
7
8
# /bootloader/makefile
# bootloader makefile
INCLUDE = -I include
# 默认编译指令
all: boot.bin
# bootloader
boot.bin : boot.asm
    nasm $< -o $@ $(INCLUDE)

简单介绍一下,首先是注释部分,注释第一行表示当前文件的文件名,可以方便大家在阅读代码时快速定位到这个文件,后面在写其他代码也一样,会在第一行注释标明其位置。代码部分最开始声明了一个变量INCLUDE,这个变量是include文件夹的路径参数。这个文件夹暂时还没有,不过我们后续会用到,所以留一个位置,include文件夹里面保存了一些库、头文件一类需要导入的文件。

目前来说,一共就只有bootloader/boot.asm这一个文件需要编译,所以暂时只有boot.bin : boot.asm这一个规则。还有一处值得提起的是all: boot.bin,其指明了make工具的默认工作。

完成了第一版makefile的编写,我们先来尝试一下编译这个文件。进入wsl,切换目录到/bootloader/,然后输入以下命令:

1
make

可以看到控制台输出了所执行的命令nasm boot.asm -o boot.bin -I include,然后在/bootloader/目录下生成了一个boot.bin文件。这个就是之前的汇编程序编译成二进制的文件。

软盘镜像

我们可以观察一下这个文件属性的大小,刚好512B,正好是一个扇区的大小。但是我们没有办法直接把这个文件用在虚拟机上,因为它是只一个扇区的数据(可以看一下其中的内容,如下图),并非一个数据载体(光盘、硬盘、U盘等)的镜像,所以我们需要把这个文件里的内容写到一个软盘镜像文件里面。

引导扇区二进制内容

为了生成一个fat12格式的软盘镜像(img),我们会使用到下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
#!/usr/bin/ruby
# Manipulate FAT12 (2HD disk format) image file.
# Usage:
#   fat12img [image file name] [command] [arguments...]
# Commands:
#   format : Format image file.
#   dir : Show directory files.
#   save [target file name] : Put file entity into image file.
#   write [target file name] [cluster] : Put file entity at cluster directly.
FD_SIZE = 1474560
FAT1_ADDRESS = 0x0200
FAT_BYTES = 0x1200
FAT2_ADDRESS = 0x1400
DIR_ADDRESS = 0x2600
ENTITY_ADDRESS = 0x3e00
FILEINFO_BYTES = 32
FILE_COUNT = 224
CLUSTER_BYTES = 512
FILE_NORMAL = 0x20
def setstrlen(str, len, pad = ' ')
  if str.length < len
    str + pad * (len - str.length)
  elsif str.length > len
    str[0...len]
  else
    str
  end
end
def filename83(filename)
  fname = setstrlen(File.basename(filename, '.*'), 8)
  ext = File.extname(filename)
  ext = ext[1..-1] if ext[0] == '.'
  ext = setstrlen(ext, 3)
  return fname + ext
end
def writeopen(filename, &block)
  open(filename, File::WRONLY | File::BINARY) do |f|
    block.call(f)
  end
end
def calc_timestamp(time)
  d = ((time.year - 1980) << 9) | ((time.month - 1) << 5) | (time.day - 1)
  t = (time.hour << 11) | (time.min << 5) | (time.sec / 2)
  return d, t
end
class Fat
  attr_accessor :fat
  def initialize(filename)
    @filename = filename
    data = nil
    open(filename, 'rb') do |f|
      f.seek(FAT1_ADDRESS)
      data = f.read(FAT_BYTES).unpack('C*')
    end
    @fat = []
    (0...FAT_BYTES / 3).each do |j|
      i = j * 3
      n1 = data[i + 0] | ((data[i + 1] & 0x0f) << 8)
      n2 = (data[i + 1] >> 4) | (data[i + 2] << 4)
      @fat.push(n1)
      @fat.push(n2)
    end
  end
  def write
    data = []
    (0...FAT_BYTES / 3).each do |i|
      n1 = @fat[i * 2 + 0]
      n2 = @fat[i * 2 + 1]
      data.push(n1 & 0xff)
      data.push(((n2 & 0x0f) << 4) | (n1 >> 8))
      data.push(n2 >> 4)
    end
    data = data.pack('C*')
    writeopen(@filename) do |f|
      f.seek(FAT1_ADDRESS)
      f.write(data)
      f.seek(FAT2_ADDRESS)
      f.write(data)
    end
  end
  def have_space?(cluster_count)
    start = nil
    n = 0
    @fat.size.times do |i|
      x = @fat[i]
      if x == 0
        start = i unless start
        n += 1
        return start if n >= cluster_count
      end
    end
    return false
  end
  def find_free_cluster(start)
    (start...@fat.size).each do |i|
      return i if @fat[i] == 0
    end
    return false
  end
  def put_next(cluster, value)
    @fat[cluster] = value
  end
  def get_next(cluster)
    @fat[cluster]
  end
  def del(cluster, size)
    no = (size + CLUSTER_BYTES - 1) / CLUSTER_BYTES
    no.times do |i|
      p = @fat[cluster]
      @fat[cluster] = 0  # Free
      if p >= 0xff8
        break if i == no - 1
        $stderr.puts "FAT broken"
        exit(1)
      end
      cluster = p
    end
  end
end
class FileInfo
  attr_accessor :filename, :type, :time, :date, :cluster, :size
  def initialize(filename, type, time, date, cluster, size)
    @filename = filename
    @type = type
    @time = time
    @date = date
    @cluster = cluster
    @size = size
  end
  def pack
    return (@filename +
            ([@type] +
             [0] * 10 +
             [@time,
              @date,
              @cluster,
              @size]).pack('C11vvvV'))
  end
  def get_date
    year = ((@date >> 9) & 0x7f) + 1980
    mon = ((@date >> 5) & 0x0f) + 1
    day = (@date & 0x1f) + 1
    return year, mon, day
  end
  def get_time
    hour = ((@time >> 11) & 0x1f)
    min = ((@time >> 5) & 0x3f)
    sec = (@time & 0x1f) * 2
    return hour, min, sec
  end
end
class DirInfo
  def initialize(filename, start, count)
    @filename = filename
    @start = start
    @count = count
    @fileinfos = []
    open(filename, 'rb') do |f|
      f.seek(start)
      count.times do
        bin = f.read(FILEINFO_BYTES)
        filename = bin[0...11]
        type = bin[11].unpack('C')[0]
        time, date, cluster, size = bin[22..-1].unpack('vvvV')
        head = filename[0].ord
        if head == 0
          @fileinfos.push(nil)
        elsif head == 0xe5
          @fileinfos.push(false)
        else
          @fileinfos.push(FileInfo.new(filename, type, time, date, cluster, size))
        end
      end
    end
  end
  def size
    @fileinfos.size
  end
  def get(index)
    return @fileinfos[index]
  end
  def find_free_index(start = 0)
    (start...@fileinfos.size).each do |i|
      return i unless @fileinfos[i]
    end
    return false
  end
  def add(index, filename, size, cluster)
    date, time = calc_timestamp(Time.now)
    @fileinfos[index] = FileInfo.new(filename, FILE_NORMAL, time, date, cluster, size)
  end
  def find(filename)
    index = find_index(filename)
    return index ? get(index) : false
  end
  def del(filename)
    index = find_index(filename)
    unless index
      return nil, nil
    end
    fileinfo = @fileinfos[index]
    @fileinfos[index] = false  # Delete
    return fileinfo.cluster, fileinfo.size
  end
  def write
    writeopen(@filename) do |f|
      f.seek(@start)
      @fileinfos.each do |fileinfo|
        break if fileinfo === nil
        if fileinfo
          f.write(fileinfo.pack)
        else
          deleted = "\xe5" + "\0" * (FILEINFO_BYTES - 1)
          f.write(deleted)
        end
      end
    end
  end
  def find_index(filename)
    (0...@fileinfos.size).each do |i|
      fileinfo = @fileinfos[i]
      break if fileinfo === nil
      next unless fileinfo
      if filename == fileinfo.filename
        return i
      end
    end
    return false
  end
end
def format(filename)
  open(filename, 'wb') do |f|
    img = [0] * FD_SIZE
    img[FAT1_ADDRESS + 0] = img[FAT2_ADDRESS + 0] = 0xf0
    img[FAT1_ADDRESS + 1] = img[FAT2_ADDRESS + 1] = 0xff
    img[FAT1_ADDRESS + 2] = img[FAT2_ADDRESS + 2] = 0xff
    f.write(img.pack('C*'))
  end
end
def write_cluster(f, cluster_no, data)
  f.seek(ENTITY_ADDRESS + cluster_no * CLUSTER_BYTES)
  f.write(data[0...CLUSTER_BYTES])
end
def save(filename, target_fn, fat, dirinfo)
  return false unless File.exists?(target_fn)
  size = File.size(target_fn)
  cluster_count = (size + CLUSTER_BYTES - 1) / CLUSTER_BYTES
  start_cluster = fat.have_space?(cluster_count)
  return false unless start_cluster
  file_index = dirinfo.find_free_index
  return false unless file_index
  data = open(target_fn, 'rb').read
  # Write fileinfo.
  dirinfo.add(file_index, filename83(target_fn), data.size, start_cluster)
  # Write data and FAT.
  prev_cluster_no = -1
  writeopen(filename) do |f|
    loop do
      cluster_no = fat.find_free_cluster(prev_cluster_no + 1)
      if prev_cluster_no >= 0
        fat.put_next(prev_cluster_no, cluster_no)
      end
      if size <= CLUSTER_BYTES  # Last cluster.
        fat.put_next(cluster_no, 0xfff)  # End mark.
        write_cluster(f, cluster_no, data)
        break
      end
      write_cluster(f, cluster_no, data)
      prev_cluster_no = cluster_no
      data = data[CLUSTER_BYTES..-1]
      size -= CLUSTER_BYTES
    end
  end
  return true
end
def delete(image_fn, target_fn)
  fn = filename83(target_fn)
  dirinfo = DirInfo.new(image_fn, DIR_ADDRESS, FILE_COUNT)
  index, size = dirinfo.del(fn)
  return false unless index
  fat = Fat.new(image_fn)
  fat.del(index, size)
  dirinfo.write
  fat.write
  return true
end
def load(image_fn, target_fn, fat)
  fn = filename83(target_fn)
  dirinfo = DirInfo.new(image_fn, DIR_ADDRESS, FILE_COUNT)
  finfo = dirinfo.find(fn)
  return false unless finfo
  open(image_fn, 'rb') do |f|
    cluster = finfo.cluster
    sizeLeft = finfo.size
    while sizeLeft > 0
      f.seek(ENTITY_ADDRESS + cluster * CLUSTER_BYTES)
      size = [sizeLeft, CLUSTER_BYTES].min
      dat = f.read(size)
      print dat
      sizeLeft -= size
      cluster = fat.get_next(cluster)
    end
  end
  return true
end
def main
  image_fn = ARGV.shift
  command = ARGV.shift
  case command
  when 'format'  # Format.
    format(image_fn)
  when 'dir'  # Print directry entries.
    dirinfo = DirInfo.new(image_fn, DIR_ADDRESS, FILE_COUNT)
    dirinfo.size.times do |i|
      m = dirinfo.get(i)
      break if m === nil
      if m
        year, mon, day = m.get_date
        hour, min, sec = m.get_time
        ext = m.filename[8...11]
        dot = ext != '   ' ? '.' : ' '
        puts(sprintf("%-8s%s%-3s %7d %04d/%02d/%02d %02d:%02d:%02d", m.filename[0...8], dot, ext, m.size, year, mon, day, hour, min, sec))
      end
    end
  when 'save'  # Save file into FD image.
    target_fn = ARGV.shift
    delete(image_fn, target_fn)
    dirinfo = DirInfo.new(image_fn, DIR_ADDRESS, FILE_COUNT)
    fat = Fat.new(image_fn)
    unless save(image_fn, target_fn, fat, dirinfo)
      $stderr.puts "Save failed #{target_fn}"
      exit(1)
    end
    dirinfo.write
    fat.write
  when 'load'  # Load file from FD image.
    target_fn = ARGV.shift
    fat = Fat.new(image_fn)
    unless load(image_fn, target_fn, fat)
      $stderr.puts "Loading '#{target_fn}' failed."
      exit(1)
    end
  when 'del'  # Delete file from FD image.
    target_fn = ARGV.shift
    unless delete(image_fn, target_fn)
      $stderr.puts "Not found #{target_fn}"
      exit(1)
    end
  when 'write'  # Write file into the cluster directly, without directory table.
    target_fn = ARGV.shift
    cluster = ARGV.shift.to_i
    unless File.exists?(target_fn)
      $stderr.puts "Not found #{target_fn}"
      exit(1)
    end
    writeopen(image_fn) do |f|
      f.seek(cluster * CLUSTER_BYTES)
      data = open(target_fn, 'rb').read
      f.write(data)
    end
  end
end
if $0 == __FILE__
  main
end

建议大家在/tools/下新建一个fat12img文件,然后把这段代码复制进去(需要注意的是,这段代码共412行,在复制的时候不要漏掉了)。这段代码不是我写的,是当年我在网上找到的,具体的出处现在已经不知道了。

另外,这段代码需要ruby环境,没有的同学使用apt工具安装即可(apt install ruby)。

在准备好上述工具以后,在/(项目的根目录)下创建一个Makefile,用来制作软盘镜像,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# /Makefile

FAT12IMG=tools/fat12img
BIN_DIR = ./bin/

# 需要拷贝到硬盘中的文件(暂时)
DISK_FILES =

# 默认规则
all: bootloader KNOS.img

.PHONY: bootloader KNOS.img

# 生成系统和软件的镜像
KNOS.img: bootloader
    $(FAT12IMG) $@ format
    $(FAT12IMG) $@ write $(BIN_DIR)boot.bin 0
    for filename in $(DISK_FILES); do \
      $(FAT12IMG) $@ save $$filename; \
    done

# 一般规则
## 编译bootloader中的代码
bootloader:
    make -C bootloader

# 仅保留源代码(暂时)
clean:
    rm -f $(BIN_DIR)*.bin
    rm -f *.img

这里再单独解释一下部分代码。

首先是.PHONY: bootloader KNOS.img这一行,".PHONY:"的作用是强行按照所指定的规则来进行编译,在这里的意思自然就是强行编译bootloader和KNOS.img这两个规则。

然后可能需要解释一下bootloader的规则:make -C bootloader。首先这是一条make命令,这个make命令指定了一个选项-C,后面跟了一个参数bootloader,这个选项的意思就是执行后面参数的文件夹下的makefile,在这里也就是执行/bootloader/Makefile,如果需要参数,直接跟在后面即可,例如make -C bootloader clean,则表示执行/bootloader/Makefile中的clean规则。

在编写完生成镜像的规则后,还需要对bootloader中的makefile进行一些修改,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# /bootloader/Makefile
# bootloader makefile
# 添加了一个BIN_DIR变量,该变量值为bin文件目录的位置+
INCLUDE = -I include
BIN_DIR = ../bin/
# 默认编译指令
all: boot.bin
# 新增了.PHONY:规则,用法上面说过了
.PHONY: boot.bin
# bootloader
# 编译后加了一行命令将bin文件移动到bin文件目录
boot.bin : boot.asm
    nasm $< -o $@ $(INCLUDE)
    mv boot.bin $(BIN_DIR)

对于增加的部分,代码和注释已经写得比较清楚了,这里就不再详细说明。不过需要说明的是:一是,以后编译后的二进制文件都会放在/bin/下,包括汇编或者C语言编译生成的二进制文件。二是,执行mv boot.bin $(BIN_DIR)这条命令(也就是执行/bootloader/Makefile)前,需要确保/bin/目录存在,否则可能报错。当然,也可以自己写几行命令来判断该目录是否已经存在,如果不存在则创建。

一切就绪以后,返回根目录执行make clean执行,删除一下镜像和二进制文件,然后执行make重新编译,在根目录会生成一个KNOS.img

启动

经过前面这些步骤所得到的KNOS.img可以说就是我们的“第一版操作系统”了,其功能很简单,就是在屏幕上打印Start booting KNOS字符串。接下来,我们就要试试把这个镜像安装到VMware虚拟机上,然后开机试试了。

首先需要创建一台新的虚拟机,其过程不难,所以我用文字描述一下就行了,也懒得去截图了。 :ava_bitipop: :ava_bitipop: :ava_bitipop:

我使用的16.2.4 build-20089737,不同版本的界面可能有所不同,不过大体步骤都是一样的。

  1. 进入VMware workstation后,选择菜单中的 文件->新建虚拟机,然后会打开新建虚拟机向导。
  2. 安装方法选择自定义(高级)
  3. 硬件兼容性不用修改,选择下一步
  4. 安装客户机操作系统选择稍后安装操作系统(因为我们的"系统"就在软盘里面,不用安装)
  5. 客户机操作系统选择Linux->其他 Linux 2.2.x 内核
  6. 虚拟机的名称和位置大家可以自己定义
  7. 处理器数量选择1,每个处理器的内核数量也选1,等后续开发SMP时再增加核心数量
  8. 内存分配512MB即可,如果有需要后续再增加
  9. 网络连接使用默认即可(前中期暂时不会涉及到网卡开发)
  10. 后续的IO磁盘类型和控制器类型都不用修改,默认即可
  11. 磁盘大小4G即可
  12. 一直下一步直到完成

此时,虚拟机已经创建完毕,但是如果这个时候启动的话什么都没有,因为我们还没有设置挂载软盘(系统盘)。挂载方法按照如下操作即可:

  1. 在左侧菜单栏,我的计算机中找到刚才创建的虚拟机并右键点击选择设置
  2. 进入设置页面后在左下角找到添加按钮并点击
  3. 打开添加硬件向导后,添加硬件类型选择软盘驱动器
  4. 返回到虚拟机设置窗口,在设备中选中软盘,将左侧连接菜单中的使用物理驱动器修改为使用软盘映像文件
  5. 点击浏览,并找到之前编译生成的镜像,然后点击确定保存即可

完成上述步骤后,点击开启此虚拟机,然后就可以看到屏幕上显示的Start booting KNOS字符串了,具体效果如下图:

好好好