3.5 算术和逻辑操作

其实除了第一个leadl, 这些指令都有带操作数的变种, 只是这里省略了, 例如add可以是addb, addw 和 addl.

 

3.5.1 加载有效地址

指令leal(load effective address)实际上是movl的变种, 但是有一个很明显的特征, 他实际并不真的引用存储器. 例如 leal 7(%edx, %edx, 4), %edx, 假设%edx里面的值是x, 那么这里的含义并不是以5x+7 为地址去取值然后存回%eax中, 而是单纯地把5x+7存在%eax中... 同时该指令的目的操作数(最右边的)必须是一个寄存器.

 

3.5.2 一元操作和二元操作

唯一要注意的是对于一元操作而言, 只有一个操作数, 该操作数既是源又是目标, 而对于二元操作数而言, 左边是源, 右边才是目标.

 

3.5.3 移位操作

移位操作有两个操作数, 左边的是移位量, 右边的是目标. 对于移位量而言, 只允许两种形式 : 一种是一个立即数, 默认只检查该立即数的低5位, 也就是说只允许0到31位的移位. 第二种是单字节寄存器%cl, 只允许只用改寄存器作为移位量, 当然也是只看低5位. 对于左移而言, SHL(shift left) 和 SAL(shift arithmetic left)没区别, 右移的话前者做零扩展, 后者做符号扩展.

 

3.5.5 特殊的算术操作

这里给出的是64位的操作, 有如下几点要注意 :

1. 乘法运算指令本身只有一个操作数, 但实际参与运算的却有两个, 这是因为另外一个默认(也必须)放在寄存器%eax当中...

2. 乘法运算结果的高32位将存放在%edx中, 低32位存放在%eax中...

3. 细心的人可能发现这里的imull与之前出现的32位imull乘法将发生名字相同, 然而实际使用过程中是根据提供的参数的数量来区分两者的.

4. 除法运算恰好与乘法运算相反, 被除数是64位, 高32位为%edx, 低32位为%eax, 除数即为操作数, 最终商存于%eax中, 而余数存于%edx中...

5. 设置除法较为常规的方式是使用指令cltd, 它将%eax符号扩展到%edx当中, 当然我们也可以使用移位等操作给出相同的效果. 该指令在Intel格式中叫做cdq, 我个人是这样理解的 : cdq 即为 change double-word to quad, cltd 即为change long(double-word) to divisible ...

 

练习题3.12确实有点味道, 所以放上来了 :

 

这道题有一点首先要明确, 否则无法解释, 那就是这个算法是建立在小端机器的基础上, 同时该算法是模拟了一个64位数(num_t)乘以一个32位数(unsigned)的乘法, num_t既可以是无符号的, 也可以是有符号的, 下面我来解释一下为什么...

1. 假如num_t是无符号的, 那么比如说是 unsigned long long, 一个unsigned long long * unsigned 的第一步就是 unsigned 被提升到unsigned long long, 但其实扩展不扩展都不所谓, 因为反正是零扩展... 此时取y_h * x的低32位(高32是结果中的65到96位要被截断的)加上y_l * x 的高32位之后作为结果的高32为显然是合理的.

2. 其实只需要搞清楚一个概念, 就是说有无符号并不影响乘法指令的运算过程和结果(imul 和 mul 实际上在求值层面上一模一样), 他们 唯一的区别只在对flag的设置上(flag是条件判断里面的概念, 之后会讲到).

3. 根据2可以知道如果num_t 是有符号的long long, 结果也一样...

 

3.6 控制

3.6.1 条件码

CPU中除了有整数寄存器, 还维护着这一组单个位条件码, 他们描述了最近的算术或者逻辑操作的属性, 他们可用来控制机条件分支指令.

另外有几点要注意的 :

1. leal 不会改变条件码, 但是 inc, dec, neg, not, add, sub, imul, xor, or, and, sal, shl, sar, shr 都会设置条件码...

2. 逻辑操作中, 对于xor而言, CF, OF将会被设置为0(很好理解), 其他两个看情况... 对于4种移位操作而言, 进位标志设置为最后一个被移出的位, 溢出设置为0... 

