汇编语言学习笔记(四)-MOV指令

汇编语言和高级语言不通,需要清晰掌握数据存储和机器相关的大量细节,高级语言会自动处理很多赋值的细节,有时甚至都不知道编译器给我创建了多少变 量,赋值了几次,但在汇编里,一切都变得很清晰了,你需要自己处理每个赋值的细节,这是通过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)。

Built with Hugo
主题 StackJimmy 设计