diff --git a/CODEOWNERS b/CODEOWNERS index e0591e2c112..99a865682eb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -464,6 +464,7 @@ /lib/gui/ @vanwinkeljan /lib/open-amp/ @arnopo /lib/os/ @dcpleung @nashif @andyross +/lib/os/cbprintf_packaged.c @npitre /lib/posix/ @pfalcon /lib/cmsis_rtos_v2/ @nashif /lib/cmsis_rtos_v1/ @nashif diff --git a/include/sys/cbprintf.h b/include/sys/cbprintf.h index 39aa37c65d5..83a33f3f30b 100644 --- a/include/sys/cbprintf.h +++ b/include/sys/cbprintf.h @@ -9,6 +9,7 @@ #include #include +#include #include #ifdef CONFIG_CBPRINTF_LIBC_SUBSTS @@ -44,6 +45,102 @@ extern "C" { */ typedef int (*cbprintf_cb)(/* int c, void *ctx */); +/** @brief Capture state required to output formatted data later. + * + * Like cbprintf() but instead of processing the arguments and emitting the + * formatted results immediately all arguments are captured so this can be + * done in a different context, e.g. when the output function can block. + * + * In addition to the values extracted from arguments this will ensure that + * copies are made of the necessary portions of any string parameters that are + * not confirmed to be stored in read-only memory (hence assumed to be safe to + * refer to directly later). + * + * @param packaged pointer to where the packaged data can be stored. Pass a + * null pointer to store nothing but still calculate the total space required. + * The data stored here is relocatable, that is it can be moved to another + * contiguous block of memory. The pointer must be aligned to a multiple of + * the largest element in the argument list. + * + * @param len this must be set to the number of bytes available at @p packaged. + * Ignored if @p packaged is NULL. + * + * @param format a standard ISO C format string with characters and conversion + * specifications. + * + * @param ... arguments corresponding to the conversion specifications found + * within @p format. + * + * @retval nonegative the number of bytes successfully stored at @p packaged. + * This will not exceed @p len. + * @retval -EINVAL if @p format is not acceptable + * @retval -ENOSPC if @p packaged was not null and the space required to store + * exceed @p len. + */ +__printf_like(3, 4) +int cbprintf_package(void *packaged, + size_t len, + const char *format, + ...); + +/** @brief Capture state required to output formatted data later. + * + * Like cbprintf() but instead of processing the arguments and emitting the + * formatted results immediately all arguments are captured so this can be + * done in a different context, e.g. when the output function can block. + * + * In addition to the values extracted from arguments this will ensure that + * copies are made of the necessary portions of any string parameters that are + * not confirmed to be stored in read-only memory (hence assumed to be safe to + * refer to directly later). + * + * @param packaged pointer to where the packaged data can be stored. Pass a + * null pointer to store nothing but still calculate the total space required. + * The data stored here is relocatable, that is it can be moved to another + * contiguous block of memory. The pointer must be aligned to a multiple of + * the largest element in the argument list. + * + * @param len this must be set to the number of bytes available at @p packaged. + * Ignored if @p packaged is NULL. + * + * @param format a standard ISO C format string with characters and conversion + * specifications. + * + * @param ap captured stack arguments corresponding to the conversion + * specifications found within @p format. + * + * @retval nonegative the number of bytes successfully stored at @p packaged. + * This will not exceed @p len. + * @retval -EINVAL if @p format is not acceptable + * @retval -ENOSPC if @p packaged was not null and the space required to store + * exceed @p len. + */ +int cbvprintf_package(void *packaged, + size_t len, + const char *format, + va_list ap); + +/** @brief Generate the output for a previously captured format + * operation. + * + * @param out the function used to emit each generated character. + * + * @param ctx context provided when invoking out + * + * @param packaged the data required to generate the formatted output, as + * captured by cbprintf_package() or cbvprintf_package(). The alignment + * requirement on this data is the same as when it was initially created. + * + * @note Memory indicated by @p packaged will be modified in a non-destructive + * way, meaning that it could still be reused with this function again. + * + * @return the number of characters printed, or a negative error value + * returned from invoking @p out. + */ +int cbpprintf(cbprintf_cb out, + void *ctx, + void *packaged); + /** @brief *printf-like output through a callback. * * This is essentially printf() except the output is generated diff --git a/lib/os/CMakeLists.txt b/lib/os/CMakeLists.txt index 61b026d8fe0..155e26c5881 100644 --- a/lib/os/CMakeLists.txt +++ b/lib/os/CMakeLists.txt @@ -4,6 +4,7 @@ zephyr_sources_ifdef(CONFIG_BASE64 base64.c) zephyr_sources( cbprintf.c + cbprintf_packaged.c crc32c_sw.c crc32_sw.c crc16_sw.c diff --git a/lib/os/cbprintf_packaged.c b/lib/os/cbprintf_packaged.c new file mode 100644 index 00000000000..e6975d0ddbc --- /dev/null +++ b/lib/os/cbprintf_packaged.c @@ -0,0 +1,574 @@ +/* + * Copyright (c) 2021 BayLibre, SAS + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include + + +/** + * @brief Check if address is in read only section. + * + * @param addr Address. + * + * @return True if address identified within read only section. + */ +static inline bool ptr_in_rodata(const char *addr) +{ +#if defined(CBPRINTF_VIA_UNIT_TEST) + /* Unit test is X86 (or other host) but not using Zephyr + * linker scripts. + */ +#define RO_START 0 +#define RO_END 0 +#elif defined(CONFIG_ARC) || defined(CONFIG_ARM) || defined(CONFIG_X86) \ + || defined(CONFIG_RISCV) + extern char _image_rodata_start[]; + extern char _image_rodata_end[]; +#define RO_START _image_rodata_start +#define RO_END _image_rodata_end +#elif defined(CONFIG_NIOS2) || defined(CONFIG_RISCV) + extern char _image_rom_start[]; + extern char _image_rom_end[]; +#define RO_START _image_rom_start +#define RO_END _image_rom_end +#elif defined(CONFIG_XTENSA) + extern char _rodata_start[]; + extern char _rodata_end[]; +#define RO_START _rodata_start +#define RO_END _rodata_end +#else +#define RO_START 0 +#define RO_END 0 +#endif + + return ((addr >= (const char *)RO_START) && + (addr < (const char *)RO_END)); +} + +/* + * va_list creation + */ + +#if defined(__aarch64__) +/* + * Reference: + * + * Procedure Call Standard for the ARM 64-bit Architecture + */ + +struct __va_list { + void *__stack; + void *__gr_top; + void *__vr_top; + int __gr_offs; + int __vr_offs; +}; + +BUILD_ASSERT(sizeof(va_list) == sizeof(struct __va_list), + "architecture specific support is wrong"); + +static int cbprintf_via_va_list(cbprintf_cb out, void *ctx, + const char *fmt, void *buf) +{ + union { + va_list ap; + struct __va_list __ap; + } u; + + /* create a valid va_list with our buffer */ + u.__ap.__stack = buf; + u.__ap.__gr_top = NULL; + u.__ap.__vr_top = NULL; + u.__ap.__gr_offs = 0; + u.__ap.__vr_offs = 0; + + return cbvprintf(out, ctx, fmt, u.ap); +} + +#define VA_STACK_MIN_ALIGN 8 + +#elif defined(__x86_64__) +/* + * Reference: + * + * System V Application Binary Interface + * AMD64 Architecture Processor Supplement + */ + +struct __va_list { + unsigned int gp_offset; + unsigned int fp_offset; + void *overflow_arg_area; + void *reg_save_area; +}; + +BUILD_ASSERT(sizeof(va_list) == sizeof(struct __va_list), + "architecture specific support is wrong"); + +static int cbprintf_via_va_list(cbprintf_cb out, void *ctx, + const char *fmt, void *buf) +{ + union { + va_list ap; + struct __va_list __ap; + } u; + + /* create a valid va_list with our buffer */ + u.__ap.overflow_arg_area = buf; + u.__ap.reg_save_area = NULL; + u.__ap.gp_offset = (6 * 8); + u.__ap.fp_offset = (6 * 8 + 16 * 16); + + return cbvprintf(out, ctx, fmt, u.ap); +} + +#define VA_STACK_MIN_ALIGN 8 + +#elif defined(__xtensa__) +/* + * Reference: + * + * gcc source code (gcc/config/xtensa/xtensa.c) + * xtensa_build_builtin_va_list(), xtensa_va_start(), + * xtensa_gimplify_va_arg_expr() + */ + +struct __va_list { + void *__va_stk; + void *__va_reg; + int __va_ndx; +}; + +BUILD_ASSERT(sizeof(va_list) == sizeof(struct __va_list), + "architecture specific support is wrong"); + +static int cbprintf_via_va_list(cbprintf_cb out, void *ctx, + const char *fmt, void *buf) +{ + union { + va_list ap; + struct __va_list __ap; + } u; + + /* create a valid va_list with our buffer */ + u.__ap.__va_stk = (char *)buf - 32; + u.__ap.__va_reg = NULL; + u.__ap.__va_ndx = (6 + 2) * 4; + + return cbvprintf(out, ctx, fmt, u.ap); +} + +#else +/* + * Default implementation shared by many architectures like + * 32-bit ARM and Intel. + * + * We assume va_list is a simple pointer. + */ + +BUILD_ASSERT(sizeof(va_list) == sizeof(void *), + "architecture specific support is needed"); + +static int cbprintf_via_va_list(cbprintf_cb out, void *ctx, + const char *fmt, void *buf) +{ + union { + va_list ap; + void *ptr; + } u; + + u.ptr = buf; + + return cbvprintf(out, ctx, fmt, u.ap); +} + +#endif + +/* + * Special alignment cases + */ + +#if defined(__i386__) +/* there are no gaps on the stack */ +#define VA_STACK_ALIGN(type) 1 +#endif + +#if defined(__riscv) +#define VA_STACK_MIN_ALIGN (__riscv_xlen / 8) +#endif + +#if defined(__sparc__) +/* no gaps on the stack even though the CPU can't do unaligned accesses */ +#define VA_STACK_ALIGN(type) 1 +#define VA_STACK_LL_DBL_MEMCPY true +#endif + +/* + * Default alignment values if not specified by architecture config + */ + +#ifndef VA_STACK_MIN_ALIGN +#define VA_STACK_MIN_ALIGN 1 +#endif + +#ifndef VA_STACK_ALIGN +#define VA_STACK_ALIGN(type) MAX(VA_STACK_MIN_ALIGN, __alignof__(type)) +#endif + +#ifndef VA_STACK_LL_DBL_MEMCPY +#define VA_STACK_LL_DBL_MEMCPY false +#endif + +int cbvprintf_package(void *packaged, size_t len, + const char *fmt, va_list ap) +{ + char *buf = packaged, *buf0 = buf; + unsigned int align, size, i, s_idx = 0; + uint8_t str_ptr_pos[16]; + const char *s; + bool parsing = false; + + /* + * Make room to store the arg list size and the number of + * appended strings. They both occupy 1 byte each. + * + * Given the next value to store is the format string pointer + * which is guaranteed to be at least 4 bytes, we just reserve + * a pointer size for the above to preserve alignment. + */ + buf += sizeof(char *); + + /* + * When buf0 is NULL we don't store anything. + * Instead we count the needed space to store the data. + */ + if (!buf0) { + len = 0; + } + + /* + * Otherwise we must ensure we can store at least + * thepointer to the format string itself. + */ + if (buf0 && buf - buf0 + sizeof(char *) > len) { + return -ENOSPC; + } + + /* + * Then process the format string itself. + * Here we branch directly into the code processing strings + * which is in the middle of the following while() loop. That's the + * reason for the post-decrement on fmt as it will be incremented + * prior to the next (actually first) round of that loop. + */ + s = fmt--; + align = VA_STACK_ALIGN(char *); + size = sizeof(char *); + goto process_string; + + /* Scan the format string */ + while (*++fmt) { + if (!parsing) { + if (*fmt == '%') { + parsing = true; + align = VA_STACK_ALIGN(int); + size = sizeof(int); + } + continue; + } + switch (*fmt) { + case '%': + parsing = false; + continue; + + case '#': + case '-': + case '+': + case ' ': + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '.': + case 'h': + case 'l': + case 'L': + continue; + + case '*': + break; + + case 'j': + align = VA_STACK_ALIGN(intmax_t); + size = sizeof(intmax_t); + continue; + + case 'z': + align = VA_STACK_ALIGN(size_t); + size = sizeof(size_t); + continue; + + case 't': + align = VA_STACK_ALIGN(ptrdiff_t); + size = sizeof(ptrdiff_t); + continue; + + case 'c': + case 'd': + case 'i': + case 'o': + case 'u': + case 'x': + case 'X': + if (fmt[-1] == 'l') { + if (fmt[-2] == 'l') { + align = VA_STACK_ALIGN(long long); + size = sizeof(long long); + } else { + align = VA_STACK_ALIGN(long); + size = sizeof(long); + } + } + parsing = false; + break; + + case 's': + case 'p': + case 'n': + align = VA_STACK_ALIGN(void *); + size = sizeof(void *); + parsing = false; + break; + + case 'a': + case 'A': + case 'e': + case 'E': + case 'f': + case 'F': + case 'g': + case 'G': { + /* + * Handle floats separately as they may be + * held in a different register set. + */ + union { double d; long double ld; } v; + + if (fmt[-1] == 'L') { + v.ld = va_arg(ap, long double); + align = VA_STACK_ALIGN(long double); + size = sizeof(long double); + } else { + v.d = va_arg(ap, double); + align = VA_STACK_ALIGN(double); + size = sizeof(double); + } + /* align destination buffer location */ + buf = (void *) ROUND_UP(buf, align); + if (buf0) { + /* make sure it fits */ + if (buf - buf0 + size > len) { + return -ENOSPC; + } + if (VA_STACK_LL_DBL_MEMCPY) { + memcpy(buf, &v, size); + } else if (fmt[-1] == 'L') { + *(long double *)buf = v.ld; + } else { + *(double *)buf = v.d; + } + } + buf += size; + parsing = false; + continue; + } + + default: + parsing = false; + continue; + } + + /* align destination buffer location */ + buf = (void *) ROUND_UP(buf, align); + + /* make sure the data fits */ + if (buf0 && buf - buf0 + size > len) { + return -ENOSPC; + } + + /* copy va_list data over to our buffer */ + if (*fmt == 's') { + s = va_arg(ap, char *); +process_string: + if (buf0) { + *(const char **)buf = s; + } + if (ptr_in_rodata(s)) { + /* do nothing special */ + } else if (buf0) { + /* + * Remember string pointer location. + * We will append it later. + */ + if (s_idx >= ARRAY_SIZE(str_ptr_pos)) { + __ASSERT(false, "str_ptr_pos[] too small"); + return -EINVAL; + } + /* Use same multiple as the arg list size. */ + str_ptr_pos[s_idx++] = (buf - buf0) / sizeof(int); + } else { + /* + * Add the string length, the final '\0' + * and size of the pointer position prefix. + */ + len += strlen(s) + 1 + 1; + } + buf += sizeof(char *); + } else if (size == sizeof(int)) { + int v = va_arg(ap, int); + + if (buf0) { + *(int *)buf = v; + } + buf += sizeof(int); + } else if (size == sizeof(long)) { + long v = va_arg(ap, long); + + if (buf0) { + *(long *)buf = v; + } + buf += sizeof(long); + } else if (size == sizeof(long long)) { + long long v = va_arg(ap, long long); + + if (buf0) { + if (VA_STACK_LL_DBL_MEMCPY) { + memcpy(buf, &v, sizeof(long long)); + } else { + *(long long *)buf = v; + } + } + buf += sizeof(long long); + } else { + __ASSERT(false, "unexpected size %u", size); + return -EINVAL; + } + } + + /* + * We remember the size of the argument list as a multiple of + * sizeof(int) and limit it to a 8-bit field. That means 1020 bytes + * worth of va_list, or about 127 arguments on a 64-bit system + * (twice that on 32-bit systems). That ought to be good enough. + */ + if ((buf - buf0) / sizeof(int) > 255) { + __ASSERT(false, "too many format args"); + return -EINVAL; + } + + /* + * If all we wanted was to count required buffer size + * then we have it now. + */ + if (!buf0) { + return len + buf - buf0; + } + + /* Clear our buffer header. We made room for it initially. */ + *(char **)buf0 = 0; + + /* Record end of argument list and number of appended strings. */ + buf0[0] = (buf - buf0) / sizeof(int); + buf0[1] = s_idx; + + /* Store strings prefixed by their pointer location. */ + for (i = 0; i < s_idx; i++) { + /* retrieve the string pointer */ + s = *(char **)(buf0 + str_ptr_pos[i] * sizeof(int)); + /* clear the in-buffer pointer (less entropy if compressed) */ + *(char **)(buf0 + str_ptr_pos[i] * sizeof(int)) = NULL; + /* find the string length including terminating '\0' */ + size = strlen(s) + 1; + /* make sure it fits */ + if (buf - buf0 + 1 + size > len) { + return -ENOSPC; + } + /* store the pointer position prefix */ + *buf++ = str_ptr_pos[i]; + /* copy the string with its terminating '\0' */ + memcpy(buf, s, size); + buf += size; + } + + /* + * TODO: remove pointers for appended strings since they're useless. + * TODO: explore leveraging same mechanism to remove alignment padding + */ + + return buf - buf0; +} + +int cbprintf_package(void *packaged, size_t len, const char *format, ...) +{ + va_list ap; + int ret; + + va_start(ap, format); + ret = cbvprintf_package(packaged, len, format, ap); + va_end(ap); + return ret; +} + +int cbpprintf(cbprintf_cb out, void *ctx, void *packaged) +{ + char *buf = packaged, *fmt, *s, **ps; + unsigned int i, args_size, s_nbr, s_idx; + + if (!buf) { + return -EINVAL; + } + + /* Retrieve the size of the arg list and number of strings. */ + args_size = ((uint8_t *)buf)[0] * sizeof(int); + s_nbr = ((uint8_t *)buf)[1]; + + /* Locate the string table */ + s = buf + args_size; + + /* + * Patch in string pointers. + */ + for (i = 0; i < s_nbr; i++) { + /* Locate pointer location for this string */ + s_idx = *(uint8_t *)s++; + ps = (char **)(buf + s_idx * sizeof(int)); + /* update the pointer with current string location */ + *ps = s; + /* move to next string */ + s += strlen(s) + 1; + } + + /* Retrieve format string */ + fmt = ((char **)buf)[1]; + + /* skip past format string pointer */ + buf += sizeof(char *) * 2; + + /* Turn this into a va_list and print it */ + return cbprintf_via_va_list(out, ctx, fmt, buf); +}