Arm汇编
zsk Lv4

以下ARM笔记我基于 _周壑 大佬在b站的视频教程整理来的,推荐配合视频使用

环境搭建

arm文档下载链接:https://documentation-service.arm.com/static/644a406baa78c007af74e6fd?token=
其中指令集在目录 F5.1 Alphabetical list of T32 and A32 base instruction set instructions。
在android studio中创建设备,选择api17的,armeabi-v7a架构。
创建后的设备默认路径在 C:\Users\zsk.android\avd,snapshots保存的是快照
快捷方式启动设备:
创建bat文件,输入D:\Android\Sdk\emulator\emulator.exe -avd Pixel_XL_API_17_2,运行启动设备
将ida的android-server push到设备上,android-server文件在ida目录下的dbgsrv
push后修改文件权限为777,运行,端口转发:adb forward tcp:23946 tcp:23946
运行后就可以在ida选择设备调试,Debugger -> Attach -> Remote ARMLinux/Android debugger
hostname填localhost,没有进行端口转发则需要填手机ip,能看到进程列表则成功

c文件编译成arm运行

在window下把c文件编译为arm的前置准备
hello.c

1
2
3
4
5
#include <stdio.h>
int main(){
printf("hello\n");
return 0;
}

在同级目录下新建Application.mk,Android.mk文件
https://developer.android.google.cn/ndk/guides/android_mk?hl=zh-cn

Application.mk

1
2
3
APP_ABI := armeabi-v7a
APP_BUILD_SCRIPT := Android.mk
APP_PLATFORM := android-16

Android.mk

1
2
3
4
5
6
LOCAL_PATH := $(call my-dir)
#include $(CLEAR_VARS)
LOCAL_ARM_MODE := arm
LOCAL_MODULE := "hello"
LOCAL_SRC_FILES := hello.c
include $(BUILD_EXECUTABLE)

编译,可以弄成bat方便执行

1
ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk

需要在Android studio中下载ndk工具,下载好后会在sdk下有ndk文件,进入选择随便一个版本路径配置环境变量
运行显示过程
[armeabi-v7a] Compile thumb : hello <= hello.c
[armeabi-v7a] Executable : hello
[armeabi-v7a] Install : hello => libs/armeabi-v7a/hello
结束后会在目录生成obj和libs文件,在libs下是hello可执行文件,obj下是debugger版的可执行文件,调试的话就用obj下的。
将文件push到手机,修改777权限,执行运行

1
2
3
root@android:/data/local/tmp # chmod 777 hello
root@android:/data/local/tmp # ./hello
hello

这样一下子就运行玩完了,没法调试,修改代码让它一直运行

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(){
while(1){
printf("hello\n");
getchar();
}
return 0;
}

重新编译并推送执行

ida调试

回到ida,调试运行中的hello程序

避免每次都要执行多行命令,可以整合成一个bat

1
2
3
adb push D:\Android\arm_test\obj\local\armeabi-v7a\hello /data/local/tmp/
adb shell "chmod 777 /data/local/tmp/hello"
adb shell "/data/local/tmp/hello"

寄存器和指令基本格式

没有隐式内存操作指令,

一条ARM汇编指令可以包含0到3个操作数。操作数是指执行指令时所涉及的数据或寄存器。内存操作数和里脊操作数不能同时存在,意味着在一条指令中,不能同时存在既是内存操作数又是寄存器操作数。你要么使用内存地址作为操作数,要么使用寄存器。内存操作数至多出现一次,寄存器操作数总在最前面

  • 特殊情况:

    • 读PC寄存器,arm读PC加8,thumb读PC加4
    • C标志位使用
  • 寄存器:

    • 寄存器是计算机中一种高速的、临时的、可用于存储和操作数据的存储单元。在ARM架构中,通常使用 R0、R1、R2 等寄存器来表示通用寄存器。这些寄存器可以用来存储临时数据、地址或执行算术和逻辑运算。
  • 立即数:

    • 立即数是在指令中直接提供的常数值,而不是从内存中加载。在汇编语言中,可以使用 # 符号表示立即数。例如,在 MOV R0, #10 中,#10 就是一个立即数,表示将值 10 直接存储到寄存器 R0 中。
  • 操作数:

    • 操作数是参与运算的数据或者指令中的一个参数。在指令中,你可能会看到源操作数和目标操作数。在 MOV R0, #10 中,#10 是源操作数,表示移动的数据;而 R0 是目标操作数,表示数据要移动到的位置。
  • MOV:

    • 用于将一个数值(立即数或寄存器中的值)移动到目标寄存器。
      MOV R0, #10 ; 将立即数 10 移动到寄存器
      R0 MOV R1, R2 ; 将寄存器 R2 中的值移动到寄存器 R1
  • 基本运算:

    • ADD:加法指令。
    • ADR:将地址加载到寄存器的伪指令。
    • SUB:减法指令。
    • RSB:反向减法指令。
    • AND:按位与运算指令。
    • BIC:按位与非运算指令。
    • ORR:按位或运算指令。
    • EOR:按位异或运算指令。
    • LSL:逻辑左移指令。
    • LSR:逻辑右移指令。
    • ASR:算术右移指令。
  • 访存:

    • LDR:从内存中加载数据到寄存器。
    • STR:将寄存器中的数据存储到内存。
  • 块访存:

    • LDMFD:从内存中加载多个寄存器,然后递减栈指针。
    • LDMIA:从内存中加载多个寄存器,然后递增基址寄存器。
    • STMFD:将多个寄存器的值存储到内存,然后递减栈指针。
    • STMIA:将多个寄存器的值存储到内存,然后递增基址寄存器。
  • 分支:

    • B:无条件分支。
    • BL:带链接的无条件分支,用于函数调用。
    • BX:无条件分支并切换指令集(ARM/Thumb)。
    • BLX:带链接的无条件分支并切换指令集。

