当前位置: 首页 > article >正文

linux0.12-6-4

[259页]

6-4 head.s程序

6-4-1 功能描述

(a)首先是加载各个数据段寄存器。
(b)重新设置中断描述符idt,共256项,并使各个表项均指向一个只报错误的哑中断子程序ignore_int。
中断门描述符中段选择符设置为0x0008,表示该哑中断处理子程序在内核中。
©本程序又重新设置了全局段描述符表gdt。
(d)检查A20地址线是否已真的开启。
(e)设置管理内存的分页处理机制。
(f)head.s程序利用返回指令将预先放置在堆栈中的/init/main.c程序的入口地址弹出,去运行main()程序。

6-4-2 代码注释

/*
 *  linux/boot/head.s
 *
 *  (C) 1991  Linus Torvalds
 */

/*
head.s含有32位启动代码。
注意!!!!32位启动代码是从绝对地址0x00开始的,这里也同样是页目录将存在的地方,
因此这里的启动代码将被页目录覆盖掉。
*/
.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir:			#页目录将会存放在这里。

/*
$0x10的含义是请求特权级0(位0-1=0)、选择全局描述符(位2=0)、
选择表中第2项(为3-15=2)。
*/
startup_32:
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss _stack_start,%esp		#将堆栈放置在stack_start指向的user_stack数组区。
	call setup_idt				#调用设置中断描述符子程序。
	call setup_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					#因为修改了gdt,所以需要重新装载所有的段寄存器。
	mov %ax,%gs					#CS代码段寄存器已经在setup_gdt中重新加载过了。
/*作者的意思:有点问题,但没有产生问题的原因:段限长没有超过8MB,且后面内核执行过程中会重新加载CS*/
	lss _stack_start,%esp
/*
测试A20地址线是否已经开启。在0地址写入值,检查0x100000(1MB)处是否一致,如果是一致检查,否则跳出。
*/
	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
/*
注意!在下面这段程序中,486应该将位16置位,以检查在超级用户模式下的写保护,此后"verify_area()"
调用就不需要了。486的用户通常也会想将NE(#5)置位,以便对数学协处理器的出错使用int 16。
*/
#上面原注释中提到的486CPU中CR0控制寄存器的位16是写保护标志WP,
#用于禁止超级用户级的程序向一般用户只读页面中进行写操作。该标志主要用于操作系统
#在创建新进程时实现写时复制方法。

/*
下面这段程序用于检查数学协处理器芯片是否存在。方法是修改控制寄存器CR0,在假设
存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存在,需要设置
CR0中的协处理器仿真位EM(bit2),并复位协处理器存在标志MP(bit1)
*/
	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

/*
 * 我们依赖于ET标志的正确性来检测287/387存在与否。
 */
check_x87:
	fninit				#向协处理器发出初始化命令。
	fstsw %ax			#取协处理器状态字到ax寄存器中。
	cmpb $0,%al			#初始化后状态字应该为0,否则说明协处理器不存在。
	je 1f				#如果存在则向前跳转到标号1处,否则修改cr0.
	movl %cr0,%eax		
	xorl $6,%eax		/* reset MP, set EM */
	movl %eax,%cr0
	ret
/*
下面的两个字节值是80287协处理器指令fsetpm的机器码。其作业是把80287设置为保护模式。
80387无需该指令,并且将会把该指令看作是空操作。
*/	
.align 2
1:	.byte 0xDB,0xE4		/* fsetpm for 287, ignored by 387 */ #287协处理器码
	ret

