simple cross compiling systems programming

Jon Brookes

2025-01-21

It is possible and often overkill to compile a program to do something a single line of Bash, Perl, Python, Ruby, even plain sh can do but what is not as easy is to handle json, present a web service such as a RESTful API endpoint in the same one liner or short script.

for now though, lets keep our example simple, just a few system commands for now and see how to compile and cross compile for an architecture different to our own.

having installed go, to prep a starter project

     mkdir etc_backup && cd etc_backup
     go mod init github.com/marshyon/etc_backup

a program to create a backup file of the /etc directory of our system could look like

     package main
     
     import (
     	"fmt"
     	"os"
     	"os/exec"
     	"time"
     )
     
     func main() {
     
     	cmd := exec.Command("sh", "-c", "echo 'hi there from sh cli'")
     	cmd.Stdout = os.Stdout
     	cmd.Stderr = os.Stderr
     	err := cmd.Run()
     	if err != nil {
     		fmt.Println("Error:", err)
     	}
     
     	currentTime := time.Now()
     	timeString := currentTime.Format("2006-01-02-1504")
     	tarFileName := fmt.Sprintf("backup_etc_%s.tar.tgz", timeString)
     	fmt.Println("backup file will be :", tarFileName)
     	tarCmd := exec.Command("sh", "-c", fmt.Sprintf("tar -czf %s /etc", tarFileName))
     	tarCmd.Stdout = os.Stdout
     	tarCmd.Stderr = os.Stderr
     	err = tarCmd.Run()
     	if err != nil {
     		fmt.Println("Error creating tar file:", err)
     	}
     }

To quickly run our go program in a shell of the above directory we can use go run main.go and this will run the above code.

But what we want to use this elsewhere as a complied executable, so that can be done with

	  go build -o etc_backup_x86_64

I’ve called this x86_64 here because thats the architecure I’m running on Linux. So the binary file this produces looks like

	  file etc_backup_x86_64 
	  etc_backup_x86_64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=dQlpa8rRiWq486-YXwRn/mGQCD_hKj83HXsTp-xr6/WE-E6m6h4ArHGpGNNnJI/dWE6TLhulkFWc2-MaeqC, with debug_info, not stripped

and it will run on any system with this chipset and architecture

I have an OpenWRT One device that arrived over the weekend and this has an ARM chipset, so like a mac, this would have different architecture and this executable would not run on that.

	  env GOOS=linux GOARCH=arm64 go build -ldflags "-s -w" -o etc_backup_arm64

this creates a new executable file that looks like this :

file etc_backup_arm64
etc_backup_arm64: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=ko7OIBeH7Dvd-zGKVNYi/k_0neJnA3MHW8ax43Rhx/l0UPdgHjDeAj3QL34I2i/k7WIdTXSAG7zVSDn05Rz, stripped

the -ldflas switch strips debug symbols and reduces the size of the executatble on my system from 2.5M to 1.7M and I can copy this to and run it on my new ARM chipset OpenWRT system :

     scp etc_backup_arm64  root@192.168.1.1:/root
     etc_backup_arm64                                               100% 2549KB  18.1MB/s   00:00

on my OpenWRT One in a new shell this can be executed like any other binary for that platform :

	  root@OpenWrt:~# ./etc_backup_arm64 
	  hi there from sh cli
	  backup file will be : backup_etc_2025-01-21-1740.tar.tgz
	  tar: removing leading '/' from member names

Now, this is only printing things out to the shell and running commands using systems exectuion but it has potential to do much more.

What is more, this executable is not easy to change and obscures what it does, unless we add command line options to show help of course but that would be another missive.

Importantly, we can checksum this script and see that it is unchanged but it is also, inconvenient for someone to poke around inside it, change it and cause mischief, should that be a concern that we may have.

The possible use cases are endless and this opens up another world of programming in Go to add to our skills in shell scripting and automation where a fully functional program can be copied to a server and work without needing dependencies as would often a script in Python, Ruby or others like them. This has produced for us a ‘statically compiled’ and ‘stand alone’ executable. So long as it matches the architecture of the target system, it will run and we dont need to write our code on the same architecture even.

This makes light(er) work of developing software for embedded devices or appliances like the OpenWRT One that would typiclly need us to set up an entire build tool chain to cross compile C. This can be a lot of bother.

Not any more.