深入理解计算机系统读书笔记 第三章

目前 x86_64 架构的虚拟地址空间的高 16 位必须为 0,对应的空间大小为 64TiB

一、寄存器

  1. 程序计数器 %rip

  2. 16 个整数寄存器,每个 64 位

    名称低32位名称低16位名称低8位名称用途
    %rax%eax%ax%al返回值
    %rbx%ebx%bx%bl被调用者保存
    %rcx%ecx%cx%cl第四个参数
    %rdx%edx%dx%dl第三个参数
    %rsi%esi%si%sil第二个参数
    %rdi%edi%di%dil第一个参数
    %rbp%ebp%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被调用者保存
  3. 条件码寄存器,每个 1 位(用于实现程序控制流)

    CF 进位标志,表示最近的操作使最高位发生了进位或借位,用于检查无符号操作的溢出。
    ZF 零标志,表示最近的操作结果为 0。
    SF 符号标志,表示最近的操作结果为负数。
    OF 溢出标志,表示最近的操作产生了补码正溢出或负溢出。

  4. 向量寄存器(存放多个整数或浮点数)

二、ATT 格式对比 Intel 格式

  1. ATT(gcc, objdump) 是先写源,后写目的;Intel 是先写目的后写源

  2. ATT 指令有指示类型和大小的后缀;Intel 没有

    后缀名bwlqs
    含义字节双字四字/双精度浮点数单精度浮点数
  3. ATT 寄存器前面有 % 符号;Intel 没有

  4. 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+ris+Imm
    这四个参数分别是立即数(不带 $ 符号)、基址寄存器、变址寄存器、比例因子(只能是 1、2、4、8),其中任意一项均可忽略

    例:0x9C(%rax,%rcx,2)

  5. Intel 访问内存格式:QWORD PTR [rbx]

  6. ATT 的立即数格式:$ 符号后写 C 格式整数

三、MOV 系列指令

  1. 源和目的不能都是内存地址。
  2. 使用的寄存器大小和指令后缀必须匹配,通常只更新低位,高位不变,但 movl 会把高 32 位置零,原因是 x86-64 的惯例是一切给寄存器赋 32 位值的指令都必须把高 32 位置零。
  3. movq 的源如果是立即数,其只支持 32 位补码范围内的立即数,若要使用 64 位补码范围内的立即数,则要使用 movabsq 指令
  4. movz 指令后面跟两个后缀,其中第一个后缀要小于第二个后缀,该指令表示传送时把高位置零(零扩展)
  5. movs 指令后面跟两个后缀,其中第一个后缀要小于第二个后缀,该指令表示传送时把高位置为符号位(符号扩展)
  6. cltq 相当于 movs %eax, %rax
  7. clto%rax 进行符号扩展(扩展到八字),高 64 位放在 %rdx 处,低 64 位放在 %rax

四、栈

  1. pushq %rbx 相当于先执行 movq %rbx,-8(%rsp),再执行 subq $8,%rsp
  2. 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。
有常量操作数和目的操作数:左移 salshl),算术右移 sar,逻辑右移 shr
以上指令的后缀和寄存器大小要对应,且均会影响条件码。

特殊:leaq 内存地址,目的寄存器,表示将该地址值赋给目的寄存器,该指令不会影响条件码。
例:leaq 7(%rdx,%rdx,4),rax 假设 %rdx 中的值为 x x x,则 %rax 会被赋值 5 x + 7 5x+7 5x+7

xor 会将溢出和进位标志置 0。
incdel 会设置溢出和零标志,但不会改变进位标志。

移位操作的位移量操作数也可以是 8 位寄存器 %cl,对于 ω \omega ω 位数据的移位操作,具体的位移量取决于 %cl 的低 l o g 2 w log_2w log2w 位,进位标志为最后被移出的位,溢出标志会被置 0。

单操作数乘法指令 imulqmulq 是全乘法,即会生成 128 位的结果,前者为有符号乘法,后者为无符号乘法,运算结果的高 64 位放在 %rdx 处,低 64 位放在 %rax 处。

六、流程控制系列指令

6.1 CMP 和 TEST 系列指令

