分类:
2010-07-28 20:58:09
欢迎!
本系列文章和教程是关于计算机和操作系统的。主要作为开发操作系统过程中的一个指导,描述了系统级编程的体系结构和概念。
本系列的目标是提供最全面的操作系统和计算机系统的向导,并尝图涵盖它的每一个方面。
在开始本系列教程前,我感觉应该介绍一下我们选择的语言,以及哪些是读者需要知道的,语言中的一些重要概念和如何去使用它。我们也会涵盖只在嵌入式平台和系统级软件中使用的概念。
本系列使用C和X86汇编语言。在开始教程前应当对这些语言有一个比较好的了解。本章包含对这些语言的回顾。
C语言概述
首先假设您已经知道如何用C语言编程。这是一个对C语言重要部分的快速的概述,以及它能为我们做什么。
如何在内核中使用C语言16位和32位C在开始编写系统前你会发现没有什么东西能够帮助到我们。在开机时,系统是运行在32位编译器不支持的16位实模式下,这是第一个比较重要的事情:如果你想要创建一个16位实模式操作系统,你必须使用16位的C编译器。然而,如果你决定创建一个32位的操作系统,你就必须使用一个32位的C编译器。16位C与32位C代码并不兼容。
在本系列教程中,我们会创建一个32位的操作系统,因此,我们将使用32位的C编译器
C和可执行文件格式C语言的一个问题是它不支持输出扁平二进制程序。一个扁平二进制程序基本可以定义为入口点函数(比如main())始终在程序文件的第一个字节的程序。等等,什么?为什么要这样?
这应当追溯到DOS COM编程的美好日子。DOS COM程序是扁平二进制-这们既没有定义入口点与没有符号名称。要执行这些程序,所需要做的是“跳”到程序的第一个字节。扁平二进制程序没有特殊的内部格式,所以也就没有什么标准。它只是一堆1和0。当PC开机后,系统BIOS ROM获得控制权,它不知道该如何去启动一个操作系统。正因为如此,它运行另一个程序-引导程序去加载 加载操作系统。BIOS并不需要知道这个程序的文件内部格式和它做什么。因此,它将所有的引导程序看作是扁平二进制程序。不论引导磁盘的引导扇区是什么内容,它都会加载,并跳到那个程序的第一个字节。
因此,引导扇区的第一部分,也被称为引导代码或都第一阶段(Stage 1),它不能用C编写。这是因为所有的C编译器输出的程序文件有特殊的内部格式-它们可以是库文件、对象文件、或者是可执行程序。只有一种语言本身支持这点(可以编写汇引导代码)-汇编语言。
如何在引导程序中使用尽管引导程序第一部分必须用汇编是事实,但也可以用C语言。有许多不同的方法可以做到这样(用C语言)。一种是Windows和我们自己的操作Neptune使用的。我们把汇编代码与C语言代码结合到一个文件里。汇编代码负责启动系统和调用我们的C程序。因为这些程序结合到一个文件中,所以第一阶段只需要加载我们的包含汇编和C的程序文件。
这是一种方法 - 还有其它的。大部分引导程序是使用C的,包括GRUB、Neptune引导程序,微软的NTLDR和Boot Manager。因为我们使用32位C语言,也有方法把混合16位和32位C代码。
实现这个很复杂并且需要很多技巧。因此,我们坚持在引导程序系统中使用汇编。如果读者需要,我们可能会在后续高级教程部分描述如何使用C。
调用C内核
当引导程序准备好后,它通过调用我们的入口函数来加载我们的可执行的C内核。因为C程序有特殊的内部格式,引导程序必须知道如何解析内核文件和定位到入口点函数并调用它。本系统,稍微晚一些我们会涵盖如何做到这些。这就允许我们在内核中使用C和其它我们建立的库文件。
指针介绍
因为你以读到这里,我假设你已经善于使用指针。在系统软件中,指针到处都在使用。因此,掌握好指针是非常重要的。
指针只是保存地址的简单的变量。定义指针,我们使用*操作符:
char* pointer;
这里有一个例子,在编写应用程序时,如下例子会导致一个段故障错误,导致程序崩溃:
这里创建一个指向内存地址为0的指针(并不归你所有),因此,系统不允许你向其写入数据。char* pointer = 0; *pointer = 0;
现在,在我们未来的C内核中尝试一下...没有崩溃!而是覆盖了中断向量表(IVT)的第一个字节。
由此,我们可以总结一些重要的区别:如果试图从一个不存在的内存地址读取数据,会得到垃圾数据(当时系统数据总线上的数据)。尝试往一个不存在的地址写入数据什么也不会做。向一个不存在的内存地址写,并立即往回读,可以会得到与写入相同的结果,也可能得不到,这取决于所写的数据是否仍然在数据总线上。
事情变得越来越有趣了。ROM设备被映射到相同的PAS。这意味着使用指针可能会写/读ROM设备的部分。系统BIOS是一个比较好的ROM设备的例子。因为ROM设备是只读的,写一个ROM设备与写一个不存在的内存位置效果是相同的。然而,却可以从ROM设备读
其它设备可能也会被映射到PAS,取决于系统配置。这意味着读/写PAS不同部分可能产生不同类型的结果。
正如您所知道的,与编写应用程序相比,指针在系统编程中起到了更大的作用。可能更容易的想到指针不是作为“指向内存地址的变量”,而是“指向PAS地址的变量”,因为它可能是内存,也可能不是。
动态内存分配
在编写应用程序时中,正常会调用malloc()和free()或者new和delete申请堆内的一块内存。这与系统编程不同,申请内存,我们这些做:
这真是太酷了,Huh?因为我们能够控制任何东西,我们指定指针指向PAS的某一个地址(必须是RAM),并说“这是我们新的1024字节缓存”等。char* pointer = (char*)0x5000;
在这里重要的事情是没有动态内存分配。C/C++中的动态内存分配是需要操作系统提供的系统服务。但是,等等!我们不是正在开发我们自己的操作系统?这就是问题所在:)为了提供malloc()和free()或new和delete我们需要自己写内存管理服务和函数。
在那之前,唯一的方式是“分配”一个地址空间上没有使用的地址作为缓冲区。
内联汇编有一些事情C本身不能做。系统服务和与硬件会话我们需要使用汇编语言。
大部分编译器提供了内联汇编的关键字。比如,微软VC++使用_asm:
我们也可以使用汇编代码块:_asm cli ; disable interrupts
标准库和运行时库(RTL)_asm { cli hlt }
可以使用外部库 - 这些库函数没有使用系统服务,任何像printf(), scanf(), 内存函数, 或几乎一切,只有一小部分可以使,大约有90%函数需要重写,所以最好是写自己写。
RTL是应用程序在运行时使用的服务和函数的集合。从本质上讲,它们需要操作系统动行,并提供了这些服务。因此,需要开发自己的RTL
启动RTL负责调用C++构造和析构函数。如果想使用C++,必须开发支持这些的RTL代码,这使用到了编译器的扩展。
本系统中,我们了支持C/C++特性的RTL和基本的标准库。
修正错误调试因为没有printf和某种方式可以使用的调试器,当当工作时该如何办?本系统使用(解释)Bochs调试器 - Bochs模拟器带的调试器。这可以用于运行自己的操作系统,并可以用于协助修复遇到的大部分更常见的错误。
另外唯一的方法是开发自己的允许输出信息的函数,这是最有可能告诉你程序崩溃前走了多远。
下回分解本章到此为止:)下一章,我们开始到操作系统世界去冒险,看一看它们是什么,以及我们在整个系列中都会使用到的工具。
原文地址: