pwshub.com

How Statically and Dynamically Linked Go Binaries Work

How Statically and Dynamically Linked Go Binaries Work

One of the biggest strengths of Go is its compiler. It abstracts many things for you 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 different executables.

In this article, we’ll explore statically and dynamically linked executables, internal and external linkers, and examine binaries using tools like file, ld, and ldd.

Here's what we'll cover:

  • Overview

  • What is Static and Dynamic linking?

  • Statically Linked Program

  • What is a binary anyway?

  • Dynamically Linked Program

  • Can we make it statically linked?

  • Internal vs External linker

  • Cross-Compilation

  • Bonus Point: Reduce binary size

  • Beware: LD_PRELOAD trick

  • Conclusion

  • Further Reads

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. This is because it's 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 it won't depend 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 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 not 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, so everything can be packaged in a static binary. Our program only prints a simple message to stdout, which Go can do internally without needing to use something from libc.

package main
import "fmt"
func main() {
    fmt.Println("hi, user")
}

What is a Binary Anyway?

We can use a file program to examine the file type first.

$ go build main1.go
$ file main1 | tr , '\n'
main1: ELF 64-bit LSB executable
 ARM aarch64
 version 1 (SYSV)
 statically linked
 Go BuildID=...
 with debug_info
 not stripped

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, and so on.

Note: in this article we’ll be working with Linux (Ubuntu) and its tooling, but the same is possible on other platforms.

And there is another Linux program called ldd that can tell us if the binary is statically or dynamically linked.

$ ldd main1
not a dynamic executable

Dynamically Linked Program

As mentioned above, Go has a mechanism called cgo to call C code from Go. 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.

package main
import (
    "fmt"
    "log"
    "net"
)
func main() {
    ipv4Addr, ipv4Net, err := net.ParseCIDR("192.0.2.1/24")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(ipv4Addr)
    fmt.Println(ipv4Net)
}

We can use our file and ldd programs again to examine the second binary.

$ go build main2.go
$ file main2 | tr , '\n'
main2: ELF 64-bit LSB executable
 ARM aarch64
 version 1 (SYSV)
 dynamically linked
 interpreter /lib/ld-linux-aarch64.so.1
 Go BuildID=...
 with debug_info
 not stripped
$ ldd main2
    linux-vdso.so.1 (0x0000ffff87c81000)
    libc.so.6 => /lib/aarch64-linux-gnu/libc.so.6 (0x0000ffff87a80000)
    /lib/ld-linux-aarch64.so.1 (0x0000ffff87c44000)

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 why you might want your binaries to be static, but the main one is to make deployment and distribution easier. But! It’s not always necessary, and by linking libc you benefit from host updates. Also, in case of our net package, you 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.

$ go build -tags netgo main2.go
$ ldd main2
not a dynamic executable
$ CGO_ENABLED=0 go build main2.go
$ ldd main2
not a dynamic executable

The 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), but you can specify which linker to use during the compilation time. This can give you 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.

$ go build -ldflags "-linkmode 'external' -extldflags '-static'" main2.go
# command-line-arguments
/usr/bin/ld: /tmp/go-link-629224677/000004.o: in function `_cgo_97ab22c4dc7b_C2func_getaddrinfo':
/tmp/go-build/cgo_unix_cgo.cgo2.c:60:(.text+0x30):
warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
$ ldd main2
not a dynamic executable

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. But it can be very tricky if your program uses cgo, because it’s generally tricky to cross-compile C code.

$ CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build main2.go
$ CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build main2.go
# runtime/cgo
cgo: C compiler "clang" not found: exec: "clang":
executable file not found in $PATH

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 notice, the output of the file command above had the following: “with debug_info not stripped“. This means that our binary has debugging information in it. But we usually don’t need it, and removing it may reduce the binary size.

$ go build main1.go
$ du -sh main1
1.9M    main1
$ go build -ldflags="-w -s" main1.go
$ du -sh main1
1.3M    main1
$ file main1 | tr , '\n'
main1: ELF 64-bit LSB executable
 ARM aarch64
 version 1 (SYSV)
 statically linked
 Go BuildID=...
 stripped

Beware: LD_PRELOAD Trick

The 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.

LD_PRELOAD=/path/to/my/malloc.so /bin/ls

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.

Hopefully, after reading this article, you now have a better understanding of how Go compilation works.

Further Reads

Source: freecodecamp.org

Related stories
1 month ago - TypeScript has become an industry standard for building large-scale applications, with many organizations choosing it as their primary language for application development. This tutorial will serve as your introductory guide to...
3 weeks ago - Go, also known as Golang, is a statically typed, compiled programming language designed by Google. It combines the performance and […] The post Go long by generating PDFs in Golang with Maroto appeared first on LogRocket Blog.
1 day ago - Get a sneak peek at the upcoming features in Python 3.13 aimed at enhancing performance. In this tutorial, you'll make a custom Python build with Docker to enable free threading and an experimental JIT compiler. Along the way, you'll...
6 days ago - CSS variables are *really* cool, and they're incredibly powerful when it comes to React! This tutorial shows how we can use them with React to create dynamic themes. We'll see how to get the most out of CSS-in-JS tools like...
6 days ago - The z-index property can be a tricky little bugger. Sometimes, no matter how much you crank up the number, the element never rises to the top! In this article, we explore stacking contexts, and see how they can thwart our efforts to use...
Other stories
1 hour ago - This release candidate, a near-final look at Deno 2, includes the addition of Node's process global, better dependency management, and various API stabilizations, and more.
1 hour ago - Published: September 19, 2024 The CSS Working Group has combined the two CSS masonry proposals into one draft specification. The group hopes that...
1 hour ago - Stay organized with collections Save and categorize content based on your preferences. Published: September...
3 hours ago - DNS monitoring tool is a cloud-based scanner that constantly monitors DNS records and servers for anomalies. This tool aims to ensure that users are sent to genuine and intended website pages, instead of fakes or replicas. It alerts users...
4 hours ago - Email spoofing is a malicious tactic in which cybercriminals send fake emails that look like they come from trusted organizations or individuals. When unsuspecting users act on these emails, they may unknowingly share sensitive data,...