bluewind1230 发表于 2018-4-14 22:53

arm汇编的学习笔记,对比x86和arm(1)-从最简单的函数谈起

前一段时间,在准备脱安卓壳的时候发现需要学习arm汇编的知识,这里记录了当初自己的学习过程,希望给予后来者有一点帮助,还请高手勿喷!
## 最简单的函数
直接上c/c++代码:
```
int f()
{
return 123;
}
```
### x86下汇编指令
gcc编译器产生的汇编指令,如下:
```
f:
   mov eax,123
   ret
```
MSVC编译的程序和上述指令基本一致;
这个函数仅仅由两条指令构成:第一条指令把数值123存放于eax寄存器中,根据函数调用约定,后面一条指令把eax的值当做返回值传递给函数调用者(caller),而caller会从eax寄存器里面取值,把它当做返回值;
**注:在x86体系中,一般eax存放返回值.**
### ARM下汇编指令
```
f PROC
    MOV r0,#0x7b;123
    BX lr
    ENDP
```
ARM程序使用R0寄存器传递函数返回值,所以指令把123传递给r0; (R0寄存器类似于x86中的eax作用)
ARM程序使用LR(Link Register)寄存器存储函数结束之后的返回地址(RA/Return Address).x86程序使用”栈”结构存储上述返回地址,可以看出,BX LR指令的作用是跳转至返回地址,即:返回到当前函数的上一层,然后继续执行caller的后续指令.
## Hello World
c/c++中源代码:
```
int main()
{
printf("hello, world\n");
return 0;
}
```
### x86中汇编指令
MSVC中:
```
00CD1790 55                   push      ebp
00CD1791 8B EC                mov         ebp,esp
00CD1793 81 EC C0 00 00 00    sub         esp,0C0h
00CD1799 53                   push      ebx
00CD179A 56                   push      esi
00CD179B 57                   push      edi
00CD179C 8D BD 40 FF FF FF    lea         edi,
00CD17A2 B9 30 00 00 00       mov         ecx,30h
00CD17A7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh
00CD17AC F3 AB                rep stos    dword ptr es:
开辟栈帧以及security cookie
--------------------------------------------------------------------
--------------------------------------------------------------------
    printf("Hello World\n");
00CD17AE 68 30 6B CD 00       push      offset string "Hello World\n" (0CD6B30h)
    printf("Hello World\n");
00CD17B3 E8 5E FB FF FF       call      _printf (0CD1316h)
00CD17B8 83 C4 04             add         esp,4//堆栈平衡
    return 0;
00CD17BB 33 C0                xor         eax,eax
--------------------------------------------------------------------
--------------------------------------------------------------------
00CD17BD 5F                   pop         edi
00CD17BE 5E                   pop         esi
00CD17BF 5B                   pop         ebx
00CD17C0 81 C4 C0 00 00 00    add         esp,0C0h
00CD17C6 3B EC                cmp         ebp,esp
00CD17C8 E8 41 F9 FF FF       call      __RTC_CheckEsp (0CD110Eh)
00CD17CD 8B E5                mov         esp,ebp
00CD17CF 5D                   pop         ebp
00CD17D0 C3                   ret
回收栈帧,堆栈平衡

```
GCC编译器中生成的汇编指令,
在IDA中观察到的汇编指令:
```
Main   proc near
var_10= dword ptr -10h//这里var_10表示是为局部变量的意思,距离ebp大小为 -10h
      push ebp
      mov ebp,esp
      and esp,0FFFFFF0h
      sub esp,10h
      mov eax,offset aHelloWorld;"helllo,world\n"   
      mov ,eax
      call _printf
      mov eax,0
      leave
      retn
main   endp
```
**注:leave:等效于”Mov ESP,EBP”和”POP EBP”两条指令.**
整体逻辑上与MSVC中一样;


