x86_64汇编系列:
-
x86_64汇编之一:AT&T汇编语法
-
x86_64汇编之二:x86_64的基本架构(寄存器、寻址模式、指令集概览)
-
x86_64汇编之三:x86_64汇编和x86_32汇编的区别
-
x86_64汇编之四:函数调用、调用约定
-
x86_64汇编之五:System V AMD64调用约定下的函数调用
一、x86_64中的寄存器
x86_64架构中的寄存器可分为以下几类:
- 通用寄存器 (General-Purpose registers)
- 状态和控制寄存器(RFLAGS register)
- 指令寄存器 (RIP)
- XMM寄存器
- 浮点控制和状态寄存器 (MXCSR)
- 等等
如下图所示:
通用寄存器
通用寄存器主要用于完成一些通用的功能,包括算数运算、逻辑运算、比较运算、数据转移、地址计算,还可以临时存放常量、中间结果、指针等内容。
x86_64中的通用寄存器如下:
x86_64中的一个通用寄存器有4种用法。以RAX为例:
- 可以被当做RAX使用——64位
- 可以被当做EAX使用——32位
- 可以被当做AX使用——16位
- 可以被当做AL使用——8位
对于通用寄存器的用途,有一些隐含的规定,有时候某些指令必须使用特定的寄存器来进行操作,例如:
-
有符号乘法指令
imul
默认会将乘积存放在RDX:RAX,EDX:EAX,DX:AX,或AX中。 -
有符号除法指令
idiv
默认要求被除数放在RDX:RAX,EDX:EAX,DX:AX,或AX中。 - x86的字符串操作要求源操作数和目的操作数的地址分别存放在RSI和RDI中
-
RBP是栈基址指针,RSP是栈顶指针。通常,x86_64中的栈的操作单位是
8字节
,有些运行环境也可能是16字节。 - 等等
RFLAGS寄存器
x86_64中,RFLAGS是一个64位寄存器,其每一个比特位有不同的含义。
其中,最常用的标记位有:
-
进位标识 CF:
无符号数
的算数运算产生溢出会导致CF被设置;寄存器移位指令也可能会导致CF被设置;等等 -
溢出标识 OF:两个
有符号数
的算数运算可能导致OF被设置 - 奇偶校验位 PF
- 符号位 SF:用于标识算数指令和逻辑运算指令运算结果的符号
- 零标识位 ZF:用于标识算数指令和逻辑运算指令运算结果是不是0
RFLAGS寄存器中每一位的含义可以参考:https://en.wikipedia.org/wiki/FLAGS_register
RFLAGS中每一位何时被设置可以参考:http://www.c-jump.com/CIS77/ASM/Instructions/I77_0070_eflags_bits.htm
每个指令对RFLAGS中标记位的影响可以参考该指令相关的文档
指令寄存器
指令寄存器(RIP)包含下一条将要被执行的指令的逻辑地址。
通常情况下,每取出一条指令后,RIP会自增指向下一条指令。在x86_64中RIP的自增也即偏移8字节。
但是RIP并不总是自增,也有例外,例如
call
指令和
ret
指令。
call
指令会将当前RIP的内容压入栈中,将程序的执行权交给目标函数;
ret
指令则执行出栈操作,将之前压入栈的8个字节的RIP地址弹出,重新放入RIP。
浮点数寄存器: XMM/YMM/ZMM
XMM0-XMM15是一系列
128位
寄存器,它主要用于:
-
32位和64位
浮点数
的操作 - SIMD指令:SIMD即Single Instruction Multiple Data。一条SIMD指令可以同时接受多个数据流,提升处理速度。
- SSE指令(Streaming SIMD Extension):一般用不到,不详细讨论。
此外,后续x86_64机器引入了AVX扩展,随之而来的是256位的YMM寄存器和512位的ZMM寄存器,也是类似的功能。随着ZMM的出现,XMM和YMM寄存器的个数被扩展到了32个。
需要说明的是,本文第一幅图对XMM/YMM/ZMM关系的描述其实不太准确,下图才是准确的:XMM是YMM的低半部分,YMM是ZMM的低半部分。
二、x86_64的寻址方式
参考:
https://blog.yossarian.net/2020/06/13/How-x86_64-addresses-memory
https://cs.nyu.edu/courses/fall10/V22.0201-002/addressing_modes.pdf
有效地址的计算
在x86_64架构中,
有效地址
(effective address) 的计算公式如下:
EffectiveAddress = BaseReg + IndexReg * ScaleFactor + Disp
其中:
-
BaseReg是
基址寄存器
,可以是任意一个通用寄存器 -
IndexReg是
索引寄存器
,可以是除了RSP之外的任意一个通用寄存器 - ScaleFactor的取值可以是1,2,4,8
- Disp是偏移量,可以是长度为8bit,16bit,32bit的有符号整数
关于上述公式中各个部分的取值,有一些隐含的规定:
- 如果没指明偏移量Disp,则默认是0
- 最终的有效地址长度始终是64bit
下标展示各种
寻址方式
(第一列)及其对应的汇编指令,可以看到,各种寻址方式其实都是由上述公式得来的。
上表中的各种寻址方式对应到C/C++代码中的何种操作呢?
-
Disp
:访问全局变量或静态变量通常使用这种方式 -
BaseReg
:在使用指针获取对应变量的值时,通常使用这种寻址方式。
rbx
存放被指针指向的变量的地址。 -
BaseReg + Disp
:访问某种数据结构(例如结构体、类对象)中的某个成员通常使用该模式 -
IndexReg * SF + Disp
:访问数组通常使用该模式。此时
SF
代表数组元素的大小,例如如果是
int
类型,那么
SF
就是4 -
访问更复杂的数据结构时,可能需要
BaseReg, IndexReg, ScaleFactor, Disp
4者的相互搭配,对应余下的情况。
更详细的内容可以参考这篇文章:https://blog.yossarian.net/2020/06/13/How-x86_64-addresses-memory
三、x86_64指令集概览
上表列出了x86_64指令集中的主要指令。其中大多数算数和逻辑运算指令都会修改RFLAGS寄存器中的若干个标记位。
某些基于条件的指令会用到RFLAGS中被设置的标记位,例如:
-
jcc
:
Jump if Condition Is Met
,即
条件跳转指令
。
jcc
是一类指令,包括
jg
(大于就跳转),
je
(等于就跳转),
jl
(小于就跳转)等等。总之,
jcc
指令的格式就是
j+后缀
表示特殊的含义。更多
jcc
指令可以参考https://www.felixcloutier.com/x86/jcc。 -
cmovcc
:
Conditional Move
,即条件mov指令。类似的,对应的指令是
cmov+后缀
格式,包括
cmovg, cmove, cmovl
等指令。 -
setcc
:
Set Byte on Condition
,即条件set指令,仅设置一个字节。类似的,对应的指令是
set+后缀
格式,包括
setg, sete, setl
等。
上述三类条件指令支持的后缀、每种后缀的含义以及使用的标记位如下表所示: