如果这篇博客帮助到你,可以请我喝一杯咖啡~
CC BY 4.0 (除特别声明或转载文章外)
实验题目
中断控制
实验目的
- 学习中断机制知识,掌握中断处理程序设计的要求
- 设计一个汇编程序,实现时钟中断处理程序
- 扩展 MyOS2,增加时钟中断服务,利用时钟中断实现与时间有关的操作
实验要求
- 操作系统工作期间,利用时钟中断,在屏幕 24 行 79 列位置轮流显示
|
、/
和\
,适当控制显示速度,以方便观察效果。 - 编写键盘中断响应程序,原有的你设计的用户程序运行时,键盘事件会做出有事反应:当键盘有按键时,屏幕适当位置显示
OUCH!OUCH!
。 - 在内核中,对 33 号、34 号、35 号和 36 号中断编写中断服务程序,分别在屏幕 1/4 区域内显示一些个性化信息。再编写一个汇编语言的程序,作为用户程序,利用
int 33
、int 34
、int 35
和int 36
产生中断调用你这 4 个服务程序。
实验方案
实验环境
软件
- 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 中一个好用的十六进制显示插件。
- GNU Make 4.1:安装在 Ubuntu 下,一键编译并连接代码,生成最终的文件。
大部分开发环境安装在 WSL 上,较之于双系统、虚拟机等其他开发方案,更加方便,也方便直接使用 Linux 下的一些指令。
硬件
开发环境配置
所用机器型号为 VAIO Z Flip 2016
- Intel(R) Core(TM) i7-6567U CPU @3.30GHZ 3.31GHz
- 8.00GB RAM
虚拟机配置
- 处理器内核总数:1
- RAM:4MB
实验原理
本次实验的关键在于写中断向量表。x86 计算机在启动时会自动进入实模式状态。系统的 BIOS 初始化8259A
的各中断线的类型(参见前图),在内存的低位区(地址为0~1023[3FFH]
,1KB)创建含 256 个中断向量的表 IVT (每个向量[地址]
占 4 个字节,格式为:16位段值:16位偏移值
)。
要实现「无敌风火轮」,可以利用时钟中断,对 8 号中断进行编程。在屏幕固定位置显示风火轮的字符,随后将字符修改为下一个。随后将 0x08 放入 0x20 的位置,处理时钟中断函数的入口放入 0x22。最后需要告诉硬件端口已经处理完中断,并正常返回。
在实验过程中,还遇到了进入操作系统时显示风火轮但是没有转起来的问题。经过思考,发现上一个实验中写的getch
是阻塞函数。于是,重新写了这个函数,重复扫描键盘缓冲区,有字符键入时再读进来。
实验过程
实验代码
bootloader.asm
和上一个实验中的代码完全相同,不再放出。
kernel.asm
操作系统内核的汇编部分代码,提供int 33
~int 36
中断在屏幕的四个象限上显示自定义信息,检测到Ctrl + C
时返回。
同时,提供了如下的全局函数供 C 语言部分调用。
_getch
从屏幕上无回显地读入一个字符。_getCursor
返回屏幕光标的位置。_setCursor
设置屏幕光标的位置。_putC
向光标位置写入一个字符。_pageUP
屏幕内容向上滚动。_loadProgram
加载用户程序。
%macro print 5 ; string, length, x, y, color
pusha
push ax
push bx
push cx
push dx
push bp
push ds
push es
mov ax, 0b800H
mov gs, ax
mov ax, cs
mov ds, ax
mov bp, %1
mov ax, ds
mov es, ax
mov cx, %2
mov ax, 1300H
mov dh, %3
mov dl, %4
mov bx, %5
int 10H
pop es
pop ds
pop bp
pop dx
pop cx
pop bx
pop ax
popa
%endmacro
%macro setIVT 2
push es
push ds
push si
pusha
mov ax, 0000H
mov es, ax
mov ax, %1
mov bx, 4
mul bx
mov si, ax
mov ax, %2
mov [es:si], ax
add si, 2
mov ax, cs
mov [es:si], ax
popa
pop si
pop ds
pop es
%endmacro
bits 16
UserPrgOffset equ 0a100H
PrgSectorOffset equ 0
extern terminal
global _start
global _getch
global _getCursor
global _setCursor
global _putC
global _pageUP
global _loadProgram
_start:
setIVT 8, int8
setIVT 33, int33
setIVT 34, int34
setIVT 35, int35
setIVT 36, int36
call terminal
ret
_getCursor:
push ebp
mov ebp, esp
push ebx
sub esp, 4
mov eax, 768
mov edx, 0
mov ebx, edx
int 0x10
mov eax, edx
mov dword [ebp-8], eax
mov eax, dword [ebp-8]
add esp, 4
pop ebx
pop ebp
ret
_getch:
mov ah, 01H
int 16H
jz _getch
mov ah, 00H
int 16H
ret
_setCursor:
push ebp
mov ebp, esp
push ebx
mov eax, 512
mov ecx, 0
mov edx, dword [ebp+8]
mov ebx, ecx
int 0x10
pop ebx
pop ebp
ret
_putC:
push ebp
mov ebp, esp
push ebx
mov eax, dword [ebp+8]
or ah, 9
mov edx, dword [ebp+12]
mov ecx, 1
mov ebx, edx
int 0x10
pop ebx
pop ebp
ret
_pageUP:
push ebp
mov ebp, esp
mov eax, dword [ebp+8]
or ah, 6
mov ecx, 0
mov edx, 184fh
int 0x10
pop ebp
ret
_loadProgram:
push ebp
mov ebp, esp
push ax
push bx
push cx
push dx
push es
mov ax, cs
mov es, ax
mov bx, UserPrgOffset
mov ah, 2
mov al, 2
mov dl, 0
mov dh, 1
mov ch, 0
mov cl, byte [ebp+8]
add cl, PrgSectorOffset
int 13H
call UserPrgOffset
pop es
pop dx
pop cx
pop bx
pop ax
mov esp, ebp
pop ebp
ret
int8:
cli
pusha
push eax
call draw_slash
mov al, 20H
out 20H, al
out 0a0H, al
pop eax
popa
sti
iret
int33:
mov word[n], 12
mov word[m], 30
mov word[top], 2
mov word[left], 40
mov word[length], 8
mov word[msg], msg1
call show
iret
int34:
mov word[n], 12
mov word[m], 30
mov word[top], 2
mov word[left], 0
mov word[length], 10
mov word[msg], msg2
call show
iret
int35:
mov word[n], 12
mov word[m], 20
mov word[top], 13
mov word[left], 0
mov word[length], 20
mov word[msg], msg3
call show
iret
int36:
mov word[n], 12
mov word[m], 14
mov word[top], 13
mov word[left], 40
mov word[length], 26
mov word[msg], msg4
call show
iret
draw_slash:
print bar,1,24,78,7
cmp byte[bar],'|'
jne rslash
mov byte[bar],'/'
ret
rslash:
cmp byte[bar],'/'
jne hslash
mov byte[bar],'-'
ret
hslash:
cmp byte[bar],'-'
jne lslash
mov byte[bar],'\'
ret
lslash:
mov byte[bar],'|'
ret
show:
dec dword[cnt]
jnz show
mov dword[cnt],99999999
mov word ax, [t]
mov word bx, [n]
add bx, bx
sub bx, 2
xor dx, dx
div bx
cmp dx, [n]
jb xok
sub bx, dx
mov dx, bx
xok:
add dx, [top]
mov word[x], dx
mov word ax, [t]
mov word bx, [m]
add bx,bx
sub bx,2
xor dx, dx
div bx
cmp dx, [m]
jb yok
sub bx, dx
mov dx, bx
yok:
add dx,[left]
mov word [y],dx
inc word[t]
print [msg],[length],[x],[y],[x]
mov ah, 01H
int 16H
jz show
print msgouch,10,[x],[y],[x]
mov ah, 00H
int 16H
cmp ax, 2e03H
jne show
ret
datadef:
cnt dd 1
t dw 0
x dw 1
y dw 0
n dw 12
m dw 32
top dw 2
left dw 40
length dw 8
msg dw 1
msg1 db ' wu-kan '
msg2 db ' 17341163 '
msg3 db ' wu.kan@foxmail.com '
msg4 db ' https://wu-kan.github.io '
msgouch db 'Ouch!Ouch!'
bar db '|'
kerner.c
操作系统内核 C 语言部分的代码。
和上一实验相比:
- 去掉了全部的内嵌汇编,改为调用外部汇编函数
- 修改了
clear
清屏的逻辑,原来是写若干个回车,现在是屏幕向上滚动一页并将光标移至首行首列 - 修改了显示回车的逻辑,原来是写若干个空格直至光标移动到下一行,现在直接移动光标
函数不能传字符变量的问题仍然没有解决。
#define SCREEN_WIDTH 80
#define SCREEN_HEIGHT 25
#define MAX_BUF_LEN (SCREEN_WIDTH * SCREEN_HEIGHT)
#define PROGRAM_NUM 4
extern int _getch();
extern int _getCursor();
extern void _pageUP(int);
extern void _loadProgram(int);
extern void _setCursor(int cur);
extern void _putC(int ch, int color);
void putchar(char c)
{
int cur = _getCursor(), curX = cur >> 8, curY = cur - (curX << 8);
if (c == '\r' || c == '\n')
{
if (++curX >= SCREEN_HEIGHT)
--curX, _pageUP(1);
return _setCursor(curX << 8);
}
_putC(c, 0x07);
if (++curY >= SCREEN_WIDTH)
putchar('\n');
_setCursor(curX << 8 | curY);
}
void gets(char *s)
{
for (;; ++s)
{
putchar(*s = _getch());
if (*s == '\r' || *s == '\n')
break;
}
*s = 0;
}
void printf(const char *s)
{
for (; *s; ++s)
putchar(*s);
}
int strcmp(const char *s1, const char *s2)
{
while (*s1 && (*s1 == *s2))
++s1, ++s2;
return (int)*s1 - (int)*s2;
}
void terminal()
{
const struct
{
const char *name, *size, *command;
int address;
} prg[PROGRAM_NUM] =
{{"prg1", " 3 bytes", "exec 1", 1},
{"prg2", " 3 bytes", "exec 2", 2},
{"prg3", " 3 bytes", "exec 3", 3},
{"prg4", " 3 bytes", "exec 4", 4}};
char str[MAX_BUF_LEN] = "$ ";
printf(str), gets(str);
const char
CLEAR_COM[] = "clear",
HELP_COM[] = "help",
EXEC_COM[] = "exec",
EXIT_COM[] = "exit",
LS_COM[] = "ls";
if (!strcmp(str, CLEAR_COM))
_pageUP(SCREEN_HEIGHT), _setCursor(0);
else if (!strcmp(str, HELP_COM))
{
const char
HELP_INFO[] =
"WuK-shell v0.0.2\n"
"These shell commands are defined internally.\n"
"Command Description\n"
"clear -- Clean the screen\n"
"help -- Show this list\n"
"exec -- Execute all the user programs\n"
"exec [num] -- Execute the num-th program\n"
"exit -- Exit OS\n"
"ls -- Show existing programs\n";
printf(HELP_INFO);
}
else if (!strcmp(str, EXEC_COM))
for (int i = 0; i < PROGRAM_NUM; ++i)
_loadProgram(prg[i].address);
else if (!strcmp(str, EXIT_COM))
return;
else if (!strcmp(str, LS_COM))
for (int i = 0; i < PROGRAM_NUM; ++i)
printf(prg[i].name), printf(prg[i].size), putchar('\n');
else
for (int i = 0;; ++i)
{
if (i == PROGRAM_NUM)
{
printf(str);
const char
COMM_NOT_FOUND[] =
" : command not found, type \'help\' for available commands list.\n";
printf(COMM_NOT_FOUND);
break;
}
if (!strcmp(str, prg[i].command))
{
_loadProgram(prg[i].address);
break;
}
}
terminal();
}
link.ld
将wukos.asm
和kernel.c
两个文件编译出来的内容连接起来。和上一个实验中的完全相同,不再放出。
prg1.asm~prg4.asm
直接调用 int 33、int 34、int 35 和 int 36 四个中断实现(编译出来的大小仅有 3bytes)。
org 0a100H
int 33
ret
上面是 prg1.asm 的内容,其余同理,不再放出。
Makefile
和上一个实验完全相同,不再放出。
运行结果
如上图,进入操作系统后开始了「无敌风火轮」(右下角)。 如上图,使用exec
指令轮流运行我的四个程序,分别调用软中断int 33
~int 36
。按下 Ctrl+C 可以退出程序。程序检测到键盘输入,因此显示Ouch!Ouch!
风火轮仍然在转。 输入若干指令。
风火轮仍然在转。 继续输入若干指令,此时超出屏幕显示范围,自动滚屏。
风火轮仍然在转。
实验总结
这次实验让我深入理解了中断服务程序的工作原理。中断响应后,先到内存指定位置找到中断向量表,然后跳转到中断服务程序。中断服务程序需先保存寄存器。中断服务程序完成后,需先还原寄存器,然后调用中断返回指令。所以在做实验的时候,就需要修改操作系统内核,使操作系统内核修改中断向量表,才能实现自定义中断服务程序。
在实验中,因为乱用宏以及没有恢复寄存器出了挺多问题,好在最终一一解决了。这也让我意识到之前的代码写的有多差、有多少隐患。因此,几乎将原先的代码完全重构了一遍。希望自己还是要多多注意一下这方面的问题。