裸机控制权与引导程序

实验题目

裸机控制权与引导程序

实验目的

  • 掌握 NASM 的语法
  • 掌握用汇编器的用法
  • 掌握创建空白软盘镜像的方法
  • 掌握虚拟机的设置及使用方法

实验要求

搭建和应用实验环境

虚拟机安装,生成一个基本配置的虚拟机 XXXPC 和多个 1.44MB 容量的虚拟软盘,将其中一个虚拟软盘用 DOS 格式化为 DOS 引导盘,用 WinHex 工具将其中一个虚拟软盘的首扇区填满你的个人信息。

接管裸机的控制权

设计 IBM_PC 的一个引导扇区程序,程序功能是:用字符‘A’从屏幕左边某行位置 45 度角下斜射出,保持一个可观察的适当速度直线运动,碰到屏幕的边后产生反射,改变方向运动,如此类推,不断运动;在此基础上,增加你的个性扩展,如同时控制两个运动的轨迹,或炫酷动态变色,个性画面,如此等等,自由不限。还要在屏幕某个区域特别的方式显示你的学号姓名等个人信息。将这个程序的机器码放进放进第三张虚拟软盘的首扇区,并用此软盘引导你的 XXXPC,直到成功。

实验方案

实验环境

软件

  • Windows 10, 64-bit (Build 17763) 10.0.17763
  • Windows Subsystem for Linux [Ubuntu 18.04.2 LTS]:WSL 是以软件的形式运行在 Windows 下的 Linux 子系统,是近些年微软推出来的新工具,可以在 Windows 系统上原生运行 Linux。
  • gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04):C 语言程序编译器,Ubuntu 自带。
  • NASM version 2.13.02:汇编程序编译器,通过sudo apt install nasm安装在 WSL 上。
  • Oracle VM VirtualBox 6.0.4 r128413 (Qt5.6.2):轻量开源的虚拟机软件。
  • VSCode - Insiders v1.33.0:好用的文本编辑器,有丰富的插件。
  • hexdump for VSCode 1.7.2: VSCode 中一个好用的十六进制显示插件。

这里,没有使用老师推荐的 WinHex 来打开十六进制,而是使用了 VSCode 下一个相对好用的插件来查看十六进制;同时,向软盘内填充信息可以直接敲代码解决。

此外,大部分开发环境安装在 WSL 上,较之于双系统、虚拟机等其他开发方案,更加方便,也方便直接使用 Linux 下的一些指令。

硬件

开发环境配置

所用机器型号为 VAIO Z Flip 2016

  • Intel(R) Core(TM) i7-6567U CPU @3.30GHZ 3.31GHz
  • 8.00GB RAM
虚拟机配置
  • 处理器内核总数:1
  • RAM:4MB

实验过程

搭建和应用实验环境

安装、配置虚拟机

  1. 打开 Oracle VM VirtualBox
  2. 点新建,名称填「WuKPC」,并指定位置,类型选「Other」,版本选择「Other/Unknown」,点「下一步」
  3. 内存选 4MB
  4. 选不添加虚拟硬盘

创建并格式化虚拟软盘

Linux 上可以直接用如下的指令创建虚拟软盘,其中要注意的是 1.44MB=1440KB。

/sbin/mkfs.msdos -C a.img 1440

同目录下出现a.img,得到如下反馈

mkfs.fat 4.1 (2017-01-24)

这说明生成的虚拟软盘已经自动格式化了。打开「设置」-「存储」-「添加软盘驱动器」-「完成」,然后在新生成的控制器中添加虚拟软驱,点「注册」,打开刚刚生成的「a.img」,并确认。现在开启虚拟机,发现虚拟机的屏幕上已经有了显示:

图片描述

这说明虚拟软盘格式化成功。

填满个人信息

