Building Go Binaries for Siemens IoT 2040: Fix MMX Error

David Krause13 min read
PLC HardwareSiemensTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Problem Statement

A Go program compiled on a standard Linux workstation with GOOS=linux GOARCH=386 GO386=quark produces an ELF binary that links successfully but fails at process startup when executed on the Siemens SIMATIC IoT 2040. The runtime aborts with the following diagnostic printed to stderr and the process exits with a non-zero status:

This program can only be run on processors with MMX support.

The same source code compiled with the generic GOARCH=386 toolchain target produces a binary that loads and runs on any modern x86 laptop, server, or industrial PC, but refuses to start on the IoT 2040 because its Intel Quark SoC lacks the MMX instruction set extension. This article documents the root cause, the affected Go toolchain versions, the patch set carried in the official meta-iot2000 Yocto layer, and the verified procedure to produce an executable that runs natively on the Quark core.

Affected Hardware: SIMATIC IoT 2040 Architecture

The Siemens SIMATIC IoT 2040 is an industrial IoT gateway in the SIMATIC family. The relevant CPU, memory, and OS characteristics that drive the Go binary failure are summarized below.

Parameter Value
SoC Intel Quark SE x1000 (also marketed as Quark C1000)
CPU core Single Pentium-class x86 32-bit core, no out-of-order execution
Clock Up to 100 MHz core / 32 kHz on the always-on sensor subsystem
SIMD extensions None — no MMX, no SSE, no SSE2, no AVX
FPU IEEE 754 single-precision hardware float, double-precision microcoded
RAM 512 MB DDR3 on-board
Storage 8 GB or 16 GB eMMC, plus microSD slot
Default OS image Siemens Yocto-based Linux (Poky), Intel microarchitecture quark
Kernel Linux 4.4 / 4.9 / 4.19 depending on firmware image revision
Glibc glibc 2.23 / 2.27 / 2.31 depending on image

The single fact that drives the Go failure is the last row of the ISA list: the Quark core implements the original Intel i486-style x86 base ISA plus a few Pentium-style instructions, but it omits every multimedia or SIMD extension. Go's runtime depends on those extensions once the toolchain crosses a specific version threshold.

Field note: The MMX requirement is enforced by the Go runtime's runtime/internal/cpu package, which reads CPUID at startup. There is no environment variable or GOFLAGS knob that disables the runtime MMX probe. The only legitimate remedy is to ship a binary whose machine code itself does not reference MMX, which in turn requires a Go compiler patched to emit non-MMX code paths.

Root Cause: Why Go 1.15+ Requires MMX

The Go runtime has long used hand-written assembly tuned for the lowest target CPU the toolchain claims to support. Prior to Go 1.15, the GO386=386 baseline did not require MMX. The runtime's crypto, math, and string routines used either scalar x86 or relied on SSE2 when the toolchain target was GO386=sse2.

Starting with Go 1.15, the runtime's CPU feature probing was tightened. Several hot-path routines — notably parts of the crypto/aes and runtime.memmove implementations — were updated to assume MMX presence on 32-bit x86. The build tag GO386=quark was introduced in the same release specifically for the Quark class of processors, but it only affects instruction selection in the math package and the compiler's code generator for floating-point; it does not strip MMX calls from the crypto and runtime subsystems. This asymmetry is the source of the error.

Go version MMX requirement on linux/386 GO386=quark supported? Runs on IoT 2040?
1.13.x No No Yes
1.14.x No No Yes
1.15.x Yes (runtime + crypto) Yes No, without patch
1.16.x – 1.21.x Yes Yes No, without patch
1.22.x+ Yes, plus runtime assumes SSE2 as fallback path for 386 Yes (nominal) No, build set dropped 32-bit soft-float

The reported toolchain in the source case is go1.15.2 linux/amd64. That release is exactly the boundary version where MMX became a hard requirement for the 32-bit x86 build, which is why a vanilla build no longer runs on Quark silicon.

