• PIE(Position Independent Executable,位置无关的可执行文件)通过随机化可执行文件各个部分在虚拟内存中的地址使得攻击者无法通过预测地址进行恶意行为。

汇编开发工具:

# 5-basic.s
.section __TEXT,__text
.globl _main
.p2align 2
_main:
mov w0, #0
ret

注释

在 macOS 的 as 汇编器语法下,如果一行由 # 开头,那么这一行会被认为是注释行。

我们习惯上将注释写在语句的上方(如例程)或后方。在语句后方写注释时,一般采用 ; 作为注释开头的符号,如:

mov    w0, #0    ; Mov 0 to register w0

缩进

在最古老的机器上,汇编代码的文本包含四列:标签、助记符、操作数与注释。汇编器通过识别一个文本在哪个列来判断该文本有什么作用。现代的汇编器已经抛弃了这种方法,采用先进的词法分析技术来判断。但是,我们最好仍然按照这种格式来缩进。

也就是说,我们在写一个完整程序的时候,一般会将指令缩进 4 个空格,而如 _main: 之类的标签则不进行缩进。

汇编器指令(Directive)

"Directive"是汇编语言中一个重要的组成部分,然而它的中文译名似乎还不固定,这里暂且叫它汇编器指令。在汇编语言中,以 . 开头的都是汇编器指令,如例程中的 .section, .globl等。由汇编器指令开头的语句,一般不会被直接翻译成机器码。汇编器指令并不是告诉汇编器做什么, 而是告诉汇编器如何做。就比如说例程中,mov w0, #0 会被汇编器直接翻译为机器码,最终会由 CPU 直接执行,而 .section __TEXT,__text, 则不会被翻译成机器码,在最终的可执行文件中也不会找到这句话的踪影。它的作用是告诉汇编器如何汇编。

.section

Mach-O 可执行文件的 Data 部分拥有许多段(Segment), 每个段又有许多节(section)。同一个段的作用往往是类似的,同时在执行的时候一个段会被分配到一个页之中。而 .section 最常用的格式,就是:

.section    segname, sectname

其中 segname 是段名,sectname 是节名。我们目前编写的第一个汇编语言程序,只包含纯代码。在 Mach-O 中,纯代码被放在了 __TEXT 段的 __text 节中,因此,我们在文件的第二行写了:

.section    __TEXT, __text

代表之后的语句都是 __TEXT 段的 __text 节中。

此外,由于这个节过于常用,因此,汇编器给予了我们一个简单的记号:.text。我们可以直接用 .text 代替 .section __TEXT, __text

除了 __TEXT__text 节后,还有许多段和节。常用的段和节的名称和作用可参见 Assembler Directives

.globl

在一个程序编译、链接、动态链接的过程中,有一些变量、函数的名字,需要作为字符串存储在二进制程序中,以便将来的某些时候使用。因此,我们可以指定一些标识符的可见性(Visibility)。

对于这个程序而言,我们在学习 C 语言的时候就了解到,main 函数是一个 C 语言程序开始的起点。事实上,链接器需要知道 main 函数这个名字,以便后续与 C 运行时的链接。因此,我们可以用 .globl _main 的方式,让链接器知道我们提供了 main 函数。

_main

macOS 中,C 语言程序执行的起点在汇编层面是 _main 函数。

.p2align

.section.globl 一样,这也是一个汇编器指令。这个汇编器指令的作用是指令对齐。

mov

mov 是我们遇到的第一个真正的指令。在汇编语言中,这种能直接翻译成机器码的指令被称作助记符(mnemonic)。在 GNU 语法下,一条指令可以粗略地看作是 助记符+目的+源,也就是说,它后面紧跟的是目的操作数,然后是源操作数。

首先我们先要理解 mov。 这是一个在汇编语言中很常见的指令,意思是赋值。mov a b 就是将 b 赋值给 a。 它可以将立即数赋值给寄存器,可以把寄存器赋值给寄存器。

#0

mov 的源操作数是 #0。一般来说,在汇编语言中的常数都会在前加 # 符号,让读者看得更清楚。当然,不加这个 # 一样可以正常进行汇编。

此外,我们也可以在前面加 0x 来表示 16 进制数,如

mov    w0, #0xFF

ret

这个指令可以类似于 C 语言中的 return

总结

因此,根据以上的讨论,我们可以将第一个汇编程序翻译成 C 程序了:

// 5-basic.c
int main() {
return 0;
}

这就是我们第一个汇编程序的作用,也就是将 main 函数返回 0

A64 指令集的汇编指令格式一般来说,是:

{opcode {dest{, source1{, source2{, source3}}}}}

其中,opcode 指这条指令的操作码,在汇编语言中常用助记符表示。dest 为目的操作数,source 为源操作数。

寄存器

在 AArch64 架构下,有 31 个通用寄存器。这些通用寄存器可以作为大部分指令的操作数参与运算。

有三套记号用于指代这 31 个通用寄存器:

  • r0r30

    一般用这套记号来指代这些寄存器本身。这些记号通常用于描述汇编指令行为,不会参与到汇编指令中。

  • x0x30

    一般用这套记号表示这些寄存器的 64 位部分。例如,x3 表示 r3 寄存器的 64 位部分。由于 AArch64 架构下的通用寄存器都是 64 位的,所以这套记号其实就代表这些寄存器的所有位。

  • w0w30

一般用这套记号表示这些寄存器的低 32 位部分。例如,w3 表示 r3 寄存器的低 32 位部分。

寄存器 xzrwzr 被称为零寄存器。所谓零寄存器,就是指读取该寄存器的值,永远为 0;向该寄存器写入数值将无效,也就是说无法向该寄存器写入数值。其中 xzr 为 64 位的零寄存器,wzr 为 32 位的零寄存器。

为什么存在零寄存器:由于精简指令集的原因,部分指令无法直接使用常数作为操作数。但是 0 作为一个特殊的常数经常出现在各种程序逻辑中,那么零寄存器的出现就可以省去将常数 0 存储到寄存器中的步骤。此外,使用零寄存器,也可以简化指令内部的伪指令逻辑。零寄存器并不需要是一个物理意义上的寄存器。

sp 寄存器代表栈顶的内存地址。

pc 寄存器全称为 Program Counter,该寄存器内存储的是即将执行的指令的地址,当 CPU 执行一个指令时,其首先会访问 pc 寄存器,将其存储的值看作下一条指令地址,从内存中获取相应的指令,进一步译码、执行。对于黑客来说,攻击一个程序,往往本质上都是控制程序的 pc 寄存器,使其值由自己控制,从而能够让程序执行攻击者想要执行的指令。

赋值指令

同宽度赋值:

mov    x0, x1  ; 源寄存器和目的寄存器的宽度必须相同

扩展赋值:

  • 有符号扩展赋值(signed extend):sxtbsxthsxtw
  • 无符号扩展赋值(unsigned extend):uxtbuxthuxtw(事实上,uxtw 有些特殊,该指令并没有在 ARM 官方文档中记录,汇编器也是将其翻译为 ubfx 指令)。

其中,

  • b(byte)结尾的指令:将源寄存器的最低位的一个字节赋值给目的寄存器,并进行相应的扩展。
  • h(halfword)结尾的指令:将源寄存器的最低位的两个字节赋值给目的寄存器,并进行相应的扩展。
  • w(word)结尾的指令:将源寄存器的最低位的四个字节赋值给目的寄存器,并进行相应的扩展。

这类指令的源操作数必须是 32 位寄存器,而目的操作数则可以是 64 位寄存器,也可以是 32 位寄存器(以 w 结尾的指令除外)。

截断赋值:

截断就是指,从大宽度的寄存器向小宽度的寄存器赋值。这一过程比较粗暴,就是直接将相应的部分赋值即可,不考虑任何符号因素。例如,如果想将 x0 的值赋值给 w1,我们需要做的就是使用 mov w1, w0,也就是不考虑其高位,也不考虑其符号。

常数赋值:

由于 AArch64 是定长指令集架构,其所有的指令在二进制层面长度都是 32 位。因此无法直接装载太大的立即数。借助 ldr (load register)伪指令,可以将立即数存储在二进制镜像的数据区,然后产生一个内存读取指令,再通过读取相应内存将立即数载入寄存器。

mov    w0, #0  ; 较小的立即数赋值
ldr x1, =0x0123456789abcdef ; 大数必须使用 ldr

数据处理指令

常见的数据处理指令包括加、减、乘、除、求余、与、或、非、异或等。大部分的数据处理指令都是二元运算,即,将两个操作数进行计算,然后赋值给第三个操作数。因此,这些二元运算指令大都有如下的形式:

opcode    dest, source1, source2
add    dest_reg, src_reg1, src_reg2/imm  ; 加
sub dest_reg, src_reg1, src_reg2/imm ; 减
and dest_reg, src_reg1, src_reg2/imm ; 与
orr dest_reg, src_reg1, src_reg2/imm ; 或
mvn dest_reg, src_reg ; 非
eor dest_reg, src_reg1, src_reg2/imm ; 异或
mul    dest_reg, src_reg1, src_reg2
umull dest_reg, src_reg1, src_reg2
smull dest_reg, src_reg1, src_reg2

其中,mul 指令的三个操作数都是 32 位寄存器,umullsmull 的源操作数是 32 位寄存器,目的操作数是 64 位寄存器。