在ida中按ctrl+alt+k可以修改指令

条件和标志位响应

条件指令

image
条件指令是加在运算符后面的,如何看懂?
例如机器码为:
03 00 10 E3 TST
小端序排序的,前高4位是条件,看最高位也就是E,E的对应的bit为1110,也就是None,无条件,可以说E是出现最多的
06 00 00 0A BEQ
最高位0,0就是EQ,
03 00 52 21 CMPCS
最高位为2,2就是0010,CS
指令后缀带S的,表示执行完标志位会改变
01 00 80 E0 ADD R0, R0, R1
01 00 90 E0 ADDS R0, R0, R1
01 00 40 E0 SUB R0, R0, R1
01 00 50 E0 SUBS R0, R0, R1
S是由第20个bit控制,也就是上面的8,9

image

image

S位为0就是不带S,为就是S
大部分算术指令都可以加S,CMP不行,CMP的20位固定为1

  • 运算指令可以分为以下三种:
    • 第一种既要结果又要标志寄存器,subs,adds
    • 第二种只要结果,sub,add
    • 第三种只要标志寄存器,cmp,cmn

MOV指令

立即数

Move(immediate)将立即数值写入目标寄存器。
mov是不访问内存,没有写内存的功能。那就只有寄存器操作数或者立即操作数
mov是把第二个操作数(可能是寄存器也可能是立即数)写到第一个操作数,那第一个操作数只能是寄存器操作数
arm指令是定长32位的,也就是立即数长度肯定不会超过32位

image
这里立即数的位数是imm4+imm12,也就是16位
例如:
34 82 01 E3 MOVW R8, #0x1234
如果是超过16位,则写不进。
第15-12位4位二进制的长度刚好16对应着r0-r15
还有一种A1格式,立即数是可以超过16-32位的

image

02 81 A0 E3 MOV R8, #0x80000000
虽然这里长度比16位大,但是它的有效位只有最高位的8,这里是把2向右移动1*2位, 0010 ->右移2位-> 1000 -> 8(十进制)
01 81 A0 E3 MOV R8, #0x40000000
把1向右移动1*2位,0001 ->右移2位-> 0100 -> 4(十进制)
02 82 A0 E3 MOV R8, #0x20000000
把2向右移动4位,0010 ->右移4位-> 0010 -> 2(十进制)
立即数的有效位数比较密集,可以集中在8位范围内还是偶数
如果真要写入32位有效数如何做?
先写一条指令 mov r0, 0x5678
再写 movt r0, 0x1234 写到高16位,ida会两句合成一句伪指令
78 06 05 E3 34 02 41 E3 MOV R0, #0x12345678

寄存器

Move(register)将值从寄存器复制到目标寄存器。

上面说的是立即数到寄存器,这个是寄存器到寄存器

image

除了: 01 00 A0 E1 MOV R0, R1
还能: 01 02 A0 E1 MOV R0, R1,LSL#4 (R0是一个操作数,R1,LSL#4整体是一个操作数)R1向左移动4位写到r0里
看第7-11位,5位的长度也就是2的32次方来表示偏移
第5-6位,stype2位的长度有4种情况,逻辑左移,逻辑右移,算术右移 ,循环移位(算术左移的逻辑和逻辑左移是一样的,循环移位不区分左右移,比如循环左移1位和循环右移32位是一样的)
此指令由别名 ASRS (immediate), ASR (immediate), LSLS (immediate), LSL (immediate), LSRS (immediate), LSR (immediate), RORS (immediate), ROR (immediate), RRXS, and RRX使用.
指令lsl r0, r1,4 的效果跟 R0, R1,LSL#4 是一样的
所以 ASRS, ASR, LSLS, LSL, LSRS, LSR, RORS, ROR, RRXS, RRX 实际上都可以认为是mov指令的一个宏,另一种写法,在ida中都会翻译成mov指令
常用的也就是逻辑左移,逻辑右移,算术右移,都是一些数组,结构体偏移寻址的

寄存器移位寄存器

Move(寄存器移位寄存器)将寄存器移位后的寄存器值复制到目标寄存器。

image

例如:11 02 A0 E1 MOV R0, R1,LSL R2

基本整型运算

指令基本都是3个操作数,第一个是写入,第二三个是做运算。(寄存器,寄存器,立即数) 或者是 (寄存器,寄存器,寄存器)。

下面指令都有多种格式,立即数,寄存器,移位寄存器等等。只列举立即数的情况

ADD, ADDS (immediate)

加法指令。

image

立即数长度12,分为有效数字低8位,高4位做位移循环。这种叫A32ExpandImm,把imm12扩展成32位

ADD, ADDS (register)

image

ADR

将地址加载到寄存器的伪指令。
从PC相对地址将立即值添加到PC值以形成PC相对地址,并写入结果发送到目标寄存器。
该指令由伪指令ADD(立即,到PC)和SUB(立即,从PC)使用。这个伪指令从来都不是首选的反汇编。
例如指令 add, r0, pc, 4(把pc+4赋给r0)会在ida变成
04 00 8F E2 ADR R0, loc_16FD4
从pc+4会涉及到读pc,读pc 要+8,加上4就是12
00016FD0 04 00 8F E2 ADR R0, loc_16FDC
00016FDC loc_16FDC

SUB

减法指令。
从PC中减去从对齐(PC,4)值减去立即值以形成一个PC相关地址,并将结果写入目标寄存器。
02 00 41 E0 SUB R0, R1, R2(r0=r1-r2)

RSB

反向减法指令。
反向减法指令,反向减法(立即数)从立即数中减去寄存器值,并将结果写入目的地寄存器。
08 00 61 E2 RSB R0, R1, #8(8-r1的值写入r0)
08 00 61 E2 RSB R0, R1, R2(r2-r1的值写入r0)

AND

按位与运算指令。
按位与(立即数)对寄存器值和立即数执行位与,并将结果写入到目标寄存器。
02 00 01 E0 AND R0, R1, R2(r0 = r1 & r2)

BIC

按位与非运算指令。
逐位清除(立即数)对寄存器值和立即数的补码执行逐位“与”运算,并将结果写入目标寄存器。
相当于第二个操作数和第三个操作数取反之后取and
02 00 C1 E1 BIC R0, R1, R2(r0 = r1 & ~r2)

ORR

按位或运算指令。
逐位OR(立即数)执行寄存器值和立即数的逐位(包括)OR,并将结果写入目标寄存器。
02 00 81 E1 ORR R0, R1, R2 (r0 = r1 | r2)

EOR

按位异或运算指令。
逐位异或(立即数)对寄存器值和立即数执行逐位异运算,并将结果写入目标寄存器。
02 00 21 E0 EOR R0, R1, R2(r0 = r1 ^ r2)

LSL

逻辑左移指令。
逻辑左移(立即数)将寄存器值左移立即数位,移位为零,并将结果写入目标寄存器。

lsl r0, r1, r2
11 02 A0 E1 MOV R0, R1,LSL R2(r0 = r1 << r2)

LSR

逻辑右移指令。
逻辑右移(立即数)将寄存器值右移一个立即数位数,移位为零,并将结果写入目标寄存器。

lsr r0, r1, r2
31 02 A0 E1 MOV R0, R1,LSR R2(r0 = r1 >> r2)

ASR

算术右移指令。
算术右移(立即数)将寄存器值右移立即位数,将其符号位的副本移位,并将结果写入目标寄存器。

asr r0, r1, r2
51 02 A0 E1 MOV R0, R1,ASR R2(r0 = r1 >> r2)

asr r0, r1, r2和lsr r0, r1, r2是一致的吗

