操作系统实验:用汇编与 C 语言开发独立内核

 Max.C     2020-05-30   19355 words    & views

一、实验题目

实验三:用汇编与 C 语言开发独立内核

二、实验目的

  1. 加深理解操作系统内核概念
  2. 了解操作系统开发方法
  3. 掌握汇编语言与高级语言混合编程的方法
  4. 掌握独立内核的设计与加载方法
  5. 加强磁盘空间管理工作

三、实验要求

  1. 知道独立内核设计的需求
  2. 掌握一种x86汇编语言与一种C高级语言混合编程的规定和要求
  3. 设计一个程序,以汇编程序为主入口模块,调用一个C语言编写的函数处理汇编模块定义的数据,然后再由汇编模块完成屏幕输出数据,将程序生成COM格式程序,在DOS或虚拟环境运行。
  4. 汇编语言与高级语言混合编程的方法,重写和扩展实验二的的监控程序,从引导程序分离独立,生成一个COM格式程序的独立内核。
  5. 再设计新的引导程序,实现独立内核的加载引导,确保内核功能不比实验二的监控程序弱,展示原有功能或加强功能可以工作。
  6. 编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性。

实验内容

  1. 寻找或认识一套匹配的汇编与c编译器组合。利用c编译器,将一个样板C程序进行编译,获得符号列表文档,分析全局变量、局部变量、变量初始化、函数调用、参数传递情况,确定一种匹配的汇编语言工具,在实验报告中描述这些工作。
  2. 写一个汇编程和c程序混合编程实例,展示你所用的这套组合环境的使用。汇编模块中定义一个字符串,调用C语言的函数,统计其中某个字符出现的次数(函数返回),汇编模块显示统计结果。执行程序可以在DOS中运行。
  3. 重写实验二程序,实验二的的监控程序从引导程序分离独立,生成一个COM格式程序的独立内核,在1.44MB软盘映像中,保存到特定的几个扇区。利用汇编程和c程序混合编程监控程序命令保留原有程序功能,如可以按操作选择,执行一个或几个用户程序、加载用户程序和返回监控程序;执行完一个用户程序后,可以执行下一个。
  4. 利用汇编程和c程序混合编程的优势,多用c语言扩展监控程序命令处理能力。
  5. 重写引导程序,加载COM格式程序的独立内核。
  6. 拓展自己的软件项目管理目录,管理实验项目相关文档。

四、实验方案

1、实验环境

实验所使用的计算机配置为:

  • CPU:Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
  • RAM:8.00GB
  • 系统:Windows 10 - x64 10.0.17763

使用的开发工具如下:

  • Oracle VM VirtualBox - 6.0.14 r133895 (Qt5.6.2):虚拟机软件,相比起VMware,有着小巧轻便的特点,功能也很齐全。用于创建、运行虚拟机,创建虚拟软盘,引导虚拟软盘启动。
  • WinHex - 19.6 x64:16进制文件编辑器,用于查看、编辑虚拟软盘文件的内容。
  • Visual Studio Code - 1.44.2 :代码编辑器,可以根据需要配置插件,用于编辑代码。
  • bochs:x86硬件平台的开源模拟器,可以在运行过程中停止,本次实验中用于跟踪寄存器、堆栈,调试虚拟机。
  • DOSBox version 0.74-3:DOS 模拟程序,用于tcc、tasm等16位工具的使用。
  • TCC+TASM+Tlink:本次实验使用的汇编和C组合环境,完成独立内核的实现。
  • NASM version 2.14.02 :引导程序可以直接使用nasm编译。

2、环境配置

由于TCC+TASM只能在16位模式下运行,无法直接在Windows 10系统上使用,所以使用了模拟程序DOSBox构建一个16位环境,用以编译代码。

DOSBox在启动时会默认在虚拟的Z盘启动,为了定位到实验文件所在的目录,我们可以修改配置文件DOSBox 0.74-3 Options,在末尾添加以下命令:

mount c: C:\Users\34961\Desktop\03
c:
set PATH=%PATH%;c:\tool;

其中实验文件位于桌面的03文件夹中,使用mount将其设定为虚拟C盘的根目录;其中TCC、TASM工具位于该目录中的tool文件夹中,使用set PATH设置环境变量;这样,在其他的目录中,我们也可以使用TCC、TASM进行编译。

