Linux0.11中的head.s代码分析


head.s程序被编译后,会被连接成system模块的最前面位置,它被setup.s加载到内存绝对地址0处开始的地方,并执行。此时Linux内核已经完全在保护模式下运行。head.s的主要功能包括:

1. 设置内核堆栈;
2. 设置中断描述符表idt;
3. 设置全局描述符表gdt;
4. 设置页目录表和页表;
5. 将/init/main.c程序的入口地址预先压入堆栈中,并在随后利用返回指令弹出该地址,去执行main()程序。
在head.s执行结束之后,其中部分内存空间将被覆盖,而控制权转至system模块中紧接在head.s之后的main程序代码。
 
1. 设置内核堆栈
在head.s中,堆栈段被设置为内核数据段(0x10),堆栈指针ESP设置为kernel模块中定义的全局user_stack数据的顶端,有1页内存(4K)可作为堆栈。
相关代码如下:
movl $0x10, %eax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
lss _stack_stack, %esp
段选择符长度为16位,包括三个部分:
B15--B3,索引值,共13位,用于选择指定描述符表中8192个描述符中的一个。处理器将该索引值乘上8(描述符的字节长度),并加上描述符表的基地址即可访问表中指定的段描述符。
B2:表指示器(TI),指出选择符所引用的描述符表。值位0表示GDT表,1表示指定当前的LDT表。
B1--B0,请求者的特权级(RPL):用于保护机制。
因此这里的0x10(0x0000000000010000),是一个描述符表项的选择符,含义是请求特权级0、选择全局描述符表、选择表中第2项,即偏移在字节16处的段描述符,它正好指向表中的数据段描述符项(实际上在setup.s中为0x00C09200000007FF,在head.s中为0x00C0920000000FFF)。
 
2. 设置中断描述符表(暂缺)
 
3. 设置全局描述符表
刚进入head.s时,使用的是在setup.s中设置的全局描述符表。head.s将重新设置全局描述符表,并在以后的代码中使用之。
全局描述符表长度为2KB,每项8个字节,共2048/8=256项。其中前4项分别为空项、代码段描述符、数据段描述符、系统段描述符,其中Linux没有使用系统段描述符。后面还预留了252项的空间,用于放置所创建任务的局部描述符(LDT)和对应的任务状态段TSS的描述符。
.quad 0x0000000000000000 空描述符
.quad 0x00C09A0000000FFF 16MB
.quad 0x00C0920000000FFF 16MB
.quad 0x0000000000000000 未使用
用于程序代码段和数据段的描述符格式如下:
B63--B56,基地址的B31--B24
B55:G,颗粒度(Granularity),指定限长字段值代表的单元含义。当为0时,限长单元值为1字节;当为1时,限长的单元值为4KB字节。
B54:X
B53:0
B52:AVL
B51--B48:段限长的B19--B16
B47:P,段存在位。如果该位为0,则描述符无效,不能用于地址变换过程。当指向该描述符的选择符被加载到段寄存器中时,处理器就会发出一个异常信号。
B46--B45:DPL,描述符特权级(Descriptor Priviledge Level),用于保护机制,共有4级。0为最高特权级,3为最低特权级。在Linux中只用到了两级:0和3。
B44:0
B43--B40:TYPE,用于区分各种不同类型的描述符。
B39--B32:基地址的B23--B16
B31--B16:段基址的B15--B0
B15--B0:段限长的B15--B0
由此我们来理解代码段描述符:
0x00C09A0000000FFF,即
0b00000000110000001001101000000000
0b00000000000000000000111111111111
段基址为0x00000000。
段限长为0x00FFF,即4K,由于颗粒度标志为1,即以4KB为单位,故最大为16MB。
同时P标志为1,DPL标志为00(特权级0),
数据段描述符类似,只不过是类型(TYPE)标志从A(1010)换成了2(0010)。
 
