Skip to main content

Go Build!

Shenzhen, China

In the previous post I build an NTS Client from source. I now need to compile it for an ARM system and reduce the file size of the generated binary.

Cross-compiling with Go

The original binary was compiled on a x86-64 Linux system and with default settings resulted in the following file:

file ntsclient

ntsclient: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, Go BuildID=jk1ySsUE9xCbQQElzPBj/mKJ-lNqe4rCLOALwJ0Uh/lg1ziBDAYw1FdjS_SnD_/6JHvJ15niw3mnL010nFR, with debug_info, not stripped

To use the binary on an ARM system we need to use the amazing cross-compiling capabilities of Go. E.g. to create the arm64 version of the file I can run:

env GOOS=linux GOARCH=arm64 go build -o ntsclient_arm64

The resulting binary can be used on an 64bit ARM system:

file ntsclient_arm64

ntsclient_arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=MR5GvnV5S4XWxDENNLTE/ZZ2k_Dx5K6CdKRTzWlK5/Zbl1ahI9CUA-f28opmhH/0Zh0CFmxrhuWRZ-kOiMt, with debug_info, not stripped

You can check the supported combinations of GOOS and GOARCH with:

go tool dist list

aix/ppc64 | android/386 | android/amd64 | android/arm | android/arm64 | darwin/amd64 | darwin/arm64 | dragonfly/amd64 | freebsd/386 | freebsd/amd64 | freebsd/arm | freebsd/arm64 | illumos/amd64 | ios/amd64 | ios/arm64 | js/wasm | linux/386 | linux/amd64 | linux/arm | linux/arm64 | linux/loong64 | linux/mips | linux/mips64 | linux/mips64le | linux/mipsle | linux/ppc64 | linux/ppc64le | linux/riscv64 | linux/s390x | netbsd/386 | netbsd/amd64 | netbsd/arm | netbsd/arm64 | openbsd/386 | openbsd/amd64 | openbsd/arm | openbsd/arm64 | openbsd/mips64 | plan9/386 | plan9/amd64 | plan9/arm | solaris/amd64 | windows/386 | windows/amd64 | windows/arm | windows/arm64

We can automate a multi-architecture build with a script build.sh:

#!/usr/bin/bash
archs=(amd64 arm arm64)

for arch in ${archs[@]}
do
        env GOOS=linux GOARCH=${arch} go build -o prepnode_${arch}
done

Golang on a Diet

The resulting files - compared to a similar C program - are generally huge. In the NTS client example I end up with 7.4 - 7.7 MB files:

7965759 Oct  5 15:37 ntsclient_amd64
7665133 Oct  5 15:37 ntsclient_arm
7635040 Oct  5 15:24 ntsclient_arm64

The following build flags can help us reducing the binary size:

  • ldflags
    • -s omits the symbol table and debug information
    • -w omits DWARF debugging information.

So let's update the build script accordingly:

#!/usr/bin/bash
archs=(amd64 arm arm64)

for arch in ${archs[@]}
do
        env GOOS=linux GOARCH=${arch} go build -ldflags "-s -w" -o prepnode_${arch}
done

Now we are down to 5.0 - 5.2 MB:

5459968 Oct  5 16:55 ntsclient_amd64
5242880 Oct  5 16:55 ntsclient_arm
5242880 Oct  5 16:55 ntsclient_arm64

UPX

UPX is a free, secure, portable, extendable, high-performance executable packer for several executable formats. You can install the latest version from Github or use your package manager:

sudo pacman -S utx
upx --help

                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2020
UPX git-d7ba31+ Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020

Usage: upx [-123456789dlthVL] [-qvfk] [-o file] file..

Commands:
  -1     compress faster                   -9    compress better
  --best compress best (can be slow for big files)
  -d     decompress                        -l    list compressed file
  -t     test compressed file              -V    display version number
  -h     give this help                    -L    display software license

Options:
  -q     be quiet                          -v    be verbose
  -oFILE write output to 'FILE'
  -f     force compression of suspicious files
  --no-color, --mono, --color, --no-progress   change look

Compression tuning options:
  --brute             try all available compression methods & filters [slow]
  --ultra-brute       try even more compression variants [very slow]

So to get the maximum amount of compress let's try Ultra Brute:

upx --ultra-brute -ontsclient_upx_arm ntsclient_arm

    File size         Ratio      Format      Name
--------------------   ------   -----------   -----------
5242880 ->   1421184   27.11%    linux/arm    ntsclient_upx_arm

So we went from around 7 MB down to 1.4 MB.