这样,在每次启动DOSBox时,都会自动进行挂载和载入环境变量。

3、实验思路

根据实验要求和相关知识,我们按照以下思路完成实验:

  • 根据要求完成样例和实例程序,了解混合编程的相互调用、参数传递等情况;
  • 设计新的引导程序、独立内核和用户程序,写入软盘,使用bochs进行调试;

4、实验文件列表

文件位于/src中,包含examplecombos三个子文件夹:

  • example:样例C程序相关文件;
  • comb:混合编程实例相关文件;
  • os:独立内核相关文件。

五、实验过程

1、样例C程序分析

为了学习汇编与C编译器组合,我们先使用一个样例程序熟悉C与汇编的相互调用、参数传递等情况。

其中upper.c为C程序代码,包含全局变量messagenum和函数FunInC,函数将message中的字母全部转换为大写;

/*程序源代码(upper.c)*/

char Message[10]="AaBbCcDdEe";
int  num;   
       
/*变量_Message,初值为AaBbCcDdEe*/

void  FunInC(){
       int inner=1234,i=0;
       while(Message[i]) {
              if (Message[i]>='a'&&Message[i]<='z')  
                     Message[i]=Message[i]+'A'-'a';
              i++;
       }
       return ;
}

showstr.asm为汇编代码,调用C程序中的函数FunInC,并将message输出至屏幕。

		;程序源代码(showstr.asm)
; 外部标识符
extrn _FunInC:near   	;声明一个c程序函数FunInC
extrn _Message:near     	;声明一个外部变量


;*****************************************
.8086                                   ;*
_TEXT segment byte public 'CODE'        ;*
assume cs:_TEXT                         ;*
DGROUP group _TEXT,_DATA,_BSS           ;* 
org 100h                                ;*
                                        ;*
start:                                  ;*
;*****************************************

	mov  ax,  cs
	mov  ds,  ax           	; DS = CS
	mov  es,  ax          	; ES = CS
	mov  ss,  ax       	; SS = CS
	mov  sp, 100h    
	call near ptr _FunInC 	;调用C的函数
	mov  bp, offset _Message ; BP=当前串的偏移地址
	mov  ax, ds		;BP = 串地址
	mov  es, ax		;置ES=DS 
	mov  cx, 10        	; CX = 串长(=10)
	mov  ax, 1301h	;  AH = 13h(功能号) AL = 01h(光标置于串尾)
	mov  bx, 0007h	; 页号为0(BH = 0) 黑底白字(BL = 07h)
	mov  dh, 24	; 行号
	mov  dl, 0		; 列号
	int  10h		; BIOS的10h功能:显示一行字符
    retf
	jmp $

;*****************************************
                                        ;*
_TEXT ends                              ;*
                                        ;*
_DATA segment word public 'DATA'        ;*
                                        ;*
_DATA ends                              ;*
                                        ;*
_BSS	segment word public 'BSS'       ;*
_BSS ends                               ;*
                                        ;* 
end start                               ;*
;*****************************************

使用命令编译C程序和汇编程序并进行链接,并生成C程序对应的汇编代码文件,为了便于操作,编写一个make.bat文件实现这些功能:

del *.obj
tcc -mt -S -oupper.asm upper.c >c2asm.txt
tcc -mt -c -oupper.obj upper.c >c2obj.txt
tasm showstr.asm  showstr.obj  > a2obj.txt
tlink /3 /t showstr.obj upper.obj, showstr.com

运行链接好的程序showstr.com,程序成功执行:

分析

查看tcc生成的汇编文件upper.asm,可以看到c程序的全局变量message被声明在数据段中,而num由于未初始化,被放在_BSS段中:

_DATA	segment word public 'DATA'
_Message	label	byte
	db	65
	db	97
	db	66
	db	98
	db	67
	db	99
	db	68
	db	100
	db	69
	db	101
_DATA	ends

_BSS	segment word public 'BSS'
_num	label	word
	db	2 dup (?)
_BSS	ends

函数以proc的格式构造,位于代码段中:

_TEXT	segment	byte public 'CODE'
;	?debug	L 8
_FunInC	proc	near
;code
;..
;..
_FunInC	endp
_TEXT	ends

