如何阅读ARM64汇编语言

2021-03-15 11:19:01

ARM64是一款计算机建筑竞争与流行的英特尔X86-64架构竞争,桌面,笔记本电脑等CPUS架构。 ARM64在Mobilebhones 1中很常见,以及基于格格的Amazonec2实例和大量的Ballyhooed Apple M1芯片,所以知道它可能是有用的!事实上,由于iPhone,我几乎与ARM64比X86-64更多的时间花了更多的时间。

这篇文章是我上一篇文章的替代版本如何readassemblylanguage。 ITWalk通过相同的例子,显示ARM64组装instead。背景内容,如指令的解释,并且还为您的阅读方便重新选择。

汇编语言的基本单位是指令。每Machine指令是一个小的操作,如添加两个数字,从内存加载一些数据,跳到另一个内存位置(如可被可被可怕的GoTostatatatatistating),或者从函数调用或返回。与X86-64不同,每个ARM64指令始于4个字节,因此您可以通过计算指令来告诉Howmuch Memory一块ARM64代码。

我们的第一个玩具示例将让我们熟悉简单的结构。它只计算2D载体的规范的正方形:

#include< cstdint> struct vec2 {int64_t x; Int64_t y;}; int64_t nammsquared(vec2 v){return v.x * v.x + v.y * v.y;}

第一个指令,MUL X8,X1,X1,执行倍增。 Unligethe X86-64汇编语法我们以前使用过,目标操作数在左边。此MUL指令将X1的内容平方分,并将结果分为X8。

接下来,我们有Madd x0,x0,x0,x8。 “乘法添加”的Maddstands:IT方块X0,添加X8,并在X0中存储Theresult。

让我们简短地解释我们在我们的申请中看到的寄存器是什么。寄存器是“汇编”的“变量”。与您最喜欢的编程语言(可能)中的变量不同,它们有一个有限数量,它们有标准化名,我们将谈论的是最多64位的Inlize。 ARM64具有31个名为X0至X30的通用寄存器。要引用其较低的32位而不是完整的64位,Wecan通过W30写入W2。还有一个专用SP(StackPointer)寄存器。核心寄存器名称的FullDocumation是在ARM'SPETE上。

#include< cstdint> struct vec2 {int64_t x; int64_t y; void debugpretter()const;}; int64_t normsquared(vec2 v){v.debugpretp();返回v.x * v.x + v.y * v.y;}

SUB SP,SP,#32 STP X29,X30,[SP,#16]添加X29,SP,#16 STP X0,X1,[SP] MOV X0,SP BL VEC2 :: Debugpretter()Const LDP X8,X9, [sp] ldp x29,x30,[sp,#16] mul x8,x8,x8 madd x0,x9,x9,x8添加sp,sp,#32 ret

我们用一个新的寄存器开始:sp。与x86-64上的%rsp一样,它是“堆栈指针”,用于维护Collstack。它指向堆栈的斑点,其增长“向下”(朝向下地址)ONARM64。因此,我们的SUP,SP,#32指令是通过从StackPointer中减去堆栈上的四个64位整数的空间。接下来,STP X29,X30,[SP,#16]存储Apairof寄存器:它正在保存旧帧指针(x29)和linkregister(x30 - 它包含返回地址,正如我们在下面的返回地址)在堆栈启动在地址SP + 16.(方形括号Denote一个内存访问。)我们计算使用AddX29,SP,#16的新帧指针;需要指向先前保存的帧指针堆栈指针。这结束了3指导功能。

然后,以下STP X0,X1,[SP]指令将第一个第二参数存储到NARMSQUARED,它是V.X和V.Y,TOTEE Stack,有效地在地址SP中创建V的内存中的v副本。接下来,我们将指针与MovX0,SP中的X0中的v副本置于X0中,并使用BL调用Vec2 :: debugprett()const。 BL是“分支与链接”的“分支”的主题,它与X86-64呼叫指令略有不同:而不是将返回地址推到堆栈上,它将其保存为InRegister X30,也称为链接寄存器或LR。

