天天看点

ARM pwn 入门 (1)2. 指令集

最近笔者刚刚加入了一个项目组,需要用到ARM架构的东西,和ARM pwn也有一定关系,因此一不做二不休,决定开始学习ARM pwn,顺便熟悉项目前置知识,一举两得。

ARM与x86分属不同架构,指令集不同,需要从头开始学习,本文从寄存器、指令方面对x86-64和ARM架构下的汇编语言做比较与学习。(配图选自清华大学出版社《ARM Cortex-M3与Cortex-M4权威指南》,侵删)

1. 寄存器

寄存器是汇编语言的核心,在x86-64系统中,最为常见的寄存器有以下这些:

64位:
rax, rbx, rcx, rdx
rsi, rdi, rsp, rbp, rip
r8, r9, r10, r11, r12, r13, r14, r15
32位:
eax, ebx, ecx, edx
esi, edi, esp, ebp, eip
r8d, r9d, r10d, r11d, r12d, r13d, r14d, r15d
           

在大多数程序中,这17个寄存器是最为常用的寄存器,其中

rax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, rip

有专门的作用,但其中的

rax, rbx, rcx, rdx, rsi, rdi

功能相对更加灵活,不像

rsp

只能用于表示栈顶地址,

rbp

只能用于表示栈帧地址,

rip

只能用于表示当前指令地址等。另外的8个寄存器则是通用寄存器,想用来干嘛就干嘛。

那么在ARM架构中,寄存器则是以下这些:

64位:
X0, X1, X2, X3, X4, X5, X6, X7, X8, X9, X10, X11, X12
X13, X14, X15
32位:
R0, R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12
R13, R14, R15
           

其中

R0~R12

为通用寄存器,共13个,剩下的3个有特殊用途:

  • R13

    为栈指针,又称

    SP

    ,相当于

    rsp

    ,在物理上实际上有两个栈指针:主栈指针和进程栈指针,一般的进程只有一个栈指针可见。这个也好理解,就好比在x86-64系统中,内核的栈指针和用户进程的栈指针不同一样。
  • R14

    为链接寄存器,又称

    LR

    ,用于保存函数调用时的返回值。在x86-64系统中,函数调用的返回值是保存在子函数栈帧的上面,即

    rbp+8

    的位置,在ARM系统中,函数调用同样需要将返回地址保存到栈中,因为

    LR

    在函数返回时会进行自动更新,如果栈中没有返回地址,那么

    LR

    就不知道要更新成什么值了。当然

    LR

    的作用不止这些,在后面遇到具体问题时再进行分析。
  • R15

    为程序计数器,又称

    PC

    ,可读可写。读操作返回当前指令地址+4(由ARM指令集特性决定,ARM指令集中任何一条指令都是偶数长度,与x86-64不同),写操作会导致执行流跳转。

    PC

    的最低有效位(LSB)是一个控制结构,为1时表示进入Thumb状态。当有些时候程序跳转更新PC时需要将新PC值的LSB置1,否则会触发错误异常。这也可以看做是一种程序恶意跳转的保护机制。有时还会将

    PC

    作为基址访问数据。

除了这些寄存器之外,两个架构下都各自有各自的特殊寄存器,如x86-64架构下的

rflags

控制寄存器用于保存程序执行的状态。在ARM中同样具有类似功能的控制寄存器:

ARM pwn 入门 (1)2. 指令集
  • APSR

    :应用状态寄存器
  • EPSR

    :执行状态寄存器
  • IPSR

    :中断状态寄存器

    上面的三个寄存器可以通过一个组合寄存器

    PSR

    访问,在不同的ARM架构中状态寄存器的排布有一定不同:
    ARM pwn 入门 (1)2. 指令集

3个中断-异常屏蔽寄存器的功能较少用到,这里先不进行讨论。

CONTROL

寄存器确定了栈指针的选择和线程模式的访问等级,其只能够在特权等级下才能进行修改。

ARM pwn 入门 (1)2. 指令集

其中具体的细节阐述较为繁琐,不是本文的重点,略过。

