ABItarget

Target Triples

The string that names a platform: `x86_64-unknown-linux-gnu`, `aarch64-apple-darwin`, `wasm32-unknown-unknown`. Four fields — architecture, vendor, OS, ABI — and each one changes a different part of the compiler.

The question

What does each field of x86_64-unknown-linux-gnu actually change in the compiler?

SourceLexingTokensParsingASTSemanticsTypedIROptimizeCodegenMachine codeLinkExecute
What the program is here

A platform as a hyphen-separated identifier that a toolchain can look up. It is the key into every target-dependent table the compiler owns — instruction set, register file, type widths, calling convention, object file format, symbol prefix, default libraries — and it exists because those tables are not independent: choosing an architecture does not choose an ABI, and choosing an OS does not choose a C library.

What this phase may assume or do

A triple is a valid selector only if the compiler has a complete, consistent set of target facts for it: a backend for the architecture, an object-file writer for the OS, an ABI lowering for the environment field, and matching predefined macros. Two artifacts may be linked together only when their triples agree in every field that affects representation — architecture, data model, object format and ABI. Agreement in the vendor field is not required, and agreement in the OS field is not sufficient, which is why x86_64-unknown-linux-gnu and x86_64-unknown-linux-musl objects can fail to work together despite sharing three of four fields.

Key points

  • A triple is arch-vendor-os-environment, and despite the name it usually has four fields and sometimes five.
  • Architecture selects the backend; OS selects the object format and system interface; environment selects the C library and the ABI variant; vendor usually selects nothing except on Apple platforms.
  • gnu versus musl and eabi versus eabihf are one-field differences that produce genuinely incompatible binaries on identical hardware.
  • wasm32-unknown-unknown has no OS field on purpose: no syscalls, no files, only what the embedder imports. wasm32-wasi is the same architecture with a system interface.
  • The triple is the key that toolchains, standard-library distributions, conditional compilation and package managers all agree to use, which is why it appears far outside the compiler.

Four fields, four different jobs

typicalThe four-field convention is what LLVM and rustc normalise to, and it is not rigidly enforced: aarch64-apple-darwin has three fields with the environment implied, arm-none-eabi uses none as an OS meaning bare metal, and GNU autotools triples predate and differ slightly from LLVM ones. Toolchains normalise on input, so several spellings of the same platform are accepted and one canonical form comes back.

The name "triple" is historical — most modern triples have four components, and some have five. The convention is arch-vendor-os-environment, and the reason it is a structured string rather than a flat enum is that the fields are genuinely independent axes that the compiler consults at different points.

The architecture field decides the backend: which instructions exist, how many registers there are, the natural word size, endianness. The OS field decides the object file format, the executable format, the system call mechanism and which predefined macros are set. The environment field — the one most often ignored — decides the ABI and the C library, and it is the field that makes -gnu and -musl different platforms on the same kernel and the same processor. The vendor field decides almost nothing and is usually unknown or pc, kept only because tooling parses positionally.

The table below is worth reading as a list of things that break when you get one field wrong, because that is how the distinction shows up in practice.

What each field of a target triple controlstarget
FieldExample valuesWhat it decides
Architecturetargetx86_64, aarch64, riscv64, wasm32, thumbv7emInstruction set, register file, word size, endianness, pointer width, which backend runs
Vendortypicalunknown, pc, apple, nvidiaAlmost nothing. Apple uses it to select platform defaults; elsewhere it is a positional placeholder
OStargetlinux, darwin, windows, none, wasiObject and executable format (ELF, Mach-O, PE, wasm), syscall mechanism, predefined macros, default libraries
Environment / ABItargetgnu, musl, msvc, gnueabihf, android, eabiThe C library, the calling-convention variant, whether hardware floating point is used in the ABI, exception model

Three triples read closely

targetEach row above names one field difference and several behavioral consequences, all of which are properties of the named platforms. Notably windows-msvc and windows-gnu differ in the C++ ABI and the name mangling, so C++ objects built for one cannot be linked into the other even though both are Windows on x86-64; a plain C boundary between them works.

