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字節, 那麼好, 我們把它做成引導扇區直接在虛擬機上跑起來.
在這裏插入圖片描述
統計成功, 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與彙編是否正常協同工作. 這樣看來應該是問題不大了, 下面我們真正來開發一個簡陋的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和彙編協同編譯的一套軟件, 以及合適的命令. 這裏選擇了開發比較簡單的gcc和nasm, 我個人是感覺比老師推薦的tcc+tasm好很多. 然後我在正式實驗前, 嘗試了很多更簡單的c+asm小程序的編寫, 從而把所有可能遇到的坑都踩一遍, 比如不使用org要怎樣告訴編譯器內存偏移, c的32位地址和asm的16位近地址調用該如何協同統一, 段寄存器的修改會在怎樣的程度上影響程序的運行, 如何保護寄存器等等. 幸運的是經過兩天半的努力還是把這件事做完了, 雖然踩坑的過程比較痛苦, 但是真正把一整套流程做下來, 對我的提升還是相當大的,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章