Skip to content

Commit 518d842

Browse files
Nicolas Pitrefabiobaltieri
authored andcommitted
lib: heap: compute Z_HEAP_MIN_SIZE from actual struct layouts
Z_HEAP_MIN_SIZE and Z_HEAP_MIN_SIZE_FOR were defined in kernel.h as hardcoded magic numbers gated by a growing tower of #ifdefs — one per Kconfig option that happened to affect the heap struct layout. Every internal change required manually recomputing the constants, duplicating layout knowledge across files, and praying nobody forgot to update the #ifdef matrix. This is fragile and unscalable: adding a single new heap feature (e.g. a chunk canary trailer) would add yet another dimension to the combinatorial explosion. Replace this with build-time computation from the actual C structures. A new lib/heap/heap_constants.c is compiled as part of the offsets library and uses GEN_ABSOLUTE_SYM to emit the correct values into the generated offsets.h. Z_HEAP_MIN_SIZE is derived through an iterative fixed-point expansion (3 rounds, always convergent) that mirrors the runtime logic in sys_heap_init(). Z_HEAP_MIN_SIZE_FOR overhead and bucket sizes are also generated, keeping all internal heap layout knowledge in one place. Big vs small heap determination uses CONFIG_SYS_HEAP_SMALL_ONLY, CONFIG_SYS_HEAP_BIG_ONLY, and sizeof(void *), mirroring the big_heap_chunks() logic in heap.h. kernel.h picks up the generated values via __has_include(<zephyr/offsets.h>) so there is no circular dependency with the offsets compilation itself. The old _Z_HEAP_SIZE manual sizeof and BUILD_ASSERT scaffolding in heap.c are removed. gen_offset_header.py is updated to accept multiple input object files so that the heap constants object can coexist with the per-arch offsets object in the same offsets library. COMMAND_EXPAND_LISTS is added to the offsets generation custom command so that CMake correctly expands the $<TARGET_OBJECTS:> generator expression into separate arguments when the offsets library contains more than one object file. Signed-off-by: Nicolas Pitre <npitre@baylibre.com>
1 parent b439029 commit 518d842

File tree

5 files changed

+131
-48
lines changed

5 files changed

+131
-48
lines changed

