0%

head 模块

再次解释,如果你没了解过保护模式,后续的一些名词和概念你可能不知所以然。

从这里开始,后面所使用的汇编都是 32 bit 的 AT&T 汇编

如果有兴趣了解 AT&T 汇编语法,请参考 x86 Assembly Guide

我没有找到比较好的文档,如果你知道,请推荐给我。

以下汇编注释,都是笔者遇到不懂,进行查阅并注释,并未非常详细的进行注释。

我的书写顺序可能有点奇怪,不是按照代码的编写顺序来完成的,而是按照代码的执行顺序来的。

head.s

head的源码为boot/head.s 文件。

查看源码中可配合反汇编提高效率:

1
2
3
4
as --32  -o boot/head.o boot/head.s
objdump -m i386 -d boot/head.o
#如果需要配合注释 可使用 as -g 选项 ,objdump -S 选项
#阅读更为方便

下面为源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pg_dir:
#注意这个标号,实际上代表着地址为0 的内存,下文中建立页目录的时候会用到。
.globl startup_32
startup_32:
movl $0x10, %eax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
lss stack_start, %esp

#前面部分使用设置段寄存器中选择子为 0x10,即使用 GDT 中索引号为 2 的描述符(其实就是之前定义第二个段描述符)
#stack_start 符号并没有定义 head.s 中,而是定义在 kernel/sched.c 中,见以下代码
#lss 指令为低32位加载到 esp
#高16位加载到 SS
#最终生成的机器码为反汇编为 ds:0x000252c0
#这里也了产生疑问,如何安排能够找到 kernel/sched.c 的符号的?
#下面也可以看到 stack_start 对应结构体如下
1
2
3
4
5
6
struct {         
long * a;
short b;
} stack_start = { & user_stack [PAGE_SIZE>>2] , 0x10 };
// long 4 字节 的指针
// short 2 字节

疑问中。

1
2
call setup_idt          # 设置中断描述符表
call setup_gdt # 设置全局描述符表

查看setup_idt 例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
setup_idt:
lea ignore_int, %edx
movl $0x00080000, %eax
movw %dx, %ax
movw $0x8E00, %dx


lea idt, %edi # idt是中断描述符表的地址
mov $256, %ecx

#实际上这里在构造 中断门描述符
#首先将 标号 ignore_int 处的偏移地址放入 EDX
#0x00080000 置入 EAX
#将偏移地址的低16位置入 AX,现在EAX 的值为 0x0008|偏移地址低16位(中断门描述符低32位)
#目标代码段选择子为 0x08 ,描述符索引为 1 ,即为 前面定义的代码段
#0x8E00置入 DX ,现在EDX 的值为 偏移地址高16位|0x8e00(中断门描述符高32位)
rp_sidt:
movl %eax, (%edi)
movl %edx, 4(%edi)
addl $8, %edi
dec %ecx
jne rp_sidt
lidt idt_descr # 加载中断描述符表寄存器值
ret
#将中断门描述符填入中断描述符表所在的内存
#填入中断描述符表,一共256次
#最后 ret 至调用处
#最后的结果是所有的中断描述符最终都指向 ignore_int 例程
......
.align 4
ignore_int:
pushl %eax
pushl %ecx
#这部分略
......
.align 8
#对齐为8字节
idt: .fill 256, 8, 0 # idt is uninitialized
#该指令格式为 .fill repeat , size , value #重复次数,重复的大小,重复的值
#即这里为重复256次的 0x00,每个0x00 8字节
#这里为中断描述符表预留空间
......
.word 0
idt_descr:
.word 256 * 8 - 1 # idt contains 256 entries
.long idt
#前 2 字节是描述符的段界限(段长度-1)
#后 4 字节是中断描述符表的内存地址
1
b 0x19

实际上 中断门描述符表的 256 项均已设置,这里仅展示前2个。所以的中断门描述符指向同一中断。

查看setup_gdt 例程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
setup_gdt:
lgdt gdt_descr
ret
......
.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 :^)
......
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 */
#很容易猜到 .quad 是伪指令 声明了 8字节(64位)
#定义了4个描述符
#第一个描述符为空(null)
#----------------------------------------------------------------
#第二个描述符(索引号为 0x08)
#段基地址 0x0000 0000
#粒度为 1 段界限单位为 4KB 默认操作数大小为 32位
# TYPE 1010 代码段 可执行、可读
#段界限为 0x0 0fff 实际使用的段界限为 0xFF FFFF 实际上这段空间即为 16MB
#----------------------------------------------------------------
#第三个描述符(索引号为 0x10)
#段基地址 0x0000 0000
#粒度为 1 段界限单位为 4KB 默认操作数大小为 32位
#TYPE 0010 数据段:可读、可写
#段界限为 0x0 0fff 实际使用的段界限为 0xFF FFFF 实际上这段空间即为 16MB
#---------------------------------------------------------------
#第四个描述符(索引号为 0x18) 为空
#其余 252 个描述符使用 0 填充。为其余的段描述符预留空间