3. inc和dec 会设置溢出和零标志, 但进位标志不会改变, 不知道为什么...

4. 我之前提到的所谓mul 和 imul的区别中, 所谓的flag就是这些条件码...

下面还有一些指令, 他们则只改变条件码不改变数值... 这里提供一些用法 :

1. cmp 可以用来比较两数的大小, 如果s1 和 s2 相同, 零标被设置为1, 如果不同, 则根据其他标志判断, 但是要注意的是, 如果SF是1, 并不一定能说明S1小于S2, 要考虑到S1 是正数, S2是负数的情况, 此时S1 - S2 相当于两个整数相加, 很有可能发送溢出, 结果反而变成负数...

2. test可以用来检测一个数的正负情况, 例如 testl %eax, %eax... 或者以其中一个操作数作为掩码, 来测试另外一个操作数中的某些位.

 


3.6.2 访问条件码

当然条件码的使用有三种方法.

1. 根据条件码的组合将某个寄存器或者存储器的某个位置设置为0或者1... 如图所示, 这里要注意如何根据条件码的组合推倒出这些效果, 首先对于有符号数而言, 我们先从最简单的小于开始看, 小于无外乎两种情况, 一种是不发生溢出, 此时SF为1则为小于, 另外一种是发生溢出, 此时SF必须为0(结果大于等于0), 然后根据这个结果反推出大于等于. 无符号数的比较, 可以先分析低于, 因为无符号数的运算结果永远是正数, 也就是说SF永远是0, 所谓低于, 就是用小的无符号数减去大的, 过程中肯定会发生最高位借位, 此时进位标志CF 会被设置为1, 根据这个再反推其他的....

 

这里还需要注意到 : 一个set指令的操作数必须是单字节的寄存器或者是1个字节的存储器位置, 若果想要得到32位的结果, 必须要把高24位清零.

 

3.6.3 跳转指令及其编码

无条件跳转方式有两种 :

1. 直接跳转 : 给出一个label作为跳转目标...

2. 间接跳转 : *后面跟着操作数指示符, 如jmp *%eax, 使用%eax的值作为地址进行跳转, jmp *(%eax)用%eax的值所指向的内存中的值作为地址进行跳转.

 

条件码的第二种使用方法就是条件跳转, 对于条件跳转而言, 只能是直接跳转.

 

这里还提到跳转过程中的多种寻址方式 : 绝对寻址和相对寻址(也叫作与PC相关的寻址, 因为是根据PC的位置+偏移量来达到寻址的效果), 下面的例子简要的描述了相对寻址的过程...

 

我们重点看反汇编第一条以及第七条, 第一条中, 指令为7e 0d, 你会发现 0x0d + 0x0a(下一条指令的地址也就是) == ox17 也就是需要跳转的目标第八条指令的位置, 也就是说它只通过了下一条指令的位置与跳转指令的相对位置就确定了跳转的目标, 这就是相对寻址. 至于为什么是以下一条指令的相对位置而不是该跳转指令本身位置来计算相对位置, 据说这是因为处理器早起的实现过程中, 更新程序计数器(PC指向下一条指令的位置)是作为执行下一条指令的第一步, 也就是说不管你要跳转还是要做别的什么动作, 先更新PC再说. 这样做的好处有两点 :

1. 不需要把绝对地址完整地记录在指令中, 可以缩短指令的长度...

2. 在该程序被链接重定向到不同的地址的时候, 我们不需要对这条指令进行改动...

 

练习题有一点要注意, 在偏移量计算过程中, 要注意这个偏移量可以为负值...

 

3.6.4 翻译条件分支

如下图是使用我们所学的的条件码以及跳转的组合实现的C语言中的if从句, 当然这个程序从健壮性的角度其实是不够严密的, 发生负溢出时会出现我们不希望的跳转...

 

另一方面我们可以大致总结出汇编对于if语句的通用实现形式...

 

posted on 2016-07-04 20:16  内脏坏了  阅读(290)  评论(0编辑  收藏  举报