单片机堆栈是什么?
在片内RAM中,常常要指定一个专门的区域来存放某些特别的数据,它遵循顺序存取和后进先出(LIFO/FILO)的原则,这个RAM区叫堆栈。
单片机堆栈有什么用?
1)子程序调用和中断服务时CPU自动将当前PC
值压栈保存,返回时自动将PC值弹栈。
2)保护现场/恢复现场
3)数据传输
单片机堆栈原理:
堆栈区由特殊功能寄存器堆栈指针SP管理 堆栈区可以安排在 RAM区任意位置,一般不安排在工作寄存器区和可按位寻址的RAM区,通常放在RAM区的靠后的位置。
堆栈区由特殊功能寄存器堆栈指针SP管理 堆栈区可以安排在 RAM区任意位置,一般不安排在工作寄存器区和可按位寻址的RAM区,通常放在RAM区的靠后的位置。
堆栈区由特殊功能寄存器堆栈指针SP管理 堆栈区可以安排在 RAM区任意位置,一般不安排在工作寄存器区和可按位寻址的RAM区,通常放在RAM区的靠后的位置。
我认为单片机堆栈溢出最重要的原因是我们编程序有问题,即在程序设计初期没有留出足够的空间供堆栈使用,堆栈一旦溢出程序一般会乱指,就是我们所说的程序跑分。一般我们不容许出现这个现象,因此我们在设计程序的时候首先要在内部RAM里开辟一段连续的地址当堆栈使用,且只能让它通过PUSH和POP指令进行访问,而且每次访问堆栈后注意别让堆栈溢出。
个人理解,呵呵如有错误或者不明白的地方,我们再一起交流共同进步。
全局变量太多是不是也会导致堆栈空间不够,
uchar xdata store_lc[18];
uchar xdata a_add=0,b_add=0,c_add=0,d_add=0,e_add=0,f_add=0;
刚开始没加这些变量之前一切都很正常(这些变量都是用于中间变量存储的),加上之后有时候会突然复位?
我用的是汇编,觉着有可能是你在添加这些变量的时候,堆栈区和工作寄存器区重叠了或者堆栈区占用了你程序已经使用的地址,导致原有数据被覆盖了,仔细看看有没有这种情况
一、局部变量不要使用太多,尤其是局部大数组,最是杀堆栈的;
二、函数调用纵深不要太大;
三、尽量给堆栈留有余地;
下面一段话是看本论坛上的一位大侠说的:
MSP的位置 = 全局变量数+HEAP_SIZE+Stack_Size。
KEIL编译器对RAM的排列方式(地址由低到高):
1、全局变量(包括已初始化的变量和没初始化的变量);
2、Heap所占的空间;
3、主Stack所占空间;
首先,我认为这段话肯定是对的,应该没有问题,,,
可是,我不明白的是:编译器根据下面两段代码,就能知道我定义的存储区是堆栈吗???
Stack_Size EQU 0x0200;
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
Heap_Size EQU 0x000;
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
SPACE伪指令是汇编语言中用于定义内存空间的,可是,编译器怎么知道我定义的Stack_Mem存储区是栈空间呢???难道是根据这个存储区的名字叫:Stack_Mem来判断的吗???
发完帖子就发现,自己问了一个愚蠢的问题:
AREA STACK, NOINIT, READWRITE, ALIGN=3
AREA HEAP, NOINIT, READWRITE, ALIGN=3
这个问题一点也不愚蠢啊,反倒是红色的两个名字会愚弄人呢!是时候揭开它们的假面具了。
STACK, HEAP只是两个section的名字。程序就是由各种代码和数据section组成的,最后由链接器排开并生成一个映像文件。这两个名字没有特殊的法力,很有欺骗性啊。不信的话,你把它改成你和你女朋友的名字也可以的!
不是因为名字叫"STACK", "HEAP"就人如其名了。事实上,关键的一句是
__initial_sp (顶格写的,而且必须顶格写)
这个标号,它最终会被链接器解析成这个名为STACK的section结束后的地址。往下看向量表的定义,第一条就是
__Vectors DCD __initial_sp
这才是确定了MSP的初始值为__initial_sp
堆也是一样,"HEAP"这个名字并不能赋予这个区神奇的力量,而是看前呼后拥的"__heap_base"和"__heap_limit",它们这两个标号最后变成两个地址,可想而知,就是堆的起止范围!
在启动文件的最后有一个汇编例程,用于和C语言库对接,才初始化了HEAP。
说句题外话,在MDK中的确有些名字是可以有特殊功能的,比如
AREA |.ARM.__at_0x02FC|, CODE, READONLY
这里的"|.ARM.__at_0x02FC|"也是个名字,不过它指定了地址。这是MDK的特有功能。不过,往往成事不足败事有余,建议还是不要招惹它。
首先,非常感想您的回复,您说的都是对的,我验证过,把section的名字从STACK改为其他的字符串,堆栈的分配照样进行,并且是正确的。。也就是,如您所说:STACK只是一个很简单的section name,没有什么特殊的含义或是神奇的力量,HEAP也一样。。
但是,我想说的是:您回答的问题,并不是我问的问题,,,
我问的问题是,我在startup.s文件中写下了如下代码:
Stack_Size EQU 0x0200;
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
很显然,编译器知道我要定义一个512字节的Section,此Section的名字叫STACK,并且应该把这个Section放到RAM里面去。。。
可是,链接器并不知道我定义的这个Section是用来当堆栈用的啊,至少我没告诉连接器或是编译器,这个Section是用来当堆栈使用的;或者说:我不知道我是怎么告诉链接器的。。。
既然连接器不知道这个Section是用来当堆栈使用的。。那么,连接器怎么知道一定要把这个Section放到所有全局变量的后面去呢???连接器完全可以把这个Section随便放一个地方啊,比如,从0x20000000处分配一块512字节的RAM内存给名字叫做STACK的Section。。。可是,我们知道,连接器没有这么做,它总是把这个Section放到所有的全局变量后面去,显然,这是因为连接器知道上面的代码定义的那个Section是用来当栈使用的,,
可是,我是怎么在代码里面告诉连接器,名字叫STACK的Section是要用来当堆栈的呢???
仔细看看上面那一堆的代码,既然STACK只是一个名字,一个很普通的名字,那么好像有点特殊的就只剩下“__initial_sp”了,
再看下面的一段代码:
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
“__initial_sp”竟然需要EXPORT,也就是说:编译器或者库函数可以使用这个“字符串”,看来这个东西确实比较特殊。。。。
我在Keil的Help里面查找了一下__initial_sp,果然查到了不少东西,可是我现在看不太懂帮助里面说的都是些什么,我还在努力的看,,,感觉应该就是__initial_sp 使得连接器知道,前面定义的那个RAM中的Section是用来当作堆栈用的,,,
“
One of several methods you can use to specify the initial stack pointer and heap bounds is to define the following symbols:
__initial_sp
__heap_base
__heap_limit.
”
“
The C library requires you to specify where the stack pointer begins.
”
仔细研读这几句话,我认为,他们的意思是说:我们需要定义几个符号来把堆栈的地址和边界告诉库函数!!!
所以,您这段话依然不是回答我问题的答案。。。
我具体问的问题是什么,您可以看一下6楼的帖子。。。。
简单来说:连接器是怎么知道要把名字叫做STACK的Section放到所有全局变量的后面去的,难道连接器知道这个Section是要当作栈存储区来使用的???
那链接器又是怎么知道的呢???
下面是MDK下的一个完整的Scatter文件的内容:
LR_IROM1 0x00000000 0x00004000 { ; load region size_region
ER_IROM1 0x00000000 0x00004000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00000800 { ; RW data
.ANY (+RW +ZI)
}
}
很显然,这里面并没有特别指明堆、栈的相关的任何信息。。。
没有指明的话,那么就是按照某种链接器默认的顺序进行排列。比如.o文件的名称,比如出现在linker参数列表中的顺序,etc。
Linker不会对Section的名字进行任何特殊处理。
不知是谁强加给了你这样的概念,好像链接器与C语言要有某种默契似的。事实上,链接器并不关心哪段地址要做栈,也并没有什么告诉链接器”我是栈,我要特殊照顾“的代码。或者你也可以理解为链接器不知道栈是何许人也。链接器只是要把各个输入sections定位并产生映像文件,栈和堆都是普通的sections,在链接器看来一视同仁,与汇编代码中定义的其它AREAs,以及C语言中的全局变量,静态全局变量,静态局部变量都是平起平坐。
栈空间不一定要放在全局变量后头,就算碰巧被链接器放在后面也是巧合,也可能因为它在.s文件中定义,刚好.s文件被最后处理。不信的话,你可以仿照栈区的定义方式再定义个"Stack2"什么的,靠在栈区的后面定义,然后只要在程序中引用它以避免它被当作死代码而被链接器删除,你可以在生成的.map文件中看到它的位置是在STACK后面。
全局变量是C语言中的术语,除此之外,还有静态全局变量,静态局部变量,与在链接器看来全局变量有相同的身份
不知“MSP的位置 = 全局变量数+HEAP_SIZE+Stack_Size”这句话是在什么上下文里说出来的,不能断章取义地了解,有可能它刚好在解释一个程序中安排栈区的方式而已。
补充一句,你也可以把栈空间理解成一个全局数组变量,如果按C语言的说法的话,你完全可以在C语言中以
"extern unsigned int Stack_Mem[];"
的方式定义并直接操作栈区,就如同它只是在C中定义的全局数组变量一般。
当然,这只是为了说明问题,千万不要随意写它,否则可能破坏栈中的内容,造成不可预知的错误!
栈和堆都是普通的sections,在链接器看来一视同仁,与汇编代码中定义的其它AREAs,以及C语言中的全局变量,静态全局变量,静态局部变量都是平起平坐。栈空间不一定要放在全局变量后头,就算碰巧被链接器放在后面也是巧合"
您这个观点我实在不敢同意。我记得我看过一篇文章,这篇文章的大致意思是:
在ARM中:堆是向上增长的,而栈是向下增长的,之所以把栈放在内存的最后面,把堆放在内存的倒数第二位置。那是有科学依据的:
比如:在栈向下增长溢出的时候,如果程序里面恰好堆分配的使用的空间比较少而没有增长到堆的最顶端(这种情况应该是非常非常常见的,因为谁都不会把堆分配的正好够程序用,肯定都是有盈余的),那么,栈多占用的RAM空间也不会覆盖到全局变量或者堆中的内容,从而,即使溢出也不会使得程序崩溃,从而增加程序的健壮性。。。
而像您说的:堆栈就当作普通的Secton进行处理,可以放在RAM里面的任何位置,这个观点我还是第一次见到。。。我感觉,您这个观点十有八九是错的,当然,我也不是特别懂,希望高人指点一下。。。。
以前有些AVR和8/16位机的编译器的确是这样做的,它们有固定的内存划分策略。不过MDK中不这样。比如,你可以看它在构建成功时统计的内存使用量,这是把栈空间算在里面的。未用到的内存不会用于当栈溢出时的备用。栈指针的起始地址也不是内存末尾,更何况有些单片机还有一止一块内存,彼此地址并不连续。
如果确实难以置信,只要再在栈区定义的后面定义个类似的AREA,并且在生成的map文件中查看就可以验证了。
另外,如果是有操作系统的,每个任务都有自己的栈,更谈不上把栈放在最后,以便溢出时还有得救这种想法了。
我做了实验,发现我用的MDK版本,连接器是按名字的字母顺序排序的!
比如,如果依照Stack的定义方式再定义一个名为"RSec"的AREA(汇编中的AREA到了连接器中就是section),它会被放在Stack的前面。如果名字是"TSec",它就会被放在Stack的后面。
注意,为了防止连接器把新定义的AREA当作无用区删掉,可以在向量表中找一个没有用到的向量,把新定义的section里面的名字填到一个向量中。
例如,
AREA TSec, NOINIT, READWRITE, ALIGN=3
TSec_Mem SPACE 128
然后找一个保留的向量,改为
DCD TSec_Mem
构建它,然后查看map文件,可以发现类似
STACK 0x10003510 Section 2816 startup_lpc177x_8x.o(STACK)
TSec 0x10004010 Section 128 startup_lpc177x_8x.o(TSec)
TSec_Mem 0x10004010 Data 128 startup_lpc177x_8x.o(TSec)
__initial_sp 0x10004010 Data 0 startup_lpc177x_8x.o(STACK)
的文字。在这里,显然栈是放在了TSec的前面
说句题外话,你甚至可以在连接器控制文件(.sct文件)中按自己的意志指定每个section的位置
多谢大侠的耐心指教,我试了一下,果然如大侠所说,我把HEAP的section name改成了THEAP,结果堆被放在了栈的后面去了,,,
看来,链接器真的不会对堆栈Section特殊照顾。。。
| 其实 是按照数据空间的名字进行排序的。 全局变量的默认区域是.data段 或者.bbs段 注意有一个句点。 剩下的才是 Statck heap等。 是按照 首字母的顺便安排的。 |
为什么既要有“堆”又要有“栈”呢?
它们有何区别?
堆是用来申请内存空间用的。例如malloc函数free函数。
栈是调用函数用的。
不过我们一般直接使用全局变量,不会申请内存。所以你不用了解堆的概念。
你要是会dos 编程就更加明白堆和栈的概念。
但是为什么程序里还要写“堆”的部分呢
| 这是C语言运行环境要求的。必须要有堆的空间。 因为C语言环境要防止万一有人使用malloc函数。 一些习惯在pc上编程的人,就习惯使用malloc函数。 一些只在单片机上玩的人,反倒不习惯使用malloc函数,因为单片机的ram空间有限。 但现在的arm 似乎又不在乎ram空间了,呵呵 |
| 很多8/16位单片机的编译器,尤其是AVR的一些编译器,的确是按楼主的说法排布内存的,那是因为它们太专用了,为特定的某款型号生成代码。而ARM只是架构,有太多半导体公司生产ARM芯片,编译器不可能再作类似的假设,而是按一般的计算机来处理 |