一、实验题目
实验三:用汇编与 C 语言开发独立内核
二、实验目的
- 加深理解操作系统内核概念
- 了解操作系统开发方法
- 掌握汇编语言与高级语言混合编程的方法
- 掌握独立内核的设计与加载方法
- 加强磁盘空间管理工作
三、实验要求
- 知道独立内核设计的需求
- 掌握一种x86汇编语言与一种C高级语言混合编程的规定和要求
- 设计一个程序,以汇编程序为主入口模块,调用一个C语言编写的函数处理汇编模块定义的数据,然后再由汇编模块完成屏幕输出数据,将程序生成COM格式程序,在DOS或虚拟环境运行。
- 汇编语言与高级语言混合编程的方法,重写和扩展实验二的的监控程序,从引导程序分离独立,生成一个COM格式程序的独立内核。
- 再设计新的引导程序,实现独立内核的加载引导,确保内核功能不比实验二的监控程序弱,展示原有功能或加强功能可以工作。
- 编写实验报告,描述实验工作的过程和必要的细节,如截屏或录屏,以证实实验工作的真实性。
实验内容
- 寻找或认识一套匹配的汇编与c编译器组合。利用c编译器,将一个样板C程序进行编译,获得符号列表文档,分析全局变量、局部变量、变量初始化、函数调用、参数传递情况,确定一种匹配的汇编语言工具,在实验报告中描述这些工作。
- 写一个汇编程和c程序混合编程实例,展示你所用的这套组合环境的使用。汇编模块中定义一个字符串,调用C语言的函数,统计其中某个字符出现的次数(函数返回),汇编模块显示统计结果。执行程序可以在DOS中运行。
- 重写实验二程序,实验二的的监控程序从引导程序分离独立,生成一个COM格式程序的独立内核,在1.44MB软盘映像中,保存到特定的几个扇区。利用汇编程和c程序混合编程监控程序命令保留原有程序功能,如可以按操作选择,执行一个或几个用户程序、加载用户程序和返回监控程序;执行完一个用户程序后,可以执行下一个。
- 利用汇编程和c程序混合编程的优势,多用c语言扩展监控程序命令处理能力。
- 重写引导程序,加载COM格式程序的独立内核。
- 拓展自己的软件项目管理目录,管理实验项目相关文档。
四、实验方案
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
中,包含example
、comb
、os
三个子文件夹:
- example:样例C程序相关文件;
- comb:混合编程实例相关文件;
- os:独立内核相关文件。
五、实验过程
1、样例C程序分析
为了学习汇编与C编译器组合,我们先使用一个样例程序熟悉C与汇编的相互调用、参数传递等情况。
其中upper.c
为C程序代码,包含全局变量message
、num
和函数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
目录中。
程序思路如图:
- 在汇编程序和C程序中各声明了一个字符串。
- 汇编程序作为入口,调用C程序的函数Calculate;
- 在函数Calculate中计算汇编程序字符串的字符个数,计算完成后调用汇编程序的OutPut函数;
- OutPut函数中使用中断显示两个字符串,完成后跳转回函数Calculate;
- 将计算结果作为返回值传递给汇编程序,在汇编程序中使用中断显示。
全局变量声明
在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
使用两次中断,显示两个字符串,作为子程序,需要使用proc
和endp
进行定义,在最后使用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,返回结果正确。
汇编程序中数据变量的位置
在实验中发现了无法正确读取汇编字符串变量的问题,为了解决该问题,尝试将该字符串变量分别放置于代码段或数据段中:
将其放在代码段时,最终得到的程序能够成功运行,且正确显示字符(如上面运行结果所显示);
但将其放在数据段时,汇编所读取的汇编字符串变量会出现乱码:
通过反汇编内存找到了访问标号地址的指令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
函数; - 从主函数返回后,显示独立内核的结束信息,之后重新设置
sp
和ss
值,返回引导程序中。
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 near
和endp
格式,且在子函数始末使用了push
和pop
操作,这是为了让函数中栈的调用(即参数传递)得到正常数值(因为在编程中发现,如果不使用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
压栈,再将900h
和100h
压栈。(如图)
完成压栈操作后,使用retf
进行跳转,此时程序会赋值cs=900h
、ip=100h
,便成功跳转至用户程序;用户程序返回时,retf
指令会将剩下两个参数出栈,使cs和ip恰好指向我们需要的返回地址。
同时要注意,虽然用户程序被加载至900h:100h
区域,但OffSetOfUserPrg1
的值不是9100h
而是1100h
,这是由于此时段起始地址为800h
,800h: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、运行结果
引导程序加载:
独立内核加载:
帮助信息:
文件列表:
清屏:
用户程序运行:
退出程序:
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的原始值进行保护,重新编译运行,成功解决问题。
六、实验总结
在熟悉混合汇编的工作中,主要问题有以下几点:
- 对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中。