第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
ad

本文深入探讨了《x86汇编语言:从实模式到保护模式》一书中关于保护模式的基础知识,特别是第13章至第14章的内容。从主引导程序加载内核到内核加载用户程序的过程,再到内核中任务和特权级的管理,文章详细记录了学习心得,为读者提供了宝贵的学习指南。
最低0.47元/天 解锁文章
3127

被折叠的 条评论
为什么被折叠?



