linux内核源码分析-head.s

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

head.s程序

  head.s程序会被编译生成目标文件之后与其他内核程序一起被链接成system模块,而该程序是system模块最开始的部分,因而也被称为head。
  在setup结尾处,我们已经完成了从实模式到保护模式的转换。并且head使用的汇编语法的AT&T的语法格式,与前面的bootsect和setup是不一样的,该汇编语法的赋值是从左向右赋值的。
  head程序是位于0x0000地址处的,它首先加载各个数据段寄存器,重新设置中断描述符表IDT,之后再重新设置全局描述符表GDT,并且会开启分页模式,并把页目录和四个页表写到0x0000地址处,会覆盖掉head程序。

.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir:				#页目录写入的地址
startup_32:
	movl $0x10,%eax		#%0x10表示0x10本身,直接写0x10表示0x10内存地址存放的数据
	mov %ax,%ds		#这里是在重新初始化各个段寄存器,在保护模式下,0x10表示GDT中的第二个描述符
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss _stack_start,%esp	#表示ss:sp赋值为_stack_start,初始化堆栈
	call setup_idt			#之前设了一个空的idt表,现在重新设置idt
	call setup_gdt			#重新设置gdt表
	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	#初始化堆栈
	xorl %eax,%eax
/*  检查A20地址线是否开启,如果未开启,就只能写入低于1MB地址的内容,如果写入高于1MB地址的内容,
	就会直接忽略高于20位的地址,只取低20位作为地址,相当于mod1MB,例如在地址1MB处写入,则会
	写入到0x0000处。
	下面就通过不断的往地址1MB处写入不同的数字,并检测于0x0处数据是否相同,如果相同就不断尝试
	写入新的数字,直到不相同,才会向下执行,确保已经开启A20地址线。 */
1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b
/*
 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
 * mode. Then it would be unnecessary with the "verify_area()"-calls.
 * 486 users probably want to set the NE (#5) bit also, so as to use
 * int 16 for math errors.
 */
	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,ET,PE
/* "orl $0x10020,%eax" here for 486 might be good */
	orl $2,%eax		# set MP,开启MP位
	movl %eax,%cr0
	call check_x87			# 检测是否有数学协处理器芯片的函数
	jmp after_page_tables	# 设置返回地址并且开启和设置页表的函数

  刚刚上面使用到的lss _stack_start,%esp是将结构体stact_start 的值传送到 ss:esp ,而stack_start定义在/kernel/sched.c文件中,定义如下

#define PAGE_SIZE 4096
long user_stack [ PAGE_SIZE>>2 ] ;

struct {
	long * a;
	short b;
	} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };

  stact_start 的长指针为user_stack 数组的结尾处,还有一个short类型的数据为0x10,lss _stack_start,%esp 即令ss=0x10(段选择子)和esp=& user_stack [PAGE_SIZE>>2],让user_stack 数组作为栈,从后往前增长。

  下面是setup_idt函数中的代码,这是一段设置中断门描述符的代码。每个中断描述符都由32位组成,与GDT和LDT类似,中断描述符的格式如下图所示。其中P表示段存在标志,DPL的描述符的优先级,需要与CPL和RPL配合使用。在head程序中,IDT的段选择子被设置成0x0008,偏移值被指向了ignore_int中断处理程序在head中的偏移值,表示中断之后会运行ignore_int程序。
  中断门包含中断处理程序所在的段选择子和段内偏移地址,当通过此方式进入中断后,标志寄存器eflags中的IF位自动置0,表示把中断关闭,避免中断嵌套。中断门只存在于中断描述符表IDT。
在这里插入图片描述

/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It then loads
 *  idt. Everything that wants to install itself
 *  in the idt-table may do so themselves. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok. This routine will be over-
 *  written by the page tables.
 */
setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */
						/* %edx存储IDT描述符的高32位,%eax存储低32位 */
	lea _idt,%edi		# _idt是中断描述符表的地址
	mov $256,%ecx		# 写入256个描述符
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)	# 每个描述符都是高位写%edx,低位写%eax
	addl $8,%edi		# 每次写完加8字节,因为每个描述符都是8字节
	dec %ecx			# 使用%ecx来计数
	jne rp_sidt
	lidt idt_descr		# 重新加载idt表地址到idtr中
	ret
	
