ARM 汇编学习笔记

2017/08/19 blog

最近在做技术攻关的时候,遇到一些问题很诡异。从源码看起来完全没有问题。这就有点头疼,无奈,只能从汇编看了。用汇编结合lldb 的调试技巧,成果还不错,总有一些突破。

学习汇编并没有想象的那么难,我们学习汇编,并不是要我们拿汇编来写程序,只是帮助我们定位问题所在,还可以帮助我们更加深刻的理解计算机。

工具

如果我们有源码就能解决的问题,又何须去看汇编呢。汇编不是你想看就能看的,因此我们需要一些反汇编的工具来辅助我们进行汇编代码查看。我推荐几个工具:Hopper Disassembler 收费应用、IDA Pro 也是收费应用,这两个工具看起来确实方便。还有不要忘了,Xcode 本身给我们带的工具:lldb

(lldb) disassemble
pthreadtest`main:
    0x100000960 <+0>:   push   rbp
    0x100000961 <+1>:   mov    rbp, rsp
    0x100000964 <+4>:   sub    rsp, 0xc0
    0x10000096b <+11>:  lea    rax, [rip + 0x10ee]       ; empty_sem

效果也差不多,掌握一些调试技巧,这个还能动态调试。

寄存器

寄存器是CPU中的高速存储单元,要比内存中存取要快的多的一些存储单元,至于为什么比内存快,建议看看这篇文章 为什么寄存器比内存快?,让我大开眼界。

寄存器总共分为:通用寄存器、专用寄存器和控制寄存器。ARM64位架构有32个整数寄存器,包括1个专用的零寄存器,1个链接寄存器和1个帧指针寄存器,还有1个寄存器预留给平台,另外28 个则为通用整数寄存器。

R:Register;寄存器
PC:Program Counter;程序计数器
CPSR:Current Program Status Register;当前程序状态寄存器
SPSR:Saved Program Status Register;保存的程序状态寄存器
SP:Stack Pointer;数据栈指针
LR:Link Register;连接寄存器
SB:静态基址寄存器
SL:数据栈限制指针
FP:帧指针
IP:Intra-Procedure-call Scratch Register;内部程序调用暂存寄存器
理论联系实际
(lldb) register read
General Purpose Registers:
        x0 = 0x0000000010004005
        x1 = 0x0000000007000806
        x2 = 0x0000000000000000
        x3 = 0x0000000000000c00
        x4 = 0x0000000000002303
        x5 = 0x00000000ffffffff
        x6 = 0x0000000000000000
        x7 = 0x0000000000000001
        x8 = 0x00000000fffffbbf
        x9 = 0x0000000007000000
       x10 = 0x0000000007000100
       x11 = 0x0000000000041f88
       x12 = 0x00005e0000005f03
       x13 = 0x0000000000000000
       x14 = 0x00005f0000005f00
       x15 = 0x0000000000000000
       x16 = 0xffffffffffffffe1
       x17 = 0x00000001892ab2a4  CoreFoundation`-[__NSArrayM count]
       x18 = 0x0000000000000000
       x19 = 0x0000000000000000
       x20 = 0x00000000ffffffff
       x21 = 0x0000000000002303
       x22 = 0x0000000000000c00
       x23 = 0x000000016fd7eca8
       x24 = 0x0000000007000806
       x25 = 0x0000000000000000
       x26 = 0x0000000007000806
       x27 = 0x0000000000000c00
       x28 = 0x0000000000000001
        fp = 0x000000016fd7ebb0
        lr = 0x00000001883ab09c  libsystem_kernel.dylib`mach_msg + 72
        sp = 0x000000016fd7eb60
        pc = 0x00000001883ab224  libsystem_kernel.dylib`mach_msg_trap + 8
      cpsr = 0x60000000
  • R0 - R30

R0 - R30 是31个通用整形寄存器。每个寄存器可以存取一个64位大小的数。当使用x0 - x30 访问时,他就是一个64位的数,当使用w0 - w30 访问时,访问的是这些寄存器的低32 位。