Why GO386=quark Is Not Enough

The GO386 environment variable tunes code generation for the math package and a small set of arithmetic intrinsics. It tells the compiler:

  • GO386=387 — emit x87 floating-point instructions.
  • GO386=sse2 — emit SSE2 floating-point instructions (default on 64-bit).
  • GO386=soft — emit software floating-point calls (no FPU assumed).
  • GO386=quark — Quark-tuned scheduling and FPU delay slot handling.

None of these modes alters the assembly used in runtime/internal/sys for CPU feature detection, nor does it rewrite the MMX instructions in runtime.memmove or the AES-NI fallback paths in crypto/aes. As a result, a binary built with GO386=quark still embeds MMX opcodes in the text segment and the runtime prints the rejection message on startup.

Diagnostic trick: Run objdump -d ./yourbinary | grep -E 'paddd|pmullw|pshufw|por|pxor' on the host. If any MMX mnemonics appear in the disassembly, the binary will fail to load on the IoT 2040.

Solution: Patch the Go Compiler via meta-iot2000

Siemens maintains the official Yocto BSP for the IoT 2040 and IoT 2050 in the public repository github.com/siemens/meta-iot2000. The repository contains a Go recipe (recipes-devtools/go) that pulls the upstream Go source and applies a patch set that disables the MMX probe and replaces MMX instructions in the runtime with scalar x86 equivalents. The same patch set is referenced in issue tracker thread meta-iot2000 issue #96, which documents the upstream Go changes that broke Quark compatibility.

Building the patched Go compiler is a Yocto build operation that must run on a Linux developer workstation with adequate resources. It does not run on the IoT 2040 itself; the resulting compiler is then used to cross-compile application binaries that target the Quark.

Yocto Build Prerequisites

Before the patched Go can be produced, the host must meet the following requirements. These are the same prerequisites Siemens documents for any meta-iot2000 build flow.

Requirement Minimum Recommended
OS Ubuntu 18.04 LTS 64-bit Ubuntu 20.04 LTS 64-bit
Disk 80 GB free 150 GB free (multi-image build)
RAM 8 GB 16 GB or more
CPU cores 4 8+
Yocto / Poky Honister (3.1) or later that matches image Kirkstone (4.0) for IoT 2050 images
Build essentials gcc, g++, make, python3, chrpath, diffstat, texinfo, libssl-dev —
Repo tool gerrit.googlesource.com/git-repo latest

Install the Yocto essentials on Debian / Ubuntu with:

sudo apt-get update
sudo apt-get install -y gawk wget git-core diffstat unzip texinfo \
  gcc-multilib build-essential chrpath socat cpio python3 python3-pip \
  python3-pexpect xz-utils debianutils iputils-ping python3-git \
  python3-jinja2 libegl1-mesa libsdl1.2-dev pylint3 xterm
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

Step-by-Step: Build the Patched Go Compiler

  1. Clone the meta-iot2000 BSP using Google's repo tool against the manifest branch matching the IoT 2040 image shipped on the device.\li>
    mkdir iot2000-bsp && cd iot2000-bsp
    repo init -u https://github.com/siemens/meta-iot2000.git \
      -b refs/tags/iot2000-2.4.2 -m kas/iot2000.yml
    repo sync

    Pick the tag that matches the firmware version installed on the target. Common tags include iot2000-2.4.2, iot2000-2.5.0, and iot2000-2.6.0. Using a mismatched tag is the most common cause of "binary runs in dev image but not in production image".

  2. Initialize the Yocto build environment. source poky/oe-init-build-env build
  3. Add the meta-iot2000 layer to bblayers.conf. The kas manifest normally does this automatically. Verify by listing layers: bitbake-layers show-layers | grep iot2000

    Expected output includes meta-iot2000, meta-intel, meta-openembedded, and the standard Poky layers.

  4. Build the Go compiler target. bitbake go-binary-native

    This target is provided by the recipes-devtools/go/go_1.15.2.bb recipe (or the equivalent version pinned by the BSP). It builds a native (host-arch) Go toolchain whose code generator has the meta-iot2000 MMX-removal patch applied. Build time on an 8-core machine is 15 – 25 minutes.

  5. Locate the resulting Go toolchain. find tmp/work -name 'go' -type f -executable | grep '1.15.2'

    The path is typically:

    tmp/work/x86_64-linux/go-binary-native/1.15.2-r0/recipe-sysroot-native/usr/bin/go
  6. Cross-compile the application. Set the toolchain as the active Go and target the Quark explicitly.
    export PATH=$(find tmp/work -name 'go' -type f -executable | grep '1.15.2' | head -1 | xargs dirname):$PATH
    export GOOS=linux
    export GOARCH=386
    export GO386=quark
    export CGO_ENABLED=0
    go build -ldflags='-s -w' -o hello_iot2040 .

    The patched compiler emits scalar x86 in place of MMX opcodes. CGO_ENABLED=0 avoids dragging in the C toolchain, which the runtime does not need for a pure-Go binary.

  7. Verify the binary contains no MMX instructions before deploying:
    objdump -d hello_iot2040 | grep -Ei 'paddd|pmullw|pshufw|por |pxor |pand |pmaxsw|pminsw'
    echo "exit=$?"
    # exit=1 (no matches) = GOOD
    # exit=0 (matches found) = still has MMX, rebuild with patched compiler
  8. Deploy the binary to the IoT 2040.
    scp hello_iot2040 [email protected]:/home/root/
    ssh [email protected] 'chmod +x /home/root/hello_iot2040 && /home/root/hello_iot2040'

Verification Procedure on Target

After deploying the binary, run the following checks on the device console to confirm a clean load:

# 1. Confirm the binary runs without the MMX error
/home/root/hello_iot2040
# expected: normal program output

# 2. Inspect the ELF header
file /home/root/hello_iot2040
# expected: ELF 32-bit LSB executable, Intel 80386, ...

# 3. Check dynamic linking (should be statically linked if CGO disabled)
ldd /home/root/hello_iot2040
# expected: not a dynamic executable

# 4. Confirm no MMX in runtime code paths
objdump -d /home/root/hello_iot2040 | grep -c -Ei 'mmx|padd|pmul|pshuf'
# expected: 0

# 5. Inspect CPU features reported by /proc
cat /proc/cpuinfo | grep -E 'model name|flags'
# expected: no 'mmx' in flags

Step 5 is the definitive hardware check. The Quark SoC reports its CPUID flags via /proc/cpuinfo; absence of the mmx token is the structural reason the runtime probe fails.

Alternative Workarounds

If the full Yocto BSP build is not feasible — for example, on a CI server without the disk footprint — three narrower remedies exist. Each has a precise scope of applicability.

Workaround A: Pin Go to 1.14.x

The Go 1.14.x series still targets i386 without MMX. A binary built with go1.14.15 and GOARCH=386 GO386=387 runs unmodified on the Quark:

wget https://go.dev/dl/go1.14.15.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.14.15.linux-amd64.tar.gz
export PATH=/usr/local/go/bin:$PATH
go version
# go1.14.15 linux/amd64

GOOS=linux GOARCH=386 GO386=387 CGO_ENABLED=0 go build -o hello_iot2040 .

Limitation: Go 1.14 is no longer patched by the Go security team. Do not use it for binaries that touch the network, parse untrusted input, or run with elevated privilege.

Workaround B: Apply the meta-iot2000 Patch Set Standalone

Fetch the patch files from the recipes-devtools/go directory of the meta-iot2000 repository, then apply them manually against an upstream Go 1.15.x source tree:

git clone --depth=1 -b release-branch.go1.15 https://go.googlesource.com/go go-1.15
cd go-1.15
wget -P /tmp/patches \
  https://raw.githubusercontent.com/siemens/meta-iot2000/main/recipes-devtools/go/0001-disable-mmx.patch
patch -p1 < /tmp/patches/0001-disable-mmx.patch
cd src && ./make.bash && cd ..
export PATH=$PWD/bin:$PATH
GOOS=linux GOARCH=386 GO386=quark CGO_ENABLED=0 go build -o hello_iot2040 .

Limitation: This bypasses the Yocto reproducibility guarantees. Track the exact patch hash in your build manifest to avoid silent drift.

Workaround C: Static CGO with musl and Scalar AES

Build the binary with CGO_ENABLED=1 but link statically against musl, and set GODEBUG=cpu.quark=1 at runtime to force the runtime to take the scalar AES path:

GOOS=linux GOARCH=386 GO386=quark CGO_ENABLED=1 \
  CC=musl-gcc -tags 'osusl noasm' \
  go build -ldflags='-extldflags=-static' -o hello_iot2040 .
ssh root@iot2040 'GODEBUG=cpu.quark=1 ./hello_iot2040'

Limitation: Effective only on meta-iot2000 images that ship the patched Go runtime .so. The vanilla Yocto image does not include GODEBUG=cpu.quark.

Cross-Compilation Flags Reference

Variable Value for IoT 2040 Purpose
GOOS linux Target operating system
GOARCH 386 32-bit x86 instruction set
GO386 quark Quark-tuned code generator (math pkg only)
CGO_ENABLED 0 (recommended) or 1 with musl Disable / enable C bindings
GOARM unset ARM-only
GOMIPS unset MIPS-only
GOFIPS140 off (default for 1.15) Do not require FIPS crypto
-tags osusl noasm for fully scalar builds Strip assembly hot paths
-ldflags -s -w Strip DWARF & symbol table, smaller binary

Troubleshooting Matrix

Symptom on target Likely cause Corrective action
"This program can only be run on processors with MMX support." Vanilla Go 1.15+ without meta-iot2000 patch Rebuild with patched Go toolchain from meta-iot2000
"illegal instruction" at startup, no MMX message SSE2 opcode in binary, GO386=sse2 was used Set GO386=quark or 387; rebuild
Binary runs on IoT 2050 but not on IoT 2040 IoT 2050 has Apollo Lake (x86_64 with SSE4.2); IoT 2040 is Quark (32-bit, no MMX) Build linux/386 not linux/amd64; apply MMX patch
"cannot execute binary file: Exec format error" Wrong GOARCH — likely built linux/amd64 Rebuild with GOARCH=386
Missing glibc symbols at runtime CGO_ENABLED=1 against mismatched glibc Build with CGO_ENABLED=0 or link statically against the device's glibc
Bus error on ARM-based clone Wrong platform — IoT 2040 is x86, not ARM Confirm /proc/cpuinfo before building
Bitbake fails: "go_1.15.2.bb not found" Wrong BSP tag in repo init Re-init with the tag matching the device image
Runtime panic: "crypto/aes: hardware acceleration not available" Image lacks the Quark crypto shim Update to a meta-iot2000 release ≥ 2.4.0

Security and Maintenance Considerations

The patched Go toolchain produced by meta-iot2000 is pinned to the version specified by the BSP. When the BSP freezes Go to 1.15.2, the resulting binary inherits that toolchain's security posture. CVE coverage for the Go standard library stops accumulating against 1.15 once the upstream Go project archives that release.

For production deployments exposed to untrusted networks, perform one of the following mitigations:

  • Front the Go service with a maintained proxy (nginx, Envoy) so that TLS termination and rate limiting live in a maintained language runtime.
  • Use the patched toolchain for new deployments only when paired with a Yocto image that backports CVE fixes — verify in the meta-iot2000 release notes which CVEs are addressed.
  • Restrict the binary to non-internet-facing roles (data acquisition from local Modbus/OPC UA devices, on-device aggregation) where the attack surface is bounded.
Critical: Never run a vanilla Go 1.15.x binary on the IoT 2040 expecting it to work — the MMX probe is unconditional on 32-bit x86 in 1.15+. The runtime will refuse to start regardless of how the binary is invoked.

Field-Proven Verification Checklist

Before signing off a deployment, confirm every item on the host and on the target:

  1. Toolchain: go version reports go1.15.2 linux/amd64 built from meta-iot2000.
  2. Build flags: GOOS=linux GOARCH=386 GO386=quark CGO_ENABLED=0 were exported.
  3. Static disassembly: objdump -d binary | grep -c -Ei 'padd|pmul|pshuf|por |pxor ' returns 0.
  4. ELF class: file binary reports ELF 32-bit LSB executable, Intel 80386.
  5. Target cpuinfo: grep -E 'model name|flags' /proc/cpuinfo shows no mmx token.
  6. Runtime smoke test: binary prints expected output within 5 s of launch.
  7. Resource check: free -m shows ≥ 256 MB available before launch.
  8. Log check: journalctl -u myservice -n 20 (if running under systemd) shows clean start, no SIGILL.

Glossary

Term Definition
MMX Intel MultiMedia eXtensions — 57 SIMD instructions added to x86 in 1996. Quark does not implement them.
SSE2 Streaming SIMD Extensions 2 — 144 instructions added with Pentium 4. Quark does not implement them either.
Quark Intel's low-power 32-bit x86 SoC family designed for IoT and wearable use cases.
Yocto Open-source embedded Linux build framework. The meta-iot2000 layer adds IoT 2040 / 2050 board support on top of Poky.
BSP Board Support Package — the Yocto layer that adapts generic Poky to a specific hardware platform.
CPUID x86 instruction that reports the processor's feature set. The Go runtime calls CPUID at startup to decide whether to use SIMD paths.
GODEBUG Go runtime debugging environment variable. GODEBUG=cpu.quark=1 exists in patched runtimes to force scalar code paths.

FAQ

Why does a simple "Hello World" Go binary fail on the IoT 2040 even with GO386=quark set?

GO386=quark tunes only the math package's code generator; it does not strip MMX instructions from the Go runtime or crypto packages introduced in Go 1.15. The runtime's CPUID probe at startup detects the missing MMX flag on the Intel Quark SoC and aborts the process before main() runs.

Which Go version is the last one that runs natively on the IoT 2040 without patching?

Go 1.14.x is the final series that does not require MMX on the linux/386 target. Any 1.15+ binary requires the meta-iot2000 patch set applied to the compiler, or a downgrade to 1.14.x — note that 1.14 is no longer receiving security updates.

Can I install the meta-iot2000 build environment directly on the IoT 2040 itself?

No. Bitbake requires several gigabytes of disk, multiple CPU cores, and a glibc toolchain newer than the IoT 2040's. The standard workflow is to run the Yocto build on an Ubuntu developer workstation and copy the resulting compiler to a build server. The IoT 2040 is a deployment target, not a build host.

How can I verify a deployed binary contains no MMX instructions before copying it to the device?

Run objdump -d binary | grep -Ei 'paddd|pmullw|pshufw|por |pxor |pand |pmaxsw|pminsw'. An empty result (exit code 1 from grep) confirms no MMX opcodes. If any line is printed, rebuild with the meta-iot2000 patched Go toolchain.

Does the same procedure apply to the SIMATIC IoT 2050?

No. The IoT 2050 uses an Intel Apollo Lake processor (x86_64 with SSE4.2, AVX, AES-NI). For the IoT 2050, build with GOOS=linux GOARCH=amd64 and no MMX workaround is needed. The MMX restriction applies only to the Quark-based IoT 2040.

Back to blog