介绍ARM汇编(1)

ARM笔记

一、背景介绍

把关键代码放在So中,已经是Android应用安全防护的主流,Java层用JADX等反编译工具可以得到与源代码相差无几的伪代码,So层反编译的难度则大很多,主要使用IDA等工具,得到的是汇编代码,虽然F5可以得到伪C代码,但准确性不高,而且会遇到各种各样的问题,因此汇编语言成了逆向So的基础。ARM笔记面向ARM汇编新手,是一个系列学习文章。

1.1 定义

首先我们需要聊一下机器语言,机器语言是最低级的语言,指令是由0和1组成的序列,它可以由计算机直接执行,在计算机内部被转变成一列高低电平,驱动计算机的电子器件,从而实现运算。

我们来看一条简单的ARM机器指令

1110 0001 1010 0000 0011 0000 0000 1001

CPU执行这条指令时,会将9号寄存器的值拷贝到3号寄存器(寄存器是CPU中可以存储数据的器件,以ARM为例,有37个寄存器),理解这条指令对于计算机毫无压力,但作为编程者,我们很难辨别和记忆机器指令,更别说排查一个二进制数的误写,当然,我们可以用十六进制表示它,即e1a03009,这确实是一种更简短的表示方式,但仍不方便记忆和书写,因此汇编语言诞生了。

汇编语言如何表述上述指令呢?如下:

MOV R3, R9

显然,它比二进制序列好看多了,而且语义比较明显。

助记符 含义
mov move即移动,数据的移动即复制、拷贝
R3 Register 3 ,即三号寄存器
R9 Register 9 ,即九号寄存器

需要明白的是,汇编语言并没有脱离机器语言,事实上,它只是根据映射规则进行了翻译,为了理解这个概念,我们提供一个错误但原理一致的汇编器伪代码。

# 汇编指令
asm = "mov r3,r9"
# 用于汇编代码和机器代码转换的查找表
mapList = dict()
mapList["mov"] = "1110000110100000"
mapList["r3"] = "0011"
mapList["r9"] = "000000001001"

machineCode = ""
for symbol in asm.replace(",", " ").split(" "):
    machineCode += mapList[symbol]
   
print(machineCode)

这个查找表是错误的,但读者应该能从中明白,汇编指令只是机器指令的助记符,就像你会把13988888888这个电话号码记成“139八个8”一样,汇编语言和机器语言两者实为一体.1

1.2 指令集架构

在高级语言的橱窗中,Java,Python,Ruby,C,C++等等琳琅满目,它们在各自的工作场景中发光发热,机器语言的世界当然也不例外。世界上有远不止一种机器语言,在不同的硬件、工作场景和年代里,工程师们设计了繁多的机器语言。但万幸的是,它们之间的差别不大,因为新的处理器需要兼容之前老的版本。举个例子,因特尔公司1980年设计了80386 处理器,2008年发布了酷睿 i7处理器,虽然两款CPU的开发时隔三十年,但它们的汇编语言却极其类似。

扫描二维码关注公众号,回复: 9886174 查看本文章

在大学时代,有一门名为《汇编语言程序设计》的计算机课程,采用的CPU是1978年由Intel公司所设计的8086CPU,它是X86的鼻祖,但知识并不会因此过时,这就是兼容带来的好处。那是不是说, 不管处理器千变万变,我们都只用学习8086CPU就能一招鲜呢?当然不行。因为我们上述讨论的处理器都属于大名鼎鼎的因特尔公司,而且它们统属于X86架构,而世上有远不止一种架构。

在各种各样的指令集架构中,X86最广为流传,它牢牢占据着PC机的处理器市场。除此之外,MIPS,Power PC都曾经大放异彩。而我们这次学习和研究的是ARM指令集,我们下面来探究一下原因。

Android支持七种CPU架构X86,X86-64,MIPS,MIPS64,Armeabi,Armv7a,Armv8a。

在这里插入图片描述
排除32位和64位系统的差异,即Android支持X86,MIPS,ARM三家的CPU,但是,由于商业策略的成功,市面上99%的手机使用的都是ARM芯片,而X86和MIPS寥寥无几

App安装包的lib目录下会提供各种架构的SO文件,由手机根据自身架构进行最优化选择。

1.png

似乎你可以选择自己熟悉架构的SO文件进行分析,但这个想法因为另外一个原因破灭——应用体积优化