其实通用寄存器有32个,低32个寄存器x31 ,在指令编码中,使用来做zero register,即ZRXZR/WZR 分别代表64/32位,zero register 的作用就是0,写进去代表丢弃结果,拿出来是0.

其中r29 又被叫做fp(frame pointer),r30又被叫做lr (link register).

  • SP

SP 寄存器指向栈顶,也就是低地址。

  • PC

PC 寄存器存的是当前执行的指令的地址

  • SPRs

SPRs是状态寄存器,用于存放程序运行中的一些状态标识,根据状态寄存器中的一些状态来控制分支的执行。状态寄存器又分为 The Current Program Status Register (CPSR)The Saved Program Status Registers (SPSRs)。 一般都是使用CPSR, 当发生异常时, CPSR会存入SPSR。当异常恢复,再拷贝回CPSR

  • V0 - V31

V0 - V31 是向量寄存器,也可以说是浮点型寄存器。它的特点是每个寄存器的大小是 128 位的。 分别可以用Bn Hn Sn Dn Qn的方式来访问不同的位数

  • 一般来说,arm64 上x0 - x7 分别会存放方法的前8个参数
  • 如果方法的参数个数超过8个,多余的参数会存放在栈上,新方法会通过栈来读取。
  • 方法的返回值一般都在x0 上。x86 的方法返回值会存放在eax
  • 如果方法返回值是一个较大的数据结构时,结果会存放在x8 执行的地址上。
指令

了解了寄存器后,来看看汇编指令,单条汇编指令是很简单的。汇编指令没有高级语言那么多的逻辑,每一条指令都是无条件执行,所以看起来就没有那么头晕。在这一节中,不讲那么多的概念,直接拎出几条比较经典常用的指令来加深理解,在具体实践中,遇到不会的就google。

所有指令大致可以分为3类,分别是数据操作指令、内存指令、分支指令。

  1. 数据操作指令

数据操作指令有以下2条规则:

  • 所有操作数均为32bit;

  • 所有结果均为32bit,且只能存放在寄存器中;

总的来说,数据操作指令的基本格式是:

op{cond}   {s}    Rd,    Rn,   op2

其中,”cond” 和 “s” 是两个可选后缀;”cond” 的作用是指定指令 “op” 在什么条件下执行。

数据操作指令可以分为以下4类:

  • 算术操作
ADD    R0, R1, R2					;R0 = R1 + R2
SUB    R0, R1, R2					;R0 = R1 - R2
RSB    R0, R1, R2					;R0 = R2 - R1

  • 逻辑操作
AND    R0, R1, R2					;R0 = R1 & R2  
ORR    R0, R1, R2					;R0 = R1 | R2 
EOR    R0, R1, R2					;R0 = R1 ^ R2
BIC    R0, R1, R2					;R0 = R1 &~ R2
MOV    R0, R2                       ;R0 = R2
MVN    R0, R2                       ;R0 = ~R2
  • 比较操作
CMP    R1, R2                       ; 执行R1 - R2 并依结果设置flag
CMN    R1, R2                       ; 执行R1 + R2 并依结果设置flag
TST    R1, R2                       ; 执行R1 & R2 并依结果设置flag
TEQ    R1, R2                       ; 执行R1 ^ R2 并依结果设置flag

比较操作其实就是改变flag 的算术操作或逻辑操作,只是操作结果不保留在寄存器里而已

  • 乘法操作
MUL    R4, R3, R2					;R4 = R3 * R2  
MLA    R4, R3, R2, R1				;R4 = R3 * R2 + R1

乘法操作的操作数必须来自寄存器。

  1. 内存操作指令

内存操作指令的基本格式是:

op {cond}{type}   Rd,   [Rn,op2]