另外,在x86-64架构和ARM架构中都有很多的浮点数寄存器,用于进行浮点数计算。在ARM架构中,浮点数寄存器有32个32位寄存器

S0~S31

,其中可以两两组合访问为

D0~D15

,如

S0

S1

组合为

D0

2. 指令集

ARM的指令集和x86-64有一些相似之处,但也有一些不同,需要注意的是,ARM的立即数前面需要加上#标识,如#0x12345678。下面的指令均为32位系统下的指令。

A. 寄存器传送数据

与x86相同,ARM使用

MOV

系列指令进行寄存器与寄存器(立即数)之间的数据传送:

  • MOV/MOVS reg1, <reg2/imm8>

    :赋值

    reg1

    reg2/imm8

  • MOVW <reg32>, <imm16>

    :赋值

    reg32

    的低16位为

    imm16

  • MOVT <reg32>, <imm16>

    :赋值

    reg32

    的高16位为

    imm16

  • MVN reg1, <reg2>

    :将

    reg2

    的值取反之后赋值给

    reg1

  • LDR <reg32>, =<imm32>

    ①:赋值

    reg32

    imm32

备注:

① 这里的指令并不是一条真正的指令,而是一条伪指令。ARM汇编器会将字符数据汇总组成一个称为 “文字池” 的数据块,与x86-64不同,后者如果需要实现将立即数赋值到寄存器,会直接将立即数写死到指令中。这里的

LDR

指令实际是做了寻址操作,将文字池地址中的数据赋值到寄存器中。如果需要将32位立即数赋值到32位寄存器,可以使用这条指令,也可以将

MOVW

MOVT

指令配合使用分别赋值前16位和后16位。

B. 存储器传送数据

