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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (12投票s)

2011年7月3日

BSD

5分钟阅读

viewsIcon

61576

介绍如何在 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 中,插件在三种场景下是必需的

  1. 内部调用
  2. PInvoke
  3. 直接汇编

内部调用和 PInvoke

一些 .NET 类中的方法不是用 .NET 代码实现的。它们是用本地代码实现的。原因有两个:

  1. 该方法依赖于 Windows API (PInvoke)。
  2. 该方法依赖于 .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);
    }
© . All rights reserved.