其中Rn 是基址寄存器,用于存放基地址;”cond” 的作用与数据操作指令相同,”type” 指定指令“op” 操作的数据类型,共有4中:

  • B(unsigned Byte)
    无符号(执行时扩展到32位,以0 填充);
  • SB(Signed Byte)
    有符号byte(仅用于LDR 指令;执行时扩展到32bit,以符号位填充);
  • H(unsigned Halfword)
    无符号halfword (执行时扩展到32位,以0 填充
  • SH(signed Halfword)
    有符号halfword (仅用于LDR 指令;执行时扩展到32bit,以符号位填充)。

如果不指定“type”,则默认数据类型是word

ARM 内存操作基础指定只有两个:LDR(Load from memory into a register) 将数据从内存中读出来,存到寄存器中;STR(Store from a register into memory) 将数据从寄存器中读出来,存到内存中。

由于ARM 是RSIC 结构,数据从内存到CPU 之间的移动很稚嫩通过L/S 指令来完成,也就是ldr/str 指令比如想把数据从内存中某处读取到寄存器中,只能使用ldr比如:ldr r0,0x12345678 ;就是把0x12345678 这个地址中的值存放到r0而mov 不能干这个活,mov只能在寄存器之间移动数据,或者把立即数移动到寄存器中,这个和x86 这种CISC架构的芯片区别最大的地方x86 中没有ldr 这种指令,因为x86 的mov 指令可以将数据从内存中移动到寄存器中。

  • STR 指令

str 指令是一个典型的存储指令,用于从源寄存器中讲一个32位的字数据传送到存储器中,该指令在程序设计中比较常用,寻址方式灵活多样。该指令的使用方式如下:

STR R0, [R1], #8 ; 将R0 中的字数据写入以R1 为地址的存储器中,并将新地址R1 + 8 写入R1.
STR R0, [R1, #8] ; 将R0 中的字数据写入以R1 + 8 为地址的存储器中。
STR R1, [R0]     ; 将r1 寄存器的值,传送到地址值为r0 的内存中
  • LDR 指令

sdr 指令将数据从内存中读取到寄存器中,ldr 只能在当前pc 的4KB 返回内跳转。该指令的使用方式如下:

LDR R0,[R1]              ;将存储器地址为R1的字数据读入寄存器R0。

LDR R0,[R1,R2]           ;将存储器地址为R1+R2的字数据读入寄存器R0。

LDR R0,[R1,#8]           ;将存储器地址为R1+8的字数据读入寄存器R0。

LDR R0,[R1],R2            ;将存储器地址为R1的字数据读入寄存器R0,并将R1+R2的值存入R1。

LDR R0,[R1],#8            ;将存储器地址为R1的字数据读入寄存器R0,并将R1+8的值存入R1。

LDR R0,[R1,R2]!           ;将存储器地址为R1+R2的字数据读入寄存器R0,并将R1+R2的值存入R1。

LDR R0,[R1,LSL #3]        ;将存储器地址为R1*8的字数据读入寄存器R0。

LDR R0,[R1,R2,LSL #2]    ;将存储器地址为R1+R2*4的字数据读入寄存器R0。

LDR R0,[R1,,R2,LSL #2]!   ;将存储器地址为R1+R2*4的字数据读入寄存器R0,并将R1+R2*4的值存入R1。

LDR R0,[R1],R2,LSL #2     ;将存储器地址为R1的字数据读入寄存器R0,并将R1+R2*4的值存入R1。

LDR R0,Label               ;Label为程序标号,Label必须是当前指令的-4~4KB范围内

此外,LDR 和STR 的变种LDRD 和 STRD 还可以操作双字(Doubleword),即一次操作两个寄存器,其基本格式如下:

op{cond} Rt, Rt2, [Rn {, #offset}]

如下:

STRD R4, R5, [R9, #offset]      ;*(R9 + offset) = R4; *(R9 + offset + 4) = R5
LDRD R4, R5, [R9, #offset]      ;R4 = *(R9 + offset); R5 = *(R9 + offset + 4)

此外,除了str 和ldr 外,还可以通过LDM(loaD Multiple) 和STM(STore Multiple)进行块传输,一次性操作多个寄存器。

另外还有一个就是ldr 伪指令,虽然伪指令和arm 的ldr 指令很像,但是作用不太一样,ldr 伪指令可以在立即数前加上=,以表示把一个值(一般是一个地址)写到某个寄存器中,比如:
ldr r0, =0x12345678 这样就把0x12345678 这个值写到r0 中了。所以ldr 伪指令和mov 是比较相似的。只不过mov 指令限制了立即数的长度为8位,也就是不能超过512.而伪指令没有这个限制。如果使用ldr 伪指令时,后面跟的立即数没有超过8位,那么在实际汇编的时候ldr 指令是被转换为mov 指令的。

其实ldr 指令可以装载一个32bit 立即数的说法并不确切,因为实际上并不是这一条语句装载了一个32bit立即数,真正的汇编代码是将某个地址的值传递给r1,就是说需要一个地址存放0x12345678 这个立即数。而且如果这个立即数可以用mov 指令的形式来表达,会被编译器实际用mov 来代替比如:

ldr r1,=0x10
会变成
mov r1,#0x10
综上所述:ldr 伪指令用于加载32 位的立即数或一个地址值到指定寄存器。在汇编编译源程序时,ldr伪指令被编译器替换成一条合适的指令。若加载的常熟未超出mov 或者mvn 的范围,则使用mov 或mvn 指令代替该ldr 伪指令,否则汇编器将常量放入文字池,并使用一条程序相对偏移的ldr 指令从文字池读出常量。

  1. 分支指令

分支指令可以分为无条件分支和条件分支。

  • 无条件分支
B Label           ;PC = Label
BL Lable          ;LR = PC - 4;PC = Label
BX Rd             ;PC = Rd 并切换指令集

B 表示无返回的跳转,BL 表示有返回的跳转。有返回的意思就是会存lr,因此BL 的L也可以理解为LR的意思。存了LR也就意味着可以返回到本方法继续执行,一般用于不同方法的直接条用,B 相关的跳转没有LR,一般是本方法内的跳转,如while 循环,if else 等等。

  • 条件分支

在条件分支指令前会有一条数据操作指令来设置flag,分支指令根据flag 的值来决定代码走向,如:

Label:
    LDR R0, [R1], #4
    CMP R0, 0               ;如果R0 == 0,Z = 1;否则Z = 0
    BNE Label               ;Z == 0 则跳转

栈是从高地址到低地址的,栈顶是低地址,栈底是高地址。fp 指向当前frame 的栈底,也就是高地址,SP 指向栈顶,也就是低地址。一般来说arm64 上x0 - x7 分别存放方法的前8个参数,如果参数个数超过了8个,多余的参数会存放在栈上,新方法会通过栈来读取方法的返回值,一般都在x0上。如果方法返回值是一个较大的数据结构时,结果会存放在x8 执行的地址上。

寻址

既然是和内存相关的,那就是两种,一种是存,一种是取。一般来说
L打头的基本都是取值指令,如LDR LDP;
S打头的基本都是邨值指令,如STR STP;
如:

ldr x0,  [x1]              ; 从x1 指向的地址里取出一个64位大小的数存入x0 中
ldp x1,  x2,[x10,#0x10]    ;从x10 + 0x10指向的地址里取出2个64位的数,分别存入x1,x2
str x5,  [sp,#24]          ;把x5 的值(64位数值)存到sp+24 指向的内存地址上
stp x29, x30,[sp,#-16]!    ;把x29,x30 的值存到sp-16 的地址上,并且把sp -= 16,其中 !代表writeback ,就是改变sp 的值
最后

在开发过程中难免会遇到一些令我们头疼的bug,学习一些汇编,让我们更加深刻的理解计算机,理解代码执行过程,可以定位一些疑难杂症的问题。汇编指令的执行是简单确定的,是无条件执行的。本人也是在学习过程中,经常会遇到一些疑难杂症,本想能躲开汇编就躲开,但是后来发现,有些使我们无法避免的。在学习过程中建议拿hooper 或者IDA Pro多看看,理解每一行指令的意义,融会贯通。

学习过程中…..共勉!

参考资料:
iOS开发同学的arm64汇编入门
iOS 逆向工程

Search

    Post Directory