linux内核源码分析-引导启动程序

  本人最近在学linux0.11的源码,顺便写成博客可以供自己记忆还有分享给别人学习,以下的内容是基于作者本人的理解写的,如有疑问可以提出,大家一起探讨学习,如写得有错误,也欢迎指正。

引导启动程序


  本文主要描述boot/目录下的三个重要汇编文件:bootsect.s、setup.s、head.s。bootsect.s和setup.s这两个文件是linux在16位的实模式下运行的程序,而head.s是在32位的保护模式下运行的汇编程序。
  当计算机通电之后,8086结构的cpu将直接进入实模式,并且从0xFFFF0地址处开始执行代码,这里是ROM-BIOS(Base Input Output System,基本输入输出系统)中的地址。BIOS完成的事情主要就是完成硬件的自检和把硬盘的0柱面、0磁头、1扇区(即引导扇区)读到内存0x7C00处,该扇区装载的程序就是bootsect.s,之后再跳转到该地址0x7C00执行bootsect.s。
  因为PC机是采用冯诺依曼结构体系,该体系就决定了计算机需要先将执行指令转载入内存中才能完成取指令和执行指令,BIOS的一个重要作用就是完成引导扇区的载入,计算机才能取第一条指令进行执行。

bootsect.s程序

  该程序的主要作用就是在装载bootsect.s转载入内存0x7C00之后,将自己也就是0x7C00处的512字节的数据复制到0x90000的位置,这里只复制512 byte是因为bootsect.s本身就只有512 byte,然后挪到0x90000是为了给后面载入system模块腾空间。之后再把setup.s程序载入到0x90200处,这个位置也是bootsect.s结束的位置。最后再把system模块读入到0x10000处,这里是默认system模块的大小是不会超过512KB的,所以位于0x90000的bootsect还有setup两个程序是不会被覆盖的。载入system模块也是为什么要把bootsect挪到0x90000处,而不直接在0x7c00执行了,因为在0x7c00处执行的话,不能保证bootsect不会被system所覆盖。这样子就完成bootsect.s的使命了,之后会把控制权交给setup.s,执行setup的代码。
  bootsect.s和setup.s是在实模式下运行的16位代码程序,使用的是近似于Intel的汇编语言语法并且需要使用Intel 8086汇编编译器as86和连接器ld86。语法格式为 instruct dest source,例如mov ax,#BOOTSEG,就是把ax寄存器的值赋值为BOOTSEG,add ax,bx就是ax=ax+bx。
  实模式下使用的寻址模式也是比较简单的,就是段寄存器的值右移四位再加上偏移值,例如取指令时使用的 cs:ip = cs<<4+ip。

!定义全局代码段、数据段、bss段等等,这里的代码段、数据段和bss段就定义在同一个位置了
.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

SETUPLEN = 4                            ! setup程序的扇区数为4块
BOOTSEG  = 0x07c0                       ! bootsect程序的起始地址
INITSEG  = 0x9000                       ! 复制的目标地址
SETUPSEG = 0x9020                       ! setup的起始地址
SYSSEG   = 0x1000                       ! system模块的起始地址
ENDSEG   = SYSSEG + SYSSIZE             ! where to stop loading

! ROOT_DEV:     0x000 - same type of floppy as boot.
!               0x301 - first partition on first drive etc

ROOT_DEV = 0x306

entry _start	!程序的入口
_start:
        mov     ax,#BOOTSEG
        mov     ds,ax
        mov     ax,#INITSEG
        mov     es,ax
        mov     cx,#256
        sub     si,si
        sub     di,di
        rep
        movw
!想要把bootsect从0x7C00复制到0x90000中,linux0.11每个扇区为512byte,
!而且现在为16位的实模式,现在一个字2个字节。rep movw就是把DS:si处的内容移
!到ES:di处,大小为cx=256字,也就是512byte。
        jmpi    go,INITSEG	!jmpi指令的意思为cs=INITSEG,ip=go,也就是跳转到0x90000处
        					!执行bootsect接下来的代码
go:     mov     ax,cs	!让es拓展段和ds数据段都指向0x90000
        mov     ds,ax
        mov     es,ax
