购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.1.1 设置页表

在32位的操作系统上,Linux只需要使用二级页表(即页目录表和页表)就可以支持4GB地址空间寻址。所以,接下来就可以将页目录表放在物理地址0处,它的大小是4KB,共包含1024个页目录项,每个页目录项的大小为4B。

因为setup模块已经将内核代码搬到物理地址0处,所以代表页目录表的pd_dir就可以放到head.S代码的开始处。设置页表的代码如代码清单3-1所示。

代码清单3-1 设置页表(head.S)

1  .code32

2  .text

3  .globl startup_32,idt,gdt,pg_dir,tmp_floppy_area

4  pg_dir:

5  startup_32:

6    movl $0x10, %eax

7    ...

8

9    xorl %eax, %eax

10 1:

11   incl %eax

12   movl %eax,0x000000

13   cmpl %eax,0x100000

14   je 1b

15

16   jmp after_page_tables

17

18 setup_idt:

19   leal ignore_int,%edx

20   ...

21

22 setup_gdt:

23   lgdt gdt_descr

24   ret

25

26 .org 0x1000

27 pg0:

28

29 .org 0x2000

30 pg1:

31

32 .org 0x3000

33 pg2:

34

35 .org 0x4000

36 pg3:

37

38.org 0x5000

39

40 tmp_floppy_area:

41 .fill 1024,1,0

42

43 after_page_tables:

44 /* 我们可以在此处跳转至main函数

45   pushl $0

46   pushl $0

47   pushl $0

48   pushl $L6

49   pushl $main

50   mp setup_paging

51 L6:

52   jmp L6

53   ...

54

55 .align 4

56 setup_paging:

57   movl $1024 *5,%ecx

58   xorl %eax, %eax

59   xorl %edi,%edi

60   cld

61   rep

62   stosl

63

64   movl $pg0+7,pg_dir

65   movl $pg1+7,pg_dir+4

66   movl $pg2+7,pg_dir+8

67   movl $pg3+7,pg_dir+12

68   movl $pg3+4092,%edi

69   movl $0xfff007, %eax

70   std 711:

72   stosl

73   subl $0x1000, %eax

74   jge1b

75   xorl %eax, %eax

76   movl %eax,%cr3

77   movl %cr0, %eax

78   orl$0x80000000, %eax

79   movl %eax,%cr0

80

81   ret

注意,bootsect中已经使用过上述代码的.org伪指令,该指令可以规定程序的起始地址,例如pg0就被强制放在了文件开头偏移0x1000的位置,这就相当于在二进制文件中预留下了页目录表和页表的空间。bootsect将head模块加载进内存以后,setup模块还会进一步将它移到物理内存的0的位置。移动以后的内存布局如图3-1所示。

图3-1 内存布局

实际上,在图3-1中,pg_dir处不仅有页目录项,还有很多代码,即startup_32处的那些代码。这些代码会先执行,当它们执行完以后就跳到after_page_tables处继续执行。跳转以后,startup_32处的代码就再也不会被使用了,这时就可以设置页目录表,将这部分内存全部重写。

第3行将gdt、idt、pg_dir等符号导出,以让内核的其他模块可以访问这些符号。第4行,即head.S的开头处,定义了pg_dir,前面已经介绍过了,这是定义页目录表的位置。在第26行,.org伪指令为页目录表预留了4KB(0x1000)的空间,pg0定义了第一张页表,第29行的伪指令也是相同的作用,它为pg0页表预留了4KB空间。后面的pg1、pg2、pg3的定义与pg0是相同的,这里不再赘述。

第40行的代码又定义了一个名为tmp_floppy_area,这个空间是为虚拟软盘准备的,这里先不介绍,等后文讲解到软盘的输入/输出实现时再加以介绍。

在这些数据结构定义之后,才是设置页表的代码。这些代码必须放在tmp_floppy_area之后,否则就会被新的页目录表覆盖。这段代码的开始处以after_page_tables进行标识。第45~49行代码往栈上存放了一个名为main的符号。这个动作的作用稍后讲解,我们还是先把设置页表的动作讲解完。

setup_paging一开始先把从0~5KB这一段物理内存全部清零(第57~62行),从这里开始,head.S的部分代码就会永远地从内存中擦除了。第64~67行代码负责将前4个页表,即pg0~pg3的地址填入相应的页目录项。这里的地址加上7表示页目录项的低三位都是1,参考第2章中介绍的页目录项的结构(参考图2-6),可以知道,这代表页目录项所对应的页表是用户态可读写,并且在内存中存在。

第68行至第74行代码使用了一个循环,将0x0~0xfff000的物理内存的地址填入页表。因为每个物理页的大小是4KB(0x1000),所以在每一次循环中,地址的值都会减掉0x1000。stosl指令的作用是将eax寄存器中的值保存到edi指向的地址中,若eflags中的方向已置位(即在stosl指令前使用std指令),则edi自减4。所以这里的循环会把16MB(0xfff000)物理内存的地址都设置在页表中,共需要4096个页表项,刚好占据4个页表。这4个页表的最后位置是pg3+4092,所以将edi的初始值设成页表的末尾地址,然后每一次循环设置一个页表项,之后edi减4,直到地址0被填入pg0的第一页,循环就会结束(第71~74行)。

最后,把页目录表的物理地址(也就是0),送入cr3寄存器(第75~76行),然后把cr0的最高位置为1,代表打开保护模式的分页机制(第77~79行)。至此,页表的设置就全部完成了。

在第81行有一个ret指令。你可能会有这样的疑惑:我们明明没有使用call指令进行控制流的转移,为什么要使用ret指令呢?这个ret指令又“返回”到哪里执行呢?实际上,call/ret指令并不是一定要成对出现的。call指令的作用是把返回地址放到栈上,然后转移到目标地址执行。而ret指令的作用是把返回地址从栈上取出,然后跳转到返回地址继续执行。可见,只要把返回地址放到栈上,然后执行ret指令就可以让CPU转移到目标地址执行,而把地址放到栈上这个操作并一定非得使用call指令才能实现,使用push指令也可以实现同样的功能。第49行代码将main这个符号放到栈上,所以当CPU执行第81行的ret指令时,就会跳转到main函数的入口执行。45~48行是为main函数准备的参数和返回地址,实际上这些值不会再起作用,可以忽略。这里当然也可以使用call指令或者jmp指令进行控制流的转移。但以后内核的代码还会使用iret进行特权级的转换,这里为了统一,就都使用ret指令了。

而main这个符号位于main.c中,main.c的代码如下所示:

1 #define__LIBRARY__

2

3 void main(void)

4 {

5   __asm__("int $0x80\n\r"::);

6   __asm__ __volatile__(

7      "loop:\n\r"

8      "jmp loop"

9      ::);

10 }

main函数里包含了两段内嵌汇编代码,第5行的汇编代码触发了一个0x80号中断,实际上第6行的汇编代码开启了一个死循环,让main函数不会结束。第6行换成while语句或者for语句效果是一样的,读者可以自己动手尝试一下。同时,makefile文件中也要添加对main.c文件的编译支持:

1  GCC:=gcc

2  CCFLAG:=-I../include-nostdinc-Wall-fomit-frame-pointer-c

3  LDFLAG:=-Ttext 0x0-s--oformat binary-m elf_x86_64

4  INCDIR:=../include

5  OBJS:=head.o main.o

6

7  system: $(OBJS)

8   $(LD) $(LDFLAG)-e startup_32-o$@ $^

9

10 head.o:head.S

11   $(GCC)-traditional-c-o$@ $<

12

13 main.o:main.c

14   $(GCC) $(CCFLAG)-o$@ $<

这种方式可以把head.S和main.c两个文件链接在一起,形成了新的内核文件。相比第2章,这里把“int 0x80”指令从head.S移到了main函数里,这说明操作系统确实进入main函数执行了。最后运行的结果虽然与第2章结尾时是一样的,但现在已经利用C语言里编程了。 WCQ9GQ8mpNp9HvVrwhsTSLizp6kYj6zyPDTwUUA80ZghGYXFwJAaJ3tfc2Xzlawt

点击中间区域
呼出菜单
上一章
目录
下一章
×