传送指令主要有如下:
- 普通传送
- 零扩展传送
- 符号扩展传送
- 压栈和弹栈
算术指令比较多了,而且也都区分长度,主要从以下几个方面介绍:
- leaq 指令
- 一元与二元操作
- 移位操作
- 特殊操作 - 128位扩展
数据传送指令
我们不生产数据,我们只是数据的搬运工. -MOV类指令. MOV类指令有四个变体,对应不同的数据长度.
MOV类指令
指令 |
效果 |
说明 |
MOV S D |
把S移动到D |
传送数据 |
movb |
|
传送1字节 |
movw |
|
传送2字节(一个字) |
movl |
|
传送4字节 |
movq |
|
传送8字节 |
movbsq |
|
传送绝对4字 |
MOV的两个操作数都不能都是内存地址. 也就是说,一条MOV指令要么从内存移入寄存器,要么寄存器移入内存,要么寄存器移入寄存器,而不能直接从内存移入内存.如果操作数是寄存器,必须是寄存器里那些带名字的寄存器,而不能是仅有编号的寄存器.
如果MOV传送4字节到寄存器,按照之前的规则,就会将寄存器的高四位清零. 常规mov命令只能处理32位的源操作数,将其符号扩展得到64位. 而movbsq可以直接将64位数作为源操作数, 但目的只能是寄存器.
MOV系还有两个变种,就是0扩展和符号扩展传送,用于在不同大小的寄存器之间传送数据(当然,扩展意味着从小寄存器传给大寄存器)
零扩展传送和符号扩展传送
- MOVZ 零扩展系列 源是内存地址或者寄存器 目的只能是寄存器
movzbw
, 从b扩展到w
movzbl
, 从b扩展到l
movzbq
, 从b扩展到q
movzwl
, 从w扩展到l
movzwq
, 从w扩展到q
- MOVS 符号扩展系列 源是内存地址或者寄存器 目的只能是寄存器
movsbw
, 从b扩展到w
movsbl
, 从b扩展到l
movsbq
, 从b扩展到q
movswl
, 从w扩展到l
movswq
, 从w扩展到q
movslq
, 从l扩展到q
cltq
, 这条指令不需要加操作数. 这条指令内置的源固定是%eax, 目标固定是%rax, 将%eax符号扩展到%rax.
对比一下可以发现零扩展中没有四字节扩到八字节的指令,这是因为传入32位的时候自动会将高位设置为0,就隐含了零扩展.
练习题 3.2 根据操作数确定指令后缀
movl
%eax, (%rsp). 源数据是32位寄存器,所以应该是 movl
movw
(%rax), %dx. 目标是一个字,应该使用movw
movb
$0xFF, &bl. 是一个字节传送,应该使用movb
movb
(%rsp, %rdx, 4), %dl. 目标是一个字节,使用movb.
movq
(%rdx), %rax. 目标是64位寄存器, 所以使用movq
movw
%dx, (%rax) ,从一个字传送到64位寄存器,使用 movw
练习题 3.3 找错误
movb $0xF, (%ebx)
, 取地址一定是从64位寄存器中取,所以报错
movl %rax, (%rsp)
, 操作数的长度与指令不符
movw (%rax), 4(%rsp)
, 不能都是内存地址
movb %al, %sl
, 没有叫做sl的寄存器名称
movq %rax, $0x123
, 目标是一个常数, 无法操作
movl %eax, %rdx
, 寄存器长度不一致
movb %si, 8(%rbp)
,si长度是w, 与指令的长度不符
再回头看最开始的汇编例子, 可以发现, 对于指针的操作, 其实就是对于存在某个地方的整数使用取地址操作.
练习题 3.4 汇编编写代码
X86-64体系下, 有两个类型的指针变量:
src_t *sp; //地址存储在%rdi中
dest_t *dp; //地址存储在%rsi中
要根据不同的类型编写对应的汇编语句来实现下边这条语句, 中转寄存器可以使用%rax及其更短的系列:
*dp = (dest_t) *sp;
src_t的类型 |
dest_t的类型 |
指令 |
long |
long |
long代表8字节,目标的字节数也是8,无需转换长度, 则这指令是:
movq (%rdi),%rax
movq %rax, (%rsi) |
char |
int |
把char转换成int, 很显然需要进行符号扩展. 想要把char装入到%rax中, 很显然需要先扩展符号到32位, 之后写到目标寄存器中
movsbl (%rdi), %eax
movl %eax, (%rsi) |
char |
unsigned |
把char写入到unsigned中去,其实就是先把char要转换成unsigned int, 这个过程就要用到符号扩展
movsbl (%rdi), %eax
movl %eax, (%rsi) |
unsigned char |
long |
这个需要把字节扩展成四字,由于是unsigned, 那就用零扩展. 注意零扩展没有扩展到64位的指令,只有32位,就相当于64位的零扩展了.
movzbl (%rdi), %rax
movq %rax, (%rsi) |
int |
char |
这里要弄明白的是, 原始字节还是要读出来的, 但是只传送低位字节, 不能只从原始位置读一个字,读的反而是高位
movl (%rdi), %eax
movb %al, (%rsi) |
unsigned |
unsigned char |
这个与上边的本质是一样的, 在移动的时候不管unsigned不unsigned
movl (%rdi), %al
movb %al, (%rsi) |
char |
short |
这个很显然要进行符号扩展
movsbw (%rdi), %ax
movw %ax , (%rsi) |
由此可见, 汇编指令不管目标操作数如何, 在读的时候必须先完整的读入原来的内容,再进行操作, 根据这个原则来选取对应的指令. 对于要截断的数字也是如此, 不能因为只要低位就只读入1个字节.
练习题 3.5 将汇编翻译成C语言代码
一个函数原型void decode1(long *xp, long *yp, long *zp);
的汇编语句如下:
decode1:
movq (%rdi), %r8
movq (%rsi), %rcx
movq (%rdx), %rax
movq %r8, (%rsi)
movq %rcx, (%rdx)
movq %rax, (%rdi)
ret
这个函数的三个参数按顺序一开始保存在 %rdi, %rsi, %rdx中.
一条条分析:
- 第一条, 对指针*xp取值, 放入 %r8寄存器中, 可以翻译成 long temp1 = *xp
- 第二条, 对指针*yp取值, 放入 %rcx寄存器中, 可以翻译成 long temp2 = *yp
- 第三条, 对指针*zp取值, 放入 %rax寄存器中, 可以翻译成 long temp3 = *zp
- 第四条, 把%r8寄存器里的值, 放入yp指针指向的地址中, 可以翻译成 *yp = temp1
- 第五条, 把%rcx寄存器里的值, 放入zp指针指向的地址中, *zp = temp2
- 第六条, 把%rax寄存器里的值, 放入xp指针指向的地址中, *xp = temp3
这个函数的作用就是传入三个long指针 xp,yp,zp, 函数结束之后, *yp的值是*xp的初始值, *zp的值是*yp的初始值, *xp的值是*zp的初始值. 将前边直接翻译的6行优化一下, 写成C语言就是:
void decode1(long *xp, long *yp, long *zp){
long temp = *zp;
*zp = *yp;
*yp = *xp;
*xp = temp;
}
压栈和弹栈
首先要明白栈的底层的表示. 在X86里, 栈是从大地址往小地址的方向增长的, 一个程序所用的栈被存放在内存的某个区域.
操作栈主要涉及到两个指令和一个寄存器:
push
,先把%rsp 栈指针寄存器的值减少对应的长度, 然后取其地址, 将该操作数的值写入栈中.
pop
, 其后接一个操作数, 是将该操作数的值从栈里弹出, 并写入到之后的操作数中. 然后%rsp寄存器的值会增加对应的长度.
%rsp
, 不管何时, %rsp中保存着当前运行的程序的栈顶的地址
注意push指令之后%rsp的值会先变化, 再写入值. 而取数的时候是先取出来, 然后%rsp的值才会变动.
从%rbp寄存器中压一个四字入栈的指令pushq
可以分解为:
subq $8, %rsp
push %rbp, (%rsp)
这两条指令效果一样, 然而在可执行文件中, pushq仅仅一个字节, 而上边这两条需要8个字节长度.
popq
可以分解为:
movq (%rsp), %rax
addq %$8, %rsp
可以注意到, 始终操作完整的%rsp, 这是因为在之前已经知道, 操作地址一定要是完整的寄存器名称.
而且还应该知道, 栈的内存也可以通过正常的方法访问,比如已经知道 %rsp内的地址是0x100, 如果想读第二个四字, 就可以使用 8(%rsp)来直接读取栈里的第二个四字.
算术指令
算术指令类比较多,除了leaq
之外,所有的指令类都有b,w,l,q结尾的四条具体指令.
算术指令类
指令 |
效果 |
描述 |
leaq S D |
把S放入D中 |
这并不是加载有效地址,实际上是把S放入到D中, S可以是一个计算后的结果, 经常用来描述计算, 所以leaq经常使用寻址计算的规律完成一些算术运算. |
INC D |
让D增加1 |
D增加1之后的结果,依然存储在D中 |
DEC D |
D-- |
结果也存放在D中 |
NEG D |
取负 |
结果依然存放在D中 |
NOT D |
取反(补) |
结果还是存放在D中 |
ADD S, D |
把S 和 D 相加 |
结果存放在D中 |
SUB S, D |
D-S |
结果存放在D中 |
IMUL S, D |
D * S |
结果存放在D中 |
XOR S, D |
D和S做异或运算 |
结果存放在D中 |
OR S, D |
D和S做或运算 |
结果存放在D中 |
AND S, D |
D和S做与运算 |
结果存放在D中 |
SAL k, D |
D左移k位 |
结果存放在D中 |
SHL k, D |
D左移k位,和 SAL相同 |
结果存放在D中 |
SAR k, D |
算术右移 |
即补符号位, 结果存放在D中 |
SHR k, D |
逻辑右移 |
即补0, 结果存放在D中 |
练习 3.6 leaq的灵活使用
%rax存放的值是x, %rcx中的值是y, 填写下列指令执行后 %rdx中的值:
表达式 |
%rdx |
leaq 6(%rax) ,%rdx |
x+6 |
leaq (%rax, %rcx), %rdx |
x+y |
leaq (%rax, %rcx, 4), %rdx |
x+4y |
leaq 7(%rax, %rax, 8) |
9x+7 |
leaq 0xA(, %rcx, 4), %rdx |
4y+10 |
leaq 9(%rax, %rcx, 2) %rdx |
x + 2y + 9 |
练习 3.7 看汇编写C代码
long scale2(long x, long y, long z){
long t = __________
return t;
}
对应的汇编代码是:
scale2:
leaq (%rdi, %rdi, 4), %rax
leaq (%rax, %rsi, 2), %rax
leaq (%rax, %rdx, 8), %rax
三个参数依次存放在%rdi, %rsi和%rdx中.
来一步步看其中的结果:
- 第一步, 计算 5x , 存放在 %rax中
- 第二步, 计算5x + 2y, 存放在%rax中
- 第三步, 计算5x + 2y + 8z, 存放在%rax中
最后的返回值就是5x + 2y + 8z
算术指令还有一个突出的特点是有一部分指令只有一个操作数,这叫做一元操作指令.
一元操作指令的操作数既可以是寄存器,也可以是内存地址. 比如 incq(%rsp)会使栈顶的8字节元素+1.
而另外一部分算术指令的二元操作也比较特别, 第二个操作数既是源(之一)也是目标, 所以第一个操作数可以是立即数,寄存器或者内存地址, 第二个操作数只能是寄存器或者内存地址, 不能是立即数.
练习 3.8 一元和二元操作
已知下列内存位置,寄存器和其中的值:
地址 |
值 |
0x100 |
0xFF |
0x108 |
0xAB |
0x110 |
0x13 |
0x118 |
0x11 |
%rax |
0x100 |
%rcx |
0x1 |
%rdx |
0x3 |
写出下边指令的会被更新的寄存器或者内存位置,以及更新的值:
指令 |
目的 |
值 |
addq %rcx, (%rax) |
取%rax地址中的值加上%rcx的值,再写入到 %rax的地址中去,所以目的是 0x100的内存位置 |
值是 0xFF+ 0x1 = 0x100 |
subq %rdx, 8(%rax) |
表示从 %rax+8的地址中减去%rdx, 再写入到%rax+8的位置中去,所以目标位置是 0x108的地址 |
值是 0xAB - 3 = 0xA8 |
imulq $16 , (%rax, %rdx ,8) |
寻址得到0x100+0x18 = 0x118,所以目标位置就是0x118的内存地址 |
值是0x11*16 = 0x110 |
incq 16(%rax) |
地址是 0x100+0x10 = 0x110 |
值是0x14 |
decq %rcx |
目标就是%rcx寄存器 |
结果是0x0 |
subq %rdx, %rax |
目标就是%rax寄存器 |
值是 0x100 - 0x3 = 0xFD |
还有一类指令是移位操作.先要给出一个移位量, 再给出要移位的操作数. 注意移位量可以是一个立即数, 也可以是单字节的%cl寄存器, 其他寄存器不能用于移位. 目的操作数可以是寄存器或者内存地址.
然后一个特殊之处在于, 对于w位的数据进行移位, 并不是直接使用%cl的值, 而是使用%cl的低m位的值, 这个m就是2的m次幂等于当前操作位数的大小, 大于m的位会被忽略.所以移位的对应关系是:
salb
, 长度是8位,所以只看%cl的后三位, 所以最多移动7位
sall
, 长度是16位,所以只看%cl的后四位,所以最多移动15位
sald
, 长度是32位,所以只看%cl的后五位,所以最多移动31位
salq
, 长度是32位,所以只看%cl的后六位,所以最多移动63位
练习3.9 根据C语言写出汇编代码
long shift_left4_rightn(long x, long n){
x <<= 4;
x >>= n;
return x;
}
这个函数对应的汇编代码是:
// x in %rdi, n in %rsi
shift_left4_rightn:
movq %rdi, %rax;
________________
movq %esi, %ecx;
________________
这里的第一条位移是一个已知的立即数,而且不超过long最长可以移动的63位, 所以可以直接将其移位即可. 则第一条指令就是 SAL $4, %rax
第二条指令要注意,移动的是参数n的低7位, 所以要通过寄存器%cl来移动, 指令就是 SAR %cl, %rax
练习 3.10 通过汇编代码写出函数代码
long arith2(long x, long y, long z)
arith2:
orq %rsi, %rdi
sarq $3, %rdi
notq %rdi
movq %rdx, %rax
subq %rdi, %rax
ret
逐个语句来分析:
- 这个是用%rsi和%rdi进行或运算, 结果放在%rdi中, 即 long t1 = x|y
- 算术右移3 结果放在%rdi中, 即 long t2 = t1 >> 3
- 取反, 即 long t3 = ~t2
- 将%rdx 移动到%rax中, 这个就是long t4 = z
- 这个是从%rax中减去%rdi, 也就是此时的t3, 结合上一条看, 就是 long t4 = z - t3
练习 3.11 汇编代码分析
xorq %rdx, %rdx
这个代码就是把%rdx中的值与自己进行异或运算, 结果再放到%rdx中, 由于一个值和自己的异或等于0, 因此这是一个生成全0的位的操作.
这个操作如果直接表达的话, 就是 movq $0, %rdx.
这个没有亲自试验,看了答案, xorq的字节数比较少,只有3个字节. 而movq要七字节. 另外还一个方法就是只异或32位,也会同时把高4字节置0.
还有一类特殊算术操作, 实际上提供了128位的支持, 这就是乘法和除法.
imulq S
, 注意,这和之前的imul指令类不同, 是单操作数. 这暗含了另外一个操作数的就是%rax, 然后把结果的低64位放入%rax中, 高64位放入%rdx中, 这是固定的. 而两操作数的imul则只计算64位的乘法. 为何两操作数的imul不区分高低位, 是因为截取到64位的时候,补码和无符号的运算相同.
mulq S
, 两个64位相乘, 得到128位的无符号乘法. 同样也暗含另外一个操作数在%rax中, 结果存放在高位%rdx和地位%rax中.
clto
, 这个指令没有操作数, 是将%rax中的64位按照符号扩展到%rdx, 两个寄存器拼成一个128位数值.
idivq S
, 这个是有符号数的除法指令, 操作数S是除数, 被除数固定使用%rax作为低64字节, %rdx作为高64字节. 除法进行完之后,商存储在%rax中, 而余数存储在%rdx中.
divq S
, 无符号的除法指令, 隐含条件与有符号一样.
大多数64位应用里, 其实比较多的还是64位的被除数, 64位被除数需要放在%rax中, 在除法进行之前, 需要将%rax的符号位扩展到%rdx中, 这可以用一条无操作数的指令cqto
来完成.
可以看出, 实际上被除数都要用到%rax 和 %rdx两个寄存器.执行完除法之后, 可以直接从%rax中得到商, %rdx中得到余数.
64无符号除法的%rdx一般会直接预先设置为0.
练习 3.12 无符号64位除法的商和余数
void uremdiv(unsigned long x, unsigned long y, unsigned long *qp, unsigned long *rp) {
unsigned long q = x / y;
unsigned long r = x % y;
*qp = q;
*rp = r;
}
// x in %rdi, y in %rsi, qp in %rdx, rp in %rcx
这是无符号的除法, 需要将%rdx的值在除法指令前设置为0, 汇编代码如下:
uremdiv:
movq %rdx, %r8 把qp的地址放进%r8存储器
movq %rdi, %rax 把x的值放进%rax, 准备被除数
movl $0, %edx 设置%rdx为0, 只需要设置低32位, 高32位也变成0
divq %rsi x/y
movq %rax, (%r8) 把商传递到qp指针指向的内存位置
movq %rdx, (%rcx) 把余数传递给rp指针指向的内存位置
ret
数据传送和算术指令都看完了, 有了这些指令, 可以编写出数据操作的指令, 相当于刚学完一门编程语言的数据类型部分.
下边就是控制语句部分, 即分支和循环.