/*
下面这段是设置中断描述符表子程序 setup_idt
将中断描述符表idt设置成具有256个项,并都指向ignore_int中断门。然后加载中断描述符表
寄存器(用lidt指令)。真正实用的中断门以后再安装。当我们再其他地方认为一切都正常时再开启中断。
该子程序将会被页表覆盖掉。
*/
#中断描述符表中的项虽然也是8字节组成,但其格式与全局表中的不同,被称为门描述符(Gate Descriptor)。
#它的0-1,6-7字节是偏移量,2-3字节是选择符,4-5字节是一些标志。
#这段代码首先在edx、eax中组合设置处8字节默认的中断描述符值,然后再idt表每一项中都放置
#该描述符,共256项。eax含有描述符低4字节,edx含有高4字节。内核在随后的初始化过程中会
#替换安装那些真正实用的中断描述符项。
setup_idt:
	lea ignore_int,%edx				#将ignore_int的有效地址(偏移值)值->edx寄存器
	movl $0x00080000,%eax			#将选择符0x0008置入eax的高16位中。
	movw %dx,%ax					/* selector = 0x0008 = cs */
									#偏移值的低16位置入eax的低16位中。此时eax含有门描述符低4字节的值。
	movw $0x8E00,%dx				/* interrupt gate - dpl=0, present */ #此时edx含有门描述符高4字节的值。
	lea _idt,%edi					#_idt是中断描述符表的地址。
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)				#将哑中断门描述符存入表中。
	movl %edx,4(%edi)				#eax内容放到edi+4所指内存位置处。
	addl $8,%edi					#edi指向表中下一项。
	dec %ecx
	jne rp_sidt
	lidt idt_descr					#加载中断描述符表寄存器值。
	ret

/*
设置全局描述符表项setup_gdt
这个子程序设置一个新的全局描述符gdt,并加载。此时仅创建了两个表项,
与前面的一样。该子程序只有两行,"非常的"复杂,所以当然需要这么长的注释了。
该子程序将被页表覆盖掉。
*/
setup_gdt:
	lgdt gdt_descr			#加载全局描述符表寄存器。
	ret

/*
Linus将内核的内存页表直接放在页目录之后,使用了4个表来寻址16MB的物理内存。
如果你有多余16MB的内存,就需要在这里进行扩展修改。
*/
#每个页表长为4KB字节(1页内存页面),而每个页表项需要4个字节,因此一个页表共可以存放
#1024个表项。如果一个页表项寻址4KB的地址空间,则一个页表就可以寻址4MB的物理内存。
#页表项的格式为:项的前0~11位存放一些标志,例如是否在内存中(P位0)、读写许可(R/W位1)、
#普通用户还是超级用户使用(U/S位2)、是否修改过(是否脏了)(D位6)等;表项的位12~31是
#页框地址,用于指出一页内存的物理起始地址。
.org 0x1000			#从偏移0x1000处开始时第1个页表(偏移0开始处将存放页表目录)。
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000				#定义下面的内存数据块从偏移0x5000处开始。
/*
当DMA(直接存储器访问)不能访问缓冲块时,下面的tmp_floppy_area内存块
就可供软盘驱动程序使用。其地址需要对其调整,这样就不会跨越64KB边界。
*/
_tmp_floppy_area:
	.fill 1024,1,0		#工保留1024项,每项1B,填充数值0/*
下面这几个入栈操作用于跳转到init/main.c中的main()函数准备工作。第139行上的指令
在栈中压入了返回地址,而第140行则压入了main()函数代码地址。当head.s最后再第218行
执行ret指令时就会弹出main()的地址,并把控制权转移到init/main.c程序中。参见第3章中
有关C函数调用机制的说明。
*/
#前面3个入栈0值应该分别表示envp、argv指针和 argc的值,但main()没有用到。
#
after_page_tables:
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# main函数退出时,返回到L6,方便分析问题。
	pushl $_main
	jmp setup_paging
L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.

/* 下面是默认的中断"向量句柄" */
int_msg:
	.asciz "Unknown interrupt\n\r"		#定义字符串"未知中断(回车换行)".align 2								#按4字节方式对齐内存地址。
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				#中断返回


/*
上面英文注释第2段的含义是指在机器物理内存中大于1MB的内存空间主要被用于主内存区。主内存
区空间由mm模块管理。它涉及页面映射操作。内核中所有其它函数就是这里指的一般(普通)函数。
若要使用主内存区的页面,就需要使用get_free_page()等函数获取。因为主内存区中内存页面是
共享资源的,必须又程序进行统一管理以避免资源争用和竞争。

在内存物理地址0x0处开始存放1页页目录表和4页页表。页目录表是系统所有进程公用的,而这里
的4页页表则属于内核专用,它们一一映射线性地址起始16MB空间范围到物理内存上。对于新的进程,
系统会在主内存区为其申请页面存放页表。另外,1页内存长度是4096字节。
 */
