Nix Tricks

Published:
January 30, 2024

In which I compile snippets of useful Nix code

Cross-Compiling Arm Assembly in a Nix Flake

We want to compile this trivial assembly targeting ARM assembly on a x86_64 Linux machine (running Debian 12 with Nix in this case):

  .global main
  .type main, %function
main:
  mov w0, #123
  ret

The assembly file is in a file called src/main.s.

The program return 123 as a status code, so we can check whether the program ran correctly later by checking echo $? in bash or similar. We can run the program using qemu-aarch64 (not qemu-system-aarch64) to emulate an ARM CPU and corresponding Linux user land, instead of having to spin up a virtual machine – quite handy when reverse engineering binaries on an x86_64 host.

We want to work with a Nix flake here. That means better support for different targets, and the ability to have better reproducibility between builds, as all Nix dependencies are fixed in a flake.lock file.

I also want to use flake-utils for an easier time specifying the flake file.

{
  description = "Arm64 (aarch64) cross-compile demo";

  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs {
          inherit system;
          crossSystem = {
            config = "aarch64-unknown-linux-gnu";
          };
        };
      in
      {
        packages = rec {
          cross-arm-64-asm = pkgs.callPackage
            ({ stdenv, gcc }:
              stdenv.mkDerivation {
                name = "cross-arm-64-asm";
                nativeBuildInputs = [
                  gcc
                ];
                phases = [ "buildPhase" "installPhase" ];
                buildPhase = ''
                  $CC -march=armv8-a ${./src/main.s} -o main
                '';
                installPhase = ''
                  mkdir -p $out/bin
                  cp main $out/bin
                '';

              }
            )
            { };
          default = cross-arm-64-asm;
        };
      }
    );
}

Build the file by running nix build -L .#. On my system I got the following output:

warning: Git tree '/home/justusperlwitz/projects/nix-tricks' is dirty
warning: Ignoring setting 'auto-allocate-uids' because experimental feature 'auto-allocate-uids' is not enabled
warning: Ignoring setting 'impure-env' because experimental feature 'configurable-impure-env' is not enabled
cross-arm> Running phase: buildPhase
cross-arm> Running phase: installPhase

We see that the output is placed in a symlinked folder called result.

# tree result
result
└── bin
    └── main

2 directories, 1 file

We check the contents of the resulting result/bin/main file:

# file result/bin/main
result/bin/main: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /nix/store/j8h1n0kgl15gpk270i7mr441yamnzvdg-glibc-aarch64-unknown-linux-gnu-2.38-27/lib/ld-linux-aarch64.so.1, for GNU/Linux 3.10.0, not stripped

If you have QEMU on your system, you can then run it like so:

$qemu-aarch64 ./result/bin/main
$echo $?
# Outputs `123`

Further reading

I would be thrilled to hear from you! Please share your thoughts and ideas with me via email.

Back to Index