用汇编语言实现一个操作系统雏形(SnailOS 0.00)

效果图 (修改了调度算法后,下面描述的问题已经没有了,希望不会引入新的BUG)

这里期待按下ESC挂起第一个进程,按下F1键恢复第一个进程,但是并未实现,

实际运行结果为有时挂起第一个进程,有时挂起其余两个进程,有时则宕机。

百度网盘下载地址:

https://pan.baidu.com/s/1_-IznMWL3z1CROziiD6mCw

用汇编语言实现一个操作系统雏形(SnailOS 0.00)_第1张图片

 

;mykernel01.s
;This is Kernel entry.

bits 32 ;32位模式
newstart:
	mov esp, stack ;置堆栈指针为本段程序,最后面定义的空间。
	
	lidt [idtr] ;加载中断描述符表寄存器。
	lgdt [gdtr] ;加载全局描述符表寄存器。
	
	; 加载段选择子(从第0个算起)。
	; 只有gs加载了第4个,段基地址为
	; 0xb8000,即是文本模式下显存地址。
	; 其他都加载为第2个,即2 * 8,
	; 段基地址为0x10000。
	mov ax, 2 * 8 
	mov ds, ax
	mov es, ax
	mov fs, ax
	mov ss, ax
	mov ax, 4 * 8
	mov gs, ax
	
	jmp dword 3 * 8 : mystart ;这是重新加载cs选择子。
	
mystart:
	;入栈5个参数	
	push divide_error ;第一个参数是函数入口地址。
	push 0 ;特权级
	push 14 ;描述符属性,14可能是陷阱门
	push 0 ;这是第0个异常,即除零异常。
	push idt ;idt在本段的偏移地址。
	call _set_gate ;调用设置门描述的函数。
	add esp, 5 * 4 ;丢弃5个参数。
	
	;入栈5个参数	
	push timer_int
	push 0
	push 14
	push 0x20
	push idt
	call _set_gate ;调用设置门描述的函数
	add esp, 5 * 4 ;丢弃5个参数
	
	;入栈5个参数	
	push keyboard_int
	push 0
	push 14
	push 0x21
	push idt
	call _set_gate ;调用设置门描述的函数
	add esp, 5 * 4 ;丢弃5个参数
	
	;入栈5个参数	
	push system_call
	push 3
	push 15
	push 0x80
	push idt
	call _set_gate ;调用设置门描述的函数
	add esp, 5 * 4 ;丢弃5个参数
	
;	call old_set_gate ;这个程序仅供测试用。

	call cls ;调用清除屏幕的过程。

	;调用带参数的打印函数。
	;两个参数一个是字符串指针
	; 另一个是字符属性。
		
;	mov edx, 0 ;奇怪的机器,edx如果不赋值,也异常。
;	mov eax, 1
	mov ebx, 100 ;除零操作,目的是产生0号异常。
	div bx ;这里虽然未除以0,但是却产生了异常。

	mov eax, 0
	int 0x80

	push 0xf
	push str3
	call disp
	add esp, 2 * 4
	
	call _init_8259 ;初始化可编程中断控制器
					;完全是抄书了。
	call _init_8253 ;初始化可编程定时器。还是抄书。
	call _enable_timer_key ;始终中断和键盘中断。


	;因为内核程序是从0x10000(64k处)处开始,所以这里实际
	;是将全局描述符表中的对应描述符的基地址改为从64k开始。
	mov ebx, 0x10000
	mov ecx, gdt

	lea eax, [tss0]
	mov edi, 5 * 8
	call set_base
	
	lea eax, [ldt0]
	mov edi, 6 * 8
	call set_base	

	lea eax, [tss1]
	mov edi, 7 * 8
	call set_base
	
	lea eax, [ldt1]
	mov edi, 8 * 8
	call set_base	
	
	lea eax, [tss2]
	mov edi, 9 * 8
	call set_base
	
	lea eax, [ldt2]
	mov edi, 10 * 8
	call set_base	

	;这里与赵炯先生大作一样,是切换到用户态的典型模式。
	pushf
	and dword [esp],0xffffbfff ;据说是复位任务嵌套标志,
	popf					   ;但我不确定。
	mov eax,5 * 8 
	ltr ax			;加载任务状态段的描述符。
	mov eax,6 * 8
	lldt ax			;加载局部描述符表的描述符。

	sti ;CPU的中断允许标志置位,只有置位才能开启可屏蔽中断。
	;这是典型的利用中断返回指令,切换到用户态。
	;据说linux0.11也是用的这种方式。
	push long 2 * 8 + 7 ;将局部描述表的第2个描述符的选择子入栈,
						;即是进程的堆栈段入栈。(从0开始)
	push long f0_u_stack ;堆栈指针入栈。
	pushf ;标志寄存器入栈。
	push long 1 * 8 + 7 ;将局部描述表的第1个描述符的选择子入栈,
						;进程代码段入栈。
	push long myfunc0 ; 指令指针入栈。
	iret ;中断返回,从此进入用户态,下面的代码不会被执行。
	
myloop:
	push 0xf
	push my_0x
	call disp
	add esp, 8
	push dword [myval]
	call _myprint
	add esp, 4
	inc dword [myval]

	mov eax, 1
	int 0x80

	push 0xf
	push mysapce
	call disp
	add esp, 8
	call mydelay
	
	push 0xd
	push my_0x
	call disp
	add esp, 8
	push dword [myval]
	call _myprint
	add esp, 4
	inc dword [myval]

	mov eax, 0
	int 0x80

	push 0xf
	push mysapce
	call disp
	add esp, 8
	jmp myloop
;	jmp $ ;无限循环。

myval: dd 0
mysapce: db " ", 0
my_0x: db "0x", 0

mydelay: ;重复执行,起到延时作用。
	push ecx
	mov ecx, 0xffff
.1:
	loop .1
	pop ecx
	ret

;清除屏幕的子程序,没有什么难度。	
cls:
	push eax
	push ecx
	push edi
	mov ecx, 2000
	xor eax, eax
	xor edi, edi
.1:
	mov [gs:edi], ax ;gs是显存段,edi是偏移。
	inc edi
	inc edi
	loop .1
	pop edi
	pop ecx
	pop eax
	ret


scr_p: dd 0	;保存字符显示位置的全局变量

;这是屏幕上显示字符的程序。
;它接收两个参数,第一个是串地址,第二个是
;字符属性。如果遇到换行符则自动换行,打印到屏幕
;结尾处,在清除屏幕,并从屏幕左上角开始显示。
disp: ; void disp(char *str, int attr);
	push ebp
	mov ebp, esp
	push eax
	push esi
	push edi ;保存用到的寄存器。
	mov esi, [ebp + 2 * 4]
	mov ah, [ebp + 3 * 4] ;取得两个参数。
	mov edi, [scr_p] ;取得位置变量。
get_ch:	
	mov al, [esi] ;要显示字符串的位置。
	cmp al, 0 ;看是否是字符串结束字符。
	je exit ;是,程序就返回。
	cmp edi, 4000 ;是否到屏幕结尾处。
	je top ;是则置位置为屏幕开头处,并清屏。
	cmp al, 0xa ;是换行符吗,是则进行换行操作。
	jne d_c ;换行无非是整数行增加160字节,因此除以160
	push edx ;然后在乘以160,得到目前的整数行,然后再
	push eax ;加上160。
	push ebx
	xor edx, edx
	mov eax, edi
	mov ebx, 160
	div ebx
	mul ebx
	add eax, ebx
	mov edi, eax
	pop ebx
	pop eax
	pop edx
	jmp inc_esi
d_c:
	mov [gs:edi], ax ;显示一个字符。
	add edi, 2
inc_esi:	
	inc esi
	jmp get_ch
	
top:
	call cls
	xor edi, edi
	jmp get_ch
	
exit:
	mov [scr_p], edi
	pop edi
	pop esi
	pop eax
	pop ebp
	ret

;*************
;这段程序我们期待它能够正常的进行二进制数到十六进制数的转化
global _bin2ascii
extern _buf
_bin2ascii:
;char *bin2ascii(unsigned int value);
;ret_addr   ebp                 \						
;	\        \                   \
; 0 * 4	   1 * 4	            2 * 4
	push ebp
	mov ebp, esp
	push ebx
	push esi
	push ecx
	lea ecx, [ebp + 2 * 4] ;取入栈数值的地址
	xor esi, esi  ;esi清零
.1:	cmp dword [ecx], 0 ;数值是否为零,为零则退出并做后处理
	je .2
	push 16 ;这里是如调用除法函数,我们把数值除以16得到返回值为余数
	push ecx
	call _mydiv
	add esp, 2 * 4
	mov ebx, [mystr + eax] ;在表中查找16进制数对应的ascii,则保存在
	mov [_mybuf + esi], bl ;临时数组_mybuf中
	inc esi
	cmp dword [ecx], 0 ;如果循环除法等最终结果为零,则转化完毕,退出后处理
	je .3
	jmp .1
.2:
	mov byte [_mybuf + esi], '0'
	inc esi
