C# 开源托管操作系统 - 插件介绍






4.88/5 (12投票s)
介绍如何在 Cosmos 中使用 C#、Assembly 或 X# 编写插件
引言
本文将演示如何在 Cosmos 中实现依赖 Windows API 或内部调用的 .NET 代码。此外,还将介绍如何使用 Cosmos 和汇编语言或 X# 直接与硬件接口。
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 中,插件在三种场景下是必需的
- 内部调用
- PInvoke
- 直接汇编
内部调用和 PInvoke
一些 .NET 类中的方法不是用 .NET 代码实现的。它们是用本地代码实现的。原因有两个:
- 该方法依赖于 Windows API (PInvoke)。
- 该方法依赖于 .NET 运行时中高度优化的 C++ 或汇编(内部调用)。
PInvoke 用于绘制到屏幕、访问现有的 Windows 加密 API、访问网络和其他类似功能。
内部调用 (icalls) 用于需要直接访问 .NET 运行时的类。例如,需要访问内存管理的类,或者在某些情况下为了获得原始速度。Math.Pow
方法使用内部调用。
插件可以用 C#(或任何 .NET 语言)或汇编编写。
直接汇编
为了与硬件通信,Cosmos 必须能够与 PCI 总线、CPU IO 总线、内存等进行交互。内存通常可以使用不安全指针来访问;然而,在其他情况下,汇编代码必须手工编写。插件可用于将 C# 直接与汇编代码接口,使汇编调用可以像调用普通 C# 代码一样被调用。
在 Cosmos 中编写 X86 汇编
Cosmos 可以使用类来编写 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)
但 Cosmos 还支持一种更高级别的抽象,称为 X#。X# 是一种类型安全的汇编语言,它映射到 X86 汇编。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();
编写插件
要编写插件,我们首先必须确定插件的目标。例如,Math.Abs(double)
实现为内部调用。
.method public hidebysig static float64 Abs(float64 'value') cil managed internalcall
{
.custom instance void System.Security.SecuritySafeCriticalAttribute::.ctor()
}
如果在没有插件的情况下在 Cosmos 中调用它,编译器将产生一个错误:“需要插件”,因为它没有 IL 代码供 IL2CPU 编译成 X86。因此,“需要插件”错误意味着您使用了一个依赖于内部调用或 PInvoke 的方法,因此 Cosmos 无法编译它。
在 Math.Pow
的情况下,它将起作用,因为 Cosmos 内核已经包含了一个插件,该插件会被自动使用。
插件由编译器在运行时用来替换代码。而不是执行内部调用或 Windows API(这对于 Cosmos 来说是不可能的,因为它不是在 CLR 或 Windows 下运行的),插件提供的替换代码将被插入并使用。这是一种强制内联和替换的形式。
要创建插件,我们需要创建一个新类。内核中的插件保存在单独的程序集中,并由内核引用。这使得 IL2CPU 能够包含和使用插件。
[Plug(Target = typeof(global::System.Math))]
public class MathImpl {
public static double Abs(double value) {
if (value < 0) {
return -value;
} else {
return value;
}
}
插件类可以包含多个方法,尽管本节只显示了一个。Plug
属性是此示例中的关键项。它告诉 IL2CPU 这个类用于替换 System.Math
类的相关方法。然后,它会查找与 System.Math
中的方法匹配的方法。
直接汇编插件
直接汇编插件用于允许 C# 直接与 X86 汇编代码进行接口。例如,IOPort
类允许设备驱动程序直接访问 CPU 总线,这对于与许多硬件设备通信是必需的。
首先,创建一个部分用 C# 编写的空类。将创建将由汇编插件的方法。但是,如果它们不是返回类型为 void
,则必须返回一个虚拟值,以便 C# 编译器可以编译它。然而,返回的值不会被使用,因为插件会导致目标方法的实现被忽略,而使用插件的实现。
public abstract class IOPortBase {
public readonly UInt16 Port;
// all ctors are internal - Only Core ring
// can create it.. but hardware ring can use it.
internal IOPortBase(UInt16 aPort)
{
Port = aPort;
}
internal IOPortBase(UInt16 aBase, UInt16 aOffset)
{
// C# math promotes things to integers, so we have this constructor
// to relieve the use from having to do so many casts
Port = (UInt16)(aBase + aOffset);
}
//TODO: Reads and writes can use this to get port instead of argument
static protected void Write8(UInt16 aPort, byte aData) { } // Plugged
static protected void Write16(UInt16 aPort, UInt16 aData) { } // Plugged
static protected void Write32(UInt16 aPort, UInt32 aData) { } // Plugged
static protected byte Read8(UInt16 aPort) { return 0; } // Plugged
static protected UInt16 Read16(UInt16 aPort) { return 0; } // Plugged
static protected UInt32 Read32(UInt16 aPort) { return 0; } // Plugged
正如您所看到的,Write
方法是空的,但 Read
方法需要一个虚拟值。
然后使用以下代码为类添加插件:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cosmos.IL2CPU.Plugs;
using Assembler = Cosmos.Compiler.Assembler.Assembler;
using CPUx86 = Cosmos.Compiler.Assembler.X86;
namespace Cosmos.Core.Plugs
{
[Plug(Target = typeof(Cosmos.Core.IOPortBase))]
public class IOPortImpl
{
[Inline]
public static void Write8(UInt16 aPort, byte aData)
{
//TODO: This is a lot of work to write to a single port.
// We need to have some kind of inline ASM option that can
// emit a single out instruction
new CPUx86.Move { DestinationReg = CPUx86.Registers.EDX,
SourceReg = CPUx86.Registers.EBP, SourceDisplacement = 0x0C,
SourceIsIndirect = true };
new CPUx86.Move { DestinationReg = CPUx86.Registers.EAX,
SourceReg = CPUx86.Registers.EBP, SourceDisplacement = 0x08,
SourceIsIndirect = true };
new CPUx86.Out { DestinationReg = CPUx86.Registers.AL };
}
[Inline]
public static void Write16(UInt16 aPort, UInt16 aData)
{
new CPUx86.Move { DestinationReg = CPUx86.Registers.EDX,
SourceReg = CPUx86.Registers.EBP, SourceIsIndirect = true,
SourceDisplacement = 0x0C };
new CPUx86.Move { DestinationReg = CPUx86.Registers.EAX,
SourceReg = CPUx86.Registers.EBP, SourceIsIndirect = true,
SourceDisplacement = 0x08 };
new CPUx86.Out { DestinationReg = CPUx86.Registers.AX };
}
[Inline]
public static void Write32(UInt16 aPort, UInt32 aData)
{
new CPUx86.Move { DestinationReg = CPUx86.Registers.EDX,
SourceReg = CPUx86.Registers.EBP, SourceIsIndirect = true,
SourceDisplacement = 0x0C };
new CPUx86.Move { DestinationReg = CPUx86.Registers.EAX,
SourceReg = CPUx86.Registers.EBP, SourceIsIndirect = true,
SourceDisplacement = 0x08 };
new CPUx86.Out { DestinationReg = CPUx86.Registers.EAX };
}
[Inline]
public static byte Read8(UInt16 aPort)
{
new CPUx86.Move { DestinationReg = CPUx86.Registers.EDX,
SourceReg = CPUx86.Registers.EBP, SourceIsIndirect = true,
SourceDisplacement = 0x08 };
//TODO: Do we need to clear rest of EAX first?
// MTW: technically not, as in other places,
// it _should_ be working with AL too..
new CPUx86.Move { DestinationReg = CPUx86.Registers.EAX, SourceValue = 0 };
new CPUx86.In { DestinationReg = CPUx86.Registers.AL };
new CPUx86.Push { DestinationReg = CPUx86.Registers.EAX };
return 0;
}
[Inline]
public static UInt16 Read16(UInt16 aPort)
{
new CPUx86.Move { DestinationReg = CPUx86.Registers.EDX,
SourceReg = CPUx86.Registers.EBP,
SourceIsIndirect = true, SourceDisplacement = 0x08 };
new CPUx86.Move { DestinationReg = CPUx86.Registers.EAX, SourceValue = 0 };
new CPUx86.In { DestinationReg = CPUx86.Registers.AX };
new CPUx86.Push { DestinationReg = CPUx86.Registers.EAX };
return 0;
}
[Inline]
public static UInt32 Read32(UInt16 aPort)
{
new CPUx86.Move { DestinationReg = CPUx86.Registers.EDX,
SourceReg = CPUx86.Registers.EBP,
SourceIsIndirect = true, SourceDisplacement = 0x08 };
new CPUx86.In { DestinationReg = CPUx86.Registers.EAX };
new CPUx86.Push { DestinationReg = CPUx86.Registers.EAX };
return 0;
}
}
}
请注意,在这种情况下,代码不是用 X# 编写的。我们许多较旧的插件仍用旧语法编写。
现在我们有了插件,我们可以直接在 C# 代码中访问 IOPort
类。此示例来自 ATA(硬盘)类。
public override void ReadBlock(UInt64 aBlockNo, UInt32 aBlockCount, byte[] aData) {
CheckDataSize(aData, aBlockCount);
SelectSector(aBlockNo, aBlockCount);
SendCmd(Cmd.ReadPio);
IO.Data.Read8(aData);
}
另一个插件示例
在 BCL 中,Console
类调用多个内部方法,最终调用 Windows API。我们不需要只为直接映射到 Windows API 的方法编写插件,而是为树中高得多的方法编写插件,并完全替换实现,以调用我们的 TextScreen
类。
namespace Cosmos.System.Plugs.System.System {
[Plug(Target = typeof(global::System.Console))]
public static class ConsoleImpl {
private static ConsoleColor mForeground = ConsoleColor.White;
private static ConsoleColor mBackground = ConsoleColor.Black;
public static ConsoleColor get_BackgroundColor() {
return mBackground;
}
public static void set_BackgroundColor(ConsoleColor value) {
mBackground = value;
Cosmos.Hardware.Global.TextScreen.SetColors(mForeground, mBackground);
}