在汇编部分调用C部分的函数时,需要使用外部标识符extrn _FunInC:near 声明一个c程序函数,外部变量同理,在汇编时需要注意DGROUP group _TEXT,_DATA,_BSS以进行段对齐,否则无法正确加载与链接。

函数调用直接使用call near ptr _FunInC格式,如果调用的函数有传入参数,需要先将参数按顺序push压栈,C程序的函数会自动读取栈中元素。在结束调用时记得将栈中元素弹出。

2、混合编程实例

为了更深刻体会混合编程的工作机制,我们自己设计一个程序实例,实现汇编与C的相互调用,并使用C函数的返回值传递。代码文件位于/src/comb目录中。

程序思路如图:

  1. 在汇编程序和C程序中各声明了一个字符串。
  2. 汇编程序作为入口,调用C程序的函数Calculate;
  3. 在函数Calculate中计算汇编程序字符串的字符个数,计算完成后调用汇编程序的OutPut函数;
  4. OutPut函数中使用中断显示两个字符串,完成后跳转回函数Calculate;
  5. 将计算结果作为返回值传递给汇编程序,在汇编程序中使用中断显示。

全局变量声明

在C程序中定义的全局变量如下,来自汇编程序的外部变量使用extern标识。

extern char InnerMessage[];
extern void OutPut();

char Message[] = "in C program";
int length = 12;
int count = 0;

在汇编程序中定义的全局变量如下,需要加上public标识,来自C程序的外部变量使用extern标识。

; 外部标识符
extrn _Calculate:near   	;声明一个c程序函数Calculate
extrn _Message:near     	;声明一个外部变量
extrn _count:near     	;声明一个外部变量
extrn _length:near     	;声明一个外部变量

public _OutPut
public _InnerMessage

; code


_datadef:
_InnerMessage	db  '18340011_Messsssage',0	
_InnerMessageLength  equ ($-_InnerMessage)

Calculate

简单的C程序,其中调用了外部函数output(),且将count作为返回值返回;

int Calculate(){
       int i=0;
       while(InnerMessage[i]) {
              if (InnerMessage[i]=='s')  
                     count = count + 1;
              i++;
       }
       OutPut();
       return count;
}

OutPut

使用两次中断,显示两个字符串,作为子程序,需要使用procendp进行定义,在最后使用ret返回;

_OutPut proc near
	mov  ax,  cs
	mov  ds,  ax           	; DS = CS
	mov  es,  ax          	; ES = CS
	mov  ss,  ax       	; SS = CS

	;显示汇编程序中的字符
	mov bp, offset _InnerMessage ; BP=当前串的偏移地址
	mov ax, ds		;BP = 串地址
	mov es, ax		;置ES=DS 
	mov cx, _InnerMessageLength        	; CX = 串长
	mov ax, 1301h	;  AH = 13h(功能号) AL = 01h(光标置于串尾)
	mov bx, 0007h	; 页号为0(BH = 0) 黑底白字(BL = 07h)
	mov dh, 23	; 行号
	mov dl, 40		; 列号
	int 10h		; BIOS的10h功能:显示一行字符

	;显示C程序中的字符
	mov bp, offset _Message ; BP=当前串的偏移地址
	mov ax, ds		;BP = 串地址
	mov es, ax		;置ES=DS 
	mov cx, 12        	; CX = 串长
	mov ax, 1301h	;  AH = 13h(功能号) AL = 01h(光标置于串尾)
	mov bx, 0007h	; 页号为0(BH = 0) 黑底白字(BL = 07h)
	mov dh, 24	; 行号
	mov dl, 40		; 列号
	int 10h		; BIOS的10h功能:显示一行字符

	ret		; 返回C程序

_OutPut endp

返回值操作

操作紧接在call指令之后,此时返回值存储在寄存器ax中,将其简单处理成字符形式,使用中断,在光标位置显示10次,然后结束当前程序。

	call near ptr _Calculate 	;调用C的函数; 显示字符
	
	add ax, 30h
	mov ah, 09h
	mov bx, 0007h	; 页号为0(BH = 0) 黑底白字(BL = 07h)
	mov cx, 10	; 显示10次
	int 10h		; BIOS的10h功能:显示一行字符

	mov ax, 0
	mov ss, ax
    retf
	jmp $

运行结果

编译、链接生成showstr.com文件,在DOSBox中成功运行,依次显示了两个字符串,且计算字符串18340011_Messsssage中字符s的数量为5,返回结果正确。

