ARM与X86架构下的汇编基础知识:
ARM与X86的区别
二者使用的指令集不同,ARM架构使用“精简指令集”,能效比高、功耗低但是性能上限不高;X86架构反之。同时,两种架构给寄存器命名的方式也不同。
寄存器
寄存器是架构运行中最重要的组件之一。我们一般说的32位系统、64位系统指的是寄存器的长度为32比特和64比特。
ARM架构下的常规寄存器如下:
其中,R0-R10都是常规寄存器,使用时无区别;
R7寄存器也用于存储系统调用号;
R11又称FP,是 Frame Pointer的缩写,意为“帧指针”,指向当前函数栈顶;
R12用处较少不做阐述;
R13又称SP,是 Stack Pointer的缩写,意为“栈指针”,只想当前函数的栈底;
R14又称LR,是 Link Register的缩写,意为“链接寄存器”,用于存储返回地址,即调用函数的地址(区别于被调用函数);
R15又称PC,是 Program Counter的缩写,意为“程序计数器”,用于存储当前指令执行的位置,以便用于相对寻址,事实上,由于指令执行的“三步流水线”工序(取指、译码、执行),它实际指向的是当前指令后两条指令(总是指向正在取指阶段的指令)
CSPR是 Current Program Status Register的缩写用于存储当前系统状态的信息与X86中的EFLAGS寄存器等效
下图为ARM架构与X86架构的寄存器对应关系:
满栈递减机制:
SP总指向一个有内容的内存地址,插入数据时,SP向低地址方向移动
左二为满栈递减
SP与FP:
每一个函数在运行时都会分配到一块内存空间,称为“栈帧”:
FP指向顶部(高地址),SP指向底部(低地址),二者之间即为函数所分配的内存区域
关于函数:
函数在汇编层面上分为三个部分:Prologue、Body、Epilogue
即:序言、主体、尾声三个部分
一、序言
序言的目的是保存程序的当前状态,类似于bl指令,使得程序在执行完被调用函数后能够正确地返回调用函数,这一目的的实现通常是将LR、R11寄存器中存储的地址进行压栈操作
对于上述过程解释如下:
1、用push指令将R11、LR寄存器存储的地址压栈备用
2、将当前SP寄存器的值存入R11寄存器;前面说过,R11就是FP,而FP指向函数内存的顶部、SP指向底部。这里的赋值操作相当于让FP下移,使之成为被调用的函数内存区域的底部(以调用函数的内存底部为顶部)
3、用sub指令将SP指针下移(减法操作处理地址),下移的大小即为被调用函数的栈帧大小
二、主体
就是函数具体执行的内容,重点不在这里
三、尾声
1、运用sub指令将SP归位至调用函数的底部,因为FP在序言部分被挪至调用函数的内存底部、即被调用函数的顶部,这里的sub指令就是将R11的存储内容存入SP,就是让SP重新指向调用函数的底部
2、pop指令将R11,PC寄存器在栈中的值弹出,即将FP复位至调用函数内存顶部,
为什么是pop {r11, pc}而不是pop {r11, lr}:
CPU始终根据PC寄存器指向的指令进行跟进执行,如果我们只是变更了LR寄存器的值而没有变更PC寄存器的值,那么CPU就会根据PC寄存器的值继续向非预期的方向执行,LR寄存器存储了返回地址,我的感觉就像是PC寄存器的参考值的存在
说回来,pop {r11, pc}这条指令在ARM架构中有个隐式的返回行为,即pc被包含在pop指令中时,该指令在弹出另一个寄存器的栈值时,还会将PC设置为栈顶的值。我们最后一次压栈行为发生在函数的序言阶段,指令为push {r11, lr},这里进行了多次STR指令,即压栈操作,将LR的值压入栈后再将R11的值压入栈中,此时LR的值位于栈顶,这样也就完成了令PC寄存器的值为LR寄存器的值,从而令程序能够正确地返回调用函数继续执行
事实上,pop {r11, pc}这条指令本质上是等效于pop {r11, lr};bx lr这两条指令的。弹出LR的值后,就设定了一个调转回去的“目标”,利用bx指令完成跳转操作,而跳转指令的本质就是更改PC寄存器的值,所以这两条语句在功能上与pop {r11, pc}是完全等价的