ARM 汇编语言
一门语言通常有自己的关键字、代码规范、子程序调用、注释等,汇编语言也一样
汇编语言:将一系列与处理器相关的汇编指令用某种语法和结构组织在一起的程序语言形式
用特定汇编语法规范编写的汇编代码,可被完整地编译或嵌入其他高级语言
Android 中的 ARM 汇编使用 GNU 汇编格式,现在开始学习 GNU ARM 汇编的一般语法格式及特点
ARM 汇编程序结构
编译器在编译阶段会在内部将程序代码编译成与机器相关的汇编指令,上一节的 hello.c 的汇编代码就是一个完整的 ARM 汇编程序
现在编写一个新的程序 app.c,代码:
#include
int add(int a, int b, int c, int d)
{
return a + b + c + d;
}
int main(int argc, char const *argv[])
{
printf("add: %dn", add(1, 2, 3, 4));
return 0;
}
执行如下命令,生成 app.s 汇编代码:
系统、NDK 版本、CC 环境变量设置等信息见上一节
app.s 内容如下:
.text
.syntax unified
.cpu arm1022e
.eabi_attribute 6, 4 @ Tag_CPU_arch
.eabi_attribute 8, 1 @ Tag_ARM_ISA_use
.eabi_attribute 15, 1 @ Tag_ABI_PCS_RW_data
.eabi_attribute 16, 1 @ Tag_ABI_PCS_RO_data
.eabi_attribute 17, 2 @ Tag_ABI_PCS_GOT_use
.eabi_attribute 20, 1 @ Tag_ABI_FP_denormal
.eabi_attribute 21, 1 @ Tag_ABI_FP_exceptions
.eabi_attribute 23, 3 @ Tag_ABI_FP_number_model
.eabi_attribute 24, 1 @ Tag_ABI_align_needed
.eabi_attribute 25, 1 @ Tag_ABI_align_preserved
.eabi_attribute 18, 4 @ Tag_ABI_PCS_wchar_t
.eabi_attribute 26, 2 @ Tag_ABI_enum_size
.file "app.c"
.globl add
.align 2
.type add,%function
add: @ @add
.fnstart
.Leh_func_begin0:
@ BB#0: @ %entry
.pad #16
sub sp, sp, #16
str r0, [sp, #12]
str r1, [sp, #8]
str r2, [sp, #4]
str r3, [sp]
ldr r0, [sp, #12]
ldr r1, [sp, #8]
add r0, r0, r1
ldr r1, [sp, #4]
add r0, r0, r1
ldr r1, [sp]
add r0, r0, r1
add sp, sp, #16
bx lr
.Ltmp0:
.size add, .Ltmp0-add
.cantunwind
.fnend
.globl main
.align 2
.type main,%function
main: @ @main
.fnstart
.Leh_func_begin1:
@ BB#0: @ %entry
.save {r4, r5, r11, lr}
push {r4, r5, r11, lr}
.setfp r11, sp, #8
add r11, sp, #8
.pad #24
sub sp, sp, #24
ldr r2, .LCPI1_6
.LPC1_0:
add r2, pc, r2
ldr r3, .LCPI1_2
ldr r12, .LCPI1_3
ldr lr, .LCPI1_4
ldr r4, .LCPI1_5
ldr r5, .LCPI1_0
str r5, [r11, #-12]
str r0, [sp, #16]
str r1, [sp, #12]
mov r0, r3
mov r1, r12
str r2, [sp, #8] @ 4-byte Spill
mov r2, lr
mov r3, r4
bl add(PLT)
ldr r1, .LCPI1_1
ldr r2, [sp, #8] @ 4-byte Reload
add r1, r1, r2
str r0, [sp, #4] @ 4-byte Spill
mov r0, r1
ldr r1, [sp, #4] @ 4-byte Reload
bl printf(PLT)
ldr r1, .LCPI1_0
str r0, [sp] @ 4-byte Spill
mov r0, r1
sub sp, r11, #8
pop {r4, r5, r11, pc}
.align 2
@ BB#1:
.LCPI1_0:
.long 0 @ 0x0
.LCPI1_1:
.long .L.str(GOTOFF)
.LCPI1_2:
.long 1 @ 0x1
.LCPI1_3:
.long 2 @ 0x2
.LCPI1_4:
.long 3 @ 0x3
.LCPI1_5:
.long 4 @ 0x4
.LCPI1_6:
.long _GLOBAL_OFFSET_TABLE_-(.LPC1_0+8)
.Ltmp1:
.size main, .Ltmp1-main
.cantunwind
.fnend
.type .L.str,%object @ @.str
.section .rodata.str1.1,"aMS",%progbits,1
.L.str:
.asciz "add: %dn"
.size .L.str, 9
.ident "clang version 3.5 "
一个完整的汇编程序由以下几部分组成:
处理器类型声明:可用 .arch 指定处理器架构,用 .cpu 指定处理器型号。若使用浮点指令,建议用 .fpu 指定协处理器类型,如 softvfp、neon
代码与数据段声明:一个完整程序,编译后都会有用于存放代码的代码段(在 ARM EABI 的 ELF 中段名为 .text),以及用于存放数据的数据段(在 ARM EABI 的 ELF 中段名为 .rodata)。一些特殊的程序可能没数据段,但通常有代码段。在汇编代码中声明段时要用伪指令 .section,代码段的声明 .section.text 可简写为 .text。在代码中用 .section 声明的所有的段信息,在被编译成程序后可通过如下命令查看:
符号:表示一系列可检索和引用的目标,它可以是一个变量、一个常量或一个函数。在汇编代码中,可直接引用外部符号。如,汇编指令 bl printf 就调用了外部函数 printf(),这里的 printf 就是一个外部符号。还可用 .globl 声明全局符号。全局符号可被程序外部引用,如代码中的 add() 和 main() 函数就是全局符号。将代码编译成程序后,可用 nm 命令查看它的所有符号信息(见下图)。全局符号用大写字母 T 表示,含义是“text section symbol”,表示在代码段中定义的符号。外部符号用大写字母 U 表示,含义是“undefined”,表示未定义的符号,要在程序运行时动态设置。大写字母 D 的含义是“data section symbol”,表示在数据段中定义的符号。大写字母 A 的含义是“absolute”,表示绝对符号。小写字母 r 的含义是“readonly symbol”,表示一个只读的数据代码中的符号。更多的符号信息可通过 man nm 命令查看
子程序:高级语言中的函数在汇编语言中称子程序。汇编语言中的子程序由 .type 伪指令声明为 %function 的符号,如 app.s 中的 add 符号。子程序由 .fnstart 伪指令开始,.fnend 伪指令结束,伪指令前面是一条条汇编指令的有机集合。ARM 汇编中声明子程序的完整格式:
.globl 子程序名
.type 子程序名, %function
子程序名: @ @子程序名
.fnstart
<……汇编指令语句……>
.fnend
汇编指令:汇编指令是最小的单元数据,存在于子程序的各个角落,每条汇编指令完成一项特定的工作。将多条汇编指令组织在一起,就形成应用程序
标号与注释:汇编语言的一大特色即标号。在汇编代码中,调用程序没有像在高级语言中那样的语法糖,很多数据与代码的引用甚至跳转、执行都依赖标号完成。声明标号时,只要在标号内容后加冒号即可。为方便阅读,标号一般顶格,中间内容缩进。汇编代码支持单行注释,在注释内容前加“@”即可,若在一行代码中加了“@”,那么“@”后面的内容都被解释为注释。GNU 汇编不支持多行注释
汇编指令
汇编语言能实现什么功能,完全由其子程序的功能决定,而子程序中几乎都是处理器指令。归根结底,一个程序中,除了里面的数据,最重要的就是汇编指令
ARM 处理器使用基于精简架构的处理器指令集(Reduced Instruction Set Computer, RISC),其特点是所有指令的长度都是相同的。如,ARM 指令集采用 32 位指令,Thumb 指令集采用 16 位指令。这和 Inter x86 的变长指令不同。这样的好处:程序执行时处理器取指令的速度相对较快,执行效率更高
ARM 中定义的每条汇编指令都有特定含义。如,add 表示加法,sub 表示减法。以 app.s 中的 add() 子程序(片段)为例:
sub sp, sp, #16
str r0, [sp, #12]
str r1, [sp, #8]
str r2, [sp, #4]
str r3, [sp]
ldr r0, [sp, #12]
ldr r1, [sp, #8]
add r0, r0, r1
ldr r1, [sp, #4]
add r0, r0, r1
ldr r1, [sp]
add r0, r0, r1
add sp, sp, #16
bx lr
sub:减法指令。第一条指令 sub sp, sp, #16 中,sub 是汇编减法指令,出现在汇编语句前面,后跟结果对象 sp、原操作对象 sp、目的操作对象数值 16,所做的工作为将 sp 寄存器减 16 字节的结果存入 sp 寄存器(即四个 4 字节空间),目的是存储后面指令执行的中间结果。即开辟栈空间
str:存数据指令。第二条指令 str r0, [sp, #12],用于将 r0 寄存器的内容存入 sp 寄存器加 12 字节的位置。ARM 指令操作的数据长度为 32 位,即 4 字节。第二至五条指令所做的工作是将 r0 ~ r3 寄存器中的内容存入第一条指令开辟的栈空间
ldr:取数据指令。执行的操作与 str 相反。第六条指令 ldr r0, [sp, #12],即读取 sp 寄存器加 12 字节位置的数据存入 r0 寄存器
add:加法指令。第八条指令 add r0, r0, r1,即将 r0 与 r1 寄存器中的数据相加,将结果存入 r0 寄存器;第十三条指令 add sp, sp, #16 与第一条指令相反,为关闭那 16 字节的栈空间
bx:带状态切换的跳转指令,用于跳转到 lr 寄存器指定的位置并执行代码,通常表示子程序结束并返回
ARM 中程序的返回结果存在 r0 寄存器。从整体上看上述代码,相当于执行了如下操作:
int add(int r0, int r1, int r2, int r3) {
return r0 + r1 + r2 + r3;
}
可看出,上述汇编代码不够精简,在 r0 ~ r3 间进行的一次临时保存为多余操作。之所以如此,是因为没为编译器启动代码优化。可为编译器指定参数 -O(大写)来确定代码的优化等级。开启 -O3 优化等级后 add() 子程序的汇编代码:
可看出,优化后的代码中汇编指令全用加法完成,没多余部分
前面所学为汇编指令的基本语义,汇编指令的具体格式和规范将在下一节学习
寄存器
很多汇编指令要指定源操作对象与目标操作对象,操作的对象很多时候是寄存器。如,add r0, r1, r0 指令用于将 r1 寄存器的值和 r0 寄存器的值相加,结果存入 r0 寄存器
寄存器是处理器特有的高速存储部件,可用于暂存指令、数据和地址。高级语言中用的变量、常量、结构体、类等数据到了 ARM 汇编语言中,就是用寄存器保存值或内存地址。寄存器数量有限,32 位的 ARM 微处理器共有 37 个 32 位寄存器,其中 31 个为通用寄存器(Inter x86 32 位才 8 个,64 位才 16 个),6 个为状态寄存器
ARM 处理器支持的运行模式:
用户模式(usr):ARM 处理器正常的程序执行状态
快速中断模式(fiq):用于高速数据传输或通道处理
外部中断模式(irq):用于通用的中断处理
管理模式(svc):操作系统使用的保护模式
数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于虚拟存储及存储保护
系统模式(sys):运行具有特权的操作系统任务
未定义指令中止模式(und):当未定义的指令执行时进入该模式
ARM 处理器的运行模式即可通过软件改变,也可通过外部中断或异常处理改变。不同模式下,处理器使用的寄存器不尽相同,可供访问的资源也不一样。以上模式除了用户模式,都是特权模式。特权模式下,处理器可任意访问受保护的系统资源。现在只关注 ARM 程序逆向分析技术涉及的用户模式
32 位用户模式下,处理器可访问的寄存器为不分组寄存器 R0 ~ R7、分组寄存器 R8 ~ R14、程序计数器 R15(PC)、当前程序状态寄存器 CPSR
ARM 处理器有两种工作状态,即 ARM 状态和 Thumb 状态,处理器可在这两种状态间随意切换。处理器处于 ARM 状态时,会执行 32 位对齐的 ARM 指令;处于 Thumb 状态时,会执行 16 位对齐的 Thumb 指令。Thumb 状态下对寄存器的命名与 ARM 状态下有所差异,它们的关系:
Thumb 状态下的 R0 ~ R7 与 ARM 状态下的 R0 ~ R7 相同
Thumb 状态下的 CPSR 与 ARM 状态下的 CPSR 相同
Thumb 状态下的 FP 对应 ARM 状态下的 R11
Thumb 状态下的 IP 对应 ARM 状态下的 R12
Thumb 状态下的 SP 对应 ARM 状态下的 R13
Thumb 状态下的 LR 对应 ARM 状态下的 R14
Thumb 状态下的 PC 对应 ARM 状态下的 R15
到了 arm64-v8a 时代,一切有了全新面貌。AArch64 用 32 位固定长度的指令集,有如下特性:
引入异常等级的概念,有 EL0 ~ EL3 四种异常等级
提供基于 5 位寄存器说明符的简洁解码表
指令语义与 AArch32 中大致相同
提供 31 个可随时访问的通用 64 位寄存器 x0 ~ x30
提供无模式 GP 寄存器组
提供程序计数器(PC)和堆栈指针(SP)非通用寄存器
提供可用于大多数指令的专用零寄存器 XZR/WZR
异常等级与 armeabi 的处理器运行模式类似,等级越高,拥有的特权越高。我们的应用一般运行于 EL0 等级,操作系统内核运行于 EL1,EL2、EL3 留给安全监控软件和虚拟化软件用。对应用来说,EL0 没有权限进行 AArch64 和 AArch32 状态的切换,这就是我们的 AArch64 指令无法转换为 AArch32 与 T16 指令的原因
AArch64 和 AArch32 的主要差异:
AArch64 支持 64 位操作数的指令,大多数指令可具有 32 位或 64 位的参数
AArch64 地址假定为 64 位,主要的目标数据模型是 P64 和 LLP64
AArch64 的条件指令远少于 AArch32 架构
AArch64 没有任意长度的加载/存储多重指令,增加了用于处理寄存器对的 LD/ST 指令
虽然 AArch64 与 AArch32 的寄存器数目通常都为 32 个,但在使用上有很大不同:
AArch64 的寄存器名变为 x0 ~ x30。x0 ~ x7 用于传递参数与计算结果;x8 为直接结果位置寄存器;x9 ~ x15 为临时寄存器;x16、x17 为内部过程调用寄存器(也可作为临时寄存器 IP0 和 IP1);x18 为临时寄存器;x19 ~ x28 为调用备份寄存器;x29 为帧指针寄存器;x30 作为过程链接寄存器 PLR 使用
AArch64 的 x0 ~ x30 寄存器都为 64 位,每个寄存器可通过 W0 ~ W30 访问低 32 位。读 32 位寄存器 Wn 时,不影响高 32 位的值;写 Wn 时,高 32 位全部清零
AArch64 在运行态下没有与 CPSR 对应的寄存器,要分别访问每种状态标志。处于异常等级 EL0 的程序,只能访问 N(负数)、Z(零)、C(进位)、V(溢出)四个状态标志
除了通用寄存器,arm64-v8a 还提供 32 个 128 位 NEON 浮点寄存器 V0 ~ V31,它们可作为半精度寄存器 H、单精度寄存器 S、双精度寄存器 D 使用
处理器寻址方式
指通过指令中给出的地址码字段寻找真实操作数地址的方式
虽然 ARM 采用精简指令集,但指令间组合的灵活度却比 x86 高。x86 支持七种寻址方式,ARM 支持九种
立即寻址
最简单的寻址方式
大多数处理器都支持这种方式
立即寻址指令中,后面的地址码部分为立即数(即常量或常数)
立即寻址多用于给寄存器赋初值
立即数只能用于源操作数字段,不能用于目的操作数字段
示例:
MOV R0, #1234
上述指令执行后,R0=1234
立即数以“#”为前缀,表示十六进制数值时以“0x”开头,如“#0x20”
寄存器寻址
操作数的值在寄存器中,指令执行时直接从寄存器中取值进行操作
示例:
MOV R0, R1
上述指令执行后,R0=R1
寄存器移位寻址
ARM 指令集特有的寻址方式
与寄存器寻址类似,只是在操作前要对源寄存器操作数进行移位操作
支持以下五种移位操作:
LSL:逻辑左移,移位后对寄存器空出的低位补 0
LSR:逻辑右移,移位后对寄存器空出的高位补 0
ASR:算术右移,移位过程中符号位不变。若源操作数为正数,则移位后对空出的高位补 0,否则补 1
ROR:循环右移,移位后将移出的低位补到空出的高位
RRX:带扩展的循环右移,操作数右移 1 位,移位空出的高位用带 C 标志的值填充
示例:
MOV R0, R1, LSL #2
上述指令的功能是将 R1 寄存器左移 2 位,即 R1 << 2 后赋值给 R0 寄存器。执行后,R0=R1*4
寄存器间接寻址
由地址码给出的寄存器是操作数的地址指针,所需的操作数保存在由寄存器指定的地址的存储单元中
示例:
MOV R0, [R1]
上述指令的功能是将 R1 寄存器的值作为地址,取出此地址中的值赋给 R0 寄存器
基址寻址
指将地址码给出的基址寄存器与偏移量相加,形成操作数的有效地址,所需的操作数保存在有效地址指向的存储单元中
多用于查表、数组访问等操作
示例:
LDR R0, [R1, #-4]
上述指令的功能是将 R1 寄存器的值减 4 作为地址,取出此地址中的值赋给 R0 寄存器
多寄存器寻址
一条指令最多可完成 16 个通用寄存器值的传送
示例:
LDMIA R0, {R1, R2, R3, R4}
LDM 是数据加载指令,指令的后缀 IA 表示每次执行加载操作后,R0 寄存器自增 1 个字。在 ARM 指令集中,1 个字表示一个 32 位的值。这条指令执行后,R1=[R0],R2=[R0+#4],R3=[R0+#8],R4=[R0+#12]
堆栈寻址
ARM 汇编语言
一门语言通常有自己的关键字、代码规范、子程序调用、注释等,汇编语言也一样
汇编语言:将一系列与处理器相关的汇编指令用某种语法和结构组织在一起的程序语言形式
用特定汇编语法规范编写的汇编代码,可被完整地编译或嵌入其他高级语言
Android 中的 ARM 汇编使用 GNU 汇编格式,现在开始学习 GNU ARM 汇编的一般语法格式及特点
ARM 汇编程序结构
编译器在编译阶段会在内部将程序代码编译成与机器相关的汇编指令,上一节的 hello.c 的汇编代码就是一个完整的 ARM 汇编程序
现在编写一个新的程序 app.c,代码:
#include
int add(int a, int b, int c, int d)
{
return a + b + c + d;
}
int main(int argc, char const *argv[])
{
printf("add: %dn", add(1, 2, 3, 4));
return 0;
}
执行如下命令,生成 app.s 汇编代码:
系统、NDK 版本、CC 环境变量设置等信息见上一节
app.s 内容如下:
.text
.syntax unified
.cpu arm1022e
.eabi_attribute 6, 4 @ Tag_CPU_arch
.eabi_attribute 8, 1 @ Tag_ARM_ISA_use
.eabi_attribute 15, 1 @ Tag_ABI_PCS_RW_data
.eabi_attribute 16, 1 @ Tag_ABI_PCS_RO_data
.eabi_attribute 17, 2 @ Tag_ABI_PCS_GOT_use
.eabi_attribute 20, 1 @ Tag_ABI_FP_denormal
.eabi_attribute 21, 1 @ Tag_ABI_FP_exceptions
.eabi_attribute 23, 3 @ Tag_ABI_FP_number_model
.eabi_attribute 24, 1 @ Tag_ABI_align_needed
.eabi_attribute 25, 1 @ Tag_ABI_align_preserved
.eabi_attribute 18, 4 @ Tag_ABI_PCS_wchar_t
.eabi_attribute 26, 2 @ Tag_ABI_enum_size
.file "app.c"
.globl add
.align 2
.type add,%function
add: @ @add
.fnstart
.Leh_func_begin0:
@ BB#0: @ %entry
.pad #16
sub sp, sp, #16
str r0, [sp, #12]
str r1, [sp, #8]
str r2, [sp, #4]
str r3, [sp]
ldr r0, [sp, #12]
ldr r1, [sp, #8]
add r0, r0, r1
ldr r1, [sp, #4]
add r0, r0, r1
ldr r1, [sp]
add r0, r0, r1
add sp, sp, #16
bx lr
.Ltmp0:
.size add, .Ltmp0-add
.cantunwind
.fnend
.globl main
.align 2
.type main,%function
main: @ @main
.fnstart
.Leh_func_begin1:
@ BB#0: @ %entry
.save {r4, r5, r11, lr}
push {r4, r5, r11, lr}
.setfp r11, sp, #8
add r11, sp, #8
.pad #24
sub sp, sp, #24
ldr r2, .LCPI1_6
.LPC1_0:
add r2, pc, r2
ldr r3, .LCPI1_2
ldr r12, .LCPI1_3
ldr lr, .LCPI1_4
ldr r4, .LCPI1_5
ldr r5, .LCPI1_0
str r5, [r11, #-12]
str r0, [sp, #16]
str r1, [sp, #12]
mov r0, r3
mov r1, r12
str r2, [sp, #8] @ 4-byte Spill
mov r2, lr
mov r3, r4
bl add(PLT)
ldr r1, .LCPI1_1
ldr r2, [sp, #8] @ 4-byte Reload
add r1, r1, r2
str r0, [sp, #4] @ 4-byte Spill
mov r0, r1
ldr r1, [sp, #4] @ 4-byte Reload
bl printf(PLT)
ldr r1, .LCPI1_0
str r0, [sp] @ 4-byte Spill
mov r0, r1
sub sp, r11, #8
pop {r4, r5, r11, pc}
.align 2
@ BB#1:
.LCPI1_0:
.long 0 @ 0x0
.LCPI1_1:
.long .L.str(GOTOFF)
.LCPI1_2:
.long 1 @ 0x1
.LCPI1_3:
.long 2 @ 0x2
.LCPI1_4:
.long 3 @ 0x3
.LCPI1_5:
.long 4 @ 0x4
.LCPI1_6:
.long _GLOBAL_OFFSET_TABLE_-(.LPC1_0+8)
.Ltmp1:
.size main, .Ltmp1-main
.cantunwind
.fnend
.type .L.str,%object @ @.str
.section .rodata.str1.1,"aMS",%progbits,1
.L.str:
.asciz "add: %dn"
.size .L.str, 9
.ident "clang version 3.5 "
一个完整的汇编程序由以下几部分组成:
处理器类型声明:可用 .arch 指定处理器架构,用 .cpu 指定处理器型号。若使用浮点指令,建议用 .fpu 指定协处理器类型,如 softvfp、neon
代码与数据段声明:一个完整程序,编译后都会有用于存放代码的代码段(在 ARM EABI 的 ELF 中段名为 .text),以及用于存放数据的数据段(在 ARM EABI 的 ELF 中段名为 .rodata)。一些特殊的程序可能没数据段,但通常有代码段。在汇编代码中声明段时要用伪指令 .section,代码段的声明 .section.text 可简写为 .text。在代码中用 .section 声明的所有的段信息,在被编译成程序后可通过如下命令查看:
符号:表示一系列可检索和引用的目标,它可以是一个变量、一个常量或一个函数。在汇编代码中,可直接引用外部符号。如,汇编指令 bl printf 就调用了外部函数 printf(),这里的 printf 就是一个外部符号。还可用 .globl 声明全局符号。全局符号可被程序外部引用,如代码中的 add() 和 main() 函数就是全局符号。将代码编译成程序后,可用 nm 命令查看它的所有符号信息(见下图)。全局符号用大写字母 T 表示,含义是“text section symbol”,表示在代码段中定义的符号。外部符号用大写字母 U 表示,含义是“undefined”,表示未定义的符号,要在程序运行时动态设置。大写字母 D 的含义是“data section symbol”,表示在数据段中定义的符号。大写字母 A 的含义是“absolute”,表示绝对符号。小写字母 r 的含义是“readonly symbol”,表示一个只读的数据代码中的符号。更多的符号信息可通过 man nm 命令查看
子程序:高级语言中的函数在汇编语言中称子程序。汇编语言中的子程序由 .type 伪指令声明为 %function 的符号,如 app.s 中的 add 符号。子程序由 .fnstart 伪指令开始,.fnend 伪指令结束,伪指令前面是一条条汇编指令的有机集合。ARM 汇编中声明子程序的完整格式:
.globl 子程序名
.type 子程序名, %function
子程序名: @ @子程序名
.fnstart
<……汇编指令语句……>
.fnend
汇编指令:汇编指令是最小的单元数据,存在于子程序的各个角落,每条汇编指令完成一项特定的工作。将多条汇编指令组织在一起,就形成应用程序
标号与注释:汇编语言的一大特色即标号。在汇编代码中,调用程序没有像在高级语言中那样的语法糖,很多数据与代码的引用甚至跳转、执行都依赖标号完成。声明标号时,只要在标号内容后加冒号即可。为方便阅读,标号一般顶格,中间内容缩进。汇编代码支持单行注释,在注释内容前加“@”即可,若在一行代码中加了“@”,那么“@”后面的内容都被解释为注释。GNU 汇编不支持多行注释
汇编指令
汇编语言能实现什么功能,完全由其子程序的功能决定,而子程序中几乎都是处理器指令。归根结底,一个程序中,除了里面的数据,最重要的就是汇编指令
ARM 处理器使用基于精简架构的处理器指令集(Reduced Instruction Set Computer, RISC),其特点是所有指令的长度都是相同的。如,ARM 指令集采用 32 位指令,Thumb 指令集采用 16 位指令。这和 Inter x86 的变长指令不同。这样的好处:程序执行时处理器取指令的速度相对较快,执行效率更高
ARM 中定义的每条汇编指令都有特定含义。如,add 表示加法,sub 表示减法。以 app.s 中的 add() 子程序(片段)为例:
sub sp, sp, #16
str r0, [sp, #12]
str r1, [sp, #8]
str r2, [sp, #4]
str r3, [sp]
ldr r0, [sp, #12]
ldr r1, [sp, #8]
add r0, r0, r1
ldr r1, [sp, #4]
add r0, r0, r1
ldr r1, [sp]
add r0, r0, r1
add sp, sp, #16
bx lr
sub:减法指令。第一条指令 sub sp, sp, #16 中,sub 是汇编减法指令,出现在汇编语句前面,后跟结果对象 sp、原操作对象 sp、目的操作对象数值 16,所做的工作为将 sp 寄存器减 16 字节的结果存入 sp 寄存器(即四个 4 字节空间),目的是存储后面指令执行的中间结果。即开辟栈空间
str:存数据指令。第二条指令 str r0, [sp, #12],用于将 r0 寄存器的内容存入 sp 寄存器加 12 字节的位置。ARM 指令操作的数据长度为 32 位,即 4 字节。第二至五条指令所做的工作是将 r0 ~ r3 寄存器中的内容存入第一条指令开辟的栈空间
ldr:取数据指令。执行的操作与 str 相反。第六条指令 ldr r0, [sp, #12],即读取 sp 寄存器加 12 字节位置的数据存入 r0 寄存器
add:加法指令。第八条指令 add r0, r0, r1,即将 r0 与 r1 寄存器中的数据相加,将结果存入 r0 寄存器;第十三条指令 add sp, sp, #16 与第一条指令相反,为关闭那 16 字节的栈空间
bx:带状态切换的跳转指令,用于跳转到 lr 寄存器指定的位置并执行代码,通常表示子程序结束并返回
ARM 中程序的返回结果存在 r0 寄存器。从整体上看上述代码,相当于执行了如下操作:
int add(int r0, int r1, int r2, int r3) {
return r0 + r1 + r2 + r3;
}
可看出,上述汇编代码不够精简,在 r0 ~ r3 间进行的一次临时保存为多余操作。之所以如此,是因为没为编译器启动代码优化。可为编译器指定参数 -O(大写)来确定代码的优化等级。开启 -O3 优化等级后 add() 子程序的汇编代码:
可看出,优化后的代码中汇编指令全用加法完成,没多余部分
前面所学为汇编指令的基本语义,汇编指令的具体格式和规范将在下一节学习
寄存器
很多汇编指令要指定源操作对象与目标操作对象,操作的对象很多时候是寄存器。如,add r0, r1, r0 指令用于将 r1 寄存器的值和 r0 寄存器的值相加,结果存入 r0 寄存器
寄存器是处理器特有的高速存储部件,可用于暂存指令、数据和地址。高级语言中用的变量、常量、结构体、类等数据到了 ARM 汇编语言中,就是用寄存器保存值或内存地址。寄存器数量有限,32 位的 ARM 微处理器共有 37 个 32 位寄存器,其中 31 个为通用寄存器(Inter x86 32 位才 8 个,64 位才 16 个),6 个为状态寄存器
ARM 处理器支持的运行模式:
用户模式(usr):ARM 处理器正常的程序执行状态
快速中断模式(fiq):用于高速数据传输或通道处理
外部中断模式(irq):用于通用的中断处理
管理模式(svc):操作系统使用的保护模式
数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于虚拟存储及存储保护
系统模式(sys):运行具有特权的操作系统任务
未定义指令中止模式(und):当未定义的指令执行时进入该模式
ARM 处理器的运行模式即可通过软件改变,也可通过外部中断或异常处理改变。不同模式下,处理器使用的寄存器不尽相同,可供访问的资源也不一样。以上模式除了用户模式,都是特权模式。特权模式下,处理器可任意访问受保护的系统资源。现在只关注 ARM 程序逆向分析技术涉及的用户模式
32 位用户模式下,处理器可访问的寄存器为不分组寄存器 R0 ~ R7、分组寄存器 R8 ~ R14、程序计数器 R15(PC)、当前程序状态寄存器 CPSR
ARM 处理器有两种工作状态,即 ARM 状态和 Thumb 状态,处理器可在这两种状态间随意切换。处理器处于 ARM 状态时,会执行 32 位对齐的 ARM 指令;处于 Thumb 状态时,会执行 16 位对齐的 Thumb 指令。Thumb 状态下对寄存器的命名与 ARM 状态下有所差异,它们的关系:
Thumb 状态下的 R0 ~ R7 与 ARM 状态下的 R0 ~ R7 相同
Thumb 状态下的 CPSR 与 ARM 状态下的 CPSR 相同
Thumb 状态下的 FP 对应 ARM 状态下的 R11
Thumb 状态下的 IP 对应 ARM 状态下的 R12
Thumb 状态下的 SP 对应 ARM 状态下的 R13
Thumb 状态下的 LR 对应 ARM 状态下的 R14
Thumb 状态下的 PC 对应 ARM 状态下的 R15
到了 arm64-v8a 时代,一切有了全新面貌。AArch64 用 32 位固定长度的指令集,有如下特性:
引入异常等级的概念,有 EL0 ~ EL3 四种异常等级
提供基于 5 位寄存器说明符的简洁解码表
指令语义与 AArch32 中大致相同
提供 31 个可随时访问的通用 64 位寄存器 x0 ~ x30
提供无模式 GP 寄存器组
提供程序计数器(PC)和堆栈指针(SP)非通用寄存器
提供可用于大多数指令的专用零寄存器 XZR/WZR
异常等级与 armeabi 的处理器运行模式类似,等级越高,拥有的特权越高。我们的应用一般运行于 EL0 等级,操作系统内核运行于 EL1,EL2、EL3 留给安全监控软件和虚拟化软件用。对应用来说,EL0 没有权限进行 AArch64 和 AArch32 状态的切换,这就是我们的 AArch64 指令无法转换为 AArch32 与 T16 指令的原因
AArch64 和 AArch32 的主要差异:
AArch64 支持 64 位操作数的指令,大多数指令可具有 32 位或 64 位的参数
AArch64 地址假定为 64 位,主要的目标数据模型是 P64 和 LLP64
AArch64 的条件指令远少于 AArch32 架构
AArch64 没有任意长度的加载/存储多重指令,增加了用于处理寄存器对的 LD/ST 指令
虽然 AArch64 与 AArch32 的寄存器数目通常都为 32 个,但在使用上有很大不同:
AArch64 的寄存器名变为 x0 ~ x30。x0 ~ x7 用于传递参数与计算结果;x8 为直接结果位置寄存器;x9 ~ x15 为临时寄存器;x16、x17 为内部过程调用寄存器(也可作为临时寄存器 IP0 和 IP1);x18 为临时寄存器;x19 ~ x28 为调用备份寄存器;x29 为帧指针寄存器;x30 作为过程链接寄存器 PLR 使用
AArch64 的 x0 ~ x30 寄存器都为 64 位,每个寄存器可通过 W0 ~ W30 访问低 32 位。读 32 位寄存器 Wn 时,不影响高 32 位的值;写 Wn 时,高 32 位全部清零
AArch64 在运行态下没有与 CPSR 对应的寄存器,要分别访问每种状态标志。处于异常等级 EL0 的程序,只能访问 N(负数)、Z(零)、C(进位)、V(溢出)四个状态标志
除了通用寄存器,arm64-v8a 还提供 32 个 128 位 NEON 浮点寄存器 V0 ~ V31,它们可作为半精度寄存器 H、单精度寄存器 S、双精度寄存器 D 使用
处理器寻址方式
指通过指令中给出的地址码字段寻找真实操作数地址的方式
虽然 ARM 采用精简指令集,但指令间组合的灵活度却比 x86 高。x86 支持七种寻址方式,ARM 支持九种
立即寻址
最简单的寻址方式
大多数处理器都支持这种方式
立即寻址指令中,后面的地址码部分为立即数(即常量或常数)
立即寻址多用于给寄存器赋初值
立即数只能用于源操作数字段,不能用于目的操作数字段
示例:
MOV R0, #1234
上述指令执行后,R0=1234
立即数以“#”为前缀,表示十六进制数值时以“0x”开头,如“#0x20”
寄存器寻址
操作数的值在寄存器中,指令执行时直接从寄存器中取值进行操作
示例:
MOV R0, R1
上述指令执行后,R0=R1
寄存器移位寻址
ARM 指令集特有的寻址方式
与寄存器寻址类似,只是在操作前要对源寄存器操作数进行移位操作
支持以下五种移位操作:
LSL:逻辑左移,移位后对寄存器空出的低位补 0
LSR:逻辑右移,移位后对寄存器空出的高位补 0
ASR:算术右移,移位过程中符号位不变。若源操作数为正数,则移位后对空出的高位补 0,否则补 1
ROR:循环右移,移位后将移出的低位补到空出的高位
RRX:带扩展的循环右移,操作数右移 1 位,移位空出的高位用带 C 标志的值填充
示例:
MOV R0, R1, LSL #2
上述指令的功能是将 R1 寄存器左移 2 位,即 R1 << 2 后赋值给 R0 寄存器。执行后,R0=R1*4
寄存器间接寻址
由地址码给出的寄存器是操作数的地址指针,所需的操作数保存在由寄存器指定的地址的存储单元中
示例:
MOV R0, [R1]
上述指令的功能是将 R1 寄存器的值作为地址,取出此地址中的值赋给 R0 寄存器
基址寻址
指将地址码给出的基址寄存器与偏移量相加,形成操作数的有效地址,所需的操作数保存在有效地址指向的存储单元中
多用于查表、数组访问等操作
示例:
LDR R0, [R1, #-4]
上述指令的功能是将 R1 寄存器的值减 4 作为地址,取出此地址中的值赋给 R0 寄存器
多寄存器寻址
一条指令最多可完成 16 个通用寄存器值的传送
示例:
LDMIA R0, {R1, R2, R3, R4}
LDM 是数据加载指令,指令的后缀 IA 表示每次执行加载操作后,R0 寄存器自增 1 个字。在 ARM 指令集中,1 个字表示一个 32 位的值。这条指令执行后,R1=[R0],R2=[R0+#4],R3=[R0+#8],R4=[R0+#12]
堆栈寻址
举报