! put stack at 0x9ff00.
        mov     ss,ax	!初始化栈,栈指针为ss:sp,将堆栈指针指向到0x9ff00
        mov     sp,#0xFF00              ! arbitrary value >>512

  这里把临时栈顶设置到了0x9ff00处,当然栈是向低地址增长的,那这个栈最多能增长多大呢,bootsect是512byte,setup是2KB,所以setup的结束位置为0x90a00,即可以从0x9ff00最多增长到0x90a00,这个长度是完全够用的,不用担心栈会不会把程序覆盖的问题,因为这只是一个临时设置的栈,到后面会重新设置的。

! load the setup-sectors directly after the bootblock.
! Note that 'es' is already set up.
! 这是加载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
        								! ah=0x2表示读磁盘扇区到内存中
        int     0x13                    ! 调用0x13中断,读磁盘
        jnc     ok_load_setup           ! 读取成功则跳转到ok_load_setup接着执行
        mov     dx,#0x0000
        mov     ax,#0x0000              ! reset the diskette
        int     0x13
        j       load_setup				!失败则重新跳转到load_setup,重新读取setup
        
ok_load_setup:

! Get disk drive parameters, specifically nr of sectors/track
! 读取PC的各个参数
        mov     dl,#0x00
        mov     ax,#0x0800              ! AH=8 is get drive parameters
        int     0x13					! ah=8表示取磁盘驱动器的参数,可以与读读磁盘区分开
        mov     ch,#0x00
        seg cs
! 下面保存每磁道扇区数
        mov     sectors,cx
        mov     ax,#INITSEG
        mov     es,ax

! Print some inane message
! 显示Load system ...
        mov     ah,#0x03                ! read cursor pos
        xor     bh,bh					! 读取光标位置,位置返回到dx中,dh=行,dl=列,供显示字符串使用
        int     0x10

        mov     cx,#24					! 一共显示24个字符
        mov     bx,#0x0007              ! page 0, attribute 7 (normal)
        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),想将system模块读到0x10000处

		mov	ax,#SYSSEG	! SYSSEG=0X1000
		mov	es,ax		! segment of 0x010000
		call	read_it	! 读磁盘上的system模块,es为输入参数,表示要写入的首地址
		call	kill_motor	! 关闭驱动器

! 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
		cmp	ax,#0
		jne	root_defined
		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

! after that (everyting loaded), we jump to
! the setup-routine loaded directly after
! the bootblock:

		jmpi	0,SETUPSEG	! 间接跳转到0x90200处执行SETUP代码,cs=SETUPSEG=0x9020,ip=0! cs:ip=cs<<4+ip=0x90200,至此bootsect已经完成它的历史使命了,可以退出舞台了。
...
!将system模块读到0x10000处的函数
...
sectors:
        .word 0

msg1:
        .byte 13,10
        .ascii "Loading system ..."
        .byte 13,10,13,10

.org 508		! .org表示写入到指定的字节为止,即在508byte处写入以下内容
root_dev:
        .word ROOT_DEV
! 0x55AA是有效引导扇区的标志,必须位于引导扇区的最后两个字节处,仅供BIOS加载引导程序时识别使用
boot_flag:
        .word 0xAA55	

.text
endtext:
.data
enddata:
.bss
endbss:

  上面是bootsect的部分代码,现在16位实模式,其寻址方式为CS:ip=(CS<<4)+ip,这可以确定20位的逻辑地址。上面我只写了读取setup.s的汇编,读取system模块的也类似,但需要多读入几个参数,而且要比读setup复杂得多,感兴趣的读者可以自己去看源码,这里就不多加赘述,上面该程序执行完 jmpi 0,SETUPSEG 就会跳转到setup执行。

setup.s程序

  setup是一个操作系统加载程序,它主要是利用中断读取计算机的各个参数,并将这些参数写入0x90000处(虽然会覆盖之前读取的bootsect程序,但我们之后是不需要用到它的,覆盖了也没关系),接着把bootsect载入的system模块(载入到0x10000-0x8FFFF处)搬运到以0x00000开始处,即0x0000-0x7FFFF。之后进入32位保护模式,并跳转到0x0000处执行system模块的第一行代码——head.s程序。
  这里就又有同学要问了:既然setup要把system从0x10000处移动到0x0000处,那为什么不一开始就直接在bootsect中把system读到0x0000处,这会降低代码的效率的。
  因为开机时ROM-BIOS的初始中断向量表等等的系统模块是放在低地址处(0x0000)的,如果在bootsect就把system模块写到这,会覆盖了中断向量表等等这些很重要的系统模块。

INITSEG  = 0x9000	! we move boot here - out of the way
SYSSEG   = 0x1000	! system loaded at 0x10000 (65536).
SETUPSEG = 0x9020	! this is the current segment

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text

entry start
start:

! ok, the read went well so we get current cursor position and save it for
! posterity.
! 读取光标位置并写入到0x90000
	mov	ax,#INITSEG	! this is done in bootsect already, but...
	mov	ds,ax
	mov	ah,#0x03	! read cursor pos
	xor	bh,bh		
	int	0x10		! save it in known place, con_init fetches
	mov	[0],dx		! it from 0x90000.

! Get memory size (extended mem, kB)
! ah=0x88作为参数的int 0x15是获得物理内存的大小,并写入0x90002,这将会在main函数中用到
	mov	ah,#0x88
	int	0x15
	mov	[2],ax		! 间接寻址,默认使用段寄存器ds=0x9000,拓展内存大小?

! Get video-card data:
! 读取显卡当前显示模式,0x90004存放当前页,0x90006存放显示模式,0x90007存放字符列数
	mov	ah,#0x0f
	int	0x10
	mov	[4],bx		! bh = display page
	mov	[6],ax		! al = video mode, ah = window width

! check for EGA/VGA and some config parameters
! 检查显示方式并获取参数
	mov	ah,#0x12
	mov	bl,#0x10
	int	0x10
	mov	[8],ax
	mov	[10],bx
	mov	[12],cx

! Get hd0 data
! 取第一块硬盘信息
	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x41]
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0080
	mov	cx,#0x10
	rep
	movsb

! Get hd1 data

	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x46]
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0090
	mov	cx,#0x10
	rep
	movsb

! Check that there IS a hd1 :-)

	mov	ax,#0x01500
	mov	dl,#0x81
	int	0x13
	jc	no_disk1
	cmp	ah,#3
	je	is_disk1
no_disk1:
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0090
	mov	cx,#0x10
	mov	ax,#0x00
	rep
	stosb
is_disk1:

! now we want to move to protected mode ...

	cli			! no interrupts allowed ! 关中断
	

  上面的代码是比较无聊的,也相对来说不是很重要,可以选择适当跳过。下面开始挪动system模块和加载idt中断描述符表和gdt全局描述符表。idtr和gdtr是分别存储idt表和gdt表的48位寄存器,前两字节表示表的长度,后四字节表示表的起始基地址。当然在GDT中还存放着各个进程的LDT描述符表和TSS表。

! 下面是一个循环,ax从0增长到0x80000,每次增长0x10000,循环分别把
! ax+0x1000:0x0处代码copy到ax:0x0处。  
! first we move the system to it’s rightful place
		mov	ax,#0x0000
		cld			! 'direction'=0, movs moves forward          
do_move:
        mov     es,ax           ! es:di的目的地址为ax:0x0,初始为0x0
        add     ax,#0x1000
        cmp     ax,#0x9000
        jz      end_move
        mov     ds,ax           ! ds:si的源地址为ax+0x1000:0x0,初始地址为0x10000
        sub     di,di
        sub     si,si
        mov     cx,#0x8000		! 每次移动0x8000个字,也就是0x10000byte=64KB
        rep
        movsw
        jmp     do_move

! then we load the segment descriptors
end_move:
        mov     ax,#SETUPSEG    ! DS指向本程序段(setup)
        mov     ds,ax
        lidt    idt_48          ! 加载IDT寄存器,将idt_48标记处的后6个字节内容读入到idtr中
        lgdt    gdt_48          ! 加载LDT寄存器,将gdt_48标记处的后6个字节内容读入到gdtr中
...
一段开启A20地址线的内容
...
! Well, now‘s the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.
! 接下来要设置并进入32位保护模式,并跳转到0x0的system执行head的程序
        mov     ax,#0x0001      ! 保护模式比特位(PE)
        lmsw    ax              ! 加载机器状态字,修改cr0基础器,开启保护模式
        jmpi    0,8             ! 跳转执行head
...
! gdt每8个字节为一个段描述符,寻址时使用字节为单位,低三位表示权限
gdt:
	.word	0,0,0,0		! dummy

	.word	0x07FF		! 8Mb - limit=2047 (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也就是前面lidt指令指向的标记处,段长度为0,基地址也为0,要开启保护模式一定需要idt表,
! 这里将简单的先设置一个空的idt表
idt_48:
	.word	0			! idt limit=0
	.word	0,0			! idt base=0L
! gdt_48表示gdt表长度为2048,其中包含了256个GDT描述符,每个描述符都为8字节,基地址指向gdt标记处
gdt_48:
	.word	0x800		! gdt limit=2048, 256 GDT entries
	.word	512+gdt,0x9	! gdt base = 0X9xxxx

  要设置并加载进入保护模式,首先要加载机器状态字,也称为控制寄存器CR0,其最低位为0表示实模式,为1表示保护模式。
  这里发生了一个有趣的事情,最终是使用 jmpi 0,8 跳转到head,按照前面的跳转习性8是段地址0是偏移地址,那这得到的地址为0x8:0x0=0x10,这显然不是我们要的0x0的地址,如果跳到0x10处的话,这电脑也会死机了。因为我们这已经切换到了保护模式,保护模式的寻址方式和实模式是不一样的。保护模式的寻找方式8是段表索引也称段选择子,这是需要到GDT系统的段头表中寻找其对应的真实的段地址的,之后再做偏移,0x8=1000,低三位表示权限TI和RPL,之后的0x1表示描述符索引,也就是GDT中的第一个描述符。
在这里插入图片描述

  下图是一张对数据描述符和代码描述符格式的说明表,之所以设计的这么难看,主要也因为这不是为了给人看的,每次读取描述符都是使用硬件直接读取的,这么设计就是因为硬件读取得更快。回去代码中看gdt表就能查得到第一个描述符内容为 0x00C0 0x9A00 0x0000 0x07FF,解读一下就是段限长度8MB,基地址为0x0。
段描述符格式

  为了能够访问和使用1MB以上的地址,我们需要开启A20地址线,这一段很无聊,也很难,建议跳过。

! that was painless, now we enable A20
        call    empty_8042
        mov     al,#0xD1                ! command write
        out     #0x64,al
        call    empty_8042
        mov     al,#0xDF                ! A20 on
        out     #0x60,al
        call    empty_8042
! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.

	mov	al,#0x11		! initialization sequence
	out	#0x20,al		! send it to 8259A-1
	.word	0x00eb,0x00eb		! jmp $+2, jmp $+2
	out	#0xA0,al		! and to 8259A-2
	.word	0x00eb,0x00eb
	mov	al,#0x20		! start of hardware int's (0x20)
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x28		! start of hardware int's 2 (0x28)
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x04		! 8259-1 is master
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x02		! 8259-2 is slave
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0x01		! 8086 mode for both
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al
	.word	0x00eb,0x00eb
	mov	al,#0xFF		! mask off all interrupts for now
	out	#0x21,al
	.word	0x00eb,0x00eb
	out	#0xA1,al

! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
...
! This routine checks that the keyboard command queue is empty
! No timeout is used - if this hangs there is something wrong with
! the machine, and we probably couldn't proceed anyway.
empty_8042:
	.word	0x00eb,0x00eb
	in	al,#0x64	! 8042 status port
	test	al,#2		! is input buffer full?
	jnz	empty_8042	! yes - loop
	ret

  执行完setup之后,各个段在内存中的分布大概如下图所示。
在这里插入图片描述

  以上是我最近学习linux0.11源码的理解,参考至赵炯老师的《linux内核完全注释 3.0》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值