I just discovered something really surprising.
I can compile a simple C# hello world on my windows computer using csc
(from Visual Studio), copy the resulting exe
file to my Linux computer, and execute it with mono helloworld.exe
. So far, everything makes sense to me: according to this SO post, on Windows, helloworld.exe
is basically just a trick that ends up starting the C# runtime, and the CIL bytecode is just read from some data section later on in the exe
file. Likewise, I imagine that, on Linux, doing mono helloworld.exe
just starts the C# runtime and directly reads the bytecode without bothering with the exe trickery.
For posterity, here is the source, which I got from Charles Petzold's excellent free C# book:
//---------------------------------------------
// FirstProgram.cs (c) 2006 by Charles Petzold
//---------------------------------------------
class FirstProgram
{
public static void Main()
{
System.Console.WriteLine("Hello, Microsoft .NET Framework!");
}
}
But here's where things get interesting: on Linux (uname -r
on my machine gives 4.14.188-1-MANJARO
) I can simply do ./helloworld.exe
and it works!
I started doing some sleuthing, and here are the first few lines from running strace ./helloworld.exe
:
execve("./hw.exe", ["./hw.exe"], 0x7fffae8e6070 /* 61 vars */) = 0
[ Process PID=3381 runs in 32 bit mode. ]
brk(NULL) = 0x7eedb000
arch_prctl(0x3001 /* ARCH_??? */, 0xffb19948) = -1 EINVAL (Invalid argument)
readlink("/proc/self/exe", "/usr/bin/wine", 4096) = 13
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7faa000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/../lib32/tls/i686/sse2/libwine.so.1", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat64("/usr/bin/../lib32/tls/i686/sse2", 0xffb18e00) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/../lib32/tls/i686/libwine.so.1", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat64("/usr/bin/../lib32/tls/i686", 0xffb18e00) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/../lib32/tls/sse2/libwine.so.1", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat64("/usr/bin/../lib32/tls/sse2", 0xffb18e00) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/../lib32/tls/libwine.so.1", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat64("/usr/bin/../lib32/tls", 0xffb18e00) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/../lib32/i686/sse2/libwine.so.1", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = -1 ENOENT (No such file or directory)
stat64("/usr/bin/../lib32/i686/sse2", 0xffb18e00) = -1 ENOENT (No such file or directory)
I would have expected some error saying "this file is not a valid executable", since I only expect the Linux program loader to understand ELF files instead of Windows's PE format. Instead, it seems that somehow the system is smart enough to start looking for wine (in the strace
output, you can see it start to look for wine libraries, and because I've installed wine on my Linux machine, it does eventually find them later on).
So what's going on? Is the execve
call smart enough to try using wine if it detects a PE file, or is this something that bash
is doing? Or is it something else entirely different?
strace
, it is indeed Wine taking care of things here (on top ofbinfmt_misc
), but either Wine or Mono would do the trick. – Stephen Kitt Aug 14 '20 at 18:41