程序与内存的关系,如鱼和水一般密不可分。内存时承载程序运行的介质,也是程序进行各种运算和表达的场所。
1 内存布局
32位处理器对应的内存空间一般是4GB,分为内核空间和用户空间两部分。
内核空间: 给内核使用,应用程序无法直接访问。默认情况下,linux会将高地址的1GB空间分给内核,windows则是将高地址的2GB空间分给内核。
用户空间:余下的2GB或3GB给用户使用的空间。一般由如下区域组成:
- 栈:用于维护函数调用的上下文
- 堆:用来容纳应用程序动态分配的内存区域
- 可执行文件的映像
- 保留区:不允许访问的区域,如0地址 NULL.
ARM32-linux 内存布局图:
2 栈与函数调用惯例
2.1 什么是栈?
栈(stack)是现代计算机程序里最为重要的概念之一,没有栈就没有函数调用和局部变量。栈保留了一个函数调用所需要的维护信息,即栈帧(Stack Frame)。堆栈帧一般包括以下几个方面信息:
- 函数的返回地址和参数
- 临时变量:包括函数的非静态局部变量和编译器自动生成的其他临时变量
- 保存的上下文:在函数调用前后需要保持不变的寄存器值
栈中的数据传输遵循一个规则:先入栈的数据后出栈(FIFO)。
2.2 栈的生长方向
当堆栈指针指向最后压入堆栈的数据时,称为满堆栈(FullStack),而当堆栈指针指向下一个将要放入数据的空位置时,称为空堆栈(Empty Stack)。
同时,根据堆栈的生成方式,又可以分为递增堆栈(AscendingStack)和递减堆栈(DecendingStack),当堆栈由低地址向高地址生成时,称为递增堆栈,当堆栈由高地址向低地址生成时,称为递减堆栈。
所以共有四种组合方式,在ARM指令集中它们对应的出栈入栈指令分别为:
- 满递减堆栈:指令如LDMFA,STMFA等。
- 满递增堆栈:指令如LDMEA,STMEA等
- 空递减堆栈:指令如LDMFD,STMFD等。
- 空递增堆栈:指令如LDMED,STMED等。
ARM处理器核虽然对于两种生长方式的堆栈均支持,但ADS的C语言编译器仅支持一种方式,即从上往下长,并且必须是满递减堆栈。
【引申】:内存的生长方向——“大小端对齐”
- 小端对齐 :低字节放在低地址,高字节放在高地址(多)
- 大端对齐 :高字节放在低地址,低字节放在高地址(服务器)
2.3 调用惯例
函数的调用方和被调用方,对函数如何调用应当有着统一的理解,即要遵守一定的约定——调用惯例(Calling Convention),一个调用惯例会规定如下几个方面的内容:
- 函数参数的传递顺序和方式: 函数参数的传递方式有多重,最常见的是栈传递,有些惯例还允许使用寄存器传递参数。函数调用方将参数压栈的顺序:从左至右,从右至左。
- 栈的维护方式:栈在调用前后保持一致。将入栈的数据出栈可以由函数调用方或被调用方完成
- 名字修饰(name-mangling)的策略。
几种常见的调用惯例对比如下,其中在C语言中,默认的调用惯例是cdecl。
调用惯例 | 参数传递 | 出栈方 | 名字修饰 |
---|---|---|---|
codec | 函数调用方 | 从右至左的顺序压参数入栈 | 下划线+函数名 |
stdcall | 函数本身 | 从右至左的顺序压参数入栈 | 下划线+函数名 + @ + 参数的字节数,如函数int func(int a, double b)的修饰名是_func@12 |
fastcall | 函数本身 | 头两个DWORD(4 bytes)或者占更少字节的参数被放入寄存器,其他剩余参数从右至左入栈 | @+函数名+@+参数的字节数 |
pascal | 函数本身 | 从左至右的顺序压参数入栈 | 较为复杂,参考文档 |
2.4 函数返回值传递
小于4字节的返回值,一般通过寄存器传递,如X86体系中的eax寄存器,5~8个字节则通过eax+edx传递。对于大于8字节的返回值,传递思路一般如下:
- 由函数调用方在栈上额外开辟一片空间,用于暂存返回值,这里称为temp.
- 调用方将temp的地址作为隐藏参数传递给被调用函数
- 被调用方执行完后,将返回值存储在temp中,并将temp的地址通过寄存器传出。
- 调用方将寄存器指向的temp对象的内容copy出来。
由此可看出,结果返回值会被copy两次。所以不到万不得已,不要轻易返回大尺寸的对象。
2.5 ARM 函数过程调用标准 APCS (ARM Procedure Call Standard)
Apcs 规定了一些子程序间调用的基本规则,这些规则包括子程序调用过程中寄存器的使用规则,数据栈的使用规则,参数的传递规则。有了这些规则之后,单独编译的C语言程序就可以和汇编程序互相调用。
2.5.1寄存器使用规则:
ARM 核包含37个通用寄存器(R0 ~ R15)和额外的专用寄存器(如CPSR 状态寄存器)。
寄存器 | 用途 |
---|---|
R0 ~ R3 | 用来作为函数入参(从左到右的顺序,大于4个参数使用栈来传递)和返回值。在函数内部,r0~r3也可以用来存储局部变量 |
R4~R11 | 保存局部变量。在Thumb程序中,通常只能使用寄存器R4~R7来保存局部变量 |
R12 | 过程调用中间临时寄存器,记作IP。在子程序之间的连接代码段中常常有这种使用规则 |
R13 | 用作堆栈指针,记作SP。在子程序中寄存器R13不能用作其他用途。寄存器SP在进入子程序时的值和退出子程序时的值必须相等。arm中SP指针不只一个,分别对应不同工作模式 。 |
R14 | 连接寄存器,记作LR。它用于保存子程序的返回地址。如果在子程序中保存了返回地址,寄存器R14则可以用作其他用途。 |
R15 | 程序计数器,记作PC,指向当前执行指令的下一条。 |
CPSR | CPSR为当前程序状态寄存器,存放有:APSR标记、当前处理器模式、中断禁用标志、当前处理器状态(ARM/Thumb/ThumbEE/Jazelle等)、 IT块的执行状态位 |
2.5.2 堆栈的使用:
ATPCS规定堆栈为FD,即满递减堆栈,并且对堆栈的操作是8字节对齐。所以经常使用的指令就有STMFD和LDMFD。
对于汇编程序来说,如果目标文件中包含了外部调用,则必须满足下列条件:
- 外部接口的堆栈必须是8字节对齐的。
- 在汇编程序中使用PRESERVE8伪指令告诉连接器,本汇编程序数据是8字节对齐的。
2.5.3 参数传递规则:
根据参数个数是否固定,可以将子程序分为参数个数固定的子程序和参数个数可变化的子程序。
- 参数个数固定子程序参数传递规则
结果为一个32位整数时,可以通过寄存器R0返回;
结果为一个64位整数时,可以通过寄存器R0和R1返回;
结果为一个浮点数时,可以通过浮点运算部件的寄存器f0、d0或s0来返回;
结果为复合型浮点数(如复数)时,可以通过寄存器f0~fn或d0~dn来返回;
对于位数更多的结果,需要通过内存来传递。
- 参数个数不固定子程序参数传递规则
将所有参数看作是存放在连续的内存字单元的字数据。然后,依次将各字数据传递到寄存器R0,R1,R2和R3中。如果参数多于4个,则将剩余的字数据传递到堆栈中。入栈的顺序与参数传递顺序相反,即最后一个字数据先入栈。
具体 可参考文章 ——> 可变参数函数实现原理