第10章至第12章作者介绍了保护模式的入门知识,本书前12章的代码量都是比较小的,而第13章作者写了三份代码,介绍主引导程序加载并运行内核,内核再加载并运行用户程序的过程,代码量比以往大大增加,共900多行。但不要害怕,因为后面的章节仅仅是以第13章的代码为基础做扩充,所以笔者认为第13章是一个坎,但顺利越过第13章,后面的章节将容易学习,本文结合第13章,阅读第14章,并以第14章作为突破口,详细记录学习心得,奠定后续学习之路,笔者相信认真拿下第14章,将基本取得本书学习之胜利,开始吧!
图13-2 本章内存布局示意图
本章主引导程序和用户程序直接使用第13章的,内核代码是对第13章的改进,增加了任务、特权级保护等内容。
主引导程序
主引导程序负责加载内核,是内核代码的加载器。
内核头部提供内核大小、各段的汇编地址及内核入口地址等信息,以便于主引导程序加载内核代码。c14_core.asm第16行到第29行提供的内核头部如下。
全部代码在文末给出,代码来自《x86汇编语言:从实模式到保护模式》及鼠侠网。
1、c13_mbr.asm代码0-55行,【前期准备工作】
(1)指定GDT线性基地址,左移4位得到逻辑段地址,余数是段内偏移。
(2)创建主引导程序段、0-4GB线性地址空间段、系统堆栈段、显示缓冲区段的描述符,存入GDT中,当前GDT算上0号描述符(不可用)共5个段描述符,占据40字节,GDT边界是39。
(3)设置cr0寄存器的PE位,进入保护模式。
(4)第55行jmp dword 0x0010:flush
,注意0x0010选择子,对应GDT表中的2号描述符段(0000_0000_00010_0_00)。清流水线并串行化处理器,跳转到0x00007C00:flush处执行。
注意段选择子存放在段选择器中,每个段选择器还配备了描述符高速缓存器,存放对应段的线性及地址、界限和属性。
2、c13_mbr.asm代码59-93行,【加载内核到0x00040000】
需要利用内核头部代码,详见c14_core.asm第16行到第29行。
(1)第138行read_hard_disk_0方法,从硬盘读一个扇区的数据到内存。
注意参数和返回值(参数:EAX=逻辑扇区号,DS:EBX=目标缓冲区地址。返回值:EBX=EBX+512 )。
(2)先读入一个扇区,根据内核头部第16行内核大小计算出还剩余多少扇区,再依次调用read_hard_disk_0方法加载剩余的内核扇区。
3、c13_mbr.asm代码95-135行,【建立内核各段描述符到GDT,把CPU交给内核】
注意第60行代码DS存放了0-4G数据段基地址0,后续一直都是。
注意第67行代码EDI存放了内核程序的起始地址,后续一直都是。
(1)第195行make_gdt_descriptor方法用于制造段描述符,注意参数和返回值。
(2)代码96-129行制造内核公共例程段、内核数据段和内核代码段描述符。
(3)第131-133行代码更新GDT表界限并重新加载到GDTR。
(4)第135行,DS:EDI+0x10取得c14_core.asm的第28-29行内容,即段选择子core_code_seg_sel(0x38)和段内偏移start,从而进入内核代码段的c14_core.asm的775行start处执行。
内核程序
本章14.1节介绍了任务和特权级等基础知识,其中第256页的特权级检查规则总结比较重要,理解后开始过内核代码。
1、c14_core.asm代码776-833行,【安装公共例程的调用门到GDT】
(1)代码776-780行,打印内核数据段message_1处的信息,提示内核加载完成。
第37行put_string公共例程是用来显示0终止的字符串并移动光标的,理解该过程需要重点参考143页流程图。
(2)代码783-809行,读取CPU品牌信息到内核数据段cpu_brand处,并打印到屏幕。
(3)代码812-833行,安装调用门。调用门其实就是类似于段描述符的一种数据结构,调用门可以定位到一个例程,如图14-9所示。
利用调用门可以实现低特权级代码调用高特权级代码的情形,具体要求需要满足260页条件,即CPL<=调用门描述符的DPL,且RPL<=调用门描述符的DPL,且CPL>=目标代码段描述符的DPL。
本代码的各个调用门其实定位的就是内核的各个公共例程,便于用户程序调用。(注意和第13章的不同,用户程序的特权级不再是0而是3,不能直接调用内核公共例程,故在此把内核各个公用例程使用调用门进行定位。)
各个公共例程的基本信息登记在符号检索表salt中,如代码行364-386所示,每个条目来说,前256字节是该公共例程的名字,紧接着是两个字的该公共例程所在段的偏移地址,最后一个字是该公共例程所在的段选择子,建立调用门后,就把该段选择子改成对应调用门的选择子.
2、c14_core.asm代码836-846行,【加载用户程序并创建任务(完成TCB和TSS)】
(1)代码836-838行,创建用户程序的任务控制块,并插入到TCB控制块链表尾部.TCB格式如264页图14-12所示.首先要为控制块分配内存,然后在该内存处创建TCB.
注意内存分配的起始地址(0x00100000)及方式,为用户程序分配内存的起始地址是代码361行处的ram_alloc,即0x00100000.每次分配内存都接着上次分配过的内存继续分配.并强制4字节对齐.详见代码233行处allocate_memory.
**注意TCB链表头的地址如代码行414行tcb_chain所示.**在TCB链表尾插入TCB如代码735行append_to_tcb_link所示,思路就是链表如果为空就直接插入,否则寻找链表最后一个TCB,插在最后那个TCB后面.
(2)代码840-843行,通过栈传递参数,调用load_relocate_program例程.
840-841行代码压入用户程序逻辑扇区号和TCB的线性地址.843行调用加载重定位用户程序的例程load_relocate_program,段内调用,自动压入EIP到栈.
第468行代码压栈操作pushad,依次把EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI 入栈,第470-471行代码压入DS和ES,注意默认压栈是双字,不够双字则高位扩零,弹出时高位零舍弃.
注意栈访问规则,第一种是SS:ESP方式,由高地址向低地址推进;第二种使用EBP基址寻址(段地址由SS提供),由低地址向高地址推进,类似于正常的数据段数据DS:[偏移]寻址.如图14-15所示.load_relocate_program例程中会使用第二种方式访问栈.
(3)代码481-484行,为LDT表分配160字节内存,登记LDT基地址和界限到TCB.为创建用户程序的各个段描述符做准备(用户程序不复杂,不会超过20个段描述符,分配160字节足够用的).
代码487-492行,加载用户程序到内核数据段的缓冲区core_buf处.
代码495-519行,计算用户程序大小,保证512字节对齐,分配内存并从硬盘加载到内存.登记用户程序加载的基地址到TCB.
代码521-576行,建立用户程序头部段,用户代码段,用户数据段和用户栈段的描述符到LDT,并把各段选择子登记到用户程序头部.登记头部段选择子到TCB.
代码579-620行,对用户程序中的salt表重定位,用户程序需要调用的各个例程的名字改成调用例程所在的段内偏移地址和对应的调用门选择子.
代码622-673行,建立0,1,2特权级堆栈并登记到TCB中.笔者迷糊了一会儿的问题是为堆栈分配的线性基地址加上4096为啥就可以作为栈的高端基地址了呢?不应该是加上4095吗?仔细想下,确实应该加上4096,比如当压入第一个字的时候4096-2.
代码676-681行,把LDT段的描述符登记到GDT,并把LDT段的选择子登记到TCB.
代码683-725行,完成TSS并登记相关信息加粗样式到TCB,并在GDT中登记TSS的描述符.
代码732行,弹出当初调用这个函数时所传递的两个参数共8字节.
3、c14_core.asm代码848-864行,【模仿从调用门返回,从内核跳到用户程序】
具体参考书278-284页.
用户程序所处扇区:50
内核扇区:1
主引导程序扇区:0
用户程序所用的数据扇区:100
在对应扇区导入如上各个代码和数据后,启动虚拟机,得到如下界面.
然而启动虚拟机发生错误,调试找到错误原因,c13.asm程序的第80行代码从用户程序返回到内核jmp far [fs:TerminateProgram]
出错,因为内核代码段描述符是显示内核代码是非依从的,参考279页表格知道jmp通过调用门是返回不了内核的.
为了不出现错误还是先把这里的jmp改成call(这样做并没有把CPU交给内核,带着这个问题留给后续章节解决吧!笔者此刻觉得可以把内核代码段描述符修改成依从的,同时处理好返回到内核return_point的异常就可以了).
附上代码
c13_mbr.asm
;代码清单13-1
;文件名:c13_mbr.asm
;文件说明:硬盘主引导扇区代码
;创建日期:2011-10-28 22:35 ;设置堆栈段和栈指针
core_base_address equ 0x00040000 ;常数,内核加载的起始内存地址
core_start_sector equ 0x00000001 ;常数,内核的起始逻辑扇区号
mov ax,cs
mov ss,ax
mov sp,0x7c00
;计算GDT所在的逻辑段地址及段内偏移
mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位物理地址
xor edx,edx
mov ebx,16
div ebx ;除以16得到的商就是16位段地址
mov ds,eax ;令DS指向该段以进行操作
mov ebx,edx ;段内起始偏移地址
;跳过0#号描述符的槽位
;创建1#描述符,这是一个 <- 0~4GB的线性地址空间数据段
mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xFFFFF 0000_0000_0000_0000_1111_1111_1111_1111
mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符 00000000_1_1_0_0_1111_1_00_1_0010_00000000
;创建保护模式下初始代码段描述符 <- 主引导程序
mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,界限0x1FF
mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符
;建立保护模式下的堆栈段描述符 <- 系统堆栈段
mov dword [ebx+0x18],0x7c00fffe ;基地址为0x00007C00,界限0xFFFFE
mov dword [ebx+0x1c],0x00cf9600 ;粒度为4KB ,详见215页实际栈范围的推导 0x00006C00~0x00007BFF(ESP初始是0)
;建立保护模式下的显示缓冲区描述符 <- 显示缓冲区
mov dword [ebx+0x20],0x80007fff ;基地址为0x000B8000,界限0x07FFF
mov dword [ebx+0x24],0x0040920b ;粒度为字节
;初始化描述符表寄存器GDTR
mov word [cs: pgdt+0x7c00],39 ;描述符表的界限
lgdt [cs: pgdt+0x7c00]
in al,0x92 ;南桥芯片内的端口
or al,0000_0010B
out 0x92,al ;打开A20
cli ;中断机制尚未工作
mov eax,cr0
or eax,1
mov cr0,eax ;设置PE位
;以下进入保护模式... ...
jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移 0000_0000_00010_0_00
;清流水线并串行化处理器
[bits 32]
flush:
mov eax,0x0008 ;加载数据段(0..4GB)选择子
mov ds,eax
mov eax,0x0018 ;加载堆栈段选择子
mov ss,eax
xor esp,esp ;堆栈指针 <- 0
;以下加载系统核心程序
mov edi,core_base_address
mov eax,core_start_sector
mov ebx,edi ;起始地址
call read_hard_disk_0 ;以下读取程序的起始部分(一个扇区)
;以下判断整个程序有多大
mov eax,[edi] ;核心程序尺寸
xor edx,edx
mov ecx,512 ;512字节每扇区
div ecx
or edx,edx
jnz @1 ;未除尽,因此结果比实际扇区数少1
dec eax ;已经读了一个扇区,扇区总数减1
@1:
or eax,eax ;考虑实际长度≤512个字节的情况
jz setup ;EAX=0 ?
;读取剩余的扇区
mov ecx,eax ;32位模式下的LOOP使用ECX
mov eax,core_start_sector
inc eax ;从下一个逻辑扇区接着读
@2:
call read_hard_disk_0
inc eax
loop @2 ;循环读,直到读完整个内核
setup:
mov esi,[0x7c00+pgdt+0x02] ;不可以在代码段内寻址pgdt,但可以
;通过4GB的段来访问
;建立公用例程段描述符
mov eax,[edi+0x04] ;公用例程代码段起始汇编地址
mov ebx,[edi+0x08] ;核心数据段汇编地址
sub ebx,eax ;公共例程段长度
dec ebx ;公用例程段界限
add eax,edi ;公用例程段基地址
mov ecx,0x00409800 ;字节粒度的代码段描述符
call make_gdt_descriptor
mov [esi+0x28],eax
mov [esi+0x2c],edx
;建立核心数据段描述符
mov eax,[edi+0x08] ;核心数据段起始汇编地址
mov ebx,[edi+0x0c] ;核心代码段汇编地址
sub ebx,eax
dec ebx ;核心数据段界限
add eax,edi ;核心数据段基地址
mov ecx,0x00409200 ;字节粒度的数据段描述符
call make_gdt_descriptor
mov [esi+0x30],eax
mov [esi+0x34],edx
;建立核心代码段描述符
mov eax,[edi+0x0c] ;核心代码段起始汇编地址
mov ebx,[edi+0x00] ;程序总长度
sub ebx,eax
dec ebx ;核心代码段界限
add eax,edi ;核心代码段基地址
mov ecx,0x00409800 ;字节粒度的代码段描述符
call make_gdt_descriptor
mov [esi+0x38],eax
mov [esi+0x3c],edx
mov word [0x7c00+pgdt],63 ;描述符表的界限
lgdt [0x7c00+pgdt]
jmp far [edi+0x10]
;-------------------------------------------------------------------------------
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
ret
;-------------------------------------------------------------------------------
make_gdt_descriptor: ;构造描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性(各属性位都在原始
; 位置,其它没用到的位置0)
;返回: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 ;装配属性
ret
;-------------------------------------------------------------------------------
pgdt dw 0
dd 0x00007e00 ;GDT的物理地址
;-------------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55,0xaa
c14_core.asm
;代码清单14-1
;文件名:c14_core.asm
;文件说明:保护模式微型核心程序
;创建日期:2011-11-6 18:37
;以下常量定义部分。内核的大部分内容都应当固定
core_code_seg_sel equ 0x38 ;内核代码段选择子
core_data_seg_sel equ 0x30 ;内核数据段选择子
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-4GB内存的段的选择子
;-------------------------------------------------------------------------------
;以下是系统核心的头部,用于加载核心程序
core_length dd core_end ;核心程序总长度#00
sys_routine_seg dd section.sys_routine.start
;系统公用例程段位置#04
core_data_seg dd section.core_data.start
;核心数据段位置#08
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
jz .exit
call put_char
inc ebx
jmp .getc
.exit:
pop ecx
retf ;段间返回
;-------------------------------------------------------------------------------
put_char: ;在当前光标处显示一个字符,并推进
;光标。仅用于段内调用
;输入:CL=字符ASCII码
pushad
;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
inc dx ;0x3d5
in al,dx ;高字
mov ah,al
dec dx ;0x3d4
mov al,0x0f
out dx,al
inc dx ;0x3d5
in al,dx ;低字
mov bx,ax ;BX=代表光标位置的16位数
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
jmp .roll_screen
.put_other: ;正常显示字符
push es
mov eax,video_ram_seg_sel ;0xb8000段的选择子
mov es,eax
shl bx,1
mov [es:bx],cl
pop es
;以下将光标位置推进一个字符
shr bx,1
inc bx
.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 ;段间返回
;-------------------------------------------------------------------------------
;汇编语言程序是极难一次成功,而且调试非常困难。这个例程可以提供帮助
put_hex_dword: ;在当前光标处以十六进制形式显示
;一个双字并推进光标
;输入:EDX=要转换并显示的数字
;输出:无
pushad
push ds
mov ax,core_data_seg_sel ;切换到核心数据段
mov ds,ax
mov ebx,bin_hex ;指向核心数据段内的转换表
mov ecx,8
.xlt:
rol edx,4
mov eax,edx
and eax,0x0000000f
xlat
push ecx
mov cl,al
call put_char
pop ecx
loop .xlt
pop ds
popad
retf
;-------------------------------------------------------------------------------
allocate_memory: ;分配内存
;输入:ECX=希望分配的字节数
;输出:ECX=起始线性地址
push ds
push eax
push ebx
mov eax,core_data_seg_sel
mov ds,eax
mov eax,[ram_alloc]
add eax,ecx ;下一次分配时的起始地址
;这里应当有检测可用内存数量的指令
mov ecx,[ram_alloc] ;返回分配的起始地址
mov ebx,eax
and ebx,0xfffffffc
add ebx,4 ;强制对齐
test eax,0x00000003 ;下次分配的起始地址最好是4字节对齐
cmovnz eax,ebx ;如果没有对齐,则强制对齐 (不为0则传送,见233页)
mov [ram_alloc],eax ;下次从该地址分配内存
;cmovcc指令可以避免控制转移
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] ;把GDTR内容加载到内核数据段的pgdt处
mov ebx,mem_0_4_gb_seg_sel
mov es,ebx
movzx ebx,word [pgdt] ;GDT界限(movzx传送,高位0扩充)
inc bx ;GDT总字节数,也是下一个描述符偏移
add ebx,[pgdt+2] ;下一个描述符的线性地址
mov [es:ebx],eax
mov [es:ebx+4],edx
add word [pgdt],8 ;增加一个描述符的大小
lgdt [pgdt] ;对GDT的更改生效
mov ax,[pgdt] ;得到GDT界限值(注意比GDT总字节数少1)
xor dx,dx
mov bx,8
div bx ;除以8,去掉余数(余数是7)
mov cx,ax ;商ax存的是索引号
shl cx,3 ;索引号左移3位,移到选择子的索引号位置,TI=0,RPL=0(内核代码段)
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
;-------------------------------------------------------------------------------
make_gate_descriptor: ;构造门的描述符(调用门等)
;输入:EAX=门代码在段内偏移地址
; BX=门代码所在段的选择子
; CX=段类型及属性等(各属
; 性位都在原始位置)
;返回:EDX:EAX=完整的描述符
push ebx
push ecx
mov edx,eax
and edx,0xffff0000 ;得到偏移地址高16位
or dx,cx ;组装属性部分到EDX
and eax,0x0000ffff ;得到偏移地址低16位
shl ebx,16
or eax,ebx ;组装段选择子部分
pop ecx
pop ebx
retf
sys_routine_end:
;===============================================================================
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_2 db ' System wide CALL-GATE mounted.',0x0d,0x0a,0
message_3 db 0x0d,0x0a,' Loading user program...',0
do_status db 'Done.',0x0d,0x0a,0
message_6 db 0x0d,0x0a,0x0d,0x0a,0x0d,0x0a
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 db 0x0d,0x0a,' ',0
cpu_brand times 52 db 0
cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,0
;任务控制块链
tcb_chain dd 0
core_data_end:
;===============================================================================
SECTION core_code vstart=0
;-------------------------------------------------------------------------------
fill_descriptor_in_ldt: ;在LDT内安装一个新的描述符
;输入:EDX:EAX=描述符
; EBX=TCB基地址
;输出:CX=描述符的选择子
push eax
push edx
push edi
push ds
mov ecx,mem_0_4_gb_seg_sel
mov ds,ecx
mov edi,[ebx+0x0c] ;获得LDT基地址
xor ecx,ecx
mov cx,[ebx+0x0a] ;获得LDT界限
inc cx ;LDT的总字节数,即新描述符偏移地址
;用CX,不可用ECX,因为初始偏移是0xFFFF,加1变成0,不要进位。
mov [edi+ecx+0x00],eax
mov [edi+ecx+0x04],edx ;安装描述符
add cx,8
dec cx ;得到新的LDT界限值
mov [ebx+0x0a],cx ;更新LDT界限值到TCB
mov ax,cx
xor dx,dx
mov cx,8
div cx ;商代表的是段选择子的索引号
mov cx,ax
shl cx,3 ;左移3位,后三位是TI和RPL,并且
or cx,0000_0000_0000_0100B ;使TI位=1,指向LDT,最后使RPL=00
pop ds
pop edi
pop edx
pop eax
ret
;-------------------------------------------------------------------------------
load_relocate_program: ;加载并重定位用户程序
;输入: PUSH 逻辑扇区号
; PUSH 任务控制块基地址
;输出:无
pushad
push ds
push es
mov ebp,esp ;为访问通过堆栈传递的参数做准备
mov ecx,mem_0_4_gb_seg_sel
mov es,ecx
mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址
;以下申请创建LDT所需要的内存
mov ecx,160 ;允许安装20个LDT描述符
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x0c],ecx ;登记LDT基地址到TCB中
mov word [es:esi+0x0a],0xffff ;登记LDT初始的界限到TCB中
;以下开始加载用户程序
mov eax,core_data_seg_sel
mov ds,eax ;切换DS到内核数据段
mov eax,[ebp+12*4] ;从堆栈中取出用户程序起始扇区号
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 ;9位都为0
test eax,0x000001ff ;程序的大小正好是512的倍数吗?
cmovnz eax,ebx ;不是。使用凑整的结果
mov ecx,eax ;实际需要申请的内存数量
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x06],ecx ;登记程序加载基地址到TCB中
mov ebx,ecx ;ebx -> 申请到的内存首地址
xor edx,edx
mov ecx,512
div ecx
mov ecx,eax ;总扇区数
mov eax,mem_0_4_gb_seg_sel ;切换DS到0-4GB的段
mov ds,eax
mov eax,[ebp+12*4] ;起始扇区号
.b1:
call sys_routine_seg_sel:read_hard_disk_0
inc eax
loop .b1 ;循环读,直到读完整个用户程序
mov edi,[es:esi+0x06] ;获得程序加载基地址
;建立程序头部段描述符
mov eax,edi ;程序头部起始线性地址
mov ebx,[edi+0x04] ;段长度
dec ebx ;段界限
mov ecx,0x0040f200 ;字节粒度的数据段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
;安装头部段描述符到LDT中
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [es:esi+0x44],cx ;登记程序头部段选择子到TCB
mov [edi+0x04],cx ;和头部内
;建立程序代码段描述符
mov eax,edi
add eax,[edi+0x14] ;代码起始线性地址
mov ebx,[edi+0x18] ;段长度
dec ebx ;段界限
mov ecx,0x0040f800 ;字节粒度的代码段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [edi+0x14],cx ;登记代码段选择子到头部
;建立程序数据段描述符
mov eax,edi
add eax,[edi+0x1c] ;数据段起始线性地址
mov ebx,[edi+0x20] ;段长度
dec ebx ;段界限
mov ecx,0x0040f200 ;字节粒度的数据段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [edi+0x1c],cx ;登记数据段选择子到头部
;建立程序堆栈段描述符
mov ecx,[edi+0x0c] ;4KB的个数
mov ebx,0x000fffff
sub ebx,ecx ;对于4KB粒度,0xFFFFF-2=段界限
mov eax,4096 ;4KB
mul ecx
mov ecx,eax ;准备为堆栈分配内存
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;得到堆栈的高端物理地址
mov ecx,0x00c0f600 ;字节粒度的堆栈段描述符,特权级3
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0011B ;设置选择子的特权级为3
mov [edi+0x08],cx ;登记堆栈段选择子到头部
;重定位SALT
mov eax,mem_0_4_gb_seg_sel ;这里和前一章不同,头部段描述符
mov es,eax ;已安装,但还没有生效,故只能通
;过4GB段访问用户程序头部
mov eax,core_data_seg_sel
mov ds,eax
cld
mov ecx,[es:edi+0x24] ;U-SALT条目数(通过访问4GB段取得)
add edi,0x28 ;U-SALT在4GB段内的偏移
.b2:
push ecx
push edi
mov ecx,salt_items
mov esi,salt
.b3:
push edi
push esi
push ecx
mov ecx,64 ;检索表中,每条目的比较次数
repe cmpsd ;若为0则重复比较,每次比较4字节(239页)
jnz .b4 ;不相同。
mov eax,[esi] ;若匹配,则esi恰好指向其后的地址
mov [es:edi-256],eax ;将字符串改写成偏移地址
mov ax,[esi+4]
or ax,0000000000000011B ;以用户程序自己的特权级使用调用门
;故RPL=3
mov [es:edi-252],ax ;回填调用门选择子
.b4:
pop ecx
pop esi
add esi,salt_item_len
pop edi ;从头比较
loop .b3
pop edi
add edi,256
pop ecx
loop .b2
mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址
;创建0特权级堆栈
mov ecx,4096
mov eax,ecx ;为生成堆栈高端地址做准备
mov [es:esi+0x1a],ecx
shr dword [es:esi+0x1a],12 ;登记0特权级堆栈尺寸到TCB
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;堆栈必须使用高端地址为基地址
mov [es:esi+0x1e],eax ;登记0特权级堆栈基地址到TCB
mov ebx,0xffffe ;段长度(界限)
mov ecx,0x00c09600 ;4KB粒度,读写,特权级0
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
;or cx,0000_0000_0000_0000 ;设置选择子的特权级为0
mov [es:esi+0x22],cx ;登记0特权级堆栈选择子到TCB
mov dword [es:esi+0x24],0 ;登记0特权级堆栈初始ESP到TCB
;创建1特权级堆栈
mov ecx,4096
mov eax,ecx ;为生成堆栈高端地址做准备
mov [es:esi+0x28],ecx
shr [es:esi+0x28],12 ;登记1特权级堆栈尺寸到TCB
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;堆栈必须使用高端地址为基地址
mov [es:esi+0x2c],eax ;登记1特权级堆栈基地址到TCB
mov ebx,0xffffe ;段长度(界限)
mov ecx,0x00c0b600 ;4KB粒度,读写,特权级1
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0001 ;设置选择子的特权级为1
mov [es:esi+0x30],cx ;登记1特权级堆栈选择子到TCB
mov dword [es:esi+0x32],0 ;登记1特权级堆栈初始ESP到TCB
;创建2特权级堆栈
mov ecx,4096
mov eax,ecx ;为生成堆栈高端地址做准备
mov [es:esi+0x36],ecx
shr [es:esi+0x36],12 ;登记2特权级堆栈尺寸到TCB
call sys_routine_seg_sel:allocate_memory
add eax,ecx ;堆栈必须使用高端地址为基地址
mov [es:esi+0x3a],ecx ;登记2特权级堆栈基地址到TCB
mov ebx,0xffffe ;段长度(界限)
mov ecx,0x00c0d600 ;4KB粒度,读写,特权级2
call sys_routine_seg_sel:make_seg_descriptor
mov ebx,esi ;TCB的基地址
call fill_descriptor_in_ldt
or cx,0000_0000_0000_0010 ;设置选择子的特权级为2
mov [es:esi+0x3e],cx ;登记2特权级堆栈选择子到TCB
mov dword [es:esi+0x40],0 ;登记2特权级堆栈初始ESP到TCB
;在GDT中登记LDT描述符
mov eax,[es:esi+0x0c] ;LDT的起始线性地址
movzx ebx,word [es:esi+0x0a] ;LDT段界限(235页)
mov ecx,0x00408200 ;LDT描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x10],cx ;登记LDT选择子到TCB中
;创建用户程序的TSS
mov ecx,104 ;tss的基本尺寸
mov [es:esi+0x12],cx
dec word [es:esi+0x12] ;登记TSS界限值到TCB
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x14],ecx ;登记TSS基地址到TCB
;登记基本的TSS表格内容
mov word [es:ecx+0],0 ;反向链=0
mov edx,[es:esi+0x24] ;登记0特权级堆栈初始ESP
mov [es:ecx+4],edx ;到TSS中
mov dx,[es:esi+0x22] ;登记0特权级堆栈段选择子
mov [es:ecx+8],dx ;到TSS中
mov edx,[es:esi+0x32] ;登记1特权级堆栈初始ESP
mov [es:ecx+12],edx ;到TSS中
mov dx,[es:esi+0x30] ;登记1特权级堆栈段选择子
mov [es:ecx+16],dx ;到TSS中
mov edx,[es:esi+0x40] ;登记2特权级堆栈初始ESP
mov [es:ecx+20],edx ;到TSS中
mov dx,[es:esi+0x3e] ;登记2特权级堆栈段选择子
mov [es:ecx+24],dx ;到TSS中
mov dx,[es:esi+0x10] ;登记任务的LDT选择子
mov [es:ecx+96],dx ;到TSS中
mov dx,[es:esi+0x12] ;登记任务的I/O位图偏移(这里登记的是TSS界限值103)
mov [es:ecx+102],dx ;表明不存在I/O映射区,见274页
mov word [es:ecx+100],0 ;T=0
;在GDT中登记TSS描述符
mov eax,[es:esi+0x14] ;TSS的起始线性地址
movzx ebx,word [es:esi+0x12] ;段长度(界限)
mov ecx,0x00408900 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [es:esi+0x18],cx ;登记TSS选择子到TCB
pop es ;恢复到调用此过程前的es段
pop ds ;恢复到调用此过程前的ds段
popad
ret 8 ;丢弃调用本过程前压入的参数(ESP=ESP+8)
;-------------------------------------------------------------------------------
append_to_tcb_link: ;在TCB链上追加任务控制块
;输入:ECX=TCB线性基地址
push eax
push edx
push ds
push es
mov eax,core_data_seg_sel ;令DS指向内核数据段
mov ds,eax
mov eax,mem_0_4_gb_seg_sel ;令ES指向0..4GB段
mov es,eax
mov dword [es: ecx+0x00],0 ;当前TCB指针域清零,以指示这是最
;后一个TCB
mov eax,[tcb_chain] ;TCB表头指针
or eax,eax ;链表为空?
jz .notcb
.searc:
mov edx,eax
mov eax,[es: edx+0x00]
or eax,eax
jnz .searc
mov [es: edx+0x00],ecx
jmp .retpc
.notcb:
mov [tcb_chain],ecx ;若为空表,直接令表头指针指向TCB
.retpc:
pop es
pop ds
pop edx
pop eax
ret
;-------------------------------------------------------------------------------
start:
mov ecx,core_data_seg_sel ;使ds指向核心数据段
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
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_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 edi,salt ;C-SALT表的起始位置
mov ecx,salt_items ;C-SALT表的条目数量
.b3:
push ecx
mov eax,[edi+256] ;该条目入口点的32位偏移地址
mov bx,[edi+260] ;该条目入口点的段选择子
mov cx,1_11_0_1100_000_00000B ;特权级3的调用门(3以上的特权级才
;允许访问),0个参数(因为用寄存器
;传递参数,而没有用栈)
call sys_routine_seg_sel:make_gate_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov [edi+260],cx ;将返回的门描述符选择子回填
add edi,salt_item_len ;指向下一个C-SALT条目
pop ecx
loop .b3
;对门进行测试
mov ebx,message_2
call far [salt_1+256] ;通过门显示信息(偏移量将被忽略)
mov ebx,message_3
call sys_routine_seg_sel:put_string ;在内核中调用例程不需要通过门
;创建任务控制块。这不是处理器的要求,而是我们自己为了方便而设立的
mov ecx,0x46 ;TCB长度
call sys_routine_seg_sel:allocate_memory
call append_to_tcb_link ;将任务控制块追加到TCB链表
push dword 50 ;用户程序位于逻辑50扇区
push ecx ;压入任务控制块起始线性地址
call load_relocate_program
mov ebx,do_status
call sys_routine_seg_sel:put_string
mov eax,mem_0_4_gb_seg_sel
mov ds,eax
ltr [ecx+0x18] ;加载任务状态段
lldt [ecx+0x10] ;加载LDT
mov eax,[ecx+0x44]
mov ds,eax ;切换到用户程序头部段
;以下假装是从调用门返回。摹仿处理器压入返回参数
push dword [0x08] ;调用前的堆栈段选择子
push dword 0 ;调用前的esp
push dword [0x14] ;调用前的代码段选择子
push dword [0x10] ;调用前的eip
retf
return_point: ;用户程序返回点
mov eax,core_data_seg_sel ;因为c14.asm是以JMP的方式使用调
mov ds,eax ;用门@TerminateProgram,回到这
;里时,特权级为3,会导致异常。
mov ebx,message_6
call sys_routine_seg_sel:put_string
hlt
core_code_end:
;-------------------------------------------------------------------------------
SECTION core_trail
;-------------------------------------------------------------------------------
core_end:
c13.asm
;代码清单13-3
;文件名:c13.asm
;文件说明:用户程序
;创建日期:2011-10-30 15:19
;===============================================================================
SECTION header vstart=0
program_length dd program_end ;程序总长度#0x00
head_len dd header_end ;程序头部的长度#0x04
stack_seg dd 0 ;用于接收堆栈段选择子#0x08
stack_len dd 1 ;程序建议的堆栈大小#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)/256 ;#0x24
salt: ;#0x28
PrintString db '@PrintString'
times 256-($-PrintString) db 0
TerminateProgram db '@TerminateProgram'
times 256-($-TerminateProgram) db 0
ReadDiskData db '@ReadDiskData'
times 256-($-ReadDiskData) db 0
header_end:
;===============================================================================
SECTION data vstart=0
buffer times 1024 db 0 ;缓冲区
message_1 db 0x0d,0x0a,0x0d,0x0a
db '**********User program is runing**********'
db 0x0d,0x0a,0
message_2 db ' Disk data:',0x0d,0x0a,0
data_end:
;===============================================================================
[bits 32]
;===============================================================================
SECTION code vstart=0
start:
mov eax,ds
mov fs,eax
mov eax,[stack_seg]
mov ss,eax
mov esp,0
mov eax,[data_seg]
mov ds,eax
mov ebx,message_1
call far [fs:PrintString]
mov eax,100 ;逻辑扇区号100
mov ebx,buffer ;缓冲区偏移地址
call far [fs:ReadDiskData] ;段间调用
mov ebx,message_2
call far [fs:PrintString]
mov ebx,buffer
call far [fs:PrintString] ;too.
call far [fs:TerminateProgram] ;将控制权返回到系统
code_end:
;===============================================================================
SECTION trail
;-------------------------------------------------------------------------------
program_end:
diskdata.txt
The Intel386 Processor (1985)
The Intel386 processor was the first 32-bit processor in the IA-32 architecture family. It introduced 32-bit registers for use both to hold operands and for addressing. The lower half of each 32-bit Intel386 register retains the properties of the 16-bit registers of earlier generations, permitting backward compatibility. The processor also provides a virtual-8086 mode that allows for even greater efficiency when executing programs created for 8086/8088 processors. [END]
参考资料
[1] 《x86汇编语言:从实模式到保护模式》李忠 著
[2] 鼠侠网