umull 代表无符号乘法,smull 代表有符号乘法。

  • sdiv   dest_reg, src_reg1, src_reg2
    udiv dest_reg, src_reg1, src_reg2

    其中,sdiv 代表有符号除法,udiv 代表无符号除法。

  • 求余

    A64 指令集不提供直接的求余计算。如果我们想求存储有符号整数的寄存器 w1w2 的余数,结果存储在 w0 中,那么我们可以这么做:

    sdiv   w0, w1, w2
    mul w0, w2, w0
    sub w0, w1, w0

移位操作

逻辑左移(Logical Shift Left):

lsl   w0, w1, #2

操作数的可选移位:将某个寄存器的值乘以 2 的倍数往往是一个常见的中间操作。因此,AArch64 针对这种情况,对部分指令进行了优化。当我们使用部分指令的时候,可以附带一个移位。例如:

add    w0, w1, w2, lsl #2  ; 先将 w2 的值乘 4,再加上 w1 的值,赋值给 w0

内存交互指令

基本的内存交互指令是 ldrstr。这两条指令的用法为:

ldr[{sign}]{size}    dest_reg, [mem_addr]  ; Load Register,将内存数据读取到寄存器中
str{size} dest_reg, [mem_addr] ; Store Register,将寄存器数据存储到内存中
  • {sign} 表示是否有符号扩展。例如,ldrsb 将内存中 1 字节的内容,有符号扩展地存储到寄存器中。ldrb 则是无符号扩展。

  • {size} 是操作长度。b(byte)表示 1 字节,h(halfword)表示 2 字节,w(word)表示 4 字节。当我们想表示的字节与目的操作数的宽度一致时,可以省略。例如,如果想将 w0 的全部 4 字节存储到内存中,那么我们既可以写 strw w0, [mem_addr],也可以写 str w0, [mem_addr]

例:

strb    w0, [mem_addr]  ; 将 r0 寄存器最低位的 1 字节的内容,存储到地址为 mem_addr 的内存中
ldrh x1, [mem_addr] ; 将内存 mem_addr 处开始的 2 字节的内容,无符号扩展地存储到 r1 寄存器的低 2 字节位置
ldrsb w2, [mem_addr] ; 将内存 mem_addr 处开始的 1 字节的内容,有符号扩展地存储到 r2 寄存器的最低的 1 字节中

端序:macOS 使用的是小端序,即数值的低位存储到内存的低位。

数据对齐(Alignment)

在绝大多数指令集架构中,都会有数据对齐的要求。意思是说,我们读取/写入内存时,对内存地址本身也是有要求的。一般来说,对齐的字节数与读取/写入的字节数相同。例如,我们使用 ldrw 从内存中读取 4 字节的内容,那么根据要求,我们读取的地址本身,需要是 4 的倍数。

struct AlignedStruct {
short a; // 2 Bytes, pos 0x0
char b; // 1 Byte, pos 0x2
// 1 Byte padding
int c; // 4 Bytes, pos 0x4
}; // pos 0x8

寻址模式

寄存器寻址

将地址存储在寄存器中,访问寄存器指向的内存。

ldr    w1, [x0]
基址寻址
基寄存器加偏移

访问结构体的某一字段:

struct Foo {
int a; // pos 0x0
int b; // pos 0x4
}; struct Foo *foo = get_foo_ptr();
// accessing foo->b
// ...

对应的 ASM 代码:

ldr    w1, [x0, #4]
基址寄存器加寄存器偏移
char a[64];
for (size_t i = 0; i < 64; i++) {
char b = a[i];
// ...
}
ldr    w2, [x0, x1]

对于整型数组:

ldr    w2, [x0, x1, lsl #2]

调用 malloc

bl    _malloc
; Here x0 has heap address

跳转

switch (a) {
case 0: /* do something A */ break;
case 1: /* do something B */ break;
}
// do something C
    ; Decide whether to do something A, B or C by a's value
zero_case:
; Do something A
b after_switch
one_case:
; Do something B
b after_switch
after_switch:
; Do something C

参考:在 Apple Silicon Mac 上入门汇编语言

参见:ARM Compiler toolchain Assembler Reference | ARM Developer

AArch64 汇编学习笔记的更多相关文章

  1. 汇编学习笔记(11)int指令和端口

    格式 int指令也是一种内中断指令,int指令的格式为int n,n是中断类型码.也就是说,使用int指令可以调用任意的中断例程,例如我们可以显示的调用0号中断例程,还记得在汇编学习笔记(10)中我们 ...

  2. 汇编学习笔记(3)[bx]和loop

    本文是<汇编语言>一书的学习笔记,对应书中的4-6章. 汇编程序的执行 要想将源代码变为可执行的程序需经过编译.连接两个步骤,WIN7操作系统下需要MASM程序来进行编译连接工作.将MAS ...

  3. 汇编学习笔记——DOS及DEBUG介绍

    转自:https://www.shiyanlou.com/courses/running/332 一.课程简介 声明:该课程基于<汇编语言(第2版)>郑晓薇 编著,机械工业出版社.本节实验 ...

  4. 汇编学习笔记(AT&T语法)

    一个最基本的汇编程序如下所示: .section .data .section .text .globl _start _start: movl $, %eax # the number 1 is t ...

  5. ARM汇编学习笔记

    ARM  RISC  (Reduced Instruction Set Computers) X86   CISC  (Complex Instruction Set Computers)      ...

  6. 汇编学习笔记(14)BIOS对键盘输入的处理

    字符的处理 键盘输入的字符一般由int9中断例程从60h端口中读取,并存放在键盘缓冲区中,由int16h例程从键盘缓冲区中读取相应字符,CPU对键盘输入a.shift_a的处理过程如下 1.一开始没有 ...

  7. 汇编学习笔记(7)call和ret指令

    ret和retf CPU执行ret指令时进行以下两步操作: (IP)=((ss)*16+(sp)) (sp)=(sp)+2 这相当于pop IP CPU执行retf指令时进行以下四步操作: (IP)= ...

  8. [汇编学习笔记][第十七章使用BIOS进行键盘输入和磁盘读写

    第十七章 使用BIOS进行键盘输入和磁盘读写 17.1 int 9 中断例程对键盘输入的处理 17.2 int 16 读取键盘缓存区 mov ah,0 int 16h 结果:(ah)=扫描码,(al) ...

  9. [汇编学习笔记][第十三章int指令]

    第十三章int指令 13.1 int指令 格式: int n, n 为中断类型码 可以用int指令调用任何一个中断的中断处理程序(简称中断例程). 13.4 BIOS和DOS 所提供的中断例程 BIO ...

  10. [汇编学习笔记][第十章 CALL和RET指令]

    第十章 CALL和RET指令 call和ret指令都是转移指令,它们都修改CS和IP.经常被共同用于实现子程序的设计.这一章,我们讲解call和ret指令的原理 10.1 ret和retf ret指令 ...

随机推荐

  1. VS图片

  2. 【ClickHouse】5:clickhouse集群部署

    背景介绍: 有三台CentOS7服务器安装了ClickHouse HostName IP 安装程序 程序端口 centf8118.sharding1.db 192.168.81.18 clickhou ...

  3. rust项目中通过log4rs将日志写入文件

    java项目中使用最广泛的日志系统应该是log4j(2)了.如果你也是一个Java程序员,可能在写rust的时候会想怎么能顺手地平移日志编写习惯到rust中来. log4rs就是干这个的.从名字就能看 ...

  4. css-渐变简约的登录设计

    代码如下 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF- ...

  5. webgl智慧楼宇发光系列之线性采样下高斯模糊

    目录 webgl智慧楼宇发光系列之线性采样下高斯模糊 效率问题 线性采样 代码讲解 总结 参考文档 webgl智慧楼宇发光系列之线性采样下高斯模糊 前面一篇文章 <webgl智慧楼宇发光效果算法 ...

  6. 统计里面PV 和 UV代表什么意思

    1.网站流量bai统计中"PV"它所代表的意思是访问量了,具体指的du就是网站zhi的页面点击量或是浏览量,亦或是页面的刷新量dao了,网站的页面每刷新一次,就统计一个" ...

  7. oeasy教您玩转vim - 39 - # 剪切粘贴

    ​ 剪切粘贴 回忆上节课内容 我们大幅度地复习了整个 motion: 直接运动 h j k l 行运动 首行g g 末行G 第n行n G 单词运动 wbe w 是到下一个 word 的开头 b 是到当 ...

  8. ASP.NET Core WebAPI 使用CreatedAtRoute通知消费者

    一.目的 我想告诉消费者我的api关于新创建的对象的位置 二.方法说明 public virtual Microsoft.AspNetCore.Mvc.CreatedAtRouteResult Cre ...

  9. 2023/4/18 SCRUM个人博客

    1.我昨天的任务 初步学习dlib的安装,了解dlib的基础组件 2.遇到了什么困难 对pandas库了解不到位,需要学习其中的基础 3.我今天的任务 初步了解了pandas库,对series和dat ...

  10. c# 多线程环境下控制对共享资源访问的办法

    Monitor: 定义:Monitor 是 C# 中最基本的同步机制,通过 Enter 和 Exit 方法来控制对共享资源的访问.它提供了排他锁的功能,确保在任何时刻只有一个线程可以访问共享资源. 优 ...