使用 hexdump for VSCode 打开刚刚创建的a.img,发现其有着如下的结构(以下仅截取前三十四行):

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: EB 3C 90 6D 6B 66 73 2E 66 61 74 00 02 01 01 00    k<.mkfs.fat.....
00000010: 02 E0 00 40 0B F0 09 00 12 00 02 00 00 00 00 00    .`[email protected]..........
00000020: 00 00 00 00 00 00 29 95 B1 98 3C 4E 4F 20 4E 41    ......).1.<NO.NA
00000030: 4D 45 20 20 20 20 46 41 54 31 32 20 20 20 0E 1F    ME....FAT12.....
00000040: BE 5B 7C AC 22 C0 74 0B 56 B4 0E BB 07 00 CD 10    >[|,"@t.V4.;..M.
00000050: 5E EB F0 32 E4 CD 16 CD 19 EB FE 54 68 69 73 20    ^kp2dM.M.k~This.
00000060: 69 73 20 6E 6F 74 20 61 20 62 6F 6F 74 61 62 6C    is.not.a.bootabl
00000070: 65 20 64 69 73 6B 2E 20 20 50 6C 65 61 73 65 20    e.disk...Please.
00000080: 69 6E 73 65 72 74 20 61 20 62 6F 6F 74 61 62 6C    insert.a.bootabl
00000090: 65 20 66 6C 6F 70 70 79 20 61 6E 64 0D 0A 70 72    e.floppy.and..pr
000000a0: 65 73 73 20 61 6E 79 20 6B 65 79 20 74 6F 20 74    ess.any.key.to.t
000000b0: 72 79 20 61 67 61 69 6E 20 2E 2E 2E 20 0D 0A 00    ry.again........
000000c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000130: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000140: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000150: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000160: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000170: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000190: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 AA    ..............U*
00000200: F0 FF FF 00 00 00 00 00 00 00 00 00 00 00 00 00    p...............

要填满在第一个扇区除了引导区之外填满个人信息,只需要在0x500B~0x1f00D间填满个人信息即可。这里直接偷懒用一段 C(b.c)解决:

#include <stdio.h>
char s[] = "17341163-wu-kan ", b[1440 << 10];
int main()
{
	fread(b, sizeof(*b), sizeof(b), fopen("a.img", "rb+"));
	for (int i = 91; i < 510; ++i)
		b[i] = s[i % (sizeof(s) - 1)];
	fwrite(b, sizeof(*b), sizeof(b), fopen("b.img", "wb"));
}

运行得到的b.img,其十六进制有着如下的结构(前三十四行),可以发现已经填充成功。

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000: EB 3C 90 6D 6B 66 73 2E 66 61 74 00 02 01 01 00    k<.mkfs.fat.....
00000010: 02 E0 00 40 0B F0 09 00 12 00 02 00 00 00 00 00    .`[email protected]..........
00000020: 00 00 00 00 00 00 29 B9 CA 4B BC 4E 4F 20 4E 41    ......)9JK<NO.NA
00000030: 4D 45 20 20 20 20 46 41 54 31 32 20 20 20 0E 1F    ME....FAT12.....
00000040: BE 5B 7C AC 22 C0 74 0B 56 B4 0E BB 07 00 CD 10    >[|,"@t.V4.;..M.
00000050: 5E EB F0 32 E4 CD 16 CD 19 EB FE 2D 6B 61 6E 20    ^kp2dM.M.k~-kan.
00000060: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000070: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000080: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000090: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000000a0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000000b0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000000c0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000000d0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000000e0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000000f0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000100: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000110: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000120: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000130: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000140: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000150: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000160: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000170: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000180: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
00000190: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000001a0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000001b0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000001c0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000001d0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000001e0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 6E 20    17341163-wu-kan.
000001f0: 31 37 33 34 31 31 36 33 2D 77 75 2D 6B 61 55 AA    17341163-wu-kaU*
00000200: F0 FF FF 00 00 00 00 00 00 00 00 00 00 00 00 00    p...............

用虚拟机加载 b.img,如下:

图片描述

接管裸机的控制权

老师给的代码(stone.asm)如下:

; 程序源代码(stone.asm)
; 本程序在文本方式显示器上从左边射出一个*号,以45度向右下运动,撞到边框后反射,如此类推.
;  凌应标 2014/3
;   MASM汇编格式
     Dn_Rt equ 1                  ;D-Down,U-Up,R-right,L-Left
     Up_Rt equ 2                  ;
     Up_Lt equ 3                  ;
     Dn_Lt equ 4                  ;
     delay equ 50000					; 计时器延迟计数,用于控制画框的速度
     ddelay equ 580					; 计时器延迟计数,用于控制画框的速度
     .386
     org 100h					; 程序加载到100h,可用于生成COM
     ASSUME cs:code,ds:code
code SEGMENT
start:
	;xor ax,ax					; AX = 0   程序加载到0000:100h才能正确执行
      mov ax,cs
	mov es,ax					; ES = 0
	mov ds,ax					; DS = CS
	mov es,ax					; ES = CS
	mov ax,0B800h				; 文本窗口显存起始地址
	mov gs,ax					; GS = B800h
      mov byte[char],'A'
loop1:
	dec word[count]				; 递减计数变量
	jnz loop1					; >0:跳转;
	mov word[count],delay
	dec word[dcount]				; 递减计数变量
      jnz loop1
	mov word[count],delay
	mov word[dcount],ddelay

      mov al,1
      cmp al,byte[rdul]
	jz  DnRt
      mov al,2
      cmp al,byte[rdul]
	jz  UpRt
      mov al,3
      cmp al,byte[rdul]
	jz  UpLt
      mov al,4
      cmp al,byte[rdul]
	jz  DnLt
      jmp $

DnRt:
	inc word[x]
	inc word[y]
	mov bx,word[x]
	mov ax,25
	sub ax,bx
      jz  dr2ur
	mov bx,word[y]
	mov ax,80
	sub ax,bx
      jz  dr2dl
	jmp show
dr2ur:
      mov word[x],23
      mov byte[rdul],Up_Rt
      jmp show
dr2dl:
      mov word[y],78
      mov byte[rdul],Dn_Lt
      jmp show

UpRt:
	dec word[x]
	inc word[y]
	mov bx,word[y]
	mov ax,80
	sub ax,bx
      jz  ur2ul
	mov bx,word[x]
	mov ax,-1
	sub ax,bx
      jz  ur2dr
	jmp show
ur2ul:
      mov word[y],78
      mov byte[rdul],Up_Lt
      jmp show
ur2dr:
      mov word[x],1
      mov byte[rdul],Dn_Rt
      jmp show



UpLt:
	dec word[x]
	dec word[y]
	mov bx,word[x]
	mov ax,-1
	sub ax,bx
      jz  ul2dl
	mov bx,word[y]
	mov ax,-1
	sub ax,bx
      jz  ul2ur
	jmp show

ul2dl:
      mov word[x],1
      mov byte[rdul],Dn_Lt
      jmp show
ul2ur:
      mov word[y],1
      mov byte[rdul],Up_Rt
      jmp show



DnLt:
	inc word[x]
	dec word[y]
	mov bx,word[y]
	mov ax,-1
	sub ax,bx
      jz  dl2dr
	mov bx,word[x]
	mov ax,25
	sub ax,bx
      jz  dl2ul
	jmp show

dl2dr:
      mov word[y],1
      mov byte[rdul],Dn_Rt
      jmp show

dl2ul:
      mov word[x],23
      mov byte[rdul],Up_Lt
      jmp show

show:
      xor ax,ax                 ; 计算显存地址
      mov ax,word[x]
	mov bx,80
	mul bx
	add ax,word[y]
	mov bx,2
	mul bx
	mov bx,ax
	mov ah,0Fh				;  0000:黑底、1111:亮白字(默认值为07h)
	mov al,byte[char]			;  AL = 显示字符值(默认值为20h=空格符)
	mov es:[bx],ax  		;  显示字符的ASCII码值
	jmp loop1

end:
    jmp $                   ; 停止画框,无限循环

datadef:
    count dw delay
    dcount dw ddelay
    rdul db Dn_Rt         ; 向右下运动
    x    dw 7
    y    dw 0
    char db 'A'
code ENDS
     END start

在终端里敲下面的指令编译一下看看:

nasm stone.asm -o stone.bin

报错了,一脸懵逼…

stone.asm:11: warning: label alone on a line without a colon might be in error [-w+orphan-labels]
stone.asm:11: error: attempt to define a
local label before any non-local labels
stone.asm:13: error: parser: instruction
expected
stone.asm:14: warning: macro `SEGMENT' exists, but not taking 0 parameters [-w+macro-params]
stone.asm:14: error: parser: instruction
expected
stone.asm:162: error: symbol `code' redefined
stone.asm:162: error: parser: instruction expected
stone.asm:163: error: parser: instruction expected

把上面这段东西放在 Stack Overflow 上搜一下看看,找到一个差不多的问题(见参考文献),仅有一个的回答是这样的;

That assembly language is MASM, not NASM.

For starters, NASM segments are defined differently. Instead of

Code segment word public ‘CODE’

we write

.section text

And that “ASSUME” declaration… You must have an ancient book. That is old, old MASM code. Brings back memories from the early 1980s for me!

There are many differences between NASM and MASM, and your code needs quite a bit of work to port. If you want to port that MASM code to NASM, see MASM/NASM Differences or the NASM documentation or google “NASM vs MASM”

TL;DR: you are writing MASM code, not NASM.

就是说,老师给我们的是「from the early 1980s」的「MASM」代码而不是「NASM」。 干脆重构了老师的代码:

  • 又要显示学号,又要有弹跳的a,不如让学号跟着一起弹跳(a在我的姓名里有)。于是每次打印整个字符串,并设置弹跳的右边界为WIDTH-LENGTH
  • 打印前有一句mov ah,0Fh,是用来设置输出字符串的颜色的(黑底白字),这里去掉它,这时输出字符的颜色就会随行号(ah中原来的值)来变化。
  • 原先老师用来实现延迟是使用了两层循环+两个dw变量,感觉没有必要,因此修改成单层循环+一个dd变量。
  • 几处「撞墙」检测的逻辑修改。原先的逻辑是假如当前下标出界,修改方向,并把下标调整到正常的位置。修改之后的逻辑是假如当前下标在边界上,改变方向。修改后的逻辑更加简洁有效。
  • 某些代码逻辑的优化,例如在进行运算时减少中间变量的过渡。这样可以减小生成代码的体积。

重构的代码(c.asm)如下,编译生成的代码只有 290bytes,可见这个重构还是很有效的。

2019-03-19 update:这是直接从老师的代码中改来的代码,有一个 BUG,就是字符运行到四个顶点的时候会从角上飞出,导致显示内容错乱;原因是老师只考虑了边反射没有考虑角反射。在下一个实验「加载用户程序的监控程序」里,我再次重构了这段代码,修复了这个 BUG,并且大大减小了编译后的大小(137bytes)。

DNRT equ 1	; D-Down,U-Up,R-right,L-Left
UPRT equ 2
UPLT equ 3
DNLT equ 4
WIDTH equ 80
HEIGHT equ 25
LENGTH equ 17	; 字符串的长度
org 7c00h
section .data
	count dd 1
	x dw 0
	y dw 0
	rdul db DNRT	; 向右下运动
	message db ' 17341163 wu-kan '
section .text
	mov ax,0B800h
	mov gs,ax	; GS = 0xB800h,文本窗口显存起始地址
myLoop:
	dec dword[count]	; 递减计数变量
	jnz myLoop	; >0:跳转;
	mov dword[count],99999999
	cmp byte[rdul],DNRT
	jz  dnrt
	cmp byte[rdul],UPRT
	jz  uprt
	cmp byte[rdul],UPLT
	jz  uplt
	cmp byte[rdul],DNLT
	jz  dnlt
dnrt:
	inc word[x]
	inc word[y]
	mov ax,HEIGHT-1
	sub ax,word[x]
	jz  dr2ur
	mov ax,WIDTH-LENGTH
	sub ax,word[y]
	jz  dr2dl
	jmp show
dr2ur:
	mov byte[rdul],UPRT
	jmp show
dr2dl:
	mov byte[rdul],DNLT
	jmp show
uprt:
	dec word[x]
	inc word[y]
	mov ax,2
	sub ax,word[x]
	jz  ur2dr
	mov ax,WIDTH-LENGTH
	sub ax,word[y]
	jz  ur2ul
	jmp show
ur2dr:
	mov byte[rdul],DNRT
	jmp show
ur2ul:
	mov byte[rdul],UPLT
	jmp show
uplt:
	dec word[x]
	dec word[y]
	mov ax,2
	sub ax,word[x]
	jz  ul2dl
	mov ax,1
	sub ax,word[y]
	jz  ul2ur
	jmp show
ul2dl:
	mov byte[rdul],DNLT
	jmp show
ul2ur:
	mov byte[rdul],UPRT
	jmp show
dnlt:
	inc word[x]
	dec word[y]
	mov ax,HEIGHT-1
	sub ax,word[x]
	jz  dl2ul
	mov ax,1
	sub ax,word[y]
	jz  dl2dr
	jmp show
dl2ul:
	mov byte[rdul],UPLT
	jmp show
dl2dr:
	mov byte[rdul],DNRT
	jmp show
show:
	xor ax,ax
	mov word ax,[x]
	mov bx,WIDTH
	mul bx
	add word ax,[y]
	mov bx,2
	mul bx
	mov bx,ax
	mov si, message
	mov cx, LENGTH
	printStr:
		mov byte al,[si]
		mov [gs:bx],ax
		inc si
		inc bx
		inc bx
	loop printStr
	jmp myLoop

在终端中依次执行下列指令,将上述汇编代码编译并写进新的软驱c.img

nasm c.asm -o c.com
/sbin/mkfs.msdos -C c.img 1440
dd if=c.com of=c.img bs=1440k conv=notrunc

用虚拟机打开c.img,运行结果如下:

图片描述

实验总结

终于有空静下心来做操作系统实验。在第一次的实验过程中,我遇到了很多的问题,包括编译失败、在虚拟机上运行没有正确输出等。通过查找资料、自己动手尝试,检查究竟是哪里出问题,找到问题的根源。

同时,也对老师提供的实验思路展开了一些自己的思考。例如:

  1. 向软盘的第一个扇区内填充自己的个人信息,这里我直接使用一段 C 语言,用二进制打开生成的软盘文件进行操作,方便快捷。
  2. 向软盘内写入自己的引导扇区程序,老师课上演示的方案是使用 WinHex 打开之后手动拷贝,并修改扇区尾部。但是,Linux 下其实是有着自己的软盘拷贝指令的(见参考资料),使用这个指令简化了很多操作。

参考文献