★PART1:32位保护模式下内核简易模型
1. 内核的结构,功能和加载
每个内核的主引导程序都会有所不同,因为内核都会有不同的结构。有时候主引导程序的一些段和内核段是可以共用的(事实上加载完内核以后就不需要主引导程序了),和加载一般的用户程序一样,主引导程序也是需要从硬盘中读取程序到指定的内存空间中。
同时,作为一个内核,也是一个程序,而且是一个具有管理全局的能力的程序,应该有固定的段,一般来说,内核应该包括以下几个部分:
1. 公用例程段(实现API功能)
2. 内核数据区(用于预读一些数据和一些内核内置的保留的内容)
3. 内核代码区(用于执行内核自己的代码…)
PS:上述段都应该具有特权0级,特权级在教材的14章讲述
在主引导程序中,为了加载内核,首先应该在GDT中加载内核的所有的描述符。但是内核的描述符是不确定的(因为段界限和段的线性基地址不知道),所以要根据内核头部的信息来确定,内核的头部和普通的程序是一样的,也是应该把各个段的信息和入口点的相对地址列出来,同时还需要具有段的选择子(因为内核是固定的,选择子也应该固定)。然后主引导程序构建这些段的描述符(需要重定位,不过不需要再把段信息写入内核头部了,因为选择子有了),并且把描述符写入GDT并且重新加载GDT即可(主要是刷新GDT的大小)。
那怎么根据内核所给出的信息构建内核的描述符呢?教材给出了一种方法。(edi的内容是内核头部的地址)
其实原理很简单,就是给出需要加载的段的属性(因为加载什么段我们是知道的),然后只要填充好描述符的高32位和低32位就完了,教材这里用到了一个bswap(bswap r32)命令,简单来说这个命令就是将以某个寄存器中8位为一个段的首尾交换,如图:
2. 保护模式下的用户程序的重定位和加载
首先要明白一个事情,在保护模式下(特别是在有权限管理的情况下),程序是很难进行自我加载的,必须通过内核来加载。当然了,用户程序必须符合固定的格式,才能被内核识别和加载。一般来说,内核需要给用户程序提供3个段,这三个段一般是:
1. 代码段,
2. 数据段(这里应该有两个区域:用户程序头部和文字常量区),
3. 栈段,其中栈段要根据用户程序的建议的栈段大小来定(一般不建议用户自己创建栈段)。
(正常来讲还要有堆和BSS段,但是先不搞那么复杂)
内核从硬盘读取了用户程序并加载到相应位置的时候,就读取用户的程序头。我们知道,内核有公用程序段,是专门用来给用户程序提供公用例程的,这就是现代操作系统的API(Application Programming Interface),更直观来说就是就是一堆库函数名(比如C中的scanf,printf…)。早期的系统,API是通过中断号的方式公布的(也就是要通过软中断进入),现在常用方法是使用符号名。正常来讲,一个现代的操作系统应该是具有一个专门的链接器(Linker)来构建文件头的。教材用一个比较原始的方式来构建文件头了,其实这只是一个简单的字符串的匹配的过程而已。
(内核数据区的符号表的样貌)
在我们的规定中,用户程序的符号表固定是256个字符,同时每个符号还需要预留一个区域来保存符号对应过程的选择子(本章过程的调用是通过GDT进行的,下一章将会介绍调用门)。两个字符串的比较可以使用cmpsb(字节比较),cmpsw(字比较),cmpsd(双字比较),在16为模式下,源字符串的首地址由DS:SI指定,目的字符串的首地址由ES:DI指定,在32位模式下,则分别是DS:ESI和ES:EDI,在处理器内部,cmps指令的操作是把两个操作数相减,然后根据结果设置标志寄存器中的标志位。
单纯的cmps(指令族)只比较一次,需要通过rep前缀来不断驱动(直到ecx为0),但是这里不能单纯地使用rep指令,否则就无法比较了
重定位符号表后,我们就要对用户程序的段进行重定位和内存的分配,当然了,理论上在读取用户头部的时候就应该开辟一个内存放置用户程序,但是内核的做法是,先预读头部到内核数据区,然后再给用户程序分配内存,在现代操作系统中,内存分配是一个很重要的工作。内存管理程序不仅要把内存分块管理以便给应用程序分配内存,还要负责回收,还要进行虚拟内存管理的工作,非常复杂。这里教材用的是一个非常简单的分配demo,没有检测内存是否越界的功能,而且也没有内存回收。我往上加了一点东西。
接下来就是段的重定位和描述符的创建了,当然这一章没有讲特权级,所以教材呢就直接把所有的程序都按特权0级的权限加载了。当然这是一种很不好的做法,教材在14章会介绍如何用特权3级来加载程序。其实也不难,懂怎么加载内核就知道怎么怎么加载用户程序了,主要是栈段的分配要用到内存分配函数而已。
这里要注意的地方是Set_New_GDT这个过程,要在GDT中安装描述符,必须知道他的物理地址和大小,要知道这些消息,可以用sdgt指令(Store Global Descriptor Table Regiser)它用于将GDT军训器的基地址和边界信息保存到指定的内存位置,sgdt的指令格式是sgdt m/48,这个指令不会影响任何标志位。另外还有一个新的指令movzx
movzx是一个带零拓展的传送(Move With Zero-Extend)指令格式为(其实就是相当于先xor一下16/32位寄存器再写入低位的寄存器)
movzx r16,r/m8
movzx r32,r/m8
movzx r32,r/m16
注意movzx的指令目的操作数只能是16位或者是32位的通用寄存器,源操作数只能是8位或者16位的寄存器或者内存地址,而且目的操作数和源操作数的大小是不一样的。和movzx指令差不多的指令是movsx指令,他是带符号的传送,这个指令就不像movzx一样只会拓展0了,而是根据数的最高位来拓展。
GDTR的界限部分在初始化的时候会初始化为0xFFFF,当+1时,如果看成16位数,则会是0x0000,如果看成32位数,则会变成0x00010000,为了避免不必要的麻烦,直接写成inc bx,那这样的话开始填充描述符的时候就不是从0x00010000这个偏移地址开始了,而是0x00000000开始了。
因为应用程序给出的都是栈的建议大小,所以一般都是直接给出的是建议大小,所以我们直接按建议大小来乘以4KB的大小来分配内存,要注意的是栈段是向下拓展的,高地址才是栈段的线性基地址。
2. 用户程序的执行
一旦加载了用户程序,那么我们就可以直接直接一个跳转指令跳转到用户程序了
此时ds应该指向用户程序头部。0x10刚好是偏移地址(32位)+选择子(16位),接下来就是在用户程序执行一系列操作了,因为我们已经在用户程序填入了公用例程段的选择子,所以我们直接用fs指向用户头部,然后执行这些公用例程就可以了。
TerminateProgram是返回内核的公用例程,返回内核后就可以回收用户程序用的内存和创建的GDT了。
方法简单粗暴,当然学了14,15章以后我们就能学习到正确的任务切换方法了。
最后,教材还给出一种调试程序的方法,其实个人感觉没有什么用,还不如直接在bochs看呢。应该就是为了讲xlat这个查表指令而写的,该指令要求事先在DS:(E)BX出定义一个用于转换编码的表格,在16位下使用bx,在32位下使用ebx。指令执行的时候,处理器访问该表格,用AL寄存器作为偏移量,从表格中取出一个字节,传回AL寄存器。教材上写了一个调试函数。
每次将edx移动4次,总共需要移动8次,而且每次只取4位,Core_Data_Segement段的bin_hex中有16进制的对照表。Xlat不影响任何标志位。
★PART2:13章的代码
1. 源代码:
;========================保护模式主引导扇区代码========================
core_phy_base: equ 0x00040000 ;内核加载地址
core_sector_address: equ 0x00000001 ;内核所在扇区 mov ax,cs
mov ss,ax
mov sp,0x7c00 mov eax,[cs:pgdt_base+0x7c00+0x02]
xor edx,edx
mov ebx,0x10
div ebx mov ds,eax ;让ds指向gdt位置进行操作
mov ebx,edx ;别忘了还有可能出现偏移地址 ;---------------------描述符#0---------------------
mov dword [ebx+0x00],0x00000000 ;空描述符
mov dword [ebx+0x04],0x00000000
;---------------------描述符#1---------------------
mov dword [ebx+0x08],0x0000ffff ;4GB向上拓展数据段
mov dword [ebx+0x0c],0x00cf9200
;---------------------描述符#2---------------------
mov dword [ebx+0x10],0x7c0001ff ;代码段
mov dword [ebx+0x14],0x00409800
;---------------------描述符#3---------------------
mov dword [ebx+0x18],0x7c00fffe ;栈段
mov dword [ebx+0x1c],0x00cf9600
;---------------------描述符#4---------------------
mov dword [ebx+0x20],0x80007fff ;屏幕显示段
mov dword [ebx+0x24],0x0040920b mov word[cs:pgdt_base+0x7c00], ;加载gdt
lgdt [cs:pgdt_base+0x7c00] in al,0x92 ;快速开启A20
or al,0x02 ;是写入2,不要搞错了,写入1就是重启了
out 0x92,al
cli ;关掉BIOS中断 mov eax,cr0
or eax,0x01 ;设置PE位
mov cr0,eax jmp dword 0x0010:flush ;进入保护模式 [bits ]
flush:
mov eax,0x0008 ;选择4GB的代码段直接给ds
mov ds,eax
mov eax,0x0018
mov ss,eax ;设置栈段
xor esp,esp ;接下来开始读取内核头部
mov esi,core_sector_address
mov edi,core_phy_base
call read_harddisk_0 mov eax,[core_phy_base] ;读取用户总长度
xor edx,edx
mov ebx,
div ebx cmp edx,
jne @read_last_sector
dec eax
@read_last_sector:
cmp eax,
je @setup
mov ecx,eax
.read_last:
inc esi
call read_harddisk_0
loop .read_last
@setup:
mov edi,core_phy_base
mov esi,[pgdt_base+0x7c00+0x02] ;重新构建描述符#1
mov eax,[edi+0x04] ;eax线性地址,ebx是下一个线性地址
mov ebx,[edi+0x08]
sub ebx,eax ;得到段的总长度
dec ebx ;段长度-1就是段界限
add eax,edi ;得到真正的段的加载地址
mov ecx,0x00409800 ;公用例程段
call make_gdt_descriptor
mov [esi+0x28],eax ;gdt低32位
mov [esi+0x2c],edx ;gdt高32位 ;重新构建描述符#2
mov eax,[edi+0x08] ;eax线性地址,ebx是下一个线性地址
mov ebx,[edi+0x0c]
sub ebx,eax ;得到段的总长度
dec ebx ;段长度-1就是段界限
add eax,edi ;得到真正的段的加载地址
mov ecx,0x00409200 ;内核数据段
call make_gdt_descriptor
mov [esi+0x30],eax ;gdt低32位
mov [esi+0x34],edx ;gdt高32位 ;重新构建描述符#3
mov eax,[edi+0x0c] ;eax线性地址,ebx是下一个线性地址
mov ebx,[edi+0x00] ;注意这里是程序的总长度(很容易搞错变成0x10)
sub ebx,eax ;得到段的总长度
dec ebx ;段长度-1就是段界限
add eax,edi ;得到真正的段的加载地址
mov ecx,0x00409800 ;内核代码段
call make_gdt_descriptor
mov [esi+0x38],eax ;gdt低32位
mov [esi+0x3c],edx ;gdt高32位 mov word[pgdt_base+0x7c00], ;现在ds指向的就是4GB的内存,不可以用cs(会引发中断的)
lgdt [pgdt_base+0x7c00] jmp far [edi+0x10] ;可以这样跳的原因是因为在内核中,选择子都是固定的,所以不用自己操心 ;=============================函数部分=================================
read_harddisk_0: ;esi存了28位的硬盘号
push ecx mov edx,0x1f2 ;读取一个扇区
mov al,0x01
out dx,al mov eax,esi ;0~7位,0x1f3端口
inc edx
out dx,al mov al,ah ;8~15位,0x1f4端口
inc edx
out dx,al shr eax, ;16-23位,0x1f5端口
inc edx
out dx,al mov al,ah ;24-28位,LBA模式主硬盘
inc edx
and al,0x0f
or al,0xe0
out dx,al inc edx ;读命令,0x1f7端口
mov al,0x20
out dx,al .wait:
in al,dx
and al,0x88
cmp al,0x08
jne .wait mov dx,0x1f0
mov ecx,
.read:
in ax,dx
mov [edi],ax
add edi,
loop .read pop ecx ret
;----------------------------------------------------------------------
make_gdt_descriptor:
;eax:线性基地址
;ebx:段界限
;ecx:属性
mov edx,eax
and edx,0xffff0000 ;得到线性基地址的16-31位
rol edx,
bswap edx ;强行把0-7位和16-23位互换
or edx,ecx ;装载属性 shl eax,
or ax,bx ;配好段界限低16位 and ebx,0x000f0000
or edx,ebx ;装载段界限的16-19位 ret
;======================================================================
pgdt_base dw
dd 0x00007e00 ;GDT的物理地址
;======================================================================
times -($-$$) db
dw 0xaa55
;===============================内核程序=================================
;定义内核所要用到的选择子
All_4GB_Segment equ 0x0008 ;4GB的全内存区域
Stack_Segement equ 0x0018 ;内核栈区
Print_Segement equ 0x0020 ;显存映射区
Sys_Routine_Segement equ 0x0028 ;公用例程段
Core_Data_Segement equ 0x0030 ;内核数据区
Core_Code_Segement equ 0x0038 ;内核代码段
;----------------------------------------------------------------
User_Program_Address equ ;用户程序所在逻辑扇区
;=============================内核程序头部===============================
SECTION header vstart=
Program_Length dd Program_end ;内核总长度
Sys_Routine_Seg dd section.Sys_Routine.start ;公用例程段线性地址
Core_Data_Seg dd section.Core_Data.start ;内核数据区线性地址
Core_Code_Seg dd section.Core_Code.start ;内核代码区线性地址
Code_Entry dd start ;注意偏移地址一定是32位的
dw Core_Code_Segement
;----------------------------------------------------------------
[bits ]
;=========================================================================
;============================公用例程区===================================
;=========================================================================
SECTION Sys_Routine align= vstart=
ReadHarddisk: ;esi:28位磁盘号
;ebx:偏移地址
pushad mov dx,0x1f2
mov al,0x01 ;读一个扇区
out dx,al inc edx ;0-7位
mov eax,esi
out dx,al inc edx ;8-15位
mov al,ah
out dx,al inc edx ;16-23位
shr eax,
out dx,al inc edx ;24-28位,主硬盘,LBA模式
mov al,ah
and al,0x0f
or al,0xe0
out dx,al inc edx
mov al,0x20
out dx,al _wait:
in al,dx
and al,0x88
cmp al,0x08
jne _wait mov dx,0x1f0
mov ecx, _read:
in ax,dx
mov [ebx],ax
add ebx,
loop _read popad
retf
;----------------------------------------------------------------
put_string: ;ebx:偏移地址
pushad _print:
mov cl,[ebx]
cmp cl,
je _exit
call put_char
inc ebx
jmp _print
_exit:
popad
retf ;段间返回
;--------------------------------------------------------------
put_char: ;cl就是要显示的字符
push ebx
push es
push ds mov dx,0x3d4
mov al,0x0e ;高8位
out dx,al
mov dx,0x3d5
in al,dx
mov ah,al ;先把高8位存起来
mov dx,0x3d4
mov al,0x0f ;低8位
out dx,al
mov dx,0x3d5
in al,dx ;现在ax就是当前光标的位置 _judge:
cmp cl,0x0a
je _set_0x0a
cmp cl,0x0d
je _set_0x0d
_print_visible:
mov bx,ax
mov eax,Print_Segement
mov es,eax
shl bx, ;注意这里一定要把ebx变成原来的两倍,实际位置是光标位置的两倍
mov [es:bx],cl ;注意这里是屏幕!
mov byte[es:bx+],0x07
add bx,
shr bx,
jmp _roll_screen
_set_0x0d: ;回车
mov bl,
div bl
mul bl
mov bx,ax
jmp _set_cursor
_set_0x0a: ;换行
mov bx,ax
add bx,
jmp _roll_screen
_roll_screen:
cmp bx,
jl _set_cursor
mov eax,Print_Segement
mov ds,eax
mov es,eax cld
mov edi,0x00
mov esi,0xa0
mov ecx,
rep movsw
_cls:
mov bx,
mov ecx,
_print_blank:
mov word[es:bx],0x0720
add bx,
loop _print_blank
mov bx, ;别总是忘了光标的位置!
_set_cursor: ;改变后的光标位置在bx上
mov dx,0x3d4
mov al,0x0f ;低8位
out dx,al mov al,bl
mov dx,0x3d5
out dx,al mov dx,0x3d4
mov al,0x0e ;高8位
out dx,al mov al,bh
mov dx,0x3d5
out dx,al pop ds
pop es
pop ebx
ret
;----------------------------------------------------------------
allocate_memory: ;简易内存分配策略
;输入ecx:想要分配的总字节数
;输出ecx:分配的线性基地址
push ds
push eax
push ebx mov eax,Core_Data_Segement
mov ds,eax
mov eax,[ram_alloc]
mov edx,eax ;edx暂存一下eax
add eax,ecx cmp eax,edx ;发现新分配的现地址比原来的还小,说明已经溢出
jge _alloc
mov ebx,mem_alloc_fail
call Sys_Routine_Segement:put_string
mov ecx, ;分配为0说明已经分配失败
jmp _exit1
_alloc: mov ebx,eax
and ebx,0xfffffffc
add ebx, ;强行向上取整
test eax,0x00000003
cmovnz eax,ebx
mov ecx,[ram_alloc] ;要返回要分配的初始地址
mov [ram_alloc],eax ;下一次分配的线性基地址
add [ram_recycled],eax
sub [ram_recycled],ecx ;记录大小 _exit1:
pop ebx
pop eax
pop ds retf
;----------------------------------------------------------------
recycled_memory_and_gdt:
mov eax,[ram_recycled]
sub [ram_alloc],eax
mov dword[ram_recycled], ;因为我们还没学到多任务,先这样简单地清零 sgdt [pgdt_base_tmp]
sub word[pgdt_base_tmp], ;应用程序的4个段全部减掉
lgdt [pgdt_base_tmp] ;重新加载内核
retf
;----------------------------------------------------------------
PrintDword: ;显示edx内容的一个调试函数
pushad
push ds mov eax,Core_Data_Segement
mov ds,eax mov ebx,bin_hex
mov ecx, _query:
rol edx,
mov eax,edx
and eax,0x0000000f
xlat push ecx
mov cl,al
call put_char
pop ecx loop _query pop ds
popad retf
;----------------------------------------------------------------
Make_Descriptor: ;构造描述符
;输入:
;eax:线性基地址
;ebx:段界限
;ecx:属性
;输出:
;eax:描述符低32位
;edx:描述符高32位
mov edx,eax
and edx,0xffff0000
rol edx,
bswap edx
or edx,ecx shl eax,
or ax,bx
and ebx,0x000f0000
or edx,ebx
retf
;----------------------------------------------------------------
Set_New_GDT: ;装载新的描述符
;输入:edx:eax描述符
;输出:cx选择子
push ds
push es mov ebx,Core_Data_Segement
mov ds,ebx mov ebx,All_4GB_Segment
mov es,ebx sgdt [pgdt_base_tmp] movzx ebx,word[pgdt_base_tmp]
inc bx ;注意这里要一定是inc bx而不是inc ebx,因为gdt段地址初始化是0xffff的
;要用到回绕特性
add ebx,[pgdt_base_tmp+0x02] ;得到pgdt的线性基地址 mov [es:ebx],eax
mov [es:ebx+0x04],edx ;装载新的gdt符
;装载描述符要装载到实际位置上 add word[pgdt_base_tmp], ;给gdt的段界限加上8(字节) lgdt [pgdt_base_tmp] ;加载gdt到gdtr的位置和实际表的位置无关 mov ax,[pgdt_base_tmp] ;得到段界限
xor dx,dx
mov bx, ;得到gdt大小
div bx
mov cx,ax
shl cx, ;得到选择子,ti=0(全局描述符),rpl=0(申请特权0级) pop es
pop ds
retf
;=========================================================================
;===========================内核数据区====================================
;=========================================================================
SECTION Core_Data align= vstart=
;-------------------------------------------------------------------------------
pgdt_base_tmp: dw ;这一章的用户程序都是从GDT中加载的
dd ram_alloc: dd 0x00100000 ;下次分配内存时的起始地址(直接暴力从0x00100000开始分配了)
ram_recycled dd ;这里储存程序实际用的大小
salt:
salt_1: db '@Printf' ;@Printf函数(公用例程)
times -($-salt_1) db
dd put_string
dw Sys_Routine_Segement salt_2: db '@ReadHarddisk' ;@ReadHarddisk函数(公用例程)
times -($-salt_2) db
dd ReadHarddisk
dw Sys_Routine_Segement salt_3: db '@PrintDwordAsHexString' ;@PrintDwordAsHexString函数(公用例程)
times -($-salt_3) db
dd PrintDword
dw Sys_Routine_Segement salt_4: db '@TerminateProgram' ;@TerminateProgram函数(内核例程)
times -($-salt_4) db
dd _return_point
dw Core_Code_Segement salt_length: equ $-salt_4
salt_items_sum equ ($-salt)/salt_length ;得到项目总数 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, message_5 db ' Loading user program...', do_status db 'Done.',0x0d,0x0a, message_6 db 0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
db ' User program terminated,control returned.',
message_7 db 0x0d,0x0a,0x0d,0x0a
db ' We have been backed to kernel.',
message_8 db 0x0d,0x0a
db ' The GDT and memory have benn recycled.', bin_hex db '0123456789ABCDEF'
;put_hex_dword子过程用的查找表
core_buf times db ;内核用的缓冲区(2049个字节(2MB)) esp_pointer dd ;内核用来临时保存自己的栈指针 cpu_brnd0 db 0x0d,0x0a,' ',
cpu_brand times db
cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,
mem_alloc_fail db 'The Program is too large to load'
;=========================================================================
;===========================内核代码区====================================
;=========================================================================
SECTION Core_Code align= vstart=
load_program: ;输入esi:磁盘号
;输出ax: 用户程序头部选择子
push esi
push ds
push es mov eax,Core_Data_Segement
mov ds,eax ;切换到内核数据段 mov ebx,core_buf ;ebx要在内核数据缓冲区(先读取头部在缓冲区,esi已经是有扇区号了)
call Sys_Routine_Segement:ReadHarddisk mov eax,[core_buf] ;读取用户程序长度 mov ebx,eax ;给ebx一个副本
and ebx,0xfffffe00 ;清空低9位(强制对齐512)
add ebx, ;加上512
test eax,0x000001ff
cmovnz eax,ebx ;低9位不为0则使用向上取整的结果 mov ecx,eax ;eax是整个程序的向上取整的大小
call Sys_Routine_Segement:allocate_memory ;先分配内存给整个程序,再分配内存给栈区
mov ebx,ecx ;这个才是真正的程要用到的线性基地址 push ebx ;ebx就是用户程序加载到内存的地址
xor edx,edx
mov ecx, ;千万不要改掉ebx
div ecx
mov ecx,eax mov eax,All_4GB_Segment ;切换到4GB段区域(平坦模式)
mov ds,eax _loop_read:
call Sys_Routine_Segement:ReadHarddisk ;esi还是User_Program_Address
inc esi
add ebx,
loop _loop_read ;加载用户程序头部段(下面的所有段的地址都要转化为选择子)
pop edi ;程序被装载的基地址
mov eax,edi
mov ebx,[edi+0x04] ;用户程序头部的长度
dec ebx ;段界限
mov ecx,0x00409200
call Sys_Routine_Segement:Make_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [edi+0x04],cx ;放入选择子 ;加载用户程序代码段
mov eax,edi
add eax,[edi+0x14] ;别忘记重定位了
mov ebx,[edi+0x18] ;用户程序代码段的长度
dec ebx ;段界限
mov ecx,0x00409800
call Sys_Routine_Segement:Make_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [edi+0x14],cx ;放入选择子 ;加载用户程序数据段
mov eax,edi
add eax,[edi+0x1c] ;别忘记重定位了
mov ebx,[edi+0x20] ;用户程序数据段的长度
dec ebx ;段界限
mov ecx,0x00409200
call Sys_Routine_Segement:Make_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [edi+0x1c],cx ;放入选择子 ;加载用户程序栈段(用户程序给出的建议大小)
mov ecx,[edi+0x0c] ;确定栈段的建议大小
mov ebx,0x000fffff
sub ebx,ecx ;这就是段界限了
mov eax, ;4KB
mul ecx
add [ram_recycled],eax ;加上分配的内存 mov ecx,eax ;这个时候eax是大小
call Sys_Routine_Segement:allocate_memory ;分到内存
add eax,ecx ;eax线性基地址,ebx段界限
mov ecx,0x00c09600 ;4KB粒度向下拓展数据段
call Sys_Routine_Segement:Make_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [edi+0x08],cx ;现在开始重定位API符号表
;---------------------------------------------------------------------
mov eax,[edi+0x04] ;先设es再ds,不要搞反了,现在es的0x04这个地方是头部的选择子
mov es,eax
mov eax,Core_Data_Segement
mov ds,eax cld
mov ecx,[es:0x24] ;得到用户程序符号表的条数
mov edi,0x28 ;用户符号表的偏移地址是0x28 _loop_U_SALT:
push edi
push ecx mov ecx,salt_items_sum
mov esi,salt _loop_C_SALT:
push edi
push esi
push ecx mov ecx, ;比较256个字节
repe cmpsd
jne _re_match ;如果成功匹配,那么esi和edi刚好会在数据区之后的 mov eax,[esi] ;偏移地址
mov [es:edi-],eax ;把偏移地址填入用户程序的符号区
mov ax,[esi+0x04] ;段的选择子
mov [es:edi-],ax ;把段的选择子填入用户程序的段选择区 _re_match:
pop ecx
pop esi
add esi,salt_length
pop edi
loop _loop_C_SALT pop ecx
pop edi
add edi,
loop _loop_U_SALT
;---------------------------------------------------------------------
mov ax,[es:0x04] ;把头部的段选择子给ax pop es
pop ds
pop esi
ret
start:
mov eax,Core_Data_Segement
mov ds,eax mov ebx,message_1
call Sys_Routine_Segement:put_string mov eax,
cpuid
cmp eax,0x80000004 ;判断是否有0x80000002-0x80000004功能
jl _@load ;显示处理器品牌信息,从80486的后期版本开始引入
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
cpuid
mov [cpu_brand+0x20],eax
mov [cpu_brand+0x24],ebx
mov [cpu_brand+0x28],ecx
mov [cpu_brand+0x2c],edx mov ebx,cpu_brnd0
call Sys_Routine_Segement:put_string
mov ebx,cpu_brand
call Sys_Routine_Segement:put_string
mov ebx,cpu_brnd1
call Sys_Routine_Segement:put_string _@load:
mov ebx,message_5
call Sys_Routine_Segement:put_string
mov esi,User_Program_Address
call load_program mov ebx,do_status
call Sys_Routine_Segement:put_string mov [esp_pointer],esp ;临时保存一下栈指针
mov ds,ax ;使ds指向用户程序头部 jmp far [0x10] _return_point: mov eax,Core_Data_Segement
mov ds,eax
mov eax,Stack_Segement
mov ss,eax ;重新设置数据段和栈段
mov esp,[esp_pointer]
mov ebx,message_7
call Sys_Routine_Segement:put_string call Sys_Routine_Segement:recycled_memory_and_gdt
mov ecx,[ram_alloc] mov ebx,message_8
call Sys_Routine_Segement:put_string
cli
hlt
;=========================================================================
SECTION core_trail
;----------------------------------------------------------------
Program_end:
;==============================用户程序=======================================
SECTION header vstart= program_length dd program_end ;程序总长度#0x00 head_len dd header_end ;程序头部的长度#0x04 stack_seg dd ;用于接收堆栈段选择子#0x08
stack_len dd ;程序建议的堆栈大小#0x0c
;以4KB为单位 prgentry dd start ;程序入口#0x10
code_seg dd section.code.start ;代码段位置#0x14
code_len dd code_end ;代码段长度#0x18 data_seg dd section.data.start ;数据段位置#0x1c
data_len dd data_end ;数据段长度#0x20
;-------------------------------------------------------------------------------
;符号地址检索表
salt_items dd (header_end-salt)/ ;#0x24 salt: ;#0x28
Printf: db '@Printf'
times -($-Printf) db TerminateProgram:db '@TerminateProgram'
times -($-TerminateProgram) db ReadHarddisk: db '@ReadHarddisk'
times -($-ReadHarddisk) db header_end:
;===============================================================================
SECTION data align= vstart= buffer times db ;缓冲区 message_1 db 0x0d,0x0a,0x0d,0x0a
db '**********User program is runing**********'
db 0x0d,0x0a,
message_2 db ' Disk data:',0x0d,0x0a, data_end: ;===============================================================================
[bits ]
;===============================================================================
SECTION code align= vstart=
start:
User_Data_File equ ;数据文件存放地点
mov eax,ds
mov fs,eax mov eax,[stack_seg]
mov ss,eax
mov esp, mov eax,[data_seg]
mov ds,eax mov ebx,message_1
call far [fs:Printf] mov esi,User_Data_File
mov ebx,buffer ;缓冲区偏移地址
call far [fs:ReadHarddisk] ;相当于调用函数 mov ebx,message_2
call far [fs:Printf] mov ebx,buffer
call far [fs:Printf] jmp far [fs:TerminateProgram] ;将控制权返回到系统 code_end: ;===============================================================================
SECTION trail
;-------------------------------------------------------------------------------
program_end:
2. 课后习题
其实这一章的课后习题很无聊的,就是把栈段那里改一下就好了,用户程序按照第八章那样自己填一个区域。
;加载用户程序栈段
mov eax,edi
add eax,[edi+0x08]
mov ebx,[edi+0x0c] ;用户程序栈段的长度
add eax,ebx ;得到栈段的线性基地址
mov edx,0xffffffff
sub edx,ebx
mov ebx,edx ;得到段界限 mov ecx,0x00409600
call Sys_Routine_Segement:Make_Descriptor
call Sys_Routine_Segement:Set_New_GDT
mov [edi+0x08],cx ;放入选择子
;==============================用户程序=======================================
SECTION header vstart= program_length dd program_end ;程序总长度#0x00 head_len dd header_end ;程序头部的长度#0x04 stack_seg dd section.stack.start ;栈段位置#0x08
stack_len dd stack_end ;栈段长度#0x0c prgentry dd start ;程序入口#0x10
code_seg dd section.code.start ;代码段位置#0x14
code_len dd code_end ;代码段长度#0x18 data_seg dd section.data.start ;数据段位置#0x1c
data_len dd data_end ;数据段长度#0x20
;-------------------------------------------------------------------------------
;符号地址检索表
salt_items dd (header_end-salt)/ ;#0x24 salt: ;#0x28
Printf: db '@Printf'
times -($-Printf) db TerminateProgram:db '@TerminateProgram'
times -($-TerminateProgram) db ReadHarddisk: db '@ReadHarddisk'
times -($-ReadHarddisk) db header_end:
;===============================================================================
SECTION data align= vstart= buffer times db ;缓冲区 message_1 db 0x0d,0x0a,0x0d,0x0a
db '**********User program is runing**********'
db 0x0d,0x0a,
message_2 db ' Disk data:',0x0d,0x0a, data_end: ;===============================================================================
[bits ]
;===============================================================================
SECTION code align= vstart=
start:
User_Data_File equ ;数据文件存放地点
mov eax,ds
mov fs,eax mov eax,[stack_seg]
mov ss,eax
mov esp, mov eax,[data_seg]
mov ds,eax mov ebx,message_1
call far [fs:Printf] mov esi,User_Data_File
mov ebx,buffer ;缓冲区偏移地址
call far [fs:ReadHarddisk] ;相当于调用函数 mov ebx,message_2
call far [fs:Printf] mov ebx,buffer
call far [fs:Printf] jmp far [fs:TerminateProgram] ;将控制权返回到系统 code_end:
;===============================================================================
SECTION stack align= vstart=
times db
stack_end:
;===============================================================================
SECTION trail
;-------------------------------------------------------------------------------
program_end: