嵌入式系统上的 malloc 行为

Posted

技术标签:

【中文标题】嵌入式系统上的 malloc 行为【英文标题】:malloc behaviour on an embedded system 【发布时间】:2014-04-20 18:59:06 【问题描述】:

我目前正在研究一个嵌入式项目(STM32F103RB,CooCox CoIDE v.1.7.6 with arm-none-eabi-gcc 4.8 2013q4),我试图了解malloc() 在普通C 上的行为当 RAM 已满时。

我的 STM32 有 20kB = 0x5000Bytes 的 RAM,0x200 用于堆栈。

#include <stdlib.h>
#include "stm32f10x.h"

struct list_el 
   char weight[1024];
;

typedef struct list_el item;

int main(void)

    item * curr;

    // allocate until RAM is full
    do 
        curr = (item *)malloc(sizeof(item));
     while (curr != NULL);

    // I know, free() is missing. Program is supposed to crash

    return 0;

我希望malloc() 在堆太小而无法分配时返回NULL

0x5000 (RAM) - 0x83C (bss) - 0x200 (stack) = 0x45C4 (heap)

所以当第 18 次执行 malloc() 时。一项为 1024=0x400 字节大。

但是 uC 在第 18 次之后调用HardFault_Handler(void)(甚至不是MemManager_Handler(void)

是否有人建议如何预测 malloc() 失败 - 因为等待 NULL 返回似乎不起作用。

谢谢。

【问题讨论】:

我没有任何答案,但感谢您提出一个有趣且清晰的问题。 (+1) 我也没有任何答案,但这表明 C 库的 malloc() 函数存在错误。 你在使用uClibc吗? 有什么方法可以获取剩余内存的大小? @BernhardSchlegel uClibc 是标准 C 库的特定实现,您可以使用调试符号编译您的 C 库,然后使用调试器进入 malloc 并查看究竟是哪一行导致调用硬故障处理程序。您可以将 GCC 与 C 库的不同实现一起使用,因此说您使用 GCC 并不能真正说明您使用的是 C 库的哪个实现。我们只能假设您使用默认的。 【参考方案1】:

看起来malloc 根本没有做任何检查。您遇到的故障来自硬件检测到对无效地址的写入,这可能来自malloc 本身。

malloc 分配内存时,它会从其内部池中取出一个块,并将其返回给您。但是,它需要为free 函数存储一些信息才能完成释放。通常,这是块的实际长度。为了保存该信息,malloc 从块本身的开头获取几个字节,将信息写入那里,然后返回地址经过它写入自己的信息的位置。

例如,假设您要求一个 10 字节的块。 malloc 会在地址0x3200..0x320F 处获取一个可用的 16 字节块,将长度(即 16)写入字节 1 和 2,然后将 0x3202 返回给您。现在您的程序可以使用从0x32020x320B 的十个字节。其他 4 个字节也可用 - 如果您调用 realloc 并要求 14 个字节,则不会重新分配。

malloc 将长度写入它即将返回给您的内存块时,关键点就出现了:它写入的地址必须是有效的。似乎在第 18 次迭代之后,下一个块的地址为负数(转换为非常大的正数),因此 CPU 捕获了写入,并触发了硬故障。

在堆和堆栈相互向对方增长的情况下,没有可靠的方法来检测内存不足,同时让您使用内存的最后一个字节,这通常是非常理想的事情。 malloc 无法预测分配后您将使用多少堆栈,因此它甚至不会尝试。这就是为什么在大多数情况下,字节数由你来负责。

一般来说,在嵌入式硬件上,当空间限制为几十 KB 时,您可以避免在“任意”位置调用 malloc。相反,您可以使用一些预先计算的限制预先分配所有内存,并将其分配给需要它的结构,并且永远不会再调用malloc

【讨论】:

最后一次成功的分配返回 0x20004908 - 我认为这应该已经不可能了。我使用结构的原因是我从 SD 卡中读取了可变大小(100Byte 到 2kByte)的结构。【参考方案2】:

使用标准的c malloc 很难区分,而malloc 在我看来似乎有问题。因此,您可以通过使用您的 RAM 地址实现一些自定义 malloc 来管理内存。

我不确定这对你有帮助,但我在我的控制器相关项目中做了一些自定义malloc,如下所示

#define LENGTH_36_NUM   (44)
#define LENGTH_52_NUM   (26)
#define LENGTH_64_NUM   (4)
#define LENGTH_128_NUM  (5)
#define LENGTH_132_NUM  (8)
#define LENGTH_256_NUM  (8)
#define LENGTH_512_NUM  (18)    
#define LENGTH_640_NUM  (8) 
#define LENGTH_1536_NUM (6) 

#define CUS_MEM_USED        (1)
#define CUS_MEM_NO_USED     (0)

#define CALC_CNT    (0)
#define CALC_MAX    (1)

#define __Ram_Loc__         (0x20000000) ///This is my RAM address
#define __TOP_Ram_Loc__     (0x20000000 + 0x8000 -0x10) //Total 32K RAM and last 16 bytes reserved for some data storage

typedef struct _CUS_MEM_BLOCK_S 
    char used;
    int block_size;
    char *ptr;
    char *next;
 cus_mem_block_s;

static struct _MEM_INFO_TBL_S 
    int block_size;
    int num_max;
    cus_mem_block_s *wm_head;
    int calc[2];
 memInfoTbl[] = 

 36,  LENGTH_36_NUM  , 0, 0,0 ,
 52,  LENGTH_52_NUM  , 0, 0,0 ,
 64,  LENGTH_64_NUM  , 0, 0,0 ,
 128, LENGTH_128_NUM , 0, 0,0 ,
 132, LENGTH_132_NUM , 0, 0,0 ,
 256, LENGTH_256_NUM , 0, 0,0 ,
 512, LENGTH_512_NUM , 0, 0,0 ,
 640, LENGTH_640_NUM , 0, 0,0 ,
 1536,LENGTH_1536_NUM, 0, 0,0 ,
;
#define MEM_TBL_MAX     (sizeof(memInfoTbl)/sizeof(struct _MEM_INFO_TBL_S))

BOOL MemHeapHasBeenInitialised = FALSE;

这基本上是为 RAM 地址定义的宏,并为经常需要分配的块大小手动选择了更多的块号,比如 36 字节需要我更多,所以我需要更多的数字。

这是mem init的初始化函数

void cus_MemInit(void)

    int i,j;
    cus_mem_block_s *head=NULL;
    unsigned int addr;

    addr = __Ram_Loc__;

    for(i=0; i<MEM_TBL_MAX; i++) 
    
        head = (char *)addr;
        memInfoTbl[i].wm_head = head;
        for(j=0;j<memInfoTbl[i].num_max; j++)
        
            head->used =CUS_MEM_NO_USED;
            head->block_size = memInfoTbl[i].block_size;
            head->ptr = (char *)(addr + sizeof(cus_mem_block_s));
            addr += (memInfoTbl[i].block_size + sizeof(cus_mem_block_s));
            head->next =(char *)addr;
            head = head->next;
            if(head > __TOP_Ram_Loc__) 
            
                printf("%s:error.\n",__FUNCTION__);
                return;
            
        
    
    head->ptr = 0;
    head->block_size = 0;
    head->next = __Ram_Loc__;

    MemHeapHasBeenInitialised=TRUE;

这个用于分配

void* CUS_Malloc( int wantedSize )

    void *pwtReturn = NULL;
    int i;
    cus_mem_block_s *head;

    if(MemHeapHasBeenInitialised == FALSE) 
            goto done_exit;

    for(i=0; i<MEM_TBL_MAX; i++)
    
        if(wantedSize <= memInfoTbl[i].block_size)
        
            head = memInfoTbl[i].wm_head;
            while(head->ptr)
            
                if(head->used == CUS_MEM_NO_USED)
                
                    head->used = CUS_MEM_USED;
                    pwtReturn = head->ptr;
                    goto done;
                
                head = head->next;
            
            goto done;

        
    
 done:


    if(pwtReturn)
    
        for(i=0; i<MEM_TBL_MAX; i++)
        
            if(memInfoTbl[i].block_size == head->block_size)
            

                memInfoTbl[i].calc[CALC_CNT]++;
                if(memInfoTbl[i].calc[CALC_CNT] > memInfoTbl[i].calc[CALC_MAX] )
                    memInfoTbl[i].calc[CALC_MAX]=memInfoTbl[i].calc[CALC_CNT];
                break;
            
        
    
  done_exit:
    return pwtReturn;

这个是免费的

void CUS_Free(void *pm)

    cus_mem_block_s *head;
    char fault=0;


    if( (pm == NULL) || (MemHeapHasBeenInitialised == FALSE) )
        goto done;
    if( (pm < __RamAHB32__) && (pm > __TOP_Ram_Loc__) )
    
        printf("%s:over memory range\n",__FUNCTION__);
        goto done;
    

    head = pm-sizeof(cus_mem_block_s);


    if(head->used)
        head->used = CUS_MEM_NO_USED;
    else
    
        printf("%s:free error\n",__FUNCTION__);
        fault=1;
    


    if(fault)
        goto done;
    int i;
    for(i=0;i<MEM_TBL_MAX;i++)
    
        if(memInfoTbl[i].block_size == head->block_size)
        
            memInfoTbl[i].calc[CALC_CNT]--;
            goto done;
        
    
 done:;


毕竟你可以使用上面的功能

void *mem=NULL;
mem=CUS_Malloc(wantedsize);

然后也可以如下查看你使用的内存

void CUS_MemShow(void)

    int i;
    int block_size;
    int block_cnt[MEM_TBL_MAX];
    int usedSize=0, totalSize=0;
    cus_mem_block_s *head;

    if(MemHeapHasBeenInitialised == FALSE)
            return;

    memset(block_cnt, 0, sizeof(block_cnt));

    head = memInfoTbl[0].wm_head;
    i=0;
    block_size = head->block_size;
    vTaskSuspendAll();
    while( head->ptr !=0)
    
        if(head->used == CUS_MEM_USED )
        
            block_cnt[i]++;
            usedSize +=head->block_size;
        
        usedSize += sizeof(cus_mem_block_s);

        totalSize += (head->block_size+ sizeof(cus_mem_block_s));

        /* change next memory block */  
        head = head->next;
        if( block_size != head->block_size)
        
            block_size = head->block_size;
            i++;
        
    
    xTaskResumeAll();

    usedSize += sizeof(cus_mem_block_s);
    totalSize+= sizeof(cus_mem_block_s);

    dprintf("----Memory Information----\n");

    for(i=0; i<MEM_TBL_MAX; i++) 
        printf("block %d used=%d/%d (max %d)\n",
                    memInfoTbl[i].block_size, block_cnt[i], 
                    memInfoTbl[i].num_max,
                    memInfoTbl[i].calc[CALC_MAX]);
    

    printf("used memory=%d\n",usedSize);
    printf("free memory=%d\n",totalSize-usedSize);
    printf("total memory=%d\n",totalSize);
    printf("--------------------------\n");

通常先预先计算内存,然后按我的方式提供。

【讨论】:

三个问题: 1. 你能解释一下你在memInfoTbl[] 中定义的宏到底是做什么用的吗? 2. 我看不到您将堆栈放在哪里。你检查head__TOP_Ram_Loc__ 但不应该还有一些字节吗? 3. __RamAHB32__ 是干什么的?【参考方案3】:

您的程序很可能由于非法内存访问而崩溃,这几乎总是合法内存访问的间接(后续)结果,但您确实这样做了不打算表演。

例如(这也是我对您系统上发生的事情的猜测):

您的堆很可能在堆栈之后立即开始。现在,假设您在main 中有一个堆栈溢出。然后,您在main 中执行的其中一个操作(就您而言自然是合法的操作)用一些“垃圾”数据覆盖了堆的开头。

作为后续结果,下次您尝试从堆中分配内存时,指向下一个可用内存块的指针不再有效,最终导致内存访问冲突。

首先,我强烈建议您将堆栈大小从 0x200 字节增加到 0x400 字节。这通常在链接器命令文件中定义,或通过 IDE 在项目的链接器设置中定义。

如果您的项目在 IAR 上,那么您可以在 icf 文件中进行更改:

define symbol __ICFEDIT_size_cstack__ = 0x400

除此之外,我建议您在HardFault_Handler 中添加代码,以便在崩溃之前重建调用堆栈和寄存器值。这可能允许您跟踪运行时错误并找出它发生的确切位置。

在文件“startup_stm32f03xx.s”中,确保您有以下代码:

EXTERN  HardFault_Handler_C        ; this declaration is probably missing

__tx_vectors                       ; this declaration is probably there
    DCD     HardFault_Handler

然后,在同一个文件中,添加以下中断处理程序(所有其他处理程序所在的位置):

    PUBWEAK HardFault_Handler
    SECTION .text:CODE:REORDER(1)
HardFault_Handler
    TST LR, #4
    ITE EQ
    MRSEQ R0, MSP
    MRSNE R0, PSP
    B HardFault_Handler_C

然后,在文件 'stm32f03xx.c' 中,添加以下 ISR:

void HardFault_Handler_C(unsigned int* hardfault_args)

    printf("R0    = 0x%.8X\r\n",hardfault_args[0]);         
    printf("R1    = 0x%.8X\r\n",hardfault_args[1]);         
    printf("R2    = 0x%.8X\r\n",hardfault_args[2]);         
    printf("R3    = 0x%.8X\r\n",hardfault_args[3]);         
    printf("R12   = 0x%.8X\r\n",hardfault_args[4]);         
    printf("LR    = 0x%.8X\r\n",hardfault_args[5]);         
    printf("PC    = 0x%.8X\r\n",hardfault_args[6]);         
    printf("PSR   = 0x%.8X\r\n",hardfault_args[7]);         
    printf("BFAR  = 0x%.8X\r\n",*(unsigned int*)0xE000ED38);
    printf("CFSR  = 0x%.8X\r\n",*(unsigned int*)0xE000ED28);
    printf("HFSR  = 0x%.8X\r\n",*(unsigned int*)0xE000ED2C);
    printf("DFSR  = 0x%.8X\r\n",*(unsigned int*)0xE000ED30);
    printf("AFSR  = 0x%.8X\r\n",*(unsigned int*)0xE000ED3C);
    printf("SHCSR = 0x%.8X\r\n",SCB->SHCSR);                
    while (1);

如果你在执行中这个特定的Hard-Fault中断发生的时候不能使用printf,那么把上面的所有数据保存在一个全局缓冲区中,这样你就可以在到达while (1)之后查看它.

然后,请参阅http://www.keil.com/appnotes/files/apnt209.pdf 上的“Cortex-M 故障异常和寄存器”部分以了解问题,或者如果您需要进一步的帮助,请在此处发布输出。

更新:

除了以上所有内容之外,请确保正确定义了堆的基地址。它可能是在项目设置中硬编码的(通常在数据部分和堆栈之后)。但它也可以在运行时,在程序的初始化阶段确定。通常,您需要检查程序的数据段的基地址和堆栈(在构建项目后创建的映射文件中),并确保堆不与其中任何一个重叠。

我曾经遇到过将堆的基地址设置为常量地址的情况,这很好。但后来我通过向程序添加全局变量逐渐增加了数据部分的大小。堆栈位于数据部分之后,随着数据部分变大,它“向前移动”,因此其中任何一个都没有问题。但最终,堆被分配在堆栈的“顶部”部分。所以在某个时候,堆操作开始覆盖堆栈上的变量,堆栈操作开始覆盖堆的内容。

【讨论】:

您正在寻找的短语是“堆栈--堆冲突”。在现代、全方位服务的操作系统上非常罕见,但它们曾经是许多平台上的问题,并且在更受限制的环境中仍然是一个问题。 @dmckee:感谢您提供的术语。我在使用 ThreadX OS 时遇到过这个问题,它在回调函数中(即运行时)为您提供first unused memory 地址,并允许您在该地址分配堆。出现问题是因为我使用的是常量地址,假设它“足够好”。【参考方案4】:

arm-none-eabi-* 工具链 发行版包括newlib C 库。当为嵌入式系统配置 newlib 时,用户程序必须 provide an _sbrk() function 才能正常工作。

malloc() 仅依靠_sbrk() 来确定堆内存的开始位置和结束位置。对_sbrk() 的第一次调用返回堆的开始,如果所需的内存量不可用,后续调用应该返回-1,然后malloc() 将依次返回@987654330 @ 应用程序。你的_sbrk() 看起来坏了,因为它显然让你分配了比可用内存更多的内存。您应该能够修复它,使其返回-1之前堆预计与堆栈发生冲突。

【讨论】:

【参考方案5】:

如果堆太小而无法根据 berendi 先前的回答进行分配,您可以在这里找到如何“强制”malloc() 返回 NULL。我估计了堆栈的最大数量,并基于此计算出最坏情况下堆栈可以开始的地址。

#define STACK_END_ADDRESS       0x20020000
#define STACK_MAX_SIZE              0x0400
#define STACK_START_ADDRESS   (STACK_END_ADDRESS - STACK_MAX_SIZE)

void * _sbrk_r(
   struct _reent *_s_r,
   ptrdiff_t nbytes)

   char  *base;     /*  errno should be set to  ENOMEM on error */

   if (!heap_ptr)  /*  Initialize if first time through.       */
      heap_ptr = end;
   
   base = heap_ptr; /*  Point to end of heap.           */
   #ifndef STACK_START_ADDRESS
      heap_ptr += nbytes;   /*  Increase heap.              */
      return base;      /*  Return pointer to start of new heap area.   */
   #else
      /* End of heap mustn't exceed beginning of stack! */        
      if (heap_ptr <= (char *) (STACK_START_ADDRESS - nbytes) )   
         heap_ptr += nbytes;    /*  Increase heap.              */
         return base;       /*  Return pointer to start of new heap area.   */
       else 
         return (void *) -1;         /*   Return -1 means that memory run out  */
      
   #endif // STACK_START_ADDRESS

【讨论】:

以上是关于嵌入式系统上的 malloc 行为的主要内容,如果未能解决你的问题,请参考以下文章

用于嵌入式系统的小型 libc [关闭]

第5章 嵌入式系统开发与维护知识 5.1

第5章 嵌入式系统开发与维护知识 5.1

RTX——第18章 内存管理

嵌入式系统上的 SQLite

嵌入式 linux 系统上的 zeroconf 实现