‎CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,8 @@ target_include_directories(${OFFSETS_LIB} PRIVATE
10481048
# Make sure that LTO will never be enabled when compiling offsets.c
10491049
set_source_files_properties(${OFFSETS_C_PATH} PROPERTIES COMPILE_OPTIONS $<TARGET_PROPERTY:compiler,prohibit_lto>)
10501050

1051+
target_sources(${OFFSETS_LIB} PRIVATE ${ZEPHYR_BASE}/lib/heap/heap_constants.c)
1052+
10511053
target_link_libraries(${OFFSETS_LIB} zephyr_interface)
10521054
add_dependencies(zephyr_interface
10531055
${SYSCALL_LIST_H_TARGET}
@@ -1061,6 +1063,7 @@ add_custom_command(
10611063
COMMAND ${PYTHON_EXECUTABLE} ${ZEPHYR_BASE}/scripts/build/gen_offset_header.py
10621064
-i $<TARGET_OBJECTS:${OFFSETS_LIB}>
10631065
-o ${OFFSETS_H_PATH}
1066+
COMMAND_EXPAND_LISTS
10641067
DEPENDS
10651068
${OFFSETS_LIB}
10661069
$<TARGET_OBJECTS:${OFFSETS_LIB}>

‎include/zephyr/kernel.h‎

Lines changed: 34 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6108,50 +6108,55 @@ void *k_heap_realloc(struct k_heap *h, void *ptr, size_t bytes, k_timeout_t time
61086108
*/
61096109
void k_heap_free(struct k_heap *h, void *mem) __attribute_nonnull(1);
61106110

6111-
/* Minimum heap sizes needed to return a successful 1-byte allocation.
6112-
* Assumes a chunk aligned (8 byte) memory buffer.
6111+
/*
6112+
* Heap sizing constants computed at build time from actual struct layouts
6113+
* in lib/heap/heap_constants.c via the gen_offset mechanism.
61136114
*/
6114-
#ifdef CONFIG_SYS_HEAP_RUNTIME_STATS
6115-
#define Z_HEAP_MIN_SIZE ((sizeof(void *) > 4) ? 80 : 52)
6116-
#else
6117-
#define Z_HEAP_MIN_SIZE ((sizeof(void *) > 4) ? 56 : 44)
6118-
#endif /* CONFIG_SYS_HEAP_RUNTIME_STATS */
6115+
#if __has_include(<zephyr/offsets.h>)
6116+
#include <zephyr/offsets.h>
6117+
#endif
6118+
6119+
/* chunk0 size in bytes for nb buckets */
6120+
#define _Z_HEAP_C0(nb) \
6121+
ROUND_UP(___z_heap_struct_SIZEOF + \
6122+
(nb) * ___z_heap_bucket_SIZEOF, ___z_heap_chunk_unit_SIZEOF)
61196123

6120-
/* Size of `struct z_heap` */
6121-
#define _Z_HEAP_SIZE \
6122-
((4 * sizeof(uint32_t)) + \
6123-
(3 * (IS_ENABLED(CONFIG_SYS_HEAP_RUNTIME_STATS) ? sizeof(size_t) : 0)))
6124+
/* Allocation chunk size in bytes */
6125+
#define _Z_HEAP_AC(ab) \
6126+
ROUND_UP(___z_heap_hdr_SIZEOF + (ab), ___z_heap_chunk_unit_SIZEOF)
61246127

6125-
/* Number of buckets required to store @a bytes */
6126-
#define _Z_HEAP_NUM_BUCKETS(bytes) ((31 - __builtin_clz((bytes / 8) - 1)) + 1)
6128+
/* Total heap size in chunk units */
6129+
#define _Z_HEAP_SZ(nb, ab) \
6130+
((_Z_HEAP_C0(nb) + _Z_HEAP_AC(ab)) / ___z_heap_chunk_unit_SIZEOF)
61276131

6128-
/* Number of bytes consumed by buckets */
6129-
#define _Z_HEAP_BUCKETS_SIZE(bytes) (_Z_HEAP_NUM_BUCKETS(bytes) * sizeof(uint32_t))
6132+
/* Bucket count from heap size in chunk units (mirrors bucket_idx() + 1) */
6133+
#define _Z_HEAP_NB(sz) \
6134+
(32 - __builtin_clz((unsigned int)((sz) - \
6135+
___z_heap_min_chunk_SIZEOF + 1)))
6136+
6137+
/* 3-round convergent iteration starting from 1 bucket */
6138+
#define _Z_HEAP_NB1(ab) _Z_HEAP_NB(_Z_HEAP_SZ(1, ab))
6139+
#define _Z_HEAP_NB2(ab) _Z_HEAP_NB(_Z_HEAP_SZ(_Z_HEAP_NB1(ab), ab))
6140+
#define _Z_HEAP_NB3(ab) _Z_HEAP_NB(_Z_HEAP_SZ(_Z_HEAP_NB2(ab), ab))
61306141

61316142
/**
61326143
* @brief Minimum heap size required for allocating a given size
61336144
*
61346145
* Heaps store metadata at the start of the provided array, resulting in a
61356146
* heap declaration of N bytes having an actual allocation capacity of less
6136-
* than N bytes. This macro approximates how much overhead in bytes the metadata
6137-
* consumes, allowing for real allocations closer to the requested size.
6138-
*
6139-
* Assumes small heaps, since for large heaps the importance of tuning single bytes
6140-
* is small.
6141-
*
6142-
* The parameters considered:
6143-
* Size of the header "struct z_heap"
6144-
* Buckets holding chunk metadata
6145-
* Free heap chunk
6146-
* End chunk
6147-
* Chunk metadata for single allocation
6147+
* than N bytes. This macro returns the exact heap buffer size needed to
6148+
* satisfy a single allocation of @a alloc_bytes bytes.
6149+
* A chunk aligned (8 byte) memory buffer is assumed.
61486150
*
61496151
* @param alloc_bytes Size of a desired allocation
61506152
*
6151-
* @return Approximate size of the heap required to allocate @a alloc_bytes
6153+
* @return Size of the heap required to allocate @a alloc_bytes
61526154
*/
61536155
#define Z_HEAP_MIN_SIZE_FOR(alloc_bytes) \
6154-
((alloc_bytes) + _Z_HEAP_SIZE + _Z_HEAP_BUCKETS_SIZE(alloc_bytes) + (3 * 8))
6156+
(_Z_HEAP_C0(_Z_HEAP_NB3(alloc_bytes)) + \
6157+
_Z_HEAP_AC(alloc_bytes) + ___z_heap_ftr_SIZEOF)
6158+
6159+
#define Z_HEAP_MIN_SIZE Z_HEAP_MIN_SIZE_FOR(1)
61556160

61566161
/**
61576162
* @brief Define a static k_heap in the specified linker section

‎lib/heap/heap.c‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313
#include <sanitizer/msan_interface.h>
1414
#endif
1515

16-
/* Validate the `kernel.h` macro */
17-
BUILD_ASSERT(_Z_HEAP_SIZE == sizeof(struct z_heap), "Incorrect _Z_HEAP_SIZE value");
1816

1917
#ifdef CONFIG_SYS_HEAP_RUNTIME_STATS
2018
static inline void increase_allocated_bytes(struct z_heap *h, size_t num_bytes)

‎lib/heap/heap_constants.c‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (c) 2026 BayLibre SAS
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*
6+
* Build-time computation of heap constants from actual struct layouts.
7+
* Compiled as part of the offsets library so that the generated offsets.h
8+
* header provides heap sizing values derived from real structure sizes,
9+
* eliminating manual synchronization of magic numbers in kernel.h.
10+
*
11+
* The heap layout for a single allocation looks like:
12+
*
13+
* [ chunk0: struct z_heap + buckets ] [ alloc chunk ] [ftr]
14+
* | |
15+
* hdr data (trailer)
16+
*
17+
* Each region is sized as follows:
18+
*
19+
* chunk0 = ROUND_UP(sizeof(struct z_heap) + nb * bucket_size, CHUNK_UNIT)
20+
* alloc = ROUND_UP(chunk_header_bytes + alloc_bytes, CHUNK_UNIT)
21+
* ftr = heap_footer_bytes (end marker, not chunk-aligned)
22+
*
23+
* The number of buckets (nb) depends on the total heap size in chunk
24+
* units, creating a circular dependency that is resolved by iterating
25+
* from 1 bucket until convergence (3 rounds always suffice).
26+
*
27+
* The individual component sizes are emitted as symbols so that
28+
* kernel.h can compute exact heap sizes without any magic numbers.
29+
*/
30+
31+
#include <gen_offset.h>
32+
#include <zephyr/kernel.h>
33+
#include "heap.h"
34+
35+
/*
36+
* Determine whether the heap uses big (8-byte) or small (4-byte) chunk
37+
* headers and footers. This mirrors big_heap_chunks() in heap.h: for
38+
* the minimum-size computation the heap is always tiny, so the runtime
39+
* size threshold never triggers — only Kconfig and pointer width matter.
40+
*/
41+
#ifdef CONFIG_SYS_HEAP_SMALL_ONLY
42+
#define _IS_BIG 0
43+
#elif defined(CONFIG_SYS_HEAP_BIG_ONLY)
44+
#define _IS_BIG 1
45+
#else
46+
#define _IS_BIG (sizeof(void *) > 4)
47+
#endif
48+
49+
#define _HDR (_IS_BIG ? 8 : 4) /* chunk_header_bytes */
50+
#define _FTR (_IS_BIG ? 8 : 4) /* heap_footer_bytes */
51+
52+
/* Round up to chunk units (compile-time version of chunksz()) */
53+
#define _CHUNKSZ(n) (((n) + CHUNK_UNIT - 1U) / CHUNK_UNIT)
54+
55+
/* min_chunk_size: smallest allocatable chunk (in chunk units) */
56+
#define _MINC _CHUNKSZ(_HDR + 1)
57+
58+
/* --- emit symbols into offsets.h --- */
59+
60+
GEN_ABS_SYM_BEGIN(_HeapConstantAbsSyms)
61+
62+
GEN_ABSOLUTE_SYM(___z_heap_struct_SIZEOF, sizeof(struct z_heap));
63+
64+
GEN_ABSOLUTE_SYM(___z_heap_bucket_SIZEOF,
65+
sizeof(struct z_heap_bucket));
66+
67+
GEN_ABSOLUTE_SYM(___z_heap_chunk_unit_SIZEOF, CHUNK_UNIT);
68+
69+
GEN_ABSOLUTE_SYM(___z_heap_hdr_SIZEOF, (unsigned int)_HDR);
70+
71+
GEN_ABSOLUTE_SYM(___z_heap_ftr_SIZEOF, (unsigned int)_FTR);
72+
73+
GEN_ABSOLUTE_SYM(___z_heap_min_chunk_SIZEOF, (unsigned int)_MINC);
74+
75+
GEN_ABS_SYM_END

‎scripts/build/gen_offset_header.py‎

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
#
77

88
"""
9-
This script scans a specified object file and generates a header file
10-
that defined macros for the offsets of various found structure members
9+
This script scans one or more specified object files and generates a header
10+
file that defined macros for the offsets of various found structure members
1111
(particularly symbols ending with ``_OFFSET`` or ``_SIZEOF``), primarily
1212
intended for use in assembly code.
1313
"""
@@ -27,7 +27,7 @@ def get_symbol_table(obj):
2727
raise LookupError("Could not find symbol table")
2828

2929

30-
def gen_offset_header(input_name, input_file, output_file):
30+
def gen_offset_header(input_files, output_file):
3131
include_guard = "__GEN_OFFSETS_H__"
3232
output_file.write(
3333
f"""/* THIS FILE IS AUTO GENERATED. PLEASE DO NOT EDIT.
@@ -41,19 +41,21 @@ def gen_offset_header(input_name, input_file, output_file):
4141
#define {include_guard}\n\n"""
4242
)
4343

44-
obj = ELFFile(input_file)
45-
for sym in get_symbol_table(obj).iter_symbols():
46-
if isinstance(sym.name, bytes):
47-
sym.name = str(sym.name, 'ascii')
44+
for input_path in input_files:
45+
with open(input_path, 'rb') as input_file:
46+
obj = ELFFile(input_file)
47+
for sym in get_symbol_table(obj).iter_symbols():
48+
if isinstance(sym.name, bytes):
49+
sym.name = str(sym.name, 'ascii')
4850

49-
if not sym.name.endswith(('_OFFSET', '_SIZEOF')):
50-
continue
51-
if sym.entry['st_shndx'] != 'SHN_ABS':
52-
continue
53-
if sym.entry['st_info']['bind'] != 'STB_GLOBAL':
54-
continue
51+
if not sym.name.endswith(('_OFFSET', '_SIZEOF')):
52+
continue
53+
if sym.entry['st_shndx'] != 'SHN_ABS':
54+
continue
55+
if sym.entry['st_info']['bind'] != 'STB_GLOBAL':
56+
continue
5557

56-
output_file.write(f"#define {sym.name} 0x{sym.entry['st_value']:x}\n")
58+
output_file.write(f"#define {sym.name} 0x{sym.entry['st_value']:x}\n")
5759

5860
output_file.write(f"\n#endif /* {include_guard} */\n")
5961

@@ -67,12 +69,12 @@ def gen_offset_header(input_name, input_file, output_file):
6769
allow_abbrev=False,
6870
)
6971

70-
parser.add_argument("-i", "--input", required=True, help="Input object file")
72+
parser.add_argument("-i", "--input", required=True, nargs='+', help="Input object file(s)")
7173
parser.add_argument("-o", "--output", required=True, help="Output header file")
7274

7375
args = parser.parse_args()
7476

75-
with open(args.input, 'rb') as input_file, open(args.output, 'w') as output_file:
76-
ret = gen_offset_header(args.input, input_file, output_file)
77+
with open(args.output, 'w') as output_file:
78+
ret = gen_offset_header(args.input, output_file)
7779

7880
sys.exit(ret)

0 commit comments

Comments
 (0)