Hacker News new | past | comments | ask | show | jobs | submit login
EFI Boot Application in C# (github.com/michalstrehovsky)
217 points by pjmlp on March 25, 2019 | hide | past | favorite | 39 comments



This is pretty wild. I hadn't realized that there were easy ways to work with EFI in higher-level languages than C.

Other languages:

Rust: https://medium.com/@gil0mendes/an-efi-app-a-bit-rusty-82c36b...

Wasn't able to find an example for Go, but seems likely it's out there.


C# is higher-level in aggregate, but if you look at individual features, it lets you go as low as C in most respects - raw pointers with arithmetic, unions, stack allocations, you name it. It seems that this EFI project uses it at that level, and higher-level features that would require e.g. GC aren't supported.


This is one of the benefits of C# that makes it approachable to beginners.


I wouldn't say pointers and unsafe code are features that makes it approachable to beginners.


C# with boatloads of `unsafe` is by no means an higher level language; it's pretty much the same level of C/C++ or Rust. You're just using a more sophisticated complier and a stricter language, which can be great if used correctly, but if you dereference a null pointer, it's still going to crash and burn; this also applies to unsafe Rust.


Following the footsteps of system languages like PL/S or Mesa/Cedar, just to cite two quite old examples.

The good part is that the unsafe bits can be easily constrained, instead of hidding away in every single line of code.


> Wasn't able to find an example for Go, but seems likely it's out there.

NERF/LinuxBoot has a lot of Go components though as I remember it the EFI application is actually the Linux kernel, with the userland (i.e. init) being written in Go.


The kexec paradigm makes this execution vector rather more useful than most people realize. It's also what powers inline kernel crash dumps.


Go has segmented stacks, which could make it awkward to use in a bare-metal environment like this. I’m sure it’s possible, but it might be quite tedious to get working.


Go previously used segmented stacks, but stopped in the 1.4 release (2014)


How do they handle growing tons of stacks now?


Goroutine stacks start out small (2KB), are heap-allocated and copyable (unlike most Go memory).


I genuinely had no idea that you could produce bare object files with ilc. This is so exciting to me, as compiler work has ended up being the majority of the work that goes into pure-managed OSes. Guess I know what I'm going to do when I actually have some free time, down the road!


EFI is actually itself already much of an OS (it's certainly bigger than DOS, for example), and the executables are PE format just like Windows application binaries, so this isn't quite as low-level as it may sound at first.


EFI just means you don't have to deal with the arcane x86 boot process. If you want to do anything kernel-like then you are still pretty much on your own, you're still gonna need a memory manager, process scheduler, etc.


Same thing, I had no idea either! I was trying to figure out how to even use the native compilers on the side but never found the time to really sit down and do it. This seems like exactly what I needed to get started!


I have started pure pointers algorithms and data structures library half year ago. Got hit by lack of generics of pointers and unmanaged generics. These seems fixed in preview of f# and c#. There are many other improvements to have non GC runtime going and considerrd in C# and F# code and other movements into Rust like features. So it may be reasonable to write low level code in c# within 2 versions I guess. F# even better as it has templates(c++ like generics) and other non oop compositional stuff. Given this tendency I stopped to try to learn Rust.


Pretty sure F# allocates like nobody's business


By default yes, but that's the price of using immutable objects and datastructures.

However, you can cut pretty much all of those allocations with a bit of care and still retain some F# niceties.

I recently wrote a very low allocation text file parser that could ingest log files at over 200MB/s single threaded. All in F# using structs and Span the same way I would in C#.

Except I get the safety, terse syntax and rigor of options, records and matches


That's pretty cool. I wish some of the foundational f# libs and webserver were written that way. They probably use more idiomatic code which means application code pays those costs too.


For years, a lot of pain was caused by F# using classes for critical types such as Option, Tuple and Choice. Additionally most records should have been structs by default.

They have started fixing this by introducing value tuple, value option and supporting C# performance primitives like Span. But they have a way to go still.


Ah, that summary is helpful, thanks


f# is multiparadigm and can do same low level as c#. you can not to allocate if do not want so.


But why use C# at all if your target has actual options?


May be write something not in the internet yet but soon will be and check limitations of lang


If I understand correctly, that uses this:

https://github.com/dotnet/corert

Written about here:

https://mattwarren.org/2018/06/07/CoreRT-.NET-Runtime-for-AO...

"One of the first things people asked about CoreRT is “what is the size of a ‘Hello World’ app” and the answer is ~3.93 MB (if you compile in Release mode), but there is work being done to reduce this."

..

"So Test.CoreLib really is a minimal runtime!! But the difference in size is dramatic, it shrinks down to 0.49 MB compared to 3.93 MB for the fully-featured runtime!"


What is new compared to this post is even Runtime.Base is optional. You can get Hello World down to 5k by implementing a even more stripped down core library:

https://github.com/MichalStrehovsky/zerosharp/blob/master/no...


Thanks. One level above your link is I think the best explanation:

https://github.com/MichalStrehovsky/zerosharp

"no-runtime is a rather pointless sample that demonstrates how to write code in C# that is directly runnable without a runtime. C# has value types and you can p/invoke into an unmanaged memory allocator, so you can do things with this, but you're so severily limited it's rather pointless. But Hello world ends up being about 4-5 kB native EXE, so that's rather cool.

with-runtime is something that can be actually useful. This includes the full managed and unmanaged runtime - GC, exception handling, and interface dispatch all work. Test.CoreLib used as the class library here is the same Test.CoreLib that you can find in the CoreRT repo. Don't look for things like Object.ToString() because being compatible with .NET is not the point. This sample comes down to about 400 kB, most of which is the C runtime library.

efi-no-runtime is an EFI boot application that lets you run C# on bare metal, without an OS. Similar restrictions to the no-runtime sample apply. Making a version of this sample with a runtime would require some porting work on the runtime side."


Is there a detailed description of Test.CoreLib anywhere?


> Is there a detailed description of Test.CoreLib anywhere?

You can see the code and README for it here https://github.com/dotnet/corert/tree/master/src/Test.CoreLi... and I also wrote a bit about it in this post https://mattwarren.org/2018/06/07/CoreRT-.NET-Runtime-for-AO...


No, the EFI demo uses no runtime at all


I never claimed it uses some specific runtime (my specific wording was "that uses this" followed by the link to the CoreRT) as I saw it uses the ILCompiler which is, if I understand correctly, a part of CoreRT:

https://github.com/dotnet/corert/tree/master/src/ILCompiler

The rest was the quotes about how big the runtime has to be to be "minimally useful."

As the author wrote about "no runtime" example (see my other comment for the reference): "C# has value types and you can p/invoke into an unmanaged memory allocator, so you can do things with this, but you're so severily limited it's rather pointless." and about his efi example: "Similar restrictions to the no-runtime sample apply."


Thanks - answers all my questions as to how this works :)


Importantly there's a limitation, which is that the CoreRT runtime doesn't support EFI directly so you won't be able to use most of what you're familiar with when it comes to .NET. But AOT compilation for .NET is maturing and can be used today for other exciting targets like WebAssembly. Perhaps EFI one day in the future.


I hope this is just a "for fun"/"to see if it can be done" type of thing, because otherwise it is more than a little disturbing when the source code in a high-level language takes more lines of code than doing the same thing in pure Asm:

https://gist.github.com/yackx/a52010a05a430496ae11

https://github.com/MichalStrehovsky/zerosharp/blob/master/ef...

(The Asm above was for good old BIOS, here's the EFI version: http://x86asm.net/articles/uefi-programming-first-steps/hell... )


it's not disturbing at all, the actual code is tiny, just like asm. For the EFI version you are missing all of efi.inc, just like the C#, you have datastructure definitions. http://x86asm.net/articles/uefi-programming-first-steps/efi....


> otherwise it is more than a little disturbing when the source code in a high-level language takes more lines of code than doing the same thing in pure Asm

The vast majority of the code is just defining the minimal set of standard C# data types needed to compile any C# program.


There is an analog in the efi asm example in the include file [1] with its structures and defines which have a few types but is mostly concerned with defining the same structures as in the C# program.

Something that is very unique and interesting is the usage of the System.Runtime & System.Runtime.InteropService , I'm guessing that some of the contorting needed to get things working in the code lays with those functions. The other half would be the ilc and the linker having the EFI subsystem as an option.

[1] http://x86asm.net/articles/uefi-programming-first-steps/efi....


Runtime and InteropServices are needed for .NET's P/Invoke FFI. It's used to declare the layout of structs, foreign pointers and marshalling code. You can see this being used starting at line 127, where it appears to define EFI handles, tables and headers.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: