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)拓展自己的软件项目管理目录,管理实验项目相关文档

在NASM中嵌入GCC的C语言

我们知道C和nasm到最后都可以变成一堆机器码, 然后被CPU执行. 那么用C语言的函数设计方法和数据结构帮助我们开发操作系统就比单纯用汇编手撸方便很多. 在两者中实现交互的核心是变量和函数的共享.
在编写asm汇编文件时, 因为需要从C语言的文件中使用函数, 我们就要一行代码告诉编译器我要从其他模块中找到这个函数, 同样, 变量也要同样这样做
extern FuncFromC
extern VariableFromC
当然, C语言中也会想要调用汇编中的函数, 这时就用global修饰入口名, 导出这个入口, 让链接器识别并让C语言调用.
global FuncFromASM
global VariableFromASM
如果要用C调用asm中的函数, 那这个FuncFromASM当然也要遵循C调用约定, 后面的参数先入栈, 并由调用者清理堆栈. 为了理解这个过程, 我们可以观察gcc编译一个程序后做了什么事情. 比如这里我们写一个比较两个整形的程序.

compare.c

int num1st = 3;
int num2nd = 4;
int flag = 0;

void cmp(int a, int b) {
	if (a > b) flag = 1;
}

void main(void)
{
	cmp(num1st,num2nd);
}

cmd里敲 gcc -S compare.c就能在当前路径下得到汇编码文件compare.s. 然后我们看一看几段比较关键的汇编码

compare.s(部分)

_cmp:
LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	movl	8(%ebp), %eax
	cmpl	12(%ebp), %eax
	jle	L3
	movl	$1, _flag
L3:
	nop
	popl	%ebp
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret

_main:
LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$16, %esp
	call	___main
	movl	_num2nd, %edx
	movl	_num1st, %eax
	movl	%edx, 4(%esp)
	movl	%eax, (%esp)
	call	_cmp
	nop
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret

我们只关心最重要的函数参数压栈等过程. 首先从main入口进入, 把num2nd放到4(%esp), num1st放到(%esp), 这就是所谓的后面的参数先入栈. 然后我们调用cmp, 这时函数的返回地址入栈, 然后为了保护%ebp, ebp入栈, 这样栈里多了两个东西, 8个字节. 原来的num2nd和num1st在栈中的位置变成了4+8(%ebp)= 12(%ebp)和0+8(%ebp)= 8(%ebp). 接下来的代码, 用cmp和jle实现比较if语句赋值. 当num2nd小于等于num1st时就什么都不做, 否则赋值flag为1. 到此为止, 我们的C代码的工作就做完了.
看了这段代码, 我们就知道在编写自己的汇编程序时, 要怎么使用C文件里的函数了. 我们只需要模仿上面的代码进行压栈, 然后call目标函数, 就能实现函数传参和调用. 如果觉得函数调用麻烦, 也可以直接让C的函数操作asm中的变量, 不过那样显然不够优雅.

混合编程实例

看了那么多, 是不是应该自己试一试效果呢? 实验内容(2)要求我们实现在汇编中定义字符串, 在C中实现字符串的字符统计, 然后回到汇编打印统计结果.
那么就先写一个能统计字符串的C程序好了, 相信都大二了, 写这种程序应该秒implement. 以求简便, 我们只统计字符串中a字符的出现次数.

counter.c

__asm__(".code16gcc\n");

char CountStr[10] = {0,0,0,0,0,0,0,0,0,0};


void counta(char* str_in, int len) {
	char str[10] = "aaabbbcccd";
	int counter = 0;
	for (int i = 0;i < len;i++) {
		if (str_in[i]=='a') counter += 1;
	}
	
	int counter_str_len = 0;
	int tmp = counter;
	while (tmp != 0) {
		tmp /= 10;
		counter_str_len++;
	}
	for (int i = counter_str_len - 1;i >= 0;i--) {
		CountStr[i] = (counter % 10)+'0';
		counter = counter/10;
	}	
}
char in[10] = "abcdaaccdd";
int len = 10;

void main(void)
{
	counta(in, len);
}

