汇编语言(assembly language)是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言,亦称为符号语言.在汇编语言中,用助记符(Mnemonics)代替机器指令的操作码,用地址符号(Symbol)或标号(Label)代替指令或操作数的地址.在不同的设备中,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令,普遍地说,特定的汇编语言和特定的机器语言指令集是相互对应的,不同平台之间不可直接移植.
该系列笔记,记录了Win32汇编的常用语法规则,有些汇编代码并没有返回任何参数,如需观察,请编译后使用OllyDBG附加进程,单步跟踪调试,该笔记是为二进制软件逆向打基础的,故简化的很多项目开发章节的内容,如果你需要使用汇编开发项目(当然那样很麻烦),你可以参考《Intel 汇编语言程序设计》,《琢石成器-Win32汇编语言程序设计》
,当然本笔记的所有内容也是来自于这两本书的实例,不过简化了大量繁琐的文字描述,只保留了比较实用的内容.
汇编语言一直被认为是最难学的语言,其原因主要是因为汇编程序员在着眼于程序逻辑实现
的同时还要注重其他底层细节
,这些在学习高级语言可以不必涉及到的底层工作细节,在学习汇编语言中,便成了家常便饭,使用汇编编程,要求程序员具有对底层的清晰认识,学习汇编能够更好的理解操作系统
的运作原理,从而帮助构建底层知识体系.
汇编语言是所有程序设计语言中最古老
的,它与计算机机器语言
最为接近,通过汇编语言可以直接访问计算机的硬件
,能够直接与CPU对话,可以说汇编语言是所有编程语言中语法格式最*
的,但*的代价就是需要了解计算机体系结构和操作系统的大量细节,每编写一段程序都需要考虑各种硬件的状态,从而导致使用汇编写程序效率非常低
.
微机体系概述
在学习汇编语言之前,我们必须要了解一下处理器的发展历史.
自1946年第一台计算机问世以来,在短短的60多年中,已经历了由电子管计算机(1946年),晶体管计算机(1956年),集成电路计算机(1958年),超大规模集成电路计算机(1972年)
,这五代的更替,而且还在不断地向巨型化,微型化,网络化,智能化这四个方向不断发展.
从当今的X86架构的CPU说起,X86指令集
是Intel为其第一块16位CPU(80x86)
专门开发的,IBM公司1981年推出的世界第一台PC机中的CPU—i8088(i8086简化版)
使用的也是X86指令,同时电脑中为提高浮点数据处理
能力而增加的X87芯片
系列协处理器则另外使用X87指令,为了提高处理器性能,就将X86指令集和X87指令集统称为X86指令集.
虽然随着CPU技术的不断发展,Intel公司陆续研制出更新型的i80386、i80486、Pentium
直到今天,但为了保证电脑能继续运行以往开发的各类应用程序以保护和继承丰富的软件资源,所以Intel公司所生产的所有CPU仍然继续使用X86指令集,所以它的CPU仍属于X86系列
,由于X86系列及其兼容CPU都使用X86指令集,所以就形成了今天庞大的X86系列及兼容CPU阵容.
谈完了处理器的基本发展过程,再来了解一下CPU指令集的分类吧.
处理器分为两大架构阵营,即RISC(精简指令集计算机)
和CISC(复杂指令集计算机)
是当前CPU的两种架构,它们的区别在于不同的CPU设计理念和方法,CPU架构是厂商给属于同一系列的CPU产品定的一个规范,主要目的是为了区分不同类型CPU的重要标示.
早期的CPU全部是CISC架构
,它的设计目的是要用最少的机器语言指令来完成所需的计算任务.比如对于乘法运算,在CISC架构的CPU上,您可能只需要一条指令就可以得到相应的结果,这些幕后的操作全部依赖于CPU中设计的逻辑电路来完成,这种架构会增加
CPU结构的复杂性
和对CPU制作工艺的要求,但对于编译器的开发却十分有利.
相比CISC架构的系统,RISC架构
则要求软件来指定各个操作步骤,上面的乘法运算如果要在RISC架构上实现,则你需要具体指定其特定的实现步骤,使用这种架构生产CPU,可以很大程度上降低CPU的复杂性以及允许在同样的工艺水平下生产出功能更强大的CPU,但对于编译器的设计有更高的要求.
总结:当精简指令集出现后,所有人都说复杂指令集已经过时,英特尔密切关注,为了谨慎.英特尔同时开发复杂指令集CPU和精简指令集CPU.精简指令处理器上市后,复杂指令集CPU依旧热销.而精简指令集CPU因为无法兼容以前的软件,而销售量不好.英特尔得出复杂指令集生命依旧强大的结论,放弃在精简指令集方面的开发工作.
机器语言汇编语言和高级语言的区别 ----> (机器语言创作了汇编)->(汇编创作了C,C撑起了整个计算机世界)
机器语言(machinelanguage)
是一种指令集的体系,这种指令集被称为机器码(machinecode)
,它是电脑的CPU可直接解读
的数据,机器码有时也被称为原生码(NativeCode)
,它与系统运行平台相关联,机器语言是计算机的设计者通过计算机的硬件结构赋予计算机的操作功能,早期的机器语言程序员在编写程序时只需要使用两种符号0或1
,这种编程方式太枯燥,并且需要记忆大量的0101这种二进制格式的排布方式,实在是一件很痛苦的事,就是在这种环境下汇编语言诞生了.
汇编语言(assembly language)
是一种用于电子计算机、微处理器、微控制器或其他可编程器件的低级语言
,亦称为符号语言,是机器语言的符号化
,也就是说以往我们需要记忆大量的0101二进制,而现在则只需要记忆相应的符号,比如mov eax,1
移动指令,人们很容易理解他的含义,尽管如此,操作汇编语言还是需要了解操作系统的大量底层实现,其在应对大型应用的开发上还是不够简单.
高级语言(High-level programming language)
相对于汇编语言,它是较接近自然语言和数学公式的编程,基本脱离了机器的硬件系统,能够用人们更易理解的方式编写程序,高级语言高度封装了底层实现细节,其语法格式自然,简单易用,屏蔽了很多底层细节的实现,开发效率明显提高,这给人们能够开发出大型应用系统提供了可能性.
总结:机器语言与汇编语言的关系属于一脉相承,指令是一对一的关系,这也给软件逆向提供了一种可能,而高级语言和汇编的关系属于一对多的关系,高级语言的一条代码,可能对应机器语言的数十条,甚至数百条.
直到现在,CPU处理器也只能识别两种电位格式0或1,计算机不理解高级语言,我们必须通过编译器转成二进制代码才能运行(c/c++,java...)
,只学会高级语言,并不等于理解计算机实际的运行步骤.
80x86处理器的几种基本工作模式
IA-32处理器有三种基本的工作模式:实地址模式
,系统管理模式
,保护模式
,另外还有一种模式称为虚拟80x86模式
,其实虚拟x86模式也是保护模式的一个特例,下面个将分别简要描述这几种系统模式:
实地址模式: 在该模式下,IA-32处理器使用20位地址线,可以访问1048576(1MB)
字节的内存,其地址范围是0-FFFFF
,但8086处理器的寄存器是16位的不能存放20位的地址,为了解决这个棘手的问题提出了一种称为分段内存
的概念,所有内存被分为了多个64kb的区域,这些区域称为段(segment)
,我们使用段地址x16+偏移地址=绝对地址
来计算出绝对地址.
保护模式: 在该模式下,每个程序可寻址4GB的内存,地址范围是0-FFFFFFFF
,在该模式下编程无需进行复杂的公式计算,只需要使用一个32位整数就可以存放任何指令和变量的地址,处理器会在后台进行地址的计算和转换,这些工作对于汇编程序员变得透明了起来,保护模式下有三个段:CS:代码段
,DS:数据段
,SS:堆栈段
,其他的段操作系统负责维护.
虚拟x86模式: 在该模式下,实际上是处理器在保护模式下创建的一个具有1MB地址空间的虚拟机,虚拟机对运行于实地址模式下的80x86计算机
进行了模拟,在Windows NT
系统下,打开一个控制台窗口,就创建了一个8086虚拟机,当然你也可同时打开多个控制台,他们之间是隔离的并不互相影响.
平坦分段模式: 在该模式下,所有段都被映射到32位的物理地址空间中,一个程序至少需要2个段:代码段(CS,数据段(DS)
,每个段都由一个段描述符定义,段描述符通常是一个存放在全局描述符表(GDT)
中的一个64位地址.
内存分页机制: IA-32处理器支持一种称为分页(paging)
的特性,允许一个段被分割成称为页(page)
的4096字节的内存块,分页机制允许同时运行的程序使用总内存远大于计算机的物理内存,操作系统映射的所有页的集合称为虚拟内存
,操作系统通常都会包含一个虚拟内存管理器的程序,分页机制会使人产生内存无限大的错觉,然而程序如果过度依赖于分页的话,其运行效率会非常低下.
CPU内部的寄存器组,以及每个寄存器的作用
寄存器是CPU内部的高速存储单元,由于是固化在CPU内部的组件,其访问速度快于内存,在当下的处理器中寄存器分为几种类型,其中8个通用寄存器(EAX,EBX,ECX,EDX,EBP,ESP,ESI,EDI)
,6个段寄存器(CS,SS,DS,ES,FS,GS)
,一个处理器状态标志寄存器(EFLAGS)
,和一个指令指针寄存器(EIP)
寄存器.
通用寄存器: CPU内部有8个通用寄存器
主要用于算数运算和数据的传送,这8个寄存器都可以作为一个32位的值或两个16位的值来寻址使用,还可以按照8位寄存器来使用,比如通用寄存器都可以被拆分为高低寄存器来存储数据,例如:EAX寄存器
,可被拆分为(AX)16位
寄存器来使用,而(AX)16位
寄存器还可拆分为AH/AL(高低8位)
.
变址寄存器: CPU内部有2个通用寄存器ESI和EDI
,寄存器ESI、EDI称为变址寄存器(Index Register)
,它们主要用于存放存储单元在段内的偏移量,用它可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便.变址寄存器不可分割
成8位寄存器,在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能,该寄存器默认和DS数据段
寄存器相关联.
堆栈指针寄存器: CPU内部有2个通用寄存器EBP和ESP
,寄存器EBP、ESP称为指针寄存器(Pointer Register)
,主要用于存放堆栈
内存储单元的偏移量,它们主要用于访问堆栈内的存储单元并且规定,EBP为基址指针寄存器
,ESP为堆栈指针寄存器
,指针寄存器不可分割
成8位寄存器,该寄存器默认和SS堆栈段
寄存器相关联.
指令指针寄存器: CPU内部有1个指令指针寄存器EIP
,该寄存器存放下一条要执行的指令的地址,下次要执行的指令通常已被预取到指令队列中,除非发生转移情况,所以在理解它们的功能时,不考虑存在指令队列的情况,默认情况下EIP不可手动修改,一般都是由特殊的指令CALL,RET,PUSH
等间接性的修改.
段寄存器: 段寄存器是根据内存分段的管理模式而设置的,内存单元的物理地址由段寄存器的值和一个偏移量组合而成的,这样可用两个较少位数的值组合成一个可访问较大物理空间的内存地址,常规段寄存器包括CS:代码段寄存器
,DS:数据段寄存器
,SS:堆栈段寄存器
,ES:附加数据段寄存器
这些寄存器通常是由编译器或这是操作系统来维护的.
标志寄存器: 标志寄存器(EFLAGS),该寄存器用来控制CPU的操作流程,或者反应CPU某些运算的结果的独立二进制位构成,常用的标志位包括CF(进位标志),ZF(零标志),PF(奇偶标志)
等.
手动编译一段小程序
.386p
.model flat,stdcall
option casemap:none
include windows.inc
include kernel32.inc
includelib kernel32.lib
MyDef equ 1024 ; 将数值指定名称
.data
Main WORD 1024 ; 定义可赋值的变量
.data? ; 定义未知初始变量
lyshark DWORD ?
.code
main PROC
xor eax,eax
invoke ExitProcess,0
main ENDP
END main
C:\Users\lyshark> ml /c /coff lyshark.asm
C:\Users\lyshark> link /subsystem:windows lyshark.obj
数据段的定义
MASM 定义了多种内部数据类型,每种数据类型都描述了该类型的变量和表达式的取值集合,汇编语言中数据类型的基本特征是以数据位数为度量单位:8,16,32,48,64,80位
,而除此之外其他的特征如(符号,指针,浮点数)
主要是为了方便我们记忆变量中存储的数据类型.
接下来看下表,表中是IEEE委员会发布的标准内部数据类型:
数据类型 | 作用(无符号) | 数据类型 | 作用(有符号) |
---|---|---|---|
BYTE | 8位无符号整数 | SBYTE | 8位有符号整数 |
WORD | 16位无符号整数 | SWORD | 16位有符号整数 |
DWORD | 32位无符号整数 | SWORD | 32位有符号整数 |
FWORD | 48位整数(远指针) | QWORD | 64位整数定义 |
REAL4 | 32位(4字节)短实数 | REAL8 | 64位(8字节)长实数 |
数据类型定义语句为变量在内存中保留存储空间,并且可以选择为变量指定一个名字,在汇编语言中所有的数据无非就是BYTE
的集合,数据的定义语句格式如下:
[变量名] 数据定义伪指令 初始值[....]
在数据定义语句中使用BYTE(定义字节)
和SBYTE(定义有符号字节)
伪指令,可以为每一个或多个有符号或无符号字节分配存储空间,每个初始值必须是8位整数
表达式或字符常量,例如下面的定义:
.data
var1 BYTE 'A' ; 定义字符常量
var2 BYTE ? ; 定义未初始化变量
var3 BYTE 0 ; 最小的无符号字节常量
var4 BYTE 255 ; 最大的无符号字节常量
var5 SBYTE -128 ; 最小的有符号字节常量
var6 SBYTE +127 ; 最大的有符号字节常量
如果一条数据定义语句中有多个初始值,那么标号仅仅代表第一个初始值的偏移,如下我们首先定义一个BYTE数组,然后通过反汇编查看地址的偏移变化就能看到效果啦:
.data
list BYTE 10,20,30,40,50
00E71000 | B8 0030E700 | mov eax,main.E73000 | E73000=10
00E71005 | B8 0130E700 | mov eax,main.E73001 | E73001=20
00E7100A | B8 0230E700 | mov eax,main.E73002 | E73002=30
00E7100F | B8 0330E700 | mov eax,main.E73003 | E73003=40
00E71014 | B8 0430E700 | mov eax,main.E73004 | E73004=50
并非所有的数据定义都需要标号,如果想继续定义以list开始的字节数组,可以在随后的行上接着上面的定义:
.data
list BYTE 10,20,30,40,50
list BYTE 60,70,80,90,100
当然除了定义整数字符以外,还可以定义字符串,要想定义字符串应将一组字符用单引号或双引号括起来.最常见的字符串是以空格结尾0h
,在C/C++,JAVA中定义字符串无需添加结尾0h,这是因为编译器会在编译的时候自动的在字符串后面填充了0h,在汇编语言中我们需要手动添加字符串结尾的标志:
.data
string1 BYTE "hello lyshark",0h
string2 BYTE "good night",0h
00F23000 68 65 6C 6C 6F 20 6C 79 73 68 61 72 6B 00 67 6F hello lyshark.go
00F23010 6F 64 20 6E 69 67 68 74 00 00 00 00 00 00 00 00 od night........
字符串也可以占用多行,而无须为每行都提供一个编号,如下代码也是合法的:
.data
string1 BYTE "welcom to the Demo program"
BYTE "created by lyshark",0dh,0ah,
BYTE "url:lyshark"
BYTE "send me a copy",0dh,0ah,0
十六进制0dh,0ah
也称为CR/LF(回车换行符)
,或者是行结束的字符,在向标准输出设备上写的时候,回车换行符可以将光标移动到下一行的开头位置,从而继续填充新的字符串.
有时我们需要初始化一些空值的内存空间,在为内存地址分配空间的时候,DUP伪指令就显得尤为重要,初始化和未初始化数据均可使用DUP指令定义,其定义语法如下:
.data
string1 BYTE 20 DUP(0) ; 分配20字节,全部填充0
BYTE 20 DUP(?) ; 分配20字节,且未初始化
BYTE 50 DUP("stack") ; 分配50字节,"stackstack..."
.data
smallArray DOWRD 10 DUP(0) ; 分配40字节
bigArray DOWOR 5000 DUP(?) ; 分配20000字节
除了上面的例子以外,我们也可以直接定义常量,常量是不可以动态修改的数据类型,一般情况下一旦定义,那么在程序运行期间不可以被修改,常量的定义很简单,只需要将.data
换成.const
即可.
.const
var1 BYTE "hello world",0h ; 初始化为BYTE的字符串
var2 DWORD 10 ; 初始化为10的DWORD类型
var3 DWORD 100 dup(1,2) ; 200个DWORD的缓冲区
var4 BYTE 1024 dup(?) ; 1024字节的缓冲区
var5 BYTE "welcome",0dh,0ah,0 ; 0dh,0ah为换行符
有时我们需要计算数组的大小,但手动计算显得特别麻烦,此时我们可以使用MASM提供的$符号来进行数组大小的计算过程,如下.
.data
list BYTE 10,20,30,40,50
listsize = ($ - list) ; 计算字节数据大小
.data
list WORD 1000h,2000h,3000h,4000h
listsize = ($ - list) /2 ; 计算字数据大小
.data
list DWORD 100000h,200000h,300000h,400000h
listsize = ($ - list) /4 ; 计算双字数据大小
Post_1 equ 1000
Post_2 equ 2000
Post_3 equ 3000
标准输入输出
StdIn/StdOut: 使用masm32.inc
提供的函数实现标准的输入与输出.
.386
.model flat, stdcall
include masm32.inc
include kernel32.inc
includelib masm32.lib
includelib kernel32.lib
.data
len equ 20
OutText dw ?
ShowText db "请输入一个数: ",0
.code
main PROC
invoke StdOut, addr ShowText ; 输出提示信息
invoke StdIn, addr OutText,len ; 等待用户的输入
invoke StdOut, addr OutText ; 输出刚才输入的内容
ret
main ENDP
END main
WriteFile: 通过调用系统的API函数,来实现具体的输出,其过程比较复杂不推荐使用.
.386
.model flat, stdcall
include windows.inc
include kernel32.inc
includelib kernel32.lib
.data
szText db "hello lyshark!",0
.data?
hOut dd ? ; 保存句柄
hLen dd ? ; 保存字符长度
.code
main PROC
invoke GetStdHandle,STD_OUTPUT_HANDLE ; 获取设备控制台句柄
mov hOut,eax ; 把获取到的句柄给hOut
invoke lstrlen,addr szText ; 取出字符串的长度
mov hLen,eax
invoke WriteFile,hOut,addr szText,hLen,0,0 ;具体的输出
ret
main ENDP
END main
crt_printf: 使用微软C标准库中的printf函数; msvscrt.inc 把它声明做 crt_printf
.386
.model flat, stdcall
include msvcrt.inc
includelib msvcrt.lib
.data
PrintText db "EAX=%d;EBX=%d;EDX=%d | InPut ->: ",0
ScanFomat db "%s",0
PrintTemp db ?
.code
main PROC
mov eax,10
mov ebx,20
mov ecx,30
invoke crt_printf,addr PrintText,eax,ebx,ecx ; 打印提示内容
invoke crt_scanf, addr ScanFomat, addr PrintTemp ; 输入内容并接收参数
invoke crt_printf, addr PrintTemp ; 输出输入的内容
ret
main ENDP
END main
常用汇编指令
MOV指令: 从源操作数向目标操作数之间复制数据.
00A41000 | B8 24100000 | mov eax,1024 |
00A41005 | 8BD8 | mov ebx,eax |
00A41007 | 66:B9 0010 | mov cx,1000 |
MOVZX指令: 零扩展传送,该指令将源操作数的内容复制到目标操作数中,并将该值零扩展(zero-extend)
至16位或者32位,该指令适用于无符号整数
,其基本格式如下:
01301000 | 66:BB 9BA6 | mov bx,A69B | BX = 0A69B
01301004 | 0FB7C3 | movzx eax,bx | EAX = 0000A69B
01301007 | 0FB6D3 | movzx edx,bl | EDX = 0000009B
0130100A | 66:0FB6CB | movzx cx,bl | CX = 009B
MOVSX指令: 符号扩展传送,该指令将源操作数的内容复制到目标操作数中,并将该值符号扩展(sign-extend)
至16位或者是32位,该指令只能用于有符号整数
,其基本格式如下:
00FD1000 | 66:BB 9BA6 | mov bx,A69B | BX = 0A69B
00FD1004 | 0FBFC3 | movsx eax,bx | EAX = FFFFA69B
00FD1007 | 0FBED3 | movsx edx,bl | EDX = FFFFFF0B
00FD100A | 66:0FBECB | movsx cx,bl | CX = FF9B
XCHG指令: 数据交换指令,该指令用于交换两个操作数中的内容,但该指令不接受立即数操作数.
00D71000 | B8 00100000 | mov eax,1000 | EAX = 1000h
00D71005 | BB 00200000 | mov ebx,2000 | EBX = 2000h
00D7100A | 93 | xchg ebx,eax | EAX = 2000h;EBX = 1000h
INC/DEC指令: 数据递增与递减,INC指令
用于对寄存器或内存数据的递增,DEC指令
用于对寄存器或内存数据递减.
00881000 | B8 00100000 | mov eax,1000 | EAX = 1000h
00881005 | 40 | inc eax | EAX = 1001h
00881006 | 40 | inc eax | EAX = 1002h
00881007 | BB 00200000 | mov ebx,2000 | EBX = 2000h
0088100C | 4B | dec ebx | EBX = 1FFFF
0088100D | 4B | dec ebx | EBX = 1FFFE
0088100E | 4B | dec ebx | EBX = 1FFFD
ADD指令: 操作数增加,该指令用于将源操作数和目的操作数相加
,且不影响源操作数的值,而是改变目的操作数.
00BC1000 | B8 00100000 | mov eax,1000 | EAX = 1000
00BC1005 | BB 00200000 | mov ebx,2000 | EBX = 2000
00BC100A | 03D8 | add ebx,eax | EBX = EBX+EAX = 3000
SUB指令: 操作数减少,该指令用于将源操作数和目的操作数相减
,且不影响源操作数的值,而是改变目的操作数.
00811000 | B8 00200000 | mov eax,2000 | EAX = 2000
00811005 | BB 00100000 | mov ebx,1000 | EBX = 1000
0081100A | 2BC3 | sub eax,ebx | EAX = EAX-EBX = 1000
AND/OR/XOR指令: 逻辑与/逻辑或/逻辑异或.
00DD100E | B8 01000000 | mov eax,1 |
00DD1013 | BB 01000000 | mov ebx,1 |
00DD1018 | B9 00000000 | mov ecx,0 |
00DD101D | 21D8 | and eax,ebx |
00DD101F | 09CB | or ebx,ecx |
00DD1021 | 31C0 | xor eax,eax |
OFFSET操作符: 返回数据标号的偏移地址,偏移地址代表标号距数据基址的距离,单位是字节.
.data
var1 BYTE ?
var2 WORD ?
var3 DWORD ?
var4 DWORD ?
.code
main PROC
mov esi,offset var1
mov esi,offset var2
mov esi,offset var3
mov esi,offset var4
main ENDP
END main
PTR操作符: 用来重载声明操作数的默认尺寸,这在试图以不同与变量声明时所使用的尺寸来访问变量时很有用.
.data
temp DWORD 12345678h
.code
main PROC
mov eax,DWORD PTR [temp] ; 将temp以双字取值并存储到eax
mov ax,WORD PTR [temp] ; 将temp以字为单位取值并存储到ax
mov bx,WORD PTR [temp+2] ; 在偏移基础上+2
main ENDP
END main
00C11000 | A1 0030C100 | mov eax,dword ptr ds:[C13000] | EAX = 12345678
00C11005 | 66:A1 0030C100 | mov ax,word ptr ds:[C13000] | AX = 5678
00C1100B | 66:8B1D 0230C100 | mov bx,word ptr ds:[C13002] | BX = 1234
LENGTHOF操作符: 计算数组元素的数目,元素由出现在的同一行的值定义.
.data
ArrayDW DWORD 1000,2000,3000,4000,5000,6000,7000,8000,9000,0h
ArrayBT BYTE 1,2,3,4,5,6,7,8,9,0h
.code
main PROC
mov eax,lengthof ArrayDW
mov eax,lengthof ArrayBT
push 0
call ExitProcess
main ENDP
END main
TYPE操作符: 返回按照字节计算的单个元素的大小.
.data
var1 BYTE ?
var2 WORD ?
var3 DWORD ?
var4 QWORD ?
.code
main PROC
mov eax,TYPE var1 ; 1
mov ebx,TYPE var2 ; 2
mov ecx,TYPE var3 ; 4
mov edx,TYPE var4 ; 8
push 0
call ExitProcess
main ENDP
END main
SIZEOF操作符: 返回等于LENGTHOF(总元素数)和TYPE(每个元素占用字节)
返回值的乘基.
.data
var1 WORD 32 DUP(0) ; 32*2
var2 BYTE 10,20,30,40 ; 3
var3 WORD 30 DUP(?),0,0 ; 30+2
var4 DWORD 1,2,3,4 ; 4
.code
main PROC
mov eax,SIZEOF var1
mov eax,SIZEOF var2
mov eax,SIZEOF var3
mov eax,SIZEOF var4
main ENDP
END main
LOOP循环(普通循环): 该指令检测ECX
寄存器的变化,每次循环寄存器自动减1
,当ECX=0
循环结束,否则继续循环.
.code
main PROC
mov ecx,10 ; 计数循环寄存器初始化为10
top: ; 循环标号,编译器会将其转换成一个地址
xor eax,eax
mov eax,ecx
loop top ; loop跳转到指定地址,此处为top
push 0
call ExitProcess
main ENDP
END main
LOOP循环(循环中使用ECX): 如果用光了所有的通用寄存器,但又必须要使用ECX的话,可以在循环开始将ECX保存.
.data
count DWORD ?
.code
main PROC
mov ecx,10
top:
mov count,ecx ; 将ecx寄存器放入count变量
xor ecx,ecx
mov ecx,1000 ; 重置ecx寄存器的数值
add eax,ecx
mov ecx,count ; 处理完成后,恢复ECX寄存器
loop top ; 继续循环
push 0
call ExitProcess
main ENDP
END main
LOOP循环(嵌套循环): 在循环内部创建另一个循环的时候,必须考虑外层ECX中的外层循环计数该如何处理,把外层循环计数保存在内存中,是非常的理想的.
.data
count DWORD ?
.code
main PROC
mov ecx,10 ; 设置外层循环计数
L1:
mov count,ecx ; 保存外层循环计数
mov ecx,20 ; 设置内层循环计数
L2:
xor eax,eax
xor ebx,ebx
xor edx,edx
loop L2 ; 重复内层循环计数
mov ecx,count ; 恢复外层循环计数器
loop L1 ; 执行外层循环跳转
push 0
call ExitProcess
main ENDP
END main
IF-ENDIF(伪指令):
.code
main PROC
mov eax,100
mov ebx,200
.IF (eax == ebx) && (ebx == ebx)
xor eax,eax
xor ebx,ebx
.ELSEIF (eax >= 100) || (ebx == ebx)
add eax,100
add ebx,100
.ENDIF
main ENDP
END main
WHILE-ENDW(伪指令):
.data
Count DWORD 10
SumNum DWORD 0
.code
main PROC
xor eax,eax
.WHILE (eax < Count)
add SumNum,1
inc eax
.ENDW
main ENDP
END main
REPEAT-UNTIL(伪指令): 以下代码利用循环伪指令,完成了1-10相加.
.data
Count DWORD 10
SumNum DWORD 0
.code
main PROC
xor eax,eax
.REPEAT
inc eax
add SumNum,1
.UNTIL (eax >= Count)
main ENDP
END main
BREAK(伪指令): 以下是个死循环,当eax寄存器的值等于5时,则执行.break结束程序的运行.
.code
main PROC
mov eax,10
.while (1)
dec eax
.break .if(eax == 5)
.endw
ret
main ENDP
END main
CONTINUE(伪指令): 当EAX的值小于等于5时执行continue,否则执行inc ebx
,总循环数为10.
.code
main PROC
mov eax,0
mov ebx,0
.repeat
inc eax
.continue .if(eax <= 5)
inc ebx
.until (eax >= 10)
ret
main ENDP
END main
FOR 字符替换(伪指令): 该伪指令并不是循环,而是分别将指定的指令批量的替换到程序中.
.code
main PROC
for num,<1,2,3>
xor eax,eax
add eax,DWORD PTR [num]
endm
ret
main ENDP
END main
FORC字串替换(伪指令): 该伪指令并不是循环,而是分别将指定的字串批量的替换到程序中.
.code
main PROC
forc code,<@#$%^&*()<>>
BYTE "&code"
endm
ret
main ENDP
END main
内存寻址方式
Windows系统默认运行于保护模式下,当处理器运行于保护模式下时,每个程序可以寻址4GB的内存范围,地址范围是从十六进制数的0-FFFFFFFF
,微软汇编器的平坦模式,适用于保护模式编程,在平坦模式下其内存寻址的方式包括,直接寻址
,间接寻址
,基址变址寻址
,比例因子寻址
等,接下来将分别来演示.
◆直接寻址◆
在声明变量名称的后面加上一个偏移地址,可以创建直接偏移(direct-offset)
操作数,可以通过它来访问没有显示标号的内存地址,接下来看一个实验例子:
.data
ArrayB BYTE 10h,20h,30h,40h,50h
ArrayW WORD 100h,200h,300h,400h
ArrayDW DWORD 1h,2h,3h,4h
.code
main PROC
; 针对字节的寻址操作
mov al,[ArrayB] ; al=10
mov al,[ArrayB+1] ; al=20
mov al,[ArrayB+2] ; al=30
; 针对内存单元字存储操作
mov bx,[ArrayW] ; bx=100
mov bx,[ArrayW+2] ; bx=200
mov bx,[ArrayW+4] ; bx=300
; 针对内存单元双字存储操作
mov eax,[ArrayDW] ; eax=00000001
mov eax,[ArrayDW+4] ; eax=00000002
mov eax,[ArrayDW+8] ; eax=00000003
main ENDP
END main
◆间接寻址◆
在处理数组操作时完全使用直接寻址是不切实际的,我们不大可能为数组的每个元素都提供一个不同的标号,也不太可能使用非常多的常量偏移地址去寻址数组的各个元素,处理数组唯一可行的方法是用寄存器作为指针并操作寄存器的值,这种方法称为间接寻址(indirect addressing)
,操作数使用间接寻址时,就称为间接操作数(indirect operand)
.
通过ESI内存寻址: 通过使用ESI寄存器,外加偏移地址(此处DWORD=4字节)
,实现寻址.
.data
ArrayDW DWORD 10000h,20000h,300000h
.code
main PROC
mov esi,offset ArrayDW ; 获取数据段的内存基址
mov eax,[esi] ; 取出[esi]地址中的数据,并赋值给eax
add esi,4 ; 每次esi指针加4,因为数据格式为DWORD
mov eax,[esi]
add esi,4
mov eax,[esi]
main ENDP
END main
通过ESP堆栈寻址: 通过ESP堆栈寄存器,实现寻址.
.code
main PROC
mov eax,100 ; eax=1
mov ebx,200 ; ebx=2
mov ecx,300 ; ecx=3
push eax ; push 1
push ebx ; push 2
push ecx ; push 3
mov edx,[esp + 8] ; EDX = [ESP+8]=1
mov edx,[esp + 4] ; EDX = [ESP+4]=2
mov edx,[esp] ; EDX = [ESP]=3
main ENDP
END main
◆变址寻址◆
变址寻址,变址操作数(indexed operand)
把常量和寄存器相加以得到一个有效地址,任何32位通用寄存器都可以作为变址寄存器
,MASM允许使用两种不同的变址操作数据格式.
变量名+寄存器: 通过变量名和寄存器结合,变量名代表变量偏移地址的常量,通过变更ESI寄存器
的值进行数据寻址.
.data
ArrayDW DWORD 10000h,20000h,300000h
.code
main PROC
mov esi,0
mov eax,[ArrayDW + esi] ; 通过变量名+esi寄存器寻址
mov ebx,8 ; 增加8字节
mov eax,[ArrayDW + ebx] ; 定位第三个DW数据内存
main ENDP
END main
基址+偏移: 通过把变址寄存器
和内存偏移常量
结合,用寄存器存放数组基址,用常量标识各个数组元素.
.data
ArrayW WORD 1000h,2000h,3000h,4000h
.code
main PROC
mov esi,offset ArrayW ; 获取基址
mov ax,[esi] ; 显示第一个数据
mov ax,[esi + 2] ; 显示第二个数据
mov ax,[esi + 4] ; 最后一个
main ENDP
END main
基址变址寻址: 通过计算公式,这里数组中每个元素占用4字节,所以需要乘以4,寄存器ECX为需要定位的元素偏移.
.data
Array DWORD 1000h,2000h,3000h,4000h,0h
.code
main PROC
lea eax,Array
mov ecx,2
mov edx,DWORD PTR [eax + ecx * 4] ;edx=3000h
mov ecx,1
mov edx,DWORD PTR [eax + ecx * 4] ;edx=2000h
main ENDP
END main
比例因子寻址: 通过使用比例因子,以下例子每个DWORD=4字节
,且总元素下标=0-3
,得出比例因子3* type arrayDW
.
.data
ArrayDW DWORD 1000h,2000h,3000h,4000h
.code
main PROC
; 第1种比例因子寻址
mov esi,3*type ArrayDW ;总共3个下标x每个元素的类型
mov eax,ArrayDW[esi]
; 第2种比例因子寻址
mov esi,3 ; 变更ESI下标,可实现定位不同的数据
mov eax,ArrayDW[esi*4] ; 其中4代表每个数据类型4字节
; 第3种比例因子寻址
mov esi,3
mov eax,ArrayDW[esi*type ArrayDW]
main ENDP
END main
指针寻址: 变量地址的变量称为指针变量(pointer variable)
,Intel处理器使用两种基本类型的指针,即near(近指针)
和far(远指针)
,保护模式下使用Near指针
,所以它被存储在双字变量中.
.data
ArrayB BYTE 10,20,30,40,50
ArrayD DWORD 1,2,3,4,5
ptrB DWORD OFFSET ArrayB ; 指针ptrB --> ArrayB
ptrD DWORD OFFSET ArrayD ; 指针ptrD --> ArrayD
.code
main PROC
mov esi,ptrB ; 指向数组ArrayB
mov al,[esi] ; 取出 10h
mov esi,ptrD ; 指向数组ArrayD
mov eax,[esi] ; 取出 1h
main ENDP
END main
标志测试指令
在学习数据比较指令之前,需要先来了解一下标识寄存器这个东西,标志寄存器
又称程序状态寄存器(Program Status Word,PSW),这是一个存放条件码标志,控制标志和系统标志的寄存器.
标志寄存器中存放的有条件标志,也有控制标志,它对于处理器的运行和整个过程的控制有着非常重要的作用.条件标志主要包括进位标志、奇偶标志、辅助进位标志、零标志、符号标志、溢出标志等,控制标志主要有跟踪标志,因为有标志寄存器的存在才能实现各种华丽的判断循环等,常用的标志有以下6个:
标志位 | 标志全称 | 标志序号 | 标志位说明 |
---|---|---|---|
CF(Carry Flag) | 进位标志位 | 0 | 当执行一个加法(或减法)运算,使最高位产生进位(或借位)时,CF为1;否则为0 |
PF(Parity Flag) | 奇偶标志位 | 2 | 当运算结果中,所有bit位(例:1001010)中1的个数为偶数时,则PF=1;为基数PF=0 |
AF(Auxiliary Flag) | 辅助进位标志 | 4 | 执行加法(减法)运算,结果的低4位向高4位有进位(借位)时,则AF=1;否则AF=0 |
ZF(Zero Flag) | 零标志位 | 6 | 若当前的运算结果为零,则ZF=1;否则ZF=0 |
SF(Sign Flag) | 符号标志位 | 7 | 若运算结果为负数,则SF=1;若为非负数则SF=0 |
TF(Trap Flag) | 陷阱标志位 | 8 | 为方便程序调试而设计的,TF=1单步执行指令,TF=0则CPU正常执行程序 |
IF(Interrupt) | 中断允许标志 | 9 | 当IF=1CPU可响应可屏蔽中断请求,当设置IF=0则CPU不响应可屏蔽中断请求 |
DF(Direction) | 方向标志位 | 10 | 当DF=0时为正向传送数据(cld),否则为逆向传送数据(std) |
OF(Overflow) | 溢出标志位 | 11 | 记录是否产生了溢出,当补码运算有溢出时OF=1;否则OF=0 |
ZF零标志位: ZF标志相关指令执行后,结果为0则ZF=1;若结果不为0则ZF=0.
00C31000 | 90 | nop | ZF = 0
00C31001 | B8 01000000 | mov eax,1 | ZF = 0
00C31006 | 83E8 01 | sub eax,1 | ZF = 1
00C31000 | 90 | nop | ZF = 0
00C31001 | B8 02000000 | mov eax,2 | ZF = 0
00C31006 | 83E8 01 | sub eax,1 | ZF = 0
PF奇偶标志位: PF标志相关指令执行后,其结果所有bit位中的1若为偶数,则PF=1;若为奇数PF=0.
00C31000 | 90 | nop | PF = 0
00C31001 | B8 00000000 | mov eax,00000000 | PF = 0
00C31006 | 83C0 6F | add eax,00000111 | PF = 1
00C31000 | 90 | nop | PF = 0
00C31001 | B8 00000000 | mov eax,00000000 | PF = 0
00C31006 | 83C0 6F | add eax,00000011 | PF = 0
SF符号标志位: SF标志相关指令执行后,其结果是否为负,若为负则SF=1;若为非负SF=0.
00C3100B | 90 | nop | SF = 0
00C3100C | B8 E8030000 | mov eax,3E8 | SF = 0
00C31011 | 2D E9030000 | sub eax,3E9 | SF = 1
00C3100B | 90 | nop | SF = 0
00C3100C | B8 E8030000 | mov eax,3E8 | SF = 0
00C31011 | 2D E9030000 | sub eax,3E8 | SF = 0
CF进位标志位: CF标志相关指令执行后,在进行无符号运算时,如果表达式发生进位或借位则CF=1.
00C31016 | 90 | nop | CF = 0
00C31017 | 66:B8 FFFF | mov ax,FFFF | CF = 0
00C3101B | 66:83C0 01 | add ax,1 | CF = 1
00C31016 | 90 | nop | CF = 0
00C31017 | 66:B8 FFFF | mov ax,FFFF | CF = 0
00C3101B | 66:83C0 01 | sub ax,1 | CF = 0
OF溢出标志位: OF标志相关指令执行后,超出机器所能表示的范围称为溢出若发生了溢出OF=1;否则OF=0.
00C3101B | 90 | nop | OF = 0
00C3101C | B0 40 | mov al,64 | OF = 0
00C3101E | 04 40 | add al,64 | OF = 1
00C31020 | 90 | nop | OF = 0
00C31021 | B0 3F | mov al,63 | OF = 0
00C31023 | 04 40 | add al,64 | OF = 0
TEST指令: 该操作与AND指令类似,唯一不同的是它不保存结果,常用来测试标志位状态.
00DD103B | B8 01000000 | mov eax,1 | EAX = 1
00DD1040 | BB 00000000 | mov ebx,0 | EBX = 0
00DD1045 | 85D8 | test eax,ebx | ZF = 1
00DD1051 | B8 01000000 | mov eax,1 |
00DD1056 | A9 00000000 | test eax,0 | ZF = 1
00DD105B | 83E0 00 | and eax,0 | ZF = 1
00DD1062 | 83C8 01 | or eax,1 | ZF = 0
CMP指令: 在源操作数和目标操作数进行减法操作,只影响标志位.
00DD1001 | B8 00010000 | mov eax,100 | EAX = 100
00DD1006 | BB 50000000 | mov ebx,50 | EBX = 50
00DD100B | 39D8 | cmp eax,ebx | eax - ebx
00DD100D | 0F87 EDFF62FF | ja 401000 | jump
条件跳转指令
注记符 | 跳转条件 | 描述信息 |
---|---|---|
JZ/JE | ZF=1 | 为零则跳转,(leftOp - rightOp = 0) |
JNZ/JNE | ZF=0 | 不为零则跳转,(leftOp - rightOp != 0) |
JC/JNC | CF=1/0 | 设置进位标志则跳/未设置进位标志则跳 |
JO/JNO | OF=1/0 | 设置溢出标志则跳/未设置溢出标志则跳 |
JS/JNS | SF=1/0 | 设置符号标志则跳/未设置符号标志则跳 |
JP/JNP | PF=1/0 | 设置奇偶标志则跳(偶)/未设置奇偶标志则跳(基) |
无符号模式 | 有符号模式 | 跳转条件 | 描述信息 |
---|---|---|---|
JA | JG | (left > right) | 大于则跳转 |
JAE | JGE | (left >= right) | 大于或等于则跳转 |
JB | JL | (left < right) | 小于则跳转 |
JBE | JLE | (left <= right) | 小于或等于则跳转 |
JZ/JE通用跳转: 检测到ZF=1
也就说明表达式返回了0,则程序跳转,否则不跳转.
01031001 | B8 00010000 | mov eax,64 | eax=100
01031006 | BB 00010000 | mov ebx,64 | ebx=100
0103100B | 39D8 | cmp eax,ebx | eax-ebx
0103100D | 0F84 EDFF3CFF | je 401000 | jump
01031013 | 0F84 E7FF3CFF | jz 401000 | jump
JNZ/JNE通用跳转: 检测到ZF=0
也就说明表达式返回了1,则程序跳转,否则不跳转.
01031001 | B8 00010000 | mov eax,65 | eax=101
01031006 | BB 00010000 | mov ebx,64 | ebx=100
0103100B | 39D8 | cmp eax,ebx | eax-ebx
0103100D | 0F84 EDFF3CFF | jne 401000 | not jump
01031013 | 0F84 E7FF3CFF | jnz 401000 | not jump
JA/JB无符号跳转: 基于无符号
数的跳转
指令,JA大于则跳转
或JB小于则跳转
.
01031001 | B8 64000000 | mov eax,64 | eax=100
01031006 | BB C8000000 | mov ebx,C8 | ebx=200
0103100B | 3BD8 | cmp ebx,eax | ebx-eax
0103100D | 0F87 EDFF3CFF | ja 401000 | ebx>eax jump
0103100F | B8 64000000 | mov eax,64 | eax=100
01031014 | BB 32000000 | mov ebx,32 | ebx=50
01031019 | 3BD8 | cmp ebx,eax | ebx-eax
0103101B | 0F82 DFFF3CFF | jb 401000 | ebx<eax jump
01031001 | B8 64000000 | mov eax,64 | eax=100
01031006 | BB 64000000 | mov ebx,64 | ebx=100
0103100B | 3BC3 | cmp eax,ebx | eax-ebx
0103100D | 0F87 EDFF3CFF | ja 401000 | eax=ebx not jump
01031013 | 0F82 E7FF3CFF | jb 401000 | eax=ebx not jump
JAE/JBE无符号跳转: 基于无符号
数的跳转
指令,JAE大于等于则跳转
或JBE小于等于则跳转
.
01031001 | B8 64000000 | mov eax,64 | eax=100
01031006 | BB 64000000 | mov ebx,64 | ebx=100
01031010 | 3BC3 | cmp eax,ebx | eax-ebx
01031012 | 0F83 E8FF3CFF | jae 401000 | eax>=ebx jump
01031001 | B8 64000000 | mov eax,64 | eax=100
01031006 | BB C8000000 | mov ebx,C8 | ebx=200
0103100B | 3BD8 | cmp ebx,eax | ebx-eax
0103100D | 0F83 EDFF3CFF | jae 401000 | ebx>=eax jump
01031001 | B8 C8000000 | mov eax,C8 | eax=200
01031006 | BB 64000000 | mov ebx,64 | ebx=100
0103100B | 3BD8 | cmp ebx,eax | ebx-eax
0103100D | 0F86 EDFF3CFF | jbe 401000 | ebx<=eax jump
JG/JL有符号跳转: 基于有符号
数的跳转
指令,JG大于则跳转
或JL小于则跳转
.
01031001 | B0 7F | mov al,7F | al=0x7F(+127)
01031003 | B3 80 | mov bl,80 | bl=0x80(-128)
01031005 | 3AC3 | cmp al,bl | (+128)-(-127)
01031007 | 0F87 F3FF3CFF | ja 401000 | 不跳转,因为7Fh不大于80h
0103100D | 0F8F EDFF3CFF | jg 401000 | 跳转,因为(+128)大于(-127)
01031001 | B0 9C | mov al,9C | al=(-100)
01031003 | B3 32 | mov bl,32 | bl=(50)
01031005 | 3AC3 | cmp al,bl | (-100)-(50)
01031007 | 0F82 F3FF3CFF | jb 401000 | 不跳转,因为9ch不小于32h
0103100D | 0F8C EDFF3CFF | jl 401000 | 跳转,因为(-100)小于(32)
JGE/JLE有符号跳转: 基于有符号
数的跳转
指令,JGE大于等于则跳转
或JLE小于等于则跳转
.
01031001 | B8 64000000 | mov eax,64 | eax=100
01031006 | BB 64000000 | mov ebx,64 | ebx=100
0103100B | 3BC3 | cmp eax,ebx | eax-ebx
0103100D | 0F83 EDFF3CFF | jae 401000 | 跳转,无符号100=100
01031013 | 0F8D E7FF3CFF | jge 401000 | 跳转,有符号100=100
01031001 | B8 64000000 | mov eax,64 | eax=100
01031006 | BB 9CFFFFFF | mov ebx,FFFFFF9C | ebx=(-100)
0103100B | 3BD8 | cmp ebx,eax | ebx-eax
0103100D | 0F8E EDFF3CFF | jle 401000 | 跳转,有符号数(-100)<(100)
JCXZ/JECXZ跳转指令: 检测ECX
寄存器的值,如果等于零则执行跳转
,否则跳过执行.
01031001 | B9 01000000 | mov ecx,1 | ecx=1
01031006 | E3 F8 | jecxz <a.EntryPoint> | not jump
0103100A | B9 00000000 | mov ecx,0 | ecx=0
0103100F | E3 EF | jecxz <a.EntryPoint> | jump
其他测试指令:
(eax==ebx)&& zero? 如果eax=ebx并且ZF=0则执行
(eax==ebx)&& !zero? 如果eax=ebx并且ZF!=0则执行
CARRY? carry位是否置位
overflow? 溢出
parity?
sign?