...

/* This is the default interrupt "handler" :-) */
/* 下面是默认的中断“向量句柄”,这是一个只报错的哑终端子程序 */
int_msg:
	.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg
	call _printk
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret				# 中断返回
	
...

.align 2
.word 0
idt_descr:
	.word 256*8-1		# idt contains 256 entries
	.long _idt			# idtr高16位表示表的长度,低32位表示idt的入口地址
	
	.align 3
_idt:	.fill 256,8,0		# idt is uninitialized,256项,每项8个字节,填0

  在设置好中断描述符表之后,head又需要重新设置GDT全局描述符表。但是该GDT的段描述符除了表的长度之外(原8MB,现16MB),其余内容与先前在setup中设置的GDT表中描述符的内容是完全一样的,只是把GDT表位置从先前的0x902XX地址处搬运到了更适合的内核中。
  当然在GDT表中第0项是不可用的表项,通常使用一个空描述符写入。之后就是两个有效的描述符,分别是段限长度为16MB的指向0x0的代码段描述符和数据段描述符,之后就填入253项空描述符,为之后每个进程的LDT和TSS段先申请空间。

/*
 *  setup_gdt
 *
 *  This routines sets up a new gdt and loads it.
 *  Only two entries are currently built, the same
 *  ones that were built in init.s. The routine
 *  is VERY complicated at two whole lines, so this
 *  rather long comment is certainly needed :-).
 *  This routine will beoverwritten by the page tables.
 */
setup_gdt:
	lgdt gdt_descr		# 将gdtr设置成gdt_descr地址处的6byte内容
	ret

...

.align 2
.word 0
gdt_descr:
	.word 256*8-1		# so does gdt (not that that's any
	.long _gdt		# magic number, but it works for me :^)

_gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don’t use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */

  设置好IDT、GDT之后,我们需要设置一个虚假的调用栈,来为main函数返回设置一个地址。
  众所周知, C语言和汇编是通过栈来传递参数和存储返回地址eip的,栈中参数如下图所示,从右往左压入参数,最后压入eip(已经自动加一)作为返回地址。之后在函数中就可以使用esp+n来获得第n个参数的地址。这也就是为什么函数中改变形参不改变变量的值了,因为改变变量只会改变栈中参数的值,不会改变原变量。
函数调用栈

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.  
					# 压入的L6为main函数的返回地址
	pushl $_main	# 压入main地址,为setup_paging的返回地址,方便在setup_paging函数中直接返回到main中执行
	jmp setup_paging	# 注意这里是使用jmp调用而不是call,call会自动压eip入栈
L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.
				# 从main函数返回之后,就在这执行死循环,因为正常情况下是不会从main函数返回

  之后在setup_paging函数中就要开启分页模式,将页目录放在0x0绝对地址处,这也是head程序的地址,会覆盖head,再将四个页表依次放在页目录后面(每个页都是4KB),并设置它们的页表项,完成页表中0-16MB与绝对地址0-16MB之间的恒等映射(每个页有1K个页表项,4K*4KB=16MB)。
  下图是页目录表项与页表项结构的说明,每次页表只需要使用12-31位高位就可以映射到每个页贞的首地址(每4KB一个页贞,意味着低12位为0),低12位就可以用来表示权限,每个页表项权限被设置成0x7表示该页存在、用户可读写。
页目录表项与页表项结构的说明
  P:页面存在于内存标志
  R/W:读写标志
  U/S:用户/超级用户标志
  A:页面已访问标志
  D:页面已修改标志(dirty位)
Tips:不是很清楚段页地址转换的同学,可以先去看看这一部分

/*
 * I put the kernel page tables right after the page directory,
 * using 4 of them to span 16 Mb of physical memory. People with
 * more than 16MB will have to expand this.
 */
 # 在0x1000处打上pg0的标记,也是第0个页表的起始地址
.org 0x1000
pg0:
# 在0x2000打上pg1标记,作用类似
.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000
/*
 * tmp_floppy_area is used by the floppy-driver when DMA cannot
 * reach to a buffer-block. It needs to be aligned, so that it isn’t
 * on a 64kB border.
 */
_tmp_floppy_area:		/* 申请1KB作为软盘高速缓冲区 */
	.fill 1024,1,0
...

/*
 * Setup_paging
 *
 * This routine sets up paging by setting the page bit
 * in cr0. The page tables are set up, identity-mapping
 * the first 16MB. The pager assumes that no illegal
 * addresses are produced (ie >4Mb on a 4Mb machine).
 *
 * NOTE! Although all physical memory should be identity
 * mapped by this routine, only the kernel page functions
 * use the >1Mb addresses directly. All "normal" functions
 * use just the lower 1Mb, or the local data space, which
 * will be mapped to some other place - mm keeps track of
 * that.
 *
 * For those with more memory than 16 Mb - tough luck. I've
 * not got it, why should you :-) The source is here. Change
 * it. (Seriously - it shouldn't be too difficult. Mostly
 * change some constants etc. I left it at 16Mb, as my machine
 * even cannot be extended past that (ok, but it was cheap :-)
 * I've tried to show which constants to change by having
 * some kind of marker at them (search for "16Mb"), but I
 * won't guarantee that's all :-( )
 */
.align 2
setup_paging:
    movl $1024*5,%ecx           /* 5 pages - pg_dir+4 page tables,1024*5个页表项 */
    xorl %eax,%eax
    xorl %edi,%edi              /* pg_dir is at 0x000, 页目录设置在0x0处,最后再让cr3=0x0即可 */
    cld;rep;stosl				/* 清空前5个页,每个页表项都置零 */
    movl $pg0+7,pg_dir          /* set present bit/user r/w */
    movl $pg1+7,pg_dir+4        /*  写页目录的第一个表项,pg_dir+4因为每个表选项为4字节的内容 */
    movl $pg2+7,pg_dir+8        /*  $pg2+7是因为要表明权限7=4+2+1分别代表rwx的权限 */
    movl $pg3+7,pg_dir+12       /*  --------- " " --------- */
    movl $pg3+4092,%edi			/* 计算最后一个页表的最后一个表项的地址 */
    movl $0xfff007,%eax         /*  16Mb - 4096 + 7 (r/w user,p) */
    std
1:      stosl                   /* 从后往前填充页表指向的内存地址,效率会更高一些 */
    subl $0x1000,%eax		/* 完成恒等映射填写4个页表的页表项 */
    jge 1b					/* 1b表示向后跳转到标号1的地方去 */
    xorl %eax,%eax          /* pg_dir is at 0x0000 */
    movl %eax,%cr3          /* cr3 - page directory start,cr3页目录设置为0x0 */
    movl %cr0,%eax
    orl $0x80000000,%eax
    movl %eax,%cr0          /* set paging (PG) bit,开启分页 */
    ret                     /* this also flushes prefetch-queue */
/* 最后这个ret就会弹出栈中的第一个数据作为返回地址,而刚刚在after_page_tables函数中,
   最后压入了$main构造了一个假的返回地址,所以ret之后就直接返回main中执行 */

  head.s程序结束后,就到了main函数中去执行了。head主要完成了重新设置IDT和GDT表,开启分页模式还有在页表中完成了0-16MB的恒等映射。执行完head之后,system在内存中的分布如下图所示。

system模块在内存的分布图
  当然有同学就又要问了,为什么head存放在0x0处,而页表也存放在0x0处,页表覆盖了head的代码,这不会出错吗。
  当然如果覆盖了之后还需要执行的代码的话,肯定是会出错的;但如果覆盖了不会再执行的head代码,就不会出错了。发生页表填写时是执行到setup_paging函数中,而after_page_tables函数是位于0x5400处的,而页表和软盘缓冲区就覆盖到了0x5400,将不会覆盖after_page_tables函数和setup_paging函数以及之后的所有数据。
  
  以上是我最近学习linux0.11源码的理解,参考至赵炯老师的《linux内核完全注释 3.0》。

  • 33
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值