在一个APK中,SO文件占用了不小的体积,有些情况甚至能达到50%,而应用大小和用户下载率直接挂钩,因此很多APP为了“减负”,会不提供MIPS和X86架构的SO文件包,甚至只提供Armeabi或者Armv7a的SO文件包(同为ARM架构,Armeabi可以兼容所有的32或64位arm处理器,虽然会造成性能的浪费和损耗)。2

因此我们必须要学习ARM汇编而不是X86汇编或者MIPS汇编——APK很可能只提供给你ARM架构的SO。

2.ARM汇编基础

让我们开始学习ARM吧!

2.1 环境配置

在学习之前,我们要配置一下环境,好的环境可以加速我们的学习。

我们的学习需求有两方面

  • 理解单条ARM指令的语义
  • 理解ARM汇编代码块意图

我们有这些工具链可供选择,心急的话可以直接看最后一种方式:

工具链1——使用Android Studio编译ARM SO,对其使用IDA进行动态调试,这样我们就可以观察指令的变化,以及C/C++和arm指令块之间的对应。这样做真是痛苦不堪,过程复杂,效率极其。与此同时,Androd Studio编译出的arm代码不可控,你不知道它会以哪种方式编译你的C代码,你很难验证你所学的指令。

工具链2——使用ARM官方开发工具Ds-5编写Arm代码,使用arm-gcc/gcc编译C/C++。缺点是安装,配置繁琐,在Windows上配置尤为痛苦,但整体上是可行的。

工具链3——在SO逆向中,有一个炙手可热的工具叫做Unicorn,它是一个优秀的跨平台模拟执行框架,可以在我们的电脑上模拟执行arm代码,它本身比较简陋,有没有基于Unicorn开发的模拟器呢?最后我找到了这个——ARMStrong
在这里插入图片描述
它是一个基于Unicorn开发的Java项目,为教学而生,只需要Java 11的环境就可以运行。虽然它比较简陋,但基本可以满足我们的需求,本来它会是我们这篇文章的重要角色。但在寻寻觅觅之后,我找到了这两个项目,两者合在一起可以满足我们全部的需求,这显然是我们最好的选择

visUAL2 专业的ARM汇编教学工具

Compiler Explore 强大的在线编译器

2.1.1 VisUAL2配置和使用

软件最新版本下载地址
在这里插入图片描述
正常下载解压并运行其中的VisUAL2.exe即可

为了证明自己的可靠和强大,visUAL2内置了一个一千多行的汇编程序,g感兴趣的朋友可以加载和运行它,当作了解软件的功能。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

visUAL提供了指令纠错,指令图示等功能,后续使用中你会慢慢体会到它的强大,以及意识到它的不足,我们先看下一个工具。

2.1.2 Compile Explore使用

https://godbolt.org/
这个网址无需翻墙即可使用,功能极其强大,提供了众多在线编译器,我们需要学习ARM汇编,编译器选择arm gcc 4.5.4 linux。
在这里插入图片描述

2.2一个简单的程序:

接下来让我们从一个最简单的程序开始介绍,它长这样。

int f(){
    return 123;
}

我们使用Compiler Explore(以下简称CE)将它编译成ARM指令

在这里插入图片描述

这并不是我们需要分析的ARM指令,因为没有开启优化,其中含有大量冗余指令,右上角的箭头可以展开参数配置列表,也可以直接在输入框中写入配置。

在这里插入图片描述

优化级别 描述
0 关闭优化. gcc的默认选项
1 在不明显拖慢编译速度的情况下减小代码尺寸, 提升执行速度
2 在不以存储空间换运行时间的前提下, 采用几乎所有gcc支持的优化方法
3 二级优化的基础上进一步优化
S 介于2-3之间

我们学习时选择1级优化,这样C代码和ARM之间有较好的对应关系。

在这里插入图片描述

优化后代码如下

f():
        mov     r0, #123
        bx      lr

别看这个程序简单,它涉及到了非常多的内容。首先我们看一下汇编程序中的R0(R大小写无差异),它叫做寄存器,寄存器位于处理器(即CPU)中,在计算时用于存储数据。ARM CPU 包含16个简单可用的寄存器,从R0到R15,每个寄存器可以存储一个32位数。3

需要注意的是,尽管寄存器可以存储数据,但它和存储器(即内存)的概念非常不同。存储器能存储的数据量比寄存器大无数倍,存储器一般不在处理器中,而是作为额外且独立的硬件。同时,因为内存的巨大体量,访问内存耗费的时间远超过访问寄存器——通常是十倍以上。因此,各种汇编语言程序在存取数据时会尽可能优先使用寄存器

