实验5:实现系统调用

实验5:实现系统调用

实验目的

1、 学习掌握PC系统的软中断指令

2、掌握操作系统内核对用户提供服务的系统调用程序设计方法

3、掌握C语言的库设计方法

4、掌握用户程序请求系统服务的方法

实验要求

1、了解PC系统的软中断指令的原理

2、掌握x86汇编语言软中断的响应处理编程方法

3、扩展实验四的的内核程序,增加输入输出服务的系统调用。

4、C语言的库设计,实现putch()getch()printf()等基本输入输出库过程。

5、编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性

实验内容

  1. 修改实验4的内核代码,先编写save()和restart()两个汇编过程,分别用于中断处理的现场保护和现场恢复,内核定义一个保护现场的数据结构,以后,处理程序的开头都调用save()保存中断现场,处理完后都用restart()恢复中断现场。

  2. 内核增加int 20h、int 21h和int 22h软中断的处理程序,其中,int 20h用于用户程序结束是返回内核准备接受命令的状态;int 21h用于系统调用,并实现3-5个简单系统调用功能;int22h功能未定,先实现为屏幕某处显示INT22H。

  3. 保留无敌风火轮显示,取消触碰键盘显示OUCH!这样功能。

  4. 进行C语言的库设计,实现putch()、getch()、gets()、puts()、printf()、scanf()等基本输入输出库过程,汇编产生libs.obj。

  5. 利用自己设计的C库libs.obj,编写一个使用这些库函数的C语言用户程序,再编译,再与libs.obj一起链接,产生COM程序。增加内核命令执行这个程序:

1
2
3
4
5
6
7
8
9
10
void main(){
char ch,str[80];
int a;
getch(&ch);
gets(str);
scnf(“a=%d”,&a);
putch(ch);
puts(str);
printint(“ch=%c, a=%d, str=%s”, ch, a, str);
}
  1. 编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性
new

实验环境

1.系统与虚拟机
  • Windows 10 - x64 18363.1139
  • VMware Workstation 16 Player:用于跑ubuntu虚拟机
  • Ubuntu 20.04.2 LTS
  • VirtualBox-6.1.18-142142-Win:用于运行.img文件
  • DOSBox DOS Emulator 0,74,0,0:用于tcc和tasm编译,并且运行.com文件
2.windows上的相关软件、编译器等
  • NASM version 2.10.07 compiled on Jan 2 2013
  • TCC.EXE:用于16位C语言编程
  • TLINK.EXE:用于C语言与汇编语言链接
  • TASM.EXE:用于.asm文件的汇编
3.Ubuntu上的相关软件、编译器等
  • NASM version 2.14.02
  • makefile:GNU Make 4.2.1

实验基本架构

Wuhlan OS

实验过程

1.根据例程,写出保护现场与恢复现场

在C程序中定义了一个结构体,包含了汇编中的14个寄存器,用于存储原来的状态,结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct cpuRegisters{
int ax; /*0*/
int bx;
int cx;
int dx;
int di;
int bp;
int es;
int ds; /*14*/
int si; /*16*/
int ss; /*18*/
int sp; /*20*/
int ip; /*22*/
int cs; /*24*/
int flags; /*26*/
};

老师给出了Minix中的saverestart过程,我们可以先看懂该程序,再写出自己的_save_restart。我们必须要明确刚调用save的时候栈里有什么东西,包括标志寄存器flag,代码段cs,中断代码pc,和save 的返回地址。我们发现结构体中的前7个寄存器一般来说是比较容易处理的(不会对当前执行的代码造成其他意想不到的影响),且可以在save执行过程中作为中介保存一下。

仿照老师所给的代码,使用结构体保存中断现场,且在执行save之后,ss,sp,ds,cs,ip几个寄存器会变为内核态的。详情可以看下面代码的注释部分,对于栈的变化进行详细的讲述:

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
;保护中断现场,此时栈顶/flags/cs/int ip/save ip*********************
_save proc near
push ds ;/flags/cs/ip/ip/ds
push cs
pop ds ;ds=cs

push si ;/flags/cs/ip/ip/ds/si

lea si,_cpuReg ;此时si是结构体的地址
pop word ptr [si+16] ;si存入结构体,/flags/cs/ip/ip/ds
pop word ptr [si+14] ;ds存入结构体,/flags/cs/ip/ip

lea si,save_ip ;这个ip指的是save的返回地址
pop word ptr [si] ;ip存入ret_temp中,/flags/cs/ip

lea si,_cpuReg
pop word ptr [si+22] ;ip存入结构体,/flags/cs
pop word ptr [si+24] ;cs存入结构体,/flags
pop word ptr [si+26] ;flags存入结构体,/,栈为空

mov [si+18],ss ;ss存入结构体
mov [si+20],sp ;sp存入结构体

mov si,ds
mov ss,si ;将栈修改为内核栈

lea si,_cpuReg
mov sp,si ;使栈指针指向结构体
add sp,14 ;sp指向ds

push es ;将剩余的寄存器存入结构体
push bp
push di
push dx
push cx
push bx
push ax

lea si,kernelsp
mov sp,[si]
;此时ss,sp,ds,cs,ip都是内核的

lea si,save_ip ;通过保存的save_iP进行返回
mov ax,[si]
jmp ax
_save endp
;********************************************************

由于save的时候,最后才存储那7个寄存器,在恢复的时候,可以先恢复。由于我们使用si来进行寻址,所以先把恢复的si寄存器存入一个临时变量中,最后再进行恢复。比较需要注意的是栈的变化,首先把结构体作为栈,后来恢复了原来的栈,要对结构体进行手动寻址方式。

详情可以看下面代码的注释部分,对于栈的变化进行详细的讲述:

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
;恢复中断现场********************************************************
_restart proc near
lea si,kernelsp
mov [si],sp
lea sp,_cpuReg ;将栈指针指向结构体,对前7个寄存器进行出栈
pop ax
pop bx
pop cx
pop dx
pop di
pop bp
pop es
;结构体中/flags/cs/ip/sp/ss/si/ds
lea si,ds_temp ;使用一个临时变量存储ds,/flags/cs/ip/sp/ss/si
pop word ptr [si] ;结构体中/flags/cs/ip/sp/ss/si
lea si,si_temp ;使用一个临时变量存储si
pop word ptr [si] ;结构体中/flags/cs/ip/sp/ss

lea si,bx_temp ;保护一下bx
mov [si],bx

pop bx ;结构体中/flags/cs/ip/sp
mov ss,bx ;bx为原来的栈地址
mov bx,sp
mov sp,[bx] ;bx此时是结构体中sp的地址,恢复栈的sp

add bx,2 ;使bx指向ip

push word ptr [bx+4] ;原来的栈中:/flags
push word ptr [bx+2] ;原来的栈中:/flags/cs
push word ptr [bx] ;原来的栈中:/flags/cs/ip

push ax ;原来的栈中:/flags/cs/ip/ax
push word ptr [si] ;原来的栈中:/flags/cs/ip/ax/bx
lea si,ds_temp
mov ax,[si]
lea si,si_temp
mov bx,[si]
mov ds,ax ;恢复ds
mov si,bx ;恢复si

pop bx ;原来的栈中:/flags/cs/ip/ax
pop ax ;原来的栈中:/flags/cs/ip

iret

_restart endp
;********************************************************

这样,save、restart就已经写好了,我们要寻找一些方法来检验是否正确。使用方法

1
2
3
call _save
call do_INT_XXh
jmp _restart

可以在Timer的前后使用,程序照常进行。

还可以在OUCH OUCH!的前后使用,程序如常,说明save、restart是成功的。

2.增加INT 20软中断程序

int 20h用于用户程序结束是返回内核准备接受命令的状态

INT20h的内容主要是与jump过程相互配合的,提供新的跳转到用户程序并且返回的方法。与之前的区别在于保护了内核栈,提供了新的用户栈。在jump里把用户程序的ss=cs,sp准备好,并且将其压入用户栈中。具体代码如下:

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
public _jump
_jump proc near;两个参数,要跳到的cs:ip
;保护现场***********************************************
push bp
mov bp,sp ;先获取当前的栈指针,方便寻址,/jumpdest_ip/jumpdest_cs/ip/bp
push ax
push bx
push cx
push dx
push di
push es
push ds
push si
pushf

mov bx,[bp+4] ;获取参数cs
mov ax,[bp+6] ;获取参数ip

mov es,bx
lea si,PSPBegin
mov di,0
lea cx,PSPEnd
sub cx,si
rep movsb

lea si,kernelsp
mov [si],sp

mov ss,bx
mov sp,0

xor cx,cx
push cx

push bx ;要跳转的cs
push ax ;要跳转的ip
retf ;retf用栈中数据同时改CS,IP,远转移远返回指令。当它执行时,处理器先从栈中弹出
;一个字到IP,再弹出一个字到CS。retf -> pop ip pop cs
PSPBegin:
int 20h
PSPEnd:nop

INT20h,前半部分是对中断的载入,与其他中断的载入方式一致,不必赘述。从用户程序返回到内核的方法,恢复内核栈,再恢复寄存器最后直接返回即可

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
public _loadINT_20h
_loadINT_20h proc near ;载入中断,基本固定化的格式了,不必赘述
push ax
push es
CLI
xor ax,ax
mov es,ax
lea ax,INT20H
mov [es:80h],ax
mov ax,cs
mov [es:82h],ax
mov es,ax
STI

pop es
pop ax

ret

