汇编语言和高级语言不通,需要清晰掌握数据存储和机器相关的大量细节,高级语言会自动处理很多赋值的细节,有时甚至都不知道编译器给我创建了多少变 量,赋值了几次,但在汇编里,一切都变得很清晰了,你需要自己处理每个赋值的细节,这是通过mov指令实现,在汇编里,几乎有一半多的代码都是mov。
对于AT&T和MASM两种语法,最大的差别就从此开始了,因为二者的源、目的操作数是相反的!
在AT&T里mov的语法结构为:
movx source, destination
其中x为操作数的宽度,有l,w,b,q等,例如movl操作数的宽度为32位,movb操作数宽度为8位,例如:
movl $0, %eax
movl $0, %al
这里另一个不同是寄存器的写法,在AT&T里使用寄存器要在寄存器前加%符号,在MASM中则不用。另外MASM可以自动识别操作数大小,如果有些仅仅传入内存,无法识别操作数大小,则必须使用 dword ptr[] 语法,这里在寻址部分会有涉及。
在MASM中,mov语言的规则为:
mov destination, source
这里千万注意二者的操作数是相反的!
对于MOV指令还有一些规则如下:
- 两个操作数尺寸必须相同
- 两个操作数不可同为内存
- 目的操作数不可为CS,EIP和IP
- 立即数不可直接传送至段寄存器
这些规则对AT&T语法也同样适用,why?因为二者编译都会编译成相同的二进制代码,这些规则是cpu的规则,而不是某种语法的规则。
以上说的是基本的mov指令,另外为了方便或者效率,还有些相应的扩展指令: 扩展移动,因为mov指令的操作数大小必须相同,所以如果需要把一个较小值ax移动到较大值ebx中,则需要两步:
mov ebx, 0
mov bx, ax
如果ax为负数,需要把ebx高位扩展为全1,mov ebx, FFFFFFFFh,好在intel支持了相关的扩展指令,movzx和movsx movzx为零扩展,即高位补零,movsx为符号扩展,即高位补符号位,一般有符号数适用movsx。此指令目标操作数必须为寄存器,源操作数可以为内存和寄存器。 写一个简单的例子:
.section .data
num1: .byte 0x12
num2: .byte 0xf2
.section .text
.globl _main
_main:
mov num1, %al
movzx num1, %eax
movsx num1, %ebx
movzx num2, %ecx
movsx num2, %edx
pushl $0
call _exit
编译时使用 gcc -g hello.s -o hello
加入调试信息,便于调试,然后使用命令 gdb a.exe
调试:
br main // 在main函数出下断点
r // 执行程序
n // 单步运行至 pushl $0 处
info register // 查看寄存器信息
eax 0×12 18
ecx 0xf2 242
edx 0xfffffff2 -14
ebx 0×12 18
显示所有寄存器的信息,可以看到,eax和ebx都是0×12,因为num1高位是0,所以无论使用哪种方式都是高位补零,num2符号位为1,如此在使用movzx时高位补零,但是使用movsx时高位为符号位,补0,所以edx的最后结果为0xfffffff2。
这里示例了AT&T语法,对于MASM二者的用法相同,除了操作数的顺序。
mov 的另外一种变形是cmovx形式,涉及到cpu流水线的问题,cpu的eip寄存器指向当前执行的指令,但这个指令如果每次用到再从内存中读取,速度会有 所限制,于是cpu使用流水线方式,会对指令进行预读取,就是先读一定量的指令到队列中,每次都从此队列中读取,这样会加快执行速度,但是一旦遇到跳转指 令,cpu在运行到此处时并不知道程序将会走哪个分支,如此,到达跳转部分,将会有可能清空队列中所有指令,再次读取另一分支,损耗cpu执行指令的效 率,当然,cpu后来加入了乱序引擎,但是还是尽量减少跳转为上。对于一些简单的跳转移动数据,比如比较大小后决定是否赋值,使用cmovx形式便可不进 行跳转而完成这个过程。
例如对于c语言的一段分支代码:
if (a > b)
max = a;
现用汇编改写:
.686
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
.data
value1 dd 4321h
value2 dd 1234h
max dd ?
.code
start:
mov eax, value1
cmp eax, value2
cmovb eax, value2
mov max, eax
invoke ExitProcess, NULL
end start
这里要说明的是.686模式,如果使用.386模式编译会错误,说cmovb对目前的cpu模式不支持,从start开始,第一句 mov eax, value1 把value1赋值给eax寄存器,cmp比较eax和value2的大小,实际上做了一个eax-value2的操作,但是舍弃结果。如果eax小于 value2,则eflag寄存器的CF位被设置(参考寄存器),cmovb指令是CF=1时移动数据,否则不做操作。这样eax中即为大数,直接移动到 max内存中。
这样程序看不到最终的效果,还需要调试器的帮助,在Windows平台,比较方便的动态调试工具为Ollydbg,打开 Ollydbg后文件->打开cmov.exe,如此便可在代码窗口看到刚才写的指令,可以在push 0处设置断点,运行便可在右侧寄存器窗口看到结果。其使用方式与VC大致相同。
另外从调试过程中,我们也可看到invoke ExitProcess, NULL被编译成push和call指令,与AT&T的写法相同,invoke仅仅是为了简化函数调用的写法而已。
另外cmovx是一系列函数,都是根据eflag寄存器的不同标志来决定是否移动数据,其中的细节可参考Intel手册卷2A,这里简单列举一些情形以及条件:
对于无符号型比较
cmova / cmovnbe > 赋值
cmovae / cmovnb >=
cmovnc 无进位赋值
cmovb / cmovnae <
cmovc 进位
cmovbe / cmovna cmove / cmovz = 或 为0 赋值
cmovne / cmovnz 不等于
对于有符号类型
:::nasm
cmovge / cmovnl >=
cmovl / cmovnge <
cmovle /cmovng cmovo 溢出
cmovno 未溢出
cmovs 结果带符号(负数)
cmovns 无符号(非负数)
这里乍一看,如此之多的情形,还只是其中一部分,不过稍微细心观察,这里的规律还是很明显的:
a表示above,无符号大于
b表示blow,无符号小于
e表示equal,等于
n表示not,不等于
g表示great,有符号大于
l表示less,有符号小于
掌握其中规律,记住这些指令并不是难事,而且这些规律在汇编指令中是用过的,不如以后将会看到的条件跳转指令,也是如此。
到这里mov指令介绍完了,但是移动数据的指令还有很多,这里也顺便做一些归纳,简单列举几类指令,对于AT&T和MASM都是通用的。
-
xchg 指令: 格式 xchg operand1, operand2 ,交换operand1和operand2两个操作数的值,但千万别以为这个指令很方便,因为这个指令操作时会对内存加锁LOCK,可能会非常耗时,所以请慎用。这里两个操作数不能同为内存。
-
bswap 指令: 格式 bswap operand , 其中operand为32位或64位寄存器,这个指令时反转字节指令,即可以让32位寄存器的4个字节数据反转,可以方便的转换大小端数据。
-
lahf 和 sahf 指令: lahf指令将eflags寄存器的低字节复制到ah寄存器,sahf把ah寄存器的值复制到eflags寄存器的低字节,ah为隐含操作数。
-
push 指令: 把一个16位、32位、64位数据(寄存器,立即数,内存)到堆栈,esp指针做相应修改。这里AT&T指令必须自己指定大小,加后缀l, w等,与mov指令相同。
-
pop 指令: 出栈,与push相反。
另外还有pop和push的批量操作指令:
pusha/popa 全部16位通用寄存器入栈/出栈
pushad/popad 全部32位通用寄存器入栈/出栈
pushf/popf eflags寄存器低16位入栈/出栈
pushfd/popfd eflags寄存器全32位入栈/出栈
到这里介绍了常用的数据移动指令,当然这里不可能一一详细介绍,其中的细节还需要参考Intel手册卷2(A,B)。