读head.S

引自《linux内核0.11完全注释》

非常感谢赵老师,注释的很详细,我边看边写,基本1 2遍就能懂!

 

/* head.s 2011-04-12
 * 注意: 本程序是采用AT&T的汇编格式,需要使用GUN的gas和gld2进行编译链接
 * 注意代码的赋值方向是从左到右
 * head.s含有32位启动码。
 * 注意32位的启动码是从绝对地址0x00000000开始,这里同样也是页目录存放的地方
   因此启动代码将被页目录覆盖
*/
.text
.global _idt, _gdt, _pg_dir, _tmp_floppy_area
_pg_dir:   # 页目录将会存放在这里
startup32:
    movl $0x10, %eax  # 对于GUN汇编来说,每个直接数都要以"$"开始,否则是表示地址
                      # 每个寄存器名都要以'%'开头,eax表示32位的ax寄存器
                     
# 注意! 这里已经是32位运行模式,因此这里的$0x10并不是把地址0x10装入各个段寄存器
# 它现在其实是全局段描述符表中的偏移值,或者更正确的说是一个描述符表项的选择符。
                      # 置ds es fs gs中的选择符为setup.s中构造的数据段0x10
                      # 0x10(0001 0000)含义特权级0(位0-1=0)、选择全局描述表(位2=0)、
                      # 选择表中第2项(位3-15=2),正好是数据段描述符项
    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    # 调用设置全局描述表子程序
                      # 新全局段描述表中初始内容与setup.s中的基本一样,
                      # 仅段限长从8MB修改为16MB。
    movl $0x10, %eax  # reload all the segment registers
    mov %ax, %ds     
    mov %ax, %es
    mov %ax, %fs      # 因为修改了gdt,所以需要重新加载所有段寄存器
    mov %ax, %gs      # CS代码段寄存器已经在setup_gdt中重新加载过了
# 由于段描述符中的段限长从setup.s程序的8MB改成了本程序设置的16MB,因此这里再次对
# 所有段寄存器执行加载操作是必须的。
    lss _stack_start, %esp

# 下面此时A20地址线是否已经开启。采用的方法是向内存地址0x000000处写入任意
# 一个数值,然后看内存地址0x100000(1M)处是否也是这个数值。如果一直相同的话
# 就一直比较下去,即死循环,死机。表示地址A20线没有选通,结果内核就不能使用
# 1M以上的内存。
    xorl %eax, %eax
1:  incl %eax
    mov $eax, 0x000000
    cmpl %eax, 0x100000
    je lb                # '1b'表示向后(backward)跳转到标号1去
                         # 若是'5f'则表示向前(forward)跳转到标号5去

/*
 * 注意,在下面这段程序中,486应该将位16置位,以检查在超级用户模式下的写保护,
 * 以后"verify_area()"调用中就不需要了。486的用户通常也会想将NE(#5)置位,以便
 * 对数学协处理器的出错使用int 16
 */
# 下面这段程序用于检查数学协处理器芯片是否存在。方法是修改控制寄存器CR0,在
# 假设存在协处理的情况下执行一条协处理器指令,如果出错,则说明不存在。
# 需要设置CR0中的协处理仿真位EM(bit2),并复位协议处理器存在标志MP(bit1)
    mov %cr0, %eax
    andl $0x80000011, %eax
    orl $2, %eax           #set MP /* "orl $0x10020, %eax" here for 486 might be good */
    movl %eax, %cr0
    call check_x87
    jmp after_page_tables

/* 我们依赖ET标志的正确性来检测287/387 */
check_x87:
    fninit
    fstsw %ax
    cmpb $0, %ax
    je 1f              # 如果存在的则向前跳转到标号1处,否则改写cr0
    movl %cr0, %eax
    xor $6, %eax
    movl %eax, %cr0
    ret
.align 2 # 这里".align 2"的含义是指存储边界对齐调整。"2"表示调整到地址最后2位为0
         # 即按4字节方式对齐内存地址
1:  .byte 0xDB,0xE4    # 287协处理器码
    ret


/*
 * 下面这段是设置中断描述符表子程序setup_idt
 * 将中断描述符表idt设置成具有256个项,并指向ignore_int中断门。
 * 然后加载中断描述符表寄存器(用lidt指令)。真正实用的中断门以后再安装。
 * 当我们再其它地方人为一切都正常时再开启中断。该子程序将会被页表覆盖掉
 */
 
# 中断描述表中的项虽然也是8个字节,但其格式与全局表中不同,被称为门描述符
# 它的0-1、6-7字节是偏移量,2-3字节是选择符,4-5字节是一些标志。
setup_idt:
    lea ignore_int, %edx   # 将ignore_int的有效地址(偏移值)值->edx寄存器 6-7Byte
    movl $0x00080000, %eax # 将选择符0x0008置入eax的高16位中  2-3Byte
    movw %dx, %ax          # 偏移值的低16位置入eax的低16位中,0-1Byte
                           # 此时eax含有门描述符低4字节的值。
    movw $0x8E00, %dx      # 此时edx含有门描述符高4字节的值   4-5Byte
    lea _idt, %edi         # _idt是中断描述符表的地址
    mov $256, %ecx
rp_sidt:
    movl %eax, (%edi)      # 将哑中断门描述符存入表中
    movl %edx, 4(%edi)
    addl $8, %edi          # edi指向表中下一项
    dec %ecx
    jne rp_sidt
    lidt idt_descr         # 加载中断描述符表寄存器值
    ret

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

