上一章搭建了在进行操作系统开发时所需要的环境,这一章我们会第一次接触到汇编,并且写一个能在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 0和dw 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,不同版本的界面可能有所不同,不过大体步骤都是一样的。
- 进入VMware workstation后,选择菜单中的 文件->新建虚拟机,然后会打开新建虚拟机向导。
- 安装方法选择自定义(高级)
- 硬件兼容性不用修改,选择下一步
- 安装客户机操作系统选择稍后安装操作系统(因为我们的"系统"就在软盘里面,不用安装)
- 客户机操作系统选择Linux->其他 Linux 2.2.x 内核
- 虚拟机的名称和位置大家可以自己定义
- 处理器数量选择1,每个处理器的内核数量也选1,等后续开发SMP时再增加核心数量
- 内存分配512MB即可,如果有需要后续再增加
- 网络连接使用默认即可(前中期暂时不会涉及到网卡开发)
- 后续的IO磁盘类型和控制器类型都不用修改,默认即可
- 磁盘大小4G即可
- 一直下一步直到完成
此时,虚拟机已经创建完毕,但是如果这个时候启动的话什么都没有,因为我们还没有设置挂载软盘(系统盘)。挂载方法按照如下操作即可:
- 在左侧菜单栏,我的计算机中找到刚才创建的虚拟机并右键点击选择设置
- 进入设置页面后在左下角找到添加按钮并点击
- 打开添加硬件向导后,添加硬件类型选择软盘驱动器
- 返回到虚拟机设置窗口,在设备中选中软盘,将左侧连接菜单中的使用物理驱动器修改为使用软盘映像文件
- 点击浏览,并找到之前编译生成的镜像,然后点击确定保存即可
完成上述步骤后,点击开启此虚拟机,然后就可以看到屏幕上显示的Start booting KNOS字符串了,具体效果如下图:
到这里,我们就完成了今天的目标了,下一章我们会首次接触到FAT12文件系统,并且编写一个相对更完整的boot引导扇区程序。
当然,如果有同学想在物理机上测试,也可以把这个镜像通过工具刷入U盘中,然后将BIOS启动方式设置为U盘,启动后也能得到同样的效果。
最后,这一章的所有代码都可以在这里找到。
值得注意的是,在后续更新中可能会发现前期写的部分代码可能会存在问题,此时可能会进行一部分修改,文章中的代码仅供参考,具体以提交到GitHub上的为准!!!这一条后面的章节我就不每次都提醒咯!



