C# 开源托管操作系统 - 快速查看幕后






4.92/5 (18投票s)
快速查看 Cosmos 的内部结构及其工作原理
引言
去年,我写了我的第二篇 Cosmos 文章,该文章 向读者介绍了 Cosmos(C# 开源托管操作系统)。然而,那篇文章是一篇高层级的文章,仅介绍了 Cosmos 以及如何构建自定义内核。本文将简要介绍 Cosmos 是如何实现其魔力,以及它在底层是如何工作的。
Cosmos 是什么?
Cosmos 是一个操作系统开发工具包,它使用 Visual Studio 作为开发环境。尽管名称中包含 C#,但可以使用任何 .NET 语言,包括 VB.NET、Fortran、Delphi Prism、IronPython、F# 等。Cosmos 本身和内核例程主要用 C# 编写,因此得名 Cosmos。除此之外,NOSMOS(.NET 开源托管操作系统)听起来很傻。
Cosmos 从严格意义上讲并非操作系统,而是一个“操作系统工具包”,或者我更喜欢称之为“操作系统乐高”。Cosmos 允许您像 Visual Studio 和 C# 通常允许您创建应用程序一样创建操作系统。大多数用户可以在几分钟内编写并启动自己的操作系统,全部使用 Visual Studio。Cosmos 支持 Visual Studio 中的集成项目类型,以及集成调试器、断点、监视等。您可以像调试普通 C# 或 VB.NET 应用程序一样调试您的操作系统。
引言
虽然 Cosmos 可以通过 USB、以太网、DVD 甚至真实硬盘在真实硬件上启动,但大多数用户使用 VMWare,因为它在开发过程中速度更快。给定以下源代码

当您按 F5 时,您的代码将被转换成一个完整的操作系统,并在 VMWare 中启动

这对很多人来说可能像是魔法。为了使其如此无缝地工作,确实编写了大量的代码。以下是发生的事情的基本步骤。
编译
Visual Studio 使用一个名为 IL2CPU 的 Cosmos 工具将 C# 等 .NET 语言编译成中间语言 (IL)。IL 通常在 Windows 上由 JIT(即时)编译器在运行时处理。Visual Studio 生成的可执行文件实际上不是本机代码,而是 IL 字节码。通常,.NET 可执行文件中唯一的本机代码是一小段引导代码,它允许 Windows 将其作为普通可执行文件运行。此引导代码调用 .NET JIT,然后 JIT 读取并编译可执行文件中的 IL。可以使用许多工具(如 ILDasm 或 Reflector)来查看 IL。
来自 Cosmos 项目的示例 IL
.method family hidebysig virtual instance void Run() cil managed
{
.maxstack 1
.locals init (
[0] int32 i,
[1] class BreakpointsKernel.Test xTest)
L_0000: nop
L_0001: ldc.i4.0
L_0002: stloc.0
L_0003: ret
}
然后将其传递给一个名为 NASM 的汇编器,后者将其转换为二进制格式。
System_Void__BreakpointsKernel_BreakpointsOS__ctor__:
push dword EBP
mov dword EBP, ESP
System_Void__BreakpointsKernel_BreakpointsOS__ctor____DOT__00000000:
call DebugStub_TracerEntry
; [Cosmos.IL2CPU.X86.IL.Ldarg]
; Ldarg
; Arg idx = 0
; Arg type = BreakpointsKernel.BreakpointsOS
; Arg size = 4
push dword [EBP + 8]
; Stack contains 1 items: (4)
System_Void__BreakpointsKernel_BreakpointsOS__ctor____DOT__00000001:
; [Cosmos.IL2CPU.X86.IL.Call]
call System_Void__Cosmos_System_Kernel__ctor__
test dword ECX, 0x2
je near System_Void__BreakpointsKernel_BreakpointsOS__ctor____DOT__00000006
jne near System_Void__BreakpointsKernel_BreakpointsOS__ctor____DOT__END__OF__METHOD_EXCEPTION
; Stack contains 0 items: ()
然后使用各种工具链接二进制输出并将其转换为 ISO 格式。
启动
现在我们有了一个二进制文件,但我们需要启动它。为此,Cosmos 使用了引导加载程序。所有操作系统都使用引导加载程序,包括 Windows 和 Linux。计算机的 BIOS 会查找引导代码,但此引导代码必须非常小。此引导代码然后加载一个稍大的引导加载程序,该引导加载程序初始化内存并加载一个更大的操作系统。将控制权转移给操作系统后,引导代码和引导加载程序将从内存中移除。为此,Cosmos 使用了一个名为 Syslinux 的引导加载程序。尽管有此名称,它绝对不是 Linux,Cosmos 也不是基于 Linux 构建的。Syslinux 的根源与旧的 Linux 引导加载程序有关,因此得名。Syslinux 仅用于让 BIOS 启动 Cosmos 代码,一旦 Cosmos 代码运行起来,BIOS 和 Syslinux 都不再使用。
调试
Cosmos 支持正常的源代码调试,允许跟踪、断点,甚至监视。Cosmos 还支持实验性的汇编级别调试器,以及 GDB。为了支持 Visual Studio 中的调试,Cosmos 有一个小型手写汇编方法,称为 DebugStub。DebugStub 在 Cosmos 执行过程中被反复调用,并由 Cosmos 编译器自动插入。DebugStub 使用串行端口与 DebugClient 通信,DebugClient 是 Cosmos Visual Studio 调试包的一部分。在 VMWare 上,串行端口被映射到一个管道,DebugClient 与管道通信。在真实硬件上进行调试时,双方都使用串行端口。将来,还将支持通过以太网进行调试。
硬件
许多框架类库使用 icall 或 pinvoke。例如,当 .NET 需要在屏幕上绘图时,它没有这方面的代码。它使用 pinvoke 直接调用 Windows API。icall 类似,但将映射到 .NET 运行时中的内部函数。由于 Cosmos 在真实硬件上运行,没有 .NET 运行时也没有 Windows API,因此必须实现此类代码。Cosmos 使用“插装”(plugs)来实现这些。插装是一段代码,它标记一个类和/或方法,以便在 IL2CPU 阶段将其替换。插装可以用 C#(或任何 .NET 语言)、汇编或 X# 编写。插装也可以用于将 C# 代码接口到汇编代码。Cosmos 致力于尽量减少编写汇编代码的需求,但当直接与内核中的硬件交互时,必须使用汇编。插装用于通过创建实际上运行汇编的 C# 类来隐藏这些底层细节,让开发者无需关注。
public AtaPio(Core.IOGroup.ATA aIO,
Ata.ControllerIdEnum aControllerId, Ata.BusPositionEnum aBusPosition) {
IO = aIO;
mControllerID = aControllerId;
mBusPosition = aBusPosition;
// Disable IRQs, we use polling currently
IO.Control.Byte = 0x02;
mDriveType = DiscoverDrive();
if (mDriveType != SpecLevel.Null) {
InitDrive();
}
}
这是 PATA(硬盘访问)类的一个小片段。它能够使用 C# 代码直接与 CPU IO 总线通信。虽然 IOPort
类(代码中的 IO 变量)的部分是用 C# 编写的,但其中一些部分是用 X86 汇编代码进行插装的。
X#
Cosmos 是用 C# 编写的。Cosmos 开发人员,包括内核开发人员,都使用 C#。然而,我们的 IL2CPU 库必须处理汇编,而我们中少数几个处理编译器代码的人当然会涉足 X86。以前,我们有自己的基于类的“内联编译器”。它的效果还不错,但我们总觉得需要更多。我们亲切地称我们的解决方案为 X#(源自 X86)。其理念是将所有源代码保留在一个地方,并保持事物的类型安全。我们以前的基于类的内联编译器做到了这一点。典型代码看起来是这样的:
new Move(Registers.DX, (xComAddr + 1).ToString());
new Move(Registers.AL, 0.ToString());
new Out("dx", "al");// disable all interrupts
new Move(Registers.DX, (xComAddr + 3).ToString());
new Move(Registers.AL, 0x80.ToString());
new Out("dx", "al");// Enable DLAB (set baud rate divisor)
new Move(Registers.DX, (xComAddr + 0).ToString());
new Move(Registers.AL, 0x1.ToString());
new Out("dx", "al");// Set diviso (low byte)
new Move(Registers.DX, (xComAddr + 1).ToString());
new Move(Registers.AL, 0x00.ToString());
new Out("dx", "al");// // set divisor (high byte)
它是类型安全的,而且读起来不算太差。但现在与 X# 相比:
UInt16 xComStatusAddr = (UInt16)(aComAddr + 5);
Label = "WriteByteToComPort";
Label = "WriteByteToComPort_Wait";
DX = xComStatusAddr;
AL = Port[DX];
AL.Test(0x20);
JumpIfEqual("WriteByteToComPort_Wait");
DX = aComAddr;
AL = Memory[ESP + 4];
Port[DX] = AL;
Return(4);
Label = "DebugWriteEIP";
AL = Memory[EBP + 3];
EAX.Push();
Call<WriteByteToComPort>();
AL = Memory[EBP + 2];
EAX.Push();
Call<WriteByteToComPort>();
AL = Memory[EBP + 1];
EAX.Push();
Call<WriteByteToComPort>();
AL = Memory[EBP];
EAX.Push();
Call<WriteByteToComPort>();
Return();
实际上全部是 C# 并且可以编译。当它执行时,这些语句的执行会导致输出 X86 代码,然后可以使用汇编器进行构建。它是如何实现的?嗯,这与 LINQ 出现之前我在 C# 中进行 SQL 操作的方式非常相似。