.3:
	mov byte [_mybuf + esi], 0
	push _mybuf ;这里是计算字符串的长度函数调用
	call _mstrlen
	add esp, 1 * 4
	push eax ;这里是字符串反置,并保存在_mybuf1中。
	push _mybuf1
	push _mybuf
	call _cp_ch
	add esp, 3 * 4
	lea eax, [_mybuf1]
	pop ecx
	pop esi
	pop ebx
	pop ebp
	ret
	

;除法程序,将value除以base,value保存为商,而返回值为余数。
global _mydiv	
_mydiv:
;unsigned int mydiv(unsigned int *value, unsigned int base);
;ret_addr     ebp                  \			     \		
;	\          \                    \                 \
; 0 * 4	     1 * 4	               2 * 4             3 * 4
	push ebp
	mov ebp, esp
	push ebx
	push edx
	push esi
	xor edx, edx
	mov esi, [ebp + 2 * 4]
	mov eax, [esi]
	mov ebx, [ebp + 3 * 4]
	div ebx
	mov [esi], eax
	mov eax, edx
	pop esi
	pop edx
	pop ebx
	pop ebp
	ret

;计算字符串的长度,返回值为长度。
global _mstrlen
_mstrlen:
;unsigned int mstrlen(char *str);
;ret_addr     ebp           \			   
;	\          \             \           
; 0 * 4	     1 * 4	       2 * 4
	push ebp
	mov ebp, esp
	push ecx
	push esi
	xor ecx, ecx
	mov esi, [ebp + 2 * 4]
.1:
	inc ecx
	lodsb
	cmp al, 0
	jne .1
	dec ecx
	mov eax, ecx
	pop esi
	pop ecx
	pop ebp
	ret

;反置字符串,返回值为目标字符串首地址。
global _cp_ch
_cp_ch: ;char *cp_ch(char *s, char *d, unsigned int len);
;ret_addr     ebp        \         \			     \		
;	\          \          \         \                 \
; 0 * 4	     1 * 4	    2 * 4      3 * 4             4 * 4	
	push ebp
	mov ebp, esp
	push ecx
	push esi
	push edi
	mov ecx, [ebp + 4 * 4]
	mov esi, [ebp + 2 * 4]
	add esi, ecx
	dec esi
	mov edi, [ebp + 3 * 4]
.1:	mov al, [esi]
	mov [edi], al
	dec esi
	inc edi
	dec ecx
	jnz .1
	mov byte [edi], 0
	mov eax, [ebp + 3 * 4] 
	pop edi
	pop esi
	pop ecx
	pop ebp
	ret
	
_myprint: ;这里只是把上面两个函数合并封装了一下,没什么特别。
;void myprint(int val);
	push ebp
	mov ebp, esp
	push dword [esp + 2 * 4]
	call _bin2ascii
	add esp, 1 * 4
	push 0xf	
	push _mybuf1
	call disp
	add esp, 2 * 4
	pop ebp
	ret

;*************	
	
global _mybuf, _mybuf1
_mybuf: 
times 64 db 0
_mybuf1: 
times 64 db 0	
mystr: db "0123456789abcdef",0
	
	
;这段程序是参考了赵炯先生的大作《linux0.12内核完全注释》
;中的一个简单的多任务内核实例程序。
old_set_gate:
	mov edx, timer_int ;将处理函数偏移地址放入edx中
	mov eax, 0x00180000 ;将内核代码段选择符存入eax高16位
	mov ax, dx ;将处理函数偏移地址低16位放入ax
	mov dx, 0x8f00 ;门描述符属性放入dx中,应该是陷阱门
	mov edi, idt ;中断描述符表的基地址
	mov ecx, 0x20 ;异常、中断的编号,0为除零异常。
	mov [edi + ecx * 8], eax ;把以上编排好的门描述符
	mov [edi + 4 + ecx * 8], edx ;存入描述表相应位置
	ret
	
		
;*******************这段程序是参考了linux 0.12中代码改编的
;位于linux/include/asm/system.h中的	_set_gate宏函数	
;             void set_gate(int gate_addr, int int_num, int type, int dpl, int offset)
;                                \             \           \        \         \
;ret_addr 0 * 4  ebp 1 * 4      2 * 4         3 * 4       4 * 4    5 * 4     6 * 4


