一 ≠ 汇编不需要堆栈
汇编中一般不初始化,也就是直接使用系统的堆栈而已,自己定义堆栈还是要初始化的。
之前看了很多关于uboot的分析,其中就有说要为C语言的运行,准备好堆栈。
而自己在Uboot的start.S汇编代码中,关于系统初始化,也看到有堆栈指针初始化这个动作。但是,从来只是看到有人说系统初始化要初始化堆栈,即正确给堆栈指针sp赋值,但是却从来没有看到有人解释,为何要初始化堆栈。所以,接下来的内容,就是经过一定的探究,试图来解释一下,为何要初始化堆栈,即:
1 为何C语言的函数调用要用到堆栈,而汇编却不需要初始化堆栈。
差不多可以这样理解
从纯粹的 C的语法上来说,是没有办法初始化堆栈的。
例如 标准C 语言,就没有提供直接操作硬件的支持
汇编写的程序运行时也需要栈,操作系统加载可执行文件后每个线程都有自己的局部栈。理论上完全不用栈也是可以的,只要有数据区用于保存临时数据就可以。
JAVA 字节码在JVM上执行 标准的JVM线程模型中 操作数,局部变量,动态链接 返回地址 等也都是放在私有线程栈中 ,上层语言必然是基于底层架构。所以很多原因甚至系统都有很多相似之处 。
2 解堆栈的作用。
总的来说,堆栈的作用就是:保存现场/上下文,传递参数。
保存现场,也叫保存上下文。
侠义上,上下文是针对中断来体现其具体含义的,在内核设计者的眼中,当一个任务在中断时,CPU会去执行中断对应的任务。中断结束后,再执行之前的task时,原有任务的相关数据(在处理原任务所需要的数据)需要保存下来,否则无法继续执行原有任务。如果把相关数据记录到一个变量里。那这个变量就可以称为原task的上下文了。
ARM架构中
一般情况下,一个程序编译后本质上都是至少由 bss段、data段、text段三个组成的
汇编
.bss段:(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称.bss段属于静态内存分配。
.data段 :数据段(data segment)通常是指用来存放程序中 已初始化 的 全局变量 的一块内存区域。数据段属于静态内存分配。
.text/.code段: 代码段(code segment/text segment)通常是指用来存放 程序执行代码 的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于 只读 , 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些 只读的常数变量 ,例如字符串常量等。程序段为程序代码在内存中的映射.一个程序可以在内存中多有个副本.
在单片机上RAM存放data段,bss段,堆栈段;ROM(EPROM,EEPROM,Flash等非易失性存储设备)存放代码,只读数据段。
对单片机编程后,程序的代码段.data段.bss段.rodata段等都存放在Flash中。当单片机上电后,初始化汇编代码将data段,bss段,复制到RAM中,并建立好堆栈,开始调用程序的main函数。
c/c++
一个由C/C++编译的程序占用的内存分为以下几个部分
1>栈区(stack)——由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2>堆区(heap)——一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。
3>全局区(静态区)(static)——全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放
4>文字常量区——常量字符串就是放在这里的(不可修改)。 程序结束后由系统释放
5>程序代码区——存放函数体的二进制代码。
其中堆区和栈区存放在RAM中,而全局区、文字常量区、程序代码区存放在ROM(Flash)中。
3 如何保存现场及恢复
1 汇编
保存寄存器的值,一般用的是push指令,将对应的某些寄存器的值,一个个放到堆栈中,把对应的值压入到堆栈里面,即所谓的压栈。
然后待被调用的子函数执行完毕的时候,再调用pop,把堆栈中的一个个的值,赋值给对应的那些你刚开始压栈时用到的寄存器,把对应的值从堆栈中弹出去,即所谓的出栈。
其中保存的寄存器中,也包括lr的值(因为用bl指令进行跳转的话,那么之前的pc的值是存在lr中的),然后在子程序执行完毕的时候,再把堆栈中的lr的值pop出来,赋值给pc,这样就实现了子函数的正确的返回。
2 C语言
c进行函数调用的时候,常常会传递给被调用的函数一些参数,对于这些C语言级别的参数,被编译器翻译成汇编语言的时候,就要找个地方存放一下,并且让被调用的函数能够访问,否则就没发实现传递参数了。对于找个地方放一下,分两种情况。
一种情况是,本身传递的参数就很少,就可以通过寄存器传送参数。
因为在前面的保存现场的动作中,已经保存好了对应的寄存器的值,那么此时,这些寄存器就是空闲的,可以供我们使用的了,那就可以放参数,而参数少的情况下,就足够存放参数了,比如参数有2个,那么就用r0和r1存放即可。(关于参数1和参数2,具体哪个放在r0,哪个放在r1,就是和APCS中的“在函数调用之间传递/返回参数”相关了,APCS中会有详细的约定。感兴趣的自己去研究。)
但是如果参数太多,寄存器不够用,那么就得把多余的参数堆栈中了。
即,可以用堆栈来传递所有的或寄存器放不下的那些多余的参数。