linux内核(0.12)-head.s分析

函数功能介绍

  1. 重设中断门描述符表,将所有中断门描述符都指向 “哑中断(ignore_int子程序)”
  2. 重设全局描述符表(GDT)
  3. 测试是否开启A20地址线,如果没有开启,在此处死循环
  4. 判断协处理器是否存在,更具情况设置CR0 中的协处理器仿真位EM(位2)和协处理器存在标志MP(位1)。
  5. 设置页表,将head.s的最开始0~0x4ffff设置为1个页目录表和4个页表。
  6. 跳转到main.c函数

设置中断门描述符表代码介绍

前期知识:

关于中断描述符表知识介绍:

这里需要讲解一下,下图中描述了8Byte内容。
其分布如下:

  • 过程入口点偏移值15~0 === 0~1字节
  • 段选择符 === 2~3字节
  • 标志符(P DPL 0111 0000 xxxx) === 4~5字节
  • 过程入口点偏移值31~16 === 6~7字节

linux内核(0.12)-head.s分析_第1张图片

其中,P是段存在标志;DPL是描述符的优先级。在head.s程序中,中断门描述符中段选择符设置为0x0008,表示该哑中断处理子程序在内核代码中。偏移值被设置为ignore_int中断处理子程序在head.s程序中的偏移值。由于head.s程序被移动到内存地址0开始处,因此该偏移值也就是中断处理子程序在内核代码段中的偏移值。由于内核代码段一直存在于内存中,并且特权级为0,即P=1,DPL=00。因此中断门描述符的字节5和字节4的值应该是0x8E00。

有了上面的基础,再来看下面的代码,应该会简单点了吧。

问题代码:

 /* EAX和AX之间的关系,EAX、ECX、EDX、EBX:為ax,bx,cx,dx的延伸,各為32位元 * ax为EAX的低16位,其他同理*/
setup_idt:
    lea ignore_int,%edx      //取中断程序ignore_int的地址。此时地址放在了edx中
    movl $0x00080000,%eax    //在全局描述符表中获得第二个描述符,即为哑中断
    movw %dx,%ax              //把上面取到的地址低16位(在dx中)放到eax的低16位中 movw $0x8E00,%dx //然后把中断描述符属性、类型放到edx的低16位中
    lea _idt,%edi             //中断描述符表地址
    mov $256,%ecx            //设置256个中断项
rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi             //每个中断描述符占8个字节 
    dec %ecx                  
    jne rp_sidt
//上面设定的eax,edx就是中断描述符的内容。这里把它们放入idt中。共256个一样的。
    lidt idt_descr            //加载中断描述符表寄存器值
    ret

解释代码

/*
 *  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.
 */

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

;// 再次注意!!! 这里已经处于32 位运行模式,因此这里的$0x10 并不是把地址0x10 装入各
;// 个段寄存器,它现在其实是全局段描述符表中的偏移值,或者更正确地说是一个描述符表
;// 项的选择符。有关选择符的说明请参见setup.s 中的说明。这里$0x10 的含义是请求特权
;//0(位0-1=0)、选择全局描述符表(位2=0)、选择表中第2 项(位3-15=2)。它正好指向表中
;// 的数据段描述符项。(描述符的具体数值参见前面setup.s )。
;// 下面代码的含义是:
;// 设置ds,es,fs,gs 为setup.s 中构造的数据段(全局段描述符表的第2 项)=0x10,
;// 并将堆栈放置在stack_start指向的user_stack数据区,然后使用本程序后面定义的新的中断描述符表
;// 和全局段描述表.新的全局段描述表中初始内容与setup.s 中的基本一样,仅段限长从8MB修改成了16MB。
;// stack_start定义在kernel/sched.c中。它是指向user_stack数组末端的一个长指针。下面'lss _stack_start,%esp'
;// 设置这里使用的栈,姑且称为系统栈。但在移动到任务0执行(init/main.c中137行)以后该栈就被用作任务0和任务1
;// 共同使用的用户栈了。