我们在nasm编程里照葫芦画瓢把参数push到栈里即可.顺序是逆序的. 并且我们看见, 函数返回时要用一个add把栈指针增加8字节, 把压栈的2个参数收回. 下面再用nasm写一下汇编中的字符串定义, 函数调用和统计值打印.
需要注意的细节有, nasm环境是纯粹的16位, 不是c里的假的16位, 但是我们传到c函数里的地址是32位, 因此传char*的指针地址时要按小端序, 先push cs再push偏移.
另外, 我们在nasm中希望使用c里定义的函数counta和变量CountStr, 这两个在前面加extern修饰. 另外用global修饰入口_start, 让编译器识别.如果发现链接后的二进制文件很混乱, 数据和代码混在一起了, 就用[section .data]和[section .text]规定数据和代码都放在什么地方.

printer.asm

BITS 16
extern counta
extern CountStr
global _start
;global CountStr

[section .text]

_start:
    mov ax,cs
    mov ds,ax
    mov es,ax
    mov ss,ax
_call:
	mov bp, Message
	mov cx, 10
	mov ax, 1301h
	mov bx, 0007h
	mov dh, 5
	mov dl, 5
	int 10h
	push  dword[msglen]
	mov ax, Message
	add word[loc32], ax
	push  dword[loc32]
	push  cs
	call  counta
	add  esp, 8
	; 上面调用counta函数统计Message的a字符数目
	; 以字符串形式把统计值保存在CountStr中
	
	mov ax, cs
	mov ds, ax
	mov es, ax
	mov ss, ax
	mov bp, CountStr
	mov cx, 10
	mov ax, 1301h
	mov bx, 0007h
	mov dh, 13
	mov dl, 13
	int 10h	
	; 这段是定位段和bios调用打印字符串
_end:
	jmp $
	
[section .data]
Message		db	"abcaadacaa"
loc32		dd      0
msglen      dd  	10

我们把这两个文件编译并链接, 可以得到可执行文件. makefile的批处理命令如下

nasm -f elf32 2printer.asm -o 2printer.o
gcc -c 2counter.c -o 2counter.o
ld.exe -melf_i386 -N 2printer.o 2counter.o -Ttext 0x7c00  --oformat binary  -o boot.bin
objdump -D -b binary -m i386 boot.bin > boot.i
gcc -Og -S 2counter.c -o 2counter.s

除了编译和链接, 我们还objdump出来指令文件和汇编码方便debug. 用winhex打开发现没到512字节, 那么好, 我们把它做成引导扇区直接在虚拟机上跑起来.
C与汇编开发独立批处理的内核_第1张图片
统计成功, a在字符串中出现了6次.
到这里, 我们已经完成了第一个gcc和nasm混合编程的程序. 使用C开发操作系统的时代到来了, 后面我们会使用相同的技术, 把一些更有挑战性的工作交给C处理, 就像上面的工作一样, 即使只是一个itoa这样的接口, 如果用nasm自己实现, 我想没有半小时是搞不定的吧, 但是用C只需要几分钟.

混合编程监控程序

我们在实验2里开发了一个监控程序, 它具备基本的输入输出功能, 基本的读扇区能力写内存和跳转能力. 很显然, 读扇区写内存和跳转都不是C擅长的, 因此我们仍然用汇编实现这些功能, 但是屏幕的输入输出, 串处理和循环等功能, C会比汇编做得更好.
上一个实验, 我们的程序接收用户输入1~4来启动用户程序, 通过自定义中断终止程序运行. 现在, 我们可以用C把这件事情做得更好, 我们在C中定义表, 并把程序名和程序在扇区中的实地址记录在表里, 这样就能用类似shell的命令进行输入输出了. 另外, 我们还可以定义任务栈实现批处理. 为此, 我们需要在c中定义相当多的辅助函数和变量, 如字符串比较的strcmp, 打印字符串的print 接收输入的input, 管理用户程序的结构体和表, 任务栈和入栈出栈函数等等. 这些工作我们将分步实现.

(*
首先简单总结一下前面的实验告诉我们的知识, 所有汇编与c相互调用需要做的事情

  1. 汇编调用c的变量和函数, 需要先声明extern
  2. 汇编调用c的函数, 以我的16位编程为例, 先按照函数参数表的顺序逆序压入参数, 以4字节为单位, 按小端序压栈参数(先压栈0x0000h再压栈16位数据) , 然后压栈16位的cs, 最后call目标函数. c函数返回后要在汇编中移动esp回收栈资源.
  3. c使用汇编的变量, 通过栈, 也就是函数传参实现, 参考2
  4. c使用汇编的函数, 在汇编中用global声明. 因为c压栈了返回地址(16位), 参数偏移从esp+2开始. 以4字节为单位, 按小端序读取参数.
  5. 使用c时, c进行的所有函数调用都是使用32位地址, 而nasm中的call是16位近地址, 为了让两者统一, 我们在nasm统一使用call far来进行16位间接绝对远调用. 同时返回时使用retf, 从栈中拿出双字.
    *)
    使用假光标
    在学习c的时候, 我们的stdio提供跟随光标的键盘字符输入输出. 我们使用scanf时, 每敲一个字符都会在屏幕上把它显示出来, 如果敲回车输入就结束. printf时, 字符串从当前光标所指的地方开始打印, 直到遇到0. 在上面的密码系统开发时, 容易发现在每一步都考虑要打印的字符串的位置是困难的. 为此我们可以实现简单的光标型输入输出, 来更好的实现用户交互界面开发.
    我们就不要求format匹配功能了, 就设计能自动定位和换行的光标即可.
void putch(char ch, int offset);
void cls();

void clear(){
	cls();
	X = 0;Y = 0;
}

void print(char* msg){
	int offset;
	for (;*msg;msg++){
		offset = (X*80+Y)*2;
		if (*msg != '\r' && *msg != '\n'){
			putch(*msg, offset);
			Y++;
		}
		else{
			X++;
			Y = 0;
		}
		if (Y==SCREEN_WIDTH){
			Y = 0; X++;
			if (X==SCREEN_HEIGHT)
				X = 0;
		}
	}
}

void input(char* s){
	char tmp[2] = " \0";
	for (;; s++)
	{
		*s = _getch();
		*tmp = *s;
		print(tmp);
		if (*s == '\r' || *s == '\n')
			break;
	}
	*s = '\0';
}



char _getch(){
	char ch;
	asm volatile("int $0x16\n"
				 : "=a"(ch)
				 : "a"(0x0000));
	return ch;
}


int _strcmp(char* str1, char* str2){
	for (;;str1++,str2++){
		if (!*str1){
			if (!*str2) return 0;
			else return 1;
		}
		else if (*str1 && !*str2) return -1;
		if (*str1<*str2) return 1;
		else if(*str1>*str2) return -1;
	}
	return 0;
}

int _strlen(char* s){
	int len = 0;
	for (;*s;len++){}
	return len;
}

cls:
	push es
	push si
	mov ax, 0B800h
	mov es, ax
	mov si, 0
	mov cx, 80*25   ; 循环次数
	mov dx, 0
	clsLoop:
		mov [es:si], dx
		add si, 2
	loop clsLoop
	pop si
	pop es
	retf

putch:
	push es
	push si
	mov ax, 0B800h
	mov es, ax
	mov ax, word[esp + 12]
	mov si, ax
	mov dl, byte[esp + 8]
	mov dh, 0Fh
	mov [es:si], dx
	pop si
	pop es
	retf

引导扇区和内核分离
我们之前的程序都是逻辑简单而且比较短的程序, 可以直接放进512字节的首扇区里, 但是当我们使用c和汇编共同作业时, 生成的代码量将会比之前多很多. 显然512字节已经不太足够我们使用了, 为此我们要把我们写好的主程序(C+nasm)封装成COM程序, 用引导扇区把这段程序加载到内存并执行. 加载用户程序的方法上一个实验我们实现过. 我们在引导扇区通过bios调用加载代码和数据进内存, 再用jmp跳转到目标位置, 从此机器就完全由目标位置的主程序接管.
下面给出一个简单的密码操作系统内核, 在开机后它会反复请求用户输入五位的有效密码, 直到密码正确, 机器退出循环并打印问候语welcome.