### ARM汇编
```
main
   STMFD SP!,{R4,LR}
   ADR   R0,aHelloWorld;"hello,world"
   BL   _2printf
   MOV R0,#0
   LDMFD SP!,{R4,PC}
   +aHelloWorldDCB"hello,world",0;DATA XREF:main+4
```
`STMFD SP!,{R4,LR}`:`STMFD`相当于x86的push指令,它把r4寄存器和LR(Link Register)寄存器的数值放到数据栈中,这里的措辞是”相当于”,而非”完全是”.这是因为ARM模式的指令集里没有PUSH指令,只有Thumb模式里的指令集里才有”PUSH/POP”指令.一般可以在IDA中可以清楚地看到这种差别.;
`STMFD SP!,{R4,LR}`:**这条指令首先将sp(stack pointer)递减,在栈中分配一个新的空间以便存储r4和lr的值**,这里的SP类似于x86体系中的SP/ESP/RSP;(注:STMFD全称:Storage Multiple Full Descending)
#### 知识拓展:LDM/STM指令
LDM/STM指令主要用于现场保护,数据复制,参数传送等;类似于x86中pop/push指令功能.
`STMFD SP!,{R0-R7,LR}`对于这条指令伪代码的解释,上网查了一波:
```
SP = SP - 9×4;

address = SP;

for i = 0 to 7

    Memory = Ri;

    address= address + 4;

Memory = LR;
```
个人理解如下:
```
sp = address;

sp = sp - 4;

Memory = LR;

for( i=7;i>0;i--)

{

sp = sp-4;

   Memory = Ri;

}
```
由于ARM堆栈结构是从高向低压栈的,此时SP即是栈顶。

这里的sp = sp-4,是因为处理器是32位的ARM,所以每次压一次栈SP就会移动4个字节(32位)。
假设此时SP地址为: 0x40000460,由前面解释伪代码可得下图:

|   R0   |   0x4000043c      |
|------|-------------|
|   R1   |   0x40000440      |
|   R2   |   0x40000444      |
|   R3   |   0x40000448      |
|   R4   |   0x4000044c      |
|   R5   |   0x40000450      |
|   R6   |   0x40000454      |
|   R7   |   0x40000458      |
|   LR   |   0x4000045c      |


0x4000045c 为执行指令前的SP地址, 0x4000043c ,是执行指令后的SP地址,由此看出STMFD指令是向着地址减小的方向的;
#### LDMFD 指令
`LDMFD`全称:Load Multiple Full Descending
`LDMFD Rn{!},{reglist}{^} `
这条指令的意思是以Rn为基址(起始地址),取值写入寄存器列表。
eg:`LDMFD SP!,{R0-R7,PC}`
对于这条指令:
```
address = SP;

for i = 0 to 7

   Ri = Memory

    address = address + 4;

SP = address;
```
假设此时SP地址为: 0x4000043C,由前面解释伪代码可得下图:

|   R0   |   0x4000043c      |
|------|-------------|
|   R1   |   0x40000440      |
|   R2   |   0x40000444      |
|   R3   |   0x40000448      |
|   R4   |   0x4000044c      |
|   R5   |   0x40000450      |
|   R6   |   0x40000454      |
|   R7   |   0x40000458      |
|   LR   |   0x4000045c      |

0x4000043c 为执行指令前的SP地址,0x4000045c 是执行指令后的SP地址。
有点类似于x86中的pop指令

回归正线:
### ADR指令:
“ADR R0,aHelloWorld”,首先对PC(指令指针计数器,Program Counter,有点类似于X86中的IP/EIP/RIP)进行取值操作,然后把“hello world”字符串的偏移量与pc的值相加,然后将其结果存储于r0之中,ADR指令将当前指令的地址与字符串指针地址的差值传递给r0,程序借助pc指针可以找到字符串指针的偏移地址,从而使操作系统确定字符串常量在内存里的绝对地址.
#### ADR知识点扩展:
##### ADR
是一条小范围的地址读取伪指令,它将基于PC的相对偏移的地址值读到目标寄存器中。格式:ADR register,exper.
编译源程序时,汇编器首先计算当前PC值(当前指令位置)到exper的距离,然后用一条ADD或者SUB指令替换这条伪指令,
例如:
`ADD register,PC,#offset_to_exper`;
注意,标号exper与指令必须在同一代码段。
比如:adr r0, _start ://将指定地址赋到r0中
………
_start:
b _start

r0的值为标号_start与此指令的距离差 + PC值。
##### ADRL:
这是一条中等范围的地址读取伪指令,它将基于PC的相对偏移的地址值读到目标寄存器中。格式:ADRL register,exper。编译源程序时,汇编器会用两条合适的指令替换这条伪指令。
比如:
```
ADD register,PC,offset1
ADD register,register,offset2
```
与ADR相比,它能读取更大范围的地址。
注意,标号exper与指令必须在同一代码段。

接下来是LDR,首先要说两个家伙,他们都叫LDR。

一个是LDR伪指令,一个是LDR指令,名字相同却不是一个东西。

区分的方法就是看第二个参数,如果有等号,就是伪指令。
##### LDR指令:
例: ldr r0, 0x12345678
是把0x12345678这个地址中的值存放到r0中。而mov不能干这个活,mov只能在寄存器之间移动数据,或者把立即数移动到寄存器中。

LDR伪指令:
例1(立即数): ldr r0, =0x12345678
这样,就把0x12345678这个地址写到r0中了。所以,ldr伪指令和mov是比较相似的。只不过mov指令限制了立即数的长度为8位,也就是不能超过512。而ldr伪指令没有这个限制。如果使用ldr伪指令,后面跟的立即数没有超过8位,那么在实际汇编的时候该ldr伪指令会被转换为mov指令。

例2(标号): ldr r0, =_start //将指定标号的值赋给r0
这里取得的是标号_start的绝对地址,这个绝对地址(运行地址)是在链接的时候确定的。它要占用 2 个32bit的空间,一条是指令,另一条是文字池中存放_start 的绝对地址。

对比adr r0, _start和 ldr r0, =_start
它们的目的一样,都是把标签的赋给r0,区别—左边是相对地址,右边绝对地址。目的一样,但结果不一定相同。结果是否相同,要看PC值是否和链接地址相同。

#### BL指令
BL的全称为:Branch With Link,相当于x86中的call指令,;
`BL _2printf`调用printf()函数,BL实施的具体操作步骤是:
a.将下一条指令的地址,即地址0xc处的”MOV R0,#0”的地址写入LR(存放函数返回值)寄存器
b.将printf()函数的地址写入pc寄存器,引导系统执行该函数.
当printf()完成工作之后,计算机必须知道返回地址,即它应当从哪里继续执行下一条指令,故每次使用BL指令调用其他函数之前,都要把BL指令下一条指令的地址存储到LR寄存器中;

`MOV R0,#0`将R0寄存器置为0,在c代码中,主函数返回0,该指令把返回值写在r0寄存器中.
`LDMFD SP!,R4,PC`这一条指令,他与STMFD成对出现,做的工作相反,类似于x86中的pop指令,**LDMFD全称:Load Multiple Full Descending;**它将栈中的值取出,依次赋值给R4和PC,并且会调整栈指针SP;

main函数中的第一条指令就是STMFD指令,将R4寄存器和LR寄存器存储于栈中,main()函数在结尾处使用LDMFD指令,其作用是把栈中的PC的值和R4寄存器的值恢复过来;
前面提到过,**程序在调用其他函数之前,必须把返回地址保存于LR寄存器里面,因为在调用printf()函数之后LR寄存器的值会发生变化,所以第一条指令就要负责保存LR寄存器的值,在被调用的函数结束之后,LR寄存器中存储的值会被赋给PC,以便程序返回函数调用者的这一层中继续执行**,当c/c++的主函数main()结束之后,程序的控制权返回OS loader,或者CRT中的某个指针,或者作用相似的其他指令
## 总结
这里只是通过两个比较简单的函数学习了arm汇编中最基本的几条指令,如果有时间我还会继续写的,最后附上这篇文章的原文:
https://blog.csdn.net/richard1230/article/details/79812966

日常 发表于 2018-4-14 23:01

学习一下!:lol

jmpengbo 发表于 2018-4-14 23:19

谢谢分享

jmpengbo 发表于 2018-4-14 23:27

谢谢分享

傲灬世 发表于 2018-4-14 23:32

感谢分享,如果有单片机的汇编就更好了

北岛未 发表于 2018-4-15 00:44

感谢楼主的分享

lengyue 发表于 2018-4-15 01:36

技術貼!頂!

不识i 发表于 2018-4-15 09:41

谢谢楼主分享

qidians 发表于 2018-4-15 20:57

传说中得汇编呀

kanxue2018 发表于 2018-4-15 21:02

汇编指令虽然难学,但很重要了
页: [1] 2
查看完整版本: arm汇编的学习笔记,对比x86和arm(1)-从最简单的函数谈起