目录
前言:
1.程序员可见状态
2.Y86-64指令
3.指令编码
movq指令
整数操作指令
跳转指令
条件传送指令
call和ret指令
push和pop指令
halt和nop指令
4.Y86-64异常
5.Y86-64程序
本章内容是笔者学习csapp的一些读书笔记,其中大部分内容来自原书,并加入了一些自己的注释
指令系统是计算机软件和硬件交互的接口,由于x86指令集相对复杂,书中定义了一个简单的指令集 “Y86-64” ,能够满足一个处理器对于指令集的基本需求
在这里,“程序员” 既可以是用汇编代码写程序的人,也可以是产生机器级代码的编译器
程序员的可见状态是指:Y86-64中的每条指令都会读取或者修改处理器状态的某些部分,如程序寄存器、条件码、程序计数器(PC)、内存以及程序状态等等
✨ 对于程序寄存器,相比于x86,%rsp也是被用作栈指针,而Y86-64指令集体系结构中少了程序寄存器%r15,这么做的目的降低编码的复杂度,这一点会在后面提及
✨ 条件码有3个,分别是ZF、SF、OF (有关条件码的内容可以参考)条件码、条件控制和条件传送_七月不远.的博客-CSDN博客https://blog.csdn.net/weixin_58165485/article/details/123466697?spm=1001.2014.3001.5502✨ 程序计数器(PC)存放当前正在执行指令的地址 (注意:是地址而并非内容)
✨ 程序状态码表明这条指令是否正常运行
相比于x86-64指令集,Y86-64指令集做了相应的简化:
✨movq指令
x86-64中的movq指令在这里被分为四种,分别是:
Source operand | Destination operand | |
irmovq | Immediate | Register |
rrmovq | Register | Register |
rmmovq | Register | Memory |
mrmovq | Memory | Register |
指令的第一个字母表示源操作数的类型,第二个字母表示目的操作数的类型,源操作数可以是立即数(i) ,寄存器(r),内存(m),目的操作数可以是寄存器(r)和内存(m)
值得注意的是,我们不能:
✨ 整数操作指令
Y86-64定义了4个整数操作指令,分别是加(addq),减(subq),与(andq),异或(xorq)
✨ 跳转指令
跳转指令形如 jXX,Y86-64定义了7个跳转指令,分别是:
指令 | 跳转条件 | 描述 |
jmp | 1 | 直接跳转 |
jle | (SF ^ OF) | ZF | 小于或等于 (有符号 <=) |
jl | SF ^ OF | 小于 (有符号 <) |
je | ZF | 相等 / 零 |
jne | ~ZF | 不相等 / 非零 |
jge | ~(SF ^ OF) | 大于或等于(有符号 >=) |
jg | ~(SF ^ OF) & ~ZF | 大于(有符号 >) |
根据分支指令的类型和条件代码的设置来选择分支
✨ 条件传送指令
有6个条件传送指令,形如comvXX:cmovle,cmovl,cmove,cmovne,cmovge,cmovg
指令格式和rrmovq一样,但只有满足条件码组合时才发生传送
✨ call指令和ret指令
✨ push指令和pop指令
分别实现入栈和出栈,依靠%rsp来实现
✨ halt指令
halt指令会停止指令的执行,导致处理器停止,并将状态码设置为HLT
对于每条指令,Y86-64规定需要1~10个字节不等的编码长度,如ret指令只需要1个字节就能完成编码,而irmovq需要10个字节来完成编码
每条指令的第一个字节表明了指令的类型,它相当于我们的身份证,前六位指明了你所在的地址,而这个字节分为两个部分,每个部分占4个比特位,高4位表示指令代码 (icode),第4位表示指令功能 (ifun),下面会有详细介绍
当指令中有寄存器类型的操作数时,会在后面附加一个字节称为寄存器指示符字节,用来显示地给出将要操作地寄存器ID
Y86-64对于15个程序寄存器都给了一个ID,称为寄存器标识符(register ID)
数字 | 寄存器名称 | 数字 | 寄存器名称 | |
0 | %rax | 8 | %r8 | |
1 | %rcx | 9 | %r9 | |
2 | %rdx | A | %r10 | |
3 | %rbx | B | %r11 | |
4 | %rsp | C | %r12 | |
5 | %rbp | D | %r13 | |
6 | %rsi | E | %r14 | |
7 | %rdi | F | 无寄存器 |
相比于x86-64省略掉寄存器%r15,是为了满足编码地简便性,我们可以用4个比特位来描述所有寄存器类型,从0000~1111刚好16个数,而不需要再多用一个比特位来保存17种情况
下面介绍每种指令的编码形式:
以rrmovq为例,rrmovq指令是将一个寄存器的值传送到另一个寄存器中,其编码有两个字节长度
第一个字节是 2 0 ,2表示指令代码,0表示指令功能,当处理器拿到的指令形式为2 0 rA rB时,首先读到 2 0,处理器知道了这是两个寄存器之间传送指令,寄存器指示符字节给出了 rA,rB,指明了即将用到地两个寄存器ID,因此处理器就会根据这条编码,将 rA 的值传送到 rB 中
对于irmovq,它表示将一个立即数传送到寄存器中,其编码长度有10个字节
3 0代表的是irmovq,处理器读到3 0便知道要将一个立即数传到寄存器,但是在这个过程中只有1个寄存器作为目的操作数被用到,因此在寄存器指示符字节源操作数中是F,表明不需要寄存器,而在后面的8字节中保存的就是这个立即数V,处理器将V传送到寄存器 rB 中
rmmovq和mrmovq中的8字节常数字是一个地址偏移量,表明将内存引用的地址是rB内的地址+偏移量D
之前已经说过,整数操作指令只能对两个寄存器中的数据进行操作,因此,只需要2个字节就能完成编码
fn = 0 1 2 3分别对应四种规定的整数运算
因此,类似 6 1 2 3这种编码意思就是将寄存器 %rdx 中的值减去寄存器 %rbx 中的值,并把返回结果保存到 %rdx中
和整数操作指令类似,跳转指令也有功能之分,ifun部分分为0~7:
后面的Dest指明了要跳转的地址,如果满足条件,则下一条指令跳转到这个绝对地址执行
注意:分支指令和调用指令的目的一定是一个绝对地址,而不像IA32中那样使用PC相对寻址
和跳转指令类似,条件传送指令的fn分为0~6:
当fn = 0时,代表无条件传送,也就是我们之前提到的rrmovq
其余的fn都是需要条件码满足条件时,才更新目的寄存器的值
call和ret指令分别实现函数调用和返回,其中
push和pop指令分别实现入栈和出栈的操作,注意的是即使编码中只有rA一个目的操作数,但实际上这两条指令也同时调用了%rsp(栈指针)
练习:rmmovq %rsp,0x123456789abcd(%rdx) 进行编码
rmmovq的第一个字节是40,%rsp的编号是4,%rdx编号是2,因此前两个字节编码是4042,而rmmovq的编码规则将地址偏移量放在后8字节的常数字中,故在小端机器上的编码为:
4042cdab896745230100
对于Y86-64来说,程序员可见状态包括了状态码Stat,其可能的值如下:
值 | 名称 | 含义 |
1 | AOK | 正常操作 |
2 | HLT | 遇到器执行halt指令 |
3 | ADR | 遇到非法地址 |
4 | INS | 遇到非法指令 |
对于下面代码,用sum函数计算一个整数数组的和:
long sum(long* start, long count)
{
long sum = 0;
while (count) {
sum += *start;
start++;
count--;
}
return sum;
}
x86-64(由GCC产生)和Y86-64汇编代码如下:
我们可以很清楚地看到一些差别: