CSAPP 第三章 汇编指令 - 传送指令和算术指令

CSAPP 第三章 汇编指令 - 传送指令和算术指令

传送指令主要有如下: 普通传送 零扩展传送 符号扩展传送 压栈和弹栈 算术指令比较多了,而且也都区分长度,主要从以下几个方面介绍: leaq 指令 一元与二元操作 移位操作 特殊操作 - 128位扩展 数据传送指令 我们不生产数据,我们只是数据的搬运工. -MOV类指令. MOV类指令有四个变体,对

传送指令主要有如下:
  1. 普通传送
  2. 零扩展传送
  3. 符号扩展传送
  4. 压栈和弹栈
算术指令比较多了,而且也都区分长度,主要从以下几个方面介绍:
  1. leaq 指令
  2. 一元与二元操作
  3. 移位操作
  4. 特殊操作 - 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 零扩展系列 源是内存地址或者寄存器 目的只能是寄存器
    1. movzbw, 从b扩展到w
    2. movzbl, 从b扩展到l
    3. movzbq, 从b扩展到q
    4. movzwl, 从w扩展到l
    5. movzwq, 从w扩展到q
  • MOVS 符号扩展系列 源是内存地址或者寄存器 目的只能是寄存器
    1. movsbw, 从b扩展到w
    2. movsbl, 从b扩展到l
    3. movsbq, 从b扩展到q
    4. movswl, 从w扩展到l
    5. movswq, 从w扩展到q
    6. movslq, 从l扩展到q
    7. cltq, 这条指令不需要加操作数. 这条指令内置的源固定是%eax, 目标固定是%rax, 将%eax符号扩展到%rax.
对比一下可以发现零扩展中没有四字节扩到八字节的指令,这是因为传入32位的时候自动会将高位设置为0,就隐含了零扩展.

练习题 3.2 根据操作数确定指令后缀

  1. movl %eax, (%rsp). 源数据是32位寄存器,所以应该是 movl
  2. movw (%rax), %dx. 目标是一个字,应该使用movw
  3. movb $0xFF, &bl. 是一个字节传送,应该使用movb
  4. movb (%rsp, %rdx, 4), %dl. 目标是一个字节,使用movb.
  5. movq (%rdx), %rax. 目标是64位寄存器, 所以使用movq
  6. movw %dx, (%rax) ,从一个字传送到64位寄存器,使用 movw

练习题 3.3 找错误

  1. movb $0xF, (%ebx), 取地址一定是从64位寄存器中取,所以报错
  2. movl %rax, (%rsp), 操作数的长度与指令不符
  3. movw (%rax), 4(%rsp), 不能都是内存地址
  4. movb %al, %sl, 没有叫做sl的寄存器名称
  5. movq %rax, $0x123, 目标是一个常数, 无法操作
  6. movl %eax, %rdx, 寄存器长度不一致
  7. 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中. 一条条分析:
  1. 第一条, 对指针*xp取值, 放入 %r8寄存器中, 可以翻译成 long temp1 = *xp
  2. 第二条, 对指针*yp取值, 放入 %rcx寄存器中, 可以翻译成 long temp2 = *yp
  3. 第三条, 对指针*zp取值, 放入 %rax寄存器中, 可以翻译成 long temp3 = *zp
  4. 第四条, 把%r8寄存器里的值, 放入yp指针指向的地址中, 可以翻译成 *yp = temp1
  5. 第五条, 把%rcx寄存器里的值, 放入zp指针指向的地址中, *zp = temp2
  6. 第六条, 把%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里, 栈是从大地址往小地址的方向增长的, 一个程序所用的栈被存放在内存的某个区域. 操作栈主要涉及到两个指令和一个寄存器:
  1. push,先把%rsp 栈指针寄存器的值减少对应的长度, 然后取其地址, 将该操作数的值写入栈中.
  2. pop, 其后接一个操作数, 是将该操作数的值从栈里弹出, 并写入到之后的操作数中. 然后%rsp寄存器的值会增加对应的长度.
  3. %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中. 来一步步看其中的结果:
  1. 第一步, 计算 5x , 存放在 %rax中
  2. 第二步, 计算5x + 2y, 存放在%rax中
  3. 第三步, 计算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的位会被忽略.所以移位的对应关系是:
  1. salb, 长度是8位,所以只看%cl的后三位, 所以最多移动7位
  2. sall, 长度是16位,所以只看%cl的后四位,所以最多移动15位
  3. sald, 长度是32位,所以只看%cl的后五位,所以最多移动31位
  4. 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
逐个语句来分析:
  1. 这个是用%rsi和%rdi进行或运算, 结果放在%rdi中, 即 long t1 = x|y
  2. 算术右移3 结果放在%rdi中, 即 long t2 = t1 >> 3
  3. 取反, 即 long t3 = ~t2
  4. 将%rdx 移动到%rax中, 这个就是long t4 = z
  5. 这个是从%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位的支持, 这就是乘法和除法.
  1. imulq S, 注意,这和之前的imul指令类不同, 是单操作数. 这暗含了另外一个操作数的就是%rax, 然后把结果的低64位放入%rax中, 高64位放入%rdx中, 这是固定的. 而两操作数的imul则只计算64位的乘法. 为何两操作数的imul不区分高低位, 是因为截取到64位的时候,补码和无符号的运算相同.
  2. mulq S, 两个64位相乘, 得到128位的无符号乘法. 同样也暗含另外一个操作数在%rax中, 结果存放在高位%rdx和地位%rax中.
  3. clto, 这个指令没有操作数, 是将%rax中的64位按照符号扩展到%rdx, 两个寄存器拼成一个128位数值.
  4. idivq S, 这个是有符号数的除法指令, 操作数S是除数, 被除数固定使用%rax作为低64字节, %rdx作为高64字节. 除法进行完之后,商存储在%rax中, 而余数存储在%rdx中.
  5. 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
数据传送和算术指令都看完了, 有了这些指令, 可以编写出数据操作的指令, 相当于刚学完一门编程语言的数据类型部分. 下边就是控制语句部分, 即分支和循环.
LICENSED UNDER CC BY-NC-SA 4.0
Comment