Skip to content
Quajak edited this page Nov 19, 2020 · 3 revisions

Original article written by Kudzu

Introduction

This article will demonstrate how to implement .NET code that relies on Windows APIs or internal calls in Cosmos. In addition, it will cover how to interface directly to the hardware using Cosmos and assembly or X#.

What is Cosmos?

Cosmos is an Operating System development kit (more info) which uses Visual Studio as its development environment. Despite C# in the name, any .NET based language can be used including VB.NET, Fortran, Delphi Prism, IronPython, F#, and more. Cosmos itself and the kernel routines are primarily written in C#, and thus the Cosmos name. Besides that, NOSMOS (.NET Open Source Managed Operating System) sounds stupid.

Cosmos is not an Operating System in the traditional sense, but instead it is an "Operating System Kit", or as I like to say "Operating System Legos". Cosmos lets you create Operating Systems just as Visual Studio and C# normally let you create applications. Most users can write and boot their own Operating System in just a few minutes, all using Visual Studio. Cosmos supports integrated project types in Visual Studio, and an integrated debugger, breakpoints, watches, and more. You can debug your Operating System the same way that you debug a normal C# or VB.NET application.

The Need For Plugs

Plugs are needed in three scenarios in Cosmos:

  1. Internal Calls
  2. PInvoke
  3. Direct Assembly

Internal Calls and PInvoke

Some methods in .NET classes are implemented without using .NET code. They are implemented using native code. There are two reasons for this:

  1. The method relies on the Windows API (PInvoke).
  2. The method relies on highly optimized C++ or assembly (internal calls) which exist in the .NET runtime.

PInvoke is used to draw to the screen, access existing Windows encryption APIs, access networking, and other similar functions.

Internal Calls (icalls) are used for classes which require direct access to the .NET runtime. Examples are classes which need to access memory management, or in some cases for raw speed. The Math.Pow method uses an internal call.

Plugs can be written in C# (or any .NET language), or assembly.

Direct Assembly

To talk to hardware, Cosmos must be able to interact with the PCI bus, CPU IO Bus, memory, and more. Memory can usually be accessed using unsafe pointers; however, in other cases, the assembly code must be handwritten. Plugs can be used to interface C# to assembly code directly, making the assembly calls callable as if normal C# code is being called.

Writing X86 Assembly in Cosmos

An X86 assembly can be written in Cosmos using classes:

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)

But Cosmos also supports a higher level abstraction called X#. X# is a typesafe assembly language which maps to X86 assembly. X# looks like this:

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();

Writing Plugs

To write a plug, first we must decide the target of our plug. For example, Math.Abs(double) is implemented as an internal call.

.method public hidebysig static float64 Abs(float64 'value') cil managed internalcall
{
    .custom instance void System.Security.SecuritySafeCriticalAttribute::.ctor()
}

If you try to call it in Cosmos without a plug, the compiler will produce an error, "plug needed", because there is no IL code in it for IL2CPU to compile X86. So the "plug needed" error means that you have used some method which relies on an internal call or PInvoke, and thus Cosmos cannot compile it.

In the case of Math.Pow, it will work because the Cosmos kernel already includes a plug which is automatically used.

The plug is used by the compiler at runtime to substitute code. Instead of performing an internal call or Windows API which is impossible for Cosmos because it is not running under the CLR or Windows, the replacement code supplied by the plug is inserted and used. It is a form of forced inlining and replacement.

To create the plug, we need to create a new class. Plugs in the kernel are kept in separate assemblies and referenced by the kernel. This allows IL2CPU to include and use the plugs.

[Plug(Target = typeof(global::System.Math))]
public class MathImpl  {
        public static double Abs(double value)  {
            if (value < 0) {
                return -value;
            } else {
                return value;
            }
        }

Plug classes can contain more than one method, although this excerpt only shows one. The Plug attribute is the key item in this example. It tells IL2CPU that this class is used to replace methods of the System.Math class. It then proceeds to find methods to match against the methods in System.Math.

Direct Assembly Plugs

Direct assembly plugs are used to allow C# to interface directly to X86 assembly code. An example of this is the IOPort class which allows device drivers to directly access the CPU bus which is required to talk to many hardware devices.

First, an empty class written partially in C# is created. Methods that will be plugged by the assembly are created empty. However, if they are not of return type void, a dummy value must be returned so that the C# compiler will compile it. The return value however will not be used, because plugs cause the target method's implementation to be ignored and the plug's implementation to be used instead.

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

As you can see, the Write methods are empty, but the Read methods require a dummy value.

The class is then plugged using this code:

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;
        }
    }
}

Note that the code in this case is not written in X#. Many of our older plugs are still written in the older syntax.

Now that we have a plug, we can access the IOPort class directly in C# code. This example is from the ATA (Hard disk) class.

public override void ReadBlock(UInt64 aBlockNo, UInt32 aBlockCount, byte[] aData) {
      CheckDataSize(aData, aBlockCount);
      SelectSector(aBlockNo, aBlockCount);
      SendCmd(Cmd.ReadPio);
      IO.Data.Read8(aData);
}

Another Plug Example

In the BCL, the Console class calls several internal methods which finally end up in Windows API calls. We don't need to plug only the methods that directly map to Windows API calls, but instead we plug methods much higher up the tree and completely replace the implementation to call our TextScreen class instead.

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);
    }

Custom plug library

If you want to include your own custom plugs, you need to create a seperate standard C# library project, in which you write your plugs. To use the plugs from this library, add the following line to cosmos kernel csproj file. <PlugsReference Include="path to compiled PlugLibrary.dll" />

Clone this wiki locally