本文参考书籍
操作系统真相还原
Linux内核完全剖析:基于0.12内核
x86汇编语言 从实模式到保护模式
ps:基于x86硬件的pc系统
实模式相关介绍
实模式在上文已经做了简要的介绍,实模式的寄存器都是16位,实模式的1MB的寻址能力是通过段基址左移四位加上段内偏移实现的,由于BIOS启动的过程中就会被cpu执行,所以当bios加载完成时,1MB的内存布局如下图所示(图片来源操作系统真相还原);
通过如图可以看到,当bios加载完成后启动扇区代码后,启动代码可以通过在对应的内存位置上传入数据,就可以在屏幕上显示相应数据,此时的bios也有相应的中断向量表,可以调用,例如读写硬盘、向显示屏显示数据等都是通过该中断向量表实现。
此时可以根据相关bios的属性,构成如下主引导程序代码;
;主引导程序
;------------------------------------------------------------
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
; 清屏利用0x06号功能,上卷全部行,则可清屏
; -----------------------------------------------------------
;INT 0x10 功能号:0x06 功能号:上卷窗口
;------------------------------------------------------
;输入
;AH 功能号= 0x06
;AL = 上卷的行数(如果为0,表示全部)
;BH = 上卷行属性
;(CL,CH) = 窗口左上角的(X,Y)位置
;(DL,DH) = 窗口右下角(X,Y)位置
;无返回值
mov ax, 0x600
mov bx, 0x700
mov cx, 0 ; 左上角: (0, 0)
mov dx, 0x184f ; 右下角: (80,25),
; VGA文本模式下,一行只能容纳80个字符,共25行
; 下标从0开始,所以x18=24,0x4f=79
int 0x10 ; int 0x10
;;;;;;;;; 下面这三行代码获取光标的位置 ;;;;;;;;;
;.get_cursor 获取当前光标位置,在光标位置处打印字符
mov ah, 3 ; 输入:3号子功能获取光标位置,需要存入ah寄存器
mov bh, 0 ; bh寄存器存储的是待获取光标的页号
int 0x10 ; 输出: ch=光标开始行,cl=光标结束行
; dh=光标所在行号,dl=光标所在列号
;;;;;;;;; 获取光标位置结束 ;;;;;;;;;;;;;;;;
;;;;;;;;; 打印字符串 ;;;;;;;;;;;
;
mov ax, message
mov bp, ax ; es:bp 为串首地址,es此时同cs一致
; 开头时已经为sreg初始化
; 光标位置要用到dx寄存器中内容,cs中的光标位置可忽略
mov cx, 5 ; cx 为串长度,不包括结束符0的字符个数
mov ax, 0x1301 ; 子功能号13显示字符及属性,要存入ah寄存器
; al设置写字符方式ah=01;显示字符串,光标跟随移动
mov bx, 0x2 ; bh村粗要显示的页号,此处是第0页,
; bl中是字符属性,属性黑底绿字bl = 02h)
int 0x10 ; 执行BIOS 0x10 中断
;;;;;;;;; ´打印字符串结束 ;;;;;;;;;;;;;;;
jmp $ ; 进入死循环,使程序悬停在此
message db "1 MBR"
times 510-($-$$) db 0
db 0x55,0xaa
该段代码主要就是在bios下调用了向屏幕打印输出的功能,可查bios手册对有关内容进一步了解,最后由于mbr的固定结束时0x55aa,并且要将剩余的扇区内容填满,由此便是该段代码所做的工作。
此处的程序只是简单的在启动之后就死循环,由于该扇区的大小只有512个字节,操作系统内核一般会超过该大小,并且操作系统一般存放在软盘或者硬盘上,此时还需要将操作系统加载到内存中,此时就需要使用Loader加操作系统加载到内存中。
读取硬盘数据并加载代码如下;
;-------------------------------------------------------------------------------
;功能:读取硬盘n个扇区
rd_disk_m_16:
;-------------------------------------------------------------------------------
; eax=LBA扇区号
; ebx=将数据写入的内存地址
; ecx=读入的扇区数
mov esi,eax ;备份eax
mov di,cx ;备份cx
;读写硬盘:
;第1步:设置要读取的扇区数
mov dx,0x1f2
mov al,cl
out dx,al ;读取的扇区数
mov eax,esi ;恢复ax
;第2步:将LBA地址存入0x1f3 ~ 0x1f6
;LBA地址7~0位写入端口0x1f3
mov dx,0x1f3
out dx,al
;LBA地址15~8位写入端口0x1f4
mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al
;LBA地址23~16位写入端口0x1f5
shr eax,cl
mov dx,0x1f5
out dx,al
shr eax,cl
and al,0x0f ;lba第24~27位
or al,0xe0 ; 设置7到4位为1110,表示lba模式
mov dx,0x1f6
out dx,al
;第3步:向0x1f7端口写入读命令,¬0x20
mov dx,0x1f7
mov al,0x20
out dx,al
;第4步:检测硬盘状态
.not_ready:
;同一端口,写时表示写入命令字,读时表示读入硬盘状态
nop
in al,dx
and al,0x88 ;第4位为1表示硬盘控制器已准备好数据传输
cmp al,0x08
jnz .not_ready ;若未准备好,继续等
;第5步,从0x1f0端口读数据
mov ax, di
mov dx, 256
mul dx
mov cx, ax ; di为要读取的扇区数,一个扇区有512个字节,每次读入一个字
; 共需di*512/2次,所以di*256
mov dx, 0x1f0
.go_on_read:
in ax,dx
mov [bx],ax
add bx,2
loop .go_on_read
ret
当从硬盘读取数据到内存,只需要跳转到当前加载好的内存地址处去执行相应代码就可以了,如下;
section loader vstart=LOADER_BASE_ADDR
; 输出背景色绿色,前景色红色,并且跳动的字符串"1 MBR"
mov byte [gs:0x00],'2'
mov byte [gs:0x01],0xA4 ; A表示绿色背景闪烁,4表示前景色为红色
mov byte [gs:0x02],' '
mov byte [gs:0x03],0xA4
mov byte [gs:0x04],'L'
mov byte [gs:0x05],0xA4
mov byte [gs:0x06],'O'
mov byte [gs:0x07],0xA4
mov byte [gs:0x08],'A'
mov byte [gs:0x09],0xA4
mov byte [gs:0x0a],'D'
mov byte [gs:0x0b],0xA4
mov byte [gs:0x0c],'E'
mov byte [gs:0x0d],0xA4
mov byte [gs:0x0e],'R'
mov byte [gs:0x0f],0xA4
jmp $ ; 通过死循环使程序悬停在此
此时就会在屏幕上显示完成后就进入了死循环。接下来就是进入保护模式,然后执行相应的内核代码。
Linux内核相关实模式操作流程
Linux引导启动程序的介绍
当pc的电源打开后,80x86结构的cpu将自动进入实模式,并从地址0xFFFF0开始自动给执行代码,这个地址通常是bios中的地址。pc的bios将执行某些系统检测,并在物理地址0处开始初始化中断向量。此后,它将启动设备的第一个扇区(磁盘引导扇区,512B)读入内存绝对地址0x7c00处,并跳转到这个地方。启动设备通常是软驱或硬盘。
Linux最开始执行的就是用汇编语言编写的boot/bootsect.S汇编文件,它将由bios读入到内存绝对地址0x7c00处,当它被执行时就会把自己移动到内存绝对地址0x90000(576KB)处,并把启动设备盘中后2KB代码(boot/setup.S)读入到内存0x90200处,而内核的其他部分则被读入到从内存地址0x10000(64KB)开始处,由于当时内核模块的长度不会超过0x80000字节大小(即512KB),所以bootsect程序吧内核模块读入物理地址0x10000开始位置处时并不会覆盖从0x90000(576KB)处开始的bootsect和setup模块,后面setup程序会把内核模块移动到物理内存起始位置处,这样内核模块中代码的地址即等于实际的物理地址,便于对内核代码和数据进行操作,可由如图表示;
启动部分识别主句的某些特性以及VGA卡的类型,然后将整个系统从地址0x10000移动到0x0000处,进入保护模式并跳转到系统的余下部分,此时所有32位运行方式的设置启动被完成:IDT、GDT以及LDT被加载,分页工作也设置好了,最终调用init/main.c中的main程序。
其中bootsect.S中的部分代码如下(参考Linux内核完全剖析);
!
! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
! versions of linux
!
#include <linux/config.h>
SYSSIZE = DEF_SYSSIZE
!
! bootsect.s (C) 1991 Linus Torvalds
! modified by Drew Eckhardt
!
! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
! iself out of the way to address 0x90000, and jumps there.
!
! It then loads 'setup' directly after itself (0x90200), and the system
! at 0x10000, using BIOS interrupts.
!
! NOTE! currently system is at most 8*65536 bytes long. This should be no
! problem, even in the future. I want to keep it simple. This 512 kB
! kernel size should be enough, especially as this doesn't contain the
! buffer cache as in minix
!
! The loader has been made as simple as possible, and continuos
! read errors will result in a unbreakable loop. Reboot by hand. It
! loads pretty fast by getting whole sectors at a time whenever possible.
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0 ! original address of boot-sector
INITSEG = DEF_INITSEG ! we move boot here - out of the way
SETUPSEG = DEF_SETUPSEG ! setup starts here
SYSSEG = DEF_SYSSEG ! system loaded at 0x10000 (65536).
ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
! ROOT_DEV & SWAP_DEV are now written by "build".
ROOT_DEV = 0 ! 根文件系统设备使用与系统引导时同样的设备
SWAP_DEV = 0 ! 交换设备使用与系统引导时同样的设备
entry start ! 告知链接程序,程序从start标号开始执行
start:
mov ax,#BOOTSEG ! 设置ds段寄存器为0x7c0
mov ds,ax
mov ax,#INITSEG ! 设置es段寄存器为0x9000
mov es,ax
mov cx,#256 ! 设置移动计数值为256字
sub si,si ! 源地址 ds:si = 0x7c00:0x0000
sub di,di ! 目的地址es:di=0x9000:0x0000
rep ! 重复执行并递减cx的值,直到cx=0为止
movw ! 从内存[si]处移动cx个字到[di]处
jmpi go,INITSEG ! 段间跳转,跳转到INITSEG段,偏移为go
go: mov ax,cs ! 将ds、es和ss都置成移动后所在的段0x9000
mov dx,#0xfef4 ! arbitrary value >>512 - disk parm size
mov ds,ax
mov es,ax
push ax ! 临时保存段值(0x9000)
mov ss,ax ! put stack at 0x9ff00 - 12.
mov sp,dx
/*
* Many BIOS's default disk parameter tables will not
* recognize multi-sector reads beyond the maximum sector number
* specified in the default diskette parameter tables - this may
* mean 7 sectors in some cases.
*
* Since single sector reads are slow and out of the question,
* we must take care of this by creating new parameter tables
* (for the first disk) in RAM. We will set the maximum sector
* count to 18 - the most we will encounter on an HD 1.44.
*
* High doesn't hurt. Low does.
*
* Segments are as follows: ds=es=ss=cs - INITSEG,
* fs = 0, gs = parameter table segment
*/
push #0 ! 置段寄存器fs=0
pop fs
mov bx,#0x78 ! fs:bx is parameter table address
seg fs
lgs si,(bx) ! gs:si is source
mov di,dx ! es:di is destination
mov cx,#6 ! copy 12 bytes
cld ! 清方向标致,复制时指针递增
rep !复制12字节的软驱参数表到0x9000:0xfef4处
seg gs
movw
mov di,dx ! 将es:di指向新表,然后修改表中偏移4处的最大扇区数
movb 4(di),*18 ! patch sector count
seg fs ! 让中断向量0x1E的值指向新表
mov (bx),di
seg fs
mov 2(bx),es
pop ax !as为0x9000
mov fs,ax !设置fs=gs=0x9000
mov gs,ax
xor ah,ah ! reset FDC 复位软盘控制器,让其采用新参数
xor dl,dl !dl=0,第一个软驱
int 0x13 ! 系统调用读磁盘
! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
load_setup:
xor dx, dx ! 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 如果读取成功就跳到ok_load_setup执行
push ax ! dump error code 显示错误信息,出错码压入
call print_nl !屏幕光标回车
mov bp, sp ! ss:bp指向欲显示的字
call print_hex !显示16进制值
pop ax
xor dl, dl ! reset FDC 复位磁盘控制器 重新读
xor ah, ah
int 0x13
j load_setup !重新执行该段读磁盘程序
ok_load_setup:
! Get disk drive parameters, specifically nr of sectors/track
xor dl,dl
mov ah,#0x08 ! AH=8 is get drive parameters
int 0x13 !系统调用获取磁盘驱动器参数
xor ch,ch
seg cs
mov sectors,cx
mov ax,#INITSEG
mov es,ax !重置es值
! Print some inane message
mov ah,#0x03 ! read cursor pos 获取光标位置
xor bh,bh
int 0x10 !dh代表行,dl代表列
mov cx,#9 !显示9个字符
mov bx,#0x0007 ! page 0, attribute 7 (normal)
mov bp,#msg1 !将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 !开始在0x10000处加载system模块
mov es,ax ! segment of 0x010000
call read_it !读磁盘上system模块,es为输入参数
call kill_motor !关闭驱动器马达
call print_nl !光标回车换行
! After that we check which root-device to use. If the device is
! defined (!= 0), nothing is done and the given device is used.
! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
! on the number of sectors that the BIOS reports currently.
seg cs
mov ax,root_dev !取508、509字节处的根设备号并判断是否已被定义
or ax,ax
jne root_defined !获取每磁道扇区数,如果是15则说明是1.2,如果是18则是1.4MB
seg cs
mov bx,sectors
mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
cmp bx,#15
je root_defined
mov ax,#0x021c ! /dev/PS0 - 1.44Mb
cmp bx,#18
je root_defined
undef_root: !如果都不是则循环死机
jmp undef_root
root_defined:
seg cs
mov root_dev,ax !将检查过的设备号保存到root_dev中
! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:
jmpi 0,SETUPSEG !至此,所有程序加载完毕,我们就跳转到0x9020:000处去执行
当bootsect.S将setup.S的代码读取完成后跳转到setup.S处代码执行。setup.S代码的主要功能是利用bios中断读取机器系统数据,并将这些数据保存到0x90000开始的位置(覆盖了bootsect程序所在的位置),所取得的参数和保留的参数,会在内核中相关程序中使用,例如字符设备驱动程序等;然后setup程序将system模块从0x10000~0x8ffff(当时认为内核系统模块的长度不会大于512KB)整块下移动到内存绝对地址0x00000处,接下来加载中断描述符表寄存器idtr和全局描述符表寄存器gdtr,开启A20地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20~0x2f。最后设置cpu的控制寄存器cr0,从而进入32位保护模式运行,并跳转到位于system模块最前面部分的head.s程序继续执行。由此cpu进入到保护模式运行,保护模式相关内容将在下篇文章中进行介绍。