汇编程序中数据变量的位置

在实验中发现了无法正确读取汇编字符串变量的问题,为了解决该问题,尝试将该字符串变量分别放置于代码段或数据段中:

20

将其放在代码段时,最终得到的程序能够成功运行,且正确显示字符(如上面运行结果所显示);

但将其放在数据段时,汇编所读取的汇编字符串变量会出现乱码:

通过反汇编内存找到了访问标号地址的指令mov bp, offset _InnerMessage位置,发现标号对应的位置并不正常。该标号地址应位于代码段之后的数据段,通过Winhex的查看,大约是0x170左右的位置(com文件载入内存时需要加上100h的偏移量),但实际上标号显示为0x000a,这才导致了无法正确读取数据。

因此,最终决定在代码段声明一个代码区_datadef,存放汇编字符串变量

3、独立内核设计

使用C和汇编的混合编程,开始设计新的独立内核。最终实现的虚拟软盘包含三部分:引导程序、独立内核和用户程序;

引导程序用于加载内核到内存中,并跳转至内核中;

独立内核利用混合编程设计,实现指令操作,执行一个或多个用户程序、加载用户程序和返回监控程序等功能;

用户程序和上一个实验相同,四个不同的用户程序分别在四分之一屏幕反弹。

a、引导程序设计

引导程序被加载至内存0x7c00处,代码非常简单,运行时等待键盘输入,读取到任意输入后加载独立内核并跳转至独立内核;在运行开始、运行结束时都会显示字符串作为标识。

需要注意的是加载引导程序时段起始地址为0,寄存器ss、sp均为0,在跳转至内核文件之前手动调节sp=7d00h,这个操作是为了最后退出独立内核时返回引导程序,返回后能够显示字符串作为运行结束标识。

EnterKernel:
      mov   sp, 7d00h
      call 800h:100h     ; call

引导程序的完整代码如下:

;程序源代码(myos.asm)
  org  7c00h
  ; BIOS将把引导扇区加载到0:7C00h处,并开始执行
  OffSetOfUserPrg1 equ 8100h 
Start:
	mov	ax, cs	       ; 置其他段寄存器值与CS相同
	mov	ds, ax	       ; 数据段
      

  ; 显示一行字符
  mov	bp, Message		 ; BP=当前串的偏移地址
  mov	cx, MessageLength  ; CX = 串长(=9)
  mov   ax,0xB800		; 文本窗口显存起始地址,使用gs作为段地址访问
  mov   gs,ax			; GS = 0xB800
  mov	ax, 1301h		 ; AH = 13h(功能号)、AL = 01h(光标置于串尾)
  mov	bx, 0007h		 ; 页号为0(BH = 0) 黑底白字(BL = 07h)
  mov   dh, 0		       ; 行号=0
  mov	dl, 0			 ; 列号=0
  int	10h			 ; BIOS的10h功能:显示一行字符

EnterIns:
      mov	ax, cs	       ; 置其他段寄存器值与CS相同
	mov	ds, ax	       ; 数据段

      ; 读取键盘输入,任意输入后开始加载内核
      mov ax, 0000h
	int 16h			 ; BIOS的16h功能:读取键盘输入


LoadnEx:
     ;读软盘或硬盘上的若干物理扇区到内存的ES:BX处:     
      mov bx, OffSetOfUserPrg1  ;偏移地址; 存放数据的内存偏移地址
      mov ch,0                 ;柱面号 ; 起始编号为0
      mov cl,2                ;起始扇区号 ; 起始编号为1
      mov ah,2                 ; 功能号
      mov al,byte[kernelSize]  ;扇区数
      mov dl,0                 ;驱动器号 ; 软盘为0,硬盘和U盘为80H
      mov dh,0                 ;磁头号 ; 起始编号为0
      int 13H ;                调用读磁盘BIOS的13h功能
      ; 监控程序boot.com已加载到指定内存区域中
      mov dl,cl                  ; 信息保存在dl上

EnterKernel:
      mov   sp, 7d00h
      call 800h:100h     ; call?


AfterRun:
      mov	ax, cs	       ; 置其他段寄存器值与CS相同
      mov	ds, ax	       ; 数据段
      mov ss, ax             ; 栈指针
      mov	es, ax		 ; 置ES=DS

      ; 显示结束字符
      mov	bp, EndMessage		 ; BP=当前串的偏移地址
      mov	cx, EndMessageLength  ; CX = 串长(=9)
      mov ax,0xB800		; 文本窗口显存起始地址,使用gs作为段地址访问
      mov gs,ax			; GS = 0xB800
      mov	ax, 1301h		 ; AH = 13h(功能号)、AL = 01h(光标置于串尾)
      mov	bx, 0007h		 ; 页号为0(BH = 0) 黑底白字(BL = 07h)
      mov dh, 0 	       ; 行号=0
      mov	dl, 0			 ; 列号=0
      int	10h			 ; BIOS的10h功能:显示一行字符

      jmp $                      ;无限循环     

data:
Message  db 'Hello, the boot program is on. '
MessageLength  equ ($-Message)
EndMessage  db 'End the boot. please quit.'
EndMessageLength  equ ($-EndMessage)
kernelSize   dw 4

      times 510-($-$$) db 0
      db 0x55,0xaa

b、用户程序设计

用户程序为上次实验完成的四个.com文件,调用时实现在四分之一屏幕的反弹,在本实验使用时不需要做出修改,这里不多赘述。

c、独立内核设计

独立内核的设计是本次实验的核心,使用了tcc+tasm进行汇编和C语言的混合编程,被引导程序加载至800h:100h的内存位置,基本设计思路如下:

汇编部分作为入口模块,并作为硬件接口;

C部分作为功能实现模块,完成内核的主要功能实现;

首先介绍汇编部分的各个模块:

start

作为入口模块,首先设定了各个段寄存器的值,注意此时段起始地址为cs = 800h

  • 变量_X _Y记录当前光标位置,便于输入输出操作;
  • 设置光标后跳转至C部分的main函数;
  • 从主函数返回后,显示独立内核的结束信息,之后重新设置spss值,返回引导程序中
start:  
	mov  ax,  cs
	mov  ds,  ax           	; DS = CS
	mov  es,  ax          	; ES = CS
	mov  ss,  ax       	; SS = CS
	mov  sp, 100h    

	; 光标设置
	mov ah, 02h
	mov bh, 0	; 页号
	mov dh, ds:[_Y]	; 行
	mov dl, ds:[_X]	; 列
	int 10h		; BIOS的10h


	call near ptr _main    ;调用C的主函数

	; 显示字符
	mov bp, offset _EndMessage ; BP=当前串的偏移地址
	mov ax, ds		;BP = 串地址
	mov es, ax		;置ES=DS 
	mov cx, _EndMessageLength        	; CX = 串长
	mov ax, 1301h	;  AH = 13h(功能号) AL = 01h(光标置于串尾)
	mov bx, 0007h	; 页号为0(BH = 0) 黑底白字(BL = 07h)
	mov dh, 3	; 行号=10
	mov dl, 0		; 列号=10
	int 10h		; BIOS的10h功能:显示一行字符

	mov ax, 0
	mov ss, ax
	mov sp, 7cfch
    retf
	jmp $

键盘接口

输入输出使用了_putChar_getChar_clear_setNextLine函数,功能如下:

  • _putChar:将一个字符输出至屏幕光标位置;
  • _getChar:读取键盘输入的一个字符,并实现回显功能;
  • _clear:清屏,并将光标设置为第一行行首;
  • _setNextLine:将光标设置为下一行行首,当到达屏幕最后一行时,进行清屏操作;

在子函数的编写时,使用了 proc nearendp格式,且在子函数始末使用了pushpop操作,这是为了让函数中栈的调用(即参数传递)得到正常数值(因为在编程中发现,如果不使用push,直接取栈顶元素,会发生错误)。

_putChar proc near
	push bp
	; 显示字符
	mov bp, sp
	mov al, [bp+4]    ; 从栈中取字
	mov ah, 0eh
	mov bx, 0007h	; 页号为0(BH = 0) 黑底白字(BL = 07h)
	int 10h		; BIOS的10h功能:显示字符
	pop bp
	ret		; 返回C程序