从上面这两段话, 你应该能明白这个定义:**寄存器是有限存贮容量的高速存贮部件。**少而快,这就是寄存器的特点

因为汇编代码直接对应着机器语言,所以它的格式有严格的规定。我们可以看到每行汇编代码包含两个部分:第一部分是操作码,比如这里的mov,它表示了我们要进行何种类型的操作,第二部分是参数,这里是“r0, #123”。

每个操作码对允许的参数有严格的限制。比如mov指令必须有两个参数,第一个参数是目标寄存器,第二个参数必须是寄存器、被移位的寄存器或者一个常数(以#号为前缀),一个直接出现在指令中的常数也被称为立即数,因为当CPU处理这条指令时,可以立即取到这个值。

我们尝试一下在visUAL2中不加#号,会发现立即报错并给出了提示
在这里插入图片描述
每个操作码有准确明晰的语义,MOV指令将源操作数传送到目的寄存器,结果类似于arg1 = arg2.

根据mov指令的语义和格式,我们用软件实验一下并运行

image.png image.png

如果不习惯十六进制也可以切换为十进制显示,我们已经测试过寄存器和立即数了,还有一种情况叫做被移位的寄存器,这是什么意思呢?我们写一条包含移位寄存器的指令。

mov r1,r2,LSL#1

不是说mov操作码有两个参数吗,这怎么都像是mov的三个参数,其实并不然,我们需要了解一下移位操作。我们用VisUAL2演示一下,它提供了强大的教学图示。
在这里插入图片描述
选择Run运行,R2,R1寄存器已经发生了变化,点击浮现出的shift按键。

在这里插入图片描述
图示清晰的展示了这条汇编代码所做的事,首先,我们的立即数10被展示为32位数据,接下来LSL移位操作码对它进行了操作,这一步骤由桶型移位器完成,最后ALU(运算器)执行mov操作。
image.png

数据移位在ARM指令集中并不作为单独的指令使用,它只作为指令的一个字段。

除了LSL外,还有ASL,LSR,ASR,ASR,ROR,RXR这六种移位指令,可以在VisUAL2中练习,都会有对应的图示(一条一条试,因为软件只显示最后一条移位操作的图示)。

现在我们掌握了MOV指令的三种常见用法,接下来我们学习一下ADD指令吧!

ADD操作码需要三个参数,第一个即目标寄存器,第二个参数必须是寄存器,第三个参数可以是寄存器,立即数,或者被移位的寄存器,我们可以这样表示它:

ADD Rd,Rn,operand

接下来就可以在软件中自由练习吧!


现在我们已经学会了两个指令,其实大部分指令都是这样朴实无华,简单易懂,后面会有系统的教学,所以不妨先回来继续看我们的汇编程序,mov r0,#123即r0 = 123,接下来bx lr是什么意思呢?根据我们的原程序可以知道,在拿到123后,程序返回该值。而汇编程序中,程序的跳转由B系列指令完成:B,BL,BX,BLX,这条指令的意思自然也是结束程序,跳转出去。

那么我们很可能会有一个疑问,假设a程序调用了我们的f程序,它怎么得到f程序的返回值呢?这就涉及到ARM调用约定ATPCS了,它对调用函数时涉及到的方方面面进行了规定,我们先记第一条,一般而言

ARM程序使用R0寄存器传递返回值。

ARM程序使用R0寄存器传递返回值。

ARM程序使用R0寄存器传递返回值。

因此在使用IDA动态调试时,如果想看函数结果,我们只需要看函数结束时的R0寄存器即可。

接下来我们要详细了解一下bx lr这句汇编代码做了什么,首先我们需要知道lr是什么。


断在这里,下篇继续。


  1. 关于汇编器的更多信息,可以参考这个帖子。《汇编语言转换成机器语言,具体在机器这个层面是如何实现的》 ↩︎

  2. 更多细节可以参考《Android CPU 架构详解》https://www.jianshu.com/p/44650eaceb18 或https://blog.csdn.net/nongminkouhao/article/details/81048857 ↩︎

  3. 实际上,ARM CPU有37个寄存器,但由于ARM有七种工作模式,而每种模式下可使用不同的寄存器,我们平时处于用户模式,该模式下有16个寄存器可访问和使用。 ↩︎

发布了27 篇原创文章 · 获赞 120 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_38851536/article/details/103930944