由于每个平台的ABI(应用二进制接口)不同,所以各个平台上调用子函数时对参数、局部变量、返回地址和返回值的处理也是不一样的,目前Android的大部分终端使用的ARM平台,遵循的ABI叫做ATPCS,即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)的简称。
ABI主要解决了下面三个问题,
被调用函数如何获取函数参数,即函数参数的传递规则。
函数中的局部变量如何定义和使用。
函数调用结束后如果回到被调用位置继续执行,调用函数如何拿到返回值。
参数传递规则
根据参数个数是否固定可以将子程序参数传递规则分为以下两种:参数个数可变的子程序参数传递规则和参数个数固定的子程序参数传递规则,这里主要以参数个数固定且不包含浮点型描述为主。
参数(以8个参数为例) | 整型 | 浮点型(包含浮点型运算硬件) |
---|---|---|
1st | R0 | FPU |
2st | R1 | FPU |
3st | R2 | FPU |
4st | R3 | FPU |
5st | 栈 | FPU |
6st | 栈 | FPU |
7st | 栈 | FPU |
8st | 栈 | FPU |
注意:
1. 前四个参数是顺序放入R0~R3,但超过四个的参数是按参数倒序存入栈中,即最后一个参数最先压入栈中。
2. 被调用的子函数在返回前无需恢复寄存器R0~R3。
3. 浮点参数将按照下面的规则传递:
(1) 各个浮点参数按顺序处理;
(2) 为每个浮点参数分配FPU寄存器。
分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FPU寄存器。
4. 参数个数可变的子程序参数传递规则有兴趣的自己查阅资料。
局部变量
优先存储在寄存器中:
ARM:依次存储在R4~R11。
Thumb:依次存储在R4~R7。
不能在寄存器中存储的,则在当前栈帧中分配。
PS:大于32bit的变量和指针引用的变量会存储在栈中,eg:数组、对象等等。
函数返回方式
除了叶节点函数(Non-leaf-Function),返回地址都存储在栈上。注意这并不是由调用指令直接把返回地址存在栈上。ARM平台的调用指令BL和BLX会把下一条指令地址保存在LR寄存器中,接着CPU去执行子函数代码,在子函数的代码中,为了能够继续调用子函数,所以需要把LR寄存器的值保存在栈中,并在函数返回时通过POP或者BX指令恢复到PC寄存器,
- ARM指令调用子函数
stmia sp! , {r11 , lr} ;保存LR寄存器到栈上以便函数调用子函数,保存R11环境变量
...
bl subroutine ;调用子函数
...
ldmia sp! , {r11 , lr} ;恢复环境变量和LR寄存器值
bx lr ;返回母函数
- Thumb指令调用子函数
push {r11 , lr}
...
bl subroutine
...
pop {r11, pc}
返回值
如果结果为一个32位的整数,可以通过寄存器R0返回。
如果结果为一个64位整数,可以通过寄存器R0和R1返回,依此类推。
如果结果为一个浮点数,可以通过浮点运算的寄存器F0、D0或S0返回。
如果结果为复合型的浮点数(如复数),可以通过寄存器F0~FN或者D0~DN返回。
对于为数更多的结果,需要通过内存来传递。
缓冲区溢出
由于ARM平台下非叶节点函数的返回值最终还是存储在栈中,故仍可以简单的利用栈溢出漏洞,原理参见上一篇。
可以通过一个简单的栈溢出攻击示例调试分析来理解这个过程。