_putChar endp
_getChar proc near
	push bp

	; 接受字符
    mov ax, 0000h
	int 16h			 ; BIOS的16h功能:读取键盘输入
	; 显示字符
	mov ah, 0eh
	mov bx, 0007h	; 页号为0(BH = 0) 黑底白字(BL = 07h)
	int 10h		; BIOS的10h功能:显示字符
	mov ah, 00h
	
	pop bp
	ret		; 返回C程序

_getChar endp
_clear proc near
	push bp

	; 清屏 
	mov ax,0600h    
	mov ch,0   ; 左上角的行号
	mov cl,0   ; 左上角的列号
	mov dh,24  ; 右下角的行号
	mov dl,79  ; 右下角的行号
	mov bh,07h; 属性
	int 10h          ; 中断调用,清屏 

	; 光标设置
	mov ds:[_X], 0
	mov ds:[_Y], 0
	mov ah, 02h
	mov bh, 0	; 页号
	mov dh, ds:[_Y]	; 行
	mov dl, ds:[_X]	; 列
	int 10h		; BIOS的10h

	pop bp
	ret		; 返回C程序
_clear endp
_setNextLine proc near
	push bp

	add ds:[_Y], 1
	cmp ds:[_Y], 25
	jne SNL
	call _clear
SNL:; 光标设置
	mov ah, 02h
	mov bh, 0	; 页号
	mov dh, ds:[_Y]	; 行
	mov dl, ds:[_X]	; 列
	int 10h		; BIOS的10h

	pop bp
	ret		; 返回C程序

_setNextLine endp

用户程序调用

用户程序的调用包括加载至内存和跳转两个操作,其中跳转操作为了适配tasm的格式,使用了一段比较特殊的代码:

  push cs
  push offset return
  push 900h
  push 100h
  retf

return:
	pop bp

用户程序被加载至900h:100h区域,跳转时先将当前的cs=800h、返回的位置return压栈,再将900h100h压栈。(如图)

完成压栈操作后,使用retf进行跳转,此时程序会赋值cs=900hip=100h,便成功跳转至用户程序;用户程序返回时,retf指令会将剩下两个参数出栈,使cs和ip恰好指向我们需要的返回地址。

同时要注意,虽然用户程序被加载至900h:100h区域,但OffSetOfUserPrg1的值不是9100h而是1100h,这是由于此时段起始地址为800h800h:1100h恰为9100h

_LoadnEx proc near
	push bp
	mov bp, sp

	;读软盘或硬盘上的若干物理扇区到内存的ES:BX处:     
	mov bx, OffSetOfUserPrg1  ;偏移地址; 存放数据的内存偏移地址
	mov cx,[bp+4]             ;cl = 起始扇区号 ; 起始编号为1
	mov ch,0                 ;柱面号 ; 起始编号为0
	mov ax,[bp+6]  ;al = 扇区数
	mov ah,2                 ; 功能号
	mov dl,0                 ;驱动器号 ; 软盘为0,硬盘和U盘为80H
	mov dh,0                 ;磁头号 ; 起始编号为0
	int 13H ;                调用读磁盘BIOS的13h功能

	push cs
	push offset return
	push 900h
	push 100h
	retf

return:
	pop bp
	mov  ax,  cs
	mov  ds,  ax           	; DS = CS
	mov  es,  ax          	; ES = CS
	; 光标设置
	mov ds:[_X], 0
	mov ds:[_Y], 0
	mov ah, 02h
	mov bh, 0	; 页号
	mov dh, ds:[_Y]	; 行
	mov dl, ds:[_X]	; 列
	int 10h		; BIOS的10h
	mov ax, 0000h
	int 16h			 ; BIOS的16h功能:读取键盘输入
	ret		; 返回C程序

_LoadnEx endp

接下来为C代码部分:

main

主函数为接收指令的框架,调用各部分的子函数,实现调控功能。

void main()
{
    print_begin_message();  //输出监控程序信息

    while(1)
    {
        get_inst(str);
        if(cmp(str, "help"))  //输出帮助信息
        {
            print_help();
        } 
        else if(cmp(str, "program"))  //输出文件表
        {
            print_program_info();
        }
        else if(cmp(str, "clear"))   //清屏
        {
            clear();
        }
        else if(str[0]=='1' || str[0]=='2' || str[0]=='3' || str[0]=='4' )  //用户程序调用
        {
            int i=0;
            while(str[i])
            {
                if(str[i]=='1' || str[i]=='2' || str[i]=='3' || str[i]=='4' )
                {
                        char num = str[i]-'0'-1;
                        LoadnEx(PI[num].inDisk, PI[num].size);  
                        printStr("finish loading program ");
                        printChar(num+'0'+1);
                }
                i++; 
            }
        }
        else if(cmp(str, "exit"))  //退出程序
        {
            return;
        }
    }

    return;
}

