arm 堆栈

来源:互联网 发布:淘宝订单改地址 编辑:程序博客网 时间:2024/05/25 05:37

先转一篇

http://blog.csdn.net/slj_win/article/details/16906141

关于堆和栈已经是程序员的一个月经话题,大部分有是基于os层来聊的。

那么,在赤裸裸的单片机下的堆和栈是什么样的分布呢?以下是网摘:

刚接手STM32时,你只编写一个

int main()

{

while(1);

}

BUILD://Program Size: Code=340 RO-data=252 RW-data=0 ZI-data=1632 

编译后,就会发现这么个程序已用了1600多的RAM,要是在51单片机上,会心疼死了,这1600多的RAM跑哪儿去了,

分析map,你会发现是堆和栈占用的,在startup_stm32f10x_md.s文件中,它的前面几行就有以上定义,

这下该明白了吧。

 

Stack_Size   EQU   0x00000400

Heap_Size   EQU   0x00000200

 

以下引用网上资料 理解堆和栈的区别

(1)栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等,其操作方式类似

于数据结构中的栈。

(2)堆区(heap):一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。分配

方式类似于数据结构中的链表。

(3)全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态

变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系

统自动释放。

(4)文字常量区:常量字符串就是存放在这里的。

(5)程序代码区:存放函数体的二进制代码。

例如:

int a=0;   //全局初始化区

char *p1;   //全局未初始化区

main()

{

int b;   //栈

char s[]="abc";   //栈

char *p3= "1234567";   //在文字常量区Flash

static int c =0 ;   //静态初始化区

p1= (char *)malloc(10);   //堆区

strcpy(p1,"123456");   //"123456"放在常量区

}

所以堆和栈的区别:

stack的空间由操作系统自动分配/释放,heap上的空间手动分配/释放。

stack的空间有限,heap是很大的自由存储区。

程序在编译期和函数分配内存都是在栈上进行,且程序运行中函数调用时参数的传递也是在栈上进行。

 

------------------------------------------------------------------------------------------------------

1.堆和栈大小

定义大小在startup_stm32f2xx.s

Stack_Size  EQU  0x00000400

AREA  STACK, NOINIT, READWRITE, ALIGN=3 
Stack_Mem  SPACE  Stack_Size 
__initial_sp

; Heap Configuration 
;  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8> 
;

Heap_Size  EQU  0x00000200

AREA  HEAP, NOINIT, READWRITE, ALIGN=3 
__heap_base

2.堆和栈位置

通过MAP文件可知

HEAP  0x200106f8  Section  512  startup_stm32f2xx.o(HEAP) 
STACK  0x200108f8  Section  1024  startup_stm32f2xx.o(STACK)

__heap_base  0x200106f8  Data  0  startup_stm32f2xx.o(HEAP) 
__heap_limit  0x200108f8  Data  0  startup_stm32f2xx.o(HEAP) 
__initial_sp  0x20010cf8  Data  0  startup_stm32f2xx.o(STACK)

显然 Cortex-m3资料可知:__initial_sp是堆栈指针,它就是FLASH的0x8000000地址前面4个字节(它根据堆栈大小,由编译器自动生成)

显然堆和栈是相邻的。

image

3.堆和栈空间分配

栈:向低地址扩展

堆:向高地址扩展

显然如果依次定义变量

先定义的栈变量的内存地址比后定义的栈变量的内存地址要大

先定义的堆变量的内存地址比后定义的堆变量的内存地址要小

4.堆和栈变量

栈:临时变量,退出该作用域就会自动释放

堆:malloc变量,通过free函数释放

另外:堆栈溢出,编译不会提示,需要注意

 

------------------------------------------------------------------------------------------------------

 

如果使用了HEAP,则必须设置HEAP大小。 
如果是STACK,可以设置为0,不影响程序运行。 
IAR STM8定义STACK,是预先在RAM尾端分配一个字节的区域作为堆栈预留区域。 
当程序静态变量,全局变量,或者堆与预留堆栈区域有冲突,编译器连接的时候就会报错。 
你可以吧STACK设置为0,并不影响运行。(会影响调试,调试会报堆栈溢出警告)。 
其实没必要这么做。 
一般程序,(在允许范围内)设置多少STACK,并不影响程序真实使用的RAM大小, 
(可以试验,把STACK设置多少,编译出来的HEX文件都是一样), 
程序还是按照它原本的状态使用RAM,把STACK设置为0,并不是真实地减少RAM使用。 
仅仅是欺骗一下编译器,让程序表面上看起来少用了RAM。 
而设置一定size的STACK,也并不是真的就多使用了RAM,只是让编译器帮你 
检查一下,是否能够保证有size大小的RAM没有被占用,可以用来作为堆栈。 
以上仅针对IAR STM8.

 

------------------------------------------------------------------------------------------------------

 

从以上网摘来看单片机的堆和栈是分配在RAM里的,有可能是内部也有可能是外部,可以读写;

 

:存函数的临时变量,即局部变量,函数返回时随时有可能被其他函数栈用。所以栈是一种分时轮流使用的存储区,

      编译器里定义的Stack_Size,是为了限定函数的局部数据活动的范围,操过这么范围有可以跑飞,也就是栈溢出;

     Stack_Size不影响Hex,更不影响Hex怎么运行的,只是在Debug调试时会提示错。栈溢出也有是超过了国界进行

     活动,只要老外没有意见,你可以接着玩,有老外不让你玩,你就的得死,或是大家都死(互相撕杀),有的人写

    单片机代码在函数里定义一个大数组 int buf[8192],栈要是小于8192是会死的很惨。

 

:存的是全局变量,这变量理论上是所有函数都可以访问的,全局变量有的有初始值,但这个值不是存在RAM里的,是

     存在Hex里,下载到Flash里,上电由代码(编译器生成的汇编代码)搬过去的。有的人很“霸道”,上电就霸占已一块很

    大的RAM(Heap_Size),作为己有(malloc_init),别人用只能通过他们管家借(malloc),用完还得换(free)。所以  

    一旦有“霸道”的人出现是编译器里必须定义Heap_Size,否则和他管家借也没有用。

 

总之:堆和栈有存在RAM里,他两各分多少看函数需求,但是他两的总值不能超过单片机硬件的实际RAM尺寸,否则只能

     到海里玩(淹死了)或是自己打造船接着玩(外扩RAM)。

===========================分界线============


下面是本博主的理解了。
几年前我理解51单片机的内存感觉弄得挺懂得。甚至对于简单的程序能够大概猜到内存分配的位置。这需要你对编译器的内存分配机制非常了解,比如对各种指针的优化所占的内存,所以说一般来讲编译器优化级别为0时候更容易猜出内存分配的具体地址。说起51的内存大概是先是通用寄存器组,然后是堆栈(这里就不深究堆栈的具体含义了)然后好像是几个bit的组合,然后到了栈顶。这样运行程序的时候就进栈出栈
后来学了STM32,一直感觉堆栈弄得不太清楚,因为没用上OS,感觉没必要弄懂,就把STM32当成51用了。今天也不知道咋了,突然想知道栈指针在程序中的具体位置能不能捕捉到?这样我就知道程序运行有没有溢出啥的。

找了一会,发现在corte_cm3.C里面有__get_MSP()这个函数。这样就知道栈的位置了。

UINT32 value = 0;

value = __get_MSP();

函数内部是

/**
 * @brief  Return the Process Stack Pointer
 *
 * @return ProcessStackPointer
 *
 * Return the actual process stack pointer
 */
__ASM uint32_t __get_PSP(void)
{
  mrs r0, psp
  bx lr
}

当然我不会贸然调用这个函数,不知道当运行到  mrs r0, psp这句结束后,是否会被中断打断。C语言中用汇编我向来是及其谨慎的。当然这是在ARMCC编译器中的函数写法,在GCC编译器是别的写法,如下:


/**
 * @brief  Set the Process Stack Pointer
 *
 * @param  topOfProcStack  Process Stack Pointer
 *
 * Assign the value ProcessStackPointer to the MSP 
 * (process stack pointer) Cortex processor register
 */
void __set_PSP(uint32_t topOfProcStack) __attribute__( ( naked ) );
void __set_PSP(uint32_t topOfProcStack)
{
  __ASM volatile ("MSR psp, %0\n\t"
                  "BX  lr     \n\t" : : "r" (topOfProcStack) );
}


然后我就再一次看到了启动代码。

事实上我现在才知道一般的裸奔(非操作系统)程序,在MDK的启动代码中,将堆和栈都设置为0都是可以运行的。

就是如下:

Stack_Size      EQU     0x00000000

Heap_Size       EQU     0x00000000

顺便插一句,注意一下字节对齐,如下:

Stack_Size      EQU     0x00000000


                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

事实上也是可以用C语言对齐的。或者在C语言中用关键字。

这个armcc编译器有个特点,和当年keil的51编译器不一样就是,在armcc编译器中你定义了占用内存的变量,比如全局变量,那么如果你程序中没有函数来调用这个变量那么就不分配内存。所以这样就会发生无论你把启动代码中的heap设置为多大,你在map文件中没有发现启动文件中提到的heap,如果你没有把你定义的内存的地址指向启动代码中的heap的话。[准确说应该heap是在启动代码中定义的一个全局变量,留给外面的C代码进行调用]。


事实上__get_MSP()的值也可以在debug的时候看到就在MDK调试界面的Registers的Banked下面有两个寄存器MSP和PSP.


好,下面写段代码


void hello(void)
{
Cunsigned char aa[0x300];
aa[0]++;
}

int main(void) {

while(1) {

hello();

}

}

第一步:

在进入hello函数时候,如下


void hello(void)
{                                                      //<<=============黄色三角指向这里
UINT8  aa[0x300];
aa[0]++;
}

MDP值是200019D8

看一下反汇编,

注意这种情况下一定要单步运行才能看的清楚,


