加载 MBR 运行 x86 汇编程序

用文本模式控制显存在屏幕上打印

Posted by lzzmm on March 13, 2021
About 10 minutes to read

DCS218 - Operating Systems Lab 2021 Spring

前言

本文章介绍了 x86 汇编、计算机的启动过程、IA-32 处理器架构和字符显存原理。编写程序让计算机在启动后加载运行,使用 gdb 来调试程序,以此增进对计算机启动过程的理解。

1.1 输出 Helloworld

  1. 原理:计算机在加电启动时,会自动加载首扇区的 512 字节到内存地址 0x7c00 处执行,即加载 MBR,然后从 MBR 的第一条指令处开始执行。qemu 显示屏实际上是按 25x80 个字符来排列的矩阵,0xB8000 ~ 0xBFFFF 是显存地址,文本模式中低字节表示显示的字符,高字节表示字符的颜色属性。通过编写 MBR 来实现向屏幕输出蓝色的 Hello World。

  2. lab2 文件夹中创建 mbr.asm 文件,写入如下代码:

    0
    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
    
     org 0x7c00
     [bits 16]
     xor ax, ax ; eax = 0
     ; 初始化段寄存器, 段地址全部设为0
     mov ds, ax
     mov ss, ax
     mov es, ax
     mov fs, ax
     mov gs, ax
    
     ; 初始化栈指针
     mov sp, 0x7c00
     mov ax, 0xb800
     mov gs, ax
    
     mov ah, 0x01 ;蓝色
     mov al, 'H'
     mov [gs:2 * 0], ax
    
     mov al, 'e'
     mov [gs:2 * 1], ax
    
     mov al, 'l'
     mov [gs:2 * 2], ax
    
     mov al, 'l'
     mov [gs:2 * 3], ax
    
     mov al, 'o'
     mov [gs:2 * 4], ax
    
     mov al, ' '
     mov [gs:2 * 5], ax
    
     mov al, 'W'
     mov [gs:2 * 6], ax
    
     mov al, 'o'
     mov [gs:2 * 7], ax
    
     mov al, 'r'
     mov [gs:2 * 8], ax
    
     mov al, 'l'
     mov [gs:2 * 9], ax
    
     mov al, 'd'
     mov [gs:2 * 10], ax
    
     jmp $ ; 死循环
    
     times 510 - ($ - $$) db 0
     db 0x55, 0xaa
    

    org 0x7c00 告诉编译器代码中的代码标号和数据标号从 0x7c00 开始。也就是说,这些标号的地址会在编译时加上0x7c00[bits 16] 告诉编译器按16位代码格式编译代码。

    将ax置为0,然后借助于ax将段寄存器清0。由于汇编不允许使用立即数直接对段寄存器赋值,所以我们需要借助于ax。

    段寄存器初始化后,我们开始对显存地址赋值。由于显存地址是从0xB8000开始,而16位的段寄存器最大可表示0xFFFF,因此我们需要借助于段寄存器来寻址到0xB8000处的地址。于是我们将段寄存器gs的值赋值为0xB800

    不赋值为 0xB8000 是因为段地址需要左移 4 位 \(物理地址=段地址<<4+偏移地址\) 然后依次对显存地址赋值来实现在显示屏上输出Hello World。根据显存的显示原理,一个字符使用两个字节表示,故我们将ax的高字节部份ah赋值为颜色属性0x01,低字节部份赋值为对应的字符,然后依次放置到显存地址的对应位置。

    依次输出字符后,我们还没有实现下一步的工作,即bootloader加载内核。因此这里就在做死循环。代码的最后的times指令是汇编伪指令,表示重复执行指令若干次。$表示当前汇编地址,$$表示代码开始的汇编地址。times 510 - ($ - $$) db 0表示填充字符0直到第510个字节。最后我们填充0x55,0xaa表示MBR是可启动的。

  3. 使用 nasm 汇编器来将代码编译成二进制文件

    0
    
     nasm -f bin mbr.asm -o mbr.bin
    

    其中,-f参数指定的是输出的文件格式,-o指定的是输出的文件名。

  4. 生成了 MBR 后,我们将其写入到硬盘的首扇区。我们首先创建一个“硬盘”,这个硬盘其实是一个虚拟磁盘,使用 qemu-img

    0
    
    qemu-img create hd.img 10m
    
  5. 然后将 MBR 写入hd.img的首扇区,写入的命令使用的是 linux 下的 dd 命令。

    0
    
     dd if=mbr.bin of=hd.img bs=512 count=1 seek=0 conv=notrunc
    

    参数的解释如下

    • if表示输入文件。
    • of表示输出文件。
    • bs表示块大小,以字节表示。
    • count表示写入的块数目。
    • seek表示越过输出文件中多少块之后再写入。
    • conv=notrunc表示不截断输出文件,如果不加上这个参数,那么硬盘在写入后多余部份会被截断。
  6. 启动 qemu

    0
    
     qemu-system-i386 -hda hd.img -serial null -parallel stdio 
    
    • -hda hd.img 表示将文件 hd.img 作为第0号磁盘映像。
    • -serial dev 表示重定向虚拟串口到空设备中。
    • -parallel stdio 表示重定向虚拟并口到主机标准输入输出设备中。

    效果如下

    2021-03-13 16-45-56 的屏幕截图

