Linux内核设计的艺术-从开机加电到执行main函数之前的过程

        我们假定本书所用的计算机是基于 IA—32 系列 CPU, 安装了标准单色显示器、 标准键 盘、一个软驱、一块硬盘、16 MB 内存,在内存中开辟了 2 MB 内存作为虚拟盘,并在 BIOS 中设置软驱为启动设备。后续所有的讲解都以此为基础。

      

        目前处于实模式下,内存地址为0x00000~0xFFFFF,共1MB,20位地址线,BIOS所占地址为0xFE000~0xFFFFF,在最末尾。开机加电,CS为0xF000,IP为0xFFF0,所以程序从0xFFFF0处开始执行。BIOS程序在0x00000~0x003FF放置了中断向量表,共4*256=1023个字节,所以共有256个中断向量。在0x00400~0x004FF放置了BIOS数据区,在0x0E05B~0x0FFFE处放置了中断服务程序。如下图所示:

Linux内核设计的艺术-从开机加电到执行main函数之前的过程_第1张图片


        计算机硬件体系结构的设计与BIOS联手操作,会让CPU接收一个int 0x19的中断,CPU指向0x0E6F2,开始执行中断处理程序,将软驱0 号磁头对应盘面的 0 磁道 1 扇区的内容复制至内存0x07C00 ~0x07E00处。


       程序从0x07C00处开始执行bootsect.s

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

       BOOTSEG为0x07C0,INITSEG为0x9000,将ds:si内存地址的内容,移动到es:di内存地址处,一共移动256次,一次是一个字,最后一共移了256*2=512字节。把0x07C00 ~0x07E00移动到0x90000~0x90200


        跳转,并重新设置段寄存器

jmpi	go,INITSEG
go:	mov	ax,cs
	mov	ds,ax
	mov	es,ax
! put stack at 0x9ff00.
	mov	ss,ax
	mov	sp,#0xFF00		! arbitrary value >>512
      当时 CS 的值为 0x07C0,执行完 这个跳 转后,CS 值变为 0x9000(INITSEG),IP 的值为从 0x9000(INITSEG)到 go: mov ax, cs 这一行对应指令的偏 移。所以程序就转到执行 0x90000 这边的代码了。

      上述代码的作用是通过 ax,用 CS 的值 0x9000 来把数据段寄存器(DS)、附加段寄存器 、栈基址寄存器(SS)设置成与代码段寄存器(CS)相同的位置,并将栈顶指针 SP 指 (ES) 向偏移地址为 0xFF00 处。



       读入第二扇区开始的4个扇区

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

ok_load_setup:

       上面的int 0x19中断是机器自动产生的,现在是我们自己手动配置int 0x13参数,将软盘第二扇区开始的 4 个扇区,即 setup.s 对应的程序加载至内存的0x90200~0x90A00处。


        然后又调用int 0x13终端,软盘第六扇区开始的 约 240 个扇区(共120KB)的 system 模块加载至内存的 0x10000~0x2E000中。


      获取根设备号

	seg cs                          
	mov	ax,root_dev             !cs:root_dev地址内容付给ax
	cmp	ax,#0                   !此时并不为0,为0x306,所以跳转到root_defined
	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

.org 508               !偏移是508
root_dev:
	.word ROOT_DEV
boot_flag:
	.word 0xAA55   !引导盘最后两个字节必须是0xAA55

        

       Linux内核设计的艺术-从开机加电到执行main函数之前的过程_第2张图片


        跳转到setup.s执行

     jmpi	0,SETUPSEG
       

       setup.s首先做的第一件事情就是利用 BIOS 提供的中断服务程序从设备 上提取内核运行所需的机器系统数据,这些机器系统数据被加载到内存的0x90000 ~ 0x901FC (覆盖了0x90000~0x90200)位置。


       关中断

     cli;
       如果没有 cli,又恰好发生中断,如用户不 小心碰了一下键盘,中断就要切进来,就不得不面对实模式的中断机制已经废除、保护模式 的中断机制尚未完成的尴尬局面,结果就是系统崩溃。


      移动system模块

	mov	ax,#0x0000
	cld			! 'direction'=0, movs moves forward