startup_32:             ;//下面设置各个数据段寄存器
    movl $0x10,%eax 	;// 对于GUN汇编,每个直接操作数要以'$'开始,否则表示地址
                        ;// 每个寄存器名都要以'%'开头 ,eax表示32位的ax寄存器。
    mov %ax,%ds 
    mov %ax,%es
    mov %ax,%fs
    mov %ax,%gs
    lss _stack_start,%esp       ;// 表示_stack_start -> ss:esp,设置系统堆栈。
                                ;// stack_start定义在kernel/sched.c
    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 gs,ax                   ;// CS 代码段寄存器已经在setup_gdt 中重新加载过了。
    lss _stack_start,%esp

;// 以下5行用于测试A20 地址线是否已经开启。采用的方法是向内存地址0x000000 处写入任意
;// 一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话,就一直
;// 比较下去,也即死循环、死机。表示地址A20 线没有选通,结果内核就不能使用1M 以上内存。

    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
/*
 * 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.
 */
;/* ;* 注意! 在下面这段程序中,486 应该将位16 置位,以检查在超级用户模式下的写保护, ;* 此后"verify_area()"调用中就不需要了。486 的用户通常也会想将NE(;//5)置位,以便 ;* 对数学协处理器的出错使用int 16。 ;*/
;// 下面这段程序用于检查数学协处理器芯片是否存在。方法是修改控制寄存器CR0,在假设
;// 存在协处理器的情况下执行一个协处理器指令,如果出错的话则说明协处理器芯片不存
;// 在,需要设置CR0 中的协处理器仿真位EM(位2),并复位协处理器存在标志MP(位1)。

    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

/*
 * We depend on ET to be correct. This checks for 287/387.
 */
;// 我们依赖于ET标志的正确性来检测287/387是否存在
;// 下面fninit和fstsw是数学协处理器(80287/80387)的指令。
;// finit向协处理器发出初始化命令,他会把协处理器置于一个未受以前操作影响的已知状态,设置
;// 其控制字为默认值,清楚状态字和所有浮点栈式寄存器。非等待形式的这条指令(fninit)还会让
;// 协处理器终止执行当前正在执行的任何先前的算术操作。fstsw指令取协处理器的状态字。如果系
;// 统中存在协处理器的话,那么在执行了fninit指令后其状态字低字节肯定会为0.

check_x87:
    fninit                      ;// 向协处理器发出初始化命令
    fstsw %ax                   ;// 取协处理器状态字到ax寄存器中 
    cmpb $0,%al                ;// 初始化后状态字应该为0,否则说明协处理器不存在
    je 1f                       /* no coprocessor: have to set bits */
    movl %cr0,%eax              ;// 如果存在则向前跳转到标号1处,否则改写cr0
    xorl $6,%eax               /* reset MP, set EM */
    movl %eax,%cr0
    ret

;// 这里".align 2"的含义是指存储边界对齐调整。
.align 2
;// 下面的两个字节值是80287协处理器指令fsetpm的机器码。其作用是把80287设置为保护模式。
;// 80387无需该指令,并且将会把该指令看做是空操作。
1:  .byte 0xDB,0xE4     /* fsetpm for 287, ignored by 387 */
    ret

/*
 *  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 ; * ; * 将中断描述符表idt 设置成具有256 个项,并都指向ignore_int 中断门。然后加载 ; * 中断描述符表寄存器(用lidt 指令)。真正实用的中断门以后再安装。当我们在其它 ; * 地方认为一切都正常时再开启中断。该子程序将会被页表覆盖掉。 ; */
;// EAX和AX之间的关系,EAX、ECX、EDX、EBX:為ax,bx,cx,dx的延伸,各為32位元
;// 中断描述符表中的项虽然也是8字节组成,但其格式与全局表中的不同,被称为门描述符
;// (Gate Descriptor)。它的0-16-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    //在全局描述符表中获得第二个描述符,即为哑中断
    movw %dx,%ax              //把上面取到的地址低16位(在dx中)放到eax的低16位中   movw $0x8E00,%dx         //然后把中断描述符属性、类型放到edx的低16位中
    lea _idt,%edi             //中断描述符表地址
    mov $256,%ecx            //设置256个中断项
rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi             //每个中断描述符占8个字节 
    dec %ecx                  
    jne rp_sidt
//上面设定的eax,edx就是中断描述符的内容。这里把它们放入idt中。共256个一样的。
    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 setup.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 ; * ; * 这个子程序设置一个新的全局描述符表gdt,并加载。此时仅创建了两个表项,与前 ; * 面的一样。该子程序只有两行,“非常的”复杂,所以当然需要这么长的注释了:) ; * 该子程序将被页表覆盖掉。 ; */
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.
 */
;/* ; * Linus 将内核的内存页表直接放在页目录之后,使用了4 个表来寻址16 Mb 的物理内存。 ; * 如果你有多于16 Mb 的内存,就需要在这里进行扩充修改。 ; */
;// 每个页表长为4 Kb 字节,而每个页表项需要4 个字节,因此一个页表共可以存放1000 个,
;// 表项如果一个表项寻址4 Kb 的地址空间,则一个页表就可以寻址4 Mb 的物理内存。页表项
;// 的格式为:项的前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处开始
/*
 * 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.
 */
;/* ; * 当DMA(直接存储器访问)不能访问缓冲块时,下面的tmp_floppy_area 内存块 ; * 就可供软盘驱动程序使用。其地址需要对齐调整,这样就不会跨越64kB 边界。 ; */
_tmp_floppy_area:
    .fill 1024,1,0      ;// 共保留1024 项,每项1 字节,填充数值0 。

;// 下面这几个入栈操作(pushl)用于为调用/init/main.c 程序和返回作准备。
;// 前面3 个入栈指令不知道作什么用的,也许是Linus 用于在调试时能看清机器码用的.。
;// 139 行的入栈操作是模拟调用main.c 程序时首先将返回地址入栈的操作,所以如果
;// main.c 程序真的退出时,就会返回到这里的标号L6 处继续执行下去,也即死循环。
;// 140 行将main.c 的地址压入堆栈,这样,在设置分页处理(setup_paging)结束后
;// 执行'ret'返回指令时就会将main.c 程序的地址弹出堆栈,并去执行main.c 程序去了。
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 2
ignore_int:
    pushl %eax
    pushl %ecx
    pushl %edx
    push %ds            ;// 这里请注意!!ds,es,fs,gs 等虽然是16 位的寄存器,但入栈后
    push %es            ;// 仍然会以32 位的形式入栈,也即需要占用4 个字节的堆栈空间。
    push %fs
    movl $0x10,%eax    ;// 置段选择符(使ds,es,fs 指向gdt 表中的数据段)。
    mov %ax,%ds
    mov %ax,%es
    mov %ax,%fs
    pushl $int_msg         ;// 把调用printk 函数的参数指针(地址)入栈。
    call _printk        ;// 该函数在/kernel/printk.c 中。
                        ;// '_printk'是printk 编译后模块中的内部表示法。
    popl %eax
    pop %fs
    pop %es
    pop %ds
    popl %edx
    popl %ecx
    popl %eax
    iret                ;// 中断返回(把中断调用时压入栈的CPU 标志寄存器(32 位)值也弹出)。


/*
 * 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 :-( )
 */
;/* ; * Setup_paging ; * ; * 这个子程序通过设置控制寄存器cr0 的标志(PG 位31)来启动对内存的分页处理 ; * 功能,并设置各个页表项的内容,以恒等映射前16 MB 的物理内存。分页器假定 ; * 不会产生非法的地址映射(也即在只有4Mb 的机器上设置出大于4Mb 的内存地址)。 ; * ; * 注意!尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管 ; * 理函数能直接使用>1Mb 的地址。所有“一般”函数仅使用低于1Mb 的地址空间,或 ; * 者是使用局部数据空间,地址空间将被映射到其它一些地方去-- mm(内存管理程序) ; * 会管理这些事的。 ; * ; * 对于那些有多于16Mb 内存的家伙- 太幸运了,我还没有,为什么你会有:-)。代码就 ; * 在这里,对它进行修改吧。(实际上,这并不太困难的。通常只需修改一些常数等。 ; * 我把它设置为16Mb,因为我的机器再怎么扩充甚至不能超过这个界限(当然,我的机 ; * 器很便宜的:-))。我已经通过设置某类标志来给出需要改动的地方(搜索“16Mb”), ; * 但我不能保证作这些改动就行了 :-( ) ; */
.align 2                    ;//4 字节方式对齐内存地址边界。
setup_paging:               

