Actually Portable Executables
27 Feb 2021I came across Cosmopolitan on Hacker News, and I was initially confused, due to a few memories of cross-compilation nightmares: while it should be possible to compile for the same architecture regardless of operating system, wouldn’t the OS get confused by the leading bytes of the executable? I read the article explaining how it works, but most of it went over my head.
The example on the Github README used the following script for compilation:
gcc -g -O -static -nostdlib -nostdinc -fno-pie -no-pie -mno-red-zone \
-o hello.com.dbg hello.c -fuse-ld=bfd -Wl,-T,ape.lds \
-include cosmopolitan.h crt.o ape.o cosmopolitan.a
objcopy -S -O binary hello.com.dbg hello.com
I converted it into a simple Makefile to run the compilation commands. I tried a bunch of simple C programs (basic arithmetic, reading and writing to files) on Linux+Windows (compiled on Linux), and all of them worked.
Compiling Lua with Cosmopolitan
I decided to try compiling a high-level language built on C. I originally picked Python, but the Makefile for Python seemed too complicated to mess with, so I then picked Lua, which looked much simpler in comparison.
I started out by blindly copy-pasting the flags and includes used in the sample compilation on Github. Ah, it would have been wonderful for my laziness if it compiled out of the box. Following is a play-by-play commentary of trying to compile Lua.
First problem I ran into was header clashes: if I didn’t put -nostdlib -nostdinc
while compiling each object file, -include cosmopolitan.h
would clash with the system headers. But blocking the system headers meant I would have to change every #include
of a system header. I created a bunch of dummy headers with the same names as those in the C stdlib and and included to those instead.
Naming clashes: some of the macros in cosmopolitan.h
clashed with macro/function names in Lua: reverse
and isempty
. I changed the Lua source to avoid this.
A macro FIRST_RESERVED
was broken because UCHAR_MAX
was missing. I thought UCHAR_MAX
was supposed to be in limits.h
– the limits.h
part of cosmopolitan.h
did not have UCHAR_MAX
(It had SCHAR_MAX
, though.) I added in a #define
stating UCHAR_MAX
as __UINT8_MAX__
(ie 255).
The default Lua Makefile attempts to use _setjmp
/_longjmp
in ldo.c
when on Linux. I disabled the LUA_USE_LINUX
flag for compiling the object files, but this caused an issue with tmpnam
in loslib.c
(mkstemp
is available in Cosmopolitan). I changed the Lua source to use setjmp
/longjmp
. A similar issue showed in lauxlib.c
for sys/wait.h
(which is a no-op in non-POSIX systems, as per the Lua source code), and in liolib
for sys/types.h
so disabled LUA_USE_POSIX
over there as well.
The localeconv()
function (part of locale.h
) was not implemented in cosmopolitan.h
, and this caused an error while compiling lobject.c
(macro lua_getlocaledecpoint()
depended on localeconv()
). Changed the macro to just return '.'
.
The panic
function in Lua static int panic (lua_state*)
clashed with that in Cosmopolitan void panic(void)
. Renamed the lua function to lua_panic
. This triggered an error where the panic
function was being called in luaL_newstate
, so I changed the name there as well.
luaL_loadfilex
caused a frame size error – I have never seen this before. A quick internet search shows that this is because a large buffer is allocated on stack when entering the function, and yes, luaL_loadfilex
allocates a loadF
object containing a char
buffer of BUFSIZ
. I reduced the size of the buffer to BUFSIZ - 64
.
loslib.c
reuiqres the setlocale()
and LC_*
from locale.h
, which is defined as an extern value in cosmopolitan.h
, but that definition is somehow not enough.. screw it, I just disabled os_setlocale
in loslib.c
, and then it compiles.
Linking the object files
Ok, time for linking …
gcc -std=gnu99 -o lua lua.o liblua.a -lm -Wl,-E -ldl
/usr/bin/ld: errno: TLS definition in //lib/x86_64-linux-gnu/libc.so.6 section
.tbss mismatches non-TLS reference in liblua.a(lauxlib.o)
/usr/bin/ld: //lib/x86_64-linux-gnu/libc.so.6: error adding symbols: bad value
collect2: error: ld returned 1 exit status
I forgot, I shouldn’t -lm
or -ldl
. Ok, let’s try with all the object files instead of liblua.a
:
/usr/bin/ld.bfd: lvm.o: in function `l_strcmp':
lvm.c:(.text+0x59): undefined reference to `strcoll'
/usr/bin/ld.bfd: lmathlib.o: in function `math_tanh':
lmathlib.c:(.text+0x21f): undefined reference to `tanh'
/usr/bin/ld.bfd: lmathlib.o: in function `math_sinh':
lmathlib.c:(.text+0x24f): undefined reference to `sinh'
/usr/bin/ld.bfd: lmathlib.o: in function `math_cosh':
lmathlib.c:(.text+0x27f): undefined reference to `cosh'
collect2: error: ld returned 1 exit status
Umm… okay, it looks like some of the functions defined in the cosmopolitan header are yet to be implemented in the static library. That’s okay, I can just quickly fill in the math functions, and I’ll comment out strcoll
for now, just because I want to see it compile…. and it successfully compiles!! Let’s run objcopy
before trying it out on a system though.
$ objcopy -S -O binary lua lua.exe
$ ls -al
-rwxr-xr-x 1 1953720 Feb 27 01:33 lua
-rwxr-xr-x 1 344064 Feb 27 01:39 lua.exe
That size reduction seems a little too drastic, but let’s see if it runs on Linux:
Awesome. How about Windows?
Summary: it is actually portable
This is pretty incredible: I just had to modify a few lines in a Makefile and some C source files, and I got a Lua executable that works both on Linux and Windows (and possibly others as well). Granted, there are still some details to be filled out (floating point calculation above prints a g
), but Cosmopolitan is currently at release 0.0.2, so there is a lot of time.
Hopefully this means that other languages that have source code completely in C can also be compiled once and run anywhere. Actually Portable Python next, maybe?