读《Professional Assembly Language》之移位之谜
今年是离开校园的第五年,这五年来我一直在从事应用软件的设计、开发工作,大部分时间是在与高级编程语言、设计模式、业务逻辑打交道。它们大多流于表面,久而久之,与技术底层疏远了,诸如计算机组成原理、汇编语言、编译原理、数据结构以及算法慢慢得生疏,时至今日,我已经弄不懂IA-32里有几类寄存器、间接寻址、词法分析是怎么一回事情了。五年是一个契机,趁着下一个五年开始之际,我计划用三个月至半年时想间,重新学习这些知识,以期达到巩固基础,厚积薄发的目的。
学习过程肯定会有问题,找不出问题的学生不是好学生。因此,我把遇到的问题和解决的方法记录下来,便形成了读书笔记。本篇博文便是我在阅读《Professional Assembly Language》一书时,所作的其中一篇读书笔记。《Professional Assembly Language》,中文译作《汇编语言程序设计》,是我学习汇编语言时选择的工具书,该书对于我这种已经有了高级语言的使用经验,又热衷Linux的人来说非常合适。
《Professional Assembly Language》在第八章第二节讲解了Shift Instruction,在谈到算数左移(SAL)和逻辑左移(SHL)时,作者列举了该指令的三种格式:
- sal destination
- sal %cl, destination
- sal shifter, destination
其中,第三种格式表述存不清楚,shifter可以是immediate value?是common register?是memory location?下面先用shifter是memory location类型来做个试验。
1 2 3 4 5 6 7 8 9 10 11 12 13 | .section .data val: .int 0x1 count: .int 0x21 .section .text .global _start _start: nop sall count, val movl $1, %eax movl $0, %ebx int $0x80 |
执行汇编器以后,得到错误如下:
shift_over_32.s: Assembler messages:
shift_over_32.s:11: Error: suffix or operands invalid for `sal’
用shifter为immediate value类型再次试验:
1 2 3 4 5 6 7 8 9 10 11 | .section .data val: .int 0x1 .section .text .global _start _start: nop sall $0x21, val movl $1, %eax movl $0, %ebx int $0x80 |
这次汇编和链接都通过,说明第一个例子中,用memory location类型作为shifter是不对的。
这个问题追根究底,可以从《Intel 64 and IA-32 Architectures Software Developer’s Manual》上找到答案:
The destination operand can be a register or a memory location. The count operand can be an immediate value or the CL register.
— 4-356 Vol. 2B
也就是说count operand(即shifter)的类型可以是立即数或者CL寄存器,而destination operand则不能是立即数。
在手册中,还可以看到这么一句话:
The count is masked to 5 bits (or 6 bits if in 64-bit mode and REX.W is used). The count range is limited to 0 to 31 (or 63 if 64-bit mode and REX.W is used).
也就是说,移位操作最多只能移动31位,那么超过了31位会怎么样呢?
(gdb) print /t val
$1 = 1
(gdb) s
_start () at shift_over_32.s:9
9 movl $1, %eax
(gdb) print /t val
$2 = 10
上述代码使得数字1算术左移33位,而实际的效果是只1位。在此情况下,移动位数的低5位(32-bit模式下)才起作用,33对应的二进制的低5位是00001,那么也就是算数左移1位。
如果修改左移33位为31位,那么可以想象最低位上的数字将移动到最高位,下面的代码可以佐证。
1 2 3 4 5 6 7 8 9 10 11 | .section .data val: .int 0x1 .section .text .global _start _start: nop sall $0x1f, val movl $1, %eax movl $0, %ebx int $0x80 |
在GDB下,两次查看val的值,符合预期。
(gdb) print /t val
$10 = 1
(gdb) s
_start () at shift_over_32.s:9
9 movl $1, %eax
(gdb) print /t val
$11 = 10000000000000000000000000000000