x86_64-unknown-linux-gnu is a 64-bit x86 processor, no meaningful vendor, the Linux kernel, and the GNU C library. That last field carries the System V calling convention, the LP64 data model where long and pointers are both 64 bits, ELF object files, and glibc's symbol versioning. Replace gnu with musl and the kernel and processor are identical while the C library, the startup objects and the static-linking story all change — which is why Alpine-built binaries and Debian-built binaries are not interchangeable.

aarch64-apple-darwin is 64-bit ARM, and here the vendor field earns its keep: apple selects Mach-O rather than ELF, Apple's variant of AAPCS rather than the standard one, the leading-underscore symbol convention, and a set of platform defaults including a mandatory frame pointer. Darwin is the OS; the deployment target is carried separately as a version, because Apple's ABI evolves and the compiler needs to know which vintage to target.

wasm32-unknown-unknown is the interesting one because two fields are deliberately absent. The architecture is a 32-bit virtual machine rather than a processor. There is no OS at all — no syscalls, no files, no threads by default — so the binary can only call functions the embedder imports for it. Compare wasm32-wasi, which is the same architecture with an OS field naming a system interface, and therefore has files and clocks. The triple is where the difference between "a sandboxed computation" and "a program" is recorded. See [[wasm-model]].

Triples that differ in exactly one field, and what changes
1x86_64-unknown-linux-gnu vs x86_64-unknown-linux-musl
2 same CPU, same kernel, different libc, different startup objects,
3 different static-linking behavior, different symbol versioning
4
5x86_64-pc-windows-msvc vs x86_64-pc-windows-gnu
6 same CPU, same OS, different C runtime, different C++ ABI,
7 different name mangling, different exception model, different linker
8
9aarch64-unknown-linux-gnu vs aarch64-apple-darwin
10 same instruction set, different object format (ELF vs Mach-O),
11 different ABI variant, different symbol prefix, different libc
12
13armv7-unknown-linux-gnueabi vs armv7-unknown-linux-gnueabihf
14 same everything except: hf passes floating-point arguments in FP
15 registers. A one-character difference that silently corrupts every
16 float argument if you mix them.
17
18wasm32-unknown-unknown vs wasm32-wasi
19 same virtual ISA, but the first has no system interface at all:
20 no files, no clock, no environment. Only imports the host provides.

The eabi versus eabihf pair is the sharpest example in the domain of an ABI mismatch that no tool catches. Both are ARM, both are Linux, both are ELF, both mangle identically, and they disagree about which registers hold floating-point arguments. Objects link, the program runs, and every float is garbage.

Where the triple shows up