在Debugprint返回之后,我们从堆栈中加载v.x和v.y的r8和r9。 wealso恢复帧指针和stackpointer的旧值。然后,我们与前一个例子有相同的MUL和MADD指令。最后,我们添加了SP,SP,#32清除我们在函数开始时分配的32字节的堆栈空间(称为函数模糊;我包括旧框架指针和堆栈指针的负载发生在发生之前MUL& Madd)然后用RET返回呼叫者。

现在,让我们看看一个不同的例子。假设我们想要打印uppercased C字符串,我们希望避免堆分配串。 2我们可能会写一些东西如下:

#include< cstdio> #include< cstring> #include<记忆> void copyuppercase(char * dest,const char * src); constexpr size_t max_stack_array_size = 1024; void printuppercase(const char * s){auto ssize = strlen; if(ssize< max_stack_array_size){char temp [ssize + 1]; CopyUppercase(TEMP,S);放(TEMP); } else {// std :: make_unique_for_overwrite在godbolt上缺少。 std :: unique_ptr< char []> TEMP(新的CHAR [SSIZE + 1]); CopyUppercase(Temp.get(),s); puts(temp.get()); }}

STP X29,X30,[SP,#-4​​8]! // 16字节折叠溢出STR X21,[SP,#16] // 8字节折叠溢出STP X20,X19,[SP,#32] // 16字节折叠溢出X29,SP MOV X19,X0 BL strlen cmp x0,#1024 // = 1024添加x0,x0,#1 // = 1 b.hi .lbb0_2添加x9,x0,#15 // = 15 mov x8,sp和x9,x9,#0xffffffffffffff0 sub x20 ,x8,x9 mov x21,sp mov sp,x20 mov x0,x20 mov x1,x19 bl compyuppercase(char *,char const *)mov x0,x20 bl put sp,x21 mov sp,x29 ldp x20,x19,[ SP,#32] // 16字节折叠重新加载LDR X21,[SP,#16] // 8字节折叠重新加载LDP X29,X30,[SP],#48 // 16字节折叠重新加载RET.LBB0_2: BL操作员新[](unsigned long)mov x1,x19 mov x20,x0 bl copyuppercase(char *,char const *)mov x0,x20 bl Puts mov x0,x20 mov sp,x29 ldp x20,x19,[sp,# 32] // 16字节折叠重新加载LDR X21,[SP,#16] // 8字节折叠重新加载LDP X29,X30,[SP],#48 // 16字节折叠重载B算子删除[](void *)

我们的功能序幕已经更长了,我们也有一些新的纽约尔流指令。让我们仔细看看ThePrologue:

STP X29,X30,[SP,#-4​​8]! // 16字节折叠溢出str x21,[sp,#16] // 8字节折叠溢出stp x20,x19,[sp,#32] // 16字节折叠溢出溢出x29,sp

正如我们之前所看到的那样,我们正在将旧框架指针和stackpointer保存到堆栈中。但是,我们正在使用MoreComplicated Store指令执行它:STP X29,X30,[SP,#-4​​8]!叮叮当当。首先,它将X29和X30存储到地址SP -48。其次,它更新了与同一SP-48值的堆栈指针(这就是感叹号的东西;它是ARM'SDocumentation中描述的“indexAddressing模式”)。

接下来,我们将x21,x20和x19保存到堆栈;我们将使用HOMELLATER,我们需要保留其当前值(换句话说,它们是“Callee保存的”寄存器)。最后,我们在x29中设置了newframe指针。

(顺便说一下,编译器生成的评论中的术语“泄漏”只是意味着我们正在将寄存器保存到堆栈中。)

mov x19,x0 bl strlen cmp x0,#1024 // = 1024添加x0,x0,#1 // = 1 b.hi .lbb0_2

我们在x19中保存我们的参数,s(存储在x0中),并在我们之前看到的BL调用strlen。当strlenreturns时,我们将导致1024与我们的IF语句中的第一步进行比较。 thisssets the nzcvregister accorting to charemsion的结果,然后是b.hi .lbb0_2 branchesto .lbb0_2如果事实证明x0 or orephan 1024.因为我们的if语句的两个分支都关心ssize + 1而不是ssize,我们添加了在分支之前1到x0(ssize存储ssize)。通常,在汇编条件跳转指令中实现了更高级别的控制流原始电源仪,如果/ else语句和循环。

让我们首先看看X0< = 1024的路径,从而拍摄.LBB0_2的分支。我们有一个斑派在堆栈上创建Char Temp [SSIZE + 1]的说明:

添加x9,x0,#15 // = 15 mov x8,sp和x9,x9,#0xfffffffffffffff0 sub x20,x8,x9 mov x21,sp mov sp,x20

我们加入15到x0并将结果放在x9中。然后,我们掩盖了X9的X9的1位。在一起,这两个操作将Targetarray大小舍入到X9中的下一个倍数为16。然后,从堆栈指针WeSubTract阵列大小将旧的StackPointer值保存到x21 4中,并设置堆栈指针值。

mov x0,x20 mov x1,x19 bl compyuppercase(char *,char const *)mov x0,x20 bl puts

MOV SP,X21 MOV SP,X29 LDP X20,X19,[SP,#32] // 16字节折叠重新加载LDR X21,[SP,#16] // 8字节折叠重新加载LDP X29,X30,[SP] ,#48 // 16字节折叠重新加载RET

我们使用Framepointer的值恢复堆栈指针。然后,我们加载我们以前保存到thestack的寄存器。在这里,我们已经看到了一个新的“索引后”添加模式:LDP X29,X30,[SP],#48,将X29和X30加载到堆栈指针的当前值,然后将48加载到之后。最后,Wereturn控制到我们的来电者,我们完成了。

接下来,让我们来看看x0&gt的路径; 1024和我们分支到.lbb0_2在堆上分配我们的数组。这条道路是Morestraightforward。我们调用操作员New [],将结果(返回x0)保存到x20中,然后调用CopyUppercase并按照以前放置。我们为这种情况进行了一个单独的函数脱节,它看起来是一个bitDifferent:

MOV X0,X20 MOV SP,X29 LDP X20,X19,[SP,#32] // 16字节折叠重新加载LDR X21,[SP,#16] // 8字节折叠重新加载LDP X29,X30,[SP] ,#48 // 16字节折叠重新加载B运算符删除[](void *)

FORST MOV将X0设置为我们之前保存的HeaP-AllocatedAray的指针。与其他函数ePilogue一样,WetheN还原堆栈指针,加载保存的寄存器,并通过添加48个字节来加载48个字节。最后,我们有一个新的说明:boperator删除[](void *)。 b(对于“分支”)就像转到:Ittransfers控制给定的标签或功能。与BL不同,它不保存未来RET的返回地址。因此,当OperatorDelete []返回时,它将转移到PrintupPercase的来电者。从本质上讲,我们将BL与Opreator Delete []组合使用我们自己的RET。这称为尾部CallOptimization。

汇编语言可以追溯到19世纪40年代末期,所以有很多资源来学习它。就个人而言,MyFirst对汇编语言的介绍在EECS 370:我的母校大学母校的计算机组织级课程介绍。不幸的是,大多数课程材料与该网站上的课程都不公开。以下是伯克利(CS61C),Carnegie Mellon(15-213),斯坦福(CS107)和麻省理工学院(6.004)的相应“计算机如何真正工作”课程。 (佩带我知道我是否建议了任何Thseschools的错误课程!)NAND还似乎似乎类似的材料,项目和书籍章节自由提供。

我的第一次实际接触ARM64组件,特别是易于剥离iPhone开发。我已经知道总之单的大学曝光中的普遍方式,所以我每天都开始犹豫不决“ARM64 LDP指令”(或其他任何其他指示)并阅读它所做的。随着时间的推移,我记得我患有了什么,并没有再来谷歌。

如果您希望ARM64组装的技术演练,还可以在ARM'Swebsite上“学习架构”指南。它可能会知道架构的官方名称正处于AARCH64,但“ARM64”似乎更常见。

还要假设我们没有东西isplike absl :: filedarrayavailable。 我不想进一步复杂化这个例子。 [返回] 我用-fno-fexections构建,以通过删除异常清理路径来简化示例。 它似乎在尾呼叫后似乎可能会令人困惑。 [返回] 就像我们在本文的X86-64版本一样,我认为不需要这个MOVX21,SP。 在我们移动SP,X21之前,不会再次使用X21,但是该指令紧接在MOV SP,X19,X19,覆盖SP。 我认为我们可以通过删除往返x21来改善Thecode。 [返回]