经过例程setup_idt setup_gdt 重新设置了中断描述符表和全局描述符表,虽然段寄存器的选择子目前指向正确,但是描述符高速缓存器加载的是 setup.s执行时设置段描述符,需要重新加载选择子,刷新描述符高速缓存出去,使其指向正确的地址。

为什么要重新设置 GDT ?

原来的 GDT 指向的是 setup.s中在内存中的位置,这个位置现在被system模块覆盖,所以需要重新设置GDT 。

1
2
3
4
5
6
7
8
9
10
11
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
#这里的注释说 CS 在setup_gdt 加载过了
#从前面可以看到,例程 setup_gdt 其实非常简单 ,并没有涉及 CS 的加载。
#笔者认为这里是个错误,但是不影响接下来的执行。
#其余的指令为使用 0x10 加载到各个段寄存器,刷新描述符高速缓存器
#最后的 stack_start 和前面同理

测试 A20 gate 是否开启:

1
2
3
4
5
6
7
8
    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
#向 EAX 中写入 1,然后查看地址 0x0:0x100000 处是否也是相同的值
#如果相同,表示A20 gate 未开启,产生了地址回绕,无法访问 1MB 以上的内存
#如果不同,则表示 A20 gate 正确开启

检查是否存在x87协处理器:

协处理器 、 FPU 、浮点运算单元 都是同一个东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
    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
#这里的做法是,假设存在协处理器,执行协处理器指令,检查标志位,如果出错则表示协处理器不存在。
#和 0x80000011 进行与操作,位 31 、 位 1 、位 0 将置1 ,这几位分别代表PG(分页)、ET( 为 1 表示系统有 80387协处理器,为 0 表示使用 80287 协处理器)、PE(保护模式)
#注意,是与操作,如果 CR0 中本来未开启,则不会置1
#将位1 置 1 (MP)
#设置 CR0 的 PG(分页)、ET( 80387协处理器位)、PE(保护模式)
#注释,是与操作,如果 CRO 对应的位不存在,则不会置1 。
#最后设置 MP 位(位1 的 协处理器存在位)

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
#fninit 指令 为初始化协处理器指令,其余作用略,重点是将清除状态字(全部为0)
# fstsw %ax 指令将状态字存储到 AX中
#如果状态字为 0 ,表示存在 X87 协处理器 ,往下jmp 至 标号 1处
#如果状态字为其他,表示不存在 x97 协处理器
#将 CR0 EM(位2,协处理器仿真位)置 1、MP(位1 ,协处理器存在标志位) 置0 。

.align 4
1: .byte 0xDB,0xE4 /* fsetpm for 287, ignored by 387 */
ret
#DBE4 实际上是 fsetpm 指令的机器码,该指令为协处理器的指令
#该指令为 设置 80287协处理器 在保护模式下,80387 协处理器将该指令视为空指令

开始准备初始化页目录表和页表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.org 0x1000		
pg0:

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000
.org 0x5000
tmp_floppy_area:
.fill 1024,1,0
#1024 次 1字节,填充内容为 0x0
# org 指令表示该指令的起始地址为 0x5000
#这几个标号留待下文使用
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
jmp setup_paging #
L6:
jmp L6 # main should never return here, but
# just in case, we know what happens.
#push 了 5个双字到栈中
#前三个为 0x0000 0000
#这里连续 push 了三次 0x00
#为了模拟 main 函数的 envp、argv 指针和 argc 参数
#最后push L6 ,实际上不允许 main ret ,如果 ret 回来将陷入 L6的死循环。

开始创建分页机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
.align 4
setup_paging:
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

#从 0x0000 0000 到 0x0000 5000(不包括最后一个地址) ,填充 1024*5 次 的 0x0000 0000
#注意下面的标号
#这段空间大概为 20KB,这段空间的第一个 4KB 用作页目录表,后 16 KB 用作页表。
#为什么能够安全填充前 20KB ?
#前面的tmp_floppy_ar 的起始地址就为 0x5000 ,也就是tmp_floppy_area 后面的代码实际上都在 地址 0000 之后,所以才能够随意的填充(当前也会覆盖前面的代码)
#16KB 的页表项能够映射多少内存空间?
#每个页表项占 4 个byte(字节) ,即最多有 4096 页表项,每个页表项表示的物理内存是 4kb
#也就是说最多 16 KB 的页表项最多能寻址 16 MB 的内存。

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 /* --------- " " --------- */
#pg_dir其实为就是 head.s 开始的第一行
#创建了4个页目录项 ,为什么都加了7?原因是低12位为页目录项的一些属性位
#这里页目录属性为7 ,表示 页存在、可读可写、允许所有特权级别的程序访问。

movl $pg3 + 4092, %edi
movl $0xfff007, %eax
std
1: stosl
subl $0x1000, %eax
jge 1b
cld
#std 设置增长方向为负,开始逆序填写页表
#$pg3 + 4092 是第4个页表的最后一页表项,对应的值为 0xfff007 ,实际上该地址是16 MB 的最后一个4kb起始地址
#每次填写的页表项 -0x1000 ,实际为 4kb的内存空间
#最终将16KB 的页表项均填写完成,设置增长方向为正。


xorl %eax, %eax /* pg_dir is at 0x0000 */
movl %eax, %cr3 /* cr3 - page directory start */
#开启页机制,CR3 控制寄存器中 存放着当前任务页目录的物理地址(我这里提到了任务,请忽略,后面会解释)
#当前页目录的的物理地址为 0x0000 0000
#强调,CR3 控制寄存器、页目录项、页表项存放的都是物理地址(忽略页属性)

movl %cr0, %eax
orl $0x80000000, %eax
movl %eax, %cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
#将CR0 的位 31 置1 ,开始分页模式
#注意,开启即生效,此时 段部件合成的地址为 线性地址
#线性地址经过页部件合成之后才是 物理地址

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
b 0x54a5
#该位置实际上 movl %eax, %cr0 在执行之后
#此时CS 段寄存器的描述符缓存器的段基地址为 0x0000 0000
#IP 寄存器的值为 0x000054a8
#段部件合成的线性地址为 0x000054a8
#该地址分为三部分 高10位、中间10位、低12位
#0x00 、0x05、0x4a8
#0x00 * 4 = 0x00 对应的页目录项物理地址为 0x0000 0000 + 0x00 = 0x0000 0000
#0x05 * 4 = 0x14 对应的页表项 0x0000 1000 + 0x14 = 0x0000 1014
#0x5000 + 0x4a8 = 0x54a8
#最终页部件得到的物理地址和段部件得到的线性地址相同
#这是有意设计的
#前面的页表安排使得线性地址和物理地址相同

最后是一个ret结尾,前面pushl $mainmain符号的偏移地址 push到栈中,这里到ret将该地址popip中。至此,head.s结束,控制转移给 init/main.c中的main函数。

一些汇编指令的解释

关于 .align

1
2
3
4
5
#tmp.asm
.data
.byte 1
.byte 0x11
#只有 .data 段有数据,其他为空
1
2
as --32 -o a.o tmp.asm
objdump -s -j .text a.o

可以看到 0x11 紧接着 0x010x01的地址为 0x000x11的对称为0x11

使用伪指令:

1
2
3
4
5
#tmp.asm
.data
.byte 1
.align 2
.byte 0x11

可以看到0x11的偏移为 0x02

1
2
3
4
5
#tmp.asm
.data
.byte 1
.align 4
.byte 0x11

可以看到0x11的偏移为 0x04

1
2
3
4
5
#tmp.asm
.data
.byte 1
.align 16
.byte 0x11

可以看到0x11的偏移为 0x10

.align n下一个数据起始地址为地址%n=0的地址处。

小结

总结一下 目前 head.s完成的功能:

  • 设置 IDT 、GDT;
  • 测试 A20 gate ;
  • 检查是否存在 x87 协处理器;
  • 开启分页机制,包括创建页目录表,创建页表。
  • 最后控制转移至 init/main.c处。

总结下目前的疑问

  • 这里使用 main符号,实现了 汇编和 C 的相互调用,明天在看下这部分;
  • 汇编调用了 C 中的结构体stack_start,需要理解这里的栈式如何确定的;
  • bootsect对设备号的疑问仍然存在;
  • 如何精确的安排hdad.s在system 模块头部,或者说如何编译组织的 system模块。

参考

LInux 内核 完全注释

x86汇编语言:从实模式到保护模式(有点忘了,又去翻书)