Merge branch 'feature/heap-alloc-free-callbacks' into 'master'

heap: Add allocation and free function hooks.

Closes IDFGH-9424

See merge request espressif/esp-idf!22456
This commit is contained in:
Mahavir Jain 2023-03-24 10:55:11 +08:00
commit 55fd7d43c1
9 changed files with 160 additions and 19 deletions

View File

@ -56,6 +56,11 @@ menu "Heap memory debugging"
More stack frames uses more memory in the heap trace buffer (and slows down allocation), but More stack frames uses more memory in the heap trace buffer (and slows down allocation), but
can provide useful information. can provide useful information.
config HEAP_USE_HOOKS
bool "Use allocation and free hooks"
help
Enable the user to implement function hooks triggered for each successful allocation and free.
config HEAP_TASK_TRACKING config HEAP_TASK_TRACKING
bool "Enable heap task tracking" bool "Enable heap task tracking"
depends on !HEAP_POISONING_DISABLED depends on !HEAP_POISONING_DISABLED

View File

@ -15,6 +15,16 @@
#include "heap_private.h" #include "heap_private.h"
#include "esp_system.h" #include "esp_system.h"
#ifdef CONFIG_HEAP_USE_HOOKS
#define CALL_HOOK(hook, ...) { \
if (hook != NULL) { \
hook(__VA_ARGS__); \
} \
}
#else
#define CALL_HOOK(hook, ...) {}
#endif
/* Forward declaration for base function, put in IRAM. /* Forward declaration for base function, put in IRAM.
* These functions don't check for errors after trying to allocate memory. */ * These functions don't check for errors after trying to allocate memory. */
static void *heap_caps_realloc_base( void *ptr, size_t size, uint32_t caps ); static void *heap_caps_realloc_base( void *ptr, size_t size, uint32_t caps );
@ -32,7 +42,6 @@ possible. This should optimize the amount of RAM accessible to the code without
static esp_alloc_failed_hook_t alloc_failed_callback; static esp_alloc_failed_hook_t alloc_failed_callback;
#ifdef CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS #ifdef CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS
IRAM_ATTR static void hex_to_str(char buf[8], uint32_t n) IRAM_ATTR static void hex_to_str(char buf[8], uint32_t n)
{ {
@ -113,11 +122,7 @@ IRAM_ATTR static void *heap_caps_malloc_base( size_t size, uint32_t caps)
{ {
void *ret = NULL; void *ret = NULL;
if (size == 0) { if (size == 0 || size > HEAP_SIZE_MAX ) {
return NULL;
}
if (size > HEAP_SIZE_MAX) {
// Avoids int overflow when adding small numbers to size, or // Avoids int overflow when adding small numbers to size, or
// calculating 'end' from start+size, by limiting 'size' to the possible range // calculating 'end' from start+size, by limiting 'size' to the possible range
return NULL; return NULL;
@ -162,12 +167,15 @@ IRAM_ATTR static void *heap_caps_malloc_base( size_t size, uint32_t caps)
ret = multi_heap_malloc(heap->heap, size + 4); // int overflow checked above ret = multi_heap_malloc(heap->heap, size + 4); // int overflow checked above
if (ret != NULL) { if (ret != NULL) {
return dram_alloc_to_iram_addr(ret, size + 4); // int overflow checked above uint32_t *iptr = dram_alloc_to_iram_addr(ret, size + 4); // int overflow checked above
CALL_HOOK(esp_heap_trace_alloc_hook, iptr, size, caps);
return iptr;
} }
} else { } else {
//Just try to alloc, nothing special. //Just try to alloc, nothing special.
ret = multi_heap_malloc(heap->heap, size); ret = multi_heap_malloc(heap->heap, size);
if (ret != NULL) { if (ret != NULL) {
CALL_HOOK(esp_heap_trace_alloc_hook, ret, size, caps);
return ret; return ret;
} }
} }
@ -188,6 +196,7 @@ IRAM_ATTR void *heap_caps_malloc( size_t size, uint32_t caps){
void* ptr = heap_caps_malloc_base(size, caps); void* ptr = heap_caps_malloc_base(size, caps);
if (!ptr && size > 0){ if (!ptr && size > 0){
heap_caps_alloc_failed(size, caps, __func__); heap_caps_alloc_failed(size, caps, __func__);
} }
@ -286,6 +295,7 @@ IRAM_ATTR void *heap_caps_malloc_prefer( size_t size, size_t num, ... )
break; break;
} }
} }
if (r == NULL && size > 0){ if (r == NULL && size > 0){
heap_caps_alloc_failed(size, caps, __func__); heap_caps_alloc_failed(size, caps, __func__);
} }
@ -309,6 +319,7 @@ IRAM_ATTR void *heap_caps_realloc_prefer( void *ptr, size_t size, size_t num, ..
break; break;
} }
} }
if (r == NULL && size > 0){ if (r == NULL && size > 0){
heap_caps_alloc_failed(size, caps, __func__); heap_caps_alloc_failed(size, caps, __func__);
} }
@ -332,6 +343,7 @@ IRAM_ATTR void *heap_caps_calloc_prefer( size_t n, size_t size, size_t num, ...
break; break;
} }
} }
if (r == NULL && size > 0){ if (r == NULL && size > 0){
heap_caps_alloc_failed(size, caps, __func__); heap_caps_alloc_failed(size, caps, __func__);
} }
@ -374,6 +386,8 @@ IRAM_ATTR void heap_caps_free( void *ptr)
heap_t *heap = find_containing_heap(ptr); heap_t *heap = find_containing_heap(ptr);
assert(heap != NULL && "free() target pointer is outside heap areas"); assert(heap != NULL && "free() target pointer is outside heap areas");
multi_heap_free(heap->heap, ptr); multi_heap_free(heap->heap, ptr);
CALL_HOOK(esp_heap_trace_free_hook, ptr);
} }
/* /*
@ -427,6 +441,7 @@ IRAM_ATTR static void *heap_caps_realloc_base( void *ptr, size_t size, uint32_t
// (which will resize the block if it can) // (which will resize the block if it can)
void *r = multi_heap_realloc(heap->heap, ptr, size); void *r = multi_heap_realloc(heap->heap, ptr, size);
if (r != NULL) { if (r != NULL) {
CALL_HOOK(esp_heap_trace_alloc_hook, r, size, caps);
return r; return r;
} }
} }
@ -458,6 +473,7 @@ IRAM_ATTR void *heap_caps_realloc( void *ptr, size_t size, uint32_t caps)
{ {
ptr = heap_caps_realloc_base(ptr, size, caps); ptr = heap_caps_realloc_base(ptr, size, caps);
if (ptr == NULL && size > 0){ if (ptr == NULL && size > 0){
heap_caps_alloc_failed(size, caps, __func__); heap_caps_alloc_failed(size, caps, __func__);
} }
@ -489,6 +505,7 @@ IRAM_ATTR void *heap_caps_calloc( size_t n, size_t size, uint32_t caps)
{ {
void* ptr = heap_caps_calloc_base(n, size, caps); void* ptr = heap_caps_calloc_base(n, size, caps);
if (!ptr && size > 0){ if (!ptr && size > 0){
heap_caps_alloc_failed(size, caps, __func__); heap_caps_alloc_failed(size, caps, __func__);
} }
@ -677,6 +694,7 @@ IRAM_ATTR void *heap_caps_aligned_alloc(size_t alignment, size_t size, uint32_t
//Just try to alloc, nothing special. //Just try to alloc, nothing special.
ret = multi_heap_aligned_alloc(heap->heap, size, alignment); ret = multi_heap_aligned_alloc(heap->heap, size, alignment);
if (ret != NULL) { if (ret != NULL) {
CALL_HOOK(esp_heap_trace_alloc_hook, ret, size, caps);
return ret; return ret;
} }
} }

View File

@ -11,6 +11,7 @@
#include "multi_heap.h" #include "multi_heap.h"
#include <sdkconfig.h> #include <sdkconfig.h>
#include "esp_err.h" #include "esp_err.h"
#include "esp_attr.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -53,6 +54,26 @@ typedef void (*esp_alloc_failed_hook_t) (size_t size, uint32_t caps, const char
*/ */
esp_err_t heap_caps_register_failed_alloc_callback(esp_alloc_failed_hook_t callback); esp_err_t heap_caps_register_failed_alloc_callback(esp_alloc_failed_hook_t callback);
#ifdef CONFIG_HEAP_USE_HOOKS
/**
* @brief callback called after every allocation
* @param ptr the allocated memory
* @param size in bytes of the allocation
* @param caps Bitwise OR of MALLOC_CAP_* flags indicating the type of memory allocated.
* @note this hook is called on the same thread as the allocation, which may be within a low level operation.
* You should refrain from doing heavy work, logging, flash writes, or any locking.
*/
__attribute__((weak)) IRAM_ATTR void esp_heap_trace_alloc_hook(void* ptr, size_t size, uint32_t caps);
/**
* @brief callback called after every free
* @param ptr the memory that was freed
* @note this hook is called on the same thread as the allocation, which may be within a low level operation.
* You should refrain from doing heavy work, logging, flash writes, or any locking.
*/
__attribute__((weak)) IRAM_ATTR void esp_heap_trace_free_hook(void* ptr);
#endif
/** /**
* @brief Allocate a chunk of memory which has the given capabilities * @brief Allocate a chunk of memory which has the given capabilities
* *

View File

@ -370,7 +370,7 @@ multi_heap_handle_t multi_heap_register(void *start, size_t size)
return multi_heap_register_impl(start, size); return multi_heap_register_impl(start, size);
} }
static inline void subtract_poison_overhead(size_t *arg) { static inline __attribute__((always_inline)) void subtract_poison_overhead(size_t *arg) {
if (*arg > POISON_OVERHEAD) { if (*arg > POISON_OVERHEAD) {
*arg -= POISON_OVERHEAD; *arg -= POISON_OVERHEAD;
} else { } else {

View File

@ -162,22 +162,90 @@ TEST_CASE("malloc/calloc(0) should not call failure callback", "[heap]")
TEST_CASE("test get allocated size", "[heap]") TEST_CASE("test get allocated size", "[heap]")
{ {
const size_t iterations = 32; // random values to test, some are 4 bytes aligned, some are not
const size_t alloc_sizes[] = { 1035, 1064, 1541 };
const size_t iterations = sizeof(alloc_sizes) / sizeof(size_t);
void *ptr_array[iterations];
for (size_t i = 0; i < iterations; i++) { for (size_t i = 0; i < iterations; i++) {
// minimum block size is 12, so to avoid unecessary logic in the test, ptr_array[i] = heap_caps_malloc(alloc_sizes[i], MALLOC_CAP_DEFAULT);
// set the minimum requested size to 12. TEST_ASSERT_NOT_NULL(ptr_array[i]);
const size_t alloc_size = rand() % 1024 + 12;
void *ptr = heap_caps_malloc(alloc_size, MALLOC_CAP_DEFAULT);
TEST_ASSERT_NOT_NULL(ptr);
// test that the heap_caps_get_allocated_size() returns the right number of bytes (aligned to 4 bytes // test that the heap_caps_get_allocated_size() returns the right number of bytes (aligned to 4 bytes
// since the heap component aligns to 4 bytes) // since the heap component aligns to 4 bytes)
const size_t aligned_size = (alloc_size + 3) & ~3; const size_t aligned_size = (alloc_sizes[i] + 3) & ~3;
printf("initial size: %d, requested size : %d, allocated size: %d\n", alloc_size, aligned_size, heap_caps_get_allocated_size(ptr)); const size_t real_size = heap_caps_get_allocated_size(ptr_array[i]);
TEST_ASSERT_EQUAL(aligned_size, heap_caps_get_allocated_size(ptr)); printf("initial size: %d, requested size : %d, allocated size: %d\n", alloc_sizes[i], aligned_size, real_size);
TEST_ASSERT_EQUAL(aligned_size, real_size);
heap_caps_free(ptr); heap_caps_free(ptr_array[i]);
} }
} }
#ifdef CONFIG_HEAP_USE_HOOKS
// provide the definition of alloc and free hooks
static const size_t alloc_size = 1234; // make this size atypical to be able to rely on it in the hook
static const size_t expected_calls = 2; // one call for malloc/calloc and one call for realloc
static uint32_t *alloc_ptr = NULL;
static bool test_success = false;
static size_t counter = 0;
static void reset_static_variables(void) {
test_success = false;
alloc_ptr = NULL;
counter = 0;
}
void esp_heap_trace_alloc_hook(void* ptr, size_t size, uint32_t caps)
{
if (size == alloc_size) {
counter++;
if (counter == expected_calls) {
alloc_ptr = ptr;
}
}
}
void esp_heap_trace_free_hook(void* ptr)
{
if (alloc_ptr == ptr && counter == expected_calls) {
test_success = true;
}
}
TEST_CASE("test allocation and free function hooks", "[heap]")
{
// alloc, realloc and free memory, at the end of the test, test_success will be set
// to true if both function hooks are called.
uint32_t *ptr = heap_caps_malloc(alloc_size, MALLOC_CAP_DEFAULT);
TEST_ASSERT_NOT_NULL(ptr);
ptr = heap_caps_realloc(ptr, alloc_size, MALLOC_CAP_32BIT);
heap_caps_free(ptr);
TEST_ASSERT_TRUE(test_success);
// re-init the static variables
reset_static_variables();
// calloc, realloc and free memory, at the end of the test, test_success will be set
// to true if both function hooks are called.
ptr = heap_caps_calloc(1, alloc_size, MALLOC_CAP_DEFAULT);
TEST_ASSERT_NOT_NULL(ptr);
ptr = heap_caps_realloc(ptr, alloc_size, MALLOC_CAP_32BIT);
heap_caps_free(ptr);
TEST_ASSERT_TRUE(test_success);
// re-init the static variables
reset_static_variables();
// aligned alloc, realloc and aligned free memory, at the end of the test, test_success
// will be set to true if both function hooks are called.
ptr = heap_caps_aligned_alloc(0x200, alloc_size, MALLOC_CAP_DEFAULT);
TEST_ASSERT_NOT_NULL(ptr);
ptr = heap_caps_realloc(ptr, alloc_size, MALLOC_CAP_32BIT);
heap_caps_free(ptr);
TEST_ASSERT_TRUE(test_success);
}
#endif

View File

@ -102,3 +102,17 @@ def test_memory_protection(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests') dut.expect_exact('Press ENTER to see the list of tests')
dut.write('[heap][mem_prot]') dut.write('[heap][mem_prot]')
dut.expect_unity_test_output(timeout=300) dut.expect_unity_test_output(timeout=300)
@pytest.mark.generic
@pytest.mark.esp32
@pytest.mark.parametrize(
'config',
[
'func_hooks'
]
)
def test_heap_func_hooks(dut: Dut) -> None:
dut.expect_exact('Press ENTER to see the list of tests')
dut.write('"test allocation and free function hooks"')
dut.expect_unity_test_output(timeout=300)

View File

@ -0,0 +1 @@
CONFIG_HEAP_USE_HOOKS=y

View File

@ -24,6 +24,19 @@ To obtain information about the state of the heap:
- :cpp:func:`heap_caps_dump` and :cpp:func:`heap_caps_dump_all` will output detailed information about the structure of each block in the heap. Note that this can be large amount of output. - :cpp:func:`heap_caps_dump` and :cpp:func:`heap_caps_dump_all` will output detailed information about the structure of each block in the heap. Note that this can be large amount of output.
.. _heap-allocation-free:
Heap allocation and free function hooks
---------------------------------------
Heap allocation and free detection hooks allows you to be notified of every successful allocation and free operations:
- Providing a definition of :cpp:func:`esp_heap_trace_alloc_hook` will allow you to be notified of every successful memory allocation operations
- Providing a definition of :cpp:func:`esp_heap_trace_free_hook` will allow you to be notified of every memory free operations
To activate the feature, navigate to ``Component config`` -> ``Heap Memory Debugging`` in the configuration menu and select ``Use allocation and free hooks`` option (see :ref:`CONFIG_HEAP_USE_HOOKS`).
:cpp:func:`esp_heap_trace_alloc_hook` and :cpp:func:`esp_heap_trace_free_hook` have weak declarations, it is not necessary to provide a declarations for both hooks.
Since allocating and freeing memory is allowed even though strongly recommended against, :cpp:func:`esp_heap_trace_alloc_hook` and :cpp:func:`esp_heap_trace_free_hook` can potentially be called from ISR.
.. _heap-corruption: .. _heap-corruption:
Heap Corruption Detection Heap Corruption Detection

View File

@ -158,6 +158,7 @@ Heap Tracing & Debugging
The following features are documented on the :doc:`Heap Memory Debugging </api-reference/system/heap_debug>` page: The following features are documented on the :doc:`Heap Memory Debugging </api-reference/system/heap_debug>` page:
- :ref:`Heap Information <heap-information>` (free space, etc.) - :ref:`Heap Information <heap-information>` (free space, etc.)
- :ref:`Heap allocation and free function hooks <heap-allocation-free>`
- :ref:`Heap Corruption Detection <heap-corruption>` - :ref:`Heap Corruption Detection <heap-corruption>`
- :ref:`Heap Tracing <heap-tracing>` (memory leak detection, monitoring, etc.) - :ref:`Heap Tracing <heap-tracing>` (memory leak detection, monitoring, etc.)