do_move:
	mov	es,ax		! destination segment
	add	ax,#0x1000
	cmp	ax,#0x9000      !末尾是0x80000~0x8FFFF
	jz	end_move
	mov	ds,ax		! source segment
	sub	di,di
	sub	si,si
	mov 	cx,#0x8000      !每次共移动32764*2=64KB
	rep
	movsw                   !移动一个字
	jmp	do_move
          将ds:si内存地址的内容,移动到es:di内存地址处,一共移动32764*8次,一次是一个字,最后一共移了32764*8*2=512KB。把 0x10000 ~0x8FFFF(共512KB) 移动到 0x00000~0x7FFFF(共512KB) 。在本例中实际上就是0x10000~0x2E000移动到0x00000~0x1E000处。
         这样做能取得“一箭三雕”的效果 : 

         1)废除 BIOS 的中断向量表,等同于废除了 BIOS 提供的实模式下的中断服务程序。 

         2)收回刚刚结束使用寿命的程序所占内存空间。 

         3)让内核代码占据内存物理地址最开始的、天然的、有利的位置。


         对中断描 述符表寄存器(IDTR)和全局描述符表寄存器(GDTR)进行初始化设置

end_move:
	mov	ax,#SETUPSEG	! right, forgot this at first. didn't work :-)
	mov	ds,ax
	lidt	idt_48		! load idt with 0,0
	lgdt	gdt_48		! load gdt with whatever appropriate


      gdt位于0x90200~0x90A00的末尾

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:
	.word	0			! idt limit=0
	.word	0,0			! idt base=0L

gdt_48:
	.word	0x800		! 每个是8个字节,一共256个,所以是2048字节,一共可以有256个gdt
	.word	512+gdt,0x9	! 偏移为0x9<<16+0x200+gdt

Linux内核设计的艺术-从开机加电到执行main函数之前的过程_第3张图片

           00                      C0                          92                   00                     00                   00                         07                    FF

        Segment Limit 0x07FF            2048*4096=8Mb(因为 界限粒度为4K 字节

        Segment Base(23-0) 0x000000

        Arributes 0xC09A

        G=1 表示界限粒度为4K 字节

        D=1 表示是32位

        P=1 表示描述符对地址转换是有效的,或者说该描述符所描述的段存在,即在内存中

        DPL为00,表示内核态

        TYPE为A ,表示代码段,执行/读

        Segment Base(31-24) 0x00

       此部分请参考http://blog.csdn.net/jltxgcy/article/details/865610

       所以0x8选择子对应代码段描述符,0x10选择子对应数据段描述符,选择子的格式请参考http://blog.csdn.net/jltxgcy/article/details/8656101


       打开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

         

      设置中断,参考http://blog.csdn.net/jltxgcy/article/details/8661959

	mov	al,#0x11		!往端口20h(主片)写入ICW1
	out	#0x20,al		
	.word	0x00eb,0x00eb		
	out	#0xA0,al		!往端口A0h(从片)写入ICW1
	.word	0x00eb,0x00eb

	mov	al,#0x20		!往端口21h(主片)写入ICW2
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x28	   	!往端口A1h(从片)写入ICW2
	out	#0xA1,al
	.word	0x00eb,0x00eb

	mov	al,#0x04		!往端口21h(主片)写入ICW3
	out	#0x21,al
	.word	0x00eb,0x00eb
	mov	al,#0x02		!往端口A1h(从片)写入ICW3
	out	#0xA1,al
	.word	0x00eb,0x00eb

	mov	al,#0x01		!往端口21h(主片)写入ICW4
	out	#0x21,al
	.word	0x00eb,0x00eb		
	out	#0xA1,al		!往端口A1h(从片)写入ICW4
	.word	0x00eb,0x00eb

	mov	al,#0xFF		!遮蔽主8259所有中断,写入OCW1
	out	#0x21,al
	.word	0x00eb,0x00eb		!遮蔽从8259所有终端,写入OCW1
	out	#0xA1,al

      参考此图片:

Linux内核设计的艺术-从开机加电到执行main函数之前的过程_第4张图片


       将 CR0 寄存器第 0 位 (PE)置 1,即设定处理器工作方式为保护模式

        mov	ax,#0x0001	! protected mode (PE) bit
	lmsw	ax		! This is it!


        跳转到程序开始处,0x0,此时已经开启了保护模式,要采用保护模式下的寻址方式
    jmpi	0,8		! jmp offset 0 of segment 8 (cs)


       开始执行head.s




     设置数据段高速缓冲寄存器

startup_32:
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
    段选择子是0x10,为什么是0x10,请参考段选择子的结构。
     

     head.s自己吞噬自己,最后形成的效果图,如下:

Linux内核设计的艺术-从开机加电到执行main函数之前的过程_第5张图片

   

                     所以head占的空间是4KB*4+5KB+184B=25KB+184B



       重新设置gdt,idt

call setup_idt
	call setup_gdt
...
setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea idt,%edi          #idt表的偏移
	mov $256,%ecx         #总共256个门描述符
rp_sidt:
	movl %eax,(%edi)      #ds:edi内存中的数据
	movl %edx,4(%edi)
	addl $8,%edi          #每个门描述符是8个字节
	dec %ecx
	jne rp_sidt           #没到256,就继续 
	lidt idt_descr        #设置ldtr
	ret
...
setup_gdt:
	lgdt gdt_descr        #设置gdtr
	ret
...
.align 2
.word 0
idt_descr:
	.word 256*8-1		# idt contains 256 entries
	.long idt
.align 2
.word 0
gdt_descr:
	.word 256*8-1		# 每个描述符是8个字节,这表示里面可以存256个描述符
	.long gdt		# magic number, but it works for me :^)

	.align 8
idt:	.fill 256,8,0		# 放置在0x054b8~0x5cb8

gdt:	.quad 0x0000000000000000	/* 放置在最后0x5cb8~0x64b8
	.quad 0x00c09a0000000fff	/* 16Mb */ 原来是界限是0xFF,现在是0xFFF,所以为4096*4KB=16MB,为代码段描述符
	.quad 0x00c0920000000fff	/* 16Mb */ 为数据段描述符
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */
       设置idt稍微复杂些,请看:

       Linux内核设计的艺术-从开机加电到执行main函数之前的过程_第6张图片

        Linux内核设计的艺术-从开机加电到执行main函数之前的过程_第7张图片

      eax中0008放入Selector,5428放入Offset(15-0),edx中0000放入Offset(31-16),Attributes放入8E00,E代表386中断门。


      通过选择器重新设置描述符高速缓存寄存器

	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
      特权级为00,从gdt中取描述符。cs已经设置。

      

      检查A20是否打开

xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b
     A20 如果没打开,则计算机处于 20 位的寻址模式,超过 0xFFFFF 寻址必然“回滚” 。一个特例是 0x100000 会回滚到 0x000000, 也就是说, 地址 0x100000 处存储的值必然和地址0x000000 处存储的值完全相同,如果相同就循环。


     检测数据协处理器存在,并设置成保护模式工作状态

	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
	orl $2,%eax		# set MP
	movl %eax,%cr0
	call check_x87

      

       启动分页机制

	jmp after_page_tables
        after_page_tables代码在软盘缓冲区之后,不用担心覆盖,覆盖的是全面已经没有用的16KB(0x0000~0x5000)。

       ignore_int也在软盘缓冲区之后,所以不用担心覆盖。


       把main函数的EIP压入堆栈,执行setup_paging

after_page_tables:
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $main
	jmp setup_paging        #ret返回后执行main方法,和call类似
L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.      


.align 2
setup_paging:
        /*清零*/
	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */
	cld;rep;stosl     
              
        /*设置页目录表,分别为1007,2007,3007,4007*/
	movl $pg0+7,pg_dir		/* set present bit/user r/w */
	movl $pg1+7,pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,pg_dir+12		/*  --------- " " --------- */

        /*设置页表*/
	movl $pg3+4092,%edi
	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */
	std
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax
	jge 1b

        详细请参考http://blog.csdn.net/jltxgcy/article/details/8657809,将图中21去掉,就是我们形成的图了。


      CR3 指向页目录表,启动分页机制开关 PG 标志置位

        xorl %eax,%eax		/* pg_dir is at 0x0000 */
	movl %eax,%cr3		/* cr3 - page directory start */
	movl %cr0,%eax
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */


       返回main,因为刚才压入main的EIP

ret
       下一篇文章开始执行main函数了。



你可能感兴趣的:(Linux内核设计的艺术)