Chapter 57: Architecture Support¶
Android runs on a wider range of processor architectures than any other major operating system. From low-power ARM Cortex-A7 chips in entry-level phones to high-performance Cortex-X4 cores in flagship devices, from Intel x86 in Chromebooks and emulators to the emerging RISC-V ecosystem, the AOSP build system must produce correct, optimized code for every target. This chapter traces exactly how architecture support works -- from the Soong toolchain definitions that select compiler flags, through the bionic libc assembly routines hand-tuned for individual CPU cores, to the ART runtime entrypoints that bridge managed Java code with native hardware.
Understanding this machinery matters for several audiences. Device bring-up
engineers need to add a new BoardConfig.mk that declares the right
TARGET_ARCH and TARGET_CPU_VARIANT. Performance engineers need to know where
architecture-specific hot paths live so they can tune or replace them. Platform
developers working on the build system need to understand the layered design of
Soong's Toolchain interface, its architecture variants, and the multilib
mechanism that lets a single Android.bp module produce both 32-bit and 64-bit
binaries.
57.1 Supported Architectures¶
AOSP officially supports five CPU architectures, each mapped to a specific
Soong ArchType and Clang target triple. These five architectures are
registered via registerToolchainFactory() calls in the files under
build/soong/cc/config/:
| Architecture | Soong ArchType | Clang Triple | Device Config File | Bits |
|---|---|---|---|---|
| ARM (32-bit) | android.Arm |
armv7a-linux-androideabi |
arm_device.go |
32 |
| ARM64 | android.Arm64 |
aarch64-linux-android |
arm64_device.go |
64 |
| x86 (32-bit) | android.X86 |
i686-linux-android |
x86_device.go |
32 |
| x86_64 | android.X86_64 |
x86_64-linux-android |
x86_64_device.go |
64 |
| RISC-V 64 | android.Riscv64 |
riscv64-linux-android |
riscv64_device.go |
64 |
Each architecture has a corresponding *_device.go file in
build/soong/cc/config/ that defines architecture-specific compiler flags, CPU
variant tuning, and linker flags. These five files -- plus the shared
global.go, toolchain.go, clang.go, and bionic.go -- form the
foundation of AOSP's cross-compilation infrastructure.
57.1.1 Architecture Registration Flow¶
Every architecture registers itself with the build system during Go package
initialization. The pattern is identical across all five architectures. Here is
the registration from arm64_device.go:
// build/soong/cc/config/arm64_device.go, line 210-212
func init() {
registerToolchainFactory(android.Android, android.Arm64, arm64ToolchainFactory)
}
And the corresponding registration from riscv64_device.go:
// build/soong/cc/config/riscv64_device.go, line 131-133
func init() {
registerToolchainFactory(android.Android, android.Riscv64, riscv64ToolchainFactory)
}
The registerToolchainFactory function in toolchain.go stores these factories
in a two-dimensional map indexed by OS type and architecture type:
// build/soong/cc/config/toolchain.go, line 32-39
var toolchainFactories = make(map[android.OsType]map[android.ArchType]toolchainFactory)
func registerToolchainFactory(os android.OsType, arch android.ArchType, factory toolchainFactory) {
if toolchainFactories[os] == nil {
toolchainFactories[os] = make(map[android.ArchType]toolchainFactory)
}
toolchainFactories[os][arch] = factory
}
When Soong needs to compile a module for a given OS and architecture, it looks
up the factory and calls it with the target Arch struct. The factory returns a
Toolchain implementation that provides all the flags needed:
// build/soong/cc/config/toolchain.go, line 62-68
func findToolchain(os android.OsType, arch android.Arch) (Toolchain, error) {
factory := toolchainFactories[os][arch.ArchType]
if factory == nil {
return nil, fmt.Errorf("Toolchain not found for %s arch %q", os.String(), arch.String())
}
return factory(arch), nil
}
graph TD
A["Module: Android.bp"] --> B["Soong Architecture Mutator"]
B --> C{"Target Architecture?"}
C -->|arm| D["armToolchainFactory()"]
C -->|arm64| E["arm64ToolchainFactory()"]
C -->|x86| F["x86ToolchainFactory()"]
C -->|x86_64| G["x86_64ToolchainFactory()"]
C -->|riscv64| H["riscv64ToolchainFactory()"]
D --> I["toolchainArm<br/>ClangTriple: armv7a-linux-androideabi<br/>Cflags + Ldflags"]
E --> J["toolchainArm64<br/>ClangTriple: aarch64-linux-android<br/>Cflags + Ldflags"]
F --> K["toolchainX86<br/>ClangTriple: i686-linux-android<br/>Cflags + Ldflags"]
G --> L["toolchainX86_64<br/>ClangTriple: x86_64-linux-android<br/>Cflags + Ldflags"]
H --> M["toolchainRiscv64<br/>ClangTriple: riscv64-linux-android<br/>Cflags + Ldflags"]
I --> N["Clang Invocation"]
J --> N
K --> N
L --> N
M --> N
57.1.2 The Toolchain Interface¶
All architecture-specific toolchains implement the Toolchain interface defined
in build/soong/cc/config/toolchain.go. This is the central abstraction that
lets Soong compile C/C++ code without hard-coding any architecture details:
// build/soong/cc/config/toolchain.go, line 70-112
type Toolchain interface {
Name() string
IncludeFlags() string
ClangTriple() string
ToolchainCflags() string
ToolchainLdflags() string
Asflags() string
Cflags() string
Cppflags() string
Ldflags() string
InstructionSetFlags(string) (string, error)
ndkTriple() string
YasmFlags() string
Is64Bit() bool
ShlibSuffix() string
ExecutableSuffix() string
LibclangRuntimeLibraryArch() string
AvailableLibraries() []string
CrtBeginStaticBinary() []string
CrtBeginSharedBinary() []string
CrtBeginSharedLibrary() []string
CrtEndStaticBinary() []string
CrtEndSharedBinary() []string
CrtEndSharedLibrary() []string
CrtPadSegmentSharedLibrary() []string
DefaultSharedLibraries() []string
Bionic() bool
Glibc() bool
Musl() bool
}
The interface breaks down into several logical groups:
Identity: Name() and ClangTriple() identify the target. Name()
returns a short identifier like "arm64" or "x86". ClangTriple() returns
the full Clang target triple used with the --target= flag.
Compilation flags: Cflags(), Cppflags(), ToolchainCflags(), and
Asflags() provide the flags passed to the compiler at various levels.
ToolchainCflags() carries architecture-variant and CPU-variant specific flags
that are layered on top of the base Cflags().
Linking: Ldflags(), ToolchainLdflags(), and the CRT (C Runtime)
methods control how binaries are linked. Every bionic-based toolchain uses CRT
objects like crtbegin_dynamic and crtend_android.
Platform: Bionic(), Glibc(), and Musl() indicate which C library the
toolchain links against.
The base types toolchain64Bit and toolchain32Bit provide the Is64Bit()
method, while toolchainBionic provides the Android-specific CRT objects and
default shared libraries:
// build/soong/cc/config/bionic.go, line 17-46
type toolchainBionic struct {
toolchainBase
}
var (
bionicDefaultSharedLibraries = []string{"libc", "libm", "libdl"}
bionicCrtBeginStaticBinary = []string{"crtbegin_static"}
bionicCrtEndStaticBinary = []string{"crtend_android"}
bionicCrtBeginSharedBinary = []string{"crtbegin_dynamic"}
bionicCrtEndSharedBinary = []string{"crtend_android"}
bionicCrtBeginSharedLibrary = []string{"crtbegin_so"}
bionicCrtEndSharedLibrary = []string{"crtend_so"}
bionicCrtPadSegmentSharedLibrary = []string{"crt_pad_segment"}
)
57.1.3 The Arch Struct¶
Soong represents a target architecture using the Arch struct from
build/soong/android/arch.go. This struct carries all the information needed
to select the right toolchain, compiler flags, and source files:
// build/soong/android/arch.go (around line 95-110)
type Arch struct {
ArchType ArchType
ArchVariant string
CpuVariant string
Abi []string
ArchFeatures []string
}
Each field has a specific role:
-
ArchType: One ofArm,Arm64,X86,X86_64, orRiscv64. This determines which toolchain factory is used. -
ArchVariant: The ISA version, such as"armv8-2a"or"haswell". Maps to-march=flags. -
CpuVariant: The specific CPU micro-architecture, such as"cortex-a55"or"kryo385". Maps to-mcpu=flags. -
Abi: The list of Application Binary Interfaces supported, such as["arm64-v8a"]or["armeabi-v7a", "armeabi"]. -
ArchFeatures: Optional hardware features like"branchprot"or"sse4_2".
The ArchType itself is defined as a simple struct with name and multilib
classification:
// build/soong/android/arch.go (around line 128-138)
type ArchType struct {
Name string // "arm", "arm64", "x86", "x86_64", or "riscv64"
Field string // Property field name, e.g., "Arm64"
Multilib string // "lib32" or "lib64"
}
The five architecture types are registered as package-level variables:
// build/soong/android/arch.go (around line 160-164)
Arm = newArch("arm", "lib32")
Arm64 = newArch("arm64", "lib64")
Riscv64 = newArch("riscv64", "lib64")
X86 = newArch("x86", "lib32")
X86_64 = newArch("x86_64", "lib64")
When a BoardConfig.mk declares TARGET_ARCH := arm64 and
TARGET_ARCH_VARIANT := armv8-2a-dotprod, Soong constructs an Arch struct
with ArchType=Arm64, ArchVariant="armv8-2a-dotprod", and the appropriate
CpuVariant and ArchFeatures. This struct is then passed to
arm64ToolchainFactory(), which uses each field to select the right flags.
graph LR
A["BoardConfig.mk<br/>TARGET_ARCH = arm64<br/>TARGET_ARCH_VARIANT = armv8-2a-dotprod<br/>TARGET_CPU_VARIANT = cortex-a76"] --> B["Arch struct"]
B --> C["ArchType = Arm64"]
B --> D["ArchVariant = armv8-2a-dotprod"]
B --> E["CpuVariant = cortex-a76"]
B --> F["ArchFeatures = []"]
C --> G["arm64ToolchainFactory()"]
D --> G
E --> G
F --> G
G --> H["toolchainArm64<br/>Cflags: -march=armv8.2-a+dotprod -mcpu=cortex-a55"]
57.1.4 Architecture Hierarchy in the Toolchain¶
Each architecture toolchain is assembled from three layers through Go struct embedding:
classDiagram
class toolchainBase {
+InstructionSetFlags()
+ToolchainCflags()
+ToolchainLdflags()
+Asflags()
+YasmFlags()
}
class toolchainBionic {
+Bionic() bool
+DefaultSharedLibraries()
+CrtBeginStaticBinary()
+CrtEndStaticBinary()
+ShlibSuffix() string
}
class toolchain64Bit {
+Is64Bit() bool
}
class toolchain32Bit {
+Is64Bit() bool
}
class toolchainArm64 {
+Name() string
+ClangTriple() string
+Cflags() string
+Ldflags() string
+ToolchainCflags() string
}
class toolchainArm {
+Name() string
+ClangTriple() string
+Cflags() string
+Ldflags() string
+InstructionSetFlags()
}
class toolchainX86_64 {
+Name() string
+ClangTriple() string
+Cflags() string
+Ldflags() string
+YasmFlags() string
}
class toolchainRiscv64 {
+Name() string
+ClangTriple() string
+Cflags() string
+Ldflags() string
}
toolchainBase <|-- toolchainBionic
toolchainBionic <|-- toolchainArm64
toolchainBionic <|-- toolchainArm
toolchainBionic <|-- toolchainX86_64
toolchainBionic <|-- toolchainRiscv64
toolchain64Bit <|-- toolchainArm64
toolchain64Bit <|-- toolchainX86_64
toolchain64Bit <|-- toolchainRiscv64
toolchain32Bit <|-- toolchainArm
57.2 ARM64 (AArch64)¶
ARM64, formally AArch64, is the primary architecture for Android devices. Virtually every phone, tablet, and wearable shipping today uses an ARM64 processor. The AOSP build system supports a wide range of ARM64 micro-architectures, from the original ARMv8-A through the latest ARMv9.4-A extensions.
Source file: build/soong/cc/config/arm64_device.go (212 lines)
57.2.1 Architecture Variants¶
ARM64 supports ten architecture variants, each mapping to a specific -march=
compiler flag:
// build/soong/cc/config/arm64_device.go, line 30-41
arm64ArchVariantCflags = map[string][]string{
"armv8-a": {"-march=armv8-a"},
"armv8-a-branchprot": {"-march=armv8-a"},
"armv8-2a": {"-march=armv8.2-a"},
"armv8-2a-dotprod": {"-march=armv8.2-a+dotprod"},
"armv8-5a": {"-march=armv8.5-a"},
"armv8-7a": {"-march=armv8.7-a"},
"armv9-a": {"-march=armv9-a"},
"armv9-2a": {"-march=armv9.2-a"},
"armv9-3a": {"-march=armv9.3-a"},
"armv9-4a": {"-march=armv9.4-a"},
}
Each variant represents a generation of the ARM architecture specification, adding features:
| Variant | ARM Spec | Key Additions |
|---|---|---|
armv8-a |
ARMv8.0-A | Base 64-bit, NEON, VFPv4, AES, SHA |
armv8-a-branchprot |
ARMv8.0-A + PAC/BTI | Branch protection (see below) |
armv8-2a |
ARMv8.2-A | FP16, statistical profiling |
armv8-2a-dotprod |
ARMv8.2-A + DotProd | INT8 dot product for ML |
armv8-5a |
ARMv8.5-A | MTE, BTI, RNG, FRINTTS |
armv8-7a |
ARMv8.7-A | Enhanced PAC, WFI/WFE with timeout |
armv9-a |
ARMv9.0-A | SVE2, RME, base for new generation |
armv9-2a |
ARMv9.2-A | SME (scalable matrix), ETE tracing |
armv9-3a |
ARMv9.3-A | SME2, extended BFloat16 |
armv9-4a |
ARMv9.4-A | Latest: SVE2.1, GCS |
The armv8-a-branchprot variant is notable: it uses the same -march=armv8-a
flag as plain armv8-a, but the build system knows to apply the branchprot
architecture feature, which adds compiler flags for hardware-enforced control
flow integrity.
57.2.2 Branch Protection: PAC and BTI¶
Pointer Authentication Codes (PAC) and Branch Target Identification (BTI) are hardware security features that protect against control-flow hijacking attacks like ROP (Return-Oriented Programming) and JOP (Jump-Oriented Programming).
When the branchprot feature is enabled, AOSP applies these compiler flags:
// build/soong/cc/config/arm64_device.go, line 43-49
arm64ArchFeatureCflags = map[string][]string{
"branchprot": {
"-mbranch-protection=standard",
"-fno-stack-protector",
},
}
The -mbranch-protection=standard flag tells Clang to:
- Sign return addresses with PAC instructions (
PACIASP/AUTIASP) - Add BTI landing pads at function entries and branch targets
The -fno-stack-protector flag is deliberately paired with PAC because
PAC-signed return addresses already protect against stack buffer overflows that
corrupt the return address -- the primary threat that stack protectors also
defend against. Disabling the stack protector avoids the redundant canary check,
saving a few instructions per function entry/exit.
graph LR
subgraph "Without Branch Protection"
A1["Function Entry"] --> B1["Push LR to stack"]
B1 --> C1["Function Body"]
C1 --> D1["Pop LR from stack"]
D1 --> E1["RET"]
end
subgraph "With PAC + BTI"
A2["BTI C landing pad"] --> B2["PACIASP sign LR"]
B2 --> C2["Push signed LR to stack"]
C2 --> D2["Function Body"]
D2 --> E2["Pop signed LR from stack"]
E2 --> F2["AUTIASP verify + unsign LR"]
F2 --> G2["RET"]
end
In the PAC+BTI flow, if an attacker overwrites the saved LR on the stack, the
AUTIASP instruction will fail to authenticate the corrupted pointer, causing a
fault. The BTI landing pad ensures that indirect branches can only land at
intended targets.
57.2.3 Memory Tagging Extension (MTE)¶
ARMv8.5-A introduced the Memory Tagging Extension (MTE), a hardware feature that detects memory safety bugs such as use-after-free and buffer overflows. AOSP has deep integration with MTE at the bionic level.
The file bionic/libc/arch-arm64/bionic/note_memtag_heap_async.S contains
an ELF note that requests the kernel to enable MTE for heap allocations:
// bionic/libc/arch-arm64/bionic/note_memtag_heap_async.S, line 34-46
.section ".note.android.memtag", "a", %note
.p2align 2
.long 1f - 0f // int32_t namesz
.long 3f - 2f // int32_t descsz
.long NT_ANDROID_TYPE_MEMTAG // int32_t type
0:
.asciz "Android" // char name[]
1:
.p2align 2
2:
.long (NT_MEMTAG_LEVEL_ASYNC | NT_MEMTAG_HEAP) // value
3:
.p2align 2
Bionic's ifunc dispatchers also use MTE as a selection criterion, choosing
MTE-aware implementations when the hardware supports it. From
bionic/libc/arch-arm64/ifuncs.cpp:
// bionic/libc/arch-arm64/ifuncs.cpp, line 54-60
DEFINE_IFUNC_FOR(memchr) {
if (arg->_hwcap2 & HWCAP2_MTE) {
RETURN_FUNC(memchr_func_t, __memchr_aarch64_mte);
} else {
RETURN_FUNC(memchr_func_t, __memchr_aarch64);
}
}
MTE-aware string routines need special handling because the tag bits occupy the upper bits of pointers. Regular pointer arithmetic or SIMD-based comparisons might accidentally trip over the tag bits unless the code is written to be tag-aware.
57.2.4 CPU Variant Tuning and big.LITTLE¶
ARM's big.LITTLE (and later DynamIQ) heterogeneous computing architecture pairs high-performance "big" cores (e.g., Cortex-A76) with efficient "LITTLE" cores (e.g., Cortex-A55). This creates a scheduling optimization challenge: code compiled for the big core's pipeline might stall on the LITTLE core.
AOSP takes a pragmatic approach -- it tunes code for the LITTLE core, because that code runs correctly (and acceptably fast) on both core types, while code tuned for the big core might be pathologically slow on the LITTLE core:
// build/soong/cc/config/arm64_device.go, line 65-77
"cortex-a75": []string{
// Use the cortex-a55 since it is similar to the little
// core (cortex-a55) and is sensitive to ordering.
"-mcpu=cortex-a55",
},
"cortex-a76": []string{
// Use the cortex-a55 since it is similar to the little
// core (cortex-a55) and is sensitive to ordering.
"-mcpu=cortex-a55",
},
This is not a mistake in the source code. The comments explain the reasoning:
the Cortex-A75 (big) and Cortex-A76 (big) variants deliberately use
-mcpu=cortex-a55 (LITTLE) because the instruction scheduling for the little
core is "sensitive to ordering" -- meaning poor scheduling for the little core
causes significant performance degradation, whereas the big core's
out-of-order pipeline can compensate for sub-optimal scheduling.
The complete set of supported ARM64 CPU variants:
// build/soong/cc/config/arm64_device.go, line 58-88
arm64CpuVariantCflags = map[string][]string{
"cortex-a53": {"-mcpu=cortex-a53"},
"cortex-a55": {"-mcpu=cortex-a55"},
"cortex-a75": {"-mcpu=cortex-a55"}, // Uses little core tuning
"cortex-a76": {"-mcpu=cortex-a55"}, // Uses little core tuning
"kryo": {"-mcpu=kryo"},
"kryo385": {"-mcpu=cortex-a53"}, // kryo385 not in clang
"exynos-m1": {"-mcpu=exynos-m1"},
"exynos-m2": {"-mcpu=exynos-m2"},
}
The mapping from CPU variant to compile flags is resolved through a two-level lookup. First, the variant-to-variable map:
// build/soong/cc/config/arm64_device.go, line 123-135
arm64CpuVariantCflagsVar = map[string]string{
"cortex-a53": "${config.Arm64CortexA53Cflags}",
"cortex-a55": "${config.Arm64CortexA55Cflags}",
"cortex-a72": "${config.Arm64CortexA53Cflags}",
"cortex-a73": "${config.Arm64CortexA53Cflags}",
"cortex-a75": "${config.Arm64CortexA55Cflags}",
"cortex-a76": "${config.Arm64CortexA55Cflags}",
"kryo": "${config.Arm64KryoCflags}",
"kryo385": "${config.Arm64CortexA53Cflags}",
"exynos-m1": "${config.Arm64ExynosM1Cflags}",
"exynos-m2": "${config.Arm64ExynosM2Cflags}",
}
Notice how the big cores (A72, A73, A75, A76) all map to their corresponding LITTLE core flags (A53 or A55).
57.2.5 Cortex-A53 Erratum Workarounds¶
The Cortex-A53, one of the most widely deployed ARM cores in history, has two notable hardware errata that AOSP works around at link time:
// build/soong/cc/config/arm64_device.go, line 120
pctx.StaticVariable("Arm64FixCortexA53Ldflags", "-Wl,--fix-cortex-a53-843419")
// build/soong/cc/config/arm64_device.go, line 137-144
arm64CpuVariantLdflags = map[string]string{
"cortex-a53": "${config.Arm64FixCortexA53Ldflags}",
"cortex-a72": "${config.Arm64FixCortexA53Ldflags}",
"cortex-a73": "${config.Arm64FixCortexA53Ldflags}",
"kryo": "${config.Arm64FixCortexA53Ldflags}",
"exynos-m1": "${config.Arm64FixCortexA53Ldflags}",
"exynos-m2": "${config.Arm64FixCortexA53Ldflags}",
}
Erratum 843419 causes incorrect execution when certain sequences of ADRP
instructions appear near page boundaries. The linker flag
--fix-cortex-a53-843419 tells LLD to detect these problematic patterns and
insert veneer code to avoid them. Note that this fix is also applied to A72,
A73, Kryo, and Exynos cores, because they may be paired with A53 LITTLE cores
in big.LITTLE configurations.
57.2.6 ARM64 Toolchain Factory¶
The factory function assembles all the layers into a single toolchain:
// build/soong/cc/config/arm64_device.go, line 187-208
func arm64ToolchainFactory(arch android.Arch) Toolchain {
if _, ok := arm64ArchVariantCflags[arch.ArchVariant]; !ok {
panic(fmt.Sprintf("Unknown ARM64 architecture version: %q", arch.ArchVariant))
}
toolchainCflags := []string{"${config.Arm64" + arch.ArchVariant + "VariantCflags}"}
toolchainCflags = append(toolchainCflags,
variantOrDefault(arm64CpuVariantCflagsVar, arch.CpuVariant))
for _, feature := range arch.ArchFeatures {
toolchainCflags = append(toolchainCflags, arm64ArchFeatureCflags[feature]...)
}
extraLdflags := variantOrDefault(arm64CpuVariantLdflags, arch.CpuVariant)
return &toolchainArm64{
ldflags: strings.Join([]string{
"${config.Arm64Ldflags}",
extraLdflags,
}, " "),
toolchainCflags: strings.Join(toolchainCflags, " "),
}
}
The flags are layered in this order:
graph TB
A["Global Cflags<br/>(global.go: commonGlobalCflags)"] --> B["Device Cflags<br/>(global.go: deviceGlobalCflags)"]
B --> C["Architecture Cflags<br/>(arm64_device.go: arm64Cflags)"]
C --> D["Arch Variant Cflags<br/>e.g., -march=armv8.2-a+dotprod"]
D --> E["CPU Variant Cflags<br/>e.g., -mcpu=cortex-a55"]
E --> F["Feature Cflags<br/>e.g., -mbranch-protection=standard"]
F --> G["Final clang invocation"]
57.2.7 Page Size Configuration¶
ARM64 supports multiple page sizes (4KB, 16KB, 64KB). The linker flags enforce the maximum page size for correct segment alignment:
// build/soong/cc/config/arm64_device.go, line 92-96
pctx.VariableFunc("Arm64Ldflags", func(ctx android.PackageVarContext) string {
maxPageSizeFlag := "-Wl,-z,max-page-size=" + ctx.Config().MaxPageSizeSupported()
flags := append(arm64Ldflags, maxPageSizeFlag)
return strings.Join(flags, " ")
})
The base linker flags also include segment separation for security:
// build/soong/cc/config/arm64_device.go, line 51-54
arm64Ldflags = []string{
"-Wl,-z,separate-code",
"-Wl,-z,separate-loadable-segments",
}
These flags ensure that code segments, data segments, and read-only segments are placed in separate memory pages, preventing accidental (or malicious) execution of data or modification of code.
57.3 x86 and x86_64¶
The x86 architecture family serves two main roles in AOSP: as the target for Chromebook and embedded devices, and as the native architecture for the Android Emulator. The emulator historically ran ARM images under translation, but native x86/x86_64 images provide dramatically better performance during development.
Source files:
build/soong/cc/config/x86_device.go(193 lines)build/soong/cc/config/x86_64_device.go(200 lines)
57.3.1 x86 Architecture Variants¶
The x86 (32-bit) toolchain supports a wide range of Intel microarchitectures:
// build/soong/cc/config/x86_device.go, line 38-86
x86ArchVariantCflags = map[string][]string{
"": []string{
"-march=prescott",
},
"x86_64": []string{
"-march=prescott",
},
"alderlake": []string{"-march=alderlake"},
"atom": []string{"-march=atom"},
"broadwell": []string{"-march=broadwell"},
"goldmont": []string{"-march=goldmont"},
"goldmont-plus": []string{"-march=goldmont-plus"},
"goldmont-without-sha-xsaves": []string{
"-march=goldmont",
"-mno-sha",
"-mno-xsaves",
},
"haswell": []string{"-march=core-avx2"},
"ivybridge": []string{"-march=core-avx-i"},
"sandybridge": []string{"-march=corei7"},
"silvermont": []string{"-march=slm"},
"skylake": []string{"-march=skylake"},
"stoneyridge": []string{"-march=bdver4"},
"tremont": []string{"-march=tremont"},
}
The x86_64 toolchain has the same set of microarchitecture variants:
// build/soong/cc/config/x86_64_device.go, line 36-79
x86_64ArchVariantCflags = map[string][]string{
"": []string{"-march=x86-64"},
"alderlake": []string{"-march=alderlake"},
"broadwell": []string{"-march=broadwell"},
"goldmont": []string{"-march=goldmont"},
// ... same variants as x86
"haswell": []string{"-march=core-avx2"},
"skylake": []string{"-march=skylake"},
"tremont": []string{"-march=tremont"},
}
The default for x86 is prescott (Pentium 4 with SSE3), while x86_64 defaults
to the baseline x86-64 instruction set.
57.3.2 SIMD Instruction Sets: SSE and AVX¶
The x86 SIMD landscape is more fragmented than ARM's NEON -- instead of a single mandatory SIMD extension, x86 has a progression of optional extensions. Both the x86 and x86_64 toolchains define feature flags for these:
// build/soong/cc/config/x86_64_device.go, line 81-97
x86_64ArchFeatureCflags = map[string][]string{
"ssse3": []string{"-mssse3"},
"sse4": []string{"-msse4"},
"sse4_1": []string{"-msse4.1"},
"sse4_2": []string{"-msse4.2"},
// Not all cases there is performance gain by enabling -mavx -mavx2
// flags so these flags are not enabled by default.
// if there is performance gain in individual library components,
// the compiler flags can be set in corresponding bp files.
// "avx": []string{"-mavx"},
// "avx2": []string{"-mavx2"},
// "avx512": []string{"-mavx512"}
"popcnt": []string{"-mpopcnt"},
"aes_ni": []string{"-maes"},
}
Note the commented-out AVX/AVX2/AVX512 entries. The comment explains the
reasoning: AVX does not always provide a performance gain. In fact, on some
Intel processors, AVX instructions cause the CPU to reduce its clock frequency
("AVX frequency throttling"), which can actually hurt performance for code that
mixes AVX and non-AVX instructions. Individual libraries can opt into AVX via
their Android.bp files when they know it helps.
57.3.3 x86-Specific Compiler Flags¶
The 32-bit x86 toolchain has several unique requirements:
// build/soong/cc/config/x86_device.go, line 25-32
x86Cflags = []string{
"-msse3",
// -mstackrealign is needed to realign stack in native code
// that could be called from JNI, so that movaps instruction
// will work on assumed stack aligned local variables.
"-mstackrealign",
}
The -mstackrealign flag addresses a subtle ABI issue. The i386 System V ABI
only requires 4-byte stack alignment, but SSE instructions like movaps require
16-byte alignment. When native code is called from JNI (through the Dalvik/ART
runtime), the stack may not be 16-byte aligned, causing crashes.
-mstackrealign inserts code at function entry to realign the stack.
The x86 toolchain also uses Yasm for assembly:
// build/soong/cc/config/x86_device.go, line 117
pctx.StaticVariable("X86YasmFlags", "-f elf32 -m x86")
// build/soong/cc/config/x86_64_device.go, line 124
pctx.StaticVariable("X86_64YasmFlags", "-f elf64 -m amd64")
57.3.4 x86 Toolchain Structure¶
Both x86 toolchains share the same pattern as ARM64:
// build/soong/cc/config/x86_device.go, line 125-129
type toolchainX86 struct {
toolchainBionic
toolchain32Bit
toolchainCflags string
}
// build/soong/cc/config/x86_64_device.go, line 132-136
type toolchainX86_64 struct {
toolchainBionic
toolchain64Bit
toolchainCflags string
}
The key difference from ARM64 is the explicit -m32/-m64 toolchain flags
that control code generation model:
// build/soong/cc/config/x86_device.go, line 107-108
pctx.StaticVariable("X86ToolchainCflags", "-m32")
pctx.StaticVariable("X86ToolchainLdflags", "-m32")
// build/soong/cc/config/x86_64_device.go, line 101-102
pctx.StaticVariable("X86_64ToolchainCflags", "-m64")
pctx.StaticVariable("X86_64ToolchainLdflags", "-m64")
57.3.5 Native Bridge for ARM Compatibility¶
x86/x86_64 Android devices face a compatibility challenge: the vast majority of Android NDK apps are compiled for ARM. To run these apps, AOSP includes the Native Bridge infrastructure, which provides transparent binary translation.
The Native Bridge mechanism is defined in
frameworks/libs/binary_translation/native_bridge/native_bridge.h, which
declares the NativeBridgeCallbacks interface:
// frameworks/libs/binary_translation/native_bridge/native_bridge.h, line 48-62
struct NativeBridgeCallbacks {
uint32_t version;
bool (*initialize)(const NativeBridgeRuntimeCallbacks* runtime_cbs,
const char* private_dir,
const char* instruction_set);
void* (*loadLibrary)(const char* libpath, int flag);
// Get a native bridge trampoline for specified native method.
// The trampoline has same signature as the native method.
...
};
The Native Bridge works by intercepting library loads: when ART's class loader encounters a native library compiled for a foreign architecture, it delegates to the native bridge implementation, which translates the foreign code.
Two main implementations exist:
-
Berberis (open source, in
frameworks/libs/binary_translation/) -- Google's reference implementation for translating RISC-V to x86_64 -
Houdini (proprietary, from Intel) -- translates ARM/ARM64 to x86/x86_64
The Emulator uses a different strategy: it runs the ARM system image under QEMU-based full system emulation with hardware-accelerated virtualization, so native bridge is not involved in the typical emulator workflow.
graph TD
A["App with ARM native library"] --> B["ART loads .so"]
B --> C{"Architecture match?"}
C -->|Yes| D["Direct dlopen()"]
C -->|No| E["Native Bridge intercepts"]
E --> F["NativeBridgeCallbacks.loadLibrary()"]
F --> G["Translation Engine<br/>(Berberis or Houdini)"]
G --> H["Translated trampoline functions"]
H --> I["Execute on host x86/x86_64 CPU"]
57.3.6 The Emulator Target¶
The default x86_64 generic device configuration targets the emulator:
# device/generic/x86_64/BoardConfig.mk, line 9-15
TARGET_CPU_ABI := x86_64
TARGET_ARCH := x86_64
TARGET_ARCH_VARIANT := x86_64
TARGET_2ND_CPU_ABI := x86
TARGET_2ND_ARCH := x86
TARGET_2ND_ARCH_VARIANT := x86_64
The goldmont-without-sha-xsaves variant deserves special mention: it targets
Intel Goldmont (Apollo Lake) processors but disables SHA and XSAVES
instructions. This exists because some Chromebooks and embedded devices use
Goldmont-based processors that do not implement these optional extensions:
57.3.7 ARM 32-bit: The Legacy Secondary Architecture¶
ARM 32-bit support in AOSP exists primarily as the secondary architecture for ARM64 devices, allowing legacy 32-bit apps to run. The ARM toolchain is the most complex of all five architectures because it supports the widest range of CPU variants (from ancient Cortex-A7 to modern Cortex-A76 in 32-bit mode) and two instruction encodings (ARM and Thumb).
The ARM toolchain struct reflects its 32-bit nature:
// build/soong/cc/config/arm_device.go (line 247-252)
type toolchainArm struct {
toolchainBionic
toolchain32Bit
ldflags string
toolchainCflags string
}
The ARM factory function assembles three levels of flags:
// build/soong/cc/config/arm_device.go (line 303-316)
func armToolchainFactory(arch android.Arch) Toolchain {
toolchainCflags := make([]string, 2, 3)
toolchainCflags[0] = "${config.ArmToolchainCflags}"
toolchainCflags[1] = armArchVariantCflagsVar[arch.ArchVariant]
toolchainCflags = append(toolchainCflags,
variantOrDefault(armCpuVariantCflagsVar, arch.CpuVariant))
return &toolchainArm{
ldflags: "${config.ArmLdflags}",
toolchainCflags: strings.Join(toolchainCflags, " "),
}
}
Layer 1 -- ArmToolchainCflags: The -msoft-float flag, which is universal
for all ARM 32-bit Android targets.
Layer 2 -- Arch variant: One of armv7-a, armv7-a-neon, armv8-a, or
armv8-2a, determining the baseline ISA and FPU configuration.
Layer 3 -- CPU variant: The specific core tuning, selected from a map that includes 18 different variants:
// build/soong/cc/config/arm_device.go (line 225-244)
armCpuVariantCflagsVar = map[string]string{
"": "${config.ArmGenericCflags}",
"cortex-a7": "${config.ArmCortexA7Cflags}",
"cortex-a8": "${config.ArmCortexA8Cflags}",
"cortex-a9": "${config.ArmGenericCflags}",
"cortex-a15": "${config.ArmCortexA15Cflags}",
"cortex-a32": "${config.ArmCortexA32Cflags}",
"cortex-a53": "${config.ArmCortexA53Cflags}",
"cortex-a53.a57": "${config.ArmCortexA53Cflags}",
"cortex-a55": "${config.ArmCortexA55Cflags}",
"cortex-a72": "${config.ArmCortexA53Cflags}",
"cortex-a73": "${config.ArmCortexA53Cflags}",
"cortex-a75": "${config.ArmCortexA55Cflags}",
"cortex-a76": "${config.ArmCortexA55Cflags}",
"krait": "${config.ArmKraitCflags}",
"kryo": "${config.ArmKryoCflags}",
"kryo385": "${config.ArmCortexA53Cflags}",
"exynos-m1": "${config.ArmCortexA53Cflags}",
"exynos-m2": "${config.ArmCortexA53Cflags}",
}
The Cortex-A8 erratum workaround is also specific to ARM 32-bit:
// build/soong/cc/config/arm_device.go (line 45-47)
armFixCortexA8LdFlags = []string{"-Wl,--fix-cortex-a8"}
armNoFixCortexA8LdFlags = []string{"-Wl,--no-fix-cortex-a8"}
The Cortex-A8 has a hardware bug that can cause incorrect execution in certain branch-to-branch sequences. This is separate from the Cortex-A53 errata handled in the ARM64 toolchain.
graph TD
subgraph "ARM 32-bit Toolchain Assembly"
A["ArmToolchainCflags<br/>-msoft-float"] --> D["Combined Flags"]
B["Arch Variant<br/>e.g., armv7-a-neon:<br/>-march=armv7-a -mfloat-abi=softfp -mfpu=neon"] --> D
C["CPU Variant<br/>e.g., cortex-a55:<br/>-mcpu=cortex-a55 -mfpu=neon-fp-armv8 -D__ARM_FEATURE_LPAE=1"] --> D
D --> E["armToolchainFactory() returns toolchainArm"]
end
57.3.8 ARM 32-bit Linker Configuration¶
The ARM 32-bit linker has its own specific flags:
// build/soong/cc/config/arm_device.go (line 39-43)
armLdflags = []string{
"-Wl,-m,armelf",
"-Wl,-mllvm", "-Wl,-enable-shrink-wrap=false",
}
The -Wl,-m,armelf flag tells the linker to use the ARM ELF format. The
-enable-shrink-wrap=false flags disable an LLVM optimization that was causing
incorrect code generation (tracked as bug b/322359235). The same workaround
appears in the compiler and linker flags, showing how hardware errata and
compiler bugs create a complex web of workarounds across the toolchain.
57.4 RISC-V 64¶
RISC-V is the newest architecture supported by AOSP, first added in 2022. It is an open, royalty-free instruction set architecture that has generated significant industry interest. The AOSP RISC-V port is still maturing, as evidenced by the smaller configuration file and explicit workarounds for incomplete toolchain support.
Source file: build/soong/cc/config/riscv64_device.go (133 lines)
57.4.1 Base ISA and Extensions¶
The RISC-V configuration specifies a rich set of extensions:
// build/soong/cc/config/riscv64_device.go, line 25-36
riscv64Cflags = []string{
"-Werror=implicit-function-declaration",
// This is already the driver's Android default, but duplicated here (and
// below) for ease of experimentation with additional extensions.
"-march=rv64gcv_zba_zbb_zbs",
// TODO: remove when qemu V works
// (Note that we'll probably want to wait for berberis to be good enough
// that most people don't care about qemu's V performance either!)
"-mno-implicit-float",
}
The ISA string -march=rv64gcv_zba_zbb_zbs decodes as follows:
| Component | Meaning |
|---|---|
rv64 |
64-bit base integer ISA (RV64I) |
g |
"General" = IMAFD (Integer, Multiply, Atomic, Float, Double) |
c |
Compressed instructions (16-bit encodings for common ops) |
v |
Vector extension (RISC-V V 1.0) |
zba |
Address generation instructions (sh1add, sh2add, sh3add) |
zbb |
Basic bit manipulation (clz, ctz, cpop, rev8, etc.) |
zbs |
Single-bit instructions (bset, bclr, binv, bext) |
This is a notably modern baseline -- the Vector extension in particular enables SIMD-like operations analogous to ARM's NEON or Intel's SSE, but with a scalable design that does not hard-code the vector width.
57.4.2 QEMU and Berberis Workarounds¶
The RISC-V configuration contains a revealing TODO comment about the state of the ecosystem:
// TODO: remove when qemu V works (https://gitlab.com/qemu-project/qemu/-/issues/1976)
// (Note that we'll probably want to wait for berberis to be good enough
// that most people don't care about qemu's V performance either!)
"-mno-implicit-float",
This comment reveals the practical challenge of RISC-V development: QEMU's
Vector extension support is incomplete, and the Berberis binary translator
(which could translate RISC-V to x86_64 for development) is still maturing.
The -mno-implicit-float flag prevents the compiler from automatically using
floating-point or vector instructions for non-floating-point operations
(like structure copies), which works around QEMU V bugs.
57.4.3 Minimal Variant Configuration¶
Unlike ARM64 and x86, RISC-V has no CPU variant tuning:
// build/soong/cc/config/riscv64_device.go, line 38-49
riscv64ArchVariantCflags = map[string][]string{}
riscv64CpuVariantCflags = map[string][]string{}
The variant maps are empty, and the factory function only accepts the default (empty string) variant:
// build/soong/cc/config/riscv64_device.go, line 110-115
func riscv64ToolchainFactory(arch android.Arch) Toolchain {
switch arch.ArchVariant {
case "":
default:
panic(fmt.Sprintf("Unknown Riscv64 architecture version: %q", arch.ArchVariant))
}
// ...
}
This simplicity reflects the current state of the RISC-V Android ecosystem: there is only one target configuration, and the hardware landscape has not yet diversified to the point where micro-architecture-specific tuning is needed.
57.4.4 RISC-V Linker Configuration¶
The RISC-V linker flags are straightforward:
// build/soong/cc/config/riscv64_device.go, line 40-45
riscv64Ldflags = []string{
"-march=rv64gcv_zba_zbb_zbs",
"-Wl,-z,max-page-size=4096",
}
Note the hardcoded 4KB page size, unlike ARM64 which uses a configurable
MaxPageSizeSupported(). RISC-V Android currently only supports 4KB pages.
57.4.5 Berberis Binary Translation¶
The frameworks/libs/binary_translation/ directory contains Berberis, Google's
open-source binary translation framework. While primarily designed for
translating guest architectures to x86_64 host systems, Berberis is
strategically important for RISC-V development:
frameworks/libs/binary_translation/
assembler/ - Code generation backend
backend/ - Translation engine
decoder/ - Guest instruction decoder
guest_abi/ - ABI conversion layer
guest_loader/ - Library loading and linking
guest_state/ - CPU state abstraction
interpreter/ - Interpreted execution fallback
jni/ - JNI trampoline generation
native_bridge/ - NativeBridge interface implementation
android_api/ - Framework API proxies
lite_translator/ - Lightweight translation path
heavy_optimizer/ - Full optimization path
The Berberis enable_riscv64_to_x86_64.mk file in the top directory reveals
the primary translation direction: RISC-V 64 guest code running on an x86_64
host. This allows developers to work with RISC-V Android images on x86_64
workstations.
57.4.6 ART RISC-V Feature Detection¶
The ART runtime has full RISC-V support with its own ISA feature tracking. The
Riscv64InstructionSetFeatures class tracks extensions as a bitmap:
// art/runtime/arch/riscv64/instruction_set_features_riscv64.h (line 31-39)
class Riscv64InstructionSetFeatures final : public InstructionSetFeatures {
public:
enum {
kExtGeneric = (1 << 0), // G: IMAFD base set
kExtCompressed = (1 << 1), // C: compressed instructions
kExtVector = (1 << 2), // V: vector instructions
kExtZba = (1 << 3), // Zba: address generation
kExtZbb = (1 << 4), // Zbb: basic bit-manipulation
kExtZbs = (1 << 5), // Zbs: single-bit manipulation
};
The feature methods allow ART's JIT compiler to query capabilities:
// art/runtime/arch/riscv64/instruction_set_features_riscv64.h (line 71-79)
bool HasCompressed() const { return (bits_ & kExtCompressed) != 0; }
bool HasVector() const { return (bits_ & kExtVector) != 0; }
bool HasZba() const { return (bits_ & kExtZba) != 0; }
bool HasZbb() const { return (bits_ & kExtZbb) != 0; }
bool HasZbs() const { return (bits_ & kExtZbs) != 0; }
The FromVariant() implementation currently only recognizes the "generic"
variant and uses the full basic feature set:
// art/runtime/arch/riscv64/instruction_set_features_riscv64.cc (line 30-46)
constexpr uint32_t BasicFeatures() {
return Riscv64InstructionSetFeatures::kExtGeneric |
Riscv64InstructionSetFeatures::kExtCompressed |
Riscv64InstructionSetFeatures::kExtVector |
Riscv64InstructionSetFeatures::kExtZba |
Riscv64InstructionSetFeatures::kExtZbb |
Riscv64InstructionSetFeatures::kExtZbs;
}
Riscv64FeaturesUniquePtr Riscv64InstructionSetFeatures::FromVariant(
const std::string& variant, [[maybe_unused]] std::string* error_msg) {
if (variant != "generic") {
LOG(WARNING) << "Unexpected CPU variant for Riscv64 using defaults: " << variant;
}
return Riscv64FeaturesUniquePtr(
new Riscv64InstructionSetFeatures(BasicFeatures()));
}
Feature detection from C preprocessor defines is also implemented, allowing the build system to detect extensions at compile time:
// art/runtime/arch/riscv64/instruction_set_features_riscv64.cc (line 52-71)
Riscv64FeaturesUniquePtr Riscv64InstructionSetFeatures::FromCppDefines() {
uint32_t bits = kExtGeneric;
#ifdef __riscv_c
bits |= kExtCompressed;
#endif
#ifdef __riscv_v
bits |= kExtVector;
#endif
#ifdef __riscv_zba
bits |= kExtZba;
#endif
#ifdef __riscv_zbb
bits |= kExtZbb;
#endif
#ifdef __riscv_zbs
bits |= kExtZbs;
#endif
return FromBitmap(bits);
}
Note that the FromCpuInfo() and FromHwcap() methods are not yet implemented
for RISC-V:
// art/runtime/arch/riscv64/instruction_set_features_riscv64.cc (line 73-80)
Riscv64FeaturesUniquePtr Riscv64InstructionSetFeatures::FromCpuInfo() {
UNIMPLEMENTED(WARNING);
return FromCppDefines();
}
Riscv64FeaturesUniquePtr Riscv64InstructionSetFeatures::FromHwcap() {
UNIMPLEMENTED(WARNING);
return FromCppDefines();
}
The UNIMPLEMENTED(WARNING) calls indicate that runtime hardware detection is
not yet complete for RISC-V, which is another marker of the architecture's
early-adoption status in AOSP.
57.4.7 Comparing RISC-V and ARM64 Feature Tracking¶
The contrast between RISC-V and ARM64 feature tracking in ART reveals the maturity gap:
| Aspect | ARM64 | RISC-V 64 |
|---|---|---|
| CPU variants | 15+ recognized | Only "generic" |
| Feature flags | 7 (CRC, LSE, FP16, DotProd, SVE, errata) | 6 (G, C, V, Zba, Zbb, Zbs) |
| FromHwcap() | Fully implemented | UNIMPLEMENTED |
| FromCpuInfo() | Fully implemented | UNIMPLEMENTED |
| Errata tracking | A53 835769, A53 843419 | None |
| Runtime validation | Yes (Pixel 3a workaround) | No |
| SVE support | Defined but disabled (kArm64AllowSVE = false) |
N/A |
The ARM64 feature header explicitly disables SVE:
// art/runtime/arch/arm64/instruction_set_features_arm64.h (line 25-26)
// SVE is currently not enabled.
static constexpr bool kArm64AllowSVE = false;
This means that even though the Soong toolchain supports ARMv9 variants (which include SVE2), ART's JIT compiler does not yet generate SVE instructions. This is a pragmatic choice -- SVE support requires significant changes to the register allocator and instruction selector.
57.4.8 ART ARM64 Feature Bitmap¶
ART stores ARM64 features as a compact bitmap for serialization:
// art/runtime/arch/arm64/instruction_set_features_arm64.h (line 142-150)
enum {
kA53Bitfield = 1 << 0,
kCRCBitField = 1 << 1,
kLSEBitField = 1 << 2,
kFP16BitField = 1 << 3,
kDotProdBitField = 1 << 4,
kSVEBitField = 1 << 5,
};
And the private member variables track each feature:
// art/runtime/arch/arm64/instruction_set_features_arm64.h (line 152-158)
const bool fix_cortex_a53_835769_;
const bool fix_cortex_a53_843419_;
const bool has_crc_; // optional in ARMv8.0, mandatory in ARMv8.1
const bool has_lse_; // ARMv8.1 Large System Extensions
const bool has_fp16_; // ARMv8.2 FP16 extensions
const bool has_dotprod_; // optional in ARMv8.2, mandatory in ARMv8.4
const bool has_sve_; // optional in ARMv8.2
The JIT compiler uses these feature flags to select instruction patterns.
For example, when has_lse_ is true, atomic operations use the single-instruction
LDADD, SWPAL, etc., instead of the multi-instruction LL/SC loop
(LDAXR / STLXR). This can be 2-3x faster in high-contention scenarios.
57.4.9 RISC-V Device Configuration¶
The ART test device for RISC-V reveals the early-adoption nature of the port:
# device/generic/art/riscv64/BoardConfig.mk
include device/generic/art/BoardConfigCommon.mk
TARGET_ARCH := riscv64
TARGET_CPU_ABI := riscv64
TARGET_CPU_VARIANT := generic
TARGET_ARCH_VARIANT :=
TARGET_SUPPORTS_64_BIT_APPS := true
# Temporary hack while prebuilt modules are missing riscv64.
ALLOW_MISSING_DEPENDENCIES := true
The ALLOW_MISSING_DEPENDENCIES := true line is significant -- it allows the
build to proceed even when some prebuilt modules do not have RISC-V binaries
yet. This is a temporary measure while the RISC-V ecosystem catches up.
57.5 Multi-Architecture Builds¶
Modern Android devices typically support multiple architectures simultaneously. A 64-bit ARM device also runs 32-bit ARM code. An x86_64 device also runs x86 code. AOSP's build system handles this through the "multilib" mechanism, which builds the same module for multiple architectures.
57.5.1 Primary and Secondary Architectures¶
Device configurations declare a primary architecture and an optional secondary
architecture using TARGET_ARCH and TARGET_2ND_ARCH:
# device/generic/arm64/BoardConfig.mk, line 10-19
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := generic
TARGET_CPU_ABI := arm64-v8a
TARGET_2ND_ARCH := arm
TARGET_2ND_ARCH_VARIANT := armv7-a-neon
TARGET_2ND_CPU_VARIANT := cortex-a15
TARGET_2ND_CPU_ABI := armeabi-v7a
TARGET_2ND_CPU_ABI2 := armeabi
Similarly, for x86_64:
# device/generic/x86_64/BoardConfig.mk, line 9-15
TARGET_CPU_ABI := x86_64
TARGET_ARCH := x86_64
TARGET_ARCH_VARIANT := x86_64
TARGET_2ND_CPU_ABI := x86
TARGET_2ND_ARCH := x86
TARGET_2ND_ARCH_VARIANT := x86_64
graph LR
subgraph "arm64 Device"
A1["TARGET_ARCH: arm64<br/>64-bit primary"] --- A2["TARGET_2ND_ARCH: arm<br/>32-bit secondary"]
end
subgraph "x86_64 Device"
B1["TARGET_ARCH: x86_64<br/>64-bit primary"] --- B2["TARGET_2ND_ARCH: x86<br/>32-bit secondary"]
end
subgraph "riscv64 Device"
C1["TARGET_ARCH: riscv64<br/>64-bit only"]
end
RISC-V currently has no secondary architecture -- it is 64-bit only. There is no 32-bit RISC-V Android port.
57.5.2 Zygote Configuration¶
The multilib choice directly affects how Zygote processes are started. AOSP includes several Zygote initialization scripts:
# build/make/target/product/core_64_bit.mk, line 26-34
PRODUCT_PACKAGES += init.zygote64.rc init.zygote64_32.rc
# Set the zygote property to select the 64-bit primary, 32-bit secondary script
ifeq ($(ZYGOTE_FORCE_64),true)
PRODUCT_VENDOR_PROPERTIES += ro.zygote=zygote64
else
PRODUCT_VENDOR_PROPERTIES += ro.zygote=zygote64_32
endif
TARGET_SUPPORTS_32_BIT_APPS := true
TARGET_SUPPORTS_64_BIT_APPS := true
For 64-bit-only devices:
# build/make/target/product/core_64_bit_only.mk, line 23-33
PRODUCT_PACKAGES += init.zygote64.rc
PRODUCT_VENDOR_PROPERTIES += ro.zygote=zygote64
PRODUCT_VENDOR_PROPERTIES += dalvik.vm.dex2oat64.enabled=true
TARGET_SUPPORTS_32_BIT_APPS := false
TARGET_SUPPORTS_64_BIT_APPS := true
TARGET_SUPPORTS_OMX_SERVICE := false
57.5.3 The compile_multilib Property¶
In Android.bp files, modules use the compile_multilib property to control
which architectures they are built for:
cc_library {
name: "libexample",
compile_multilib: "both", // Build for both 32 and 64 bit
srcs: ["example.cpp"],
}
The valid values are decoded by decodeMultilibTargets() in
build/soong/android/arch.go:
// build/soong/android/arch.go, line 1939-1981
func decodeMultilibTargets(multilib string, targets []Target, prefer32 bool) ([]Target, error) {
var buildTargets []Target
switch multilib {
case "common":
buildTargets = getCommonTargets(targets)
case "both":
if prefer32 {
buildTargets = append(buildTargets, filterMultilibTargets(targets, "lib32")...)
buildTargets = append(buildTargets, filterMultilibTargets(targets, "lib64")...)
} else {
buildTargets = append(buildTargets, filterMultilibTargets(targets, "lib64")...)
buildTargets = append(buildTargets, filterMultilibTargets(targets, "lib32")...)
}
case "32":
buildTargets = filterMultilibTargets(targets, "lib32")
case "64":
buildTargets = filterMultilibTargets(targets, "lib64")
case "first":
if prefer32 {
buildTargets = FirstTarget(targets, "lib32", "lib64")
} else {
buildTargets = FirstTarget(targets, "lib64", "lib32")
}
case "first_prefer32":
buildTargets = FirstTarget(targets, "lib32", "lib64")
case "prefer32":
buildTargets = filterMultilibTargets(targets, "lib32")
if len(buildTargets) == 0 {
buildTargets = filterMultilibTargets(targets, "lib64")
}
// ...
}
return buildTargets, nil
}
| Value | Meaning |
|---|---|
"both" |
Build for both 32-bit and 64-bit |
"first" |
Build only for the primary architecture |
"32" |
Build only for 32-bit |
"64" |
Build only for 64-bit |
"prefer32" |
Build for 32-bit if available, else 64-bit |
"first_prefer32" |
Like first but prefers 32-bit |
"common" |
Architecture-independent (e.g., Java) |
57.5.4 Architecture-Specific Sources in Android.bp¶
The arch: block in Android.bp files allows modules to include
architecture-specific source files, compiler flags, or dependencies:
cc_library {
name: "libexample",
srcs: ["common.cpp"],
arch: {
arm: {
srcs: ["arm_optimized.S"],
cflags: ["-DHAS_NEON"],
},
arm64: {
srcs: ["arm64_optimized.S"],
},
x86: {
srcs: ["x86_optimized.S"],
cflags: ["-DHAS_SSE"],
},
x86_64: {
srcs: ["x86_64_optimized.S"],
},
riscv64: {
srcs: ["riscv64_optimized.S"],
},
},
}
A real example from bionic shows this pattern at scale:
// bionic/libc/Android.bp (around line 980)
arch: {
arm: {
srcs: [
"arch-arm/bionic/__aeabi_read_tp.S",
"arch-arm/bionic/__bionic_clone.S",
"arch-arm/bionic/__restore.S",
"arch-arm/bionic/_exit_with_stack_teardown.S",
"arch-arm/bionic/atomics_arm.c",
"arch-arm/bionic/setjmp.S",
"arch-arm/bionic/syscall.S",
"arch-arm/bionic/vfork.S",
"arch-arm/cortex-a7/string/memcpy.S",
"arch-arm/cortex-a7/string/memset.S",
"arch-arm/cortex-a9/string/memcpy.S",
"arch-arm/cortex-a15/string/memcpy.S",
// ... many more CPU-specific string routines
],
},
arm64: {
srcs: [
"arch-arm64/bionic/__bionic_clone.S",
"arch-arm64/bionic/_exit_with_stack_teardown.S",
"arch-arm64/bionic/setjmp.S",
"arch-arm64/bionic/syscall.S",
"arch-arm64/bionic/vfork.S",
"arch-arm64/oryon/memcpy-nt.S",
"arch-arm64/oryon/memset-nt.S",
],
},
riscv64: {
srcs: [
"arch-riscv64/bionic/__bionic_clone.S",
"arch-riscv64/bionic/_exit_with_stack_teardown.S",
"arch-riscv64/bionic/setjmp.S",
"arch-riscv64/bionic/syscall.S",
"arch-riscv64/bionic/vfork.S",
"arch-riscv64/string/memchr.S",
"arch-riscv64/string/memcmp.S",
"arch-riscv64/string/memcpy.S",
// ... more RISC-V string routines
],
},
},
57.5.5 Output Directory Structure¶
The multilib mechanism produces output in separate directories. The 32-bit
secondary architecture outputs go to obj_<arch>:
out/target/product/generic_arm64/
obj/ # 64-bit (primary) object files
obj_arm/ # 32-bit (secondary) object files
system/
lib64/ # 64-bit shared libraries
lib/ # 32-bit shared libraries
This is configured in build/make/core/envsetup.mk:
# build/make/core/envsetup.mk, line 582-586
$(TARGET_2ND_ARCH_VAR_PREFIX)TARGET_OUT_INTERMEDIATES := \
$(PRODUCT_OUT)/obj_$(TARGET_2ND_ARCH)
$(TARGET_2ND_ARCH_VAR_PREFIX)TARGET_OUT_SHARED_LIBRARIES := \
$(target_out_shared_libraries_base)/lib
57.6 Compiler Configuration¶
The compiler configuration in AOSP is centralized in
build/soong/cc/config/global.go (633 lines) and applies to all architectures.
This file defines the common compilation flags, warning policies, debug
settings, and Clang toolchain paths that form the baseline for every native
build.
57.6.1 Common Global CFLAGS¶
The commonGlobalCflags array defines flags applied to every C/C++ compilation
in AOSP:
// build/soong/cc/config/global.go, line 32-160
commonGlobalCflags = []string{
"-O2",
"-Wall",
"-Wextra",
"-Wpointer-arith",
"-Wunguarded-availability",
// Warnings treated as errors
"-Werror=bool-operation",
"-Werror=date-time", // Nondeterministic builds
"-Werror=int-conversion",
"-Werror=multichar",
"-Werror=pragma-pack",
"-Werror=sizeof-array-div",
"-Werror=sizeof-pointer-memaccess",
"-Werror=string-plus-int",
"-Werror=unreachable-code-loop-increment",
// ...
// Preprocessor defines
"-DANDROID",
"-DNDEBUG",
"-UDEBUG",
"-D__compiler_offsetof=__builtin_offsetof",
"-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__",
// Code generation options
"-faddrsig",
"-fdebug-default-version=5",
"-fcolor-diagnostics",
"-ffp-contract=off",
"-fno-exceptions",
"-fno-strict-aliasing",
"-fmessage-length=0",
"-gsimple-template-names",
"-gz=zstd",
"-no-canonical-prefixes",
}
Several of these deserve explanation:
-O2: The default optimization level for all Android code. Not -O3,
because -O3 enables aggressive optimizations (like loop unrolling and function
inlining) that can increase code size, which matters on mobile devices where
instruction cache pressure affects battery life.
-DANDROID: Defines the ANDROID preprocessor macro, which is checked by
thousands of #ifdef ANDROID blocks throughout AOSP and third-party code.
-DNDEBUG: Disables assert() in release builds. This is defined globally
because even debug builds of the platform generally do not want assert failures
in production code.
-fno-exceptions: Disables C++ exceptions globally. The Google C++ style
guide forbids exceptions, and bionic's C++ support library (libc++) is built
without exception support on Android.
-fno-strict-aliasing: Disables type-based alias analysis optimizations.
While this could improve performance, the comment explains the trade-off:
"The performance benefit of enabling them currently does not outweigh the risk
of hard-to-reproduce bugs."
-gz=zstd: Compresses debug information with Zstandard, significantly
reducing build output size without losing debug capability.
57.6.2 Device-Specific CFLAGS¶
Flags that apply only to device (not host) code:
// build/soong/cc/config/global.go, line 172-193
deviceGlobalCflags = []string{
"-ffunction-sections",
"-fdata-sections",
"-fno-short-enums",
"-funwind-tables",
"-fstack-protector-strong",
"-Wa,--noexecstack",
"-D_FORTIFY_SOURCE=3",
"-Werror=non-virtual-dtor",
"-Werror=address",
"-Werror=sequence-point",
"-Werror=format-security",
}
-ffunction-sections / -fdata-sections: Each function and data object
gets its own ELF section, enabling the linker to discard unused functions and
data via --gc-sections. This is critical for reducing binary size on mobile.
-fstack-protector-strong: Inserts stack canaries in functions that have
local arrays or take the address of a local variable. The "strong" variant
protects more functions than -fstack-protector but fewer than
-fstack-protector-all, balancing security with performance.
-D_FORTIFY_SOURCE=3: The highest level of compile-time and runtime
buffer overflow detection. Level 3 extends beyond the basic memcpy /
strcpy checks of level 2 to cover more functions and usage patterns.
57.6.3 Device Linker Flags¶
// build/soong/cc/config/global.go, line 206-220
deviceGlobalLdflags = slices.Concat([]string{
"-Wl,-z,noexecstack",
"-Wl,-z,relro",
"-Wl,-z,now",
"-Wl,--build-id=md5",
"-Wl,--fatal-warnings",
"-Wl,--no-undefined-version",
"-Wl,--exclude-libs,libgcc.a",
"-Wl,--exclude-libs,libgcc_stripped.a",
"-Wl,--exclude-libs,libunwind_llvm.a",
"-Wl,--exclude-libs,libunwind.a",
"-Wl,--compress-debug-sections=zstd",
}, commonGlobalLdflags)
The security-relevant flags:
-Wl,-z,noexecstack: Marks the stack as non-executable (NX bit).-
-Wl,-z,relro: Read-only relocations -- makes GOT entries read-only after relocation. -
-Wl,-z,now: Immediate binding -- resolves all symbols at load time rather than lazily, eliminating the window where GOT entries are writable.
Together, these flags form a defense-in-depth strategy against exploitation.
The common linker flags shared between device and host:
// build/soong/cc/config/global.go, line 195-199
commonGlobalLdflags = []string{
"-fuse-ld=lld",
"-Wl,--icf=safe",
"-Wl,--no-demangle",
}
-fuse-ld=lld: Use LLVM's LLD linker instead of GNU ld. LLD is
significantly faster and is the only linker supported by AOSP.
-Wl,--icf=safe: Identical Code Folding -- merges functions with identical
machine code to save space. The "safe" mode only folds functions whose address
is never taken, avoiding subtle bugs.
57.6.4 Non-Overridable Flags¶
Some warnings are so important that modules cannot disable them even if they use
-Wno-error or similar flags in their Android.bp:
// build/soong/cc/config/global.go, line 255-326
noOverrideGlobalCflags = []string{
"-Werror=address-of-temporary",
"-Werror=dangling",
"-Werror=format-insufficient-args",
"-Werror=fortify-source",
"-Werror=incompatible-function-pointer-types",
"-Werror=int-in-bool-context",
"-Werror=int-to-pointer-cast",
"-Werror=null-dereference",
"-Werror=return-type",
"-Werror=xor-used-as-pow",
// ... plus many temporary compiler upgrade workarounds
}
These are appended after the module's own cflags, so they cannot be overridden. The flags target critical safety issues: null dereference, dangling pointers, format string bugs, and buffer overflows.
57.6.5 The Flag Layering Model¶
The complete set of flags applied to a compilation command is assembled in layers:
graph TB
subgraph "Appended After Module Flags (Cannot Override)"
G["noOverrideGlobalCflags<br/>-Werror=null-dereference<br/>-Werror=return-type<br/>..."]
end
subgraph "Module-Level Flags"
F["Module cflags from Android.bp"]
end
subgraph "Architecture-Specific"
E["Toolchain Cflags<br/>(arch variant + CPU variant + features)"]
end
subgraph "Architecture Base"
D["Architecture Cflags<br/>(e.g., Arm64Cflags)"]
end
subgraph "Device/Host Split"
C["Device Global Cflags<br/>-ffunction-sections, -fstack-protector-strong, etc."]
end
subgraph "Common Global"
B["Common Global Cflags<br/>-O2, -Wall, -DANDROID, etc."]
end
B --> C --> D --> E --> F --> G
57.6.6 Clang Toolchain Version¶
Global.go also manages the Clang compiler version:
// build/soong/cc/config/global.go, line 410-422
CStdVersion = "gnu23"
CppStdVersion = "gnu++20"
ExperimentalCStdVersion = "gnu2y"
ExperimentalCppStdVersion = "gnu++2b"
ClangDefaultBase = "prebuilts/clang/host"
ClangDefaultVersion = "clang-r563880c"
ClangDefaultShortVersion = "21"
AOSP uses C23 (gnu23) for C code and C++20 (gnu++20) for C++ code.
The gnu prefix means GNU extensions are enabled. The specific Clang version
clang-r563880c (Clang 21) is pinned in the source and can be overridden
via environment variables.
57.6.7 Auto Variable Initialization¶
A notable security feature is automatic variable initialization:
// build/soong/cc/config/global.go, line 454-463
if ctx.Config().IsEnvTrue("AUTO_ZERO_INITIALIZE") {
flags = append(flags, "-ftrivial-auto-var-init=zero")
} else if ctx.Config().IsEnvTrue("AUTO_PATTERN_INITIALIZE") {
flags = append(flags, "-ftrivial-auto-var-init=pattern")
} else if ctx.Config().IsEnvTrue("AUTO_UNINITIALIZE") {
flags = append(flags, "-ftrivial-auto-var-init=uninitialized")
} else {
// Default to zero initialization.
flags = append(flags, "-ftrivial-auto-var-init=zero")
}
By default, all stack variables are zero-initialized. This eliminates an entire class of uninitialized-variable bugs at a small runtime cost. The comment references bug b/131390872, which tracked the rollout of this feature.
57.6.8 Sanitizer Runtime Libraries¶
The toolchain.go file defines helper functions for all the sanitizer runtime
libraries:
// build/soong/cc/config/toolchain.go, line 219-265
func AddressSanitizerRuntimeLibrary() string {
return LibclangRuntimeLibrary("asan")
}
func HWAddressSanitizerRuntimeLibrary() string {
return LibclangRuntimeLibrary("hwasan")
}
func UndefinedBehaviorSanitizerRuntimeLibrary() string {
return LibclangRuntimeLibrary("ubsan_standalone")
}
func ThreadSanitizerRuntimeLibrary() string {
return LibclangRuntimeLibrary("tsan")
}
func ScudoRuntimeLibrary() string {
return LibclangRuntimeLibrary("scudo")
}
func LibFuzzerRuntimeLibrary() string {
return LibclangRuntimeLibrary("fuzzer")
}
These functions return library names like libclang_rt.asan, which are then
resolved to architecture-specific binaries using the
LibclangRuntimeLibraryArch() method from each toolchain (e.g., "aarch64" for
ARM64, "i686" for x86).
57.6.9 External Code Flags¶
Third-party code (anything under external/, most of vendor/, and most of
hardware/) gets relaxed warning treatment:
// build/soong/cc/config/global.go (line 339-364)
extraExternalCflags = []string{
"-Wno-enum-compare",
"-Wno-enum-compare-switch",
"-Wno-null-pointer-arithmetic",
"-Wno-psabi",
"-Wno-null-pointer-subtraction",
"-Wno-string-concatenation",
"-Wno-deprecated-non-prototype",
"-Wno-unused",
"-Wno-unused-but-set-variable",
"-Wno-deprecated",
"-Wno-tautological-constant-compare",
"-Wno-error=range-loop-construct",
}
And the non-overridable flags for external code are even more permissive:
// build/soong/cc/config/global.go (line 370-393)
noOverrideExternalGlobalCflags = []string{
"-fcommon",
"-Wno-format-insufficient-args",
"-Wno-misleading-indentation",
"-Wno-unused",
"-Wno-unused-parameter",
"-Wno-unused-but-set-parameter",
"-Wno-unused-variable",
"-Wno-unqualified-std-cast-call",
"-Wno-array-parameter",
"-Wno-gnu-offsetof-extensions",
"-Wno-pessimizing-move",
"-Wno-pointer-to-int-cast",
}
The -fcommon flag (marked with bug b/151457797) is particularly notable.
Modern C compilers default to -fno-common, which makes tentative definitions
of global variables into strong symbols. Many legacy C libraries rely on the
old behavior where tentative definitions are "common" symbols that can be
merged across translation units. Without -fcommon, these libraries fail to
link.
57.6.10 Illegal Flags¶
AOSP bans certain compiler flags entirely:
// build/soong/cc/config/global.go (line 401-408)
IllegalFlags = []string{
"-w",
"-pedantic",
"-pedantic-errors",
"-Werror=pedantic",
"-Wno-all",
"-Wno-everything",
}
The -w flag suppresses all warnings, which would undermine the entire
warning infrastructure. -Wno-all and -Wno-everything have the same
effect. -pedantic flags are banned because they trigger thousands of
warnings from legitimate GNU extension usage throughout AOSP.
57.6.11 Language Standard Versions¶
AOSP specifies modern language standards:
// build/soong/cc/config/global.go (line 410-413)
CStdVersion = "gnu23"
CppStdVersion = "gnu++20"
ExperimentalCStdVersion = "gnu2y"
ExperimentalCppStdVersion = "gnu++2b"
-
C23 (
gnu23): The latest C standard (ISO/IEC 9899:2024), with GNU extensions. This enables features liketypeof,auto,constexpr, and improved_Static_assert. -
C++20 (
gnu++20): Enables concepts, ranges, coroutines, modules, three-way comparison, and many other major C++ features. -
The "experimental" versions (
gnu2y,gnu++2b) target the next standard revision and are used for modules that opt into bleeding-edge features.
57.6.12 Clang Unknown Flags Filter¶
The clang.go file maintains a list of GCC flags that Clang does not
understand, which must be filtered out when processing legacy build files:
// build/soong/cc/config/clang.go, line 25-74
var ClangUnknownCflags = sorted([]string{
"-finline-functions",
"-finline-limit=64",
"-fno-canonical-system-headers",
// ...
// arm + arm64
"-fgcse-after-reload",
"-frerun-cse-after-loop",
"-frename-registers",
// arm
"-mthumb-interwork",
"-fno-caller-saves",
// x86 + x86_64
"-finline-limit=300",
"-mfpmath=sse",
"-mbionic",
// windows
"--enable-stdcall-fixup",
})
This list is a historical artifact -- AOSP used to support GCC compilation, and many third-party projects still reference GCC-specific flags. The filter silently removes these rather than causing build failures.
57.7 Generic Device Configurations¶
AOSP provides generic device configurations under device/generic/ for
reference, testing, and emulator use. These configurations define the minimum
viable settings for each supported architecture.
57.7.1 Directory Structure¶
device/generic/
arm64/ - 64-bit ARM reference device
armv7-a-neon/ - 32-bit ARM with NEON
x86/ - 32-bit x86
x86_64/ - 64-bit x86
art/ - ART runtime test devices
armv8/ - ARM64 for ART testing
riscv64/ - RISC-V for ART testing
silvermont/ - x86 Silvermont for ART testing
arm_krait/ - ARM Krait for ART testing
arm_v7_v8/ - ARM v7/v8 for ART testing
armv8_cortex_a55/ - ARM64 Cortex-A55 tuned
armv8_kryo385/ - ARM64 Kryo 385 tuned
car/ - Android Automotive targets
trusty/ - Trusted Execution Environment
common/ - Shared resources
goldfish/ - Legacy emulator
57.7.2 ARM64 Generic Device¶
The ARM64 generic device is the most common reference target:
# device/generic/arm64/BoardConfig.mk
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := generic
TARGET_CPU_ABI := arm64-v8a
TARGET_2ND_ARCH := arm
TARGET_2ND_ARCH_VARIANT := armv7-a-neon
TARGET_2ND_CPU_VARIANT := cortex-a15
TARGET_2ND_CPU_ABI := armeabi-v7a
TARGET_2ND_CPU_ABI2 := armeabi
This configuration establishes a dual-architecture device:
- Primary: ARM64 with the baseline ARMv8-A ISA
- Secondary: 32-bit ARM with NEON, tuned for Cortex-A15
The product configuration inherits the 64-bit core and the common mini configuration:
# device/generic/arm64/mini_arm64.mk
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)
$(call inherit-product, device/generic/armv7-a-neon/mini_common.mk)
PRODUCT_NAME := mini_arm64
PRODUCT_DEVICE := arm64
57.7.3 ARM 32-bit Generic Device¶
The 32-bit ARM device targets the ARMv7-A architecture with NEON:
# device/generic/armv7-a-neon/BoardConfig.mk
TARGET_ARCH := arm
TARGET_ARCH_VARIANT := armv7-a-neon
TARGET_CPU_VARIANT := generic
TARGET_CPU_ABI := armeabi-v7a
TARGET_CPU_ABI2 := armeabi
This has no secondary architecture -- it is pure 32-bit. The armeabi
secondary ABI provides compatibility with ancient pre-NEON ARM code (ARMv5TE).
57.7.4 x86_64 Generic Device¶
# device/generic/x86_64/BoardConfig.mk
TARGET_CPU_ABI := x86_64
TARGET_ARCH := x86_64
TARGET_ARCH_VARIANT := x86_64
TARGET_2ND_CPU_ABI := x86
TARGET_2ND_ARCH := x86
TARGET_2ND_ARCH_VARIANT := x86_64
57.7.5 ART Test Devices¶
The device/generic/art/ directory contains specialized configurations for
testing the ART runtime on different CPU variants. These are not real devices
but rather build targets that exercise specific ISA features:
# device/generic/art/BoardConfigCommon.mk
TARGET_NO_BOOTLOADER := true
TARGET_NO_KERNEL := true
TARGET_CPU_SMP := true
$(call soong_config_set,art_module,source_build,true)
# device/generic/art/armv8/BoardConfig.mk
TARGET_ARCH := arm64
TARGET_CPU_ABI := arm64-v8a
TARGET_CPU_VARIANT := generic
TARGET_ARCH_VARIANT := armv8-a
TARGET_SUPPORTS_64_BIT_APPS := true
The ART test configurations also include vendor-specific variants:
| Directory | Configuration | Purpose |
|---|---|---|
armv8/ |
Generic ARMv8-A | Baseline ARM64 testing |
armv8_cortex_a55/ |
Cortex-A55 | LITTLE core testing |
armv8_kryo385/ |
Kryo 385 | Qualcomm core testing |
arm_krait/ |
Krait | Qualcomm 32-bit testing |
arm_v7_v8/ |
ARMv7/v8 | Cross-version testing |
riscv64/ |
RISC-V 64 | RISC-V ART testing |
silvermont/ |
Silvermont | Intel Atom testing |
57.7.6 Android Automotive (Car)¶
The device/generic/car/ directory demonstrates multi-architecture automotive
targets:
device/generic/car/
emulator_car64_arm64/ - ARM64 automotive emulator
emulator_car64_x86_64/ - x86_64 automotive emulator
car_x86_64/ - x86_64 automotive device
sdk_car_arm64.mk - ARM64 SDK
sdk_car_x86_64.mk - x86_64 SDK
sdk_car_md_arm64.mk - Multi-display ARM64
sdk_car_md_x86_64.mk - Multi-display x86_64
gsi_car_arm64.mk - ARM64 GSI (Generic System Image)
gsi_car_x86_64.mk - x86_64 GSI
57.7.7 Trusty TEE¶
The Trusty Trusted Execution Environment provides a secure world that runs alongside Android:
Trusty runs as a separate OS in ARM TrustZone (or equivalent secure monitor mode), and its build system must produce code for the secure world that is compatible with the normal world's architecture.
57.7.8 The AOSP Product Build¶
The full AOSP product combines a generic device with system, vendor, and product image configurations:
# build/make/target/product/aosp_arm64.mk
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/generic_system.mk)
PRODUCT_NAME := aosp_arm64
PRODUCT_DEVICE := generic_arm64
PRODUCT_BRAND := Android
PRODUCT_MODEL := AOSP on ARM64
PRODUCT_NO_BIONIC_PAGE_SIZE_MACRO := true
The PRODUCT_NO_BIONIC_PAGE_SIZE_MACRO := true flag is a recent addition that
prevents bionic from exposing a fixed PAGE_SIZE macro, allowing the system to
support 16KB pages on ARM64 (a kernel configuration option that improves TLB
performance).
57.8 Architecture-Specific Code Patterns¶
Across AOSP, several major components contain hand-tuned assembly code for each supported architecture. This section examines the patterns used in bionic, ART, and other performance-critical subsystems.
57.8.1 Bionic: Architecture Directories¶
Bionic organizes architecture-specific code in arch-<arch>/ subdirectories:
bionic/libc/
arch-arm/
bionic/ - Low-level ARM32 routines (clone, setjmp, syscall)
cortex-a7/ - Cortex-A7 tuned string functions
cortex-a9/ - Cortex-A9 tuned string functions
cortex-a15/ - Cortex-A15 tuned string functions
cortex-a53/ - Cortex-A53 tuned string functions
cortex-a55/ - Cortex-A55 tuned string functions
generic/ - Generic ARM string functions
krait/ - Qualcomm Krait tuned string functions
kryo/ - Qualcomm Kryo tuned string functions
arch-arm64/
bionic/ - Low-level ARM64 routines
string/ - ARM64 string function stubs
oryon/ - Qualcomm Oryon tuned routines
arch-x86/
bionic/ - Low-level x86 routines
string/ - x86 string functions
arch-x86_64/
bionic/ - Low-level x86_64 routines
string/ - x86_64 string functions
arch-riscv64/
bionic/ - Low-level RISC-V routines
string/ - RISC-V string functions (SiFive contributed)
57.8.2 Bionic: The ifunc Dispatch Pattern (ARM64)¶
ARM64 bionic uses the GNU indirect function (ifunc) mechanism to select the best implementation of common functions at runtime. The ifunc resolver runs during dynamic linking and chooses an implementation based on hardware capabilities:
// bionic/libc/arch-arm64/ifuncs.cpp (line 41-49)
static inline bool __bionic_is_oryon(unsigned long hwcap) {
if (!(hwcap & HWCAP_CPUID)) return false;
unsigned long midr;
__asm__ __volatile__("mrs %0, MIDR_EL1" : "=r"(midr));
// Check for implementor Qualcomm's parts 0..15 (Oryon).
return implementer(midr) == 'Q' && part(midr) <= 15;
}
The memcpy ifunc resolver demonstrates the multi-level dispatch:
// bionic/libc/arch-arm64/ifuncs.cpp (line 69-79)
DEFINE_IFUNC_FOR(memcpy) {
if (arg->_hwcap2 & HWCAP2_MOPS) {
RETURN_FUNC(memcpy_func_t, __memmove_aarch64_mops);
} else if (__bionic_is_oryon(arg->_hwcap)) {
RETURN_FUNC(memcpy_func_t, __memcpy_aarch64_nt);
} else if (arg->_hwcap & HWCAP_ASIMD) {
RETURN_FUNC(memcpy_func_t, __memcpy_aarch64_simd);
} else {
RETURN_FUNC(memcpy_func_t, __memcpy_aarch64);
}
}
The priority order is:
-
MOPS (Memory Copy and Memory Set instructions, ARMv8.8-A) -- hardware
memcpyinstructions -
Oryon -- Qualcomm's custom core with non-temporal store optimizations
- ASIMD (Advanced SIMD, a.k.a. NEON) -- vectorized copy
- Fallback -- scalar implementation
graph TD
A["memcpy() called"] --> B{"HWCAP2_MOPS?"}
B -->|Yes| C["__memmove_aarch64_mops<br/>(Hardware MOPS instructions)"]
B -->|No| D{"Is Oryon CPU?"}
D -->|Yes| E["__memcpy_aarch64_nt<br/>(Non-temporal stores)"]
D -->|No| F{"HWCAP_ASIMD?"}
F -->|Yes| G["__memcpy_aarch64_simd<br/>(NEON/AdvSIMD vectorized)"]
F -->|No| H["__memcpy_aarch64<br/>(Scalar fallback)"]
The same pattern applies to memmove, memset, strlen, strchr, and other
hot string functions. MTE-aware variants are selected when HWCAP2_MTE is
present:
// bionic/libc/arch-arm64/ifuncs.cpp (line 100-108)
DEFINE_IFUNC_FOR(memset) {
if (arg->_hwcap2 & HWCAP2_MOPS) {
RETURN_FUNC(memset_func_t, __memset_aarch64_mops);
} else if (__bionic_is_oryon(arg->_hwcap)) {
RETURN_FUNC(memset_func_t, __memset_aarch64_nt);
} else {
RETURN_FUNC(memset_func_t, __memset_aarch64);
}
}
// bionic/libc/arch-arm64/ifuncs.cpp (line 117-123)
DEFINE_IFUNC_FOR(strchr) {
if (arg->_hwcap2 & HWCAP2_MTE) {
RETURN_FUNC(strchr_func_t, __strchr_aarch64_mte);
} else {
RETURN_FUNC(strchr_func_t, __strchr_aarch64);
}
}
57.8.3 Bionic: ARM 32-bit CPU-Variant String Functions¶
ARM 32-bit bionic takes a different approach: instead of runtime ifunc dispatch,
it compiles multiple CPU-variant-specific implementations and selects at build
time based on the device's TARGET_CPU_VARIANT. The source tree contains
separate directories for each CPU variant:
arch-arm/cortex-a7/string/memcpy.S - Tuned for A7's in-order pipeline
arch-arm/cortex-a9/string/memcpy.S - Tuned for A9's partial out-of-order
arch-arm/cortex-a15/string/memcpy.S - Tuned for A15's full out-of-order
arch-arm/cortex-a53/string/memcpy.S - Tuned for A53's 64-bit in-order
arch-arm/cortex-a55/string/memcpy.S - Tuned for A55's enhanced in-order
arch-arm/krait/string/memcpy.S - Tuned for Qualcomm Krait
arch-arm/kryo/string/memcpy.S - Tuned for Qualcomm Kryo
Each memcpy.S contains different hand-written assembly that exploits the
target core's pipeline characteristics -- prefetch distances, store buffer
depths, cache line sizes, and NEON instruction scheduling.
57.8.4 Bionic: RISC-V String Functions with Vector Extension¶
The RISC-V string implementations in bionic were contributed by SiFive and use the RISC-V Vector extension:
bionic/libc/arch-riscv64/string/
memchr.S memcmp.S memcpy.S memmove.S memset.S
stpcpy.S strcat.S strchr.S strcmp.S strcpy.S
strlen.S strncat.S strncmp.S strncpy.S strnlen.S
The copyright headers in these files attribute them to both "The Android Open Source Project" and "SiFive, Inc." -- SiFive is a leading RISC-V chip designer that contributed these optimized implementations.
57.8.5 Bionic: Low-Level Architecture Functions¶
Every architecture must implement a set of core low-level functions in assembly. These cannot be written in C because they manipulate the stack, registers, or execution context in ways that C cannot express:
| Function | Purpose | Why Assembly? |
|---|---|---|
__bionic_clone.S |
clone() syscall wrapper |
Must set up child stack and call entry point |
_exit_with_stack_teardown.S |
Thread exit | Must deallocate own stack while running |
setjmp.S / longjmp.S |
Non-local jumps | Must save/restore all callee-saved registers |
syscall.S |
Raw syscall interface | Must move args to syscall registers |
vfork.S |
vfork() implementation |
Must not clobber parent's stack frame |
The ARM64 setjmp.S shows the register-level detail required:
// bionic/libc/arch-arm64/bionic/setjmp.S (line 32-51)
// According to AARCH64 PCS document we need to save:
// Core x19 - x30, sp (see section 5.1.1)
// VFP d8 - d15 (see section 5.1.2)
//
// jmp_buf layout:
// word name description
// 0 sigflag/cookie setjmp cookie in top 31 bits, signal mask flag in low bit
// 1 sigmask signal mask
// 2 core_base base of core registers (x18-x30, sp)
// 16 float_base base of float registers (d8-d15)
// 24 checksum checksum of core registers
// 25 reserved reserved entries (room to grow)
57.8.6 Bionic: MTE Integration¶
ARM64 bionic includes ELF notes that control MTE behavior. Two variants exist:
-
note_memtag_heap_async.S-- Requests asynchronous MTE checking (lower overhead, delayed error reporting) -
note_memtag_heap_sync.S-- Requests synchronous MTE checking (higher overhead, immediate error reporting)
// bionic/libc/arch-arm64/bionic/note_memtag_heap_async.S (line 34-46)
.section ".note.android.memtag", "a", %note
.p2align 2
.long 1f - 0f // int32_t namesz
.long 3f - 2f // int32_t descsz
.long NT_ANDROID_TYPE_MEMTAG // int32_t type
0:
.asciz "Android" // char name[]
1:
.p2align 2
2:
.long (NT_MEMTAG_LEVEL_ASYNC | NT_MEMTAG_HEAP) // value
3:
These notes are linked into binaries that opt into MTE. The dynamic linker reads them and configures the process's memory tagging mode before the main program runs.
57.8.7 ART Runtime: Architecture-Specific Entrypoints¶
The Android Runtime (ART) contains extensive architecture-specific code for JIT compilation, garbage collection, and JNI transitions. Each architecture has a complete set of entrypoint files:
art/runtime/arch/arm64/
asm_support_arm64.S - Assembly constants and macros
asm_support_arm64.h - Shared constants
callee_save_frame_arm64.h - Callee-save frame layout
context_arm64.cc - CPU context save/restore
entrypoints_init_arm64.cc - Entrypoint table initialization
fault_handler_arm64.cc - Signal handler for null checks
instruction_set_features_arm64.cc - ISA feature detection
jni_entrypoints_arm64.S - JNI call trampolines
quick_entrypoints_arm64.S - Quick compiler entrypoints
registers_arm64.cc - Register definitions
thread_arm64.cc - Thread-local storage access
The same structure is replicated for every architecture:
art/runtime/arch/
arm/ - ARM32 entrypoints
arm64/ - ARM64 entrypoints
x86/ - x86 entrypoints
x86_64/ - x86_64 entrypoints
riscv64/ - RISC-V 64 entrypoints
57.8.8 ART: Instruction Set Feature Detection¶
ART's instruction_set_features.cc dispatches feature detection to
architecture-specific implementations:
// art/runtime/arch/instruction_set_features.cc (line 33-53)
std::unique_ptr<const InstructionSetFeatures> InstructionSetFeatures::FromVariant(
InstructionSet isa, const std::string& variant, std::string* error_msg) {
switch (isa) {
case InstructionSet::kArm:
case InstructionSet::kThumb2:
return ArmInstructionSetFeatures::FromVariant(variant, error_msg);
case InstructionSet::kArm64:
return Arm64InstructionSetFeatures::FromVariant(variant, error_msg);
case InstructionSet::kRiscv64:
return Riscv64InstructionSetFeatures::FromVariant(variant, error_msg);
case InstructionSet::kX86:
return X86InstructionSetFeatures::FromVariant(variant, error_msg);
case InstructionSet::kX86_64:
return X86_64InstructionSetFeatures::FromVariant(variant, error_msg);
// ...
}
}
The ARM64 feature detection (instruction_set_features_arm64.cc) is
particularly detailed, tracking specific CPU errata and optional ISA extensions:
// art/runtime/arch/arm64/instruction_set_features_arm64.cc (line 52-85)
static const char* arm64_variants_with_a53_835769_bug[] = {
"default", "generic",
"cortex-a53", "cortex-a53.a57", "cortex-a53.a72",
"cortex-a57", "cortex-a72", "cortex-a73",
};
static const char* arm64_variants_with_crc[] = {
"default", "generic", "cortex-a35", "cortex-a53", ...
};
static const char* arm64_variants_with_lse[] = {
"cortex-a55", "cortex-a75", "cortex-a76", "kryo385", "kryo785",
};
static const char* arm64_variants_with_fp16[] = {
"cortex-a55", "cortex-a75", "cortex-a76", "kryo385", "kryo785",
};
static const char* arm64_variants_with_dotprod[] = {
"cortex-a55", "cortex-a75", "cortex-a76",
};
These feature lists are used by the ART JIT compiler to decide which
instructions to emit. For example, if has_lse is true, the JIT can emit LSE
(Large System Extensions) atomic instructions instead of the slower LL/SC
(Load-Linked/Store-Conditional) loop sequences.
ART also validates the compile-time feature assumptions against runtime
hardware capabilities using FromVariantAndHwcap():
// art/runtime/arch/instruction_set_features.cc (line 55-80)
std::unique_ptr<const InstructionSetFeatures> InstructionSetFeatures::FromVariantAndHwcap(
InstructionSet isa, const std::string& variant, std::string* error_msg) {
auto variant_features = FromVariant(isa, variant, error_msg);
if (variant_features == nullptr) return nullptr;
// Pixel3a is wrongly reporting itself as cortex-a75, so validate the
// features with hwcaps.
if (isa == InstructionSet::kArm64) {
auto new_features = down_cast<const Arm64InstructionSetFeatures*>(
variant_features.get())->IntersectWithHwcap();
if (!variant_features->Equals(new_features.get())) {
LOG(WARNING) << "Mismatch between instruction set variant of device ("
<< *variant_features << ") and features returned by the hardware ("
<< *new_features << ")";
}
return new_features;
}
return variant_features;
}
The comment about Pixel 3a is instructive -- it shows that even Google's own
devices can have incorrect TARGET_CPU_VARIANT settings, making runtime
validation essential.
57.8.9 ART: Quick Entrypoints¶
The quick_entrypoints_arm64.S file contains the assembly routines that bridge
managed (Java/Kotlin) code with the ART runtime. These are some of the most
performance-critical code in Android. The file begins with callee-save frame
setup macros:
// art/runtime/arch/arm64/quick_entrypoints_arm64.S (line 45-60)
.macro SETUP_SAVE_REFS_AND_ARGS_FRAME
LOAD_RUNTIME_INSTANCE xIP0
ldr xIP0, [xIP0, RUNTIME_SAVE_REFS_AND_ARGS_METHOD_OFFSET]
INCREASE_FRAME FRAME_SIZE_SAVE_REFS_AND_ARGS
SETUP_SAVE_REFS_AND_ARGS_FRAME_INTERNAL sp
str xIP0, [sp]
mov xIP0, sp
str xIP0, [xSELF, # THREAD_TOP_QUICK_FRAME_OFFSET]
.endm
These macros manage the transition between managed code (which uses ART's
calling convention) and native code (which uses the platform ABI). The
THREAD_TOP_QUICK_FRAME_OFFSET references the thread-local storage where ART
tracks the current managed stack frame -- essential for garbage collection, stack
walking, and exception handling.
57.8.10 ART: Multiple Feature Detection Strategies¶
ART implements six different strategies for detecting CPU features, reflecting the reality that no single detection method is reliable across all devices:
// art/runtime/arch/arm64/instruction_set_features_arm64.h (line 35-55)
// 1. FromVariant() - Parse a CPU variant string like "cortex-a75"
static Arm64FeaturesUniquePtr FromVariant(const std::string& variant, std::string* error_msg);
// 2. FromBitmap() - Parse a bitmap (used for serialization/deserialization)
static Arm64FeaturesUniquePtr FromBitmap(uint32_t bitmap);
// 3. FromCppDefines() - Use C preprocessor defines set by the compiler
static Arm64FeaturesUniquePtr FromCppDefines();
// 4. FromCpuInfo() - Parse /proc/cpuinfo
static Arm64FeaturesUniquePtr FromCpuInfo();
// 5. FromHwcap() - Use the kernel's AT_HWCAP auxiliary vector
static Arm64FeaturesUniquePtr FromHwcap();
// 6. FromAssembly() - Run assembly tests to probe feature availability
static Arm64FeaturesUniquePtr FromAssembly();
// 7. FromCpuFeatures() - Use external cpu_features library
static Arm64FeaturesUniquePtr FromCpuFeatures();
// 8. IntersectWithHwcap() - Validate variant features against hardware
Arm64FeaturesUniquePtr IntersectWithHwcap() const;
The preferred approach on ARM64 is FromVariantAndHwcap(), which first creates
features from the build-time variant string, then validates them against
runtime hardware capabilities. This two-step process catches cases like the
Pixel 3a, where the device incorrectly reports its CPU variant.
For RISC-V, only FromVariant() and FromCppDefines() are currently
functional -- the hardware detection paths remain as stubs.
57.8.11 Architecture-Specific Build Patterns Summary¶
graph TB
subgraph "Common C/C++ Code"
A["common.cpp"]
end
subgraph "Build-time Selection (Android.bp arch:)"
B1["arch-arm/bionic/setjmp.S"]
B2["arch-arm64/bionic/setjmp.S"]
B3["arch-x86_64/bionic/setjmp.S"]
B4["arch-riscv64/bionic/setjmp.S"]
end
subgraph "Build-time Selection (CPU variant)"
C1["cortex-a7/string/memcpy.S"]
C2["cortex-a15/string/memcpy.S"]
C3["krait/string/memcpy.S"]
end
subgraph "Runtime Selection (ifunc)"
D1["__memcpy_aarch64_mops"]
D2["__memcpy_aarch64_nt"]
D3["__memcpy_aarch64_simd"]
D4["__memcpy_aarch64"]
end
A --> E["Compiled for target arch"]
B1 --> E
B2 --> E
B3 --> E
B4 --> E
C1 --> E
C2 --> E
C3 --> E
E --> F["Linked binary"]
D1 --> G["ifunc resolver<br/>(runs at load time)"]
D2 --> G
D3 --> G
D4 --> G
G --> H["Selected function pointer<br/>in .got"]
The three-level optimization strategy:
-
Architecture selection (build time): The
arch:block inAndroid.bpselects completely different source files per architecture. This is for code that is fundamentally different between architectures (syscall wrappers, setjmp, thread creation). -
CPU variant selection (build time): For ARM 32-bit, separate hand-tuned implementations exist for each major CPU variant. The build system compiles all of them into the same binary, with the appropriate one selected by the ifunc mechanism or by the linker based on device configuration.
-
Runtime hardware detection (load time): ARM64's ifunc resolvers check
hwcapbits at library load time to select the best implementation for the actual hardware. This handles cases where the exact CPU model was not known at build time, or where a single binary must run on multiple hardware generations.
57.8.12 The ARM 32-bit Instruction Set: ARM vs. Thumb¶
ARM 32-bit has a unique feature among AOSP architectures: two instruction encodings. The ARM toolchain supports switching between them:
// build/soong/cc/config/arm_device.go (line 49-54)
armArmCflags = []string{}
armThumbCflags = []string{
"-mthumb",
"-Os",
}
The toolchain's InstructionSetFlags() method selects between them:
// build/soong/cc/config/arm_device.go (line 288-297)
func (t *toolchainArm) InstructionSetFlags(isa string) (string, error) {
switch isa {
case "arm":
return "${config.ArmArmCflags}", nil
case "thumb", "":
return "${config.ArmThumbCflags}", nil
default:
return t.toolchainBase.InstructionSetFlags(isa)
}
}
Thumb mode (the default) uses 16-bit instruction encoding with -Os
(optimize for size). This reduces code size significantly -- critical for the
32-bit secondary architecture that exists primarily for compatibility.
ARM mode uses 32-bit instruction encoding. Modules can opt into ARM mode via
the instruction_set: "arm" property in their Android.bp when they need the
full 32-bit instruction set (e.g., for hand-tuned assembly that uses
instructions not available in Thumb).
57.8.13 The ARM 32-bit Soft Float and NEON¶
ARM 32-bit Android uses soft-float ABI (-mfloat-abi=softfp), meaning
floating-point values are passed in integer registers at function call
boundaries, even though the hardware FPU is used for computation:
// build/soong/cc/config/arm_device.go (line 56-77)
armArchVariantCflags = map[string][]string{
"armv7-a": []string{
"-march=armv7-a",
"-mfloat-abi=softfp",
"-mfpu=vfpv3-d16",
},
"armv7-a-neon": []string{
"-march=armv7-a",
"-mfloat-abi=softfp",
"-mfpu=neon",
},
"armv8-a": []string{
"-march=armv8-a",
"-mfloat-abi=softfp",
"-mfpu=neon-fp-armv8",
},
"armv8-2a": []string{
"-march=armv8.2-a",
"-mfloat-abi=softfp",
"-mfpu=neon-fp-armv8",
},
}
The -mfpu= flag specifies the FPU type:
vfpv3-d16: Basic VFP with 16 double-precision registers (no NEON)neon: Full NEON SIMD unitneon-fp-armv8: NEON with ARMv8 floating-point extensions
The armv7-a-neon variant is the most commonly used for modern 32-bit devices,
as it enables NEON SIMD optimizations.
57.8.14 Bionic Strip Configuration Per Architecture¶
Even the way binaries are stripped varies by architecture. Bionic's
Android.bp configures different strip behavior for each architecture:
// bionic/libc/Android.bp (around line 135-165)
arch: {
arm: {
// arm32 does not produce complete exidx unwind information,
// so keep the .debug_frame which is relatively small and does
// include needed unwind information.
// See b/132992102 for details.
strip: {
keep_symbols_and_debug_frame: true,
},
},
arm64: {
strip: {
keep_symbols: true,
},
},
riscv64: {
strip: {
keep_symbols: true,
},
},
x86: {
strip: {
keep_symbols: true,
},
},
x86_64: {
strip: {
keep_symbols: true,
},
},
},
ARM 32-bit is the only architecture that needs keep_symbols_and_debug_frame.
This is because ARM 32-bit uses a different unwinding mechanism (EXIDX tables in
the .ARM.exidx section) that is incomplete in some cases, so the
.debug_frame section must be preserved as a fallback. All other architectures
use DWARF-based unwinding and only need the symbol table kept.
57.8.15 Page Size Macro Handling¶
The bionic page size macro handling is architecture-aware:
// build/soong/cc/config/arm64_device.go (line 98-106)
pctx.VariableFunc("Arm64Cflags", func(ctx android.PackageVarContext) string {
flags := arm64Cflags
if ctx.Config().NoBionicPageSizeMacro() {
flags = append(flags, "-D__BIONIC_NO_PAGE_SIZE_MACRO")
} else {
flags = append(flags, "-D__BIONIC_DEPRECATED_PAGE_SIZE_MACRO")
}
return strings.Join(flags, " ")
})
The same pattern exists for x86_64:
// build/soong/cc/config/x86_64_device.go (line 111-119)
pctx.VariableFunc("X86_64Cflags", func(ctx android.PackageVarContext) string {
flags := x86_64Cflags
if ctx.Config().NoBionicPageSizeMacro() {
flags = append(flags, "-D__BIONIC_NO_PAGE_SIZE_MACRO")
} else {
flags = append(flags, "-D__BIONIC_DEPRECATED_PAGE_SIZE_MACRO")
}
return strings.Join(flags, " ")
})
This is related to the ongoing effort to support 16KB pages on ARM64. The
PAGE_SIZE macro in <unistd.h> traditionally returns 4096, but this is
incorrect on devices running 16KB kernels. The
__BIONIC_NO_PAGE_SIZE_MACRO flag causes bionic to not define PAGE_SIZE at
all, forcing code to call getpagesize() or sysconf(_SC_PAGE_SIZE) at
runtime. The __BIONIC_DEPRECATED_PAGE_SIZE_MACRO flag keeps the macro but
marks it as deprecated, producing warnings when code uses it.
Product configurations opt into this behavior:
57.8.16 ARM 32-bit LPAE Workaround¶
Several ARM 32-bit CPU variants require a manual define to advertise Large Physical Address Extensions (LPAE) support:
// build/soong/cc/config/arm_device.go (line 80-100)
"cortex-a7": []string{
"-mcpu=cortex-a7",
"-mfpu=neon-vfpv4",
// Fake an ARM compiler flag as these processors support LPAE which clang
// don't advertise.
// TODO This is a hack and we need to add it for each processor that
// supports LPAE until some better solution comes around. See Bug 27340895
"-D__ARM_FEATURE_LPAE=1",
},
This is a long-standing workaround (Bug 27340895) where Clang does not
automatically define __ARM_FEATURE_LPAE for processors that support it. Code
that uses the LPAE-specific ldrd/strd instructions (64-bit atomic
load/store) relies on this macro to detect hardware support.
57.9 Try It¶
Exercise 1: Inspect Architecture Flags for Your Device¶
Look up the BoardConfig.mk for your device (or a reference device) and trace
the compiler flags:
# 1. Find the architecture configuration
cat device/generic/arm64/BoardConfig.mk
# 2. Look up the arch variant flags
grep -A 3 '"armv8-a"' build/soong/cc/config/arm64_device.go
# 3. Look up the CPU variant flags
grep -A 3 '"cortex-a55"' build/soong/cc/config/arm64_device.go
# 4. Check what global flags are applied
head -60 build/soong/cc/config/global.go
Questions to answer:
- What
-march=flag is used for your device? - Is the Cortex-A53 erratum 843419 fix applied?
- What security features does your device enable (PAC/BTI, MTE)?
Exercise 2: Add a New Architecture Variant¶
To add a hypothetical new ARM64 variant (e.g., ARMv9.5-A):
- Add the variant to
arm64ArchVariantCflagsinarm64_device.go:
- Create a
BoardConfig.mkthat uses it:
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv9-5a
TARGET_CPU_VARIANT := generic
TARGET_CPU_ABI := arm64-v8a
- Build and verify:
Exercise 3: Examine ifunc Dispatch¶
Study how bionic selects optimized implementations at runtime:
# 1. Read the ifunc resolver for memcpy
# File: bionic/libc/arch-arm64/ifuncs.cpp
# 2. Run on a device and check which implementation was selected
adb shell 'cat /proc/self/maps | grep memcpy'
# 3. Disassemble the selected implementation
adb shell 'objdump -d /apex/com.android.runtime/lib64/bionic/libc.so' | \
grep -A 20 '__memcpy_aarch64_simd'
Exercise 4: Compare String Functions Across Architectures¶
Compare how memcpy is implemented for different architectures:
# ARM 32-bit (multiple CPU-variant implementations)
ls bionic/libc/arch-arm/*/string/memcpy.S
# ARM64 (runtime-selected via ifunc)
cat bionic/libc/arch-arm64/ifuncs.cpp | grep -A 10 'DEFINE_IFUNC_FOR(memcpy)'
# RISC-V (vector extension based, from SiFive)
head -50 bionic/libc/arch-riscv64/string/memcpy.S
# x86_64 (SSE-based)
ls bionic/libc/arch-x86_64/string/
Questions to answer:
- How many distinct
memcpyimplementations exist for ARM 32-bit? - What hardware feature does the ARM64
memcpycheck first? - What RISC-V extension does the RISC-V
memcpyuse?
Exercise 5: Build for Multiple Architectures¶
Build the same module for different architectures to see the flag differences:
# Build for ARM64
lunch aosp_arm64-userdebug
m libcutils
# Build for x86_64
lunch aosp_x86_64-userdebug
m libcutils
# Compare the ninja commands to see different flags
cat out/build-aosp_arm64-userdebug.ninja | grep 'libcutils.*\.o' | head -1
cat out/build-aosp_x86_64-userdebug.ninja | grep 'libcutils.*\.o' | head -1
Exercise 6: Trace the Full Flag Chain¶
For a specific module, reconstruct the complete list of compiler flags by tracing through the Soong source:
- Start with
commonGlobalCflagsinglobal.go - Add
deviceGlobalCflagsfromglobal.go - Add the architecture-specific
Cflags()(e.g.,Arm64Cflags) - Add the
ToolchainCflags()(arch variant + CPU variant) - Add the module's own
cflagsfrom itsAndroid.bp - Append
noOverrideGlobalCflagsfromglobal.go
Write out the complete flag list and verify it matches what Ninja generates.
Exercise 7: Explore ART Architecture Support¶
Examine how ART handles architecture-specific code generation:
# 1. List all architecture-specific ART files
ls art/runtime/arch/arm64/
ls art/runtime/arch/riscv64/
# 2. Read the instruction set features for ARM64
cat art/runtime/arch/arm64/instruction_set_features_arm64.cc | head -100
# 3. Compare the entrypoint counts across architectures
wc -l art/runtime/arch/*/quick_entrypoints_*.S
Questions to answer:
- What CPU variants does ART recognize for ARM64?
- What errata does ART work around for Cortex-A53?
- Does ART have SVE support enabled for any ARM64 variant?
Exercise 8: Investigate RISC-V Readiness¶
Assess the current state of RISC-V support in AOSP:
# 1. Check which prebuilt modules are missing for RISC-V
grep -r "ALLOW_MISSING_DEPENDENCIES" device/generic/art/riscv64/
# 2. Count RISC-V-specific source files in bionic
find bionic/libc/arch-riscv64 -name '*.S' | wc -l
# 3. Check the ISA string
grep 'march=' build/soong/cc/config/riscv64_device.go
# 4. Look at the Berberis translation framework
ls frameworks/libs/binary_translation/
Questions to answer:
- What RISC-V extensions does AOSP require?
- Why is
-mno-implicit-floatused? - What is the page size for RISC-V Android?
- What translation direction does Berberis support?
Exercise 9: Create a Minimal Device Configuration¶
Write a complete BoardConfig.mk for a hypothetical RISC-V device:
# device/mycompany/myriscv/BoardConfig.mk
TARGET_NO_BOOTLOADER := true
TARGET_NO_KERNEL := true
TARGET_ARCH := riscv64
TARGET_CPU_ABI := riscv64
TARGET_CPU_VARIANT := generic
TARGET_ARCH_VARIANT :=
TARGET_SUPPORTS_64_BIT_APPS := true
# No secondary architecture for RISC-V
# (There is no 32-bit RISC-V Android port)
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 2147483648
BOARD_USERDATAIMAGE_PARTITION_SIZE := 576716800
BOARD_FLASH_BLOCK_SIZE := 512
Then create the product configuration:
# device/mycompany/myriscv/myriscv.mk
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit_only.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/generic_system.mk)
PRODUCT_NAME := myriscv
PRODUCT_DEVICE := myriscv
PRODUCT_BRAND := MyCompany
PRODUCT_MODEL := RISC-V Development Board
Questions to answer:
- Why does RISC-V use
core_64_bit_only.mkinstead ofcore_64_bit.mk? - What happens if you set
TARGET_ARCH_VARIANTto a non-empty value? - What Zygote script will be included?
Exercise 10: Analyze the Complete Flag Chain¶
Using the tools from this chapter, reconstruct the exact compiler invocation
for a specific file. Choose a simple module like liblog:
# 1. Find the ninja rule for a liblog source file
grep 'liblog.*logging.cpp.*\.o' out/combined-*.ninja | head -1
# 2. Extract and format the flags
# Look for the complete clang command line
# 3. Categorize each flag into its source:
# - global.go commonGlobalCflags
# - global.go deviceGlobalCflags
# - arm64_device.go Arm64Cflags
# - arm64_device.go toolchainCflags (arch variant + CPU variant)
# - Module's own cflags from Android.bp
# - global.go noOverrideGlobalCflags
This exercise demonstrates the practical result of the layered flag system described in Section 28.6.
Summary¶
Android's architecture support is a layered system built on a small set of design principles:
-
Toolchain abstraction: The
Toolchaininterface intoolchain.gohides architecture details behind a uniform API. Each of the five supported architectures (ARM, ARM64, x86, x86_64, RISC-V 64) provides a factory that produces aToolchainimplementation configured with the right compiler flags. -
Three-tier flag layering: Global flags (security, optimization, warnings) apply to all code. Architecture flags (
-march=,-mcpu=) tune for the target ISA. Non-overridable flags enforce critical safety warnings. -
Multilib for dual-architecture: The
compile_multilibproperty andTARGET_2ND_ARCHmechanism let a single device support both 32-bit and 64-bit code, essential for the ARM64 transition. -
Architecture-specific hot paths: Performance-critical code in bionic and ART is hand-written in assembly for each architecture, with up to three levels of optimization selection (architecture, CPU variant, runtime hardware detection via ifunc).
-
Pragmatic big.LITTLE tuning: Rather than optimizing for the fastest core, AOSP tunes for the efficiency core to avoid pathological slowdowns in heterogeneous configurations.
-
Native Bridge for cross-architecture compatibility: The
NativeBridgeCallbacksinterface enables binary translation (Berberis, Houdini) for running foreign-architecture native code.
The following table summarizes the characteristics of each supported architecture:
| Characteristic | ARM (32-bit) | ARM64 | x86 | x86_64 | RISC-V 64 |
|---|---|---|---|---|---|
| Bits | 32 | 64 | 32 | 64 | 64 |
| Clang Triple | armv7a-linux-androideabi |
aarch64-linux-android |
i686-linux-android |
x86_64-linux-android |
riscv64-linux-android |
| Arch Variants | 4 | 10 | 14 | 13 | 0 |
| CPU Variants | 18 | 10 | 0 | 0 | 0 |
| SIMD | NEON | NEON/SVE | SSE/AVX | SSE/AVX | RVV |
Default -march= |
armv7-a |
armv8-a |
prescott |
x86-64 |
rv64gcv_zba_zbb_zbs |
| Instruction Sets | ARM + Thumb | AArch64 | x86 | x86-64 | RV64 + C |
| Errata Workarounds | Cortex-A8 | Cortex-A53 | None | None | QEMU V |
| Float ABI | Soft (softfp) |
Hard | N/A | N/A | Hard |
| Yasm Support | No | No | Yes | Yes | No |
| Secondary Arch For | ARM64 | N/A | x86_64 | N/A | N/A |
| Toolchain Base | toolchain32Bit |
toolchain64Bit |
toolchain32Bit |
toolchain64Bit |
toolchain64Bit |
graph TB
subgraph "End-to-End Architecture Flow"
A["BoardConfig.mk<br/>TARGET_ARCH, TARGET_ARCH_VARIANT<br/>TARGET_CPU_VARIANT, TARGET_CPU_ABI"]
A --> B["Soong reads config<br/>Constructs Arch struct"]
B --> C["archTransitionMutator<br/>Creates per-arch module variants"]
C --> D["findToolchain()<br/>Selects toolchain factory"]
D --> E["Factory assembles flags<br/>Global + Device + Arch + Variant + CPU + Feature"]
E --> F["Clang invocation<br/>--target=TRIPLE + all flags"]
F --> G["Architecture-specific sources<br/>Selected via arch: block in Android.bp"]
G --> H["Linker invocation<br/>CRT objects + arch-specific ldflags"]
H --> I["Output binary<br/>In lib/ or lib64/ per multilib"]
end
The key source files for architecture support are:
| File | Purpose |
|---|---|
build/soong/cc/config/arm64_device.go |
ARM64 toolchain configuration |
build/soong/cc/config/arm_device.go |
ARM 32-bit toolchain configuration |
build/soong/cc/config/x86_64_device.go |
x86_64 toolchain configuration |
build/soong/cc/config/x86_device.go |
x86 toolchain configuration |
build/soong/cc/config/riscv64_device.go |
RISC-V 64 toolchain configuration |
build/soong/cc/config/global.go |
Global compiler flags |
build/soong/cc/config/toolchain.go |
Toolchain interface and helpers |
build/soong/cc/config/clang.go |
Clang-specific flag filtering |
build/soong/cc/config/bionic.go |
Bionic CRT objects and defaults |
build/soong/android/arch.go |
Multilib and architecture mutators |
bionic/libc/arch-arm64/ifuncs.cpp |
ARM64 runtime function dispatch |
art/runtime/arch/instruction_set_features.cc |
ART ISA feature detection |
device/generic/arm64/BoardConfig.mk |
ARM64 reference device config |
build/make/target/product/core_64_bit.mk |
64-bit product configuration |
frameworks/libs/binary_translation/ |
Berberis binary translation |