计算机执行的第一条指令
问题:打开电源后,计算机执行的第一句指令是什么?
对于x86的PC机,刚上电时,有一部分是固化的,基本过程如下:
- x86 PC刚开机时CPU处于实模式
- 开机时,CS=0xFFFF;IP=0x0000
- 寻址0xFFFF0(ROM BIOS映射区---固化在硬件上)
- 检查RAM、键盘、显示器、软硬磁盘
- 将磁盘0磁道0扇区(引导扇区)读入0x7c00处,也就是将操作系统引导扇区从磁盘中读入到0x7c00处
- 设置CS=0x07c0,IP=0x0000,通过寻址,后面将从引导扇区开始执行,也就是从0x7c00处开始执行。
实模式下的寻址CS:IP----->CS左移4位+IP,例如CS=0xFFFF;IP=0x0000则CS左移4位为0xFFFF0,再加上IP,为0xFFFF0,也就是ROM上的BIOS映射区。
0x7c00处存放的代码
0x7c00处存放的代码是从引导扇区读入的512个字节。
- 引导扇区是启动设备的第一个扇区
- 硬盘的第一个扇区上存放着开启后执行的第一段可控制的程序。
Linux最前面的部分是由8086汇编语言编写的bootsect.s,它将由BIOS读入到内存绝对地址0x7c00(31KB)处,当它被执行的时候会将自己移动到内存绝对地址0x90000(576KB)处,并把启动设备中后2KB字节代码(boot/setup.s)读入到内存0x90200处,而内核的其他部分system模块被读入到从内存地址0x10000(64KB)开始处,因此从机器加点开始顺序执行如图所示:
引导扇区代码:bootsect.s
引导扇区中的代码是汇编代码。C程序要经过编译,经过编译后会产生不可控制的变化,而汇编中的每一条指令最后都变成了真正的机器指令,可以对它进行完整的控制。
开始设置相关的段地址,如下:
BOOTSEG = 07c0h;// bootsect的原始地址(是段地址,以下同)
INITSEG = 9000h;// 将bootsect移到这里
SETUPSEG = 9020h;// setup程序从这里开始
SYSSEG = 1000h;// system模块加载到10000(64kB)处.
start
entry _start 告知链接程序,程序从start标号开始执行
_start:
mov ax,#BOOTSEG
mov ds,ax //这两句将ds段寄存器置为0x07c0
mov ax,#INITSEG
mov es,ax //这两句将es段寄存器置为0x9000
mov cx,#256 //设置移动计数值 = 256
sub si,si //源地址为ds:si = 0x07c0:0x0000
sub di,di //目的地址es:di = 0x9000:0x0000
rep //重复执行并递减cx的值,直到cx=0
movw //movs指令,从内存[si]处移动cx个字到[di]处。
jmpi go,INITSEG //段间跳转,标号go是段内偏移地址
jmpi指令之前的几条指令,将0x07c0:0x0000处的256个字移动到0x9000:0x0000处,也就是上面图中从1移动到2.
jmpi go,INITSEG ——> go赋值给ip,INITSEG赋值给cs,通过cs:ip将指令跳转到go处执行
go
go: mov ax,cs
mov ds,ax
mov es,ax
! put stack at 0x9ff00.
mov ss,ax
mov sp,#0xFF00 ! arbitrary value >>512
在这段代码中将es寄存器的值置为cs寄存器的值即INITSEG,0x9000,即现在es=0x9000。
load_setup
load_setup:
mov dx,#0x0000 ! drive 0, head 0
mov cx,#0x0002 ! sector 2, track 0
mov bx,#0x0200 ! address = 512, in INITSEG
mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
int 0x13 ! read it
jnc ok_load_setup ! ok - continue
mov dx,#0x0000
mov ax,#0x0000 ! reset the diskette
int 0x13
j load_setup
【小知识】在寄存器中,ah是ax的高8位,al是ax的低8位。
从上述代码中,ah是0x02(读磁盘),al=扇区数量(SETUPLEN=4),ch=0x00(柱面号),cl=0x02(开始扇区 2),dh=0x00(磁盘头),dl=0x00(驱动器号),而es:bx=内存地址。
那么int 0x13中断之前的代码表示从2号扇区开始读取4个扇区,也就正好是setup所在的扇区。
es:bx=0x9000:0x0200,所以内存地址为0x90200.
int 0x13是BIOS中断—读取磁盘的中断,关于中断的内容在下一个章节中介绍。
【总结】上述代码,利用BIOS中断INT 0x13将setup模块从磁盘的第二个扇区开始读到0x90200开始处,共读4个扇区。因此setup模块的开始处是0x90200
ok_load_setup
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
mov dl,#0x00
mov ax,#0x0800 ! AH=8 is get drive parameters
int 0x13
mov ch,#0x00
seg cs
mov sectors,cx
mov ax,#INITSEG
mov es,ax ---这两句将es的值重新设置为0x9000.
! Print some inane message
mov ah,#0x03 ! read cursor pos
xor bh,bh
int 0x10 读光标
mov cx,#24 显示字符串的字符数
mov bx,#0x0007 ! page 0, attribute 7 (normal) bh显示页面号,bl=字符属性
mov bp,#msg1 es:bp寄存器对指向要显示的字符串
mov ax,#0x1301 ! write string, move cursor
int 0x10 显示字符
! ok, we've written the message, now
! we want to load the system (at 0x10000)
mov ax,#SYSSEG
mov es,ax ! segment of 0x010000
call read_it 读入system模块,将system模块加载到0x1000开始处,es为输入参数。
call kill_motor
【INT 0x10】BIOS中断,用来读光标和显示字符到屏幕上,具体用法后续补充。
msg1
msg1表示屏幕上要显示的字符串。
sectors:
.word 0 //磁道扇区数
msg1:
.byte 13,10
.ascii "Loading system ..."
.byte 13,10,13,10
那么,我们要想修改开机时屏幕上显示的字符串,就只需要修改msg1和ok_load_setup中有关代码就可以了,相关实验后续补充。
read_it
read_it:
mov ax,es
test ax,#0x0fff
die: jne die ! es must be at 64kB boundary
xor bx,bx ! bx is starting address within segment
rp_read:
mov ax,es
cmp ax,#ENDSEG ! have we loaded all yet?
jb ok1_read
ret
boot_flag
有效引导扇区的标志,仅供BIOS中的程序加载引导扇区时识别使用,必须位于引导扇区的最后两个字节中。
boot_flag:
.word 0xAA55
把操作系统读入内存,打出logo,将setup和system读入后将控制权交给setup,从而来执行setup代码。最终跳转指令如下:
jmpi 0,SETUPSEG //cs=SETUPSEG,0x9020,ip=0x0000,所以指令跳转到0x90200处执行,也就是setup开始执行的地址。
setup.s
基本过程
setup.s是一个操作系统加载程序,主要利用RPM BIOS中断读取机器系统数据,并将数据保持在0x90000开始的位置(覆盖掉bootsect程序所在的地方),这些参数将被内核中相关程序使用。
setup程序将system模块从0x10000-0x8ffff整块向下移动到内存绝对地址0x0000处,然后加载中断描述符表寄存器idtr和全局描述符表寄存器gdtr,以此来进入32位保护模式,并跳转到位于system模块最前面部分的head.s程序继续执行。
setup将完成OS启动前的设置。
start
start:
mov ax,#INITSEG
mov ds,ax
将ds设置成INITSEG(9000),在setup程序中需要重新设置ds。
mov ah,#0x03
xor bh,bh
int 0x10
mov [0],dx 间接寻址,数据段DS=0x9000,偏移是0,对应内存的绝对地址是0x9000左移4位+0---->0x90000.也就是把dx寄存器的值保存到内存地址0x90000处。
这4句代码使用了BIOS 10号中断,截取屏幕当前光标位置,并保存在内存0x90000处。控制台初始化程序会到此处取值。
mov ah,#0x88
int 0x15
mov [2],ax
这段代码表示取扩展内存的大小值(KB),也就表示刚开机的时候需要将操作系统内存大小保存起来,那么这是为什么?
答:操作系统是管理硬件的,当然也会管理内存,要想管理内存,就需要知道内存的大小。机器之间的内存大小也不相同。通过setup的初始化让操作系统知道了内存、硬件等的信息。
利用BIOS 15号中断功能号ah = 0x88取系统所含扩展内存大小并保存到内存0x90002处(和上面0x90000来历一样)。
mov ah,#0x0f
int 0x10
mov [4],bx
mov [6],ax
这段代码用于获取显卡当前显示模式。
后面的代码会分别获取显示方式并取参数、获取第一个硬盘的信息等。
cli
mov ax,#0x0000
cld
cli表示从这里不允许中断。
do_move 移动system模块到0x00000处
mov es,ax //es=0x0000
add ax,#0x1000
cmp ax,#0x9000
jz end_move //当ax = 0x9000时移动结束
mov ds,ax //ds=0x9000
sub di,di //di=0,es:di=0x00000 目的地址
sub si,si //si=0,ds:si=0x90000 源地址
mov cx,#0x8000 //移动0x8000字(64KB字节)
rep //ds:si ---->es:di
movsw //每次移动2
jmp do_move
这段代码的目的:将处于0x10000开始处的system模块移动到0x00000位置,也就是图中4到5,将从0x10000到0x8ffff的内存数据块(512KB)整块的向内存低处移动了0x10000(64KB)的位置。【疑问:为什么要进行移动呢?】
1、从代码实现来看,每次移动2B,每轮重复0x8000次,0x8000*2=0x10000B=64KB,所以共移动8轮。 2、由此可以理解为什么bootsect.s把自己移动到了0x90000? system模块放置在0x10000处,当时假设 system 模块最大长度不会超过 0x80000 (512KB),所以从0x10000到0x8ffff都是预留给system模块的,即其末端不会超过内存地址 0x90000,所以 bootsect.s 会把自己移动到0x90000 开始的地方,并把 setup 加载到它的后面。 3、为什么Load system的时候为什么不一次性放在0x00000处? 因为0x00000处开始放的bios中断向量表。现在bios中断已经不需要了,所以可以覆盖了。
进入保护模式
mov ax,#0x0001
lmsw ax
jmpi 0,8
在这里开始,寻址方式将不再是实模式,而是保护模式。在实模式下,cs16位,ip16位通过左移4位加上ip的方法,也只有20位,相当于1M的空间,肯定是不够的,现在的内存是4G的,所以不能使用实模式寻址。
从16位切到32位模式来进行工作,也就是切换到保护模式来进行工作,也就是CPU内部对16位模式和32位模式的解释程序不一样。
1、CR0 是系统内的控制寄存器之一。控制寄存器是一些特殊的寄存器,它们可以控制CPU的一些重要特性。0位是保护允许位PE(Protedted Enable),用于启动保护模式,如果PE位置1,则保护模式启动,如果PE=0,则在实模式下运行。
2、lmsw:置处理器状态字。但是只有操作数的低4位被存入CR0,即只有PE,MP,EM和TS被改写,CR0其他位不受影响。此处把cr0的最后一位设置为1,从实模型进入保护模式。 3、为什么有保护模式? 实模式下的寻址方法只能访问1M(20bit)的内存空间,无法满足需要。后来intel有了32位处理器,寻址空间达到4G,保护模式就是32位机。
4、保护模式下的地址翻译
从图中可以看出,在保护模式下,根据cs在GDT表中的查找的值+ip来进行地址翻译的,CS也被称作选择子,里面放的是查表的索引。
4、jmpi 0, 8:ip=0,cs=8,按照保护模式,取到的段基址其实是0x0000,那么这句话就是跳转到地址为0x00000的地方开始执行,也就是system模块的开始部分。
由于jmpi 0,8会到GDT表中查找值,所以GDT表中必须有内容可以让查找,所以setup还有下面的代码,填写GDT表的表项:
gdt://表项
.word 0,0,0,0 ! dummy //0 64位
.word 0x07FF ! 8Mb - limit=2047 //8(2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L
gdt_48:
.word 0x800 ! gdt limit=2048, 256 GDT entries
.word 512+gdt,0x9 ! gdt base = 0X9xxxx
gdt中的8
8对应的表项为:
.word 0x07FF .word 0x0000 .word 0x9A00 .word 0x00C0
对应的GDT表项为:
表项中红色的对应的是段基址,那么8对应的段基址为0x00000000,加上ip=0x0,则CS:ip为0x00000.