在一般情况下,asr r0, r1, r2 和 lsr r0, r1, r2 是不一致的。
asr r0, r1, r2 执行算术右移,将 r1 的所有位向右算术移动 r2 位,并将结果存储到 r0 中。算术右移在移位过程中用符号位填充左侧空出的位,即最高位(符号位)保持不变。
lsr r0, r1, r2 执行逻辑右移,将 r1 的所有位向右移动 r2 位,并将结果存储到 r0 中。逻辑右移在移位过程中用0填充左侧空出的位。
因此,在 asr 中,符号位会影响右移时左侧空出位的填充值,而在 lsr 中,左侧空出位总是用0填充。如果 r1 是一个带符号整数,asr 和 lsr 通常会得到不同的结果。

  • 举例:
    • 当考虑一个假设的8位寄存器的情况,其中最高位(MSB)表示符号(0表示正数,1表示负数)时,我们可以通过一个例子来说明 asr 和 lsr 的区别。

假设 r1 是二进制表示为 11011010,这是一个用二进制补码表示的负数。现在,让我们使用 asr 和 lsr 进行右移操作。

1
2
3
4
5
6
7
; 假设 r1 是 11011010(二进制表示),是一个负数

asr r0, r1, #1 ; 算术右移 1 位
; 经过 asr,r0 将变为 11110110(二进制表示),仍然是一个负数

lsr r0, r1, #1 ; 逻辑右移 1 位
; 经过 lsr,r0 将变为 01110101(二进制表示),是一个正数

在这个例子中,asr 在右移过程中保留了符号位,导致结果仍然是负数。而 lsr 在填充左侧空出位时使用了 0,导致结果变为正数。这展示了算术右移和逻辑右移在处理符号位上的差异。

访存指令

访存:
LDR:从内存中加载数据到寄存器。
STR:将寄存器中的数据存储到内存。

1.数据流向
2.操作的寄存器和内存地址
3.后续附加行为
块访存指令的语法是 {寄存器列表},其中寄存器列表中的寄存器按照从右到左的顺序依次被处理。这意味着在执行块访存指令时,先处理列表中的最右边的寄存器,然后依次向左处理。

LDR

加载寄存器(立即)从基本寄存器值和立即偏移中计算一个地址,从内存中加载单词,然后将其写入寄存器。 它可以使用偏移,索引或预先指定的地址。
04 00 91 E5 LDR R0, [R1,#4]
r1是一个基址寄存器,4是偏移量,表示从基址开始往后移动4个字节的位置。
也可以使用负偏移
04 00 11 E5 LDR R0, [R1,#-4]
02 00 91 E7 LDR R0, [R1,R2]
02 02 91 E7 LDR R0, [R1,R2,LSL#4]
上面指令都是没有后续附加行为的
04 00 B1 E5 LDR R0, [R1,#4]! (加了!后除了把值写进r0,还会更新r1的地址,地址+4)

上述的指令都可以算是内偏移,还有外偏移的
04 00 91 E4 LDR R0, [R1],#4(从r1读出来的内存给r0后,后续行为把r1+4)

STR

存储寄存器(立即寄存器)根据基址寄存器值和立即偏移量计算地址,并将寄存器中的值存储到内存。它可以使用偏移量、后索引或前索引寻址。
0404 00 81 E4 STR R0, [R1],#4(将寄存器 R0 中的值存储到内存地址 R1 指向的位置,然后将 R1 的值递增 4(即地址加上 4))

可以使用LDR,STR来操作sp栈指针寄存器,达到pop和push的效果
当需要从sp取值到寄存器的时候,也就是pop,要用ldr,因为pop取值释放了空间,所以取值后sp栈指针要加4
而向sp放入值的时候,也就是push,要用str,因为push要分配空间,所以放值后sp栈指针要减4。
对于典型的栈操作,递减栈指针意味着在栈上分配一段新的空间,而递增栈指针则表示释放栈上的空间。在常见的体系结构中,栈是向低地址方向增长的,因此递减栈指针实际上是在向栈的底部分配空间。

  • pop:

    • LDR R0, [SP], #4
      等同于下面两条
      ldr r1, [sp] // 从栈中加载值到寄存器
      r1 add sp, sp, #4 // 栈指针递增4个字节
  • push:

    • STR R0, [SP, #-4]!
      等同于下面两条
      sub sp, sp, #4 // 栈指针递减4个字节
      str r0, [sp] // 将寄存器 r0 中的值存储到栈中

image

ldr的12位立即数是0扩展的,只支持12位,第23位U是表示正负的,第24位P是表示内外偏移的。str也是同理

块访存指令

块访存也是访存,读内存就是LDM开头,写内存STM开头,M表示多个数据或多个寄存器
当我们在使用LDM或STM指令时,是想加载或存储寄存器值之前还是之后增加或减少基址寄存器的值,这影响了加载或存储完成后基址寄存器的最终值。
LDM和STM指令可以跟随以下后缀:
IA,IB,DA,DB。第一个操作数后续还能加 ! :表示在加载或存储完成后更新寄存器。

image

简单归纳就是
IA:访问这块指针后,再向后加4。
IB:还没访问这块指针,先向后加4,再访问。
DA:反向读4字节,然后减4
DB:先减4,然后再访问
!表示最后指针的地址是否更新
常用只有LDMIA!,STMDB!,当第一个操作数为sp,等价于pop和push
再ida中指令为 stmdb sp!,{r0-r3} 会自动替换为
0F 00 2D E9 PUSH {R0-R3}

0F 00 2D E9 STMFD SP!, {R0-R3}
这两个是一样,在不同ida版本会显示不同
当第一个操作数是sp!时,ida会翻译成STMFD。第二个操作数无论是写 {R2,R1,R4,R3} 也会翻译成 {R1-R4},寄存器标号小的放在低地址,寄存器标号大的放在高地址。
FD是用来操作栈的,当第一个操作数是sp时候,LDMFD等价于LDMIA,STMFD等价于STMDB。ida也会优先翻译为LDMFD,STMFD。当sp加了!后,则会优先翻译为POP和PUSH。

  • 例子:
    • LDMIA SP, {R1-R4} ==> LDMFD SP, {R1-R4}
      STMDB SP, {R1-R4} ===> STMFD SP, {R1-R4}
      LDMIA SP!, {R1-R4} ==> POP {R1-R4}
      STMDB SP!, {R1-R4} ==> PUSH {R1-R4}

对于初学者先简单理解掌握下面三种
STMFD == PUSH
LDMFD == POP
***IA: 快速复制内存,根据前缀看是读还是写

分支和模式切换

分支:B,BL,BX,BLX

  1. 跳转目标
    2. 模式切换,thumb和arm的切换
    3. 写入LR的值

后续是加立即数还是寄存器,分为以下几种情况:
B imm
BL imm
BX reg
BLX imm
BLX reg

B

image

B指令是无条件跳转,不带模式切换,T标志位不变。如果是从arm跳转thumb,或者thumb跳转到arm,需要保证跳转前后的模式统一。把指令机器码改为 00 00 00 EA,会发现要跳转的地址是当前地址+8,01 00 00 EA 则要跳转的地址是8+立即数4(当前立即数为1)。如果是thumb模式下,则是8+立即数2。
当00 00 00 EA会加8,那么FE FF FF EA 则是8+(-2*4),即一直跳转到自己当前地址,进入死循环。
FE FF FF EA 需要记住,当一个进程很快执行结束的时候,又需要进行调试的时候。
用ida找到文件_start的地方,elf文件加载到内存中,_start是程序的入口点。可以同010 Editor对这个入口位置的指令修改为 FE FF FF EA。push到设备重新运行就断住了,再把指令改回来,就可以调试。
B指令的立即数可以寻址的范围是从当前指令的地址向前或向后移动的距离。由于立即数是相对偏移量,因此它可以覆盖的地址范围是从PC - 32MB到PC + 32MB。thumb模式下立即数只会更短

  • 例如:

    1. 假设当前指令的地址是0x80000000(32位地址),并且B指令的立即数字段(imm24)的值为0x001234。将imm24与两个零(’00’)连接起来,得到一个32位的立即数:imm32 = 0x00123400。
    2. 如果imm24是一个正数(无符号值),那么imm32的最高有效位将为零,扩展后的结果仍然是正数。例如,如果imm24的值为0x001234,那么imm32 = 0x00123400。
    3. 如果imm24是一个负数(有符号值),那么imm32的最高有效位将为1,扩展后的结果仍然是负数。例如,如果imm24的值为0xFF1234,那么imm32 = 0xFF123400。
    4. 将PC的当前值(0x80000000)与imm32进行相加,即PC + imm32,就可以得到B指令跳转的目标地址。目标地址为0x80000000 + 0x00123400 = 0x80123400,根据前面提到的寻址范围,PC - 32MB到PC + 32MB是从0x7F800000到0x80000000+0x02000000的范围。在这个例子中,PC的当前值为0x80000000,而且目标地址0x80123400是在这个范围之内,因此B指令是可以正确寻址和跳转的。

BL

BL指令后面加立即数,也是无条件跳转,但是会写入LR的值。执行完会把bl指令的下一条指令的地址写入到LR。如果是在thumb下,会把bl下一条指令的地址异或T位,也就是1,地址会变成奇数写入LR。当程序需要返回的时候通过LR的最低位来返回arm模式还是thumb模式,0就是arm,1就是thumb。

BX

BX指令后面加寄存器,BX会把寄存器的值拆成两部分,最低位会写入T标志位,用于指示跳转的目标指令集,剩余的部分写入pc寄存器,作为跳转的目标地址。通俗点就是如果地址是奇数,最低位就是1,偶数则是0。T标志位用于指示跳转目标的指令集,0表示ARM指令集,1表示Thumb指令集。

BLX

有两种情况,立即数和寄存器。
BLX+立即数是一定会带有模式切换的,并会把执行完会把blx指令的下一条指令的地址写入到LR,把当前T标志位写入LR值的最低位后再更新T标志的值。
BLX+寄存器跟BX+寄存器一样,会把执行完会把blx指令的下一条指令的地址写入到LR,是否模式切换以及T标志的值看寄存器地址的最低位。

使用mov跳转

跟BX reg做对比, mov pc, r0 指令将会直接将寄存器 R0 的值复制到程序计数器 PC,这意味着它是一个非条件的直接分支,不会进行任何模式切换,而且不会修改 T 标志位。

使用ldr跳转

直接从内存加载地址到pc, ldr pc, [r0],把r0寄存器指向的值给pc,这种也会根据寄存器的值奇偶数来切换模式,并更新T标志位。
有三种跳转指令 B系列指令,mov,ldr。其中mov最弱,不带有模式切换,一般也很少用。
ldr指令调用一个导入表的方法

判断导出表函数的模式

怎么判断导出表的函数时arm模式还是thumb模式?
再ida中查看libc的导出表函数时,address都是偶数,难道全都是arm模式吗?比如printf函数,地址是偶数,点击进去又是thumb指令。那在其它文件调用这个导出表函数时,如何正确知道函数是哪种模式?
readelf -s libc.so > a.txt
可以显示库的所有导出符号表,

1
2
3
...
657: 0001e775 36 FUNC GLOBAL DEFAULT 7 printf
...

下面是在ida中的地址
!image

在ida中的地址是1e774,通过readelf的地址是1e775,所以就能判断出printf是thumb模式的,在调用printf的时候,会把拆成两部分,最低位写入T标志位,跳转的时候自动模式切换了。
这也就解释了在使用frida在hook thumb函数地址的时候为什么要加1了。

Thumb模式

在thumb下短指令也就是2字节的指令一般不使用r8-r12寄存器,如果使用了就会编译为长指令。
一般也没有条件码和标志响应位,运算指令优先对第一,第二操作数相同情况下用短指令编码,以提高代码密度。这种优化可以减少指令数量和代码大小。
比如:add r0, r0, r1 会自动转为 add r0, r1
还有一些情况当短指令编码编译不过的时候编译器会在指令后缀+.w进行4字节指令编码。
add.w r0, r0, r1

IT指令块

IT 指令是 Thumb 指令集中的一种条件执行指令,用于在紧接着的多条指令中根据条件选择性地执行。IT 指令块由一个条件码和最多四条紧接着的指令组成,它们构成了一个条件执行块。在条件执行块中,只有在满足条件时才会执行相应的指令。
IT 指令的格式如下:
IT{<x>{<y>{<z>}}}{<q>} <cond>

image

IT 指令块由可选的后缀(x, y, z)和一个可选的条件码(q)组成。
后缀 x, y, z 分别指示条件执行块中的第一条、第二条和第三条指令。
<q> 是可选的条件码,用于指定条件执行块的执行条件。 如果条件码 <q> 不存在,则默认情况下,IT 指令块中的所有指令都受到相同的条件码 控制。
T表示if,E表示else
05 BF ITTET EQ
01 20 MOVEQ R0, #1
02 21 MOVEQ R1, #2
03 22 MOVS R2, #3
04 23 MOVSEQ R3, #4
在ida中比如指令 movs r0, #1 如果处于IT指令块里面,会自动给加上条件码。
假如这段指令当前的Z标志位是0,那EQ条件就不成立
it指令块下的第一条指令跟条件码EQ的条件一样的,不执行
it指令块下的第二条指令是看IT后面第一位是T,那也就是跟EQ的条件一样,不执行
it指令块下的第三条指令是看IT后面第二位是E,跟EQ的条件相反,执行
第四条是T,不执行

调用约定和栈帧分析

调用约定

前4个参数:r0-r3,其他参数栈传递
非易变寄存器:r4-r11

r11: 栈帧指针
r12: 导入表寻址
测试代码,使用ndk build后用ida打开

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
int fun(int a, int b, int c, int d, int e, int f){
int n = d + e;
printf("%d\n", n);
return n;
}
int main(){
fun(1, 2, 3, 4, 5, 6);
return 0;
}

前4个参数

前4个参数通常会被依次传递到寄存器 R0 到 R3 中。这样设计的原因是为了尽可能地利用寄存器,以提高函数调用的效率。如果函数需要的参数超过4个,那么额外的参数就会被放置在栈上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.text:00000628                               ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000628 EXPORT main
.text:00000628 main ; DATA XREF: .text:0000057C↑o
.text:00000628 ; __unwind {
.text:00000628 00 48 2D E9 PUSH {R11,LR}
.text:0000062C 0D B0 A0 E1 MOV R11, SP
.text:00000630 10 D0 4D E2 SUB SP, SP, #0x10
.text:00000634 00 00 00 E3 MOVW R0, #0
.text:00000638 04 00 0B E5 STR R0, [R11,#-4]
.text:0000063C 01 00 00 E3 MOVW R0, #1 ; a 参数1放到寄存器r0
.text:00000640 02 10 00 E3 MOVW R1, #2 ; b 参数2放到寄存器r1
.text:00000644 03 20 00 E3 MOVW R2, #3 ; c 参数3放到寄存器r2
.text:00000648 04 30 00 E3 MOVW R3, #4 ; d 参数4放到寄存器r3
.text:0000064C 05 C0 00 E3 MOVW R12, #5
.text:00000650 00 C0 8D E5 STR R12, [SP] ; e 参数5先放在r12,在从r12读取放到sp上
.text:00000654 06 C0 00 E3 MOVW R12, #6
.text:00000658 04 C0 8D E5 STR R12, [SP,#4] ; f 参数6先放在r12,在从r12读取放在sp+4上
.text:0000065C D7 FF FF EB BL fun
.text:0000065C
.text:00000660 00 10 00 E3 MOVW R1, #0
.text:00000664 08 00 8D E5 STR R0, [SP,#8]
.text:00000668 01 00 A0 E1 MOV R0, R1
.text:0000066C 0B D0 A0 E1 MOV SP, R11
.text:00000670 00 88 BD E8 POP {R11,PC}
.text:00000670 ; } // starts at 628
.text:00000670
.text:00000670 ; End of function main

非易变寄存器

在 ARM 架构中,寄存器 R4 到 R11 被称为非易失性寄存器,通常用于存储在函数调用期间需要被保留的临时数据,因此,如果一个函数在调用期间修改了这些寄存器的值,它必须在返回之前恢复这些寄存器的原始值,以确保调用者的寄存器值不会被破坏。

栈帧指针

在 ARM 架构中,寄存器 R11 通常被用作栈帧指针,栈帧指针是一个指向当前函数栈帧的指针,栈帧是在函数调用期间分配的一块内存区域,用于存储局部变量、函数参数、返回地址等信息。 通常情况下,当一个函数被调用时,它会在栈上创建一个新的栈帧,并将栈帧指针指向这个新的栈帧。在函数内部,通过栈帧指针可以方便地访问栈帧中的局部变量和参数。而当函数返回时,栈帧指针会被恢复到上一个函数的栈帧,以便正确地返回到调用函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.text:000005C0                               ; int fun(int a, int b, int c, int d, int e, int f)
.text:000005C0 EXPORT fun
.text:000005C0 fun ; CODE XREF: main+34↓p
.text:000005C0 ; __unwind {
.text:000005C0 10 4C 2D E9 PUSH {R4,R10,R11,LR}
.text:000005C4 08 B0 8D E2 ADD R11, SP, #8
.text:000005C8 20 D0 4D E2 SUB SP, SP, #0x20
.text:000005CC 0C C0 9B E5 LDR R12, [R11,#f]
.text:000005D0 08 E0 9B E5 LDR LR, [R11,#e]
.text:000005D4 48 40 9F E5 LDR R4, =0x1B0
.text:000005D8 04 40 8F E0 ADD R4, PC, R4 ; "%d\n"
.text:000005DC 0C 00 0B E5 STR R0, [R11,#-0xC]
.text:000005E0 10 10 0B E5 STR R1, [R11,#-0x10]
.text:000005E4 14 20 8D E5 STR R2, [SP,#0x14]
.text:000005E8 10 30 8D E5 STR R3, [SP,#0x10]
.text:000005EC 10 00 9D E5 LDR R0, [SP,#0x10]
.text:000005F0 08 10 9B E5 LDR R1, [R11,#e]
.text:000005F4 01 00 80 E0 ADD R0, R0, R1
.text:000005F8 0C 00 8D E5 STR R0, [SP,#0xC]
.text:000005FC 0C 10 9D E5 LDR R1, [SP,#0xC]
.text:00000600 04 00 A0 E1 MOV R0, R4 ; format
.text:00000604 08 C0 8D E5 STR R12, [SP,#8]
.text:00000608 04 E0 8D E5 STR LR, [SP,#4]
.text:0000060C C3 FF FF EB BL printf
.text:0000060C
.text:00000610 0C 10 9D E5 LDR R1, [SP,#0xC]
.text:00000614 00 00 8D E5 STR R0, [SP]
.text:00000618 01 00 A0 E1 MOV R0, R1
.text:0000061C 08 D0 4B E2 SUB SP, R11, #8
.text:00000620 10 8C BD E8 POP {R4,R10,R11,PC}
.text:00000620
.text:00000620 ; End of function fun

函数开头
PUSH {R11,LR}
MOV R11, SP
这两条指令是函数开头的典型指令序列,通常用于建立函数的栈帧。这里的操作可以理解为:
PUSH {R11, LR}:将当前函数的栈帧指针 R11 和返回地址 LR 压入栈中。这是为了保存这两个寄存器的值,以便函数执行完毕后能够正确地恢复到函数调用前的状态。
MOV R11, SP:将当前栈指针 SP 的值复制到 R11 中。这样做是为了在函数中能够方便地访问栈帧中的局部变量和其他信息。通常情况下,函数的局部变量和参数会相对于栈帧指针来进行访问。 通过这两条指令的组合,函数建立了自己的栈帧,并将栈帧指针存储在 R11 中,以便在函数内部能够轻松地访问栈上的数据。
函数结尾
MOV SP, R11 将栈帧指针 R11 的值赋给栈指针 SP,恢复栈指针到函数调用前的状态。
POP {R11,PC} 从栈中依次弹出 R11 和 PC 寄存器的值。

导入表寻址

R12 寄存器用于进行导入表寻址。导入表是一个数据结构,存储了函数的地址或函数指针。在动态链接库(DLL)或共享库中,当程序调用一个外部函数时,需要在运行时动态解析函数的地址,这就需要导入表。

1
2
3
4
5
6
7
.plt:00000520                               ; int printf(const char *format, ...)
.plt:00000520 printf ; CODE XREF: fun+4C↓p
.plt:00000520 00 C6 8F E2 ADR R12, 0x528
.plt:00000524 01 CA 8C E2 ADD R12, R12, #0x1000
.plt:00000528 D0 FA BC E5 LDR PC, [R12,#(printf_ptr - 0x1528)]! ; __imp_printf
.plt:00000528
.plt:00000528 ; End of function printf

注意:函数的返回值是放在R0里的
下面是根据上面反汇编模拟执行的栈指针,根据执行main和fun函数的栈地址偏移表示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$-30 |	ret_printf		<-- sp fun
$-2C | 5 (LDR LR, [R11,#8],STR LR, [SP,#4],将R11+0x8的值放到这)
$-28 | 6 (LDR R12, [R11,#0xC], STR R12, [SP,#8],将R11+0xC的值放到这)
$-24 | 9(n) (LDR R0, [SP,#0x10], LDR R1, [R11,#8], ADD R0, R0, R1,将$-20和$的值4+5放到这)
$-20 | R3
$-1C | R2
$-18 | R1
$-14 | R0
$-10 | R4
$-C | R10
$-8 | R11 <-- R11, 前一个函数main函数R11的位置
$-4 | LR
$==> | 5 <-- sp main / fun in
$+4 | 6
$+8 | ret_fun
$+C | 0
$+10 | R11 <-- R11,前一个函数R11的位置
$+14 | LR
$+18 | <-- main函数里栈指针稳定时候的位置,main in 0x4+0x4+0x10
$+1C |
$+20 |
$+24 |
$+28 |
$+2C |
$+30 |
 评论