中断控制 wu-kan

实验题目

中断控制

实验目的

  • 学习中断机制知识,掌握中断处理程序设计的要求
  • 设计一个汇编程序,实现时钟中断处理程序
  • 扩展 MyOS2,增加时钟中断服务,利用时钟中断实现与时间有关的操作

实验要求

  • 操作系统工作期间,利用时钟中断,在屏幕 24 行 79 列位置轮流显示|/\,适当控制显示速度,以方便观察效果。
  • 编写键盘中断响应程序,原有的你设计的用户程序运行时,键盘事件会做出有事反应:当键盘有按键时,屏幕适当位置显示OUCH!OUCH!
  • 在内核中,对 33 号、34 号、35 号和 36 号中断编写中断服务程序,分别在屏幕 1/4 区域内显示一些个性化信息。再编写一个汇编语言的程序,作为用户程序,利用int 33int 34int 35int 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.asmkernel.c两个文件编译出来的内容连接起来。和上一个实验中的完全相同,不再放出。

prg1.asm~prg4.asm

直接调用 int 33、int 34、int 35 和 int 36 四个中断实现(编译出来的大小仅有 3bytes)。

org 0a100H
int 33
ret

上面是 prg1.asm 的内容,其余同理,不再放出。

Makefile

上一个实验完全相同,不再放出。

运行结果

1 如上图,进入操作系统后开始了「无敌风火轮」(右下角)。 2 如上图,使用exec指令轮流运行我的四个程序,分别调用软中断int 33~int 36。按下 Ctrl+C 可以退出程序。程序检测到键盘输入,因此显示Ouch!Ouch!

风火轮仍然在转。 3 输入若干指令。

风火轮仍然在转。 4 继续输入若干指令,此时超出屏幕显示范围,自动滚屏。

风火轮仍然在转。

实验总结

这次实验让我深入理解了中断服务程序的工作原理。中断响应后,先到内存指定位置找到中断向量表,然后跳转到中断服务程序。中断服务程序需先保存寄存器。中断服务程序完成后,需先还原寄存器,然后调用中断返回指令。所以在做实验的时候,就需要修改操作系统内核,使操作系统内核修改中断向量表,才能实现自定义中断服务程序。

在实验中,因为乱用宏以及没有恢复寄存器出了挺多问题,好在最终一一解决了。这也让我意识到之前的代码写的有多差、有多少隐患。因此,几乎将原先的代码完全重构了一遍。希望自己还是要多多注意一下这方面的问题。