目前 x86_64 虚拟地址空间高 16 位必须为 0.对应的空间大小为 64TiB
一、寄存器
-
程序计数器 %rip
-
16 每个整数寄存器 64 位
名称 低32位名称 低16位名称 低8位名称 用途 %rax ?x %ax %al 返回值 %rbx ?x %bx %bl 被调用者保存 %rcx ?x %cx %cl 第四个参数 %rdx ?x %dx %dl 第三个参数 %rsi %esi %si %sil 第二个参数 %rdi ?i %di %dil 第一个参数 %rbp ?p %bp %bpl 被调用者保存 %rsp %esp %sp %spl 栈指针 %r8 %r8d %r8w %r8b 第五个参数 %r9 %r9d %r9w %r9b 第六个参数 %r10 %r10d %r10w %r10b 调用者保存 %r11 %r11d %r11w %r11b 调用者保存 %r12 %r12d %r12w %r12b 被调用者保存 %r13 %r13d %r13w %r13b 被调用者保存 %r14 %r14d %r14w %r14b 被调用者保存 %r15 %r15d %r15w %r15b 被调用者保存 -
每个条件码寄存器 1 位(用于实现程序控制流)
CF 进位标志,表示最近的操作使最高位置发生进位或借位,用于检查无符号操作的溢出。 ZF 零标志表示最近的操作结果是 0。 SF 符号表示最近的操作结果为负。 OF 溢出标志表示补码正溢出或负溢出是最近操作产生的。
-
向量寄存器(存储多个整数或浮点数)
二、ATT 格式对比 Intel 格式
-
ATT(gcc, objdump) 先写源,再写目的;Intel 写源前先写目的
-
ATT 指令有指示类型和大小的后缀;Intel 没有
后缀名 b w l q s 含义 字节 字 双字 四字/双精度浮点数 单精度浮点数 -
ATT 前面有寄存器 % 符号;Intel 没有
-
ATT 访问内存格式: I m m ( r b , r i , s ) = r b r i ? s I m m Imm(r_b,r_i,s) = r_b r_i * s Imm Imm(rb,ri,s)=rb+ri∗s+Imm 这四个参数分别是立即数(不带 $ 符号)、基址寄存器、变址寄存器、比例因子(只能是 1、2、4、8),其中任意一项均可忽略
例:
0x9C(%rax,%rcx,2)
-
Intel 访问内存格式:
QWORD PTR [rbx]
-
ATT 的立即数格式:$ 符号后写 C 格式整数
三、MOV 系列指令
- 源和目的不能都是内存地址。
- 使用的寄存器大小和指令后缀必须匹配,通常只更新低位,高位不变,但
movl
会把高 32 位置零,原因是 x86-64 的惯例是一切给寄存器赋 32 位值的指令都必须把高 32 位置零。 movq
的源如果是立即数,其只支持 32 位补码范围内的立即数,若要使用 64 位补码范围内的立即数,则要使用movabsq
指令movz
指令后面跟两个后缀,其中第一个后缀要小于第二个后缀,该指令表示传送时把高位置零(零扩展)movs
指令后面跟两个后缀,其中第一个后缀要小于第二个后缀,该指令表示传送时把高位置为符号位(符号扩展)cltq
相当于movs %eax, %rax
clto
对%rax
进行符号扩展(扩展到八字),高 64 位放在%rdx
处,低 64 位放在%rax
处
四、栈
pushq %rbx
相当于先执行movq %rbx,-8(%rsp)
,再执行subq $8,%rsp
popq %rax
相当于先执行addq $8,%rsp
,再执行movq -8(%rsp),%rax
由此可推出特殊情况:假定初始时 %rsp 存储的值是 0x1008,内存地址 0x1008 存储的值是 0x1234 则 pushq %rsp
会导致内存地址 0x1000 存储值 0x1008(旧值入栈),popq %rsp
会导致 %rsp 存储 0x1234。
五、算术运算系列指令
只有目的操作数:自增 1 inc
,自减 1 dec
,取负 neg
,取反 not
。 有源操作数和目的操作数:加 add
,减 sub
,乘 imul
,异或 xor
,或 or
,与 and
。 如 add %rbx,%rax
表示 %rax += %rbx。 有常量操作数和目的操作数:左移 sal
(shl
),算术右移 sar
,逻辑右移 shr
。 以上指令的后缀和寄存器大小要对应,且均会影响条件码。
特殊:leaq 内存地址,目的寄存器
,表示将该地址值赋给目的寄存器,该指令不会影响条件码。 例:leaq 7(%rdx,%rdx,4),rax
假设 %rdx
中的值为 x x x,则 %rax
会被赋值 5 x + 7 5x+7 5x+7。
xor
会将溢出和进位标志置 0。 inc
和 del
会设置溢出和零标志,但不会改变进位标志。
移位操作的位移量操作数也可以是 8 位寄存器 %cl
,对于 ω \omega ω 位数据的移位操作,具体的位移量取决于 %cl
的低 l o g 2 w log_2w log2w 位,进位标志为最后被移出的位,溢出标志会被置 0。
单操作数乘法指令 imulq
和 mulq
是全乘法,即会生成 128 位的结果,前者为有符号乘法,后者为无符号乘法,运算结果的高 64 位放在 %rdx
处,低 64 位放在 %rax
处。
六、流程控制系列指令
6.1 CMP 和 TEST 系列指令
cmp
基于后减前的结果将相应的条件码置位。 test
基于后与前的结果将相应的条件码置位。 注意指令的后缀和寄存器大小要对应。
6.2 SET 和 JMP 系列指令
set
指令只有一个操作数,且必须是单字节寄存器或内存位置,该类型指令对条件码进行指定的运算并向操作数返回 0 或 1
名称 | 运算 | 对应的 CMP 结果 |
---|---|---|
sete setz | ZF | 等于 |
setne setnz | ~ZF | 不等于 |
sets | SF | 负数 |
setns | ~SF | 非负数 |
setg setnle | ~(SF^OF) & ~ZF | 大于(有符号) |
setge setnl | ~(SF^OF) | 大于等于(有符号) |
setl setnge | SF^OF | 小于(有符号) |
setle setng | (SF^OF) | ZF | 小于等于(有符号) |
seta setnbe | ~CF & ~ZF | 超过(无符号) |
setae setnb | ~CF | 超过或相等(无符号) |
setb setnae | CF | 低于(无符号) |
setbe setna | CF | ZF | 低于或相等(无符号) |
jmp
后可以跟标签名,或加 * 号后面跟操作数。 j + 比较后缀
(参考 SET 指令)可以实现条件跳转,这类命令只能跳转到标签。 注意,不应该通过跳转到达 ret
或 retq
(同义指令),因为这样 CPU 无法推测 ret
的含义,通常在前面加 rep
或 repz
(同义指令)。
6.2.1 分支预测
由于现代处理器采用流水线,同时执行多个指令,故需要预测条件跳转会走向哪一个分支,当预测错误时,流水线预测的错误部分会被全部重置转而加载并执行正确部分,如此需要的惩罚时间较长。
预测错误的罚时计算方法: 假定错误概率为 p p p,预测正确的总运行时间为 T O K T_{OK} TOK,预测错误的罚时为 T M P T_{MP} TMP,则程序的期望运行时间为 T a v g ( p ) = T O K + T M P T_{avg}(p)=T_{OK}+T_{MP} Tavg(p)=TOK+TMP,令 T r a n = T a v g ( 0.5 ) T_{ran}=T_{avg}(0.5) Tran=Tavg(0.5),即纯随机预测的情况,则可以得到 T M P = 2 ( T r a n − T O K ) T_{MP}=2(T_{ran}-T_{OK}) TMP=2(Tran−TOK) 。
6.3 CMOV 系列指令(条件传送)
需要添加条件后缀,表示只有当满足该条件时才会进行 MOV 操作。 无需显式指定字长后缀,但不能传单字节值。
6.4 C 语言的条件分支
6.4.1 C 语言的 if-else 语句的转换
// original format
if (test-expr) {
// then-statements
}
else {
// else-statements
}
// format closer to assembly language
t = test-expr;
if (!t)
goto False;
// then-statements
goto Done;
False:
// else-statements
Done:
6.4.2 C 语言的条件表达式(三目运算符)的转换
// original format
v = test-expr ? then-expr : else-expr;
// format 1 closer to assembly language
if (!test-expr)
goto False;
v = then-expr;
goto Done;
False:
v = else-expr;
Done:
// format 2 closer to assembly language (maybe illegal)
v = then-expr;
ve = else-expr;
t = test-expr;
if (!t)
v = ve;
上述的第二种写法避免了利用条件传送避免了条件预测,但 then-expr 或 else-expr 可能为非法的表达式,亦或是需要较大计算量的表达式,这就需要编译器在条件跳转和条件传送之间进行权衡,GCC 通常在两个表达式都很容易计算时才采用条件传送。
6.5 C 语言的循环
6.5.2 C 语言的 do-while 循环的转换
// original format
do {
// body-statements
} while (test-expr);
// format closer to assembly language
loop:
// body-statements
t = test-expr;
if (t)
goto loop;
6.5.3 C 语言的 while 循环的转换
// original format
while (test-expr) {
// body-statements
}
// format 1 closer to assembly language (jump to middle)
goto test;
loop:
// body-statements
test:
t = test-expr;
if (t)
goto loop;
// format 2 closer to assembly language (guarded-do)
t = test-expr;
if (!t)
goto done;
loop:
// body-statements
t = test-expr;
if (t)
goto loop;
done:
6.5.4 C 语言的 for 循环的转换
// original format
for (init-expr; test-expr; update-expr) {
// body-statements
}
// format 1 closer to assembly language (jump to middle)
init-expr;
goto test;
loop:
// body-statements
update-expr;
test:
t = test-expr
if (t)
goto loop;
// format 2 closer to assembly language (guarded-do)
init-expr;
t = test-expr;
if (!t)
goto done;
loop:
// body-statements
update-expr;
if (t)
goto loop;
done:
6.6 C 语言的 switch-case 语句
// original format example
void fun(int n) {
switch (n) {
case 100:
statement1;
break;
case 102:
statement2;
case 103:
statement3;
break;
case 104:
case 106;
statement4;
break;
default:
statement0;
}
statement;
}
// format closer to assembly language (use jump table)
// The && operator represents a pointer to the address of the code segment
void fun(int n) {
static void* jt[7] = {
&&loc_A, // case 100
&&loc_def, // case 101
&&loc_B, // case 102
&&loc_C, // case 103
&&loc_D, // case 104
&&loc_def, // case 105
&&loc_D, // case 106
};
size_t index = n - 100;
if (index > 6)
goto loc_def;
goto *(jt[index]);
loc_A:
statement1;
goto done;
loc_B:
statement2;
loc_C:
statement3;
goto done;
loc_D:
statement4;
goto done;
loc_def:
statement0;
done:
statement;
}
fun:
subq $100, %rdi; n in %rdi
cmpq $6, %rdi
ja .L8
jmp *.L4(,%rdi,8)
.L3:
;statement1
jmp .L2
.L5
;statement2
.L6
;statement3
jmp .L2
.L7
;statement4
jmp .L2
.L8
;statement0
.L2:
;statement
ret
.section rodata
.align 8
.L4
.quad .L3
.quad .L8
.quad .L5
.quad .L6
.quad .L7
.quad .L8
.quad .L7
七、过程
7.1 栈的结构
---------------------- 高地址(栈底)
--------------- 栈帧 n
--------------- 栈帧 n - 1
.
.
.
--------------- 栈帧 2
参数 m
参数 m - 1
.
.
.
参数 7
返回地址
--------------- 栈帧 1(当前正在执行的函数)
被保存的寄存器
-------------
局部变量
-------------
参数构造区
---------------------- 低地址(栈顶)(%rsp 指向的位置)
7.2 转移控制
call
(等价于 callq
)会将 call
的下一个指令的地址(返回地址)压入栈,并将 %rip
置为调用过程的地址。 ret
(等价于 retq
)会将栈中的返回地址弹出,并将 %rip
置为返回地址。
7.3 数据传送
x86-64 允许通过寄存器传递最多 6 个整型参数,分别位于 %rdi
, %rsi
, %rdx
, %rcx
, %r8
, %r9
。 若需传递低于 64 位的数据,则使用对应的低位部分的寄存器即可。 当函数的参数多于 6 个时,把多出来的参数放在栈帧中,其中第 7 个参数靠近栈顶,即 8(%rsp)
,栈帧中存储的参数大小必须为 8 字节的倍数,若不足则会进行内存对齐,在高地址进行填充。 如果正在执行的过程调用了超过 6 个参数的函数,则需要在自己的栈帧中的参数构造区处为多出来的参数分配空间。
7.4 局部变量
局部变量必须存储在内存的情况包括:
- 寄存器数量不足
- 对变量进行了取地址操作
- 局部变量为数组或结构体类型
将局部变量保存到内存中时,先减少 %rsp
的值,再将局部变量逐个放到栈中(无需内存对齐),当前函数执行结束后将 %rsp
的值复原,即释放局部变量占用的内存。
7.5 被调用者保存寄存器
被调用者保存寄存器包括 %rbx
, %rbp
, 以及 %r12
到 %r15
,当一个过程调用另一个过程时,这些寄存器需要保证在另一个过程返回时,寄存器的值能回复到调用前的状态,即这些寄存器的值是局部的。 被调用的过程可以选择不改变这些寄存器的值,或者将这些寄存器的值存储到自己的栈帧中。 这种特性可以用来实现对递归过程的调用参数的存储。 剩下的寄存器除了 %rsp
均为调用者保存寄存器。
7.6 帧指针 %rbp
和变长数组
Linux 支持变长数组,即数组声明的长度可以不是 constexpr。若要引用变长数组中的元素,仅有栈顶指针 %rsp
是无法访问的,故引入帧指针 %rbp
,该指针指向的位置相当于不考虑返回地址和保存寄存器的栈底,变长数组通过相对于 %rbp
的偏置值来定义其首地址。
八、数组
8.1 数组的访问方式
假设 int 数组 E 的首地址在 %rdx 中,下标在 %rcx 中,则 E[i] 对应的地址为 (%rdx,%rcx,4)
。 例 1:返回 E + i - 1 leaq -4(%rdx,%rcx,4),%rax
例 2:返回 E[i - 3] movl -12(%rdx,%rcx,4),%eax
例 3:返回 &E[i] - E movl %rcx,%rax
8.2 嵌套数组
声明一个二维数组 int A[5][3]
相当于如下的声明:
// Equivalent to "using row3_t = int[3];" in C++
typedef int row3_t[3];
row3_t A[5];
二维数组在内存中以行优先的形式进行存储,即:
#define ROW_NUM 5
#define COL_NUM 3
size_t i, j;
Type D[ROW_NUM][COL_NUM];
&D[i][j] == D + sizeof(Type) * (COL_NUM * i + j); // true
; int get(int A[5][3], size_t i, size_t j) {
; return A[i][j];
;}
; A in %rdi, i in %rsi, j in %rdx
.get
leaq (%rsi,%rsi,2),%rax; 3 * i => %rax
leaq (%rdi,%rax,4),%rax; A + 4 * 3 * i = A + 12 * i => %rax
movl (&rax,%rdx,4),%eax; *(A + 12 * i + 4 * j) = *(A + 4 * (3 * i + j)) => %eax
C99 支持变长数组,通过 imulq 来计算 COL_NUM(此时是变量) * i,但这样会导致结果无法预测造成流水线的罚时。
九、结构体和联合
struct 通过对地址进行偏置访问每一个元素 union 无论访问哪一个元素其地址都是首地址,一个 union 的总大小为其最大元素的大小 union 可用于获取一些类型的位模式:
unsigned long long double2bits(double d) {
union {
double d;
unsigned long long u;
} temp;
temp.d = d;
return temp.u;
}
// Be careful of the byte order
// On a small-endian machine, word1 corresponds to the low 4 bytes
double uu2double(unsigned word1, unsigned word2) {
union {
double d;
unsigned u[2];
} temp;
temp.u[0] = word1;
temp.u[1] = word2;
return temp.d;
}
十、内存对齐
内存对齐可以简化处理器从内存读取数据的硬件设计,使处理器读取固定长度的内存,并且可以使小于等于该长度的数据类型能够存储在一个内存块中,比如对齐长度为 8,存储 double 数据时若有内存对齐,则读取该数据只需读一次内存,不会有一个数据放在两个内存块中的情况发生。 若有内存对齐,则可以推出任何长度为 k 的数据类型,其地址必为 k 的倍数。 对齐长度为 8 对应的汇编代码为 .align 8
,放在数据表前面。
十一、缓冲区溢出
写入的数组或字符串长度超出了预先分配的空间大小即为缓冲区溢出。根据实际写入的长度,可能造成如下几种溢出:写入了未被使用的栈空间、修改了返回地址、修改了调用者的栈帧。攻击者可利用其修改返回地址的特性将返回地址更改为攻击代码(一般是存储在栈中的字符串)。攻击代码可能通过启动 shell 来执行一些命令,可能把更改了的返回地址复原使用户难以察觉。
解决方案 1:栈随机化
使栈底的(等效)地址每次运行都为一个随机值。具体的方法是每次运行时先在栈上分配一段随机大小的空间,这个随机数的范围要足够大以增强不确定性。进一步地,Linux 的地址空间布局随机化会使代码段、库代码段、全局变量、栈、堆等地址全部进行随机化。但栈随机化可以通过空操作雪橇来破解,即在攻击代码前面加很长的一段空操作指令,如果这段空操作很长,则可以使返回地址大概率命中这段空操作的地址以实现攻击代码的执行。
解决方案 2:随机 canary 值
在缓冲区末尾添加一个每次运行都会随机重置的 canary 值,在恢复寄存器或返回前先检查 canary 值是否被改变,若改变则说明有缓冲区溢出的情况发生,程序异常终止。gcc 可以使用 -fno-stack-protector
来禁用 canary。
解决方案 3:设置内存访问权限
虚拟内存被分为固定长度的页,可以对页设置读、写、执行三种权限。这样可以使代码段只能执行,而栈只能读或写。
十二、AVX2 浮点体系结构
16 个寄存器分别名为 %ymm0
~ %ymm15
,每个寄存器 256 位,对应的低 128 位名为 %xmm0
~ %xmm15
,若用于存储标量浮点数,则使用寄存器的低 32 位或低 64 位。标量指令允许不对齐的内存地址。
12.1 浮点传送操作
vmovss
源和目的参数其中一个为内存地址,另一个为 XMM 寄存器,传送单精度浮点数。 vmovsd
源和目的参数其中一个为内存地址,另一个为 XMM 寄存器,传送双精度浮点数。 vmovaps
源和目的参数均为 XMM 寄存器,传送对齐的单精度浮点数向量。 vmovapd
源和目的参数均为 XMM 寄存器,传送对齐的双精度浮点数向量。
12.2 浮点数截断(向零取整)操作
源参数均为 XMM 寄存器或内存地址
vcvttss2si
目的参数为 32 位通用寄存器,将单精度浮点数转化为双字整数。 vcvttsd2si
目的参数为 32 位通用寄存器,将双精度浮点数转化为双字整数。 vcvttss2siq
目的参数为 64 位通用寄存器,将单精度浮点数转化为四字整数。 vcvttsd2siq
目的参数为 64 位通用寄存器,将双精度浮点数转化为四字整数。
12.3 整数转换为浮点数的操作
第二个源参数和目的参数均为 XMM 寄存器,一般忽略第二个源参数的具体取值
vcvtsi2ss
第一个源参数为内存地址或 32 位通用寄存器,将双字整数转化为单精度浮点数。 vcvtsi2sd
第一个源参数为内存地址或 32 位通用寄存器,将双字整数转化为双精度浮点数。 vcvtsi2ssq
第一个源参数为内存地址或 64 位通用寄存器,将四字整数转化为单精度浮点数。 vcvtsi2sdq
第一个源参数为内存地址或 64 位通用寄存器,将四字整数转化为双精度浮点数。
12.4 单精度浮点数和双精度浮点数互相转换的操作
vunpcklps
有两个源 XMM 寄存器参数和一个目的 XMM 寄存器参数,若第一个源 XMM 寄存器中存储的四个双字分别为 [ s 3 s_3 s3, s 2 s_2 s2, s 1 s_1 s
标签: 2sdq固态继电器