linux kernel 0.11 head
head的作用
注意:bootsect和setup汇编采用intel的汇编风格,而在head中,此时已经进入32位保护模式,汇编的采用的AT&T的汇编语言,编译器当然也就变成对应的编译和连接器了,很多汇编指令右侧都会多l,如or变成orl却别与实模式的汇编指令。
①设置中断描述符表:
》中断描述符表设置为256个项,初始化指向ignore_int中断门。然后加载中断描述符表寄存器。
②检查 A20 地址线是否已经打开,测试系统是否含有数学协处理器
②设置全局描述符表:
③设置分页机制:
》包括设置控制寄存器cr0:PG位,开启分页;
》初始化页目录,指向4个页表
》初始化页表,指向16MB的物理内存段基址
④跳转到 system 模块中的初始化程序main.c开始执行:
概念:
①关于中断描述符表的详细介绍参阅:http://blog.csdn.net/fwqcuc/article/details/5855460
②关于控制寄存器(CR0,CR1,CR2,CR3):http://blog.csdn.net/sunnybeike/article/details/6781353
控制寄存器(CR0,CR1,CR2,CR3)用于控制和确定处理器的操作模式以及当前执行任务的特性。他们均是32位的控制寄存器。
CR1:被保留,供今后开发的处理器使用,在 80386中不能使用CR1,否则会引起无效指令操作异常。
CR0:包括指示处理器工作方式的控制位,包含启用和禁止分页管理机制的控制位,包含控制浮点协处理器操作的控制位。
CR2、CR3:由分页管理机制使用。
其中,CR0中的位5—位30及CR3中的位0至位11是保留位,这些位不能是随意值,必须为0。 CR0的低16位等同于80286的机器状态字MSW。
详细的介绍
1.CR0中的保护控制位
控制寄存器CR0中的位0用PE标记,位31用PG标记,这两个位控制分段和分页管理机制的操作,所以把它们称为保护控制位。PE控制分段管理机制。 PE=0,处理器运行于实模式;PE=1,处理器运行于保护方式。PG控制分页管理机制。PG=0,禁用分页管理机制,此时分段管理机制产生的线性地址直接作为物理地址使 用;PG=1,启用分页管理机制,此时线性地址经分页管理机制转换位物理地址。由此可知,如果要启用分页机制,那么PE和PG标志都要置位。由于只有在保护方式下才可启用分页机制,所以尽管两个位分别为0和1共可以有四种组合,但只有三种组合方式有效。PE=0且PG=1是无效组合,因此,用PG为1且PE为0的值装入CR0寄存器将引起通用保护异常。
2.CR0协处理器控制位
控制寄存器CR0中的位1—位4分别标记为:
①MP(算术存在位)
②EM(模拟位,用于选择与协处理器进行通信所使用的协议,即指明系统中是用的是80386还是80286协处理器)
EM位控制浮点指令的执行是用软件模拟,还是由硬件执行。EM=0时,硬件控制浮点指令传送到协处理器;EM=1时,浮点指令由软件模拟。
③TS(任务切换位)
TS 位用于加快任务的切换,通过在必要时才进行协处理器切换的方法实现这一目的。每当进行任务切换时,处理器把TS置1。TS=1时,浮点指令将产生设备不可用(DNA)异常。 MP位控制WAIT指令在TS=1时,是否产生DNA异常。MP=1和TS=1时,WAIT产生异常;MP=0时,WAIT指令忽略 TS条件,不产生异常。
④ET(扩展类型位),它们控制浮点协处理器的操作。
当处理器复位时,ET位被初始化,以指示系统中数字协处理器的类型。如果系统中存在 80387协处理器,那么ET位置1;如果系统中存在80287协处理器或者不存在协处理器,那么ET位清0。
在系统刚上电时,处理器被复位成pe=0,pg=0(即实模式状态),以允许引导代码在启用分段和分页机制之前能够初始化这些寄存器和数据结构。对于这类寄存器的使用是在实模式下方可使用。
3.CR2和CR3
控制寄存器CR2和CR3由分页管理机制使用。
CR2用于发生页异常时报告出错信息。当发生页异常时,处理器把引起页异常的线性地址保存在CR2中。操作系统中的页异常处理程序可以检查CR2的内容,从而查出线性地址空间中的哪一页引起本次异常。
CR3 用于保存页目录表页面的物理地址,因此被称为PDBR。由于目录是页对齐的,所以仅高20位有效,低12 位保留供更加高级的处理器使用。向CR3中装入一个新值时,低12位必须为0;但从 CR3中取值时,低12位被忽略。每当用MOV指令重置CR3的值时,会导致分页机制高速缓冲区的内容无效,用此方法,可以在启用分页机制之前,即把PG 位置1之前,预先刷新分页机制的高速缓存。CR3寄存器即使在CR0寄存器的PG位或PE位为0时也可装入,如在实模式下也可设置CR3,以便进行分页机制的初始化。在任务切换时,CR3要被改变,但是如果新任务中CR3的值与原任务中CR3的值相同,那么处理器不刷新分页高速缓存,以便当任务共享页表时有较快的执行速度。
实模式和保护模式区别及寻址方式
实模式和保护模式的区别:
最早期的8086 CPU只有一种工作方式,那就是实模式,而且数据总线为 16位,地址总线为20位,实模式下所有寄存器都是16位.
而从80286开始就有了保护模式,从80386开始CPU数据总线和地址总线均为32位,而且寄存器都是32位。
但80386以及现在的奔腾、酷睿等等CPU为了向前兼容都保留了实模式,现代操作系统在刚加电时首先运行在实模式下,然后再切换到保护模式下运行。
三种地址的概念:
①逻辑地址:即逻辑上的地址,实模式下由“段基地址+段内偏移”组成;保护模式下由“段选择符+段内偏移”组成。
②线性地址:逻辑地址经分段机制后就成线性地址,它是平坦的;如果不启用分页,那么此线性地址即物理地址。
③物理地址:线性地址经分页转换后就成了物理地址。
两种模式下的寻址方式:
①实模式的寻址方式:段基址+段内偏移。
②保护模式寻址方式:用段选择符而非段基地址,使用段选择符寻找最终的段基址+段内偏移。如果不启用分页管理的情况下,那么此线性地址即最终的物理地址。
段选择符寻找段基址的详细过程-分段机制:
①段选择符+段内偏移地址,如果不启用分页管理的情况下,那么此线性地址即最终的物理地址。
图1 逻辑地址到线性地址转换
图2 段选择符结构
图3 段描述符结构描述
偏移地址:如图1,32位,所以每个段的大小为2^32=4GB
段选择符:如图2,16位,其中TI用来指明全局描述符表GDT(0)还是局部描述符表LDT(1),RPL表示请求特权级,索引值为13位。在保护模式下最多可以表示2^13=8192个段描述符,而TI又分GDT和LDT。所以一共可以表示8192*2=16384个段描述符,每个段描述符可以指定一个具体的段信息,所以一共可以表示16384个段。而图1看出,段内偏移地址为32位值,所以一个段最大可达4GB,这样16384*4GB=64TB,这就是所谓的64TB最大寻址能力,也即逻辑地址/虚拟地址。在保护模式实际汇编中,如下一条语句:jmpi 0, 8。其中的8即段选择符,8的二进制表示为:0000 0000 0000 1000b,所以这条语句的意思是跳转到GDT表(TI=0)中的第2个(段描述符表从0开始编号,所以这里的1指表中的第2个)段描述符定义的段中,其段内偏移为0。
段描述符:
如图3,共64位,8B,段描述符里有三个部分基地址信息(24-31,0-7,16-31),三部分组成32位地址,就决定了段基地址位置,此地址再加上段内偏移最终确定线性地址位置。
段描述符中的S位和TYPE字段(四位)的不同又分为数据段描述符、代码段描述符(S=1)和系统段描述符(S=0)。
数据段描述符、代码段描述符的TYPE字段描述,即S=1,如下图4:
图4.S=1的TYPE描述
系统段描述符的TYPE字段描述,即S=0,如下图5:
图5.S=0的TYPE描述
②段选择符+段内偏移地址,在启用分页管理的情况下,线性地址经需要经过分页转换后才能形成最终的物理地址
图6.分页机制的寻址方式
此时线性地址分为:页目录字段(10)、页表字段(10)、页偏移字段(12)。
页偏移字段:12位,页偏移位2^12=4KB,页大小就是4KB.
页目录字段:10位,指定页目录中的目录项,可以选择2^10=1024个目录项,每个目录项为4B,所以页目录为1024*4B=4KB。
页目录中的目录项:高20位用以查找页表在物理内存中的页面标号。
页表中的页表项:高20位用以定位物理地址空间中的某个页基地址。
因为只有一个页目录,所以线性地址中的页目录字段对应页目录表中的目录项,目录项的高20位对应页表,线性地址的页表字段对应页表中的段基址,加上页偏移字段就是最终的物理地址。就像图书目录索引到某一页,之后通过对应的标题找到段落,再配合偏移量,就找到对应的单词了。
分页机制如下图6所示,它把物理内存分成相同固定大小的页面,2^12=4KB。每个页面的0~4KB范围由线性地址的低12位表示,线性地址空间的高10位用来指定页目录中的位置,可以选择2^10=1024个目录项,每个目录项为四字节,所以页目录为1024*4B=4KB。每个目录项中的高20位用以查找页表在物理内存中的页面,每个页表含1024个页表项,每个页表项也是四字节,这样一页表也是1024*4B=4KB。所以一个页目录可以查找1024个页表,每个页表为4KB,所以总共可以查找的页表大小为1024*4KB=4MB大。最后每个页表项的高20位用以定位物理地址空间中的某个页基地址,此地址再加上线性地址空间的偏移值就是最后物理内存空间单元。目录项和页表项结构如图8所示。
总结:从一个逻辑地址经过分段和分页寻址物理地址的整个过程,如图7.
逻辑地址经分段机制变成线性地址,如果不启用分页的情况下,此线性地址就是物理地址;如果启用分页,那么线性地址经分页机制变成物理地址。
图7.逻辑地址经过分段和分页解析成物理地址的过程
补充:分段与分页的意义
分段的意义:
①充分发挥了段机制的对代码、数据结构和程序提供硬件保护的能力。每个程序都有自己的段描述符表和自己的段。段可以完全属于程序私有也可以和其它程序之间共享。
②访问权限的检查不仅仅用来保护地址越界,也可以保护某一特定段不允许操作。例如代码段是只读段,硬件可以阻击向代码段进行写操作。
分页的意义:
①分页为需求页、虚拟内存提供实现机制。
head的源码阅读批注:
/* * linux/boot/head.s * * (C) 1991 Linus Torvalds */ /* * head.s contains the 32-bit startup code. * * NOTE!!! Startup happens at absolute address 0x00000000, which is also where * the page directory will exist. The startup code will be overwritten by * the page directory. */ .text .globl idt,gdt,pg_dir,tmp_floppy_area pg_dir: startup_32: movl $0x10,%eax ! 0x10=1010b 请求特权级别=10 选择全局描述符表 索引值为1 mov %ax,%ds ! 数据段、扩栈段、 mov %ax,%es mov %ax,%fs mov %ax,%gs lss _stack_start,%esp ! ? 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 mov %ax,%gs lss stack_start,%esp xorl %eax,%eax ! 检测A20是否启动 采用的方法是向内 存地址 0x000000 处写入任意 一个数值,然后看内存地址 0x100000(1M) 处是否也是这个数值。 1b: incl %eax # check that A20 really IS enabled eax变为1 movl %eax,0x000000 # loop forever if it isn't 在0处赋值为1 cmpl %eax,0x100000 #比较0x100000处的值是否为1,如果想等,则一直比较下去,即死机 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,PE,ET 获取cr0的值 /* "orl $0x10020,%eax" here for 486 might be good */ orl $2,%eax # set MP movl %eax,%cr0 call check_x87 jmp after_page_tables /* * We depend on ET to be correct. This checks for 287/387. */ check_x87: fninit fstsw %ax cmpb $0,%al je 1f /* no coprocessor: have to set bits */ movl %cr0,%eax xorl $6,%eax /* reset MP, set EM */ movl %eax,%cr0 ret .align 4 1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */ ret /* * setup_idt 重新设置中断描述符表,共 256 项,并使各个表项均指向一个只报错误的哑中断程序。 * 中断描述符表设置为256个项,初始化指向ignore_int中断门。然后加载中断描述符表寄存器。 * 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 ! 将ignore_int中断门的有效地址(偏移量)放在edx寄存器 movl $0x00080000,%eax ! 将选择符0x0008置入eax高16位 movw %dx,%ax /* selector = 0x0008 = cs */ ! 低2B放在eax低2B中,eax寄存器是32位的,此时eax含有了门描述符的低4B的值 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */ ! 门描述符工8B,高4B:dx=0x00008E00,低4B:ax=0x0008+偏移量, ! 中断门中0-1,6-7位偏移量,2-3选择符,4-5标志 lea idt,%edi ! idt 是中断描述符表的地址 mov $256,%ecx ! cx作为计数器 rp_sidt: movl %eax,(%edi) ! eax中时ignore_int中断门的描述符低4B,将其放在edi所指定的idt中断描述符表的地址中 movl %edx,4(%edi) ! movl与mov区别是movl:move long移动4个字节,将高4B即edx值放在表中。 addl $8,%edi ! edi存放的地址加8B,指向中断描述符表的下一项。 dec %ecx ! 计数器减一 jne rp_sidt ! 循环存放 lidt idt_descr ! 保存中断描述符到寄存器 ret /* * 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 ret /* * 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. */ .org 0x1000 pg0: .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: .fill 1024,1,0 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#将main地址和传入的参数入栈,以便分页完毕后,出站执行main程序 jmp setup_paging #分页设置开始 L6: jmp L6 # main should never return here, but # just in case, we know what happens. /* This is the default interrupt "handler" :-) */ int_msg: .asciz "Unknown interrupt\n\r" .align 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 /* * 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. * * 这个子程序通过设置控制寄存器 cr0 的标志(PG 位 31)来启动对内存的分页处理功能, * 并设置各个页表项的内容,以恒等映射前 16 MB 的物理内存。 * 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 4 #四字节对其 setup_paging: #对五也内存(1页目录+4页表)清零 每个页表有1024项,每项大小为4B, #初始化 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 /* 清除DF标志,ESI和EDI递增,将AX的4B数据存储在EDI对应的内存中,ECX递减知道为零 */ #设置页目录,初始为存在、可读可写 movl $pg0+7,pg_dir /* set present bit/user r/w */ movl $pg1+7,pg_dir+4 /* --------- "pg_dir的高20位指向页表的地址" --------- */ movl $pg2+7,pg_dir+8 /* --------- "pg_dir的低12位007表示:该页存在,可读可写 " --------- */ movl $pg3+7,pg_dir+12 /* --------- " " --------- */ #4(页表)*1024(项)=4096项 #4096*4KB(页大小)=16MB, #每项的高20位为物理地址,低12位为控制位 #页表倒序设置值,倒数第一个页表项地址是$pg3+4092 movl $pg3+4092,%edi movl $0xfff007,%eax /* 16Mb - 4096=页基址 7 (r/w user,p)=页控制 */ std /*edi值递减*/ 1: stosl /* fill pages backwards - more efficient :-) */ subl $0x1000,%eax /*地址递减*/ jge 1b #将页目录表的初始地址0x0000,赋值给cr3 xorl %eax,%eax /* pg_dir is at 0x0000 */ movl %eax,%cr3 /* cr3 - page directory start */ #设置cr0的PG(位31)=1,表示开启分页 movl %cr0,%eax orl $0x80000000,%eax movl %eax,%cr0 /* set paging (PG) bit */ ret /* this also flushes prefetch-queue */ .align 4 .word 0 idt_descr: .word 256*8-1 # idt contains 256 entries .long idt .align 4 .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 8 idt: .fill 256,8,0 # idt is uninitialized 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 */