/*
 * Linus 将内核的内存页表直接放在页目录之后,使用了4个表来寻址16Mb的物理内存
 * 如果你有多于16Mb的内存,就需要再这里进行扩充修改
 */
# 每个页表长为4Kb字节(1页内存页面),而每个页表项需要4个字节,因此一个页表共可以
# 存放1024个表项。如果一个页表项寻址4kb的地址空间,则一个页表就可以寻址4Mb的物理内存。
# 页表项的格式为: 项的前0-11位存放一些标志,例如是否在内存中(P bit0)、读写许可(R/W bit1)
# 普通用户还是超级用户使用(U/S bit2)、是否修改过(是否脏了)(D bit6)等;
# 表项的bit12-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项,每项1字节,填充数值0。


# 下面这几个入栈操作(pushl)用于为调用/init/main.c程序和返回做准备。
# 前面3个入栈0值应该分别是envp、argv指针和argc值,但main()没有用到。
# pushl $L6此入栈操作是模拟调用main.C程序时首先将返回地址入栈的操作,
# 所以如果main.c程序真的退出时,就会返回到这里的标号L6处继续执行下去,也即死循环。
# pushl $_main 将main.c的地址压力堆栈,这样,在设置分页处理(set_paging)结束后
# 执行ret返回指令时,就会把main.c程序的地址弹出堆栈,并去执行main.c程序了
after_page_tables:
    pushl $0
    pushl $0
    pushl $0
    pushl $L6
    push $_main
    jmp setup_paging
L6:
    jmp L6

/* 下面是默认的中断"向量句柄" */
int_msg:
    .asciz "Unknown interrupt/n/r"  # 定义字符串"未知中断(回车换行)"
.align 2             # 按4字节方式对齐内存地址。
ignore_int:
    pushl %eax
    pushl %ecx
    pushl %edx
    push %ds         # 这里请注意,ds es fs gs等虽然是16位寄存器,但入栈后
                     # 仍然会以32位的形式入栈,也即需要占用4个字节的堆栈空间
    push %es
    push %fs
    movl $0x10, %eax
    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位)也弹出)

/*
 * 这个子程序通过设置控制寄存器cr0的标志(PG位31)来启动对内存的分页处理功能
 * 并设置各个页表项的内容,以恒等映射前16MB的物理内存。分页器假定不会产生非法的
 * 地址映射(也即在只有4Mb的机器上设置出大于4Mb的内存地址)。
 * 注意! 尽管所有的物理地址都应该由这个子程序进行恒等映射,但只有内核页面管理函数能
 * 直接使用>1Mb的地址。所有"一般"函数仅使用低于1Mb的地址空间,或者是使用局部数据
 * 空间,地址空间将被映射到其他一些地方去---mm(内存管理程序)会管理这些事
 * 对于多于16Mb的内存,可自行修改这里代码的参数,可以尝试去改
 */

# 在内存物理地址0x0处开始存放1页页目录和4页页表。页目录表是系统所有进程公用的。
# 而这里的4页页表则是属于内核专用的。对于新的进程,系统会在主内存区为它申请页面
# 存放页表。1页内存长度是4096字节
.align 2
setup_paging:
    movl $1024*4, %ecx   /* 首先对5页内存清零 */
    xorl %eax, %eax
    xorl %edi, %edi
    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
    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  # 最后1项对应的物理内存页面地址是0xfff000,加上属性标志7
                          # 即为0xfff007
    std                   # 方向位置位,edi值递减(4字节)
1:  stosl
    subl $0x1000, %eax    # 每填好写好一项,物理地址减0x1000
    jpe 1b                # 如果小于0,则说明全填写好了

# 设置页目录基地址寄存器cr3的值,指向页目录表
    xorl %eax, %eax       # 页目录表在0x0000处
    movl %eax, %cr3
# 设置启动使用分页处理(cr0的PG位,位31)
    movl %cr0, %eax
    orl $0x80000000, %eax # 添上PG标志
    movl %eax, %cr0
    ret
# 在改变分页处理标志后,要求使用转移指令刷新预取指令队列,这里使用的是返回指令ret
# 该返回指令的另一个作用是将堆栈中的main程序的地址弹出,并开始运行/init/main.c
# 本程序到此就真正结束了!

 

.align 2             # 按4字节方式对齐内存地址边界
.word 0
idt_descr:           # 下面两行是lidt指令的6字节操作数:长度,基址。
    .word 256*8-1
    .long _idt
   
.align 2
.word 0
gdt_descr:           # 下面两行是lgdt指令的6字节操作数:长度,基址
    .word 256*8-1
    .long _gdt
   

.align 3
_idt:
    .fill 256,8,0 # 256项,每项8个字节,填0

# 全局表。前4项分别是空项(不用)、代码段描述符、数据段描述符、系统段描述符,
# 其中,系统端描述符linux没有用。后面还预留了252项的空间,用于放置所创建任务的
# 局部描述符(LDT)和对应的任务状态段TSS的描述符
_gdt:
    .quad 0x0000000000000000  /* NULL descriptor */
    .quad 0x00c09a0000000fff  /* 16Mb */ # 0x08,内核代码段最大长度16M
    .quad 0x00c0920000000fff  /* 16Mb */ # 0x10,内核数据段最大长度16M
    .quad 0x0000000000000000  /* don't use */
    .fill 252, 8, 0           /* space for LDT's and TSS's etc */

   

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

robbie1314

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值