前言
在进入保护模式后,段的访问是基于段的信息描述符表中定义的描述符,但是操作系统是怎么知道任意一个加载程序的段的信息呢?所以有必要探讨这个问题。
1.操作系统是怎么加载用户程序,然后转移到用户程序去执行;
2.操作系统怎么提供大量的例程供用户程序使用,比如显示字符。
当然在主流的操作系统中肯定与书中提到的有所区别,但通过这个过程去认识普通程序的加载过程,对理解现代操作系统肯定是有所帮助的。
主引导程序
主引导程序负责把内核加载到内存中,因为此时已经启动保护模式,内存的访问都要通过描述符(描述符要加载进段寄存器高速缓存),因此在进入保护模式之前,全局描述表就要建立好。主引导程序中建立全局描述符表,对初始数据段、代码段、栈段以及显示缓存段的段基地址、界限及其类型等进行定义。
之后便可加载内核。
安装内核的段描述符
通过数据段访问mov $pgdt, %esi
获得全局描述符表的基地址(因为数据段是设置成了可读的),以便创建与内核相关的其他段描述符,在内核程序的头部已经给出内核各段在内核文件中的偏移地址,段界限可以通过相邻段的偏移差值再减1得到,而段的类型直接给出。将新的描述符加到全局描述符表中,并修改表的界限值。
通过重新加载GDTR,使对GDT的修改生效。
通过内核程序的头部中的内核入口点便可进入内核执行。
用户程序的加载和执行
在前面内核头部提供很多信息,才顺利被主引导程序加载执行,所以用户程序要想顺利被加载也要符合内核的要求。在这里用户程序的头部包括:
- 头部的长度,以字节为单位;
- 堆栈选择子;
内核不要求用户程序提供堆栈空间,而改由内核动态分配,以减轻用户程序编写的负担。当内核分配了堆栈空间后,会把堆栈段的选择子填写到这里,用户程序开始执行时,可以从这里取得该选择子以初始化自己的堆栈。 - 要求分配的堆栈大小;
- 用户程序入口点的 32 位偏移地址;
- 用户程序代码段的起始汇编地址;
当内核完成对用户程序的加载和重定位后,将把该段的选择子回填到这里(仅占用低字部分)。这样一来,它和入口点的 32 位偏移地址一起,共同组成一个 6 字节的入口点,内核从这里转移控制到用户程序。 - 用户程序代码段的长度,以字节为单位;
- 用户程序数据段的起始汇编地址;
当内核完成用户程序的加载和重定位后,将把该段的选择子回填到这里(仅占用低字部分)。 - 用户程序数据段的长度;
段的重定位和描述符的创建
当用户程序全部读入内存后,要根据头部信息进行重定位。描述符的创建是根据头部信息如用户程序代码段的起始汇编地址,用户程序代码段的长度,之后来更新GDT的内容,重新加载GDTR使修改生效。再根据 GDT 的新界限值,来生成相应的段选择子,将该段的选择子写回到用户程序头部,供用户程序在接管处理器控制权之后使用。
重定位用户程序内的符号地址
用户程序调用了操作系统的服务例程,通过符号的形式进行标明。当用户程序加载后,内核应该根据这些符号名来回填它们对应在内核的入口地址,这称为符号地址的重定位。为了对用户程序内的符号名进行匹配,内核也必须建立一张符号-地址对照表。内核的符号-地址对照表提供偏移地址和段选择子。在用户程序加载时,内核的任务是比对这两者的表,并将用户程序表中的符号名替换成相应的入口地址。