[libc-commits] [libc] Enable bare-metal hermetic tests on Arm (PR #207361)

Volodymyr Turanskyy via libc-commits libc-commits at lists.llvm.org
Mon Jul 6 04:41:58 PDT 2026


https://github.com/voltur01 updated https://github.com/llvm/llvm-project/pull/207361

>From c0f77cecd05343050b4cf7b9ba4e2854a01a9f47 Mon Sep 17 00:00:00 2001
From: Volodymyr Turanskyy <volodymyr.turanskyy at arm.com>
Date: Fri, 3 Jul 2026 10:23:01 +0100
Subject: [PATCH 1/2] Enable bare-metal hermetic tests on Arm

This provides missing components required to run LLVM libc hermetic tests, for example, with QEMU:
- Redirect of output, time and exit to semihosting.
- Linker script.
- Extra LIBC_MATH_ASSUME_ROUND_NEAREST_ONLY setting for LIBC_CONF_MATH_OPTIMIZATIONS to make hardfp tests pass.
---
 libc/config/baremetal/arm/config.json        |   7 +
 libc/test/UnitTest/ArmBaremetalTestUtils.cpp | 113 +++++++++++++
 libc/test/UnitTest/CMakeLists.txt            |   7 +-
 libc/test/UnitTest/llvm-libc-baremetal.ld    | 157 +++++++++++++++++++
 4 files changed, 283 insertions(+), 1 deletion(-)
 create mode 100644 libc/config/baremetal/arm/config.json
 create mode 100644 libc/test/UnitTest/ArmBaremetalTestUtils.cpp
 create mode 100644 libc/test/UnitTest/llvm-libc-baremetal.ld

diff --git a/libc/config/baremetal/arm/config.json b/libc/config/baremetal/arm/config.json
new file mode 100644
index 0000000000000..c35f32d0eacdf
--- /dev/null
+++ b/libc/config/baremetal/arm/config.json
@@ -0,0 +1,7 @@
+{
+  "math": {
+    "LIBC_CONF_MATH_OPTIMIZATIONS": {
+      "value": "(LIBC_MATH_SKIP_ACCURATE_PASS | LIBC_MATH_SMALL_TABLES | LIBC_MATH_NO_ERRNO | LIBC_MATH_NO_EXCEPT | LIBC_MATH_INTERMEDIATE_COMP_IN_FLOAT | LIBC_MATH_ASSUME_ROUND_NEAREST_ONLY)"
+    }
+  }
+}
diff --git a/libc/test/UnitTest/ArmBaremetalTestUtils.cpp b/libc/test/UnitTest/ArmBaremetalTestUtils.cpp
new file mode 100644
index 0000000000000..251ed5127b492
--- /dev/null
+++ b/libc/test/UnitTest/ArmBaremetalTestUtils.cpp
@@ -0,0 +1,113 @@
+//===-- Arm bare-metal hermetic test utilities ----------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "hdr/stdint_proxy.h"
+
+#include <llvm-libc-types/ssize_t.h>
+#include <stddef.h>
+#include <time.h>
+
+// Redirect stdout/stderr, time and exit from libc tests to semihosting
+
+namespace {
+// Semihosting constants and semihosting call wrapper.
+// https://github.com/ARM-software/abi-aa/blob/main/semihosting/semihosting.rst
+constexpr uint32_t SYS_OPEN = 0x01;
+constexpr uint32_t SYS_WRITE = 0x05;
+constexpr uint32_t SYS_CLOCK = 0x10;
+constexpr uint32_t SYS_TIME = 0x11;
+constexpr uint32_t SYS_EXIT = 0x18;
+
+constexpr uint32_t OPENMODE_W = 4;
+
+constexpr uint32_t ADP_Stopped_ApplicationExit = 0x20026;
+constexpr uint32_t ADP_Stopped_RunTimeErrorUnknown = 0x20023;
+
+#if defined(__thumb__) // T32
+#if defined(__ARM_ARCH_PROFILE) && __ARM_ARCH_PROFILE == 'M'
+#define SEMIHOST_INSTRUCTION "bkpt #0xAB"
+#else
+#define SEMIHOST_INSTRUCTION "svc 0xab"
+#endif
+#else // A32
+#define SEMIHOST_INSTRUCTION "svc 0x123456"
+#endif
+
+long semihosting_call(long val, const void *ptr) {
+  register long v __asm__("r0") = val;
+  register const void *p __asm__("r1") = ptr;
+  __asm__ __volatile__(SEMIHOST_INSTRUCTION
+                       : "+r"(v), "+r"(p)
+                       :
+                       : "memory", "cc");
+  return v;
+}
+} // namespace
+
+extern "C" {
+struct __llvm_libc_stdio_cookie {
+  int handle;
+};
+
+struct __llvm_libc_stdio_cookie __llvm_libc_stdout_cookie;
+struct __llvm_libc_stdio_cookie __llvm_libc_stderr_cookie;
+
+static void stdio_open(struct __llvm_libc_stdio_cookie *cookie, size_t mode) {
+  const char std_stream_name[] = ":tt";
+  size_t args[3];
+  args[0] = reinterpret_cast<size_t>(std_stream_name);
+  args[1] = mode;
+  args[2] = static_cast<size_t>(sizeof(std_stream_name) - 1);
+  cookie->handle = semihosting_call(SYS_OPEN, args);
+}
+
+void _platform_init(void) {
+  stdio_open(&__llvm_libc_stdout_cookie, OPENMODE_W);
+  stdio_open(&__llvm_libc_stderr_cookie, OPENMODE_W);
+}
+
+void __llvm_libc_exit(int status) {
+  uint32_t semihosting_status = (status == 0) ? ADP_Stopped_ApplicationExit
+                                              : ADP_Stopped_RunTimeErrorUnknown;
+  semihosting_call(SYS_EXIT,
+                   reinterpret_cast<const void *>(semihosting_status));
+  __builtin_unreachable(); // This semihosting call does not return.
+}
+
+ssize_t __llvm_libc_stdio_write(struct __llvm_libc_stdio_cookie *cookie,
+                                const char *buf, size_t size) {
+  size_t args[4];
+  args[0] = static_cast<size_t>(cookie->handle);
+  args[1] = reinterpret_cast<size_t>(buf);
+  args[2] = size;
+  ssize_t retval = semihosting_call(SYS_WRITE, args);
+  if (retval >= 0)
+    retval = size - retval;
+  return retval;
+}
+
+bool __llvm_libc_timespec_get_active(struct timespec *ts) {
+  long retval = semihosting_call(SYS_CLOCK, 0);
+  if (retval == -1)
+    return false;
+
+  // Semihosting uses centiseconds.
+  ts->tv_sec = (retval / 100);
+  ts->tv_nsec = (retval % 100) * (1'000'000'000 / 100);
+  return true;
+}
+
+bool __llvm_libc_timespec_get_utc(struct timespec *ts) {
+  long retval = semihosting_call(SYS_TIME, 0);
+
+  // Semihosting uses seconds.
+  ts->tv_sec = retval;
+  ts->tv_nsec = 0;
+  return true;
+}
+} // extern "C"
diff --git a/libc/test/UnitTest/CMakeLists.txt b/libc/test/UnitTest/CMakeLists.txt
index aa245b2234793..9187ac41c8005 100644
--- a/libc/test/UnitTest/CMakeLists.txt
+++ b/libc/test/UnitTest/CMakeLists.txt
@@ -111,10 +111,15 @@ add_unittest_framework_library(
     libc.hdr.stdint_proxy
 )
 
+set(libc_hermetic_test_support_srcs HermeticTestUtils.cpp)
+if(LIBC_TARGET_OS_IS_BAREMETAL AND LIBC_TARGET_ARCHITECTURE_IS_ARM)
+  list(APPEND libc_hermetic_test_support_srcs ArmBaremetalTestUtils.cpp)
+endif()
+
 add_unittest_framework_library(
   LibcHermeticTestSupport
   SRCS
-    HermeticTestUtils.cpp
+    ${libc_hermetic_test_support_srcs}
   DEPENDS
     libc.hdr.stdint_proxy
 )
diff --git a/libc/test/UnitTest/llvm-libc-baremetal.ld b/libc/test/UnitTest/llvm-libc-baremetal.ld
new file mode 100644
index 0000000000000..354d6a683cfb6
--- /dev/null
+++ b/libc/test/UnitTest/llvm-libc-baremetal.ld
@@ -0,0 +1,157 @@
+/*===-- Bare-metal LLVM libc test linker script ---------------------------===*\
+ *
+ * Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+ * See https://llvm.org/LICENSE.txt for license information.
+ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+ *
+\*===----------------------------------------------------------------------===*/
+
+ASSERT(DEFINED(__boot_flash), "__boot_flash must be defined");
+ASSERT(DEFINED(__boot_flash_size), "__boot_flash_size must be defined");
+ASSERT(DEFINED(__flash), "__flash must be defined");
+ASSERT(DEFINED(__flash_size), "__flash_size must be defined");
+ASSERT(DEFINED(__ram), "__ram must be defined");
+ASSERT(DEFINED(__ram_size), "__ram_size must be defined");
+
+MEMORY {
+  boot_flash (rx) : ORIGIN = __boot_flash, LENGTH = __boot_flash_size
+  flash      (rx) : ORIGIN = __flash,      LENGTH = __flash_size
+  ram       (rwx) : ORIGIN = __ram,        LENGTH = __ram_size
+}
+
+SECTIONS {
+  PROVIDE(__stack = ORIGIN(ram) + LENGTH(ram));
+
+  .boot_flash : {
+    KEEP(*(.text.init.enter))
+    KEEP(*(.vectors))
+    KEEP(*(.vectors.*))
+    KEEP(*(.data.init.enter))
+    KEEP(*(SORT_BY_NAME(.init) SORT_BY_NAME(.init.*)))
+  } > boot_flash
+
+  .text : ALIGN(8) {
+    KEEP(*(.text.startup .text.startup.*))
+    KEEP(*(.init))
+    *(.text .text.* .gnu.linkonce.t.*)
+    KEEP(*(.fini))
+
+    . = ALIGN(8);
+    PROVIDE_HIDDEN(__preinit_array_start = .);
+    KEEP(*(.preinit_array))
+    KEEP(*(.preinit_array.*))
+    PROVIDE_HIDDEN(__preinit_array_end = .);
+
+    PROVIDE_HIDDEN(__init_array_start = .);
+    KEEP(*(SORT_BY_INIT_PRIORITY(.init_array.*)))
+    KEEP(*(.init_array))
+    KEEP(*(SORT_BY_INIT_PRIORITY(.ctors.*)))
+    KEEP(*(.ctors))
+    PROVIDE_HIDDEN(__init_array_end = .);
+
+    PROVIDE_HIDDEN(__fini_array_start = .);
+    KEEP(*(SORT_BY_INIT_PRIORITY(.fini_array.*)))
+    KEEP(*(.fini_array))
+    KEEP(*(SORT_BY_INIT_PRIORITY(.dtors.*)))
+    KEEP(*(.dtors))
+    PROVIDE_HIDDEN(__fini_array_end = .);
+  } > flash
+
+  .rodata : ALIGN(8) {
+    *(.rodata .rodata.* .gnu.linkonce.r.*)
+    *(.srodata .srodata.*)
+    *(.rdata)
+  } > flash
+
+  .eh_frame_hdr : ALIGN(4) {
+    *(.eh_frame_hdr)
+    *(.eh_frame_entry .eh_frame_entry.*)
+  } > flash
+
+  .eh_frame : ALIGN(4) {
+    KEEP(*(.eh_frame))
+    KEEP(*(.eh_frame.*))
+  } > flash
+
+  .ARM.extab : ALIGN(4) {
+    *(.ARM.extab* .gnu.linkonce.armextab.*)
+  } > flash
+
+  .ARM.exidx : ALIGN(4) {
+    PROVIDE(__exidx_start = .);
+    *(.ARM.exidx* .gnu.linkonce.armexidx.*)
+    PROVIDE(__exidx_end = .);
+  } > flash
+
+  .data : ALIGN(8) {
+    PROVIDE(__data_start = .);
+    *(.data .data.* .gnu.linkonce.d.*)
+    *(.sdata .sdata.* .gnu.linkonce.s.*)
+    . = ALIGN(8);
+  } > ram AT> flash
+  PROVIDE(__data_source = LOADADDR(.data));
+
+  .tdata : ALIGN(8) {
+    PROVIDE(__tdata_start = .);
+    *(.tdata .tdata.* .gnu.linkonce.td.*)
+    . = ALIGN(8);
+    PROVIDE(__tdata_end = .);
+  } > ram AT> flash
+  PROVIDE(__tdata_source = LOADADDR(.tdata));
+  PROVIDE(__data_end = __tdata_end);
+  PROVIDE(__tdata_source_end = LOADADDR(.tdata) + SIZEOF(.tdata));
+  PROVIDE(__data_source_end =
+              SIZEOF(.tdata) ? __tdata_source_end : LOADADDR(.data) + SIZEOF(.data));
+  PROVIDE(__edata = __data_end);
+  PROVIDE(_edata = __data_end);
+  PROVIDE(edata = __data_end);
+  PROVIDE(__data_size = __data_end - __data_start);
+  PROVIDE(__tdata_size = __tdata_end - __tdata_start);
+  PROVIDE(__data_source_size = __data_source_end - __data_source);
+
+  .tbss (NOLOAD) : ALIGN(8) {
+    PROVIDE(__tbss_start = .);
+    *(.tbss .tbss.* .gnu.linkonce.tb.*)
+    *(.tcommon)
+    . = ALIGN(8);
+    PROVIDE(__tbss_end = .);
+  } > ram
+
+  .bss (NOLOAD) : ALIGN(8) {
+    *(.sbss .sbss.* .gnu.linkonce.sb.*)
+    *(.bss .bss.* .gnu.linkonce.b.*)
+    *(COMMON)
+    . = ALIGN(8);
+    PROVIDE(__bss_end = .);
+  } > ram
+  PROVIDE(__bss_start = SIZEOF(.tbss) ? ADDR(.tbss) : ADDR(.bss));
+  PROVIDE(__bss_size = __bss_end - __bss_start);
+
+  .noinit (NOLOAD) : ALIGN(8) {
+    *(.noinit .noinit.*)
+  } > ram
+
+  PROVIDE(__end = .);
+  PROVIDE(_end = .);
+  PROVIDE(end = .);
+
+  PROVIDE(__heap_start = .);
+  PROVIDE(__heap_end = __stack - (DEFINED(__stack_size) ? __stack_size : 0));
+  PROVIDE(__llvm_libc_heap_limit = __heap_end);
+
+  .heap (NOLOAD) : {
+    . += (DEFINED(__heap_size_min) ? __heap_size_min : 0);
+  } > ram
+
+  .stack (NOLOAD) : {
+    . += (DEFINED(__stack_size) ? __stack_size : 0);
+  } > ram
+
+  ASSERT(__data_size == __data_source_size,
+         "ERROR: .data/.tdata RAM size does not match flash load size")
+
+  /DISCARD/ : {
+    *(.note .note.*)
+    *(.comment)
+  }
+}

>From 808eaed4cc67b3916cf81733f677db141942a86c Mon Sep 17 00:00:00 2001
From: Volodymyr Turanskyy <volodymyr.turanskyy at arm.com>
Date: Mon, 6 Jul 2026 12:41:40 +0100
Subject: [PATCH 2/2] Address review comments

- Update the file headers
- Use internal includes
---
 libc/test/UnitTest/ArmBaremetalTestUtils.cpp | 16 ++++++++++------
 libc/test/UnitTest/llvm-libc-baremetal.ld    |  9 +++++++--
 2 files changed, 17 insertions(+), 8 deletions(-)

diff --git a/libc/test/UnitTest/ArmBaremetalTestUtils.cpp b/libc/test/UnitTest/ArmBaremetalTestUtils.cpp
index 251ed5127b492..9d18137723b49 100644
--- a/libc/test/UnitTest/ArmBaremetalTestUtils.cpp
+++ b/libc/test/UnitTest/ArmBaremetalTestUtils.cpp
@@ -1,16 +1,20 @@
-//===-- Arm bare-metal hermetic test utilities ----------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 //
 //===----------------------------------------------------------------------===//
+///
+/// \file
+/// Arm bare-metal hermetic test utilities.
+///
+//===----------------------------------------------------------------------===//
 
 #include "hdr/stdint_proxy.h"
-
-#include <llvm-libc-types/ssize_t.h>
-#include <stddef.h>
-#include <time.h>
+#include "hdr/types/size_t.h"
+#include "hdr/types/ssize_t.h"
+#include "hdr/types/struct_timespec.h"
 
 // Redirect stdout/stderr, time and exit from libc tests to semihosting
 
@@ -62,7 +66,7 @@ static void stdio_open(struct __llvm_libc_stdio_cookie *cookie, size_t mode) {
   size_t args[3];
   args[0] = reinterpret_cast<size_t>(std_stream_name);
   args[1] = mode;
-  args[2] = static_cast<size_t>(sizeof(std_stream_name) - 1);
+  args[2] = sizeof(std_stream_name) - 1UL;
   cookie->handle = semihosting_call(SYS_OPEN, args);
 }
 
diff --git a/libc/test/UnitTest/llvm-libc-baremetal.ld b/libc/test/UnitTest/llvm-libc-baremetal.ld
index 354d6a683cfb6..b68edb492b03a 100644
--- a/libc/test/UnitTest/llvm-libc-baremetal.ld
+++ b/libc/test/UnitTest/llvm-libc-baremetal.ld
@@ -1,10 +1,15 @@
-/*===-- Bare-metal LLVM libc test linker script ---------------------------===*\
+/*===----------------------------------------------------------------------===*
  *
  * Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
  * See https://llvm.org/LICENSE.txt for license information.
  * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  *
-\*===----------------------------------------------------------------------===*/
+ *===----------------------------------------------------------------------===*
+ *
+ * \file
+ * Bare-metal LLVM libc test linker script.
+ *
+ *===----------------------------------------------------------------------===*/
 
 ASSERT(DEFINED(__boot_flash), "__boot_flash must be defined");
 ASSERT(DEFINED(__boot_flash_size), "__boot_flash_size must be defined");



More information about the libc-commits mailing list