(由于之前的blog已经关闭了,所以将此文章迁移至这里,并非转载)
上一节已经讲过了INIT-SIPI-SIPI启动序列,用于启动AP。同时在SIPI中携带了一个段地址信息。AP启动从这个段开始执行程序。本节主要讲的就是这个段的程序应该怎么写。
1981 年 8 月,IBM 公司最初推出的个人计算机 IBM PC 使用的 CPU 是 Intel 8088。在该微机中地址线只有 20 根(A0 – A19)。在当时内存 RAM 只有几百 KB 或不到 1MB 时,20 根地址线已足够用来寻址这些内存。其所能寻址的最高地址是 0xffff:0xffff,也即 0x10ffef。对于超出 0x100000(1MB)的寻址地址将默认地环绕到 0x0ffef。
当 IBM 公司于 1985 年引入 AT 机时,使用的是 Intel 80286 CPU,具有 24 根地址线,最高可寻址 16MB,并且有一个与 8088 完全兼容的实模式运行方式。然而,在寻址值超过 1MB时它却不能象 8088 那样实现地址寻址的环绕。但是当时已经有一些程序是利用这种地址环绕机制进行工作的。
为了实现完全的兼容性,IBM 公司发明了使用一个开关来开启或禁止 0x100000 地址比特位。由于在当时的 8042 键盘控制器上恰好有空闲的端口引脚(输出端口 P2,引脚 P21),于是便使用了该引脚来作为与门控制这个地址比特位。
该信号即被称为 A20。如果它为零,则比特 20 及以上地址都被清除。从而实现了兼容性。由于在机器启动时,默认条件下,A20 地址线是禁止的,所以操作系统必须使用适当的方法来开启它。
下面这段是我从互联网上抄下来的,教科书里面也就是这么说的,但是现在都64位cpu了,还有类似其他模式等,下面可能没说清楚,但是,我们需要知道的内容都已经有了。
从80386开始,cpu有三种工作方式:实模式,保护模式和虚拟8086模式。只有在刚刚启动的时候是real-mode,等到linux操作系统运行 起来以后就运行在保护模式。实模式只能访问地址在1M以下的内存称为常规内存,我们把地址在1M 以上的内存称为扩展内存。在保护模式下,全部32条地址线有效,可寻址高达4G字节的物理地址空间; 扩充的存储器分段管理机制和可选的存储器分页管理机制,不仅为存储器共享和保护提供了硬件支持,而且为实现虚拟存储器提供了硬件支持; 支持多任务,能够快速地进行任务切换和保护任务环境; 4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码和数据的安全和保密及任务的隔离; 支持虚拟8086方式,便于执行8086程序。
虚拟8086模式是运行在保护模式中的实模式,为了在32位保护模式下执行纯16位程序。它不是一个真正的CPU模式,还属于保护模式。
下面的这段,也是我引用别人所讲述的,借鉴一下,我实在是翻译GDT的东西太麻烦了。这个算是基础知识。
在Protected Mode下,一个重要的必不可少的数据结构就是GDT(Global Descriptor Table)。
为什么要有GDT?我们首先考虑一下在Real Mode下的编程模型:
在Real Mode下,我们对一个内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是一个段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。而Offset则是相对于此Segment Base Address的偏移量。Base Address+Offset就是一个内存绝对地址。由此,我们可以看出,一个段具备两个因素:Base Address和Limit(段的最大长度),而对一个内存地址的访问,则是需要指出:使用哪个段?以及相对于这个段Base Address的Offset,这个Offset应该小于此段的Limit。当然对于16-bit系统,Limit不要指定,默认为最大长度64KB,而 16-bit的Offset也永远不可能大于此Limit。我们在实际编程的时候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段积存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。
到了Protected Mode,内存的管理模式分为两种,段模式和页模式,其中页模式也是基于段模式的。也就是说,Protected Mode的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式;否则这是纯段模式。
既然是这样,我们就先不去考虑页模式。对于段模式来讲,访问一个内存地址仍然使用Segment:Offset的方式,这是很自然的。由于 Protected Mode运行在32-bit系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32-bit所能表示的任何值(Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何指),而不象Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,Protected Mode,顾名思义,又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在Protected Mode下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段积存器装入这个段描述符。但Intel为了保持向后兼容,将段积存器仍然规定为16-bit(尽管每个段积存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段积存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段积存器来直接引用64-bit的段描述符。
怎么办?解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 -bit的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长,我们随后再讨论。
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以 Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此积存器中的内容作为GDT的入口来访问GDT了。
GDT是Protected Mode所必须的数据结构,也是唯一的——不应该,也不可能有多个。另外,正象它的名字(Global Descriptor Table)所揭示的,它是全局可见的,对任何一个任务而言都是这样。
引导工作主要分为4个部分,开启A20寻址,构造GDT表,以及切换到保护模式,最后是一个长跳转跳转到32位需要执行的实际代码(解放了,我们当时做项目的时候,终于不用写汇编进入C时代了,当时我就是这么欢呼的)。下面先贴出来代码,原谅我,这个的代码插件显示不了asm。
;
;
; PROJECT: GiraffeOS
; PURPOSE: Boot code for application processors
; PROGRAMMER: BorisJineman([email protected])
; UPDATE HISTORY:
; Created 2013-3-19
;
; The Boot Code Start With 0x00080000
;
; Segment selectors
;
%define KERNEL_CS (0x8)
%define KERNEL_DS (0x10)
%define MAIN_CS (0x18)
%define MAIN_DS (0x20)
; 16 bit code
BITS 16 ;切换为16bit模式
_APstart:
cli
test_real_start:
xor ax, ax ;清空ax,ds,ss寄存器
mov ds, ax
mov ss, ax
mov ax, 08000H ;设置要访问的段为0x00080000
mov ds, ax
mov bx, 01000H ;设置段起始偏移0
mov cx, 100 ;设置循环次数100
mov ax, 00H ;写入ax从0开始
s: mov [bx], ax ;将ax目前的值,写入bx所指向的地址
inc bx ;bx指向的地址++
inc bx ;bx指向的地址++
inc ax ;要写入的值++
loop s ;循环执行
in ax, 92h ; 开启A20
or ax, 00000010b
out 92h, ax
xor ax, ax ;清空ax,ds,ss寄存器
mov ds, ax
mov ss, ax
mov ax, 08000H ;设置要访问的段为0x00080000
mov ds, ax
mov eax, 00H + APgdt – _APstart
lgdt [eax]
mov eax, cr0 ; 开启CPU的保护模式,
or eax, 1
mov cr0, eax
jmp dword KERNEL_CS:000080000H + protected_mode_begin – _APstart
 
BITS 32
protected_mode_begin:
mov ax, MAIN_DS
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 01F000000h ; 设置程序栈指针为0xDF000000
start_main:
jmp MAIN_CS:0 ; 跳转到程序入口函数0xC0000000
hlt
; GDT寄存器
APgdt:
; Limit
dw 5*8-1
; Base
dd 080000H + gdt – _APstart
; GDT表
gdt:
dw 0x0 ; Null descriptor
dw 0x0
dw 0x0
dw 0x0
dw 0xffff ; Kernel code descriptor 0x00000000~0xFFFFFFFF
dw 0x0000
dw 0x9a00
dw 0x00cf
dw 0xffff ; Kernel data descriptor 0x00000000~0xFFFFFFFF
dw 0x0000
dw 0x9200
dw 0x00cf
dw 0xffff ; Main code descriptor 0x70000000~0x7FFFFFFF
dw 0x0000
dw 0x9a00
dw 0x70c1
dw 0xffff ; Main data descriptor 0x70000000~0x7FFFFFFF
dw 0x0000
dw 0x9200
dw 0x70c1
首先进入的时候先是在内存上留点痕迹,然后开启了A20,然后就是加载GDT表,然后切换到保护模式(具体的方法可以参考Intel开发手册,这里就直接给汇编了),最后的一部就是激动人心的一跳,这里直接跳到32位模式了,然后再简单的设置下ds、es、fs、gs、ss的值(GDT项的偏移),最后设置下初始栈指针后,就执行一个jmp,跳转到C语言编译程序的入口了。上面的汇编用请nasm编译。
今天写的东西可能由于十分的冲忙,于是又是抄又是搬的,也没详细解释。等改天有空我再回头修改这篇文章。
(注,本文中的有关AP引导代码部分的GDT已经修改了,,限制是从1792M到2048M的256M范围内)
未完,待续……