kernel.asm
BITS 16
extern main
extern info
extern print
extern password


%macro print 4	; string, length, x, y
	mov ax, cs
	mov ds, ax
	mov es, ax
	mov ss, ax
	mov bp, %1
	mov cx, %2
	mov ax, 1301h
	mov bx, 000fh
	mov edx,0
	mov dh, %3
	mov dl, %4
	int 10h
%endmacro

[section .text]

global _start
global cls
global putch
;global getch

targetAddr dw 0

%macro _CALL 2
	push eax
	mov ax, %1
	mov [targetAddr+2], ax ;targetaddr为0
	mov ax, %2
	mov [targetAddr], ax
	pop eax
	call far [targetAddr]
%endmacro

_start:
	_CALL cs, main
	print info,7,10,10
	jmp $


ckernel.c(main)
void main(void){
	while(1){
		print("Enter the password: \0");
		input(user_str);
		if (_strcmp(user_str,password)==0) 
			break;
		else
			print("incorrect password.\0");
		_getch();
		clear();
	}
	print("correct password.\0");
}



leader.asm

bits 16
org  7c00h
OffSetOfKerPrg equ 0A100h

mov  cl, 2		; 从第二个扇区开始
mov  ax, cs		; 定位
mov  ds,  ax    ; DS = CS
mov  es,  ax    ; ES = CS
mov  ss,  ax    ; SS = CS

mov ah, 2		;读扇区
mov al, 3		;读扇区数
mov dl, 0		;驱动器号
mov dh, 0		;磁头号
mov ch, 0		;柱面号

mov bx, OffSetOfKerPrg	;数据缓冲区
int 13H
jmp OffSetOfKerPrg	;跳转到内核

times	510-($-$$) 	db		0
dw		0xaa55

我们把leader编译得到的二进制引导扇区文件boot.bin放在第一个扇区, 内核编译得到的COM程序文件放在第二个和第三个扇区, 执行可以看到下面的结果
C与汇编开发独立批处理的内核_第2张图片
C与汇编开发独立批处理的内核_第3张图片
程序的逻辑很简单, 这个程序主要是为了验证我们的C与汇编是否正常协同工作. 这样看来应该是问题不大了, 下面我们真正来开发一个简陋的shell.
C Shell
我们这个简单的shell要实现的功能是, 允许主程序执行用户程序并监控返回. 首先我们把之前在实验2中实现的加载用户程序的汇编代码封装成能在C中调用的函数. 它接受一个扇区号, 然后进行bios调用加载程序并跳转.
除此之外, shell中设置记录用户程序位置的表和用户程序栈, 程序将按顺序出栈并依次执行.

cshell.c

__asm__(".code16gcc");

#define PRG_NUM 4
#define SCREEN_WIDTH 80
#define SCREEN_HEIGHT 25

void putch(char ch, int offset); //from asm
void cls(); //from asm
void LoadProgram(int, int); //from asm
void CallProgram(); //from asm
void load_user_prg(int, int); //加载执行用户程序
void clear();  //清屏
void print(char* ); //打印字符串
void input(char* ); //键盘读入字符串
char _getch(); //键盘读入单个字符
int _strcmp(char* , char* ); //比较字符串
int _strlen(char* );  //获取字符串长度

char info[12] = "system halt\0";
int TaskStack[100];
char cmd[100];
int X,Y; //光标
int stack_length;
char intchar[5] = "0\0";

typedef struct Prg{
	char name[5];
	int size;
	int address;
}Program;
//定义用户程序表, 结构体中存储程序名, 字节数, 扇区数
Program PrgTable[PRG_NUM] = {
	{"lu\0",255,11},
	{"ru\0",255,12},
	{"ll\0",255,13},
	{"rl\0",255,14},
};


void clear(){
	cls();
	X = 0;Y = 0;
}