;// 首先对5 页内存(1 页目录+ 4 页页表)清零
    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

;// 下面4 句设置页目录中的项,我们共有4 个页表所以只需设置4 项。
;// 页目录项的结构与页表中项的结构一样,4 个字节为1 项。参见上面的说明。
;// "$pg0+7"表示:0x00001007,是页目录表中的第1 项。
;// 则第1 个页表所在的地址= 0x00001007 & 0xfffff000 = 0x1000;第1 个页表
;// 的属性标志= 0x00001007 & 0x00000fff = 0x07,表示该页存在、用户可读写。
    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     /*  --------- " " --------- */

;// 下面6 行填写4 个页表中所有项的内容,共有:4(页表)*1024(项/页表)=4096 项(0 - 0xfff),
;// 也即能映射物理内存4096*4Kb = 16Mb。
;// 每项的内容是:当前项所映射的物理内存地址+ 该页的标志(这里均为7)。
;// 使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项
;// 在页表中的位置是1023*4 = 4092。因此最后一页的最后一项的位置就是$pg3+4092。
    movl $pg3+4092,%edi    ;// edi->最后一页的最后一项
    movl $0xfff007,%eax        /*  16Mb - 4096 + 7 (r/w user,p) */
                            ;// 最后1项对应物理内存页面的地址是0xfff000
                            ;// 加上属性标志7,即为0xfff007.
    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 */   # 页目录表在0x000处。
    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。
;// 该返回指令的另一个作用是将堆栈中的main 程序的地址弹出,并开始运行/init/main.c 
;// 程序。本程序到此真正结束了。

.align 2        ;//4字节方式对齐内存地址边界
.word 0     

;// 下面两行是lidt 指令的6 字节操作数:前面2字节是idt表的限长,
;//4字节是idt表在线性地址空间中的32位基地址。
idt_descr:
    .word 256*8-1       # idt contains 256 entries ;// 共256项,限长 = 长度-1
    .long _idt
.align 2
.word 0

;// 下面加载全局描述符表寄存器gdtr的指令lgdt要求的6字节操作数。前2字节是gdt表的限长,
;//4字节是gdt表的线性基地址。这里全局表长度设置为2KB字节(0x7ff即可),因为每8字节
;// 组成一个描述符项,所以表中共可有256项。符号_gdt是全局表在本程序中的偏移位置
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            ;//8(2^3)字节方式对齐内存地址边界。
_idt:   .fill 256,8,0       # idt is uninitialized ;// 256项,每项8字节,填0

;// 全局表。前4 项分别是空项(不用)、代码段描述符、数据段描述符、系统段描述符,
;// 其中系统段描述符linux 没有派用处。后面还预留了252 项的空间,用于放置所创建
;// 任务的局部描述符(LDT)和对应的任务状态段TSS 的描述符。
;// (0-nul, 1-cs, 2-ds, 3-sys, 4-TSS0, 5-LDT0, 6-TSS1, 7-LDT1, 8-TSS2 etc...)
;//
/* .quad介绍:
 * .quad 表示零个或多个bignums(用逗号分隔),对于每个bignum,其缺省值是8 字节整数。
 * 如果bignum 超过8 字节,则打印一个警告信息;并只取bignum 最低8 字节。
 */
_gdt:   .quad 0x0000000000000000    /* NULL descriptor */
    .quad 0x00c09a0000000fff    /* 16Mb */      ;//0x08,内核代码段最大长度16MB。
    .quad 0x00c0920000000fff    /* 16Mb */      ;//0x10,内核数据段最大长度16MB。
    .quad 0x0000000000000000    /* TEMPORARY - don’t use */
    .fill 252,8,0           /* space for LDT's and TSS's etc */     ;//预留空间。

参考:

linux内核完全剖析

你可能感兴趣的:(linux,kernel)