kernel/head.S

1.设置寄存器gdtr寄存器

	cld
	lgdt boot_gdt_descr - __PAGE_OFFSET  
	//ds=0x18  boot_gdt_descr=0xc033103a  
	// boot_gdt_descr-__PAGE_OFFSET=0X33103a
	//
	//0x1060001f	0x00000033  
	// gdtr=0x331060 0x33
	movl $(__BOOT_DS),%eax
	movl %eax,%ds
	movl %eax,%es
	movl %eax,%fs
	movl %eax,%gs

boot_gdt_descr是一个标签,定义在本文件中。从System.map中符号表:
c033103a D boot_gdt_descr。boot_gdt_descr - __PAGE_OFFSET就是物理地址,从这个地址可以找到boot_gdt_descr的定义。

我的疑问是:
boot_gdt_descr - __PAGE_OFFSET,就是boot_gdt_descr的入口地址,怎么对应boot_gdt_descr的入口地址?

boot_gdt_descr:
	.word __BOOT_DS+7      //0x18+7=0x001f
	.long boot_gdt_table - __PAGE_OFFSET  //00331060
	.word 0				# 32-bit align idt_desc.address//0x00 00

从System.map中符号表:c0331060 D boot_gdt_table。这样boot_gdt_descr的值如注释所示。

2. 清空BBS

/*
 * Clear BSS first so that there are no surprises...
 * No need to cld as DF is already clear from cld above...
 */
	xorl %eax,%eax
	movl $__bss_start - __PAGE_OFFSET,%edi   //0x003e3000
	movl $__bss_stop - __PAGE_OFFSET,%ecx   //c03fe0fc A __bss_stop  0x004160fc
	subl %edi,%ecx  //ecx=ecx-edi 0x330fc
	shrl $2,%ecx   //右移两个字节,相当于除以4 0xcc3f
	rep ; stosl  //清空BBS

__bss_start和__bss_stop定义在harch/i386/kernel/vmlinux.lds.S中

从System.map中符号表:0xc03e3000 A __bss_start,知道__bss_start是c03fe0fc中。

3内核临时页表

3.1产生背景

当内核被解压到线性地址0x100000后,为了继续启动内核,即启动内核的第一进程即swapper进程,内核需要建立一张临时页表供其使用。

3.2临时页表的功能

假设内核使用的段、临时页表和128K的内存范围能容纳于RAM前8MB寻址。
分页第一阶段的目标是允许在实模式和保护模式下都能很容易对8MB寻址;也
就是内,内核必须创建一个映射,把0x0000 0000到0x0007 ffff的线性地址和0xC000 0000到 0xC07f ffff的线性地址映射到0x0000 0000到0x007f ffff的物理地址。临时页表的主要功能就是完成这种映射。

举个例子:
0x0005 088C和0xC005 08CC怎么完成相同的对物理映射。

线性地址的格式

页目录表偏移 页表偏移 页内偏移
10 bit (pgdt_offset) 10 bit(paget_offset) 12 bit(page_offset)

线性地址 : 0x0050 088c (0000 0000 0101 0000 0000 1000 1000 1100)
所以:
pgdt_offset=0000 0000 01=0x1 paget_offset=01 0000 0000=0x100
page_offset=1000 1000 1100=0x88c
根据下面示意图,
因为表项 pgdt_offset=0x1,指向0x418 000那个页表,然后paget_offset=0x100,指向页表的第0x100项目,
value=0x40 0007+n0x1000=0x40 0007+0x1000x1000=0x500007
指向0x500 000的页,页内偏移量page_offset=0x88c,所以指向物理地址:
0x500 000+0x88c=0x50088c

线性地址 : 0xc050 088c (1100 0000 0101 0000 0000 1000 1000 1100)
pgdt_offset=1100 0000 01= 11 0000 0001=0x301=769
paget_offset=01 0000 0000=0x1 page_offset=1000 1000 1100=0x88c

因为表项 pgdt_offset=769,指向0x418 000那个页表,然后paget_offset=0x100,指向页表的第0x100项目,
value=0x40 0007+n0x1000=0x40 0007+0x1000x1000=0x500007
指向0x500 000的页,页内偏移量page_offset=0x88c,所以指向物理地址:
0x500 000+0x88c=0x50088c

这样就完成用户空间地址0x50088c和内核地址0xc050088c都指向了同一个地址0x50088c的映射。

说明:这就是768和769的偏移是内核的页目录项,也就是0xc00,因为只取10位,也就是0xc00向右偏移两位,得到0x300,这样得到768和769。

3.3 代码实现

   page_pde_offset = (__PAGE_OFFSET >> 20);  //

	movl $(pg0 - __PAGE_OFFSET), %edi  //0x417 000
	movl $(swapper_pg_dir - __PAGE_OFFSET), %edx  //0x3e3 000
	movl $0x007, %eax			/* 0x007 = PRESENT+RW+USER */
10:
	leal 0x007(%edi),%ecx			/* Create PDE entry */
	movl %ecx,(%edx)			/* Store identity PDE entry */
	movl %ecx,page_pde_offset(%edx)		/* Store kernel PDE entry */
	addl $4,%edx
	movl $1024, %ecx
11:
	stosl
	addl $0x1000,%eax
	loop 11b
	/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
	/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
	leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
	cmpl %ebp,%eax
	jb 10b
	movl %edi,(init_pg_tables_end - __PAGE_OFFSET)

标签10,是用来创建页目录项。
标签11,是用来创建页码项。

会进入标签10第1次,标签10会分别创建第0项和第768项,都是pg0,然后标签11里会创建pg0指向的1024个页项。然后再进入10次,然后再创建第1项和第769项,都是pg1,然后在标签11里创建pg1指向的1024个页项。

11里的循环控制是通过 loop 11b,每次ecx=ecx-1,ecx不为0则跳转到标号11处。
10的循环控制是通过cmpl %ebp,%eax是否相等.

在标签11里,
假设地址为addr,填充的值为value,填充项个数为N,第一个地址为addr1,addr1里的值为val1则会有下面公式:
addr=addr1+(N-1)*4
value=val1+(N-1)*0x1000
第一次进入标签11,addr1=ecx=0x417 000 val1=0x0000 0007,则进行了1024次后;
addr= 0x417 000+(1024-1)*4=0x417ffc
value=0x7+(1024-1)*0x1000=0x3FF007

第二次进入标签11,edi随着stosl在增加,则edi=1024*4+0x417 000=0x418 000
addr1=ecx=0x418 000 val1=0x400 007,则进行了1024次后:
addr= 0x418 000+(1024-1)*4=0x418ffc
value=0x400 007+(1024-1)*0x1000=0x7FF007

画出如下的图:
零时页表
说明:
说明:
1)PDE就是页目录项,页目录通过swapper_pg_dir: .fill 1024,4,0生成了1024项,每个项4个字节,每项填充0,第0项和第1项,第0x300项和第0x301项;分别填充了值.
2)swapper_pg_dir的第一项是0x4f2007,前20位是页表的物理地址, 12位是属性,就是0x4f200,从箭头可以看出;第二项是0x4f3007, 前20位是页表的物理地址,就是0x4f300,从箭头指向可以看出.页表pg0的第一项是0x7,指向的页框0x0,后12位是属性,7是属性.
3)edi在进行第二次进入页表后,则edi=10244+0x417 000+10244=0x419 000
下面语句:movl %edi,(init_pg_tables_end - __PAGE_OFFSET) ,就是跳过swapper_pg_dir开始页目录和两个页表(就是8M的页表)。swapper_pg_dir之前是__init_end,__init_end之前包括内核映像的text和rwdata段等等。
在init_pg_table_end的值跳过了内核的text和rwdata和临时页目录和页表。

4. 打开分页

/*
 * Enable paging
 */
	movl $swapper_pg_dir-__PAGE_OFFSET,%eax    //eax=3e3 000
	movl %eax,%cr3		/* set the page table pointer.. */   //cr3=3e3 000
	movl %cr0,%eax  //eax=  6 00000 11
	orl $0x80000000,%eax   //eax=e 00000 11
	movl %eax,%cr0		/* ..and set paging (PG) bit */ //cr0=e 00000 11
	ljmp $__BOOT_CS,$1f	/* Clear prefetch and normalize %eip */

在内存中存放了,临时页表,需要把目录的基地址放到CR3中,然后开启分页,硬件MMU单元,会把每个输入的线性地址转化为物理地址进行访问。
可以看到bochs的线性地址从10002等地址变成0xC001 0002。
CR0的第31位是控制分页。

5. 设置堆栈

1:
	/* Set up the stack pointer */
	lss stack_start,%esp //*(331000)=0xc03b 4000 esp=0xc03b 4000

6. 设置中断描述表IDT

中断描述表IDT

当通过INT指令进入一个中断服务程序时,在指令中中给出一个中断向量。CPU先根据该向量的中断向量中找到一扇门(描述项),在这种情况下一般是中断门。然后,将这个门的DPL与CPU的CPL相比,CPL必须小于或等于DPL,才能穿过这扇门,执行这个门中的中断处理函数。

中断描述表
中断描述表储存在内存中,中断描述表项的结构如上图。
这个结构64 bit(8个字节),最高16 bit(63:48)和最低16 bit(15-0) ,存储的中断相应的入口地址。
47:32= 1000 1110 P=1 DPL=0 0 D=1 110-中断门
31:16=段选择符

setup_idt:
	lea ignore_int,%edx   //把中断处理程序的地址放到edx中
	movl $(__KERNEL_CS << 16),%eax //把__KERNEL_CS放到eax的高16位
	movw %dx,%ax		/* selector = 0x0010 = cs *///把edx的低16位放到eax的低16位
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present *///edx的低16位写入0x8E00
	//这样 edx存放的中断描述项的高32位,eax存放中断描述项的低32位
	lea idt_table,%edi //把idt_table标签指向的地址放到edi中
	mov $256,%ecx //256次循环代表 初始化256个描述项
rp_sidt:
	movl %eax,(%edi)  //写入中断描述项的低32位
	movl %edx,4(%edi) //写入中断描述项的高32位
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	ret
idt_descr:
	.word IDT_ENTRIES*8-1		# idt contains 256 entries 256*8-1指定IDT的大小
	.long idt_table   //中断描述项的地址

什么时候会用到idt_descr这个变量?
就是当lidt汇编指令初始化idtr寄存器采用到这个变量。

中断响应函数:

/* This is the default interrupt "handler" :-) */
	ALIGN
ignore_int:
	cld
	pushl %eax
	pushl %ecx
	pushl %edx
	pushl %es
	pushl %ds
	movl $(__KERNEL_DS),%eax
	movl %eax,%ds
	movl %eax,%es
	pushl 16(%esp)
	pushl 24(%esp)
	pushl 32(%esp)
	pushl 40(%esp)
	pushl $int_msg
	call printk
	addl $(5*4),%esp
	popl %ds
	popl %es
	popl %edx
	popl %ecx
	popl %eax
	iret
int_msg:
	.asciz "Unknown interrupt or fault at EIP %p %p %p\n"

这个中断处理程序,是一个空的处理程序,主要做了如下工作:
1)在栈中保存一些寄存器的内容
2)调用printk()函数打印"Unknown interrupt or fault at EIP"系统消息
3)从栈中恢复寄存器的内容
4)执行iret指令以恢复被中断的程序

7. 启动参数的拷贝

启动参数是从0x90000长度为0x800。0x90000的数据如下:

0x90000-0x9001FF:是boosect的二进制,以前bootsect放在0x90000,然后覆盖应该覆盖的。
0x900200以后会存放从setup.S中存放的数据比如,e820信息,硬盘,BIOS信息等。以便后面调用使用。
0x9001FF-0x9023f:也是bzImae里面的,可能是boot参数struct linux_kernel_header结构体,该结构体里头说明了这个镜像使用的boot协议版本、实模式大小、加载标记位和需要GRUB填写的一些参数(比如:内核启动参数地址)。这些数值来自于setup.o中的头。
0x90240-0x907ff:为0

参数
0x90800起放setup.o文件内容。

/*
 * Copy bootup parameters out of the way.
 * Note: %esi still has the pointer to the real-mode data.
 */
	movl $boot_params,%edi  //edi=0xc03d8ea0 0x90000
	movl $(PARAM_SIZE/4),%ecx //ecx=0x200
	cld
	rep
	movsl
	movl boot_params+NEW_CL_POINTER,%esi
	andl %esi,%esi
	jnz 2f			# New command line protocol
	cmpw $(OLD_CL_MAGIC),OLD_CL_MAGIC_ADDR
	jne 1f
	movzwl OLD_CL_OFFSET,%esi
	addl $(OLD_CL_BASE_ADDR),%esi
2:
	movl $saved_command_line,%edi
	movl $(COMMAND_LINE_SIZE/4),%ecx
	rep
	movsl

8. 跳转start_kernel

call start_kernel

杂的

1:
checkCPUtype:

	movl $-1,X86_CPUID		#  -1 for no CPUID initially

/* check if it is 486 or 386. */
/*
 * XXX - this does a lot of unnecessary setup.  Alignment checks don't
 * apply at our cpl of 0 and the stack ought to be aligned already, and
 * we don't need to preserve eflags.
 */

	movb $3,X86		# at least 386
	pushfl			# push EFLAGS
	popl %eax		# get EFLAGS
	movl %eax,%ecx		# save original EFLAGS
	xorl $0x240000,%eax	# flip AC and ID bits in EFLAGS
	pushl %eax		# copy to EFLAGS
	popfl			# set EFLAGS
	pushfl			# get new EFLAGS
	popl %eax		# put it in eax
	xorl %ecx,%eax		# change in flags
	pushl %ecx		# restore original EFLAGS
	popfl
	testl $0x40000,%eax	# check if AC bit changed
	je is386

	movb $4,X86		# at least 486
	testl $0x200000,%eax	# check if ID bit changed
	je is486

	/* get vendor info */
	xorl %eax,%eax			# call CPUID with 0 -> return vendor ID
	cpuid
	movl %eax,X86_CPUID		# save CPUID level
	movl %ebx,X86_VENDOR_ID		# lo 4 chars
	movl %edx,X86_VENDOR_ID+4	# next 4 chars
	movl %ecx,X86_VENDOR_ID+8	# last 4 chars

	orl %eax,%eax			# do we have processor info as well?
	je is486

	movl $1,%eax		# Use the CPUID instruction to get CPU type
	cpuid
	movb %al,%cl		# save reg for future use
	andb $0x0f,%ah		# mask processor family
	movb %ah,X86
	andb $0xf0,%al		# mask model
	shrb $4,%al
	movb %al,X86_MODEL
	andb $0x0f,%cl		# mask mask revision
	movb %cl,X86_MASK
	movl %edx,X86_CAPABILITY

is486:	movl $0x50022,%ecx	# set AM, WP, NE and MP
	jmp 2f

is386:	movl $2,%ecx		# set MP
2:	movl %cr0,%eax
	andl $0x80000011,%eax	# Save PG,PE,ET
	orl %ecx,%eax
	movl %eax,%cr0

	call check_x87
	incb ready
	lgdt cpu_gdt_descr
	lidt idt_descr
	ljmp $(__KERNEL_CS),$1f
1:	movl $(__KERNEL_DS),%eax	# reload all the segment registers
	movl %eax,%ss			# after changing gdt.

	movl $(__USER_DS),%eax		# DS/ES contains default USER segment
	movl %eax,%ds
	movl %eax,%es

	xorl %eax,%eax			# Clear FS/GS and LDT
	movl %eax,%fs
	movl %eax,%gs
	lldt %ax
	cld			# gcc2 wants the direction flag cleared at all times

把中断描述符和GDT加载到相关寄存器。

ps:之前的启动过程调试都是基于bochs调试的,下面可以基于qemu调试了。

猜你喜欢

转载自blog.csdn.net/chengbeng1745/article/details/84572732
今日推荐