Hello China V1.75基于PC的版本的开发环境是Microsoft Visual C++。之所以使用Windows操作系统和VC开发环境,是考虑到相比Linux和GCC等开发工具来说,这个组合有更加广泛的使用群体,可以让更多的人参与开发。但是如果不做一番特殊处理,Visual C++是不适合操作系统开发的,主要原因有下列几点:
1.缺省情况下,VC生成的目标文件的入口地址不固定
一般情况下,Windows开发工具都是以WinMain或main函数为入口的,应用程序模块被OS加载以后,会直接跳转到这个入口点开始执行。如果编译器严格按照这种规则指定入口点,那么对于OS的开发是合适的。但是通常情况下,编译器却不是这样做的,而是在调用WinMain或main函数前,先调用其他的一些初始化函数,比如C运行库的初始化函数、C++对象的构造函数等,等这些初始化工作准备好了,再调用WinMain或main函数。这样的结果就是,一个可执行模块的入口地址是不可见的,这不适合OS的开发,因为OS的开发过程中,需要严格地知道每个模块的入口点是什么,这样才能控制程序的行为。
为了解决这个问题,可以通过编译工具提供的编译选项,来手工指定模块的入口点。在后面的叙述中,会说明如何改变模块的缺省入口地址。
2.缺省情况下,生成的目标文件的缺省加载地址不符合要求
Windows的RAD开发工具生成的目标文件一般是基于PE格式的可执行文件或动态链接库(DLL),这两种文件在链接的时候,都指定了一个缺省的加载地址,一般进程地址空间的4MB偏移处。由于Windows操作系统使用了虚拟内存技术,每个进程都独占4GB的线性空间,因此PE格式的文件缺省加载地址不论是多少,操作系统在加载这些PE模块的时候,都可以不做任何修改地加载到指定的地址。而我们的操作系统开发过程中,对每个模块的加载地址都是有严格限制的,比如,一个模块,在我们自己开发的OS中,加载地址应该是1MB,而这个模块如果按照缺省设置,则可能按照4MB加载地址进行链接,这样就会产生问题。
为了进一步说明这个问题,举一个简单的例子,下面是一段简单的C语言代码。
如果按照4MB加载地址,翻译成汇编语言以后,是如下格式。
可以看出,对ulOsVersion的一个赋值操作引用的地址是4MB(假设编译器把全局变量ulOsVersion放到了模块的开始处)。
而如果我们的操作系统要求这个模块被加载到1MB开始处,那么对ulOsVersion的引用,应该是下面的样子。
可以看出,与编译器缺省情况下的结果不一致,这在实际的系统中,是无法正常工作的。
为了解决这个问题,也可以通过设置编译器的编译链接选项来消除这个矛盾。后面会说明如何消除这种矛盾。
3.VC生成的目标文件,增加了一个PE文件头
VC生成的可执行二进制模块,在文件的开始处增加了一个PE文件头,而这个头的长度是可变的,在这个头中的特定偏移处指定了这个模块的入口地址。Windows加载器在加载这些模块的时候,根据PE头来找到入口地址,然后跳转到入口地址去执行。
而在我们的OS开发中,如果再进行这样的处理,可能就比较麻烦,有的情况下甚至是不可能的,我们OS开发的要求是直接找到模块的入口地址,通过一条跳转指令跳转到该地址开始执行。
为了解决这个问题,我们可以通过对目标文件进行修改的方式来解决。比如,单独开发一个工具软件,这个工具软件读入目标文件的头,找出目标文件的入口所在位置,然后在文件的开始处加上一条跳转指令,跳转到入口处即可。这样处理之后,在我们OS核心模块的加载过程中,只要把这个模块读入内存,并直接跳转到开始处执行即可。
为了进一步说明这个过程,考虑图C-1所示的示例。
这是一个目标模块的示例,其中文件头的长度是可变的,在文件头中的一个字段中,指明了入口地址的位置(相对于文件头的偏移量)。(www.xing528.com)
在我们的OS开发中,理想的目标是直接跳转到入口地址处开始执行。但由于文件头长度是可变的,无法确定入口地址的位置,因此,在这种情况下,可以通过软件的手段,把目标文件的开头8个字节修改成下列形式。
上述几个字节对应的汇编代码就是:
其中xx xx就是入口地址的偏移量(准确地说,应该是入口地址的偏移量减8,因为文件头距离JMP指令后一条指令的偏移是8),如图C-2所示。
图C-1 一个二进制目标模块
图C-2 处理后的二进制可执行模块
这样,在我们的代码中,把这个目标模块直接加载到内存,然后跳转到模块的第一个字节执行即可。由于模块的开头部分被我们修改,因此会间接地跳转到入口地址处开始执行。process工具就是为此而开发的,可以用这个工具来修改PE文件,使得PE文件可以直接被加载并执行。
4.目标模块加载到内存后,需要经过处理才能运行
PE格式的目标文件是按照节来组织的。比如,对模块中的代码组织到TEXT节中,对于初始化的全局变量组织到DATA节中,对于只读变量组织到RDATA节中,按照节组织好以后,然后节与节联合起来,就组成了整个文件。在PE文件头中,附加了一个节描述结构,用来描述每个节的位置、大小等属性。
问题的关键在于在磁盘上存储目标模块的时候,节与节之间的间隔一般很小,比如按16字节对齐,但加载到内存之后,节与节之间却以4KB为边界对齐。这样就产生了一个问题,把文件直接读入内存是无法直接运行的,需要根据PE文件头来适当地调整节与节之间在内存中的对应关系,然后才能运行。
在OS的开发当中,我们要求文件在内存中的映像应该与在磁盘上的映像一致,这样只要把磁盘上的文件加载到内存即可,不需要经过处理。尤其是在操作系统开发初期还没有一个成型的加载器的情况下。为了避免这个矛盾,可以通过编译器选项来告诉编译器修改这种默认行为。
在Microsoft Visual C++中,提供了一个连接选项/align,这个选项告诉编译器节与节之间的对齐方式(在内存中的对齐方式),我们把这个选项设置为16(/ALIGN 16),就可以使内存中的对齐方式与硬盘中的对齐方式一致,方便目标模块的直接运行。
另外,对于源程序中出现的未初始化的全局变量,连接程序把它们组织在.BSS节中,而这个节在硬盘中是不分配空间的,只有当加载到内存之后才根据PE头中的信息为这个节分配空间。比如,图C-3是一个PE格式的文件在磁盘中的存储格式和在内存中的映像关系。
可以看出,在内存中,比在磁盘介质中的格式多出一个节(.BSS)。
图C-3 PE文件的存储布局和内存布局
在我们的OS开发中,尤其是开发初期,没有一个完善的加载器的情况下,是无法处理这种情况的。为了避免这种情况的出现,建议在程序编码的时候,对于全局变量都进行初始化处理,这样就可以避免.BSS节的出现。
总之,由于以上原因,缺省的Visual C++的编译选项是不适合操作系统开发的,必须进行修改。下面就详细讲解开发工具的设置方法。
免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。