1.3.5 head.s开始执行(6)

来源:互联网 发布:js object 遍历 值 编辑:程序博客网 时间:2024/04/18 09:07

1.3.5 head.s开始执行(6)

执行代码如下:

  1. //代码路径:boot/head.s  
  2.     xorl %eax,%eax      /* pg_dir is at 0x0000 */  
  3.     movl %eax,%cr3  /* cr3-page directory start */  
  4.     movl %cr0,%eax  
  5.     orl $0x80000000,%eax  
  6.     movl %eax,%cr0      /* set paging (PG) bit */ 

前两行的动作是将CR3指向页目录表,意味着操作系统认定0x0000这个位置就是页目录表的起始位置,后3行的动作是启动分页机制开关PG标志置位,以启用分页寻址模式,两个动作一气呵成。到这里为止,内核的分页机制构建完毕,后续章节还会讲解如何建立用户进程的分页机制。

最重要的是

  1. xorl %eax,%eax  /* pg_dir is at 0x0000 */ 

这一行代码,它看似简单,但用意深远。回过头来看,图1-17将system模块移动到0x00000,图1-25在内存的起始位置建立内核分页机制,最后就是上面的这行代码,认定页目录表在内存的起始位置,三个动作联合起来为操作系统中最重要的目的—内核控制用户程序奠定了基础,这个位置是内核通过分页机制能够实现线性地址等于物理地址的唯一起始位置。我们会在后续章节逐层展开讨论。

head程序执行最后一步:ret。跳入main函数程序执行。

在图1-35中,main函数的入口地址被压入了栈顶,现在执行ret了,正好将压入的main函数的执行入口地址弹出给EIP。图1-43中的下方标示了出栈动作。

 图1-43 执行ret,将main函数入口地址pop给EIP

这部分代码用了底层代码才会使用的技巧,我们结合图1-44对这个技巧进行详细讲解。

我们先看看普通函数的调用和返回方法,因为Linux 0.11 用返回方法调用main函数,返回位置和main函数的入口在同一段内,我们只讲解段内调用和返回。见图1-44(仿CALL示意图)的上半部分,CALL的调用与返回。

CALL指令会将EIP的值自动压栈,保护返回现场,然后执行被调函数的程序。等到执行被调函数的ret指令时,自动出栈给EIP并还原现场,继续执行CALL的下一行指令。这是通常的函数调用方法。对操作系统的main来说,这个方法就有些怪异了。main函数是操作系统的,如果用CALL调用操作系统的main函数,那么ret时返回给谁呢?难道还有一个更底层的系统程序接收操作系统的返回吗?操作系统已经是最底层的系统了,所以逻辑上不成立。那么如何既调用了操作系统的main函数,又不需要返回呢?操作系统的设计者采用了图1-44(仿CALL示意图)的下半部分所示的方法。

这个方法的妙处在于是用ret实现的调用操作系统的main函数,既然是ret调用,当然就不需要再用ret了。不过,CALL做的压栈和跳转的动作谁来完成呢?操作系统的设计者做了一个仿CALL的动作,手工编写压栈和跳转代码,模仿了CALL的全部动作,实现了调用setup_paging函数。注意,压栈的EIP值并不是调用setup_paging函数的下一行指令的地址,而是操作系统的main函数的执行入口地址_main。这样,当setup_paging函数执行到ret时,从栈中将操作系统的main函数的执行入口地址_main自动出栈给EIP,EIP指向main函数的入口地址,实现了用返回指令“调用”main函数。

 图1-44 仿CALL示意图在图1-44中,将压入的main函数的执行入口地址弹出给CS:EIP,这句话等价于CPU开始执行main函数程序。图1-45在左下方标示了这个状态。 图1-45 开始执行main函数

点评

为什么没有最先调用main函数?

学过C语言的都知道,用C语言设计的程序都有一个main函数,而且是从main函数开始执行的。Linux 0.11的代码是用C语言编写的,奇怪的是,为什么在操作系统启动时先执行的是三个由汇编写成的程序,然后才开始执行main函数?为什么不是像我们熟知的C程序那样,从main函数开始执行呢?

通常,我们用C语言编写的程序都是用户应用程序,这类程序的执行有一个重要的特征,就是必须在操作系统的平台上执行,也就是说要由操作系统为应用程序创建进程,并把应用程序的可执行代码从硬盘加载到内存。现在我们讨论的是操作系统,不是普通的应用程序,这样就出现了一个问题,应用程序是由操作系统加载的,操作系统该由谁加载呢?

从前面的章节中我们知道,加载操作系统的时候,计算机刚刚加电,只有BIOS程序在运行,而且此时计算机处于16位实模式状态下,通过BIOS程序自身的代码形成的16位的中断向量表及相关的16位的中断服务程序,将操作系统在软盘上的第一扇区(512字节)的代码加载到内存,BIOS能主动操作的内容也就到此为止了。准确地说,这是一个约定,对于第一扇区代码的加载,不论是什么操作系统都是一样的。从第二扇区开始,就要由第一个扇区中的代码来完成后续的代码加载工作。

在加载工作完成后,好像仍然没有立即执行main函数,而是打开A20,打开pe和pg,建立IDT、GDT……然后才开始执行main函数,这是为什么?

原因是,Linux 0.11是一个32位的实时多任务的现代操作系统,main函数肯定要执行的是32位的代码,编译操作系统代码时,是有16位和32位两个不同的编译选项的,如果选了16位,C语言编译出来的代码是16位模式的,结果可能是一个int型变量只有2个字节,而不是32位的4个字节……这不是Linux 0.11想要的,Linux 0.11要的是32位的编译结果,只有这样才能成为32位的操作系统代码,这样的代码才能用到32位总线(即打开A20后的总线),才能用到保护模式和分页,才能成为32位的实时多任务的现代操作系统。

开机时的16位实模式与main函数执行需要的32位保护模式之间有很大的差距,这个差距谁来填补? head.s做的就是这项工作,这期间,head程序打开A20,打开pe和pg,废弃旧的、16位的中断响应机制,建立新的32位的IDT……这些工作都做完了,计算机已经处在32位的保护模式状态了,调用32位main函数的一切条件已经准备完毕,这时可顺理成章地调用main函数,后面的操作就可以用32位编译的main函数完成。

至此,Linux 0.11操作系统内核启动的一个重要阶段已经完成,接下来就要进入main函数对应的代码了。

需要特别提示的是,此时仍处在关闭中断的状态!

0 0
原创粉丝点击