4. 设置页目录项和页表项
偏移0开始处存放页表目录;
偏移0x1000处存放第一个页表(物理内存4KB处);
偏移0x2000处存放第二个页表(物理内存8KB处);
偏移0x3000处存放第三个页表(物理内存12KB处);
偏移0x4000处存放第三个页表(物理内存16KB处);
每个页表长为4KB字节(1页内存页面),而每个页表项需要4个字节,因此一个页表共可以存放1024个表项。如果一个页表项寻址4KB的地址空间,则一个页表就可以寻址4MB的物理内存。(因此寻址16MB的物理内存共需要4个页表项)
页目录表项和页面表项的格式:
B31-B12:共20位,为物理页码。因为内存页是位于4K边界上的,所以只需20位作为物理页码,而低12位可用作属性。在一个页目录中,页表项的前20位是一个页表的起始地址;在第二级页表中,页表项的前20位包含期望内存操作的页框的地址。
B11-B9:共3位(AVL)
B8:0
B7:0
B6:D,是已修改(Dirty)标志;
B5:A,是已访问(Access)标志;
B4:0
B3:0
B2:U/S,是用户/系统属性位,指示该表项所指定的页是否是用户级页。若U/S为1,表项所指定的页是用户级页,可由任何特权级执行的程序访问;如果为0,表项所指定的页是系统级页,只能由系统特权级下执行的程序访问。
B1:R/W,读写属性位,指示该表项所指定的页是否可读、写或执行。R/W对页的写保护只在处理器处于用户特权级时发挥作用;在系统特权级下,该位被忽略。
B0:P,存在属性位,表示该表项是否有效。
在head.s中与页目录表和页表相关的操作包括:(1)在页目录表中设置页目录项指向对应的页表。(2)在页表中设置页表项指向对应的页表。
在页目录表中设置指向上述4个页表的目录项如下:
movl $pg0+7, _pg_dir 即将页目录表的第一项的内容设置为$pg0+7,即0x00001007。其中物理页码为前20位,即0x00001,表示第1个物理内存(注意到物理内存以4K为边界)。该页面对应的属性标志为后面12个字节,即0x007,表示该页存在、用户态可读写。
movl $pg1+7, _pg_dir+4 即将页目录表的第二项的内容设置为$pg1+7,因为每个页目录项占据4个字节,因此为_pg_dir+4。
movl $pg2+7, _pg_dir+8
movl $pg3+7, _pg_dir+12
在4个页表中设置页表项内容的方式如下:因为每个页表占据一个物理页面,为4KB,而每项需要4个字节,因此每个页表为1024项,4个页表共有4096项(0-0xFFF)。
每项的映射内容是当前项所映射的物理内存地址+该页的标志(这里均为7)。填充的方式是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项在页表中的位置是1023*4=4092。因此最后一页的最后一项的位置就是$pg3+4092,它映射到的物理页面号为0xFFF,对应的代码如下:
movl $pg3+4092, %edi
movl $0xFFF07, %eax
之后,每填好一项,物理地址值减去0x1000,每项的值减4个字节,直至物理地址小于0。
在设置好页目录表和页表后,就可以设置页目录基址寄存器CR3的值,指向页目录表。
xor1 %eax, %eax
movl %eax, %cr3
并设置启动使用分页处理(CR0的PG标志,为31)
movl %CR0, %eax
orl $0x80000000, %eax
movl %eax, %cr0
 
5. 跳转到/init/main.c
至此,调用返回指令,将堆栈中的main程序的地址弹出,并开始运行/init/main.c程序。
与压栈相关的指令如下:
push1 $0
push1 $0
push1 $0
push1 $L6
push1 $_main
调用返回指令代码如下:
ret
 
在head.s程序执行结束后,已经正式完成了内存页目录和页表的设置,并重新设置了内核实际使用的中断描述符表和全局描述符表。此时system模块在内存中的详细映像如下:
0x0000--0x0FFF:内存页目录表(4KB)
0x1000--0x1FFF:内存页表PG0(4KB)
0x2000--0x2FFF:内存页表PG1(4KB)
0x3000--0x3FFF:内存页表PG2(4KB)
0x4000--0x4FFF:内存页表PG3(4KB)
0x5000--0x53FF:软盘缓冲区(1KB)
head.s部分代码
中断描述符表IDT(2KB)
全局描述符表GDT(2KB)
main.c程序代码
kernel模块代码
mm模块代码
fs模块代码
lib模块代码

相关内容