0x0800141A D1FB      BNE      0x08001414
0x0800141C 4770      BX       lr
    62: { 
    63:         UINT8  aa[0x300]; 
0x0800141E F5AD7D40  SUB      sp,SP,#0x300/<<=============黄色三角指向这里
    64:         aa[0]++; 
0x08001422 F89D0000  LDRB     r0,[sp,#0x00]
0x08001426 1C40      ADDS     r0,r0,#1


第二步:

再运行一个step oneline,则如下:

void hello(void)
{                                                      
UINT8  aa[0x300];
aa[0]++;//<<=============黄色三角指向这里
}

看一下界面的的MDP值是200016D8

0x0800141A D1FB      BNE      0x08001414
0x0800141C 4770      BX       lr
    62: { 
    63:         UINT8  aa[0x300]; 
0x0800141E F5AD7D40  SUB      sp,SP,#0x300
    64:         aa[0]++; 
0x08001422 F89D0000  LDRB     r0,[sp,#0x00]//<<=============黄色三角指向这里
0x08001426 1C40      ADDS     r0,r0,#1

第三步:step oneline 然后再steponeline

就跳出了hello函数发现界面MDP值是200019D8


通过以上看出来了,临时变量aa在栈上面进出的过程。

并且发现MDP的值变小又变大。

===========================================

常规来讲OS应该这样弄


最低地址是固定内存,如全局变量和static变量

然后是栈空间

然后是堆空间。

为什么ST公司的头文件要弄个堆的extern形式。

我现在觉得这个也挺有用的。我本来是想在RAM的最后开辟一个空间的。但是这样st的不同系列的单片机需要改不同的地址和空间大小,挺麻烦。这样我就可以在启动代码里面改这个空间大小了。而堆得起始地址直接就是分配完栈的空间紧挨的地址。

在来个思维跳跃,在启动代码的最后有:

                 IF      :DEF:__MICROLIB
                
                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit
                
                 ELSE
                
                 IMPORT  __use_two_region_memory
                 EXPORT  __user_initial_stackheap

写到如果定义了microlib这个要注意。因为在options for targets有个 use MicroCB选项。

好现在谈正事:

extern uint32  __heap_base;
extern uint32  __heap_limit;

uint32  * P; //这里不分析这个
uint8  *  P1;
uint32  *  P2;

int main (void){

P =(uint32 *) __heap_base;//这里不分析这个
P1 = (uint8 *) &__heap_base;
        P2 = (uint32 *) &__heap_limit;

while(1);

}

运行完P2这句,发现MDK界面的MSP的值是0x20001B20

而P1和P2这两个地址也是0x20001B20。地址里面的值是几?当然是0啦。好像我以前博客讲过在启动代码里面调用了C函数将内存清零了。嗯 肯定是在启动代码里面调用的,是不是C函数忘记了,反正感觉是清零的比较隐蔽。

而51单片机的汇编启动代码可以清楚看到启动代码里面的汇编清零了RAM。

接着上面继续讲,就可以将P1或者P2作为堆得起始地址了。因为不同的存储器大小的STM32系列单片机有不同的启动文件,所以这样就改启动代码来配置堆的大小了。

但是我看像freertos代码把堆直接放到了固定内存区域也行。


===============================分界线=============

再转一篇

http://blog.csdn.net/love_gaohz/article/details/49276769

引言

  这篇文章简要说说函数是怎么传入参数的,我们都知道,当一个函数调用使用少量参数(ARM上是少于等于4个)时,参数是通过寄存器进行传值(ARM上是通过r0,r1,r2,r3),而当参数多于4个时,会将多出的参数压入栈中进行传递(其实在函数调用过程中也会把r0,r1,r2,r3传递的参数压入栈),具体是什么实现的呢,我们看看。

 

函数栈

  首先我们需要了解一下linux下一个进程的内存地址空间是如何布局的,在linux中,0~3G的虚拟地址为进程所有,3G~4G由内核所使用,每一个进程都有自己独立的0~3G内存地址空间。当进程进行函数调用时,我们都知道传入被调用函数的参数是通过栈进行操作的,这里我们只需要简单了解一下linux的内存地址空间中的栈是自顶向下生长的,就是栈底出于高地址处,栈顶出于低地址处。


  好的,简单了解了内存地址空间的栈后,我们还需要简单了解一下EBPESP这两个寄存器,EBP是用保存栈低地址的,而ESP用于保存栈顶地址,而每一次函数调用会涉及到一个栈帧,栈帧结构如下图



举个实例详细说明一下一个函数帧的特点,比如

  1. /* B被A调用
  2.  * 参数:data1, data2, data3
  3.  * 局部变量: s1, s2, s3 */
  4. void B (int data1, int data2, int data3)
  5. {
  6.     int b_s1;
  7.     int b_s2;
  8.     int b_s3;
  9. }
  10.  
  11. /* A调用B函数 */
  12. void A (void)
  13. {
  14.     int a_s1;
  15.     int a_s2;
  16.     int a_s3;
  17.     
  18.     B (1, 2, 3);
  19.     printf ("1\n");
  20. }

在以上例子中栈帧情况应该如下图所示


  从图例中可以看出,当A函数没有调用B函数时,A函数的栈帧只保存着局部变量,而EBP(栈底指针)指向的是A函数的函数栈帧头,而当A函数调用B函数时,A函数会将B函数所需要的参数从右往左压入栈(在例子中先压入3,之后是2,最后是1),之后会将A调用完B之后所需要运行的第一条指令压入栈,此时建立一个B的栈帧,具体流程:

  • 从右往左将B函数所需参数压入栈
  • 压入执行完B函数之后的第一条指令地址
  • 建立B栈帧
  • 压入A栈帧的栈底
  • 压入B函数保护的寄存器
  • 压入B函数的局部变量

 

小结

   其实每一种处理器架构所使用的方式都不一样,在arm上我几个参数和不定参数的情况通过汇编代码查看又不相同,之后反汇编后研究透了会再发布一篇博文专门说这个,现在这篇就当做一个入门知识吧。



====================分界线=====================

博主再添加:

如果MDK 用了#include <stdlib.h>

就是用调用了malloc则map文件如下:

    SystemCoreClock                          0x20000014   Data           4  system_stm32f10x.o(.data)
    AHBPrescTable                            0x20000018   Data          16  system_stm32f10x.o(.data)

    __microlib_freelist                      0x200006ec   Data           4  mvars.o(.data)
    __microlib_freelist_initialised          0x200006f0   Data           4  mvars.o(.data)
    __heap_base                              0x20005530   Data           0  startup_stm32f10x_xl.o(HEAP)
    __heap_limit                             0x20005930   Data           0  startup_stm32f10x_xl.o(HEAP)
    __initial_sp                             0x20006130   Data           0  startup_stm32f10x_xl.o(STACK)


若没调用则如下:


    SystemCoreClock                          0x20000014   Data           4  system_stm32f10x.o(.data)
    AHBPrescTable                            0x20000018   Data          16  system_stm32f10x.o(.data)

    __initial_sp                             0x20005d28   Data           0  startup_stm32f10x_xl.o(STACK)



原创粉丝点击