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.
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— emitx87floating-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.
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
-
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 syncPick the tag that matches the firmware version installed on the target. Common tags include
iot2000-2.4.2,iot2000-2.5.0, andiot2000-2.6.0. Using a mismatched tag is the most common cause of "binary runs in dev image but not in production image". -
Initialize the Yocto build environment.
source poky/oe-init-build-env build -
Add the meta-iot2000 layer to
bblayers.conf. Thekasmanifest normally does this automatically. Verify by listing layers:bitbake-layers show-layers | grep iot2000Expected output includes
meta-iot2000,meta-intel,meta-openembedded, and the standard Poky layers. -
Build the Go compiler target.
bitbake go-binary-nativeThis target is provided by the
recipes-devtools/go/go_1.15.2.bbrecipe (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. -
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 -
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=0avoids dragging in the C toolchain, which the runtime does not need for a pure-Go binary. -
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 -
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.
Field-Proven Verification Checklist
Before signing off a deployment, confirm every item on the host and on the target:
- Toolchain:
go versionreportsgo1.15.2 linux/amd64built frommeta-iot2000. - Build flags:
GOOS=linux GOARCH=386 GO386=quark CGO_ENABLED=0were exported. - Static disassembly:
objdump -d binary | grep -c -Ei 'padd|pmul|pshuf|por |pxor 'returns 0. - ELF class:
file binaryreportsELF 32-bit LSB executable, Intel 80386. - Target cpuinfo:
grep -E 'model name|flags' /proc/cpuinfoshows nommxtoken. - Runtime smoke test: binary prints expected output within 5 s of launch.
- Resource check:
free -mshows ≥ 256 MB available before launch. - 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.