From 555e43fc28dae8b5e3f91bdaf4342d59b201fd87 Mon Sep 17 00:00:00 2001 From: Justin Hammond Date: Thu, 6 Aug 2026 23:46:34 +0800 Subject: [PATCH] testing/libc/arch_libc: Test every overridable string function. This test covered strcpy. A machine directory can override fourteen string and memory functions, and the mistakes such overrides actually make are specific: the word loop that works aligned and corrupts otherwise, the copy that runs past a guard byte, the overlap memmove must survive in both directions, the terminator that a length cap was supposed to beat, the sign of a comparison whose bytes wrap. So the new suite drives every function through every combination of source and destination offset within a register, twenty one sizes chosen to straddle every internal boundary, overlap at every distance up to a register width each way, terminators probed for by name, caps placed before, at and past differences, and guard bytes around every destination. A speed report follows, one line per scenario, measured through CLOCK_MONOTONIC. Two ways such a benchmark can quietly measure nothing are designed out, both learned by measuring nothing: the compiler will hoist a pure call whose arguments never change, so the pointers are laundered through an asm that claims to change them without disturbing the alignment under test; and an earlier scenario's writes can leave the compare buffers unequal, so equality is verified before the compare timings and the one scenario that needs shifted content prepares it explicitly after every clean reader has run. The suite is a separate source file behind its own option, on by default, and the program's exit status now says whether anything failed. The existing strcpy test is untouched, including its perf_gettime dependency, which configurations without hardware perf counters will want switched off. Assisted-by: Claude:claude-opus-5 Signed-off-by: Justin Hammond --- testing/libc/arch_libc/CMakeLists.txt | 3 +- testing/libc/arch_libc/Kconfig | 16 + testing/libc/arch_libc/Makefile | 4 + testing/libc/arch_libc/arch_libc_string.c | 805 +++++++++++++++++++ testing/libc/arch_libc/arch_libc_test_main.c | 16 +- 5 files changed, 842 insertions(+), 2 deletions(-) create mode 100644 testing/libc/arch_libc/arch_libc_string.c diff --git a/testing/libc/arch_libc/CMakeLists.txt b/testing/libc/arch_libc/CMakeLists.txt index 89e26e78485..411bd66964c 100644 --- a/testing/libc/arch_libc/CMakeLists.txt +++ b/testing/libc/arch_libc/CMakeLists.txt @@ -31,5 +31,6 @@ if(CONFIG_TESTING_ARCH_LIBC) MODULE ${CONFIG_TESTING_ARCH_LIBC} SRCS - arch_libc_test_main.c) + arch_libc_test_main.c + arch_libc_string.c) endif() diff --git a/testing/libc/arch_libc/Kconfig b/testing/libc/arch_libc/Kconfig index 65927909cbb..9987a65a4e4 100644 --- a/testing/libc/arch_libc/Kconfig +++ b/testing/libc/arch_libc/Kconfig @@ -16,6 +16,22 @@ config TESTING_ARCH_LIBC_STRCPY bool "test strcpy" default y +config TESTING_ARCH_LIBC_STRING + bool "string and memory function suite" + default y + ---help--- + Correctness across alignments, sizes, overlaps and terminator + placement for the string and memory functions a machine + directory may override, plus a speed report for each. + +config TESTING_ARCH_LIBC_STRING_BUFSIZE + int "string suite buffer size" + default 262144 + depends on TESTING_ARCH_LIBC_STRING + ---help--- + Each of the two test buffers, in bytes. The large-block + speed scenarios use a quarter of this. + config TESTING_ARCH_LIBC_PROGNAME string "Program name" default "arch_libctest" diff --git a/testing/libc/arch_libc/Makefile b/testing/libc/arch_libc/Makefile index 610e10fa9d6..6530e763f03 100644 --- a/testing/libc/arch_libc/Makefile +++ b/testing/libc/arch_libc/Makefile @@ -33,4 +33,8 @@ MODULE = $(CONFIG_TESTING_ARCH_LIBC) MAINSRC = arch_libc_test_main.c +ifeq ($(CONFIG_TESTING_ARCH_LIBC_STRING),y) +CSRCS += arch_libc_string.c +endif + include $(APPDIR)/Application.mk diff --git a/testing/libc/arch_libc/arch_libc_string.c b/testing/libc/arch_libc/arch_libc_string.c new file mode 100644 index 00000000000..f9f5bf1c19e --- /dev/null +++ b/testing/libc/arch_libc/arch_libc_string.c @@ -0,0 +1,805 @@ +/**************************************************************************** + * apps/testing/libc/arch_libc/arch_libc_string.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/* Correctness and speed for the string and memory functions a machine + * directory may override, aimed at the mistakes such overrides actually + * make: alignment cases (every combination of source and destination + * offset within a register), sizes straddling every internal boundary + * (word, block, and the small-size cutoff), overlap in both directions + * for memmove, the interaction of terminators with length caps, and + * writes past either end, which guard bytes around every destination + * catch. + * + * The speed half measures through CLOCK_MONOTONIC. Two things would + * otherwise make it measure nothing: the compiler hoisting a pure call + * whose arguments never change, countered by laundering the pointers + * through an asm that claims to change them, and comparison buffers an + * earlier benchmark scribbled on, countered by checking equality before + * timing. + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include + +#ifdef CONFIG_TESTING_ARCH_LIBC_STRING + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define STR_BUF (CONFIG_TESTING_ARCH_LIBC_STRING_BUFSIZE) +#define STR_GUARD 64 +#define STR_MINMS 250 +#define STR_BIG (STR_BUF / 4) +#define STR_STRLEN 4096 + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static FAR char *g_src; +static FAR char *g_dst; +static volatile unsigned long g_sink; +static int g_fails; + +static const int g_sizes[] = +{ + 0, 1, 2, 3, 7, 8, 9, 15, 16, 17, 31, 32, 63, 64, 65, 127, 128, 129, + 255, 256, 1000 +}; + +#define NSIZES ((int)(sizeof(g_sizes) / sizeof(g_sizes[0]))) + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +static void str_note(FAR const char *fn, int bad) +{ + printf(" %-10s correctness: %s (%d bad)\n", fn, + bad ? "FAIL" : "ok", bad); + g_fails += bad; +} + +static double str_now(void) +{ + struct timespec t; + + clock_gettime(CLOCK_MONOTONIC, &t); + return t.tv_sec + t.tv_nsec / 1e9; +} + +/**************************************************************************** + * Name: str_ck_copy + * + * Description: + * memcpy and memmove against distinct buffers: every alignment pair, + * every boundary size, result pointer, content, and the guard bytes. + * + ****************************************************************************/ + +static void str_ck_copy(int move) +{ + int k; + int so; + int dofs; + int i; + int bad = 0; + + for (k = 0; k < NSIZES; k++) + { + for (so = 0; so < 8; so++) + { + for (dofs = 0; dofs < 8; dofs++) + { + int n = g_sizes[k]; + FAR char *s = g_src + STR_GUARD + so; + FAR char *d = g_dst + STR_GUARD + dofs; + FAR void *r; + + for (i = 0; i < n; i++) + { + s[i] = (char)(i * 7 + so + k + 1); + } + + memset(g_dst, 0x5a, 2 * STR_GUARD + 1100); + r = move ? memmove(d, s, n) : memcpy(d, s, n); + + if (r != d) + { + bad++; + continue; + } + + for (i = 0; i < n; i++) + { + if (d[i] != (char)(i * 7 + so + k + 1)) + { + bad++; + break; + } + } + + if ((unsigned char)*(d - 1) != 0x5a || + (unsigned char)d[n] != 0x5a) + { + bad++; + } + } + } + } + + str_note(move ? "memmove" : "memcpy", bad); +} + +/**************************************************************************** + * Name: str_ck_overlap + * + * Description: + * memmove where the regions overlap, forward and backward, at every + * distance up to a register width either side, at every size. + * + ****************************************************************************/ + +static void str_ck_overlap(void) +{ + int k; + int ofs; + int i; + int bad = 0; + + for (k = 0; k < NSIZES; k++) + { + for (ofs = -8; ofs <= 8; ofs++) + { + int n = g_sizes[k]; + FAR char *base = g_dst + STR_GUARD + 16; + FAR char *d = base + ofs; + + if (n == 0) + { + continue; + } + + for (i = -8; i < n + 8; i++) + { + base[i] = (char)(i * 5 + k + 3); + } + + memmove(d, base, n); + + for (i = 0; i < n; i++) + { + if (d[i] != (char)(i * 5 + k + 3)) + { + bad++; + break; + } + } + } + } + + str_note("mv-overlap", bad); +} + +/**************************************************************************** + * Name: str_ck_memset + ****************************************************************************/ + +static void str_ck_memset(void) +{ + int k; + int dofs; + int i; + int bad = 0; + + for (k = 0; k < NSIZES; k++) + { + for (dofs = 0; dofs < 8; dofs++) + { + int n = g_sizes[k]; + FAR char *d = g_dst + STR_GUARD + dofs; + + memset(g_dst, 0x5a, 2 * STR_GUARD + 1100); + if (memset(d, 0xc3, n) != d) + { + bad++; + continue; + } + + for (i = 0; i < n; i++) + { + if ((unsigned char)d[i] != 0xc3) + { + bad++; + break; + } + } + + if ((unsigned char)*(d - 1) != 0x5a || + (unsigned char)d[n] != 0x5a) + { + bad++; + } + } + } + + str_note("memset", bad); +} + +/**************************************************************************** + * Name: str_ck_memcmp + * + * Description: + * Equal buffers, then a single difference planted at the start, the + * middle and the end: the sign both ways, and that a length cap short + * of the difference hides it. Values stay below 0x80 so the planted + * difference cannot wrap. + * + ****************************************************************************/ + +static void str_ck_memcmp(void) +{ + int k; + int so; + int dofs; + int at; + int bad = 0; + + for (k = 0; k < NSIZES; k++) + { + for (so = 0; so < 4; so++) + { + for (dofs = 0; dofs < 4; dofs++) + { + int n = g_sizes[k]; + FAR char *a = g_src + STR_GUARD + so; + FAR char *b = g_dst + STR_GUARD + dofs; + int i; + + for (i = 0; i < n; i++) + { + a[i] = b[i] = (char)((i * 3 + 40) & 0x7f); + } + + if (memcmp(a, b, n) != 0) + { + bad++; + continue; + } + + for (at = 0; at < n; at += (n > 8 ? n / 3 : 1)) + { + b[at] = (char)(a[at] + 13); + if (memcmp(a, b, n) >= 0) + { + bad++; + } + + if (memcmp(b, a, n) <= 0) + { + bad++; + } + + if (memcmp(a, b, at) != 0) + { + bad++; + } + + b[at] = a[at]; + } + } + } + } + + str_note("memcmp", bad); +} + +/**************************************************************************** + * Name: str_ck_scan + * + * Description: + * strlen, strnlen, memchr and the strchr family: every alignment, + * lengths crossing the word size, the probe at every position, a + * probe that is absent, and the terminator probed for by name. A + * tripwire byte sits just past every terminator. + * + ****************************************************************************/ + +static void str_ck_scan(void) +{ + int so; + int len; + int at; + int i; + int bad = 0; + + for (so = 0; so < 8; so++) + { + for (len = 0; len < 40; len++) + { + FAR char *s = g_src + STR_GUARD + so; + + for (i = 0; i < len; i++) + { + s[i] = 'a' + (i % 23); + } + + s[len] = '\0'; + s[len + 1] = 'Z'; + + if ((int)strlen(s) != len) + { + bad++; + } + + if ((int)strnlen(s, 1000) != len) + { + bad++; + } + + if ((int)strnlen(s, len) != len) + { + bad++; + } + + if (len > 0 && (int)strnlen(s, len - 1) != len - 1) + { + bad++; + } + + if ((int)strnlen(s, 0) != 0) + { + bad++; + } + + for (at = 0; at < len; at += (len > 6 ? len / 3 : 1)) + { + char c = s[at]; + FAR char *want = NULL; + + for (i = 0; i <= len; i++) + { + if (s[i] == c) + { + want = s + i; + break; + } + } + + if (strchr(s, c) != want) + { + bad++; + } + + if (strchrnul(s, c) != want) + { + bad++; + } + + want = NULL; + for (i = len; i >= 0; i--) + { + if (s[i] == c) + { + want = s + i; + break; + } + } + + if (strrchr(s, c) != want) + { + bad++; + } + } + + if (strchr(s, '~') != NULL) + { + bad++; + } + + if (strchrnul(s, '~') != s + len) + { + bad++; + } + + if (strrchr(s, '~') != NULL) + { + bad++; + } + + if (strchr(s, '\0') != s + len) + { + bad++; + } + + if (strrchr(s, '\0') != s + len) + { + bad++; + } + + if (len > 2) + { + if (memchr(s, s[len - 1], len) == NULL) + { + bad++; + } + + if (memchr(s, '~', len) != NULL) + { + bad++; + } + } + } + } + + str_note("str-scan", bad); +} + +/**************************************************************************** + * Name: str_ck_cmpcpy + * + * Description: + * strcmp, strncmp and strcpy: a planted difference at every position, + * the n cap before, at and past it, and the copy checked round trip + * with a tripwire past the terminator. + * + ****************************************************************************/ + +static void str_ck_cmpcpy(void) +{ + int ao; + int bo; + int at; + int i; + int bad = 0; + + for (ao = 0; ao < 8; ao++) + { + for (bo = 0; bo < 8; bo++) + { + for (at = 0; at < 24; at++) + { + FAR char *a = g_src + STR_GUARD + ao; + FAR char *b = g_dst + STR_GUARD + bo; + FAR char *d = g_dst + STR_GUARD + 40 + bo; + int exp; + + for (i = 0; i < 24; i++) + { + a[i] = b[i] = 'A' + (i % 20); + } + + a[23] = b[23] = '\0'; + if (at < 23) + { + b[at] = a[at] + 1; + } + + exp = (at < 23) ? -1 : 0; + i = strcmp(a, b); + i = (i < 0) ? -1 : ((i > 0) ? 1 : 0); + if (i != exp) + { + bad++; + } + + if (strncmp(a, b, at) != 0) + { + bad++; + } + + i = strncmp(a, b, 24); + i = (i < 0) ? -1 : ((i > 0) ? 1 : 0); + if (i != exp) + { + bad++; + } + + if (strncmp(a, b, 0) != 0) + { + bad++; + } + + d[24] = 0x33; + if (strcpy(d, a) != d) + { + bad++; + } + + if (strcmp(d, a) != 0) + { + bad++; + } + + if (d[24] != 0x33) + { + bad++; + } + } + } + } + + str_note("strcmp-fam", bad); +} + +/**************************************************************************** + * Name: str_ck_strlcpy + * + * Description: + * strlcpy: the return is always the source length, the result is + * always terminated when the cap is nonzero, at most cap-1 bytes are + * copied, and a cap of zero writes nothing at all. + * + ****************************************************************************/ + +static void str_ck_strlcpy(void) +{ + int so; + int dofs; + int len; + size_t cap; + int i; + int bad = 0; + + for (so = 0; so < 8; so++) + { + for (dofs = 0; dofs < 8; dofs++) + { + for (len = 0; len < 24; len++) + { + FAR char *a = g_src + STR_GUARD + so; + + for (i = 0; i < len; i++) + { + a[i] = 'a' + (i % 23); + } + + a[len] = '\0'; + + for (cap = 0; cap <= (size_t)len + 2; cap++) + { + FAR char *d = g_dst + STR_GUARD + dofs; + + memset(g_dst, 0x5a, 2 * STR_GUARD + 64); + if (strlcpy(d, a, cap) != (size_t)len) + { + bad++; + } + + if (cap > 0) + { + size_t want = ((size_t)len < cap - 1) ? + (size_t)len : cap - 1; + + if (d[want] != '\0') + { + bad++; + } + + for (i = 0; i < (int)want; i++) + { + if (d[i] != a[i]) + { + bad++; + break; + } + } + + if ((unsigned char)d[want + 1] != 0x5a && + want == cap - 1) + { + bad++; + } + } + else if ((unsigned char)d[0] != 0x5a) + { + bad++; + } + } + } + } + } + + str_note("strlcpy", bad); +} + +/**************************************************************************** + * Name: str_bench + * + * Description: + * Time one scenario and print a rate. The asm around the pointers + * stops the compiler treating a pure call with unchanged arguments as + * loop invariant, without disturbing the alignment under test. + * + ****************************************************************************/ + +static void str_bench(FAR const char *name, int op, int so, int dofs, int n) +{ + double t0; + double el; + long reps = 0; + int i; + + t0 = str_now(); + do + { + for (i = 0; i < 16; i++) + { + FAR char *s = g_src + STR_GUARD + so; + FAR char *d = g_dst + STR_GUARD + dofs; + + __asm__ volatile("" : "+r"(s), "+r"(d)); + + switch (op) + { + case 0: + memcpy(d, s, n); + g_sink += (unsigned char)d[0]; + break; + case 1: + memmove(d + 8, d, n); + g_sink += (unsigned char)d[8]; + break; + case 2: + g_sink += memcmp(s, d, n); + break; + case 3: + g_sink += strlen(s); + break; + case 4: + g_sink += strncmp(s, d, n); + break; + case 5: + g_sink += (uintptr_t)memchr(s, '~', n); + break; + case 6: + g_sink += (uintptr_t)strchr(s, '~'); + break; + case 7: + strcpy(d, s); + g_sink += (unsigned char)d[0]; + break; + case 8: + g_sink += (uintptr_t)strrchr(s, 'q'); + break; + case 9: + memset(d, i, n); + g_sink += (unsigned char)d[0]; + break; + case 10: + g_sink += strlcpy(d, s, n); + break; + } + } + + reps += 16; + el = str_now() - t0; + } + while (el * 1000.0 < STR_MINMS); + + printf(" %-26s %9.1f MB/s\n", name, + (double)reps * n / el / 1048576.0); +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: arch_libc_test_string + ****************************************************************************/ + +int arch_libc_test_string(void) +{ + int i; + + g_src = malloc(STR_BUF); + g_dst = malloc(STR_BUF); + if (g_src == NULL || g_dst == NULL) + { + printf("arch_libc string: cannot allocate 2 x %d\n", STR_BUF); + free(g_src); + free(g_dst); + return 1; + } + + printf("== string correctness ==\n"); + str_ck_copy(0); + str_ck_copy(1); + str_ck_overlap(); + str_ck_memset(); + str_ck_memcmp(); + str_ck_scan(); + str_ck_cmpcpy(); + str_ck_strlcpy(); + + /* Speed. Equal content in both buffers, strings terminated at the + * probe lengths, and no '~' anywhere, so the absent-probe scans walk + * the whole string. + */ + + for (i = 0; i < STR_BUF; i++) + { + g_src[i] = 'a' + (i % 23); + } + + memcpy(g_dst, g_src, STR_BUF); + g_src[STR_GUARD + STR_BIG] = '\0'; + g_dst[STR_GUARD + STR_BIG] = '\0'; + g_src[STR_GUARD + STR_STRLEN] = '\0'; + g_dst[STR_GUARD + STR_STRLEN] = '\0'; + + printf("== string speed ==\n"); + + /* Readers first: the writers below scribble on the destination + * buffer, and the compares depend on the two staying equal. + */ + + if (memcmp(g_src + STR_GUARD, g_dst + STR_GUARD, STR_BIG) != 0) + { + printf(" compare buffers differ, speed numbers void\n"); + g_fails++; + } + + str_bench("memcmp big aligned", 2, 0, 0, STR_BIG); + str_bench("memcmp big s+1/d+1", 2, 1, 1, STR_BIG); + str_bench("strncmp 4K aligned", 4, 0, 0, STR_STRLEN); + str_bench("strncmp 4K both+2", 4, 2, 2, STR_STRLEN); + str_bench("strlen 4K aligned", 3, 0, 0, STR_STRLEN); + str_bench("strlen 4K +3", 3, 3, 0, STR_STRLEN); + str_bench("memchr 4K aligned", 5, 0, 0, STR_STRLEN); + str_bench("strchr 4K absent", 6, 0, 0, STR_STRLEN); + str_bench("strchr 4K absent +5", 6, 5, 0, STR_STRLEN); + str_bench("strrchr 4K", 8, 0, 0, STR_STRLEN); + + /* Mismatched-offset compare wants equal content at shifted positions, + * and preparing that scribbles, so it comes after every clean reader. + */ + + memcpy(g_dst + STR_GUARD + 2, g_src + STR_GUARD + 1, STR_BIG + 8); + str_bench("memcmp big s+1/d+2", 2, 1, 2, STR_BIG); + + str_bench("memcpy big aligned", 0, 0, 0, STR_BIG); + str_bench("memcpy big src+1", 0, 1, 0, STR_BIG); + str_bench("memmove-bk big", 1, 0, 0, STR_BIG); + str_bench("memset big aligned", 9, 0, 0, STR_BIG); + str_bench("strcpy 4K aligned", 7, 0, 0, STR_STRLEN); + str_bench("strcpy 4K both+1", 7, 1, 1, STR_STRLEN); + str_bench("strlcpy 4K aligned", 10, 0, 0, STR_STRLEN); + + printf("== string fails: %d ==\n", g_fails); + free(g_src); + free(g_dst); + return g_fails != 0; +} + +#endif /* CONFIG_TESTING_ARCH_LIBC_STRING */ diff --git a/testing/libc/arch_libc/arch_libc_test_main.c b/testing/libc/arch_libc/arch_libc_test_main.c index b69c1dfc453..82cfb965d40 100644 --- a/testing/libc/arch_libc/arch_libc_test_main.c +++ b/testing/libc/arch_libc/arch_libc_test_main.c @@ -232,6 +232,14 @@ int arch_libc_strcpy_speed(void) #endif +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef CONFIG_TESTING_ARCH_LIBC_STRING +int arch_libc_test_string(void); +#endif + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -242,11 +250,17 @@ int arch_libc_strcpy_speed(void) int main(int argc, FAR char *argv[]) { + int ret = 0; + #ifdef CONFIG_TESTING_ARCH_LIBC_STRCPY arch_libc_test_strcpy(); arch_libc_strcpy_speed(); #endif - return 0; +#ifdef CONFIG_TESTING_ARCH_LIBC_STRING + ret = arch_libc_test_string(); +#endif + + return ret; }