_set_gate: 
	push ebp ;保存ebp在堆栈中
	mov ebp, esp ;取当前堆栈指针
	push eax
	push ecx
	push edx
	push ebx
	mov edx, [ebp + 5 * 4] ;dpl是描述符特权级
	shl edx, 13 ;左移13位后正好是门描述符特权级的位置
	mov eax, [ebp + 4 * 4] ;type是描述符属性
	shl eax, 8 ;左移8位后正好是门描述符是哪种门的位置
	add edx, eax ;这里相加应该等价于or操作
	add edx, 0x8000
	mov eax, [ebp + 6 * 4] ;offset是入口程序相对于内核代码段的偏移
	and eax, 0xffff0000 ;低16位清零
	add edx, eax
	mov eax, [ebp + 6 * 4]
	and eax, 0x0000ffff ;高16位清零
	add eax, 0x00180000 ; 高16位的0x0008是内核代码段的选择符
	mov ecx, [ebp + 3 * 4] ;门的向量号
	mov ebx, [ebp + 2 * 4] ;中断描述符表的基地址
	mov [ebx + ecx * 8], eax ;把以上编排好的门描述符
	mov [ebx + 4 + ecx * 8], edx ;存入描述表相应位置
	pop ebx
	pop edx
	pop ecx
	pop eax
	pop ebp
	ret
;*******************

;void set_tssldt_desc(int n, int addr, char type);
;ret_addr    ebp         \        \        \
;  0 * 4    1 * 4       2 * 4    3 * 4    4 * 4
_set_tssldt_desc: ;这是设置全局描述符表中,任务状态段描述符和
	push ebp ;局部描述符的程序,这里没有用到,也不知对不对。
	mov ebp, esp
	push esi
	push eax
	push ebx
	mov esi, [ebp + 2 * 4]
	mov eax, [ebp + 3 * 4]
	mov ebx, [ebp + 4 * 4]
	mov word [esi], 104
	mov [esi + 2], ax
	ror eax, 16
	mov [esi + 4], al
	mov [esi + 5], bl
	mov byte [esi + 6], 0
	mov [esi + 7], ah
	ror eax, 16
	pop ebx
	pop eax
	pop esi
	ret

	
;void set_seg_desc(int gate_addr, char type, char dpl, int base, int limit)	
;ret_addr     ebp       \               \         \        \          \
; 0 * 4       1 * 4    2 * 4          3 * 4      4 * 4    5 * 4      6 * 4      

_set_seg_desc: ;这是按照段描述符的规格,设置段描述符的程序,没有用到。
	push ebp
	mov ebp, esp
	push esi
	push eax
	push ebx
	push edx
	
	mov esi, [ebp + 2 * 4]
	
	mov eax, [ebp + 5 * 4]
	and eax, 0x0000ffff
	shl eax, 16
	mov ebx, [ebp + 6 * 4]
	and ebx, 0x0000ffff
	add eax, ebx
	
	mov edx, [ebp + 5 * 4]
	mov ebx, edx
	and edx, 0xff000000
	shr ebx, 16
	and ebx, 0x00ff
	add edx, ebx
	mov ebx, [ebp + 6 * 4]
	and ebx, 0x000f0000
	add edx, ebx
	mov ebx, [ebp + 4 * 4]
	shl ebx, 13
	add edx, ebx
	add edx, 0x00408000
	mov ebx, [ebp + 3 * 4]
	shl ebx, 8
	add edx, ebx
	
	mov [esi], eax
	mov [esi + 4], edx
	
	pop edx
	pop ebx
	pop eax
	pop esi
	pop ebp
	ret

;本程序来自赵炯先生大作,只是把描述符中的地址项目,改成与我们的
;内核地址相对应,但是这是必须的,否则程序不会正常运行。
;当然如果将内核代码移动到地址0处开始,则不用调用此程序。	
set_base: ;无非是按照段描述符的规格,填写物理地址。
	add eax, ebx
	add edi, ecx
	mov [edi + 2], ax
	ror eax, 16
	mov [edi + 4], al
	mov [edi + 7], ah
	ror eax, 16
	ret
	
;set_base example ;一个样例

;setup base fields of descriptors.
;	mov ebx, 0x10000
;	mov ecx, gdt

;	lea eax, [tss0]
;	mov edi, 5 * 8 ;注意我们这里的选择符都是采用
;	call set_base ;x * 8的格式的,也可以某个描述,然后
				  ;减去描述表首地址,当然如果是局部描述符
				  ;还用加上7,像这样x * 8 + 7
;   lea eax, [ldt0]
;   mov edi, 6 * 8
;	call set_base

;********************


;这段处理程序完全是抄袭linux0.12中断处理程序汇编代码
;赵炯先生的大作对此描述的非常详细,不过要想彻底弄懂
;必须精通堆栈的操作,也就是我们前面讲的东西。
;所以建议看大作之前,深度阅读前面的堆栈内容。
;我这里就不抄袭大作了,只是给出了代码。
divide_error: 
	push do_divide_error 
no_error_code:
	xchg [esp], eax
	push ebx
	push ecx
	push edx
	push edi
	push esi
	push ebp
	push ds
	push es
	push fs
	push dword 0
	lea edx, [esp + 11 * 4]
	push edx
	mov edx, 2 * 8
	mov dx, ds
	mov dx, es
	mov dx, fs
	call eax
	add esp, 2 * 4
	pop fs
	pop es
	pop ds
	pop ebp
	pop esi
	pop edi
	pop edx
	pop ecx
	pop ebx
	pop eax
	iret

do_divide_error:

	add dword [esp + (11 + 2) * 4], 2 ;[esp + 52]中是中断返回去到的指令的地址
						   ;也就是出现除零异常div ebx(=0)的偏移地址
						   ;因为这条指令的代码长度为2字节,所以+2
						   ;是跳过这条指令,继续执行下面的指令。
	push 0xc
	push e_0_str
	call disp
	add esp, 2 * 4
	ret
	
e_0_str: db "Divide Error!...", 0
	
_init_8259: ;这里用到的代码完全是linux0.12中的代码了
			;虽说是抄书,其实好多系统都是这么设置的。
	mov al, 0x11 ;初始化命令,据说是通知8259我要搞你了。
	out 0x20, al ;分别向主从芯片发送初始化命令。
	out 0xa0, al ;8259是两片级联的。
	mov al, 0x20 ;主芯片的中断从0x20开始,0x20是时钟中断
	out 0x21, al
	mov al, 0x28 ;从芯片从0x28开始。
	out 0xa1, al
	mov al, 0x04 ;这两条应该是主的告诉从的你连接我的什么引脚
	out 0x21, al ;而从的又告诉主的我的哪个引脚接入你。
	mov al, 0x02
	out 0xa1, al
	mov al, 0x01 ;这是告诉工作在什么方式,最重要的一点是要求,
	out 0x21, al ;发生中断后,处理程序发送此次中断结束信号。
	out 0xa1, al ;否则只发生1次中断就不在接收了。
	mov al, 0xff ;这是屏蔽所有中断信号,以待以后用到时开启。
	out 0x21, al
	out 0xa1, al
	ret

_init_8253:
	mov al, 0x36 ;定时器的初始化说起来就是一句话,它是告诉
	mov dx, 0x43 ;时钟的发生装置,多长时间发生一次中断,
	out dx, al   ;据说这个对系统的性能有着十分关键的作用,
	mov ax, 11931 ;也就是说如果发生的频繁了,运行时间就全被
	mov dx, 0x40 ;时钟中断用去了,如果是频率比较低,那么你就
	out dx, al ;进程特别的迟钝,不过我没有编代码尝试过,又是
	mov al, ah ;道听途说罢了。设置频率是100毫秒发生一次时钟
	out dx, al ;中断,这些都是抄书了。
	ret
	
_enable_timer_key: ;这是开启时钟、键盘中断
	mov dx, 0x21
	in al, dx
	and al, 0xfc ;关键就是这里,0xfc的二进制是11111100
	out dx, al ;也就是把第0位和第1位清零后,写入端口
	ret			;0x21就行了。

	

_current: dd 5 * 8

tss_table:
	dd tss0
	dd tss1
	dd tss2
tss_table_end: 
	dd 0
	
addr_t_t:
	dd tss_table

myaddr: 
		dq 0

timer_int: ;这里时钟中断处理程序简陋的没法再……
	pusha
	push ds
	push es
	push fs
	push gs
	push ss
	mov edx, 2 * 8
	mov ds, edx
;	mov ss, edx
	mov al, 0x20 ;这里既是发送本次中断结束信号,
	out 0x20, al ;之所以注释掉是当然是只让它发生一次
;	push 0xc  ;这样屏幕就不会被时钟中断弄得乱糟糟了。
;	push timer_str
;	call disp
;	add esp, 2 * 4



;全局变量中存放的是tss_table的值,
;但tss_table是常量,所以这里设置了一个变量,增加4是指向下一个任务。
.2:	add dword [addr_t_t], 4 
;是否到达任务指针数组的尾部。
	cmp dword [addr_t_t], tss_table_end
;到达则跳转到.1处。
	jae .1
;把数组的地址,存入ebx中,从而取ebx中的内容,[ebx]为第一个任务状态段的地址
	mov ebx, dword [addr_t_t]
	mov esi, [ebx]
;看是否是空任务,是则忽略。
	cmp esi, 0
	je .2
;如果不是空任务,则取其LDT选择符,我自认为这里是唯一的标识,所以取这里的值。
	mov eax, [esi + 24 * 4]
;这里是得到相对应得TSS选择符。(可以参照全局表,和tss任务状态段来理解)。
	sub eax, 8
