从开始到保护--系统开机引导
------没有一个文档能写的通俗易懂,我希望写出来。
开机引导和实模式:
两个星期加上假期吐血整理,所述为计算机的开机引导,其中包括一系列计算机内存设置等等,由于没有老师教,本人理解可能还是有点错误,希望错误的地方看的博友们能帮忙指出来,共同学习,谢谢!
目前正在看linux0.11内核源代码,参考资料极其环境配置后面会说出来。
我们的操作系统从计算机加电开始就开始运行,那么,爱思考的同学们会问计算机的运行最起码都是从内存运行的,运行的是内存的代码,那么在开机启动的时候计算机内存里面是不会有东西的,那么,到底操作系统是怎么启动的呢?
其实从计算机加电开始,操作系统首先进行一系列自检,这个自检是通过计算机加电然后发送一个信号给BIOS,BIOS进行一部分代码复制来初始化计算机(这里的代码复制的是一个固定ROM区里面的?),初始化完毕后计算机就开始自动读取磁盘,磁盘读取一个扇区,512字节,当独到某个磁盘第0磁道第一个扇区的最后两个字节为55和aa的时候计算机就自动认为其为引导程序,就会把此512字节读入内存绝对地址0x07c00处。此时,计算机的操作就完全交给程序设计者了。
那么,也就是说引导系统必须具备如下因素:
1、加载到0x7c00处
2、大小为512字节
3、最后两个字节为55和aa
满足这三个条件的程序都可以作为引导,只要将其写入磁盘的第0磁道第一扇区处即可成为引导。
说到这里,有一件比较有意思的事情,去年一学弟说自己写了个病毒,破坏磁盘,当时就想想这个是个硬件问题,前几天突然想了想,不能启动系统不就是引导坏了吗?这个是可以修的,只要将程序写入磁盘,不就OK了吗,遂要那个代码,可惜代码没了,貌似在现在的windows系统里面直接写汇编读写磁盘就是不行,保护太强了,不知道他是怎么实现的破坏磁盘。
这里说一点常识:这里说的写入磁盘的代码是二进制的代码,就是纯二进制,也就是机器码,那么在windows里面(由于目前我是在windows里面开发的,windows的批处理很好用,而Linux的GNU
Exe2bin
而如果使用nasm汇编的话直接编译成.bin文件就行了:
nasm
编译好后的com文件可以直接双击执行,而bin文件可以使用二进制查看,也可以自己写程序打开查看,都可以使用debug调试,这两种文件区别是com在内存定位为0x0100处,而bin文件不确定,由于windows的保护,也可能是一致的。
在这里不得不提到:在最初计算机自检完成后将磁盘内容读入内存的时候,dl寄存器中存的是本磁盘的磁盘号,所以可以直接保存(???)。
看了这个你可可以写个引导程序试试哦~~~
学过汇编和机组的同学知道,我们老师讲的计算机8086是20根地址线,那么我们的通用寄存器和段寄存器什么的都是16位的,关于如何寻址汇编书上讲了,我想说的是:20根地址线,寻址能力是2^20=1MB,也就是说,cpu最多只能访问1MB内存,那么现在我们的计算机有的都4GB内存该怎么办呢?
计算机加电的时候其实就是按照8086的寻址方式来寻址,只能访问1MB内存,这个能访问1MB内存的状态我们称为实模式,而把能寻址更多的内存的模式称为保护模式,先看看计算机加电后能访问的这1MB内存中内容分布(来自《操作系统引导探究》
很显然我们在实模式下有一部分内存是不可动的,我们能改变的只是自由内存区和引导程序加载区,比如你如果动了中断向量表区,那么调用BIOS中断将会出错。
关于中断向量表和显示内存曲读者可以参照齐志儒
大家都知道,实模式下的内存寻址是直接在段寄存器里面设置段地址的高16位,即绝对内存的高16位,而后后面使用偏移地址定位内存,而在32位保护模式下则不行。
32位保护模式下的寻址:
在32位保护模式下cpu是通过内存分段来寻址的。首先我们来大体看一下分段的寻址:在分段机制中,将大内存分为很多个小的单位:段,段的首地址和长度是特定保存在一个索引中的,索引的首地址是特定保存的,当cpu想访问某一块内存的时候可以指明是哪个段比如说是第N个段,然后根据段的索引的首地址和N来确定是在索引中的哪个项,根据索引中的第N(这个权且当作第N项,其实是第N+1项,后面说明)保存的信息来找到特定的内存地址:
相信读者已经明白了分段了,就是用一块固定内存存储内存的段目录,将目录首地址保存,通过选择目录的第几项来选择段。那么这个在硬件是怎么实现的呢?
大体概括如下:
在Intel的计算机中是使用一个寄存器来保存段的目录的首地址:GDTR寄存器,称为全局描述符表寄存器,而那么段的目录(实际是一张表)称为全局描述符表,英文名叫GDT,用来描述全局的内存。
事实上,与全局描述符表寄存器对应的还有个局部描述符表寄存器。这里暂时不介绍。
另外,还有个寄存器用来选择使用目录里面的哪个段,这个寄存器我们称为“段选择子”。
那么先来看看第一个寄存器:GDTR,即全局描述符表寄存器:
GDTR是一个48位(6字节)寄存器,低16位用于存储段的索引长度,即全局描述符表长度,称为表限,以1字节为一单位(后面说到其实就是全局描述符的个数)。高32位用来存储描述符表的基址。具体如下(来自《操作系统引导探究》
这里比如我们想设置全局描述符表的基址为0x10000000(记住这里是32位),描述符表共有0x7fff个字节长,那么,就设置GDTR为0x100000007fff即可。
这里,首先告诉你目录(即全局描述符表寄存器)里面的每个项,即每个全局描述符长为8字节。
再介绍段选择子:
段选择子也是一个寄存器,共16位,用来存储使用第几个段和这个段的部分属性。如下(来自《操作系统引导探究》
这里的索引值就是指的第几个段,占这个16位寄存器的高13位,比如,我想使用第一个段,那么可以设置高13位为1,也是可以说,段的选择位只有13位,共可寻址2^13=8192个段,多了就寻址不到了,前面说过,每个描述符8个字节,那么可以知道,这个目录(全局描述符表)最大可以有8192*8=64KB。
TI是个标志,TI=0时表示此选择子是个全局描述符的选择子,为1时表示这是个局部描述符的选择子,此处可以知道,全局描述符的选择子和局部描述符的选择子其实是同一个寄存器。
RPL为特权级,共有四个特权级,最高特权级为00,表示最底层的操作。
最后来说说那个目录,即全局描述符表:
全局描述符表是由一个一个的全局描述符组成的,每个描述符8字节,共64位,这64位的作用和分布如下(来自《操作系统引导探究》
这里首先来看看段限部分,段限顾名思义就是段的长度限制,也就是本描述符所描述的段最大可以有多长,从图上可以看出来,段限分为两部分,共20位,而段的长度的单位是由G标志来控制的,G=0表示长度的单位为1字节,G=1表示长度的单位为4KB。比如G=0而段限为100,那么,此段的大小为1B*100=100B,而如果G=1而段限为100,那么此段大小为4KB*100+4KB=404KB(注意,这里的段限是指向头的,需要加4KB)。
这里还可以看出来,段基址也被分为两部分,共32位基址,段的基址就是这个段在内存中的开始地址,这里需要指出的是,段的基址为此段可访问的开始地址,加上段限就是此段可访问的地址,那么如果段限为0的话此段还是可以访问开始地址的,这里不深究。
其他的位作用如下(来自《操作系统引导探究》
TYPE:
S:为
系统段描述符又称为特殊段描述符,
DPL:表示特权级,从
P:为
AVL:留给程序员随便用的
D/B:为
设置这些寄存器什么的在汇编里面都有特定的伪指令对应。
介绍完分段模式,不知道你是否已经了解了呢,如果还有疑问的话请直接留言或者发送邮件到qdtecwanli@sina.com,我们大家一起学习哦~
进入保护模式:
在由实模式到保护模式的最大的问题就是内存访问的问题了,现在内存访问问题解决了,那么怎么进入实模式呢?
很明显要设置GDTR,还有打开A20地址线,一般还要初始化8259A中断控制器,最后一步就是设置CR0控制寄存器,你可能不知道我在说什么,没关系,我一个个介绍。
进入保护模式下后,段寄存器内存的不是绝对地址,而是段选择子的值,例如,我的全局描述符表的第三项是数据段,那么我想将数据段装入DS寄存器,我只需要设置段选择子的高十三位为2(从0开始)即可,比如后三位为000,那么段选择子为0000
即可设置数据段寄存器。
GDTR不介绍了。
A20地址线:
(来自http://hi.baidu.com/yvoilee/blog/item/901476ee40a7a12badafd5e3
1981
16位段基址:16位偏移,0XFFFF:0XFFFF达到了0X10FFEF,因为8086/8088的内存不可能超过1MB,所以当时的程序超过1MB时会自动回卷至0X0FFEF。
但是到了80286地址线达到24跟。而386达到32根。芯片也达到32-bit。寻址能力达到4GB。但是为了向后兼容IBM采用了一个控制方法。使用一个开关来开启或禁止0x100000
所以,如果A20被禁止,可访问的内存只能是奇数段(2N+1)M,只有当A20被打开的时候才能访问连续的内存。
只有A20打开才能进入保护模式。
下面讨论一下如何打开A20地址线:
从理论上讲,打开A20
所以,激活A20地址线的流程为:
下面是完成打开A20
A20Enable:
后来,由于感觉使用8042控制A20运行太慢了(确实,那么长的代码,中间还要若干次的wait),所以后来又出现了所谓的Fast
读A20状态
mov
in
如果al的bit
打开A20
mov
mov
out
关闭A20
mov
mov
out
8259A中断控制器:
有关8259A可以参看齐志儒、高福祥《汇编语言程序设计》第240页,特别是端口的设置,初始化等。
CR0控制寄存器:
关于控制寄存器,其实是有4个,分别为CR0~CR3,结构如下:
其中,要想开启保护模式,需要将CR0的PE位。当真正置位时,cpu才会真正运行在保护模式下。
为了配合实践,特意使用了下面引导程序来说明(语言:nasm):
boot.asm:
org 0x7c00 ;;定位到0x7c00处
jmp start
SETUPSEG equ 0x9000 ;;将setup程序读到内存的绝对地址
DATASEG equ 0x8000 ;;一般数据的保存地址
ROOT_DEV db 0 ;;启动设备号
start:
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov [ROOT_DEV],dl ;;保存启动设备号
call load_msg ;;显示启动信息
mov ax,DATASEG
mov ds,ax
mov [0],dl ;;将设备号保存至0x8000:0x0000处
mov ax,cs
mov ds,ax
load_setup: ;;从磁盘的第二扇区读取1KB至0x9000:0x0000处
mov ax,SETUPSEG
mov es,ax
xor bx,bx
mov ax,0x0202
mov dl,[ROOT_DEV]
mov dh,0
mov cx,0x0002
int 0x13 ;;磁盘读取
jnc ok_load_setup
mov ax,0
mov dx,0
int 0x13 ;;磁盘复位
jmp load_setup
ok_load_setup:
jmp SETUPSEG:0x0000
load_msg:
call load_curser
mov ax,loadmessage
mov cx,18
mov bp,ax
mov ax,0x1301
mov bx,0x000c
int 0x10
ret
load_curser:
mov ah,0x03
xor bh,bh
int 0x10
ret
loadmessage db "Booting
db 13,10
times 510-($-$$) db 0 ;;保证512字节
dw 0xaa55 ;;设置引导标志
set.asm:
org 0x0000
jmp start
SETUPSEG equ 0x9000
DATASEG equ 0x8000
SETSEG equ 0x0400 ;;set程序读入0x9000:0x0400处
gdtaddr: ;;GDTR寄存器的值
dw 0x7fff
dw gdt
dw 0x0009
gdt:
gdtnull:
dw 0,0,0,0 ;;第一个描述符没用,但是需要存在
gdtsyscode: ;;第一个代码段
dw 0x07ff
dw 0x0000
dw 0x9a00
dw 0x00c0
gdtsysdata: ;;第一个数据段
dw 0x07ff
dw 0x0000
dw 0x9200
dw 0x00c0
idtaddr: ;;IDTR的值,暂时设置为空
dw 0x000
dw 0x000
dw 0x000
start:
call load_msg ;;显示信息
mov ax,DATASEG
mov ds,ax
mov dl,[0]
mov bx,SETSEG
mov ax,0x0208
mov dh,0
mov cx,0x0004
int 0x13 ;;读取set程序员
jnc ok_load_set
mov ax,0
mov dx,0
int 0x13 ;;磁盘复位
jmp start
ok_load_set:
;;;
;;;
;;;
call open_a20 ;;打开A20地址线
cli ;;关闭全部中断
call move_set ;;将set程序移动到0x0000:0x0000处
lgdt [gdtaddr] ;;设置GDTR的值
lidt [idtaddr] ;;设置IDTR的值
call init_8259a ;;初始化8259A
call set_cr0 ;;设置CR0的值
jmp 0x8:0x0 ;;跳到第一个代码段
load_msg:
call load_curser
mov ax,cs
mov es,ax
mov ax,loadmessage
mov cx,16
mov bp,ax
mov ax,0x1301
mov bx,0x000c
int 0x10
ret
load_curser:
mov ah,0x03
xor bh,bh
int 0x10
ret
open_a20:
mov ah,0x24
mov al,1
int 0x15
ret
move_set:
cld
mov ax,SETUPSEG
mov ds,ax
mov si,SETSEG
mov ax,0x0000
mov es,ax
xor di,di
mov cx,2048
rep movsw
ret
init_8259a:
mov al,0x11
out 0x20,al
call io_delay
out 0xa0,al
call io_delay
mov al,0x20
out 0x21,al
call io_delay
mov al,0x28
out 0xa1,al
call io_delay
mov al,0x04
out 0x21,al
call io_delay
mov al,0x02
out 0xa1,al
call io_delay
mov al,0x01
out 0x21,al
call io_delay
out 0xa1,al
call io_delay
mov al,0xff
out 0x21,al
call io_delay
out 0xa1,al
call io_delay
ret
io_delay:
mov bx,bx
mov bx,bx
mov bx,bx
ret
set_cr0:
mov eax,cr0
or eax,1
mov cr0,eax
ret
loadmessage:
db "Setup
db 13,10
times 1024-($-$$) db 0
set.asm:
[BITS
org 0x0
jmp start
start:
mov ax,0x10
mov ds,ax
xor esi,esi
mov cl,'a'
mov dl,0x04
show: ;;设置显存直接设置显示字符
mov [0xb8000+esi],cl
mov [0xb8001+esi],dl
inc
inc esi
inc esi
inc cl
cmp si,10000
je end_show
jmp show
end_show:
jmp $
编译为:
nasm
nasm
nasm
运行的时候可以使用软盘映像,你可以使用C语言创建一个1.44MB的二进制文件,里面可以直接全部写0,然后下面程序可以写入:
write.c:
#include<stdio.h>
#include<stdlib.h>
int
{
FILE
FILE
char
char
char
int
long
printf("please
scanf("%s",s);
fp=fopen(s,"rb");
if(NULL==fp)
{
printf("Have
return
}
printf("please
scanf("%s",s1);
fp1=fopen(s1,"rb+");
if(NULL==fp1)
{
printf("Have
return
}
printf("please
scanf("%ld",&pos);
fseek(fp1,pos,0);
while(!feof(fp))
{
if(mark)
{
fputc(c,fp1);
// printf("%x
}
c=fgetc(fp);
mark=1;
}
c=240;
fputc(c,fp1);
c=255;
fputc(c,fp1);
fputc(c,fp1);
fclose(fp);
fclose(fp1);
printf("write
system("pause");
return
}
程序使用bochs调试,关于bochs可以直接去百度搜,也可以直接使用虚拟机运行img文件。
编译一下:gcc
可以使用批处理来编译等。比如我的汇编源程序保存在F:\huibian\Diers里面,而write.c保存在桌面上OS
in1.txt:
boot.bin
Diers.img
0
in2.txt:
setup.bin
Diers.img
512
in3.txt:
set.bin
Diers.img
1536
我的bochs调试文件放在桌面上OS
F:
cd
nasm
nasm
nasm
xcopy
xcopy
xcopy
C:
cd
a.exe
a.exe
a.exe
xcopy
Pause
需要调试运行的时候直接双击set.bat文件,会在bochs文件夹出现Diers.img文件,使用bochs运行或者使用虚拟机运行都可。
运行效果图:
参考文献:
《操作系统引导探究》(Version
《Linux内核完全注释》修正版V1.9.5
《Linux0.11源码分析》Version0.1
《自己动手写操作系统》
《微机原理与接口技术》第2版
《汇编语言程序设计》
百度空间:http://hi.baidu.com/yvoilee/blog/item/901476ee40a7a12badafd5e3