不同于x86使用mov指令可实现寄存器、立即数和内存空间的数据交换,ARM使用单独的指令集进行寄存器和内存空间的数据交换,其中基址可以选择任意一个通用寄存器或PC寄存器,变址也可以使用任意一个通用寄存器,较x86更加灵活:

  • LDRB/LDRH/LDR reg1, [<reg2/PC>, <imm32>]<!>

    :赋值8/16/32位

    reg2+imm32

    地址的数据到

    reg1

    ,如果指令后面有叹号,表示指令执行后

    reg2

    值更新为

    reg2+imm32

    ,有叹号可等同于

    LDRB/LDRH/LDR reg1, [<reg2>], <imm32>

    ,这种形式称为后序指令。
  • LDRD reg1, <reg2>, [<reg3/PC>, <imm32>]<!>

    :赋值64位

    reg3+imm32

    地址的数据到

    reg1

    reg2

    ,有叹号可等同于

    LDRD reg1, <reg2>, [reg3], <imm32>

  • LDRSB/LDRSH reg1, [<reg2/PC>, <imm32>]<!>

    :有符号传送8/16位

    reg2+imm32

    地址的数据到

    reg1

    ,目标寄存器会进行32位有符号扩展,有叹号可等同于

    LDRSB/LDRSH reg1, [<reg2>], <imm32>

  • STRB/STRH/STR reg1, [<reg2>, <imm32>]<!>

    :保存寄存器

    reg1

    的8/16/32位值到

    reg2+imm32

    地址,有叹号可等同于

    STRB/STRH/STR reg1, [<reg2>], <imm32>

  • STRD reg1, <reg2>, [reg3, <imm32>]<!>

    :保存寄存器

    reg1

    reg2

    的64位值值到

    reg3+imm32

    地址,有叹号可等同于

    STRD reg1, <reg2>, [reg3], <imm32>

  • LDRB/LDRH/LDR reg1, [<reg2/PC>, reg3{, LSL <imm>}]

    :赋值寄存器

    reg1

    的值为

    reg2/PC+(reg3{<<imm})

    地址处的8/16/32位值
  • LDRD reg1, <reg2>, [<reg3/PC>, <reg4-32>{, LSL <imm>}]

    :赋值寄存器

    reg1

    reg2

    的值为

    reg3/PC+(reg4-32{<<imm})

    地址处的64位值
  • STRB/STRH/STR reg1, [<reg2>, reg3{, LSL <imm>}]

    :保存寄存器

    reg1

    的8/16/32位值到

    reg2+(reg3{<<imm})

    地址
  • LDMIA/LDMDB reg1<!>, <reg-list>

    :将

    reg1

    地址的值按照顺序保存到

    reg-list

    中的寄存器中,如果

    reg1

    后有叹号,则在保存值后自动增加(

    LDMIA

    )或减少(

    LDMDB

    reg1

    。如

    LDMIA R0, {R1-R5}

    LDMIA R0, {R1, R3, R6-R9}

  • STMIA/STMDB reg1<!>, <reg-list>

    :向

    reg1

    地址存入寄存器组中的多个字。如果

    reg1

    后有叹号,则在保存值后自动增加(

    STMIA

    )或减少(

    STMDB

    reg1

注意:后序指令不能使用PC寻址。

C. 入栈出栈

虽然ARM与x86都使用push和pop指令进行入栈和出栈,但ARM可以实现一条指令多次出入栈。

  • PUSH <reg-list>

    :将寄存器组中的寄存器值依次入栈,

    reg-list

    中可以有PC、LR寄存器。
  • POP <reg-list>

    :将出栈的值依次存入寄存器组中的寄存器,

    reg-list

    中可以有PC、LR寄存器。

D. 算术运算

不同于x86指令的大多数算术运算使用两个寄存器,ARM指令的算数运算指令通常包含3个寄存器,实现运算后的自由赋值而不是x86中必须赋值给目标寄存器且目标寄存器必须参与运算。

  • ADD/SUB reg1, <reg2>, <reg3/imm32>

    :计算

    <reg2>(+/-)<reg3/imm32>

    将结果保存到

    reg3

  • ADC/SBC reg1, <reg2>, reg3

    :计算

    <reg2>(+/-)reg3+(进位/借位)

    将结果保存到

    reg3

  • ADC <reg32>, <imm32>

    :计算

    reg32+imm32+进位

    将结果保存到

    reg32

  • SBC reg1, <reg2>, <imm32>

    :计算

    <reg2>-imm32-借位

    将结果保存到

    reg1

  • RSB reg1, <reg2>, <reg3/imm32>

    :计算

    <reg3/imm>-<reg2>

    将结果保存到

    reg1

  • MUL reg1, <reg2>, reg3

    :计算

    <reg2>*reg3

    将结果保存到

    reg1

  • UDIV/SDIV reg1, <reg2>, reg3

    :计算

    <reg2>/reg3

    (无符号/有符号)将结果保存到

    reg1

    ,如果除以0,则结果为0
  • MLA reg1, <reg2>, reg3, <reg4-32>

    :计算

    reg1=<reg2>*reg3+<reg4-32>

  • MLS reg1, <reg2>, reg3, <reg4-32>

    :计算

    reg1=-<reg2>*reg3+<reg4-32>

E. 逻辑运算

ARM支持x86格式的逻辑运算以及3运算符的逻辑运算。

  • AND/ORR/BIC/EOR reg1, <reg2>{, <reg3/imm32>}

    :如果

    reg3/imm

    存在,则表示

    reg1=<reg2>(&/|/&~/^)<reg3/imm32>

    ,否则表示

    reg1=reg1(&/|/&~/^)<reg2>

    (与/或/与非/异或)
  • ORN reg1, <reg2>, <reg3/imm32>

    :表示

    reg1=<reg2>|~<reg3/imm32>

    (或非)

F. 移位运算

  • ASR/LSL/LSR reg1, <reg2>{, <reg3/imm32>}

    :如果

    reg3/imm

    存在,则表示

    reg1=<reg2>(>>/<<)<reg3/imm32>

    ,否则表示

    reg1=reg1(>>/<<)<reg2>

    (算数右移、逻辑左移、逻辑右移)
  • ROR reg1, <reg2>{, reg3}

    :如果

    reg3

    存在,则表示

    reg1=<reg2>(>>)reg3

    ,否则表示

    reg1=reg1(>>)<reg2>

    (循环右移)

G. 符号扩展

对应于x86中的movsx和movzx指令。

  • SXTB/SXTH reg1, <reg2>{, ROR <imm>}

    :右移

    <imm>

    位后有符号扩展

    <reg2>

    的低8/16位并赋值给

    reg1

  • UXTB/UXTH reg1, <reg2>{, ROR <imm>}

    :右移

    <imm>

    位后无符号扩展

    <reg2>

    的低8/16位并赋值给

    reg1

H. 数据反转

将寄存器中的值按字节进行反转。

  • REV reg1, reg2

    :将

    reg2

    中的4字节数据按字节反转后赋值给

    reg1

    reg2

    值不变),原先第0,1,2,3字节的内容被换到了第3,2,1,0字节。
  • REV16 reg1, reg2

    :将

    reg2

    中的4字节以字单位分为高字和低字分别进行反转后赋值给

    reg1

    reg2

    值不变),原先第0,1,2,3字节的内容被换到了第1,0,3,2字节。
  • REVSH reg1, reg2

    :将

    reg2

    中的低2字节反转后有符号扩展赋值给

    reg1

  • REVH reg1, reg2

    REV

    指令的16位表示,只反转低2字节。

I. 位域操作

位域操作允许机器指令对寄存器中的特定位进行处理,在x86中好像是也有这样的指令,只是使用频率太低。

  • BFD reg1, #lsb, #width

    :将

    reg1

    中从第

    lsb

    位开始的连续

    width

    位清零。
  • BFI reg1, reg2, #lsb, #width

    :将

    reg2

    中最低

    width

    位复制到

    reg1

    中从

    lsb

    位开始的连续

    width

    位。
  • CLZ reg1, reg2

    :计算

    reg2

    中高位0的个数并赋值给

    reg1

    ,多用于浮点数计算。
  • RBIT reg1, reg2

    :反转

    reg2

    寄存器中的所有位并赋值给

    reg1

  • SBFX/UBFX reg1, reg2, #lsb, #width

    :取

    reg2

    中从第

    lsb

    位开始的连续

    width

    位并有/无符号扩展,赋值给

    reg1

J. 比较和测试指令

与x86使用

cmp

指令和

test

指令相似,ARM也有关于比较和测试的指令,且实现原理基本相同。

  • CMP reg1, reg2/imm

    :比较两个寄存器或寄存器与立即数,更新标志位APSR。
  • CMN reg1, reg2/imm

    :比较

    reg1

    -reg2

    -imm

    ,更新标志位APSR。
  • TST reg1, reg2/imm

    :参照x86的

    test

    指令,相与测试,更新N(负数位)和Z(零)标志
  • TEQ reg1, reg2/imm

    :异或测试,更新N和Z标志

K. 跳转指令

  • B/B.W <label>

    :无条件跳转到指定位置,

    B.W

    跳转范围更大。
  • BX reg

    :寄存器跳转。
  • BL <label> / BLX reg

    :跳转到指定位置/寄存器值,且将返回地址保存到

    LR

    寄存器中,类比x86的

    call

    指令。一般在函数开头都会首先将

    BL

    寄存器的值保存到栈中便于返回时获取。
  • 条件跳转指令族:类比x86指令:
    • BEQ == je

    • BNE == jne

    • BCS/BHS == jc

      (进位标志为1,可表示无符号大于等于)
    • BCC/BLO == jnc

      (进位标志为0,可表示无符号小于)
    • BMI == js

      (负数标志为1)
    • BPL == jns

      (负数标志为0)
    • BVS == jo

      (溢出标志为1)
    • BVC == jno

      (溢出标志为0)
    • BHI == ja

      (无符号大于)
    • BLS == jbe

      (无符号小于等于)
    • BGE == jge

      (有符号大于等于)
    • BLE == jle

      (有符号小于等于)
    • BGT == jg

      (有符号大于)
    • BLT == jl

      (有符号小于)
  • CBZ/CBNZ reg, <label>

    :比较寄存器的值为0/不为0时跳转(只支持前向跳转)

继续阅读