注意:系统导论里面的汇编代码格式是ATT格式,而计组是Intel格式,注意二者区分
基础
体系结构(又称ISA:指令集体系结构) 要进行处理器设计,需要理解或者写汇编/机器代码,ISA定义了处理器状态、指令的格式,以及每条指令对状态的影响
程序员可见的状态Programmer-Visible State
▪ PC程序计数器PC: Program counter
▪ 下条指令地址Address of next instruction
▪ 称为RIP Called “RIP” (x86-64)
▪ 寄存器堆Register file
▪ 程序频繁使用的数据Heavily used program data
▪ 条件码Condition codes
▪ 存储有关最近的算术和逻辑运算的状态信息
Store status information about most recent arithmetic or logical operation
▪ 用于条件分支Used for conditional branching
▪内存Memory
▪ 字节可寻址的数组Byte addressable array
▪ 代码和用户数据Code and user data
▪ 栈用于支持过程、函数Stack to support procedures
addq %rbx, %rax
相当于 rax += rbx 这些是64位寄存器,因此我们认为这是64位加法
访问信息
x86-64 寄存器
可以通过不同的指令访问不同字节,如16位访问最低2个字节,32位访问最低的4个字节,64位访问全部字节
操作:
- 传送数据
- 从内存装载数据到寄存器
- 存储寄存器数据到内存
- 算术运算
- 对寄存器或内存数据执行算术运算
- 传递控制
- 无条件跳转到/从过程
- 条件分支
- 间接分支
传送数据
movq Source, Dest
将Source传送到Dest中,为64位数据
操作数类型
- 立即数:常量整数 ▪ 例如:Example: $0x400, $-533 ▪ 类似C常量,$前缀 ▪ 编码1,2或4字节
- 寄存器:16个整数寄存器之一 ▪ 例如:Example: %rax, %r13 ▪ 但%rsp保留有特殊用处 ▪ 其它对特定指令有特殊用途
- 内存:因为是movq,所以是8个连续字节,需要给出内存地址
- 最简单的例子:Simplest example: (%rax)
- 各种其它“寻址方式”
movq操作数组合:
注意,单条指令中不能进行内存到内存的传送
内存寻址方式

在函数调用中,第一个参数存放在rdi寄存器,第二个在rsi寄存器
- 简单的swap函数,由于不能直接进行内存到内存的传送,因此要通过寄存器执行
数据先传送到rax和rdx寄存器中:
随后再传送回对应的内存位置:

复杂地址的寻址
D: 立即数偏移
Ri: 索引寄存器,表示第几个元素
S: 比例因子,1,2,4,8作为字节数

数据传送指令类型
movl 以寄存器作为目的时,会把该寄存器的高位4字节设置为0,原因是x86惯例,任何为寄存器生成32位值得指令都会把寄存器高位置为0

零扩展和符号扩展
将较小的源值复制到较大的目的时使用
movz将目的中剩余的字节填充为0,movs将通过符号填充
注意一个cltq,这个指令不需要source和dest,直接填充,且为符号填充
这个例子很好显示出了mov, movz, movs区别:

算数与逻辑运算
leaq 指令
leaq Scr, Dst
Src是寻址方式表达式 设置Dst成为表达式指示的地址
- 用法
- 计算地址不访问内存,
p=&x[i] - 计算x + k *i
- 计算地址不访问内存,

leaq只是一个根据公式进行计算的指令,内部内容不一定需要是地址
[!queation]为什么倾向使用lea? CPU设计师倾向使用:计算一个对象的指针 例如:数组元素
编译器设计人员喜欢用它实现普通计算
- 可以在一条指令中做复杂的计算
- x86仅有的几个三操作数指令之一
- 并不影响条件码(我们后面再讨论)
算数运算
移位:左移sal, shl,算数右移sar, 逻辑右移shr
注意移位要么存在cl,要么用立即数

特殊的算数运算
注意imulq,有两种算术运算,如果单操作数,对R[rax]内容乘S(补码乘法),再把内容分配到两个寄存器中,其中rdx存放高64位,rax存放低64位
cqto隐含操作数,效果是将rax内容转化为8个字,进行符号扩展后分到rdx和rax寄存器
对于除法,商存在rax,而余数存在rdx
控制
C语言的控制流:
汇编语言:

处理器状态

条件码
单个比特位寄存器
设置时与有无符号无关,只看二进制位的情况即可
- CF 进位标志Carry Flag (对无符号数for unsigned)
- SF 符号标志Sign Flag (对有符号数for signed)
- ZF 零标志Zero Flag
- OF 溢出标志Overflow Flag (对有符号数for signed)
条件码是隐式设置的,通过算数或者逻辑运算进行设置
addq %edx, %eax
- set CF 如果从最高有效位进位(无符号溢出)
- set ZF 如果结果为零
- set SF 如果结果小于零(有符号数)
- set OF 如果补码(有符号数)溢出
leaq指令不设置条件码

设置条件码的指令
cmp

test
test两个相同的寄存器可以用来检查寄存器值为0,负数还是正数
读取条件码 SetX指令
- 根据条件码组合设置目的操作数低字节成0或1
- 不要改变剩余的7个字节

setl
这里对最后一种情况解释下:当SF为1的时候,说明最高位为1,但是同时出现了overflow溢出,说明要么上溢,要么下溢,但是下溢出不应该出现最高位为1,所以应该为上溢,又因为这里是减法,说明是减去了一个负数,而且结果还超了,那么a应该为一个正数,那b是负数,说明没有小于
设置了以后通常用movzbl(零扩展从字节到双字)来完成工作

[!note] setl 和movzbl不会改变条件码
使用控制的条件分支
跳转指令jx:
jmp为无条件跳转

示例:
在函数中,返回值存储在%rax,第一个参数%rdi, 第二个参数在%rsi
为了更好提前汇编的分支,这里用goto表达:
long absdiff_j
(long x, long y)
{
long result;
int ntest = x <= y;
if (ntest) goto Else;
result = x-y;
goto Done;
Else:
result = y-x;
Done:
return result;
}
抽象结果:把测试条件反过来,如果没有满足测试,那么进入else,反之直接进入done
通常的代码格式:
使用条件传送指令实现条件分支
val = Test ? Then_Expr : Else_Expr;
goto版本:
result = Then_Expr;
eval = Else_Expr;
// 先让结果为then的结果,再通过判断是否要让result为else
nt = !Test;
if(nt)
result = eval;
return result;
使用条件传送:
/*对原本绝对值作差函数的改写*/
long cmovdiff(long x, long y){
long rval = y - x; // 计算出了两个作差值,看结果是什么就用哪个
long eval = x - y;
long ntest = x >= y;
if(ntest) rval = eval;
return rval;
}
对应的汇编代码: 注意一开始x - y 是放在rax寄存器的,这样可以少用一个,同时后面传送的时候直接覆盖就好
absdiff:
movq %rdi, %rax # x
subq %rsi, %rax # result = x-y 放在rax
movq %rsi, %rdx
subq %rdi, %rdx # eval = y-x 放在rdx
cmpq %rsi, %rdi # x-y
cmovle %rdx, %rax
# if <=, result = eval 如果rdi小于rsi,说明结果是y>x那么就把rdx的值给到rax
ret
优点:有利于CPU流水线运行
缺点:
- 需要大量计算
- 同时算了两个值,而条件控制只需要计算一个值
- 计算存在风险
- 当遇到空指针的时候可能会有空指针引用
- 计算可能有副作用
- 因为同时算了两个值,如果两个值对一个变量进行了改变,那么可能会导致计算出现问题
循环
do-while
这里的jne实际上是看的是shrq的结果,因为移位运算也会修改条件码
while
由于while在第一次执行body-statement之前,会对test-expr求值,可能会在这个阶段就终止循环,因此,需要使用特别的方法:跳转到中间和guarded-to
跳转到中间
跳转到中间就是第一次先跳转到test,经过条件判断后再跳转回到loop
do-while转换
先判断,然后后面的跟do-while一样

For
#define WSIZE 8*sizeof(int)
long pcount_for
(unsigned long x)
{
size_t i;
long result = 0;
for (i = 0; i < WSIZE; i++)
{
unsigned bit =
(x >> i) & 0x1;
result += bit;
}
return result;
}
对于for循环,实际上可以将其优化为while循环:

long pcount_for_while
(unsigned long x)
{
size_t i;
long result = 0;
i = 0;
while (i < WSIZE)
{
unsigned bit =
(x >> i) & 0x1;
result += bit;
i++;
}
return result;
}
switch语句
switch语句有特殊的跳转表结构:
跳转表是一个数组,表项i是一个代码段的地址,实现当开关索引值等于i时程序采用的动作。
跳转表:
- 最开始是L4
- 对于多情况标签,将指向同一个L字段
- 对于default的所有缺失情况,也将指向同一个L字段
- 对于落入其他情况标签,
注意,w不一定需要在外面初始化,因为有的跳转可能不需要w,但是在内部设置w的时候注意可能会把w的计算写在两个L字段中:









编译器设计人员喜欢用它实现普通计算