The triple is not only a compiler flag. It is the name under which precompiled standard libraries are distributed (rustup target add aarch64-unknown-linux-gnu), the prefix on cross-toolchain binaries (aarch64-linux-gnu-gcc), the key for conditional compilation (#[cfg(target_os = "linux")] and __linux__), and the identifier in package metadata that decides which wheel or artifact a package manager hands you.

It is also what a compiler prints when asked, and comparing what you asked for against what you got is a fast diagnostic. clang -print-target-triple and rustc -vV both report a normalised triple, and a mismatch between that and the artifact's actual format — checked with file — usually explains a link or load failure directly.

The general point is that the triple is the platform's name in the same way a version number is a release's name: not itself the thing, but the identifier that every tool in the chain agrees to key off. That is why the fields keep accumulating — thumbv7em-none-eabihf adds instruction-set variant and floating-point ABI into a name that once had three parts — and why nothing has replaced it despite everyone finding it awkward.

  • Toolchain selection: --target= for Clang and rustc, or a per-target driver binary name for GCC.
  • Standard library distribution: precompiled target libraries are named by triple.
  • Conditional compilation: predefined macros and cfg attributes are derived from the triple's fields.
  • Artifact naming and package resolution: which prebuilt binary a package manager selects.
  • Diagnosis: clang -print-target-triple, rustc -vV, gcc -dumpmachine, and file on the result.

How it works

The steps, in the order the compiler takes them.

  • The triple is parsed and normalised into architecture, sub-architecture, vendor, OS and environment components.
  • The architecture component selects a backend: instruction set, register file, pointer width, endianness and the legal operations for instruction selection.
  • The OS component selects the object file writer and the executable format, the system call convention, and the set of predefined macros the preprocessor defines.
  • The environment component selects the ABI lowering — argument classification, floating-point passing, exception model — and the default C library and startup objects.
  • Those choices are combined into a target description that the frontend, the backend and the driver all consult, including the type widths the preprocessor reports and the linker the driver invokes.
  • The same string is used downstream to name the artifact, to select prebuilt libraries, and to decide which conditional compilation branches are live.

How it breaks

What the engineer observes when it goes wrong — not what goes wrong internally.

  • A binary built for gnueabi is run against gnueabihf libraries and every floating-point argument is garbage, with no error from any tool at any stage.
  • A container image built on Alpine (musl) is run with a binary built on Debian (gnu), and it fails at startup with a missing dynamic linker even though the architecture and OS match.
  • A C++ library built for windows-msvc cannot be linked into a windows-gnu program, with undefined references to symbols whose demangled names are plainly present, because the two mangle differently.
  • A package manager installs a prebuilt artifact for the wrong triple and it fails to load with an architecture mismatch, or worse, loads and misbehaves under emulation.
  • A build targets wasm32-unknown-unknown and every filesystem or clock call fails at runtime, because the triple says there is no operating system and the code assumed there was one.
  • Conditional compilation keyed on the OS field takes the wrong branch on a platform whose OS field is the same and whose environment field is not, and a libc-specific workaround is compiled into a build that does not need it.

When it helps

  • Diagnosing "it does not run on that machine": comparing the triple the artifact was built for against the triple of the machine explains most of these failures in one step.
  • Setting up any cross build, where the triple is the single input from which the whole toolchain configuration follows.
  • Reading build errors from package managers and CI, which almost always name a triple and expect you to know which field is wrong.

When it hurts

  • Treating the triple as a complete description of a platform. It does not carry the OS version, the CPU feature level, the deployment target, or the libc version — all of which independently decide whether a binary will run.
  • Assuming two triples that differ only in the vendor field are different platforms. Usually they are the same, which is why unknown and pc are used interchangeably in many places.

What it costs

Every one of these is paid by something.

  • A structured identifier buys one key that every tool in the chain can agree on, and costs a naming scheme that has outgrown its own name — four or five fields under the word "triple", with several accepted spellings per platform and normalisation rules to reconcile them.
  • Encoding the ABI in the triple buys a way to distinguish gnu from musl and eabi from eabihf at all, and costs a combinatorial explosion of target names that a compiler must maintain tables for and a distribution must build artifacts for.
  • Keeping the vendor field despite its emptiness buys positional compatibility with decades of tooling, and costs a field that must be written and parsed and that means nothing almost everywhere.
  • Not encoding OS version or CPU feature level buys a stable, coarse identifier and costs a second mechanism — deployment targets, -march, feature flags — for the things it deliberately leaves out.

What else you could do

What a different compiler or language does instead, and when that is better.

  • A structured target description rather than a string: a table of properties — pointer width, endianness, ABI, object format — as Rust's custom target JSON files provide. More precise, and it does not fit on a command line or in a directory name.
  • Separate flags per property, as -march, -mabi and -mfloat-abi do for the parts the triple does not carry. Precise and easy to get subtly inconsistent, which is why the triple exists to bundle the coarse choices.
  • No target identity at all, as a virtual machine target has: the JVM, the CLR and to a large degree WebAssembly defer the platform question to load time, and the artifact is one thing. See [[wasm-vs-native]].
  • Fat or universal binaries, as Apple ships: put several architectures in one file and let the loader pick. It sidesteps distribution-side triple selection entirely and costs binary size.

See it for yourself

The flag, dump or tool that shows you this directly.

  • Ask the compiler what it will target: clang -print-target-triple, gcc -dumpmachine, rustc -vV — the last prints both the compiler's host triple and its default target.
  • See the consequences: clang --target=<triple> -dM -E - < /dev/null prints the predefined macros for that target, including pointer width, endianness and the OS and libc macros.
  • List what a toolchain can produce: llc --version prints every registered target; rustc --print target-list prints several hundred triples.
  • Check an artifact: file app names the architecture, format and, for ELF, the interpreter; readelf -h app gives the class, endianness and machine explicitly.
  • Compare two triples the hard way: build the same source for armv7-unknown-linux-gnueabi and -gnueabihf and diff the assembly of a function taking a float argument.

Plausible wrong readings

Stated the way a confident engineer states them.

  • "A triple has three parts." Most modern triples have four, and some have five. The name is historical and every toolchain parses more fields than it.
  • "The vendor field identifies the hardware vendor." It identifies almost nothing outside Apple platforms. unknown and pc are placeholders kept for positional parsing.
  • "Same architecture and same OS means compatible." gnu and musl, or msvc and gnu on Windows, are the same architecture and OS and are not interchangeable.
  • "The triple fully describes the platform." It carries no OS version, no CPU feature level and no libc version. All three independently decide whether a binary runs on a given machine.

Misconceptions

The claim, and what is actually true.

The triple selects only the instruction set.
It selects the object format, the system interface, the C library and the calling-convention variant as well, and any of those can make two artifacts incompatible on identical hardware.
Linux is Linux.
linux-gnu and linux-musl differ in the C library, the startup objects and the dynamic linker path, which is enough to make a binary built for one fail to start on the other.
wasm32-unknown-unknown runs anywhere WebAssembly runs.
It runs wherever the embedder supplies the imports it needs. With no OS field it has no files, no clock and no environment of its own — that is what the field being unknown means.

Go deeper

The same idea at increasing depth. Stop wherever it stops being useful.

overview

A target triple is the name of a platform, in four parts: the processor, a mostly-unused vendor slot, the operating system, and the ABI or C library. Compilers, package managers and cross-toolchains all key off this string, and most "it will not run on that machine" problems come down to two triples that differ in one field.

practical

When something will not build, load or link across machines, print the triples. clang -print-target-triple or rustc -vV on the build side, file on the artifact, and the target machine's own libc. The field that differs is almost always the answer, and the two that catch people are gnu versus musl on Linux and eabi versus eabihf on 32-bit ARM — the second of which produces no error at all, only wrong floating-point values.

advanced

The triple is a compression of a much larger target description into something that fits in a filename, and every awkwardness follows from that. What a compiler actually needs is a table: pointer width, endianness, alignment rules, calling convention, object format, exception model, available instruction-set extensions, default libraries. Rust exposes exactly that table as custom target JSON files, and it is the honest representation — but nobody can put it in a directory name or a package filename, so the string persists as the interchange format and the table stays internal. The residue of the compression is the set of things the triple does not carry and that therefore need parallel mechanisms: -march and feature flags for CPU capability, deployment targets for OS version, and libc version tags for symbol availability. Every one of those is a place where two artifacts with identical triples can still be incompatible.

How much this depends on

Nothing in this domain is true of every compiler. These say how much.

targetEvery consequence listed for a field is a property of the specific platforms named. The environment field means a C library on Linux, a C++ runtime and toolchain on Windows, and a floating-point ABI on 32-bit ARM — three quite different kinds of thing occupying one position, which is why the field is the most often misunderstood.
typicalThe four-field arch-vendor-os-environment form is what LLVM and rustc normalise to. GNU autotools triples differ in some spellings, three-field triples with an implied environment are common on Apple platforms, and toolchains accept several aliases per platform. Do not parse a triple positionally without normalising it first.
implementationThe set of recognised triples is a property of a particular compiler build: rustc --print target-list and llc --version will disagree with each other and with a distribution GCC. A triple that one toolchain accepts may be unknown to the next, which is a routine source of confusion in polyglot builds.

If you were asked this in an interview

  • Read armv7-unknown-linux-gnueabihf field by field and say what each one changes.
  • Two machines are both x86-64 Linux and a binary built on one will not start on the other. What is the most likely difference and how would you confirm it?
  • What does wasm32-unknown-unknown mean by having no operating system, and how does wasm32-wasi differ?

Connections

Domains that do not exist yet
  • DevOps / Production Engineering — Artifact matrices: building and publishing one binary per platform
    The triple is what a release pipeline iterates over and what names the resulting artifacts. Deciding which platforms to support, and how to test and publish each, is a release-engineering problem that this identifier is the input to.