其中对程序文件的记录使用了一个简单的结构体:

struct ProgramInfo
	{
      const char *name;
      int size;
      int inDisk;
	}PI[PROGRAM_NUMBER] = 
    {
        {"Program1", 1, 6},
        {"Program2", 1, 7},
        {"Program3", 1, 8},
        {"Program4", 1, 9}
    };

子函数

子函数包括以下几个,由于硬件接口已经由汇编代码实现,逻辑实现也比较简单,这里仅简单介绍其功能:

  • print_begin_message():输出监控程序信息;
  • cmp():比较两个字符串是否相同;
  • printChar():调控输出单个字符串,主要实现对换行符的处理;
  • get_inst():获取输入的指令;
  • print_help():输出帮助信息;
  • print_program_info():输出文件列表信息;
  • runProgram():按命令顺序执行用户程序;
  • printStr() :输出一整个字符串;

完整的C程序代码如下:

#define WIDTH 80
#define HEIGHT 25
#define PROGRAM_NUMBER 4
#define BUFFER 20

char monitorMessage[] = "Welcome to the monitor program, please enter help to get infomation.\r";
char helpMessage[] = 
"HELP MESSAGE------------------------------\r"
"help: show the help infomation\r"
"clear: clear the screen\r"
"program: show the program list\r"
"1: run program1\r"
"2: run program2\r"
"3: run program3\r"
"4: run program4\r"
"-------------------------------------------\r";

extern char getChar();
extern void putChar(char ch);
extern void LoadnEx(int inDise, int size);
extern void clear();
extern char X;
extern char Y;

char str[BUFFER] = {0};
struct ProgramInfo
	{
		const char *name;
        int size;
        int inDisk;
	}PI[PROGRAM_NUMBER] = 
    {
        {"Program1", 1, 6},
        {"Program2", 1, 7},
        {"Program3", 1, 8},
        {"Program4", 1, 9}
    };

void printChar(const char ch)
{
	if(ch == '\r')
    {
        setNextLine();
        return;
    }
    putChar(ch);
    return;
}

void printStr(char *str)
{
    int i=0;
    while(str[i])
    {
        printChar(str[i]);
        i++;
    }
}

void get_inst(char* str)
{
    int i=0;
    str[0] = 0;
    while(1)
    {
        str[i] = getChar();
        if(str[i] == 0x0D)
        {
            setNextLine();
            break;
        }    
        i++;
    }
    str[i] = 0;
    return;
}


int cmp(char str1[], char str2[])
{
    int i=0;
    for(; str1[i]!=0 && str2[i]!=0 ; i++)
    {
        if(str1[i] != str2[i])
        {
            return 0;
        }
    }
    if(str1[i]!=0 || str2[i]!=0)return 0;
    else return 1;
}


void print_begin_message()
{
    printStr(monitorMessage);
}

void print_program_info()
{
    int i=0;
    for(;i<4;i++)
    {
        printStr(PI[i].name);
        printChar('-');
        printStr("Size:");
        printChar('0'+PI[i].size);
        printChar('-');
        printStr("inDisk:");
        printChar('0'+PI[i].inDisk);
        printChar('\r');
    }
    
    return;
}


void print_help()
{
    int i=0;
    printStr(helpMessage);
}

void runProgram(char str[])
{
    int i=0;
    while(str[i])
    {
       if(str[i]=='1' || str[i]=='2' || str[i]=='3' || str[i]=='4' )
       {
            char num = str[i]-'0'-1;
            LoadnEx(PI[num].inDisk, PI[num].size);  
            printStr("finish loading program ");
            printChar(num+'0'+1);
       }
       i++; 
    }
    return;
}

void main()
{
    
    print_begin_message();

    while(1)
    {
        get_inst(str);
        if(cmp(str, "help"))
        {
            print_help();
        }
        else if(cmp(str, "program"))
        {
            print_program_info();
        }
        else if(cmp(str, "clear"))
        {
            clear();
        }
        else if(str[0]=='1' || str[0]=='2' || str[0]=='3' || str[0]=='4' )
        {
            int i=0;
            while(str[i])
            {
                if(str[i]=='1' || str[i]=='2' || str[i]=='3' || str[i]=='4' )
                {
                        char num = str[i]-'0'-1;
                        LoadnEx(PI[num].inDisk, PI[num].size);  
                        printStr("finish loading program ");
                        printChar(num+'0'+1);
                }
                i++; 
            }
        }
        else if(cmp(str, "exit"))
        {
            return;
        }
    }

    return;
}

4、运行结果

引导程序加载:

独立内核加载:

14

帮助信息:

15

文件列表:

16

清屏:

17

用户程序运行:

18

退出程序:

5、调试记录

由于本次实验设计C与汇编的交互,需要实现不少跳转功能,所以遇到了不少问题,主要使用bochs进行调试。

a、用户程序调用

实现用户程序调用的过程中包括了多次连续的跳转,依次为:内核C部分 -> 内核汇编部分 -> 用户程序 -> 返回内核汇编部分 -> 返回内核C部分。

遇到的第一个问题是无法成功调用用户程序,反汇编检查用户程序加载位置9100h,发现没有成功加载;发现加载的偏移量OffSetOfUserPrg1设置为9100h,而执行时由于cs=800h,系统默认加上了段起始地址,导致加载位置出现了偏移,也成功在内存11100h中反汇编找到用户程序代码,最后将OffSetOfUserPrg1修改为1100h,解决这一问题。

另一个问题是无法从内核汇编部分返回内核C部分,检查发现用户程序修改了寄存器ds和es的值,返回至内核时由于没有修改回来,导致程序出错。在用户程序返回后修改ds和es的值,解决这一问题。

b、用户程序无法按顺序执行

完成交互指令时发现,在使用顺序执行用户程序的指令(例如123431)时,执行完第一个程序后无法继续执行。用户程序执行的逻辑如下:

void runProgram(char str[])
{
    int i=0;
    while(str[i])
    {
       if(str[i]=='1' || str[i]=='2' || str[i]=='3' || str[i]=='4' )
       {
            char num = str[i]-'0'-1;
            LoadnEx(PI[num].inDisk, PI[num].size);  
            printStr("finish loading program ");
            printChar(num+'0'+1);
            setNextLine();
       }
       i++; 
    }
    return;
}

确认了逻辑无误,猜想是执行时LoadnEx操作影响了某些变量,使用bochs反汇编内存内容,结合c程序编译后的汇编代码,找到runProgram的位置,发现使用了寄存器si存储局部变量i;而在执行了第一次循环之后,si的值变成了不正常的0x0008

检查汇编代码中使用si的部分,发现在用户程序中使用了寄存器si,这一步操作修改了si从而导致循环出现了错误。因此,修改用户程序,使用栈对si的原始值进行保护,重新编译运行,成功解决问题。

27

六、实验总结

在熟悉混合汇编的工作中,主要问题有以下几点:

  • 对tcc和tasm语法的不熟悉:汇编代码中tasm的语法与前两次实验使用的nasm语法并不一样,代码编写后出现了大量的语法报错,而tasm语法也很难找到具体的规定记录,例如内存单元使用ds:[label]的写法而不是[ds:label],call/jmp不能直接加800h:100h,比较难找到顺利通过编译的方法。tcc的编译也和gcc有所差异,比如不能使用//进行注释,不能将bool作为函数返回值,但这些情况比较显然,更容易修改。
  • 全局变量声明、访问,函数互相调用的不熟悉:成功通过编译之后,存在的问题为函数跳转出错、访问的数据为乱码。为此我将步骤2的实例写入一个虚拟软盘,写了一个简单的引导程序进行加载和执行,利用bochs确认运行情况,最后确认了成功调用的方法。
  • 全局变量的声明——汇编代码需要放在代码位置(放在数据段会发生标号错误),加上public标识,在C部分以extern表示;C代码的全局变量在汇编部分需要以exten标识。
  • 外部函数的调用——汇编代码的函数需要使用proc near的写法,同样在C部分以extern表示;C代码的函数在汇编部分需要以exten标识。函数参数传递使用栈实现,返回值会存储在ax中。

七、参考文献