Patching GCC to build Actually Portable Executables
2023-07-13: I wrote a ~2000-line gcc
patch to simplify building
Actually Portable Executables with Cosmopolitan Libc. Now you can build
popular software such as bash
, curl
, git
, ninja
, and even gcc
itself,
with Cosmopolitan Libc via the ./configure
or cmake
build system, without
having to change source code, and the built executables should run on Linux,
FreeBSD, MacOS, OpenBSD, NetBSD, and in some cases even Windows too1.
Here’s how you can port your own software to Cosmopolitan Libc now:
- Clone the Cosmopolitan Libc repo, and set up the
/opt/cosmo
and
/opt/cosmos
directories as per the README. - Set the necessary environment variables:
export COSMO=/opt/cosmo
export COSMOS=/opt/cosmos
export CC=$COSMO/tool/scripts/cosmocc
export CXX=$COSMO/tool/scripts/cosmoc++
export LD=$COSMO/tool/scripts/cosmoc++
- (Optional): here are my forks of
gcc
and
musl-cross-make
, which you can use to buildgcc
with the
latest version of my patch. If you do this, remember to editcosmocc
and
cosmoc++
accordingly. - Let’s try a simple
hello world
example first:
#include
int main() {
printf("Actually Portable Executable built via cosmoccn");
return 0;
}
To build the APE for a single file, you can just
cosmocc -o hello.com hello.c
-
to build software that uses
./configure
orcmake
, export the
above environment variables, and then run the necessary build steps to get a
static debug executable that runs on Linux -
to get an Actually Portable Executable, run
objcopy -SO binary your-software your-software.com
$COSMO/o/tool/scripts/zipcopy.com your-software your-software.com
When Cosmopolitan Libc burst onto the scene with the Hacker News post about the
redbean
webserver in 2021, an immediate question was how existing
C software would run on it. The best way to test a new libc is to build more
code on top, and many codebases have been ported to use cosmo. The
porting efforts have also helped fill out the libc API. However, a new libc must
also fit in with how C software is conventionally built – things like
./configure
and cmake
– so that porting more software is easy. When
pthreads
became available last year, I said: porting is just a
matter of convincing the build system. I believe my gcc
patch is a
big step towards seamlessly building a lot of software with Cosmopolitan Libc.
This blog post will cover how the patch came to be.
Introduction
Lua was the first programming language to be ported to Cosmopolitan Libc,
followed by Wren, Janet, and Fabrice Bellard’s
quickjs
. There appeared to be a common change across the ports:
switch
statements with system values like SIGTERM
or EINVAL
had to be
rewritten as if
statements. 2021-03-24: I raised issue #134 on Github
about this, and got the below answer from Justine Tunney:
Code that uses
switch
on system constants needs to be updated to useif
statements. That’s unavoidable unfortunately. It’s the one major breakage we
needed to make in terms of compatibility. The tradeoff is explained in the APE
blog post […] C preprocessor macros relating to system interfaces need to be
symbolic. This is barely an issue, except in cases likeswitch(errno){case EINVAL:}
If we feel comfortable bending the rules […]
2022-09-05: After writing a bunch of ports, it was clear that the
switch(errno)
pattern is quite common, and porting software would be much
faster if the rules could be bent automatically. I decided to try solving
this
, but let’s define what this
is.
switch
to if
Consider the below code snippet:
// somewhere within your code
switch (errno) {
case EINVAL:
printf("you got EINVALn");
break;
#ifdef EAFNOSUPPORT
case EAFNOSUPPORT:
// fallthrough
#endif
case ENOSYS:
printf("you got ENOSYSn");
break;
default:
printf("unknown errorn");
break;
}
The C standard2 says that case
labels need to be compile-time
constants. If EINVAL
is not a compile-time constant, you would get an error
like case label does not reduce to an integer constant
when compiling this
snippet with gcc
. So you’d need to rewrite the switch
statement into an if
statement. My code rewrites switch
statements like this:
// rewriting the switch to an if
{ // maintain scoping
if(errno == EINVAL) goto caselabel_EINVAL;
#ifdef EAFNOSUPPORT
if(errno == EAFNOSUPPORT) goto caselabel_EAFNOSUPPORT;
#endif
if(errno == ENOSYS) goto caselabel_ENOSYS;
goto caselabel_default;
caselabel_EINVAL:
printf("you got EINVALn");
goto endofthis_switch; // break
#ifdef EAFNOSUPPORT
caselabel_EAFNOSUPPORT:
// fallthrough
#endif
caselabel_ENOSYS:
printf("you got ENOSYSn");
goto endofthis_switch;
caselabel_default:
printf("unknown errorn");
goto endofthis_switch;
endofthis_switch:
((void)0);
}
The above pattern might not be the most elegant3 but it handles all
the examples I have seen across codebases.
struct
initializations
While constructing test cases for switch
statements that would have to be
rewritten, I came across a related problem that also assumed compile-time
constants – static
or const
struct
initializations. The faulthandler
module in CPython is a real-life example of this, but let’s look
at the below snippet:
struct toy {
int status;
int value;
};
void func() {
struct toy t1 =
{.status = EINVAL, .value = 25}; // ok
const struct toy t2 =
{.status = EINVAL, .value = 25}; // error
static struct toy t3 =
{.status = EINVAL, .value = 25}; // error
}
const struct toy gt2 =
{.status = EINVAL, .value = 25}; // error
static struct toy gt3 =
{.status = EINVAL, .value = 25}; // error
If EINVAL
is not a compile-time constant, four of the initializations above
are not valid4 in C. My fix was to dummy-initialize the struct
s and
then add an if
statement or an __attribute__((constructor))
to fill
in the correct value(s) before they are used at runtime.
Outline of the problem
From the above two sections we have defined the problem space: certain switch
statements and struct
initializations may not compile, because they rely on
system values being compile-time constants. Now:
- Can we (automatically) find these
switch
statements and convert them into
if
s andgoto
s? - Can we (automatically) find these
struct
initializations and insert the
necessary code that fills in the correct runtime values?
I started off trying to automate these conversions using sed
in a shell
script. After a while, it was a python script with some extra regex work to
handle switch
fallthroughs. But neither of these worked completely because of
the C preprocessor and ifdef
s. It is difficult to perform the rewrite as a
text substitution when you do not know which ifdef
s would activate during
compilation. Then, maybe I could perform the rewrite as an AST substitution –
if the code was available as an AST (abstract syntax tree), performing a rewrite
would be replacing one subtree with another. I could also avoid dealing with
the C preprocessor…
Curiously exploring gcc
plugins
gcc
provides a plugin architecture via which you can access the AST of the
code being compiled. You compile your code as a shared object, load it alongside
gcc
with the flag -fplugin=your-plugin.so
. The gcc
internals
documentation provides detailed explanations as to when and how the
plugin writers can interact with the compilation process, and there are some
wonderful articles with examples of how you can write your own
gcc
plugins (perhaps I should write my own article with examples). For now,
let’s just remember the following:
gcc
allows plugins to activate callbacks during plugin events:
enum plugin_event
{
PLUGIN_START_PARSE_FUNCTION, /* Called before parsing the body of a function. */
PLUGIN_FINISH_PARSE_FUNCTION, /* After finishing parsing a function. */
PLUGIN_FINISH_DECL, /* After finishing parsing a declaration. */
PLUGIN_PRE_GENERICIZE, /* Allows to see low level AST in C and C++ frontends. */
PLUGIN_INFO, /* Information about the plugin. */
PLUGIN_INCLUDE_FILE, /* Called when a file is #include-d */
/* [removed some of the events] */
PLUGIN_START_UNIT, /* Called before processing a translation unit. */
PLUGIN_FINISH_UNIT, /* Useful for summary processing. */
PLUGIN_FINISH /* Called before GCC exits. */
};
The callback is provided some data from gcc
(which may be a pointer to an AST,
a string, or something else depending on the event), and a pointer to data that
you might have initialized at startup. Since we need to manipulate the AST, the
PLUGIN_PRE_GENERICIZE
, PLUGIN_FINISH_PARSE_FUNCTION
, or PLUGIN_FINISH_DECL
events seem viable.
-
The AST provided by
gcc
is a tree structure, which takes a while to connect
to the original C code, but is easy to read and manipulate afterwards.gcc
provides a function calleddebug_tree
that prints the AST. It’s probably
the function you will use the most when developing a plugin. There are also
convenient macros to access, say, the second arg of theADD_EXPR
or the
name of theVAR_DECL
. -
gcc
provides a functionwalk_tree_without_duplicates(ast, your_callback, your_data)
which walks through the entire AST in pre-order traversal and
presents each node toyour_callback
to read, process, and modify.
2022-12-27: I set up a gcc
plugin that would activate on
PLUGIN_PRE_GENERICIZE
and walk through the AST looking for switch
statements
to rewrite. Simple enough, seemed like it would work, so I tried the below
example:
switch(errval) {
case 0:
break;
case SIGILL:
printf("you got a SIGILLn");
break;
default:
printf("unknown errorn");
}
But:
examples/ex1_modded.c: In function ‘exam_func’:
examples/ex1_modded.c:25:5:
error: case label does not reduce to an integer constant
25 | case SIGILL:
| ^~~~
gcc
is still raising an error due to the case
label? Why does the plugin not
activate? I check the AST with debug_tree
, and case 0:
was there:
arg:1
stmt
arg:0 arg:2 >
stmt >
But case SIGILL:
?
stmt <cal