cmp 基于后减前的结果将相应的条件码置位。
test 基于后与前的结果将相应的条件码置位。
注意指令的后缀和寄存器大小要对应。

6.2 SET 和 JMP 系列指令

set 指令只有一个操作数,且必须是单字节寄存器或内存位置,该类型指令对条件码进行指定的运算并向操作数返回 0 或 1

名称运算对应的 CMP 结果
sete setzZF等于
setne setnz~ZF不等于
setsSF负数
setns~SF非负数
setg setnle~(SF^OF) & ~ZF大于(有符号)
setge setnl~(SF^OF)大于等于(有符号)
setl setngeSF^OF小于(有符号)
setle setng(SF^OF) | ZF小于等于(有符号)
seta setnbe~CF & ~ZF超过(无符号)
setae setnb~CF超过或相等(无符号)
setb setnaeCF低于(无符号)
setbe setnaCF | ZF低于或相等(无符号)

jmp 后可以跟标签名,或加 * 号后面跟操作数。
j + 比较后缀(参考 SET 指令)可以实现条件跳转,这类命令只能跳转到标签。
注意,不应该通过跳转到达 retretq(同义指令),因为这样 CPU 无法推测 ret 的含义,通常在前面加 reprepz(同义指令)。

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(TranTOK)

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 局部变量

局部变量必须存储在内存的情况包括:

  1. 寄存器数量不足
  2. 对变量进行了取地址操作
  3. 局部变量为数组或结构体类型

将局部变量保存到内存中时,先减少 %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 s1, s 0 s_0 s0],第二个源 XMM 寄存器中存储的四个双字分别为 [ d 3 d_3 d3, d 2 d_2 d2, d 1 d_1 d1, d 0 d_0 d0],则目的 XMM 寄存器中存储的四个双字会变为 [ s 1 s_1 s1, d 1 d_1 d1, s 0 s_0 s0, d 0 d_0 d0]。
vcvttps2pd 有一个源 XMM 寄存器参数和一个目的 XMM 寄存器参数,将源 XMM 寄存器中低 64 位的两个单精度浮点数转化为两个双精度浮点数,将结果放在目的 XMM 寄存器中。

; Conversion from single to double precision
vunpcklps %xmm0, %xmm0, %xmm0
vcvtps2pd %xmm0, %xmm0

12.5 函数的浮点参数和返回值

%xmm0 ~ %xmm7 可用于传参,xmm0 用于返回值,这些寄存器均为调用者保存,无需复原。

12.6 浮点运算操作

每个操作有一到两个源操作数和一个目的操作数,其中第一个操作数可以是 XMM 寄存器或内存地址,第二个操作数和目的操作数必须是 XMM 寄存器。
由于浮点操作无法使用立即数,故 C 语言的浮点常量必须将其二进制位写入内存

vaddssvsubssvmulssvdivss 有两个源操作数,分别表示浮点加减乘除。
vmaxssvminss 有两个源操作数,分别表示取较大者和取较小者。
sqrtss 有一个源操作数,表示取平方根。
vxorps 有两个源操作数,表示位级异或。
vandps 有两个源操作数,表示位级与。

12.7 浮点比较操作

每个操作有两个源操作数,其中第一个操作数可以是 XMM 寄存器或内存地址,第二个操作数和目的操作数必须是 XMM 寄存器。

ucomiss 比较两个单精度浮点数,依据后减前更新标志位。
ucomisd 比较两个双精度浮点数,依据后减前更新标志位。

浮点比较操作涉及的标志位有零标志位 ZF、进位标志位 CF 和奇偶标志位 PF。其中 PF 在整数运算中表示运算结果包含偶数个 1。对于浮点比较,若其中一个数为 NaN,则 PF 会置位,C 语言规定 NaN 参与比较(无序)时总是返回 0,其底层依靠 PF 标志位实现这种判定。条件跳转指令 jp 即根据 PF 位进行判断。

ucomisd %xmm0,%xmm1CFZFPF
unordered111
%xmm1 < %xmm0100
%xmm1 = %xmm0010
%xmm1 > %xmm0000
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值