65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (277投票s)

2024年5月30日

CPOL

16分钟阅读

viewsIcon

896587

downloadIcon

20523

本文通过开发简单的引导加载程序的例子,描述了低级编程的初步步骤。

目录

1. 谁可能对此感兴趣
2. 什么是引导加载程序
3. 准备深入研究
3.1 那么,开发引导加载程序需要了解什么语言?
3.2. 需要什么编译器?
3.3 系统是如何启动的
4. 开始编码
4.1 程序架构
4.2 开发环境
4.3 BIOS 中断和屏幕清除
4.4 “混合代码”
4.5 CString 的实现
4.6 CDisplay 的实现
4.7 Types.h 的实现
4.8 BootMain.cpp 的实现
4.9 StartPoint.asm 的实现
5. 将所有内容组装起来
5.1 创建 COM 文件
5.2 构建自动化
6. 测试与演示
6.1 如何测试引导加载程序。
6.2 使用 VmWare 虚拟机进行测试
6.2.1 创建虚拟机
6.2.2 使用 Disk Explorer for NTFS
6.3 在真实硬件上进行测试
6.4 调试
7. 信息来源
8. 结论

 

谁可能对此感兴趣

我写这篇文章主要是面向那些一直对事物运作方式感兴趣的人。它适用于那些通常使用 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++ 编译器。

您也可以使用其他一些替代选项

DMCDigital Mars 提供的适用于 16 位和 32 位模式的汇编语言、C、C++ 的免费编译器;

LINKDMC 编译器的免费链接器。;

Borland 也有一些产品

BCC 3.5可创建 16 位模式文件的 C、C++ 编译器;

TASM - 适用于 16 位模式的汇编语言编译器;

TLINK可创建 16 位模式 .com 文件的链接器。

本文中的所有代码示例均使用微软工具创建。

系统是如何启动的

为了解决我们的问题,我们需要回顾一下系统是如何启动的。

让我们简要看一下系统组件在启动过程中是如何交互的(见图 1)。

boot-loader/fig1.PNG

图 1 - “系统启动过程”

当控制权转移到地址 0000:7C00 后,主引导记录 (MBR) 开始工作,并启动操作系统加载过程。您可以在此处了解更多关于 MBR 结构的信息。

动手编码

在接下来的章节中,我们将通过开发自己的引导加载程序直接进行底层编程。

程序架构

我们正在开发的引导加载程序仅用于学习目的。它的任务如下:

  1. 正确加载到内存地址 0000:7C00。
  2. 调用用高级语言开发的 BootMain 函数。
  3. 从底层在显示器上显示“Hello, world...”消息。

程序架构如图 2 所示,然后是文字描述。

boot-loader/fig2.PNG

图 2 - 程序架构描述

第一个实体是 StartPoint,完全用汇编语言开发,因为高级语言没有必要的指令。它告诉编译器应使用哪种内存模型,以及从磁盘读取后应将加载到 RAM 的地址。它还会更正处理器寄存器并将控制权传递给用高级语言编写的 BootMain

下一个实体——BootMain——相当于 main,它是所有程序功能集中的主函数。

CDisplayCString 类负责程序的功能部分,并在屏幕上显示消息。正如您从图 2 中看到的,CDisplay 类在其工作中使用了 CString 类。

开发环境

这里我使用的是标准的开发环境 Microsoft Visual Studio 20052008。您可以使用任何其他工具,但我确信这两个工具经过一些设置后,可以使编译和工作变得轻松便捷。

首先,我们应该创建一个 Makefile 项目类型,主要工作将在其中进行(见图 3)。

文件->新建\项目->常规\Makefile 项目

boot-loader/fig3.PNG

图 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 展示了应用程序组装的完整过程。

boot-loader/fig4.PNG
图 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。
boot-loader/fig5.PNG
图 5 – NMake 项目设置

如果一切正确,您就可以通过按 F7Ctrl + 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。
boot-loader/fig6.PNG
图 6 – 虚拟磁盘映射参数

别忘了取消选中 “以只读模式打开文件(推荐)”复选框。当选中时,表示磁盘应以只读模式打开,并阻止所有写入尝试以避免数据损坏。

之后,我们就可以像使用普通 Windows 逻辑驱动器一样使用虚拟机的磁盘了。现在我们应该使用 Disk Explorer for NTFS 3.66,并将引导加载程序写入物理偏移量 0。

使用 Disk Explorer for NTFS

程序启动后,我们转到我们的磁盘(文件->驱动器)。在出现的窗口中,我们转到 逻辑驱动器部分,然后选择具有指定盘符的磁盘(在本例中是 Z)——见图 7。
boot-loader/fig7.PNG
图 7 – 在 Disk Explorer for NTFS 中选择磁盘

现在我们使用菜单项 查看十六进制显示 命令。在出现的窗口中,我们可以看到以 16 进制视图表示的磁盘信息,按扇区和偏移量划分。由于磁盘此时是空的,里面只有 0。您可以在图 8 中看到第一个扇区。

boot-loader/fig8.PNG
图 8 – 磁盘的扇区 1

现在我们应该将引导加载程序写入这个第一个扇区。我们将标记设置在位置 00,如图 8 所示。要复制引导加载程序,我们使用 编辑 菜单项,从文件粘贴 命令。在打开的窗口中,我们指定文件路径并点击 打开。之后,第一个扇区的内容应该会改变,看起来如图 9 所示——当然,前提是您没有更改示例代码中的任何内容。

您还应该在扇区开头 1FE 的偏移量处写入签名 55AAh。如果您不这样做,BIOS 将检查最后两个字节,找不到指定的签名,并将该扇区视为非启动扇区,不会将其读取到内存中。

要切换到编辑模式,请按 F2 并输入所需的数字——55AAh 签名。要退出编辑模式,请按 Esc

现在我们需要确认数据写入。

boot-loader/fig9.PNG
图 9 – 引导扇区外观

 

要应用写入,我们转到 工具->选项。会出现一个窗口;我们转到 模式 项目,选择写入方法——虚拟写入,然后点击 写入 按钮——图 10。

boot-loader/fig10.PNG
图 10 – 在 Disk Explorer for NTFS 中选择写入方法

 

 

大量的例行操作终于完成了,现在您可以看到我们从本文一开始一直在开发的东西了。让我们回到 VwWare 断开虚拟磁盘(文件->映射或断开虚拟磁盘……然后点击 断开连接)。

 

让我们执行虚拟机。我们现在可以看到,从深处、从机器代码和电路的王国,出现了我们熟悉的字符串““Hello, world…”, from low-level…”——见图 11。

boot-loader/fig11.PNG
图 11 – “Hello world…”

在真实硬件上进行测试

在真实硬件上测试几乎与在虚拟机上测试一样,只是如果出现问题,您将需要更多的时间来修复它,而不是创建一个新的虚拟机。为了在没有现有数据损坏风险的情况下测试引导加载程序(什么都可能发生),我建议使用 U 盘,但首先您应该重新启动 PC,进入 BIOS 并检查它是否支持从 U 盘启动。如果支持,那么一切正常。如果不支持,那么您只能将测试限制在虚拟机测试。

在 Disk Explorer for NTFS 3.66 中将引导加载程序写入 U 盘的过程与虚拟机中的过程相同。您只需选择硬盘本身而不是其逻辑分区,以在正确的偏移量处执行写入——见图 12。

boot-loader/fig12.PNG
图 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 进行构建自动化的过程。

当然,与庞大的底层编程主题相比,这只是很小的一部分,但如果这篇文章引起了您的兴趣,那将是极好的。

© . All rights reserved.