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