.align 2					#按4字节方式对齐内存地址边界。
setup_paging:				#首先对5页内存(1页目录+4页页表)清0。
	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */
							#页目录从0x000地址开始。
	cld;rep;stosl			#eax内容存到es:edi所指内存位置处,且edi增4。
	
	
	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		/*  --------- " " --------- */
/*
将4个页表都填入0xfff007
*/	
	movl $pg3+4092,%edi		#edi->最后一页的最后一项。
	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */
	std						#方向位置位,edi值递减4字节。
1:	stosl					/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax		#每填写号一项,物理地址值减0x1000.
	jge 1b					#如果小于0则说明全填写号了。
#设置页目录表基地址寄存器CR3的值,指向页目录表。CR3中保存的是页目录表的物理地址。	
	xorl %eax,%eax		/* pg_dir is at 0x0000 页目录表在0x0000处。*/
	movl %eax,%cr3		/* cr3 - page directory start */
#设置启动使用分页处理(cr0的PG标志,位31)	
	movl %cr0,%eax
	orl $0x80000000,%eax	#添上PG标志
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			/* this also flushes prefetch-queue */
/*
在修改分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
该运行指令的另一个作业是将140行压入堆栈中的main程序地址弹出,并跳转到/init/main.c 
程序去运行。本程序到此就真正结束了。
*/


=====================================================
#下面是加载中断描述符表寄存器idtr的指令lidt要求的6字节操作数。前2字节是idt表的限长,
#后4字节是idt表在线性地址空间中的32位基地址。
.align 2
.word 0
idt_descr:
	.word 256*8-1		# idt contains 256 entries
	.long _idt
	
#下面加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数。前2字节是gdt表的限长,
#后4字节是gdt表的线性基地址。这里全局长度设置为2KB字节(0x7ff即可),因为每8字节
#组成一个描述符项,所以表中共可有256项。符号_gdt是全局表在本程序中的偏移位置,见第234
.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 :^)

	.align 3
_idt:	.fill 256,8,0		# idt is uninitialized

#全局表。前4想分别是空项、内核代码段描述符、内核数据段描述符、
#系统调用段描述符(但实际没有用)、后面还预留252想的空间,
#用于防止创建任务的局部描述符LDT和对应的任务状态段TSS的描述符。
_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 */

6-4-3 其他信息

1、 程序执行结束后的内存映像
内核模块在内存中的 详细映像如图6-11所示。

2、 Intel32位保护运行机制
在保护模式下,段寄存器中存放的是一个描述符在描述符表中的偏移地址值。

3、 前导符(伪指令)align


http://www.kler.cn/a/15431.html

相关文章:

  • uniapp luch-request 使用教程+响应对象创建
  • PySpark——Python与大数据
  • IDEA优雅debug
  • 01.02、判定是否互为字符重排
  • 【Linux庖丁解牛】—Linux基本指令(下)!
  • Redis实战案例(黑马点评)
  • 交叉熵损失函数原理详解
  • c++标准模板(STL)(std::array)(四)
  • 红海云CEO孙伟解密智能化人力资源新范式
  • SaaS云HIS系统源码功能介绍
  • Flink实战-(6)FlinkSQL实现CDC
  • 冬奥会传统文化管理系统【GUI/Swing+MySQL】(Java课设)
  • DJ4-3 连续分配存储管理方式
  • window 10 安装node.js时遇到2502 2503错误(已解决)
  • 从起步到成熟:探讨APP在不同发展阶段的商业化路径和变现模式
  • MySQL提权/条件竞争漏洞分析和利用(37)
  • Android Jetpack Compose —— 控件
  • Linux上常用的防火墙软件iptables详解
  • Html5版贪吃蛇游戏制作(经典玩法)
  • IT 面试手册 - 序
  • 【KVM虚拟化】· 图形化KVM安装linux
  • 企业数字化转型的核心是什么?如何才能真正做到数字化转型?
  • Go官方指南(一)包、变量、函数
  • Vue3 element-plus el-select 无法选中,又不报错
  • Docker在Windows系统中的安装方法和使用方法
  • 【学习笔记】- 零基础学React