Static Linking with Nim


by Andreas Schipplock


Introduction

I follow nims (formerly known as nimrod) development for quite some time and always was interested in trying it out but never got time for it. But now since I had some days off and was in a "hacker mood" I tried it out. I'm aware that I'm the guy who always preaches to stay with the tools you already know and which are proven (see Programming Language Jungle) but trying out new stuff is important. I'm not saying to use nim to make a living nor do I use it myself for that purpose. But it's still fun to play with it because it's new and who knows...perhaps it has some nice ideas.

And due to my new interest in musl-libc, an alternative libc, and because nim compiles down to "c", I wanted to make nim use my "alternative" libc and compile the binary statically.

Why statically? Well, statically linked binaries start faster because there are no library lookups but the main problem with this approach is file size; especially with glibc. But they are portable which is important if you need those binaries in an initramfs e.g.

How To Link Statically

A simple nim example:

echo("Whats your name? ")
var name: string = readLine(stdin)
echo("Hi, ", name, "!")

You can create a statically compiled binary with the following command:

nim --opt:size --passL:"-static" c -d:release test.nim

On my 64bit slackware 14.1 the resulting binary will be 896K in file size. You can check with "ldd test" if your binary really is statically linked. You can cut that down to approx 400K (upx --best is your friend).

To use the musl-libc wrapper "musl-gcc", I tried this:

nim --opt:size --gcc.exe:"/usr/local/musl/bin/musl-gcc" --passL:"-static" c -d:release test.nim

Unfortunately the file size stayed the same so it's obviously *not* using my musl :P. But the people in #nim at freenode are very kind. They helped me out on this. Jehan_ suggested: --cc:$yourC-Compiler.

But with 'musl-gcc' nim says 'unknown C compiler'...god...then the user gokr redirected me to http://nim-lang.org/question.html ... if you scroll down long enough, you will find out that you can set the compiler in config/nim.cfg:

cc = gcc

Oh great! I set it to '/usr/local/musl/bin/musl-gcc' but god damn it; same "unknown C compiler" message as before. Pah! The user "def-" finally told me to use --gcc.exe:"foo" so I got this:

nim --opt:size --gcc.exe:"/usr/local/musl/bin/musl-gcc" --passL:"-static" c -d:release test.nim

But as I learned before this still seems to use the "wrong" cc to link the binary.

So regarding Mr. jehan_ I added:

gcc.exe = "/usr/local/musl/bin/musl-gcc"

to my nim.cfg but the filesize was the same. That couldn't be right.

I finally spotted the following in the config:

arm.linux.gcc.linkerexe = "arm-linux-gcc"

So I tried the following:

gcc.linkerexe = "/usr/local/musl/bin/musl-gcc"

Aaand it works!

So my first 3 lines of my nim.cfg look like this:

cc = gcc
gcc.exe = "/usr/local/musl/bin/musl-gcc"
gcc.linkerexe = "/usr/local/musl/bin/musl-gcc"

With this configuration I can create my static binary with this command:

nim --opt:size --passL:"-static" c -d:release test.nim

The resulting statically linked binary will be 48K. A lot smaller than the one linked against glibc. You can reduce this size with "upx --best $yourBinary". In my case it will shrink to 24K which is even less than the dynamic glibc linked variant.

Warning:if you don't pass "-static" to musl-gcc or --passL, then you cannot distribute the resulting binary to non-musl-based systems.

How To Use Musl As The Program Interpreter

In case you can't have a static binary and you are still using musl as your libc, you will see something like this when you execute readelf -ld yourbinary:

[Requesting program interpreter: /lib/ld-musl-x86_64.so.1]

The target system might not have this. If you target Debian, openSuse, Fedora, Ubuntu, whatever, this loader won't be present. People trying to execute your binary will get the following error:

bash: ./a.out: File or directory not found

This message is misleading at first but it basically means that it can't find the program interpreter which is defined in the INTERP header.

If you still want to use musl-libc (if you only tested your application with it, it's a good idea actually), you can still ship your application that will run on major linux distributions.

To accomplish this, copy: "libc.so" from "lib" which you can find inside your musl build directory (or copy the target file of "ld-musl-x86_64.so.1" (it's a symlink and links to a "libc.so").

Now update the INTERP header with 'patchelf':

patchelf --set-interpreter libc.so a.out

Now if you check again with readelf -ld yourbinary | grep interpreter you will see it has updated the header of your binary:

0x0000000000000001 (NEEDED)
Shared library: [libc.so] program interpreter

and then you can execute your binary without a problem :). Of course you still need to take into consideration that the target system needs to fulfill the library dependencies as your binary isn't static anymore. You can check this with objdump -x a.out | grep NEEDED. In many cases you can just create a static binary and don't need to bother. But in some cases you cannot. Some libraries cannot be built statically and then you will have a problem; well, not anymore, but you have to give up a static binary then :).

For all the tests I used Nim Compiler Version 0.10.2 (2014-12-29) [Linux: amd64].