;文件说明:保护模式微型系统内核程序
;常量部分
core_code_seg_sel equ 0x38;内核代码段选择子
core_data_seg_sel equ 0x30;内核数据段选择子;注意内核数据段选择子为110000B就是第六个加三个0
sys_routine_seg_sel equ 0x28;系统公用例程代码段的选择子
video_ram_seg_sel equ 0x20;视频显示缓冲区的段选择子
core_stack_seg_sel equ 0x18;内核堆栈段选择子
mem_0_4_gb_seg_sel equ 0x08;整个0~4G内存的段的选择子
;以下是系统核心的头部,用于加载内核程序
core_length dd core_end;核心程序总长度#00
sys_routine_seg dd section.sys_routine.start;系统公用例程段位置#04
core_data_seg dd section.core_data.start;核心数据位置#8
core_code_seg dd section.core_code.start;核心代码段位置#0c
core_entry dd start;核心代码段入口点#10
dw core_code_seg_sel
[bits 32]
SECTION sys_routine vstart=0;系统公用例程代码段
;字符串显示例程
put_string: ;显示0终止的字符串并移动光标
;输入DS:EBX=串地址
push ecx
.getc:
mov cl,[ebx]
or cl,cl;如果传入的字节为0
jz .exit;就跳转到.exit
call put_char;否则打印到屏幕
inc ebx;下一个字符
jmp .getc;循环
.exit:
pop ecx
retf;段间返回
;-----------------------------
put_char: ;在当前光标处显示一个字符,并推进
;光标。仅用于段内调用
;输入:CL=字符ASCII码
pushad
;以下取当前光标位置
mov dx,0x3d4;给dx显卡索引寄存器的端口号0x3d4
mov al,0x0e;把索引值0x0e给al
out dx,al;告诉显卡要当前光标的位置
inc dx;0x0f
in al,dx;取得当前光标的位置
mov ah,al;得到屏幕光标的高8位给ah
dec dx;0x0e
mov al,0x0f
out dx,al
inc dx
in al,dx
mov bx,ax;得到光标位置给bx
cmp cl,0x0d;判断输入的是不是回车符
jnz .put_0a;如果不是就跳转
mov ax,bx ;如果是就设置光标到当前行第一列的位置
mov bl,80
div bl
mul bl
mov bx,ax
jmp .set_cursor
.put_0a:
cmp cl,0x0a;判断输入的是不是换行符
jnz .put_other;如果不是就跳转
add bx,80;如果是就设置光标到下一行的当前列且看看是不是超出能显示的最大数量了
;如果超出了就向上滚动一行(.roll_screen里做的)
jmp .roll_screen
.put_other:
push es
mov eax,video_ram_seg_sel;0xb8000段的选择子
mov es,eax;把显卡的内存地址给es
shl bx,1;bx乘以2,指向下一个字
mov [es:bx],cl;把cl赋值给显卡的[es:bx]显示字符
pop es
;以下将光标位置推进一个字符
shr bx,1;bx除以2
inc bx;bx加1
.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏
jl .set_cursor
push ds
push es
mov eax,video_ram_seg_sel
mov ds,eax
mov es,eax
cld
mov esi,0xa0 ;小心!32位模式下movsb/w/d
mov edi,0x00 ;使用的是esi/edi/ecx
mov ecx,1920
rep movsd
mov bx,3840 ;清除屏幕最底一行
mov ecx,80 ;32位程序应该使用ECX
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls
pop es
pop ds
mov bx,1920
.set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
inc dx ;0x3d5
mov al,bh
out dx,al
dec dx ;0x3d4
mov al,0x0f
out dx,al
inc dx ;0x3d5
mov al,bl
out dx,al
popad
ret
;-----------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
push eax
push ecx
push edx
push eax
mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数
inc dx ;0x1f3
pop eax
out dx,al ;LBA地址7~0
inc dx ;0x1f4
mov cl,8
shr eax,cl
out dx,al ;LBA地址15~8
inc dx ;0x1f5
shr eax,cl
out dx,al ;LBA地址23~16
inc dx ;0x1f6
shr eax,cl
or al,0xe0 ;第一硬盘 LBA地址27~24
out dx,al
inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al
.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输
mov ecx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [ebx],ax
add ebx,2
loop .readw
pop edx
pop ecx
pop eax
retf ;段间返回
;------------------------------------------------------------
allocate_memory;分配内存
;输入:ECX=希望分配的字节数
;输出:ECX=起始线性地址
push ds
push eax
push ebx
mov eax,core_data_seg_sel;内核数据段选择子赋值给ds
mov ds,eax
mov eax,[ram_alloc];ds指向了数据段所以可以直接使用数据段的内容
add eax,ecx;下一次分配时的起始地址;就是说这一次分配完了下一次的起始地址
;这里应当有检测可用内存数量的指令
mov ecx,[ram_alloc];返回分配的起始地址
mov ebx,eax
and ebx,0xfffffffc
add ebx,4;强制4字节对齐
test eax,0x00000003;测试下次分配的起始地址是不是也是4字节
cmovnz eax,ebx;如果不是就mov eax,ebx如果是,就什么都不做
mov [ram_alloc],eax;把下一次分配内存的起始地址写回ram_alloc
pop ebx
pop eax
pop ds
retf
;-------------------------------
set_up_gdt_descriptor:;在GDT内安装一个新的描述符
;输入:EDX:EAX=描述符
;输出:CX=描述符的选择子
push eax
push ebx
push edx
push ds
push es
mov ebx,core_data_seg_sel;切换到核心数据段
mov ds,ebx
sgdt [pgdt];以便开始处理GDT;sgdt将GDTR寄存器的基地址和边界信息
;保存到指定的内存位置
mov ebx,mem_0_4_gb_seg_sel;把0~4G全局数据段选择子给es
mov es,ebx
movzx ebx,word [pgdt];把GDT界限传给ebx
inc bx;bx加1,这里有个小技巧(不重要),GDT总字节数,也是下一个描述符偏移
add ebx,[pgdt+2];下一个描述符的线性地址
mov [es:ebx],eax;描述符的低32位
mov [es:ebx+4],edx;描述符的高32位
add word [pgdt],8;增加一个描述符的大小
lgdt [pgdt];对GDT的更改生效pgdt
mov ax,[pgdt];得到GDT界限值
xor dx,dx;
mov bx,8;eax除以8,去掉余数就是索引号(因为每个描述符8个字节,
;又是从0开始计算的,所以余7舍掉,就是索引号)
div bx
mov cx,ax;
shl cx,3;将索引号移到正确位置
pop es
pop ds
pop edx
pop ebx
pop eax
retf
;------------------------------------------
make_seg_descriptor: ;构造存储器和系统的段描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性。各属性位都在原始
; 位置,无关的位清零
;返回:EDX:EAX=描述符
mov edx,eax
shl eax,16
or ax,bx ;描述符前32位(EAX)构造完毕
and edx,0xffff0000 ;清除基地址中无关的位
rol edx,8
bswap edx ;装配基址的31~24和23~16 (80486+)
xor bx,bx
or edx,ebx ;装配段界限的高4位
or edx,ecx ;装配属性
retf
;-----------------------------------------------
SECTION core_data vstart=0
pgdt dw 0;用于设置和修改GDT
dd 0
ram_alloc dd 0x00100000;下次分配内存时的起始地址
;符号地址检索表
salt:
salt_1 db '@PrintString'
times 256-($-salt_1) db 0
dd put_string
dw sys_routine_seg_sel
salt_2 db '@ReadDiskData'
times 256-($-salt_2) db 0
dd read_hard_disk_0
dw sys_routine_seg_sel
salt_3 db '@PrintDwordAsHexString'
times 256-($-salt_3) db 0
dd put_hex_dword
dw sys_routine_seg_sel
salt_4 db '@TerminateProgram'
times 256-($-salt_4) db 0
dd return_point
dw core_code_seg_sel
salt_item_len equ $-salt_4;每个符号地址对照表的长度
salt_items equ ($-salt)/salt_item_len;符号地址对照表的总长度
message_1 db 'If you seen this message,that means we'
db 'are now in protect mode,and the system'
db 'core is loaded,and the video display'
db 'routine works perfectly.',0x0d,0x0a,0
message_5 db 'Loading user program'
do_status db 'Done.',0x0d,0x0a,0
message_6 db 0x0d,0x0a,0x0d,0x0a,0x0d,0x0a;换3行
db 'User program terminated,control returned.',0
bin_hex db '0123456789ABCDEF'
;put_hex_dword子过程用的查找表
core_buf times 2048 db 0;内核用的缓冲区
esp_pointer dd 0;内核用来临时保存自己的栈指针
cpu_brnd0 dd 0x0d,0x0a,' ',0
cpu_brand times 52 db 0
cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,0
;===========================================================
SECTION core_code vstart=0
load_relocate_program: ;加载并重定位用户程序
;输入:ESI=起始逻辑扇区号
;返回:AX=指向用户程序头部的选择子
push ebx
push ecx
push edx
push esi
push edi
push ds
push es
mov eax,core_data_seg_sel
mov ds,eax;切换DS到内核数据段
mov eax,esi;读取程序头部数据
mov ebx,core_buf
call sys_routine_seg_sel:read_hard_disk_0
;以下判断整个程序有多大
mov eax,[core_buf];程序尺寸
mov ebx,eax
and ebx,0xfffffe00;使程序尺寸512字节对齐(能被512整除的数,)
add ebx,512;再加一个512就是取512倍数的尺寸大小
test eax,0x000001ff;低9位都为0,程序的大小正好是512的倍数嘛?
cmovnz eax,ebx;如果不是,就使用凑整的结果,如果是就直接使用eax
mov ecx,eax;实际需要申请的内存数量
call sys_routine_seg_sel:allocate_memory;申请分配内存
mov ebx,ecx;ebx->申请到的内存地址赋值给ebx
push ebx;保存该首地址
xor edx,edx;清零
mov ecx,512;
div ecx;EDX:EAX除以512,得到扇区数
mov ecx,eax;总扇区数赋值给ecx
mov eax,mem_0_4_gb_seg_sel;切换到DS到0-4GB的段
mov ds,eax
mov eax,esi;起始扇区号赋值给eax
.b1:
call sys_routine_seg_sel:read_hard_disk_0;读一个扇区
inc eax;eax++;读下一个扇区
loop .b1;循环
;建立程序头部段描述符
pop edi;恢复程序装载的首地址
mov eax,edi;程序头部起始线性地址
mov ebx,[edi+0x04];段长度
dec ebx;段界限
mov ecx,0x00409200;属性
call sys_routine_seg_sel:make_seg_descriptor;制作描述符
call sys_routine_seg_sel:set_up_gdt_descriptor;安装描述符
mov [edi+0x04],cx;把描述符选择子写回用户程序edi+0x04处
;建立程序代码段描述符
mov eax,edi
add eax,[edi+0x14]
mov ebx,[edi+0x18]
dec ebx
mov ecx,0x00409800
call sys_routine_seg_sel:make_seg_descriptor;制作描述符
call sys_routine_seg_sel:set_up_gdt_descriptor;安装描述符
mov [edi+0x14],cx;把描述符选择子写回用户程序edi+0x04处
;建立程序数据段描述符
mov eax,edi
add eax,[edi+0x1c]
mov ebx,[edi+0x20]
dec ebx
mov ecx,0x00409200
call sys_routine_seg_sel:make_seg_descriptor;制作描述符
call sys_routine_seg_sel:set_up_gdt_descriptor;安装描述符
mov [edi+0x1c],cx;把描述符选择子写回用户程序edi+0x04处
;建立程序堆栈段描述符
mov ecx,[edi+0x0c];4KB的倍率
mov ebx,0x000fffff;堆栈段的段界限为0x000fffff-倍率
sub ebx,ecx;得到段界限
mov eax,4096;
mul dword[edi+0x0c];用4096乘以倍率就是所需要的栈的大小
mov ecx,eax;准备为堆栈分配内存
call sys_routine_seg_sel:allocate_memory
add eax,ecx;因为返回的是低地址,而堆栈需要得到高端物理地址
;所以需要返回地址加上堆栈的大小得到真正的堆栈起始物理地址
mov ecx,0x00c09600;4KB粒度的堆栈段描述符
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+0x08],cx;把描述符选择子写回用户程序edi+0x08处
;重定位SALT
mov eax,[edi+0x04];用户程序头部段选择子给es
mov es,eax
mov eax,core_data_seg_sel;核心数据段选择子给ds
mov ds,eax
cld;清除方向标志位
mov ecx,[es:0x24];用户程序的SALT条目数
mov edi,0x28 ;用户程序内的SALT位于头部内0x28处
.b2:
push ecx;用户程序的SALT条目数入栈
push edi;用户程序的SALT偏移地址入栈
mov ecx,salt_items;核心程序的SALT条目数
mov esi,salt;核心程序内的SALT的偏移
.b3:
push edi;用户程序的SALT偏移地址入栈
push esi;核心程序的SALT偏移入栈
push ecx;核心程序的SALT条目数入栈
mov ecx,64;因为用cmpsd指令比较4字节,所以每个条目之多需要比对64次
repe cmpsd;重复比较,每次比较一个双字,比较完esi和edi各自都加4
jnz .b4;如果不相等就跳转到.b4
mov eax,[esi];如果相等,说明256个字节都比较完了,把ds:esi指向的4字节的例程入口偏移地址赋值给eax
mov [es:edi-256],eax;edi-256为该SALT初始地址,把eax赋值给它
;前4字节为偏移地址,最后两个字节为选择子,
;这里就是把用户程序的SALT里存放的字符串改为偏移地址
mov ax,[esi+4];把esi指向的内核SALT的选择子给ax
mov [es:edi-252],ax;把ax赋值给,SALT已经被赋值的偏移量的后面
.b4:
pop ecx;核心程序的SALT条目数出栈
pop esi;核心程序的SALT偏移出栈
add esi,salt_item_len;核心程序的SALT偏移加上一个条目的长度,以指向下一个条目
pop edi;用户程序的SALT偏移地址出栈
loop .b3;循环比较
pop edi;用户程序的SALT偏移地址出栈
add edi,256;用户程序的SALT偏移地址加上256,指向下一个SALT项
pop ecx;核心程序的SALT条目数出栈
loop .b2;循环
mov ax,[es:0x04];用户程序头部的段选择子给ax
pop es
pop ds
pop edi
pop esi
pop edx
pop ecx
pop ebx
ret
;---------------------------------
start:
mov ecx,core_data_seg_sel
mov ds,ecx
mov ebx,message_1
call sys_routine_seg_sel:put_string
;显示处理器品牌信息
mov eax,0x80000002
cpuid
mov [cpu_brand+0x00],eax
mov [cpu_brand+0x04],ebx
mov [cpu_brand+0x08],ecx
mov [cpu_brand+0x0c],edx
mov eax,0x80000003
cpuid
mov [cpu_brand+0x10],eax
mov [cpu_brand+0x14],ebx
mov [cpu_brand+0x18],ecx
mov [cpu_brand+0x1c],edx
mov eax,0x80000004
mov [cpu_brand + 0x20],eax
mov [cpu_brand + 0x24],ebx
mov [cpu_brand + 0x28],ecx
mov [cpu_brand + 0x2c],edx
;打印CPU信息
mov ebx,cpu_brnd0
call sys_routine_seg_sel:put_string
mov ebx,cpu_brand
call sys_routine_seg_sel:put_string
mov ebx,cpu_brnd1
call sys_routine_seg_sel:put_string
;打印加载用户程序通知信息
mov ebx,message_5
call sys_routine_seg_sel:put_string
mov esi,50 ;用户程序位于逻辑50扇区
;加载并重定位用户程序
call load_relocate_program
;打印完成状态
mov ebx,do_status
call sys_routine_seg_sel:put_string
mov [esp_pointer],esp;临时保存内核程序的堆栈指针
mov ds,ax;将指向用户程序头部的选择子给ds
jmp far[0x10];调到用户程序[0x10]存放的指令地址,控制权交给用户程序(入口点)
;堆栈可能切换;在[0x10]处,存放的是32位的偏移地址和16位的代码段选择子
return_point:;用户程序返回点
mov eax,core_data_seg_sel;使ds指向核心数据段
mov ds,eax
mov eax,core_stack_seg_sel;切换回内核自己的堆栈
mov ss,eax
mov esp,[esp_pointer];恢复esp
mov ebx,message_6;打印消息
call sys_routine_seg_sel:put_string
;这里可以防止清除用户程序各种描述符的指令
;也可以加载并启动其他程序
hlt
SECTION core_trail
core_end: