如何开发自己的引导加载程序






4.96/5 (277投票s)
本文通过开发简单的引导加载程序的例子,描述了低级编程的初步步骤。
目录
谁可能对此感兴趣
我写这篇文章主要是面向那些一直对事物运作方式感兴趣的人。它适用于那些通常使用 C、C++ 或 Java 等高级语言创建应用程序,但又面临开发底层需求的开发者。我们将以系统加载工作为例来探讨底层编程。
我们将描述您打开计算机后会发生什么;系统是如何加载的。作为实际示例,我们将考虑如何开发自己的引导加载程序,这实际上是系统启动过程的第一个环节。
什么是引导加载程序
引导加载程序是位于硬盘第一个扇区的程序;启动就是从这个扇区开始。BIOS 在刚开机时会自动将第一个扇区的所有内容读取到内存中,并跳转到它。第一个扇区也称为 主引导记录 (Master Boot Record)。事实上,硬盘的第一个扇区并非必须用于启动。这个名称是历史形成的,因为以前开发者通常用这种机制来启动他们的操作系统。
准备深入研究
在本节中,我将介绍开发自己的引导加载程序所需的知识和工具,并回顾一些关于系统启动的有用信息。
那么,开发引导加载程序需要了解什么语言?
在计算机工作的初始阶段,硬件的控制主要通过 BIOS 功能(称为中断)来实现。中断的实现仅用汇编语言编写——所以,如果您至少了解一点汇编语言,那将是极好的。但这并非必要条件。为什么?我们将使用“混合代码”技术,其中可以将高级结构与低级命令结合起来。这使得我们的任务稍微简单一些。
在本文中,主要开发语言是 C++。但是,如果您精通 C,那么学习所需的 C++ 元素会很容易。总的来说,即使只了解 C 也足够了,但那样您将不得不修改我将在此处描述的示例的源代码。
如果您精通 Java 或 C#,很遗憾,这对我们的任务没有帮助。原因在于,Java 和 C# 语言在编译后产生的代码是中间代码。需要使用特殊的虚拟机来处理它(Java 的 Java 虚拟机,C# 的 .NET 虚拟机),它将中间代码转换为处理器指令。转换完成后,就可以执行了。这种架构使得无法使用混合代码技术——而我们将使用它来使我们的工作更轻松,所以 Java 和 C# 在这里行不通。
因此,要开发简单的引导加载程序,您需要了解 C 或 C++,最好还了解一些汇编语言——这是所有高级代码最终都会被转换成的语言。
需要什么编译器?
要使用混合代码技术,您至少需要两个编译器:一个用于汇编语言,一个用于 C/C++,还需要一个链接器将目标文件 (.obj) 组合成一个可执行文件。
现在我们来谈谈特殊方面。处理器有两种运行模式:实模式和保护模式。实模式是 16 位的,有一些限制。保护模式是 32 位的,并且在操作系统中被充分使用。启动时,处理器以 16 位模式运行。因此,要构建程序并获得可执行文件,您需要一个适用于 16 位模式的编译器和汇编语言链接器。对于 C/C++,您只需要一个能够为 16 位模式创建目标文件的编译器。
现代编译器主要针对 32 位程序设计,所以我们无法使用它们。
我尝试了几款免费和商业的 16 位模式编译器,并选择了微软的产品。该编译器以及用于汇编语言、C 和 C++ 的链接器包含在 Microsoft Visual Studio 1.52 套件中,您也可以从公司官方网站下载。下面提供了一些关于我们所需编译器的详细信息。
ML 6.15–微软适用于 16 位模式的汇编语言编译器;
LINK 5.16–可创建 16 位模式 .com 文件的链接器;
CL–适用于 16 位模式的 C、C++ 编译器。
您也可以使用其他一些替代选项
DMC–Digital Mars 提供的适用于 16 位和 32 位模式的汇编语言、C、C++ 的免费编译器;
LINK–DMC 编译器的免费链接器。;
Borland 也有一些产品
BCC 3.5–可创建 16 位模式文件的 C、C++ 编译器;
TASM - 适用于 16 位模式的汇编语言编译器;
TLINK–可创建 16 位模式 .com 文件的链接器。
本文中的所有代码示例均使用微软工具创建。
系统是如何启动的
为了解决我们的问题,我们需要回顾一下系统是如何启动的。
让我们简要看一下系统组件在启动过程中是如何交互的(见图 1)。
图 1 - “系统启动过程”
当控制权转移到地址 0000:7C00 后,主引导记录 (MBR) 开始工作,并启动操作系统加载过程。您可以在此处了解更多关于 MBR 结构的信息。
动手编码
在接下来的章节中,我们将通过开发自己的引导加载程序直接进行底层编程。
程序架构
我们正在开发的引导加载程序仅用于学习目的。它的任务如下:
- 正确加载到内存地址 0000:7C00。
- 调用用高级语言开发的 BootMain 函数。
- 从底层在显示器上显示“Hello, world...”消息。
程序架构如图 2 所示,然后是文字描述。
图 2 - 程序架构描述
第一个实体是 StartPoint
,完全用汇编语言开发,因为高级语言没有必要的指令。它告诉编译器应使用哪种内存模型,以及从磁盘读取后应将加载到 RAM 的地址。它还会更正处理器寄存器并将控制权传递给用高级语言编写的 BootMain
。
下一个实体——BootMain
——相当于 main
,它是所有程序功能集中的主函数。
CDisplay
和 CString
类负责程序的功能部分,并在屏幕上显示消息。正如您从图 2 中看到的,CDisplay
类在其工作中使用了 CString
类。
开发环境
这里我使用的是标准的开发环境 Microsoft Visual Studio 2005 或 2008。您可以使用任何其他工具,但我确信这两个工具经过一些设置后,可以使编译和工作变得轻松便捷。
首先,我们应该创建一个 Makefile 项目类型,主要工作将在其中进行(见图 3)。
文件->新建\项目->常规\Makefile 项目
图 3 – 创建 Makefile 类型项目
BIOS 中断和屏幕清除
为了在屏幕上显示消息,我们首先应该清除屏幕。我们将为此目的使用特殊的 BIOS 中断。
BIOS 提供了许多用于操作计算机硬件(如视频适配器、键盘、磁盘系统)的中断。每个中断的结构如下:
int [number_of_interrupt];
其中 number_of_interrupt 是中断号。
每个中断都有一定的参数,在调用它之前应该设置好。ah
处理器寄存器始终负责当前中断的功能号,其他寄存器通常用于当前操作的其他参数。让我们看看在汇编语言中如何执行 int 10h
中断。我们将使用 00 功能来更改视频模式并清除屏幕。
mov al, 02h ; setting the graphical mode 80x25(text)
mov ah, 00h ; code of function of changing video mode
int 10h ; call interruption
我们将仅考虑我们应用程序将使用的中断和功能。我们将需要:
int 10h, function 00h – performs changing of video mode and clears screen;
int 10h, function 01h – sets the cursor type;
int 10h, function 13h – shows the string on the screen;
“混合代码”
C++ 编译器支持内置汇编语言,即在用高级语言编写代码时,您也可以使用低级语言。在高级代码中使用的汇编指令也称为 asm 插入。它们由关键字 __asm
和大括号内的汇编指令块组成。
__asm ; key word that shows the beginning of the asm insertion
{ ; block beginning
… ; some asm code
} ; end of the block
为了演示混合代码,让我们使用之前提到的执行屏幕清除的汇编代码,并将其与 C++ 代码结合起来。
void ClearScreen()
{
__asm
{
mov al, 02h ; setting the graphical mode 80x25(text)
mov ah, 00h ; code of function of changing video mode
int 10h ; call interrupt
}
}
CString
实现
CString
类用于处理字符串。它包含 Strlen()
方法,该方法以字符串指针作为参数,并返回该字符串中的字符数。
// CString.h
#ifndef __CSTRING__
#define __CSTRING__
#include "Types.h"
class CString
{
public:
static byte Strlen(
const char far* inStrSource
);
};
#endif // __CSTRING__
// CString.cpp
#include "CString.h"
byte CString::Strlen(
const char far* inStrSource
)
{
byte lenghtOfString = 0;
while(*inStrSource++ != '\0')
{
++lenghtOfString;
}
return lenghtOfString;
}
CDisplay
实现
CDisplay
类用于处理屏幕。它包含几个方法:
1) TextOut()
– 在屏幕上打印字符串。
2) ShowCursor()
– 管理屏幕上的光标显示:显示、隐藏。
3) ClearScreen()
– 更改视频模式,从而清除屏幕。
// CDisplay.h
#ifndef __CDISPLAY__
#define __CDISPLAY__
//
// colors for TextOut func
//
#define BLACK 0x0
#define BLUE 0x1
#define GREEN 0x2
#define CYAN 0x3
#define RED 0x4
#define MAGENTA 0x5
#define BROWN 0x6
#define GREY 0x7
#define DARK_GREY 0x8
#define LIGHT_BLUE 0x9
#define LIGHT_GREEN 0xA
#define LIGHT_CYAN 0xB
#define LIGHT_RED 0xC
#define LIGHT_MAGENTA 0xD
#define LIGHT_BROWN 0xE
#define WHITE 0xF
#include "Types.h"
#include "CString.h"
class CDisplay
{
public:
static void ClearScreen();
static void TextOut(
const char far* inStrSource,
byte inX = 0,
byte inY = 0,
byte inBackgroundColor = BLACK,
byte inTextColor = WHITE,
bool inUpdateCursor = false
);
static void ShowCursor(
bool inMode
);
};
#endif // __CDISPLAY__
// CDisplay.cpp
#include "CDisplay.h"
void CDisplay::TextOut(
const char far* inStrSource,
byte inX,
byte inY,
byte inBackgroundColor,
byte inTextColor,
bool inUpdateCursor
)
{
byte textAttribute = ((inTextColor) | (inBackgroundColor << 4));
byte lengthOfString = CString::Strlen(inStrSource);
__asm
{
push bp
mov al, inUpdateCursor
xor bh, bh
mov bl, textAttribute
xor cx, cx
mov cl, lengthOfString
mov dh, inY
mov dl, inX
mov es, word ptr[inStrSource + 2]
mov bp, word ptr[inStrSource]
mov ah, 13h
int 10h
pop bp
}
}
void CDisplay::ClearScreen()
{
__asm
{
mov al, 02h
mov ah, 00h
int 10h
}
}
void CDisplay::ShowCursor(
bool inMode
)
{
byte flag = inMode ? 0 : 0x32;
__asm
{
mov ch, flag
mov cl, 0Ah
mov ah, 01h
int 10h
}
}
Types.h
实现
Types.h
是一个头文件,其中包含数据类型和宏的定义。
// Types.h
#ifndef __TYPES__
#define __TYPES__
typedef unsigned char byte;
typedef unsigned short word;
typedef unsigned long dword;
typedef char bool;
#define true 0x1
#define false 0x0
#endif // __TYPES__
BootMain.cpp
实现
BootMain()
是程序的入口函数(相当于 main()
)。主要工作在此执行。
// BootMain.cpp
#include "CDisplay.h"
#define HELLO_STR "\"Hello, world…\", from low-level..."
extern "C" void BootMain()
{
CDisplay::ClearScreen();
CDisplay::ShowCursor(false);
CDisplay::TextOut(
HELLO_STR,
0,
0,
BLACK,
WHITE,
false
);
return;
}
StartPoint.asm
实现
;------------------------------------------------------------
.286 ; CPU type
;------------------------------------------------------------
.model TINY ; memory of model
;---------------------- EXTERNS -----------------------------
extrn _BootMain:near ; prototype of C func
;------------------------------------------------------------
;------------------------------------------------------------
.code
org 07c00h ; for BootSector
main:
jmp short start ; go to main
nop
;----------------------- CODE SEGMENT -----------------------
start:
cli
mov ax,cs ; Setup segment registers
mov ds,ax ; Make DS correct
mov es,ax ; Make ES correct
mov ss,ax ; Make SS correct
mov bp,7c00h
mov sp,7c00h ; Setup a stack
sti
; start the program
call _BootMain
ret
END main ; End of program
将所有内容组装起来
创建 COM 文件
现在代码已经开发完成,我们需要将其转换为 16 位操作系统的文件。这类文件是 .com 文件。我们可以从命令行启动每个编译器(用于汇编语言和 C、C++),向它们传递必要的参数,并得到几个目标文件。然后我们启动链接器,将所有 .obj 文件转换成一个具有 .com 扩展名的可执行文件。这是一种可行的方法,但不是很简单。
让我们实现自动化。为此,我们创建一个 .bat 文件并将必要的命令和参数放入其中。图 4 展示了应用程序组装的完整过程。
图 4 – 程序编译过程
Build.bat
我们将编译器和链接器放在项目目录中。在同一个目录中,我们创建一个 .bat 文件并按照示例填充它(您可以使用任何目录名称代替 VC152,其中包含编译器和链接器)。
.\VC152\CL.EXE /AT /G2 /Gs /Gx /c /Zl *.cpp
.\VC152\ML.EXE /AT /c *.asm
.\VC152\LINK.EXE /T /NOD StartPoint.obj bootmain.obj cdisplay.obj cstring.obj
del *.obj
构建自动化
作为本节的最后阶段,我们将介绍如何将 Microsoft Visual Studio 2005、2008 转化为支持任何编译器开发的集成环境。转到项目属性:项目->属性->配置属性\常规\配置类型。
配置属性选项卡包含三个项目:常规、调试、NMake。转到 NMake,并在生成命令行和重新生成命令行字段中设置 build.bat 的路径——图 5。
图 5 – NMake 项目设置
如果一切正确,您就可以通过按 F7 或 Ctrl + F7 以常规方式编译。此时,所有相关信息将在输出窗口中显示。这里的主要优点不仅在于构建自动化,还在于代码错误发生时的导航。
测试与演示
本节将介绍如何实际运行创建的引导加载程序,进行测试和调试。
如何测试引导加载程序
您可以在真实硬件上或使用专门为此目的设计的虚拟机 – VmWare 来测试引导加载程序。在真实硬件上测试会让您更确信它有效,而在虚拟机上测试会让你确信它“能”工作。当然,我们可以说 VmWare 是测试和调试的好方法。我们将同时介绍这两种方法。
首先,我们需要一个工具将引导加载程序写入虚拟或物理磁盘。据我所知,有许多免费和商业的、控制台和 GUI 应用程序。在 Windows 中工作时,我使用了 Disk Explorer for NTFS 3.66(FAT 版本名为 Disk Explorer for FAT),在 MS-DOS 中工作时使用了 Norton Disk Editor 2002。
我将仅介绍 Disk Explorer for NTFS 3.66,因为它最简单并且最适合我们的目的。
使用 VmWare 虚拟机进行测试
创建虚拟机
我们需要 VmWare 版本 5.0、6.0 或更高版本。为了测试引导加载程序,我们将创建一个新的虚拟机,磁盘大小最小,例如 1 GB。我们将其格式化为 NTFS 文件系统。现在我们需要将格式化的硬盘映射到 VmWare 作为虚拟驱动器。操作方法如下:
文件->映射或断开虚拟磁盘...
之后会弹出一个窗口。在那里你应该点击“映射”按钮。在下一个出现的窗口中,你应该设置磁盘的路径。现在你也可以选择磁盘的盘符——见图 6。
图 6 – 虚拟磁盘映射参数
别忘了取消选中 “以只读模式打开文件(推荐)”复选框。当选中时,表示磁盘应以只读模式打开,并阻止所有写入尝试以避免数据损坏。
之后,我们就可以像使用普通 Windows 逻辑驱动器一样使用虚拟机的磁盘了。现在我们应该使用 Disk Explorer for NTFS 3.66,并将引导加载程序写入物理偏移量 0。
使用 Disk Explorer for NTFS
程序启动后,我们转到我们的磁盘(文件->驱动器)。在出现的窗口中,我们转到 逻辑驱动器部分,然后选择具有指定盘符的磁盘(在本例中是 Z)——见图 7。
图 7 – 在 Disk Explorer for NTFS 中选择磁盘
现在我们使用菜单项 查看 和 十六进制显示 命令。在出现的窗口中,我们可以看到以 16 进制视图表示的磁盘信息,按扇区和偏移量划分。由于磁盘此时是空的,里面只有 0。您可以在图 8 中看到第一个扇区。
图 8 – 磁盘的扇区 1
现在我们应该将引导加载程序写入这个第一个扇区。我们将标记设置在位置 00,如图 8 所示。要复制引导加载程序,我们使用 编辑 菜单项,从文件粘贴 命令。在打开的窗口中,我们指定文件路径并点击 打开。之后,第一个扇区的内容应该会改变,看起来如图 9 所示——当然,前提是您没有更改示例代码中的任何内容。
您还应该在扇区开头 1FE 的偏移量处写入签名 55AAh。如果您不这样做,BIOS 将检查最后两个字节,找不到指定的签名,并将该扇区视为非启动扇区,不会将其读取到内存中。
要切换到编辑模式,请按 F2 并输入所需的数字——55AAh 签名。要退出编辑模式,请按 Esc。
现在我们需要确认数据写入。
图 9 – 引导扇区外观
要应用写入,我们转到 工具->选项。会出现一个窗口;我们转到 模式 项目,选择写入方法——虚拟写入,然后点击 写入 按钮——图 10。
图 10 – 在 Disk Explorer for NTFS 中选择写入方法
大量的例行操作终于完成了,现在您可以看到我们从本文一开始一直在开发的东西了。让我们回到 VwWare 断开虚拟磁盘(文件->映射或断开虚拟磁盘……然后点击 断开连接)。
让我们执行虚拟机。我们现在可以看到,从深处、从机器代码和电路的王国,出现了我们熟悉的字符串““Hello, world…”, from low-level…”——见图 11。
图 11 – “Hello world…”
在真实硬件上进行测试
在真实硬件上测试几乎与在虚拟机上测试一样,只是如果出现问题,您将需要更多的时间来修复它,而不是创建一个新的虚拟机。为了在没有现有数据损坏风险的情况下测试引导加载程序(什么都可能发生),我建议使用 U 盘,但首先您应该重新启动 PC,进入 BIOS 并检查它是否支持从 U 盘启动。如果支持,那么一切正常。如果不支持,那么您只能将测试限制在虚拟机测试。
在 Disk Explorer for NTFS 3.66 中将引导加载程序写入 U 盘的过程与虚拟机中的过程相同。您只需选择硬盘本身而不是其逻辑分区,以在正确的偏移量处执行写入——见图 12。
图 12 – 选择物理磁盘作为设备
调试
如果出了问题——这很常见——您需要一些工具来调试您的引导加载程序。我需要立即说明,这是一个非常复杂、令人疲惫且耗时的过程。您将不得不深入研究汇编语言机器码——因此需要精通这门语言。无论如何,我列出了一些用于此目的的工具:
TD (Turbo Debugger) – Borland 提供的适用于 16 位实模式的出色调试器。
CodeView – 微软提供的适用于 16 位模式的优秀调试器。
D86 – Eric Isaacson 开发的适用于 16 位实模式的出色调试器——他在汇编语言的 Intel 处理器开发领域是一位受人尊敬的老兵。
Bocsh – 一个虚拟机模拟程序,包含机器指令调试器。
信息来源
《Assembly Language for Intel-Based Computers》作者 Kip R. Irvine是一本很棒的书,它提供了关于计算机内部结构和汇编语言开发的扎实知识。您还可以找到关于 MASM 6.15 编译器的安装、配置和使用的信息。
此链接将引导您找到 BIOS 中断列表:http://en.wikipedia.org/wiki/BIOS_interrupt_call
结论
在本文中,我们探讨了引导加载程序是什么,BIOS 是如何工作的,以及系统启动时系统组件是如何交互的。实践部分提供了关于如何开发自己的简单引导加载程序的信息。我们演示了混合代码技术以及使用 Microsoft Visual Studio 2005、2008 进行构建自动化的过程。
当然,与庞大的底层编程主题相比,这只是很小的一部分,但如果这篇文章引起了您的兴趣,那将是极好的。