嵌入式系统中的malloc行为

33

我目前正在进行一个嵌入式项目(STM32F103RB,CooCox CoIDE v.1.7.6和arm-none-eabi-gcc 4.8 2013q4),并且我正在尝试理解当RAM已满时,malloc()在纯C上的行为。

我的STM32具有20kB = 0x5000字节的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
我没有任何答案,但感谢您提出一个有趣且表达清晰的问题。(+1) - NPE
1
你使用过uClibc吗? - Michael Foukarakis
@ThoAppelsin 我在 curr = (item *)malloc(sizeof(item)); 后添加了你的代码 - 结果:程序不再在第18次循环分配内存时崩溃,而是在第17次循环的 memset() 行崩溃。 - Boern
@BernhardSchlegel 好的,我从未像你那样在资源匮乏的环境下工作过。我可能不知道确切的原因,但似乎你没有触及的内存位置被视为可以借出的自由空间。在某种程度上,这类似于现代银行的运作方式... - Utkan Gezer
1
@BernhardSchlegel uClibc是标准C库的一个特定实现,您可以使用调试符号编译C库,然后使用调试器逐步进入malloc,以查看哪一行导致调用硬错误处理程序。您可以使用带有不同C库实现的GCC,因此说您使用GCC并不能真正说明您使用哪个C库实现。我们只能假设您使用默认的C库实现。 - Étienne
显示剩余7条评论
5个回答

29

看起来malloc并没有进行任何检查。你得到的故障是由硬件检测到对无效地址的写入造成的,这可能是来自 malloc 本身。

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

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

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

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

一般来说,在嵌入式硬件中,当空间仅限于几十千字节时,你要避免在“任意”位置使用malloc调用。相反,您需要使用预先计算出的限制预先分配所有内存,并将其分配给需要它的结构,从而不再调用malloc


最后一个成功的分配返回0x20004908-我相信这应该已经不可能了。我使用结构体的原因是我从SD卡中读取具有可变大小(100字节到2k字节)的结构。 - Boern

6
您的程序很可能因为“非法内存访问”而崩溃,这几乎总是由于“合法内存访问”的间接(后续)结果造成的,但您并没有执行意图。例如(这也是我猜测您系统上发生的情况):堆栈很可能位于堆的右侧。假设您在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' 中添加以下中断服务程序:

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);
}

如果在发生特定的硬件故障中断时不能使用printf,则将所有上述数据保存在全局缓冲区中,以便在到达while(1)后查看它们。然后,请参考http://www.keil.com/appnotes/files/apnt209.pdf中的“Cortex-M Fault Exceptions and Registers”部分以了解问题,或者如果您需要进一步的帮助,请在此处发布输出。 更新: 除了上述所有内容之外,还要确保堆的基地址被正确定义。它可能是在项目设置中硬编码的(通常在数据段和堆栈之后)。但它也可以在程序初始化阶段动态确定。一般来说,您需要检查程序的数据段和堆栈的基地址(在构建项目后创建的映射文件中),并确保堆不会与它们重叠。
我曾经遇到过这样一种情况,堆的基地址被设置为一个常量地址,这一开始是可以的。但是随着将全局变量添加到程序中,逐渐增加了数据段的大小。堆栈位于数据段的右侧,随着数据段的增大而“向前移动”,因此它们都没有问题。但最终,堆被分配在堆栈的一部分之上。因此,在某个时刻,堆操作开始覆盖堆栈上的变量,而堆栈操作则开始覆盖堆的内容。

2
你要找的短语是“堆栈冲突”。在现代全功能操作系统中,这是非常罕见的情况,但在许多平台上它们曾经是一个问题,并且在更受限制的环境中仍然存在问题。 - dmckee --- ex-moderator kitten
@dmckee:感谢您提供这个术语。我在使用ThreadX操作系统时遇到了这个问题,它会在回调函数中(即在运行时)给出“第一个未使用的内存”地址,并允许您在该地址上分配堆。问题出现是因为我使用了一个常量地址,假设它足够好,但实际上并不是这样。 - barak manos

5
arm-none-eabi-* 工具链包括 newlib C 库。当 newlib 配置为嵌入式系统时,用户程序必须提供一个 _sbrk() 函数 以使其正常工作。 malloc() 仅仅依赖于 _sbrk() 来确定堆内存的开始和结束位置。第一次调用 _sbrk() 返回堆的起始位置,如果所需内存量不可用,后续调用应该返回 -1,然后 malloc() 会返回 NULL 给应用程序。您的 _sbrk() 看起来有问题,因为它可以让您分配比可用内存更多的内存。您应该能够修复它,使其在堆预计与栈冲突之前返回 -1

3

使用标准的c malloc函数很难区分,从我的视角来看,malloc函数似乎有缺陷。因此,您可以通过实现一些自定义的malloc函数来管理内存,使用RAM地址。

我不确定这是否对您有所帮助,但我在我的控制器相关项目中完成了一些自定义的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
三个问题:1. 你能解释一下你的宏在 memInfoTbl[] 中到底定义了什么吗?2. 我没有看到你放置堆栈的地方。你将 head__TOP_Ram_Loc__ 进行了比较,但是不应该还有一些字节吗?3. __RamAHB32__ 是用来干什么的? - Boern

1

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

#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
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接