void print(char* msg){
	int offset;
	for (;*msg;msg++){
		offset = (X*80+Y)*2;
		if (*msg != '\r' && *msg != '\n'){
			putch(*msg, offset);
			Y++;
		}
		else{
			X++;
			Y = 0;
		}
		if (Y>=SCREEN_WIDTH){
		Y = 0; X++;}
		if (X>=SCREEN_HEIGHT)
			X = 0;
	}
}

void input(char* s){
	char tmp[2] = " \0";
	for (;; s++)
	{
		*s = _getch();
		*tmp = *s;
		print(tmp);
		if (*s == '\r' || *s == '\n')
			break;
	}
	*s = '\0';
}



char _getch(){
	char ch;
	asm volatile("int $0x16\n"
				 : "=a"(ch)
				 : "a"(0x0000));
	return ch;
}


int _strcmp(char* str1, char* str2){
	for (;;str1++,str2++){
		if (!*str1){
			if (!*str2) return 0;
			else return 1;
		}
		else if (*str1 && !*str2) return -1;
		if (*str1<*str2) return 1;
		else if(*str1>*str2) return -1;
	}
	return 0;
}

int _strlen(char* s){
	int len = 0;
	for (;*s;len++){}
	return len;
}

void run_user_prg(int sec, int file_size) {
	int snum = file_size/512+1;
	LoadProgram(sec,snum);
	print("press any key to start\n\0");
	_getch();
	clear();
	CallProgram();
	clear();
}


void printint(int x){
	intchar[0] = '0'+x;
	print(intchar);
}

void main(void)
{
	X = 0; Y = 0;
	stack_length = 0;
	//run_user_prg(PrgTable[1].address, PrgTable[1].size);
	print("Type help for command list.\n\0");
	while (1) {
		print(">>\0");
		input(cmd);
		if (!_strcmp(cmd, "help")) {
			print("shell\n\0");
			print("you can enter following command for execute a user\'s program\n\0");
			print("ls: list all the user\'s programs\' information\n\0");
			print("[prg name]: enter any program\'s name to push it into task stack, it will be executed soon\n\0");
			print("exec: execute all the program in the stack\n\0");
			print("clear: clear your screen\n\0");
			print("exit: exit your os\n\0");
		}
		else if (!_strcmp(cmd, "ls")) {
			for (int i = 0;i < PRG_NUM;i++) {
				print("Program \0");
				printint(i+1);
				print(": \0");
				print(PrgTable[i].name);
				print("\n\0");
			}
		}
		else if (!_strcmp(cmd, "clear"))
			clear();
		else if (!_strcmp(cmd, "exec")) {
			while (stack_length>0) {
				int indice = TaskStack[stack_length-1];
				print("Execute program \0");
				print(PrgTable[indice].name);
				print("\n\0");
				run_user_prg(PrgTable[indice].address, PrgTable[indice].size);
				stack_length--;
			}
		}
		else if (!_strcmp(cmd, "exit")){
			print("Goodbye!\0");
			break;
		}
		else {
			int found = 0;
			for (int i = 0;i < PRG_NUM;i++) {
				if (!_strcmp(cmd, PrgTable[i].name)) {
					TaskStack[stack_length++] = i;
					print("it is now in stack, stack length \0");
					printint(stack_length);
					print("\n\0");
					found = 1;
					break;
				}
			}
			if (!found) {
				print("invalid command, type help for command list.\n\0");
			}
		}
	}
}

kernel.asm
BITS 16
extern main
extern info

global _start
global cls
global putch
global LoadProgram
global CallProgram
OffSetOfUserPrg equ 0100h

%macro print 4	; string, length, x, y
	mov ax, cs
	mov ds, ax
	mov es, ax
	mov ss, ax
	mov bp, %1
	mov cx, %2
	mov ax, 1301h
	mov bx, 000fh
	mov edx,0
	mov dh, %3
	mov dl, %4
	int 10h
%endmacro

%macro _CALL 2
	push eax
	mov ax, %1
	mov [targetAddr+2], ax ;targetaddr为0
	mov ax, %2
	mov [targetAddr], ax
	pop eax
	call far [targetAddr]
%endmacro

[section .text]

_start:
	push es
	push si
	mov ax, 0000h		; 中断向量表从0h开始
	mov es, ax
	mov ax, 20h			; 重定义20号中断
	mov bx, 4			; 每个中断向量是4字节的地址,所以20h乘以4
	mul bx
	mov si, ax			; 偏移
	mov ax, int20h		
	mov [es:si], ax		; 把我们的int20的偏移地址作为20号中断的中断服务程序
	add si, 2
	mov ax, cs			; 放入代码段, 符合中断向量的格式(前两字节为偏移,后两字节为代码段,且为大端序)
	mov [es:si], ax
	pop si
	pop es
	
	mov ax, cs
	mov ds, ax
	mov es, ax
	mov ss, ax
	
	_CALL cs, main
	
	print info,11,10,10
	jmp $


;void LoadProgram(int sec, int snum) 
LoadProgram:
    mov ah, 2			;读扇区
	mov cl, byte[esp+4]	;读扇区号	
	mov al, byte[esp+8]	;读扇区数
	mov dl, 0		;驱动器号
	mov dh, 0		;磁头号
	mov ch, 0		;柱面号
	mov bx, OffSetOfUserPrg	;数据缓冲区
	int 13H
    retf
	
;void CallProgram() 
CallProgram:
	_CALL cs, OffSetOfUserPrg
	retf;

; void putc(char ch, int offset)
putch:
	push es
	push si
	mov ax, 0B800h
	mov es, ax
	mov ax, word[esp + 12]
	mov si, ax
	mov dl, byte[esp + 8]
	mov dh, 0Fh
	mov [es:si], dx
	pop si
	pop es
	retf


	
cls:
	push es
	push si
	mov ax, 0B800h
	mov es, ax
	mov si, 0
	mov cx, 80*25   ; 循环次数
	mov dx, 0
	clsLoop:
		mov [es:si], dx
		add si, 2
	loop clsLoop
	pop si
	pop es
	retf
	
; 自定义的中断号
int20h:
	mov ah, 01h     ;缓冲区检测
	int 16h
	jz noclick      ;缓冲区无按键
	mov ah, 00h
	int 16h
	cmp ax, 2c1ah	; 检测Ctrl + Z
	jne noclick
	mov ax, 0fffh   ;只是作为一个标志
noclick:
	iret


[section .data]
targetAddr dw 0

bootloader.asm
bits 16
org  7c00h
OffSetOfKerPrg equ 0A100h

mov ax, cs		;定位es
mov es, ax

mov cl, 2		;从第二个扇区开始
mov ah, 2		;读扇区
mov al, 6		;读扇区数
mov dl, 0		;驱动器号
mov dh, 0		;磁头号
mov ch, 0		;柱面号

mov bx, OffSetOfKerPrg	;数据缓冲区
int 13H
jmp OffSetOfKerPrg	;跳转到内核

times	510-($-$$) 	db		0
dw		0xaa55

user.asm

BITS 16
org			00100h
mov			ax, cs
mov			ds, ax
mov			es, ax
mov			al, byte[upper]
add			al, 5
mov			byte[x],al
mov			al, byte[left]
add			al, 5
mov			byte[y],al
	
loop1:
	dec word[count]				; 递减计数变量
	jnz loop1					; >0:跳转;
	mov word[count],delay
	dec word[dcount]			; 递减计数变量
    jnz loop1
	mov word[count],delay
	mov word[dcount],ddelay
	; 以上是用一个二重循环实现时延50000*580个单位时间
	
	jmp			Entrance		;进行一个周期的工作
	jmp			$				;halt

Entrance:
	int			20h
	cmp    		ax, 0fffh
	jne 		NotRet
	retf
NotRet:
	jmp			BoundaryCheckx
DispStr:
	call		Clear
	call		Reset
	mov			ax, Message			;打印字符串
	mov			bp, ax
	mov			cl, byte[Strlen]    ;字符串长
	mov			ch, 0
	mov			ax, 01301h			;写模式
	mov			bx, 000fh			;页号0,黑底白字		
	mov			dh, byte[x]			;行=x
	mov			dl, byte[y]			;列=y
	int			10h					;10h号接口
Updatexy:
	mov 		al, byte[x]
	add			al, byte[vx]
	mov			byte[x], al
	mov 		al, byte[y]
	add			al, byte[vy]
	mov			byte[y], al
	jmp			loop1				;无限循环
	
	
BoundaryCheckx:
	mov 		al, byte[x]
	add			al, byte[vx]	;预测下一刻的x
	cmp			al, byte[upper]	;如果x小于上边界
	jl			Changevx		;更新vx
	cmp			al, byte[lower]	;如果x大于下边界
	jg			Changevx		;更新vx
	
BoundaryChecky:
	mov 		al, byte[y]
	add			al, byte[vy]
	cmp			al, byte[left]	;如果y小于左边界
	jl			Changevy		;更新vy
	add			al, byte[Strlen];预测下一刻的yr=y+字符串长
	cmp			al, byte[right]	;如果yr大于下边界
	jg			Changevy		;更新vy
	jmp			DispStr			;如果不需要更新vx vy就继续打印流程			


Changevx:
	neg			byte[vx]
	jmp			BoundaryChecky
	
Changevy:
	neg			byte[vy]
	jmp			DispStr
	
	
Clear:
	mov ax, 0B800h
	mov es, ax
	mov si, 160
	mov cx, 80*24   ; 循环次数
	mov dx, 0
	clsLoop:
		mov [es:si], dx
		add si, 2
	loop clsLoop
	ret
;通过直接修改显存实现的清屏函数

Reset:
	mov ax, cs
	mov ds, ax
	mov ax, ds
	mov es, ax
	ret
;把cs ds和es指向相同的内存
	

Message:			db		"sysu"
Strlen              db      $-Message
delay 				equ 	50000
ddelay 				equ 	2000
count 				dw 		delay
dcount 				dw 		ddelay
clearcount			db		0

vx    				db 		1
vy    				db 		1
left				db		0
upper				db		13
right				db		39
lower				db		24
x    				db 		0
y    				db		0


times	510-($-$$) 	db		0
dw		0xaa55

makefile.bat
nasm -f elf32 kernel.asm -o kernel.o
gcc -Og -c cshell.c -o cshell.o
ld.exe -melf_i386 -N kernel.o cshell.o  -Ttext 0x0A100 --oformat binary  -o kernel.com
nasm leader.asm -o boot.bin
nasm user.asm -o user.com

编译执行, 我们将能够得到com形式二进制文件user.com, kernel.com, 引导扇区程序boot.bin. 我们把boot.bin放入引导扇区, 把user.com以及和它类似的用户程序放在11 12 13 14四个扇区, 把kernel.com放在第二扇区之后的6个扇区内, 就可以用虚拟机执行我们自己的shell了.
C与汇编开发独立批处理的内核_第4张图片
C与汇编开发独立批处理的内核_第5张图片
C与汇编开发独立批处理的内核_第6张图片
C与汇编开发独立批处理的内核_第7张图片
C与汇编开发独立批处理的内核_第8张图片
可以实现命令行级别的交互工作和程序运行, 到此为止, 基本的内核功能就算开发完毕.

小结

本实验还算是蛮消磨心智的, 最先遇到的问题是能否找到一条能顺利让c和汇编协同编译的一套软件, 以及合适的命令. 这里选择了开发比较简单的gcc和nasm, 我个人是感觉比老师推荐的tcc+tasm好很多. 然后我在正式实验前, 尝试了很多更简单的c+asm小程序的编写, 从而把所有可能遇到的坑都踩一遍, 比如不使用org要怎样告诉编译器内存偏移, c的32位地址和asm的16位近地址调用该如何协同统一, 段寄存器的修改会在怎样的程度上影响程序的运行, 如何保护寄存器等等. 幸运的是经过两天半的努力还是把这件事做完了, 虽然踩坑的过程比较痛苦, 但是真正把一整套流程做下来, 对我的提升还是相当大的,

你可能感兴趣的:(操作系统)