Overview
One of the biggest strengths of Go is its compiler, which abstracts many things for a programmer and lets you compile your program easily for almost any platform and architecture.
And though it seems easy, there are some nuances to it and multiple ways of compiling the same program which results in the different executable.
In this article, we’ll explore statically and dynamically linked executables, internal and external linkers, and examine binaries using tools like file, ld, ldd, etc.
What is Static and Dynamic linking?
Static linking is the practice of copying all the libraries your program needs directly into the final executable file image.
And Go loves and wants that whenever it’s possible, since it is more portable, as it doesn’t require the presence of the library on the host system where it runs. So your binary can run on any system no matter which distro/version, and not depending on any system libraries.
Dynamic linking on the other hand is when external or shared libraries are copied into the executable file by name during the run time.
And it has its own advantages too. For example the program can re-use popular libc libraries that are available on the host system and don’t re-implement them, you can also benefit from host updates without re-linking your program. It can also reduce the executable file size in many cases.
Statically Linked Program
Let’s review a program that will always get statically linked. This program doesn’t call C code using cgo, therefore everything can be packaged in a static binary.
What is a binary anyway?
We can use a file program to examine the file type first.
It tells us that it’s an ELF (Executable and Linkable Format) executable file. It also tells us that it’s “statically linked“.
We won’t dive into what ELF is, but there are other executable file formats, ELF is the default one on Linux, Mach-O is the default one for macOS, PE/PE32+ for Windows, etc.
Note: in this article we’ll be working with Linux (Ubuntu) and its tooling, however the same is possible on other platforms.
And there is another Linux program ldd that can tell us if the binary is statically or dynamically linked.
Dynamically Linked Program
As mentioned above, Go has a mechanism called cgo to call C code from Go, and even Go’s stdlib uses it in multiple places, for example in the net package, where it uses the standard C library to work with DNS.
Importing such packages or using cgo in your code by default produces a dynamically-linked binary, linked to those libc libraries.
We can use our file and ldd programs again to examine the second binary.
The file program now shows us that it is a dynamically liked binary and ldd shows us the dynamic dependencies of our binary. In this case it relies on libc.so.6 and ld-linux which is a dynamic linker for Linux systems.
Can we make it statically linked?
There are multiple reasons when you want your binaries static, but the main one is to ease the deployment and distribution. However! It’s not always necessary, by linking libc you benefit from host updates and in case of our net package use those complex DNS lookup functions included in libc.
What’s interesting is that Go’s net package also has a pure-Go version, which makes it possible to disable cgo during compile time. You can do it by specifying build tags or by fully disabling cgo using CGO_ENABLED=0.
The screenshot above proves that we end up with a static binary in both cases.
Internal vs External linker
Linker is a program that reads the Go archive or object for a package main, along with its dependencies, and combines them into an executable binary.
By default Go’s toolchain uses its internal linker (go tool link), however you can specify which linker to use during the compilation time, which can give us a combination of benefits of a static binary as well as full-fledged libc capabilities.
On Linux the default linker is gcc’s ld. And we can tell it to produce a static binary.
It works, but we have a warning here. In our case glibc uses libnss to support a number of different providers for address resolution services and you cannot statically link libnss.
Other cgo packages may produce similar warnings and you’ll have to check the documentation to see if they’re critical or not.
Cross-Compilation
As mentioned in the introduction, cross-compilation is a very nice feature of Go, it lets you compile your program for almost any platform/architecture. However, it can be very tricky if your program uses cgo, because it’s generally tricky to cross-compile C code.
You can overcome that by installing the toolchain for the target OS and/or architecture.
If you can, it’s always better to just not use cgo for cross-compilation. You’ll get stable binaries which are statically linked.
Bonus Point: Reduce binary size
As you may noticed the output of the file command above had the following: “with debug_info not stripped“. It means that our binary has a debugging information in it. But we usually don’t need it, and removing it may reduce the binary size.
Beware: LD_PRELOAD trick
Linux system program ld-linux.so (dynamic linker/loader) uses LD_PRELOAD to load specified shared libraries. In particular, before any other library, the dynamic loader will first load shared libraries that are in LD_PRELOAD.
The LD_PRELOAD trick is a powerful technique used in dynamically linked binaries to override or intercept function calls to shared libraries.
By setting the LD_PRELOAD environment variable to point to a custom shared object file, users can inject their own code into a program's execution, effectively replacing or augmenting existing library functions.
This method allows for various applications, such as debugging, testing, and even modifying program behaviour without altering the original source code.
It also shows that statically linked binaries are more secure, as they don’t have this issue since they don’t seek any external libraries. Also, there is a “secure-execution mode” - a security feature implemented by the dynamic linker on Linux systems to restrict certain behaviours when running programs that require elevated privileges.
Conclusion
Computers are not magic, you just have to understand them.
And understanding Go compilation and execution processes is crucial for developing robust cross-platform applications.
Thanks for the article ! May I ask which tool do you use to generate the code snippet images ? They look great.