实验5:实现系统调用
实验目的
1、 学习掌握PC系统的软中断指令
2、掌握操作系统内核对用户提供服务的系统调用程序设计方法
3、掌握C语言的库设计方法
4、掌握用户程序请求系统服务的方法
实验要求
1、了解PC系统的软中断指令的原理
2、掌握x86汇编语言软中断的响应处理编程方法
3、扩展实验四的的内核程序,增加输入输出服务的系统调用。
4、C语言的库设计,实现putch()
、getch()
、printf()
等基本输入输出库过程。
5、编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性
实验内容
修改实验4的内核代码,先编写save()和restart()两个汇编过程,分别用于中断处理的现场保护和现场恢复,内核定义一个保护现场的数据结构,以后,处理程序的开头都调用save()保存中断现场,处理完后都用restart()恢复中断现场。
内核增加int 20h、int 21h和int 22h软中断的处理程序,其中,int
20h用于用户程序结束是返回内核准备接受命令的状态;int
21h用于系统调用,并实现3-5个简单系统调用功能;int22h功能未定,先实现为屏幕某处显示INT22H。
保留无敌风火轮显示,取消触碰键盘显示OUCH!这样功能。
进行C语言的库设计,实现putch()、getch()、gets()、puts()、printf()、scanf()等基本输入输出库过程,汇编产生libs.obj。
利用自己设计的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); }
C++
编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性
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; int bx; int cx; int dx; int di; int bp; int es; int ds; int si; int ss; int sp; int ip; int cs; int flags; };
C
老师给出了Minix
中的save
和restart
过程,我们可以先看懂该程序,再写出自己的_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 ;********************************************************
ASM
由于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 ;********************************************************
ASM
这样,save、restart就已经写好了,我们要寻找一些方法来检验是否正确。使用方法
1 2 3 call _save call do_INT_XXh jmp _restart
ASM
可以在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
ASM
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
ASM
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 ;***********************************************************
ASM
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
ASM
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
ASM
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
ASM
4.增加INT 22软中断程序
由于save
和restart
已经准备好了,所以我们可以很轻松地写出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" ;***********************************************************
ASM
使用一个用户程序来调用int 22h
1 2 3 4 org 8c00h start: int 22h ret
ASM
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 ; } } }
C
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 ; } } }
C
6.将所有文件进行编译,并写入软盘
我们再来看看实验的要求:
利用自己设计的C库libs.obj,编写一个使用这些库函数的C语言用户程序,再编译,再与libs.obj一起链接,产生COM程序。增加内核命令执行这个程序。
此次生成的.com程序涉及到四个代码文件,对于C函数库,经过网上搜索发现不能使用.h
作为文件名,而应该直接使用.c
。之后使用批处理提高生产效率(其中,后四段命令是新添加的):
1 2 3 4 5 6 7 8 9 10 del *.objdel *.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,,
BAT
本次实验引入了两个新的用户程序:
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.imgall: 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
MAKEFILE
实验结果
开机动画正常运作
image-20210515224730500
可以注意到,右下角的风火轮可以正常转动,同时新增加两个用户程序,分别用于测试int21和int22
image-20210515224550534
int22可以正常显示
image-20210515225137314
int21执行如下
image-20210515230025451
问题与解决方式
终于解决了,之前实验中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
ASM
在dosbox编译的时候出现神奇错误:
Fatal: Command line: Can't locate file: load_stdio_test.asm
解决方法是,将文件名改短😓
在编译int 21h
测试程序的时候,出现调用.com程序失败的问题。原来是.com程序超过了512kb,在载入内存的时候需要多载入几个扇区。这个问题也卡了好久,以后一定要注意😡
实验总结
本次实验看起来内容不多,但是真正做起来,是有非常多细节需要处理的。
第一个难关就是save
和restart
的设计。
它起到保护中断现场的作用,可以使得多出来的在后续的实验中大概会起到非常重要的作用。看起来只不过是push和pop的简单问题,但是事实上,很多的寄存器(如ss,sp等)是会影响当前指令的正常执行的,常常需要额外的变量存储,并且需要对栈的过程、对函数调用中的栈的调用过程非常清晰。通过对上网查阅资料,我对汇编语言中所有的寄存器的特点有了更深刻的理解。
接下来一个难关,是用户程序常常遇到调用失败的问题。很多时候是自己加载的扇区数,磁头号,扇区号不正确导致的。也出现了用户栈与内核栈并不对应导致错误。希望在后续的实验中能够避免。
希望下次实验能够顺顺利利!!!