1.2 输出学号

  1. 要求: 请修改 example 1 的代码,使得 MBR 被加载到0x7C00后在$(12,12)$处开始输出你的学号。注意,你的学号显示的前景色和背景色必须和教程中不同。说说你是怎么做的,并将结果截图。

  2. 经过计算可得 $(12,12)$ 的显存地址是

    0xb8000 $+ (12\times80+12)\times2 = $ 0xb8798

    故修改如下

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
     ...
     ; 初始化栈指针
     mov sp, 0x7c00
     mov ax, 0xb879 ; 此处修改段地址
     mov gs, ax
        
        
     mov ah, 0x02 ; 修改颜色为 green
     mov al, '1'
     mov [gs:2 * 4], ax ; 8 开始
        
     mov al, '9'
     mov [gs:2 * 5], ax
     ...
    
  3. 使用nasm汇编器来将代码编译成二进制文件, 写入虚拟硬盘, 使用qemu运行, 结果如下

    Screenshot from 2021-03-21 00-50-37

2.1 实模式中断

  1. 实模式中断 int 10h 由于功能号不同,执行的结果也就不同。

    功能 功能号 参数 返回值
    设置光标位置 AH=02H BH=页码,DH=行,DL=列
    获取光标位置和形状 AH=03H BX=页码 AX=0,CH=行扫描开始,CL=行扫描结束,DH=行,DL=列
    在当前光标位置写字符和属性 AH=09H AL=字符,BH=页码,BL=颜色,CX=输出字符的个数

    注意,“页码”均设置为0。

    一般地,中断的调用方式如下。

    0
    1
    2
    
     将参数和功能号写入寄存器
     int 中断号
     从寄存器中取出返回值
    
  2. 要实现的是实模式下的光标中断,利用中断实现光标的位置获取和光标的移动。位置获取使用 0x03 功能,光标移动使用 0x02 功能。由于不知道要如何移动,故我获取光标位置后将它往右移动一列往下移动一行。

  3. 代码如下。本来想让它循环,但是这样很不好截图,并且由于没有掌握sleep,光标跳得飞快。

    0
    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
    
     org 0x7c00
     [bits 16]
     xor ax, ax ; eax = 0
     ; 初始化段寄存器, 段地址全部设为0
     mov ds, ax
     mov ss, ax
     mov es, ax
     mov fs, ax
     mov gs, ax
        
     ; 初始化栈指针
     mov sp, 0x7c00
     ; mov ax, 0xb000
     ; mov gs, ax
        
     mov ah, 0x03 ; 功能码,表示调用读取光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     int 0x10 ; interrupt
        
     ; movecursor:
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dh, 1 ; 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     ; jmp movecursor
        
     jmp $ ; 死循环
        
     times 510 - ($ - $$) db 0
     db 0x55, 0xaa
    
  4. 同上面的实验一样,编译并写入磁盘(覆盖之前的 `mbr.bin ),然后运行 qemu ,发现光标确实移动了。

2.2 实模式中断输出学号

  1. 思路是设置光标位置后移,然后通过 0x09 功能在该位置写字符和属性。由于我的学号是 19335025,有两个重合的3,故可以利用写两次字符的功能,同时光标後移两位。

  2. 代码如下,我用绿色输出了学号,蓝色输出了 CYH。

    0
    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
    
     org 0x7c00
     [bits 16]
     xor ax, ax ; eax = 0
     ; 初始化段寄存器, 段地址全部设为0
     mov ds, ax
     mov ss, ax
     mov es, ax
     mov fs, ax
     mov gs, ax
        
     ; 初始化栈指针
     mov sp, 0x7c00
     ; mov ax, 0xb000
     ; mov gs, ax
        
     mov ah, 0x03 ; 功能码,表示调用读取光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     int 0x10 ; interrupt
        
     mov bl, 0x02 ; green
     mov al, '1'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt    
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     mov al, '9'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     mov al, '3'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x02 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 2 ; 
     int 0x10 ; interrupt
        
     mov al, '5'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     mov al, '0'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     mov al, '2'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     mov al, '5'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 3 ; 
     int 0x10 ; interrupt
        
     mov bl, 0x01 ; blue
     mov al, 'C'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     mov al, 'Y'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     mov al, 'H'
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
        
     jmp $ ; 死循环
        
     times 510 - ($ - $$) db 0
     db 0x55, 0xaa
    
  3. 编译放入虚拟硬盘启动,效果如下

    2021-03-23 15-21-26 的屏幕截图

2.3 利用键盘中断实现键盘输入并回显

  1. 要求:利用键盘中断实现键盘输入并回显。

  2. 了解到 int 0x16 可以调用键盘I/O中断。键盘I/O中断调用有三个功能,功能号为0, 1, 2,且必须把功能号放在AH中。 00H、10H —从键盘读入字符;03H —设置重复率; 01H、11H —读取键盘状态;04H —设置键盘点击; 02H, 12H —读取键盘标志;05H —字符及其扫描码进栈。

  3. 思路是使用功能0x00,循环判断键盘是否有按键按下,若有按键按下则跳转到回显函数,回显函数与上一个实验内容差不多,都是右移光标输出刚刚输入的内容。功能0x00可以实现:从键盘读入字符送AL寄存器。执行时,等待键盘输入,一旦输入,字符的ASCII码放入AL中。若AL=0,则AH为输入的扩展码。

  4. 在前几个实验的代码框架中修改,添加的代码如下:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
     ; 键盘I/O中断调用(INT 16H
     kbIOint:
     mov ah, 0x00 ;
     int 0x16 ;
     or al, 0x00 ;
     ; and zf, 0x01;
     jnz input
     jmp kbIOint
        
     input:
        
     mov ah, 0x02 ; 功能码,表示调用设置光标位置的中断功能
     mov bh, 0x00 ; 页码,文本状态设为0 
     add dl, 1 ; 
     int 0x10 ; interrupt
        
     mov ah, 0x09 ; 功能码,表示在当前光标位置写字符和属性的中断功能
     mov cx, 0x01 ; 输出字符的个数
     int 0x10 ; interrupt  
        
     jmp kbIOint ;
    
  5. 把键盘上的字符都打了一遍,结果如下(上个实验打印学号的代码没删)

    2021-03-23 15-21-26 的屏幕截图

3 分支/循环逻辑和函数的实现

0
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
; If you meet compile error, try 'sudo apt install gcc-multilib g++-multilib' first

%include "head.include"
; you code here
    mov eax, [a1]
    mov ebx, [a2]
your_if:
; put your implementation here
    cmp eax, 12
    jl lt12 ; if a1 < 12 then
    cmp eax, 24
    jl lt24 ; else if a1 < 24 then
    shl eax, 4  ; a1 = a1 << 4
    mov [if_flag], eax 
    jmp your_while
lt12:
    sar eax, 1 ; a1 /= 2
    inc eax ; a1 ++
    mov [if_flag], eax
    jmp your_while
lt24:
    mov ecx, eax
    sub ecx, 24
    neg ecx 
    imul ecx, eax
    mov [if_flag], ecx
    jmp your_while

your_while:
; put your implementation here
    cmp byte[a2], 12
    jl loopend

    call my_random
    mov ebx, [a2]
    mov ecx, [while_flag]
    mov byte[ecx + ebx - 12] , al
    dec byte[a2]
    jmp your_while

loopend:
%include "end.include"

your_function:
; put your implementation here
    pushad
    mov eax, 0
loop:
    mov ecx, [your_string]
    cmp  byte[ecx + eax], 0
    je funcend
    pushad
    mov  ebx, dword[ecx + eax]
    push ebx
    call print_a_char
    pop ebx
    popad
    add eax, 1
    jmp loop

funcend:
    popad
    ret

image-20210323233929054

4 汇编小程序

0
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
org 7c00h

;定义各个符号
    _DR equ 1
    _UR equ 2
    _UL equ 3
    _DL equ 4
    delay equ 200
    ddelay equ 100

;代码段
;初始化各个寄存器
START:
    mov ax, cs
    mov es, ax
    mov ds, ax
    mov ax, 0b800h
    mov es, ax
    mov si, 0
    mov di, 0

;输出学号姓名
PRINT:    mov bx, name
    mov al, [bx+si]
    cmp al, 0
    jz LOOP1    
    mov bx, 52
    mov byte[es:bx+di], al    
    mov byte[es:bx+di+1], 1
    inc si
    add di, 2
    jmp PRINT

;循环实现延迟
LOOP1:
    dec word[count]
    jnz LOOP1
    
    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], _UR    
          jmp show
dr2dl:
          mov word[y], 78
         mov byte[rdul], _DL    
          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, 0
    sub ax, bx
         jz  ur2dr
    
    jmp show

ur2ul:
          mov word[y], 78
          mov byte[rdul], _UL    
          jmp show
ur2dr:
          mov word[x], 1
          mov byte[rdul], _DR    
          jmp show
    
;往左上移动,判断是否碰壁并显示字符
UpLt:
    dec word[x]
    dec word[y]
    mov bx, word[x]
    mov ax, 0
    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], _DL    
          jmp show
ul2ur:
          mov word[y], 1
          mov byte[rdul], _UR    
          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], _DR    
          jmp show
dl2ul:
          mov word[x], 23
         mov byte[rdul], _UL    
          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, byte[color]      
    mov al, byte[char]    
    mov [es:bx], ax  
    
    inc byte[char]
    cmp byte[char], 'z'+1
    jnz keep
    mov byte[char], '0'

keep:    inc byte[color]
    cmp byte[color], 0x10
    jnz LOOP1
    mov byte[color], 0x40 ;    循环显示不同样式的字符
    jmp LOOP1

end:
    jmp $

    count dw delay        ;一层延迟
    dcount dw ddelay    ;二层延迟
    rdul db _DR        ;方向变量
    color db 0x02        ;样式(颜色)变量
    x dw 0                ;横坐标
    y dw 0                ;纵坐标
    char db 'A'            ;要显示的字符
    name db '19335025 CYH', 0    ;学号姓名

    times 510-($-$$) db 0
    dw 0aa55h

效果如下

2021-03-23 23-31-17 的屏幕截图

2021-03-23 23-31-24 的屏幕截图

4. 总结

通过此次实验熟悉了x86 汇编,并且熟悉了编译asm、写入虚拟磁盘、使用qemu启动虚拟磁盘等知识。

参考

https://gitee.com/nelsoncheung/sysu-2021-spring-operating-system/tree/main

Creative Commons License本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.