;将选择符存入符合处理器远跳转指令的规格的数据结构。
	mov [myaddr + 4], eax
	
;这是模仿linux0.11的任务切换方式,可参考赵炯先生大作的switch_to的那个宏函数,
;这里与之完全相同。
;是否是当前进程,是则中断返回,不切换任务。
	mov edx, [_current]
	cmp eax, edx
	je int_end
;不是的情况下,切换任务。
	mov [_current], eax
	jmp far [myaddr] ;一定要用这个远跳转指令否则不能实现。
	jmp int_end

.1:
	mov dword [addr_t_t], tss_table - 4
	jmp .2
	
int_end:
;这里主要是用键盘使0号进程挂起,但我不清楚是否就是向进程发送的一个信号。
;但我企图实现之。
	cmp dword [kill_key], 1
	jne .3
	mov dword [tss_table + 0 * 4] , 0
.3: 
	cmp dword [kill_key], 0
	jne .4
	mov dword [tss_table + 0 * 4] , tss0
.4:

	pop ss
	pop gs
	pop fs
	pop es
	pop ds
	popa
	iret
	
timer_str: db "    TIMER is happended!      ", 0xa,0	

kill_key: dd 3
	
keyboard_int: ;键盘中断处理程序。
	pusha
	push ds
	push es
	push fs
	push gs
	push ss
	mov edx, 2 * 8
	mov ss, edx
	in al, 0x60 ;从60端口接收键盘缓冲区的数据,
	mov [value], al
	mov al, 0x20 ;如果不接收,下一次的数据就不会被
	out 0x20, al ;处理,键盘中断是按下时发生一次,
				;松开时又发生一次。
	cmp byte [value], 0xe0
	je .1
	cmp byte [value], 0xe1
	je .1
	test byte [value], 0x80
	jne .1
	cmp dword [value], 0x1 ;ESC扫描码
	jne .3
	mov dword [kill_key], 1 ;按下ESC键则kill_key置1
.3:	
	cmp dword [value], 0x3b ;F1扫描码
	jne .2
	mov dword [kill_key], 0	;按下F1键则kill_key置0
.2:	
	push dword [value]
	call _bin2ascii
	add esp, 1 * 4
	push 0x9	
	push _mybuf1
	call disp
	add esp, 2 * 4	
.1: 
	pop ss
	pop gs
	pop fs
	pop es
	pop ds
	popa
	iret
	
value: dd 0
keyboard_str: db 0, 0, "    KEYBOARD is happended!      ", 0xa,0

;我们的系统调用,也是十分的简陋了,好在没有宕机。
mycolor: dd 0
v1: dd 0
v2: dd 0xffff
system_call:
	pusha
	push ds
	push es
	push fs
	push gs
	push ss
	mov edx, 2 * 8
	mov ss, edx
	cmp eax, 0 ;当eax == 0是显示以一种颜色显示变量。
	je .1
	jne .2
.1:	
	mov dword [mycolor], 0xe
	push 0xe
	push my_0x
	call disp
	add esp, 8
	push dword [v1]
	inc dword [v1]
	jmp .5
.2: 
	cmp eax, 1 ;当eax == 1是显示以另一种颜色显示变量。
	je .3
	jne .4
.3:
	mov dword [mycolor], 0xb
	push 0xb
	push my_0x
	call disp
	add esp, 8
	push dword [v2]
	inc dword [v2]
	jmp .5
.4: 
	jmp .6
.5:	
	call _bin2ascii
	add esp, 1 * 4
	push dword [mycolor]	
	push _mybuf1
	call disp
	add esp, 2 * 4
	push 0xf
	push mysapce
	call disp
	add esp, 8
.6:	pop ss
	pop gs
	pop fs
	pop es
	pop ds
	popa
	iret
	
;这里定义了打印的字符串。	
str: db "         Hello, World!", 0xa, 0

str1: db "        Snail OS is starting......", 0xa, 0

str2: db "    I hope My Os is beautiful!", 0xa,0

str3: db "    Divide happended running ...", 0xa, 0xa, 0

str4: db 0xa, 0


times 1024 db 0 ;堆栈的定义,暂时就是这些吧,也不知多少才好。
stack:

idtr: ;被lidt加载的中断描述符表寄存器的内容
	dw 256 * 8 - 1 ;中断描述附表段限长,从0开始的字节长度,
					;据说最长就是这些了,因为只有256个中断。
	dd idt + 0x10000 ;idt的物理地址。

align 8 ;8字节对齐
idt: ;中断描述符偏移地址
times 256 dq 0 ;dq是8字节的定义法

