In the past few years, some programs written in Golang and Rust have often said they are statically compiled, and do not need any dynamic libraries.

Is that actually true?

1. Dynamically Linked Binaries

Commonly, programs are compiled with dynamically linked libraries.

This has the advantage of sharing and upgrading all common libraries, without having to recompile each program each time a library has an update.

But, this makes installing those programs more involved: each dependency much be installed first, and with a compatible version.

Most of the time, finding those required libraries is fairly straight-forward with ldd.

$ ldd /nix/store/v48s6iddb518j9lc1pk3rcn3x8c2ff0j-bash-interactive-5.1-p16/bin/bash (0x00007fff53790000) => ()/ (0x00007ff71113a000) => ()/ (0x00007ff71112c000) => ()/ (0x00007ff7110b7000) => ()/glibc-2.34-210/lib/ (0x00007ff7110b2000) => ()/glibc-2.34-210/lib/ (0x00007ff710eb4000)
  ()/glibc-2.34-210/lib/ => ()/glibc-2.34-210/lib64/ (0x00007ff711194000)

On my system, bash expects quite a few *.so libraries, as expected.

2. Statically Linked Binaries

A program statically compiled is quite easy to find out, and this is what we can expect:

$ ldd myprogram
  not a dynamic executable

2.1. Example 1: jq

On jq’s website, one can read:

jq is written in portable C, and it has zero runtime dependencies. You can download a single binary, scp it to a far away machine of the same type, and expect it to work.

Let’s check:

$ ldd jq-linux64
  not a dynamic executable

That’s legit.

2.2. Example 2: yq

On yq’s website, one can read:

yq is written in go - so you can download a dependency free binary for your platform and you are good to go! If you prefer there are a variety of package managers that can be used as well as docker, all listed below.

Let’s check:

$ ldd yq_linux_amd64
  not a dynamic executable

Also legit.

2.3. Example 3: rg

On ripgrep’s website, one can read:

Linux and Windows binaries are static executables.

Let’s check:

$ ldd rg
  not a dynamic executable

And also legit.

3. Making Your Own

In most languages before Go and Rust, static linking used to be a lot more involved.

Yet, we can still write some assembly ourselves fairly simply:

format ELF64 executable 3

segment readable executable

entry main

; sys_write
; Writes _count_ characters from buffer address _buffer_ to file descriptor _fd_.
; - rdi: fd
; - rsi: buffer address
; - rdx: count
; -> rax: number of characters written to fd
    mov rax, linux_sys_write

; sys_exit
; Exit program with given code
; - rdi: code
    mov rax, linux_sys_exit

    mov rdi, fd_stdout
    lea rsi, [msg]
    mov rdx, msg.len
    call sys_write
    mov rdi, 0
    call sys_exit

segment readable writeable

ascii_lf = 0xA
msg db "Hello, human!", ascii_lf, "how are you doing?", 0
.len = $ - msg
linux_sys_write = 1
linux_sys_exit = 60
fd_stdout = 1

Which you can compile, run and check:

$fasm hello.fasm
flat assembler  version 1.73.30  (16384 kilobytes memory, x64)
3 passes, 266 bytes.

$ ./hello
Hello, human!
how are you doing?

$ ldd hello
  not a dynamic executable