INT20H:
mov ax,cs
mov ds,ax ;ds = cs
mov ss,ax ;ss = ss
lea si,kernelsp
mov sp,[si] ;恢复栈指针
popf ;因为在调用jump过程后,push了很多寄存器,所以需要返回前需要pop
pop si
pop ds
pop es
pop di
pop dx
pop cx
pop bx
pop ax
pop bp
ret
_loadINT_20h endp

3.增加INT 21软中断程序

基本结构如下:

  • 载入int 21h(与其他中断的载入方式类似,偏移量存放在地址84h,代码段地址存放在86h的位置)
  • 执行int 21h
  • 三个功能号,分别是:输出一个字符、获取键盘输入的字符(这个并不是完整的getchar)、返回内核(与int 20h一致)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
;INT 21h****************************************************
INT_21h:
call _save
call do_INT_21h
jmp _restart
do_INT_21h:
lea si, _cpuReg
mov ax, [si] ;此时si的地址就是ax

cmp ah, 01h
jz INT_21h_1h

cmp ah, 02h
jz INT_21h_2h

cmp ah, 4ch
jz INT_21h_3h
_21h_end:
ret
;***********************************************************

1h:获取键盘输入的字符

1
2
3
4
5
6
7
8
9
10
11
12
INT_21h_1h proc near
mov ax,cs
mov ds,ax
mov ah,0
int 16h;

;此时al已经得到了输入的字符,接着我们把ah清零,并修改cpureg中的ax
xor ah,ah
lea si,_cpuReg
mov [si],ax
jmp _21h_end
INT_21h_1h endp

2h:输出一个字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INT_21h_2h proc near
mov ax,cs
mov es,ax

mov bh,0
mov ah,3
int 10h;
lea bp,_cpuReg
add bp,6
mov ax,1301h
mov bx,0007h
mov cx,1
int 10h
jmp _21h_end

INT_21h_2h endp

3h:返回内核

1
2
3
4
5
6
7
8
9
10
INT_21h_3h proc near
mov ax,cs
mov ds,ax
mov ah,0
int 16h
xor ah,ah
lea si,_cpuReg
mov [si],ax
jmp _21h_end
INT_21h_3h endp

4.增加INT 22软中断程序

由于saverestart已经准备好了,所以我们可以很轻松地写出INT 22h

加载22h中断,偏移量存放在地址88h,代码段地址存放在8ah的位置

Int22h中断处理程序(屏幕某处显示INT22H):int22h功能未定,先实现为屏幕某处显示INT22H。

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
;22号中断***************************************************
INT_22h:
call _save
call do_INT_22h
jmp _restart

do_INT_22h:
mov ax, cs
mov ds, ax
mov ax,0b800h
mov es,ax

mov cx, 6 ;字符串长度,用于循环计数
lea si, INT22H_str ;获取字符串地址

mov bx,(12*80+40)*2 ;打印于屏幕中心
mov ah,07h
loop_int_22:
mov al,[si]
mov es:[bx], ax
inc bx
inc bx
inc si
loop loop_int_22 ;loop结合cx可以使循环写的很舒服
ret
INT22H_str db "INT 22H"
;***********************************************************

使用一个用户程序来调用int 22h

1
2
3
4
org 8c00h
start:
int 22h
ret

5.完善C函数库

事实上,在之前的实验中我已经写了一个类似的库,这一次实验的主要是实现printf和scanf.

printf和scanf都是参数变长的函数。我对此完全没有概念,只好求助于搜索引擎。

关键的操作在于取str指针的地址,通过+偏移量的方式,寻址得到后面的参数。

如:itos((int)*(&str+off))可以获得%d对应的数字,并将其转换为字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void printf(char *str, ...){
int i;
int off = 1;
for (i = 0; str[i] != 0; i++){
if (str[i] != '%'){
putch(str[i]);
}
else{
i++;
if(str[i] == 'd')
puts(itos((int)*(&str + off)));
else if(str[i] == 'c')
putch((char)*(&str + off));
else if(str[i] == 's')
puts((char *)*(&str + off));
else continue;
off += 1;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void scanf(char *str, ...){
int off = 1;
int i;
for (i = 0; str[i] != 0; i++){
if (str[i] == '%'){
i++;
switch (str[i]){
if(str[i] == 'd'){
gets(num);
*((int *)(*(&str + off))) = stoi();
}else if(str[i] == 'c')
getch((char *)*(&str + off));
else if(str[i] == 's')
gets((char *)*(&str + off));
else continue;
}
off += 1;
}
}
}

6.将所有文件进行编译,并写入软盘

我们再来看看实验的要求:

  1. 利用自己设计的C库libs.obj,编写一个使用这些库函数的C语言用户程序,再编译,再与libs.obj一起链接,产生COM程序。增加内核命令执行这个程序。

此次生成的.com程序涉及到四个代码文件,对于C函数库,经过网上搜索发现不能使用.h作为文件名,而应该直接使用.c。之后使用批处理提高生产效率(其中,后四段命令是新添加的):

1
2
3
4
5
6
7
8
9
10
del *.obj
del *.com
tcc -mt -c -omain.obj main.c > ccmsg.txt
tasm monitor.asm monitor.obj > amsg.txt
tlink /3 /t monitor.obj main.obj, monitor.com,,
tcc -mt -c -otest.obj test.c > test.txt
tcc -mt -c -ostdio.obj stdio.c > stdio.txt
tasm loadtest.asm loadtest.obj > loadtest.txt
tasm lib.asm lib.obj > lib.txt
tlink /3 /t loadtest.obj lib.obj stdio.obj test.obj, loadtest.com,,

本次实验引入了两个新的用户程序:

  • test_int_22.bin,是用于测试中断向量22h的,加载到磁头号1,6号扇区的位置

  • LOADTEST.COM是用于测试中断向量21h的,加载到磁头号1,7号扇区的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BIN = loader.bin startOS.bin 19335209_A.bin 19335209_B.bin 19335209_C.bin 19335209_D.bin test_int_22.bin
IMG = wuhlan.img
all: clear $(BIN) $(IMG)
clear:
rm -f $(BIN) $(IMG)
%.bin: %.asm
nasm -fbin $< -o $@
%.img:
/sbin/mkfs.msdos -C $@ 1440
dd if=loader.bin of=$@ conv=notrunc
dd if=MONITOR.COM of=$@ seek=1 conv=notrunc
dd if=19335209_A.bin of=$@ seek=18 conv=notrunc
dd if=19335209_B.bin of=$@ seek=19 conv=notrunc
dd if=19335209_C.bin of=$@ seek=20 conv=notrunc
dd if=19335209_D.bin of=$@ seek=21 conv=notrunc
dd if=startOS.bin of=$@ seek=22 conv=notrunc
dd if=test_int_22.bin of=$@ seek=23 conv=notrunc
dd if=LOADTEST.COM of=$@ seek=24 conv=notrunc

实验结果

开机动画正常运作

image-20210515224730500

可以注意到,右下角的风火轮可以正常转动,同时新增加两个用户程序,分别用于测试int21和int22

image-20210515224550534

int22可以正常显示

image-20210515225137314

int21执行如下

image-20210515230025451

问题与解决方式

  1. 终于解决了,之前实验中C语言字符串无法显示的问题【捂脸】,经过老师的轻轻一点拨,原来就是我在载入监控程序的时候,没有控制好载入扇区的数量。如下,对al的值进行修改即可。😊

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    mov ax,cs                ;段地址 ; 存放数据的内存基地址
    mov es,ax ;设置段地址(不能直接mov es,段地址)
    mov bx, OS_offset
    mov ah,2 ;功能号
    mov al,10 ;扇区数,内核占用扇区数 注意:不止加载了一个扇区
    mov dl,0 ;驱动器号 ; 软盘为0,硬盘和U盘为80H
    mov dh,0 ;磁头号 ; 起始编号为0
    mov ch,0 ;柱面号 ; 起始编号为0
    mov cl,2 ;存放内核的起始扇区号 ; 起始编号为1
    int 13H ;调用读磁盘BIOS的13h功能
    jmp 0a00h:100h
  2. 在dosbox编译的时候出现神奇错误:

    Fatal: Command line: Can't locate file: load_stdio_test.asm

    解决方法是,将文件名改短😓

  3. 在编译int 21h测试程序的时候,出现调用.com程序失败的问题。原来是.com程序超过了512kb,在载入内存的时候需要多载入几个扇区。这个问题也卡了好久,以后一定要注意😡

实验总结

本次实验看起来内容不多,但是真正做起来,是有非常多细节需要处理的。

第一个难关就是saverestart的设计。 它起到保护中断现场的作用,可以使得多出来的在后续的实验中大概会起到非常重要的作用。看起来只不过是push和pop的简单问题,但是事实上,很多的寄存器(如ss,sp等)是会影响当前指令的正常执行的,常常需要额外的变量存储,并且需要对栈的过程、对函数调用中的栈的调用过程非常清晰。通过对上网查阅资料,我对汇编语言中所有的寄存器的特点有了更深刻的理解。

接下来一个难关,是用户程序常常遇到调用失败的问题。很多时候是自己加载的扇区数,磁头号,扇区号不正确导致的。也出现了用户栈与内核栈并不对应导致错误。希望在后续的实验中能够避免。

希望下次实验能够顺顺利利!!!


实验5:实现系统调用
https://wuhlan3.gitee.io/2021/05/16/实验5:实现系统调用/
Author
Wuhlan3
Posted on
May 16, 2021
Licensed under