How is Mono magical?

I’m learning C#, so I made a little C# program that says Hello, World!, then compiled it with mono-csc and ran it with mono:

$ mono-csc Hello.cs
$ mono Hello.exe
Hello, World!

I noticed that when I hit TAB in bash, Hello.exe was marked executable. Indeed, it runs by just a shell loading the filename!

Hello.exe is not an ELF file with a funny file extension:

$ readelf -a Hello.exe
readelf: Error: Not an ELF file - it has the wrong magic bytes at the start
$ xxd Hello.exe | head -n1
00000000: 4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............

MZ means it’s a Microsoft Windows statically linked executable. Drop it onto a Windows box, and it will (should) run.

I have wine installed, but wine, being a compatibility layer for Windows apps, takes about 5x as long to run Hello.exe as mono and executing it directly do, so it’s not wine that runs it.

I’m assuming there’s some mono kernel module installed with mono that intercepts the exec syscall/s, or catches binaries that begin with 4D 5A, but lsmod | grep mono and friends return an error.

What’s going on here, and how does the kernel know that this executable is special?

Just for proof it’s not my shell working magic, I used the Crap Shell (aka sh) to run it and it still runs natively.

Here’s the program in full, since a commenter was curious:

using System;

class Hello {
  /// <summary>
  ///   The main entry point for the application
  /// </summary>
  public static void Main(string[] args) {
    System.Console.Write("Hello, World!n");
Asked By: cat


This is binfmt_misc in action: it allows the kernel to be told how to run binaries it doesn’t know about. Look at the contents of /proc/sys/fs/binfmt_misc; among the files you see there, one should explain how to run Mono binaries:

interpreter /usr/lib/binfmt-support/run-detectors
offset 0
magic 4d5a

(on a Debian system). This tells the kernel that binaries starting with MZ (4d5a) should be given to run-detectors. The latter figures out whether to use Mono or Wine to run the binary.

Binary types can be added, removed, enabled and disabled at any time; see the documentation above for details (the semantics are surprising, the virtual filesystem used here doesn’t behave entirely like a standard filesystem). /proc/sys/fs/binfmt_misc/status gives the global status, and each binary “descriptor” shows its individual status. Another way of disabling binfmt_misc is to unload its kernel module, if it’s built as a module; this also means it’s possible to blacklist it to avoid it entirely.

This feature allows new binary types to be supported, such as MZ executables (which include Windows PE and PE+ binaries, but also DOS and OS/2 binaries!), Java JAR files… It also allows known binary types to be supported on new architectures, typically using Qemu; thus, with the appropriate libraries, you can transparently run ARM Linux binaries on an Intel processor!

Your question stemmed from cross-compilation, albeit in the .NET sense, and that brings up a caveat with binfmt_misc: some configuration scripts misbehave when you try to cross-compile on a system which can run the cross-compiled binaries. Typically, detecting cross-compilation involves building a binary and attempting to run it; if it runs, you’re not cross-compiling, if it doesn’t, you are (or your compiler’s broken). autoconf scripts can usually be fixed in this case by explicitly specifying the build and host architectures, but sometimes you’ll have to disable binfmt_misc temporarily…

Answered By: Stephen Kitt