引自《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 */