gdtr:
	dw 20 * 8 - 1
	dd gdt + 0x10000

gdt:
; 分别是第0、1、2、3、4个描述符
	dq 0x0000000000000000	; 0 * 8 
	dq 0x00c09a000000ffff	; 1 * 8
	dq 0x00c092010000ffff	; 2 * 8
	dq 0x00c09a010000ffff	; 3 * 8
	dq 0x00c0920b800000ff	; 4 * 8
other_desc:
	dw 0x68,tss0,0xe900,0x0  ; 5 * 8 ;他可以被定义成
	dw 0x40,ldt0,0xe200,0x0  ; 6 * 8 ;TSS0_selector = other_desc - gdt
									;不过我更钟情这种 2 * 8,这就看个人喜好了。
	dw 0x68,tss1,0xe900,0x0  ; 7 * 8
	dw 0x40,ldt1,0xe200,0x0  ; 8 * 8
	
	dw 0x68,tss2,0xe900,0x0  ; 9 * 8
	dw 0x40,ldt2,0xe200,0x0  ; 10 * 8
times 9 dq 0x0000000000000000 ; 11 * 8 -- 19 * 8

align 8
ldt0:
	dq 0x0000000000000000 ; 0 * 8 + 7 ;局部描述符的选择子属性为7
	dq 0x00c0fa01000003ff ; 1 * 8 + 7 ;具说应该是特权级为3,属性为1是
	dq 0x00c0f201000003ff ; 2 * 8 + 7 ;局部描述符,然后进行或操作就
	dq 0x00c0f20b800003ff ; 3 * 8 + 7 ;应该是7,这个不肯定,早忘得
										;差不多了。

;这是任务状态段,任务切换时必须要用到的。它的长度最短应该是 26 * 4字节
;这个其实到没有什么好说的,要实现多任务,就必须按照某种固定格式设置,
;我的这个就是参照大作自己做的,几乎没有什么改变。如果觉得难,多敲几次‘
;就不会有什么问题了。										
tss0:
	dd 0
	dd krn_stk0, 2 * 8
	dd 0,0,0,0,0
	dd myfunc0,0x200
	dd 0,0,0,0
	dd f0_u_stack,0,0,0
	dd 2 * 8 + 7, 1 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7
	dd 6 * 8, 0x8000000
	
times 128 * 4 db 0
krn_stk0: ;进程的内核栈,定义在这里。

;同上
ldt1:
	dq 0x0000000000000000 ; 0 * 8 + 7
	dq 0x00c0fa01000003ff ; 1 * 8 + 7
	dq 0x00c0f201000003ff ; 2 * 8 + 7
	dq 0x00c0f20b800003ff ; 3 * 8 + 7
tss1:
	dd 0
	dd krn_stk1, 2 * 8
	dd 0,0,0,0,0
	dd myfunc1,0x200
	dd 0,0,0,0
	dd f1_u_stack,0,0,0
	dd 2 * 8 + 7, 1 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7
	dd 8 * 8,0x8000000
	
times 128 * 4 db 0
krn_stk1:

;同上
ldt2:
	dq 0x0000000000000000 ; 0 * 8 + 7
	dq 0x00c0fa01000003ff ; 1 * 8 + 7
	dq 0x00c0f201000003ff ; 2 * 8 + 7
	dq 0x00c0f20b800003ff ; 3 * 8 + 7
tss2:
	dd 0
	dd krn_stk2, 2 * 8
	dd 0,0,0,0,0
	dd myfunc2,0x200
	dd 0,0,0,0
	dd f2_u_stack,0,0,0
	dd 2 * 8 + 7, 1 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7, 2 * 8 + 7
	dd 10 * 8,0x8000000
	
times 128 * 4 db 0
krn_stk2:


;我们的第一个进程。
myfunc0:
	mov eax, 2 * 8 + 7
	mov ds, eax
	mov eax, 3 * 8 + 7 ;这里如果不改成自己显存段,会宕机,
	mov gs, eax        ;不过这个问题,无确实没有思考明白。
						;暂且这样吧,待大侠答疑解惑!
.1:	
	mov eax, 0
	int 0x80 ;系统调用
	mov ecx, 0x7fffff

.2:
	loop .2
	jmp .1
	ret

times 64 db 0
f0_u_stack:
;进程本身的堆栈。

;我们的第二个进程。
myfunc1:
	mov eax, 2 * 8 + 7
	mov ds, eax
	mov eax, 3 * 8 + 7
	mov gs, eax

.1:	
	mov eax, 1
	int 0x80
	mov ecx, 0x8fffff
.2:
	loop .2
	jmp .1
	ret

times 64 db 0
f1_u_stack:
;进程本身的堆栈。

;我们的第三个进程。
myfunc2:
	mov eax, 2 * 8 + 7
	mov ds, eax
	mov eax, 3 * 8 + 7
	mov gs, eax

	mov ah, 0xf
.2:	
	mov al, 'A'
.1:	
	mov [gs:0 + 160],ax
	inc al
	mov [gs:2 + 160],ax
	inc al
	mov [gs:4 + 160],ax
	inc al
	mov [gs:6 + 160],ax
	inc al
	mov [gs:8 + 160],ax
	inc al
	cmp al, 'Z'
	ja .2
	mov ecx, 0x1fffff
.3:
	loop .3
	jmp .1
	ret

times 64 db 0
f2_u_stack: ;进程本身的堆栈。

 

; myboot01.s
; 此程序被BIOS加载的到0x7c00处所以
; 这里是跳转到我们的代码处,注意在
; 16位指令中,0x7c0表示要跳转到
; 段地址是0x7c00物理地址开始的段
; 而便宜地址是start16。这样我们就
; 跳过了前面的描述符定义的数据。
; 当然,如果你不愿意定义在前面,也
; 可以把描述表等定义在代码的后面
; 这样就不用跳转了。这里是参考了linux

jmp 0x7c0:start16

; 这里定义了一个含有5个描述符的全局
; 描述符表。
gdt:
; 分别是第0、1、2、3、4个描述符
	dq 0x0000000000000000	; 0 * 8 
	dq 0x00c09a000000ffff	; 1 * 8
	dq 0x00c092010000ffff	; 2 * 8
	dq 0x00c09a010000ffff	; 3 * 8
	dq 0x00c0920b800000ff	; 4 * 8
other_desc:
times 15 dq 0x0000000000000000 ; 5 * 8 -- 19 * 8

; 在内存中定义被全局描述符表寄存器加载的
; 内容	
gdtr:
		dw 20 * 8 - 1 ; 段限长
addr:
		dd 0x7c00 + gdt ; 段基地址

start16:
; 把所有的段寄存器都改成与代码段寄存器同。
	mov ax, 0x7c0 
	mov ds, ax
	mov es, ax
	mov ss, ax
	mov fs, ax
	mov gs, ax
	mov sp, 0x0 ;堆栈指针的段偏移为0
	
	cli ; 关闭中断
; int 0x13的典型用法	
	mov dx, 0x0080 ; dh, dl = 0x80第一个硬盘
	mov cx, 0x0002 ; 从第二个扇区开始读取
	mov ax, 0x1000 ; 缓冲区段地址0x10000
	mov es, ax ; es:bx为缓冲区地址
	xor bx, bx ; 置bx为0,也即是偏移为0。
	mov ax, 0x0239 ;ah为读取的功能号0x2,
; 读取?个扇区。
	int 0x13
	jnc next ;进位标志没有被置位,则成功读取。
	
	jmp $ ;读取失败进入无限循环。
	
next:
	
	lgdt [gdtr] ;加载全局描述符表寄存器。

	;打开A20地址线,这是关于键盘控制器的
	;古老的故事,忘得差不多了,还是自己
	;参考别的书籍吧。
	in al, 0x92
	or al, 0x2
	out 0x92, al
	
	;典型的开启保护模式方式,即给系统寄存器
	;cr0的第0位置1。
	mov eax, cr0
	or eax, 1
	mov cr0, eax

	; 加载段选择子(从第0个算起)
	; 只有gs加载了第4个,段基地址为
	; 0xb8000,即是文本模式下显存。
	; 其他都加载为第2个
	; 段基地址为0x10000
	mov ax, 2 * 8
	mov ds, ax
	mov es, ax
	mov fs, ax
	mov ss, ax
	mov ax, 4 * 8
	mov gs, ax
	
	; 跳转到代码段,即是加载代码段选择子
	; 第3个,段基地址为0x10000,为64K处
	; 段内偏移为0。
	jmp dword 3 * 8 : 0

	jmp $ ;根本就不可能运行到这里。

times 510 - ($ - $$) db 0 ;引导扇区除了前面代码和最后
;两个字节外,都填充0。
db 0x55, 0xaa ;最后两字节填充的内容,高字节0xaa。
rem myauto1.cmd
@echo off

nasm -fbin -o myboot01.bin myboot01.s
dd if=myboot01.bin of=hd10meg.img
nasm -fbin -o mykernel01.bin mykernel01.s
dd if=mykernel01.bin of=hd10meg.img seek=1 count=23

 

你可能感兴趣的:(操作系统,nasm,操作系统雏形,汇编语言,nasm,SnailOS,0.00)