第1章 基本概念
1.1 欢迎来到汇编语言的世界
Microsoft宏汇编器(MASM)
1.1.1 读者可能会问的问题
什么是汇编器和链接器?
汇编器是一种工具程序,用于将汇编语言源程序转换为机器语言。
链接器是一种工具程序,它把汇编器生成的单个文件组合为一个可执行程序。
能学到什么?
x86处理器应用的计算机体系结构的基本原理
汇编语言与机器语言有什么关系?
汇编语言与机器语言是一对一的关系:每一条汇编语言指令对应一条机器语言指令。
C++和Java与汇编语言有什么关系?
高级语言与汇编语言和机器语言的关系是一对多。比如,C++的一条语句就会扩展为多条汇编指令或机器指令。
C++
int Y;
int X=(Y+4)*3
与之等价的汇编程序:
mov eax,Y ;Y送入EAX寄存器
add eax,4 ;EAX寄存器内容加4
mov ebx,3 ;3送入EBX寄存器
imul ebx ;EAX与EBX相乘
mov X,eax ;EAX的值送入X
寄存器(register)是CPU中被命名的存储位置,用于保存操作的中间结果。
汇编语言可移植吗(portable)?
汇编语言是不可移植的,因为它是为特定处理器系列设计的。
微代码解释器 (microcode intepreter)
第3章
3.2.2 运行和调试AddTwo程序
;AddTwo.asm - add two 32-bit integers.
.386
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitCode:DWORD
.code
main PROC
mov eax,5
add eax,6
INVOKE ExitProcess,0
main ENDP
END main
[项目]->鼠标右键->生成依赖项->生成自定义(B)…->masm(.target,.props)
在解决方案属性窗口中调整以下选项:
无法像Python一样选中脚本源文件时就调试该脚本,因此需要将调试过的脚本的入口函数名进行修改,仅将需要调试的脚本入口名调整为main
在AddTwo.asm属性中调整项类型:
运行时才会显示寄存器窗口;
需要调整解决方案平台为x86
可对寄存器添加监视,以观察运行过程中值的变化情况
Registers窗口的一个重要特点是,在单步执行程序时,任何寄存器,只要当前指令修改了它的数值,就会变为红色。
3.2.3 程序模板
;程序模板 (Template.asm)
.386
.model flat,stdcall
.stack 4096
.data
;在这里声明变量
.code
main PROC
;在这里编写自己的代码
INVOKE ExitProcess,0
main ENDP
END main
3.3 汇编、链接和运行程序
第4章 数据传送、寻址和算术运算
4.1 数据传送指令
4.1.2 操作数类型
[label:] mnemonic [operands] [;comment]
指令包含的操作数个数可以是:0个,1个,2个或3个。
mnemonic
mnemonic [destination]
mnemonic [destination],[source]
mnemonic [destination],[source-1],[source-2]
操作数有3种基本类型
立即数
寄存器操作数
内存操作数
4.1.3 直接内存操作数
变量名引用的是数据段内的偏移量
.data
var1 BYTE 10h
;假设var1的地址的偏移量为10440h。
mov al var1
指令会被汇编为下面的机器指令:
A0 00010400
4.1.4 MOV指令
MOV指令将源操作数复制到目的操作数。
MOV destionation, source
其中,目的操作数的内容会发生改变,而源操作数不会改变。
原则:
●两个操作数必须是同样的大小
A2022 instruction operands must be the same size
●两个操作数不能同时为内存操作数
A2070 invalid instrcution operands
●指令指针寄存器(IP、EIP或RIP)不能作为目标操作数
MOV指令的标准格式:
MOV reg,reg
MOV mem,reg
MOV reg,mem
MOV mem,imm
MOV reg,imm
在将整型常数复制到一个变量或寄存器时,必须考虑该常量需要的最少字节数。
4.1.5 整数的全零/符号扩展
1.把一个较小的值复制到一个较大的操作数
2.MOVZX指令
MOVZX destionation,source
MOVZX reg32,reg/mem8
MOVZX reg32,reg/mem16
MOVZX reg16,reg.mem8
注意:操作数不能是常数
A2070 invalid instrcution operands
3.MOVSX指令
如果一个十六进制常数的最大有效数字大于7,那么它的最高位等于1.
4.1.6 LAHF和SAHF指令
LAHF(加载状态标志位到AH)指令将EFLAGS寄存器的低字节复制到AH。被复制的标志位包括:符号标志位、零标志位、辅助进位标志位、奇偶标志位和进位标志位。
SAHF(保存AH内容到状态标志位)指令将AH内容复制到EFLAGS(或RFLAGS)寄存器低字节。
4.1.7 XCHG指令
XCHG(交换数据)指令交换两个操作数内容。
XCHG reg,reg
XCHG reg,mem
XCHG mem,reg
除了XCHG指令不使用立即数作操作数之外,XCHG指令操作数的要求与MOV指令操作数要求是一样的。
在数组排序应用中,XCHG指令提供了一种简单的方法来交换两个数组元素
xchg ax,bx
xchg ah,al
xchg var1,bx
xchg eax,ebx
如果要交换两个内存操作数,则用寄存器作为临时容器。
4.1.8 直接-偏移量操作数
变量名加上一个位移就形成了一个直接-偏移量操作数。这样可以显示访问那些没有显式标记的内存位置。
MASM没有内置的有效地址范围检查。
.386
.model flat, stdcall
.stack 4096
ExitProcess proto, dwExitCode:dword
.data
val1 word 1000h
val2 word 2000h
arrayB BYTE 10h,20h,30h,40h,50h
arrayW WORD 100h,200h,300h
arrayD DWORD 10000h,20000h
.code
main PROC
;演示MOVZX指令
mov bx,0A69Bh
movzx eax,bx
movzx edx,bl
movzx cx,bl
;演示MOVSX指令
mov bx,0A69Bh
movsx eax,bx
movsx edx,bl
mov bl,7Bh
movsx cx,bl
;内存-内存的交换
mov ax,val1
xchg ax,val2
mov val1,ax
;直接-偏移量寻址(字节数组)
mov al,arrayB
mov al,[arrayB+1]
mov al,[arrayB+2]
;直接-偏移量寻址(字数组)
mov ax,arrayW
mov ax,[arrayW+2]
;直接-偏移量寻址(双字数组)
mov eax,arrayD
MOV eax,[arrayD+4]
mov eax,[arrayD+TYPE arrayD]
INVOKE ExitProcess,0
main ENDP
END
4.2 加法和减法
4.2.1 INC和DEC指令
INC(增加)和DEC(减少)指令分别标识寄存器或内存操作数加1和减1。语法如下所示:
INC reg/mem
DEC reg/mem
根据目标操作数的值,溢出标志位、符号标志位、零标志位、辅助进位标志位、进位标志位和奇偶标志位会发生变化。INC和DEC指令不会影响进位标志位。
4.2.2 ADD指令
ADD指令将长度相同的源操作数和目的操作数进行相加操作。
ADD dest,source
该指令可以使用的操作数与MOV指令相同
标志位 进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存入目标操作数的数值进行变化。
4.2.3 SUB指令
SUB dest,source
标志位 进位标志位、零标志位、符号标志位、溢出标志位、辅助进位标志位和奇偶标志位根据存入目标操作数的数值进行变化。
4.2.4 NEG指令
NEG (非)指令通过把操作数转换为起二进制补码,将操作数的符号取反。
NEG reg
NEG mem
(将目标操作数按位取反再加1,就可以得到这个数的二进制补码)
4.2.5 执行算术表达式
Rval=-Xval+(Yval-Zval)
Rval SDWORD ?
Xval SDWORD 26
Yval SDWORD 30
Zval SDWORD 40
;first term: -Xval
mov eax,Xval
neg eax ;eax=-26
;second term: (Yval-Zval)
mov ebx,Yval
sub ebx,Zval ;ebx=-10
;add the terms and store
add eax,ebx
mov Rval,eax ;-36
4.2.6 加减法影响的标志位
标志位的简要概述:
●进位标志位意味着无符号整数溢出。比如,如果目的操作数为8位,而指令产生的结果大于二进制的1111 1111,那么进位标志位置1.
●溢出标志位意味着有符号整数溢出。比如,指令目的操作数为16位,但其产生的负数结果小于十进制的-32768,那么溢出标志位置1.
●零博鳌值为意味着操作结果为0.
●符号标志位意味着操作产生的结果为负数。如果目的操作数的最高有效位(MSB)置1,则符号标志位置1.
●奇偶标志位是指,在一条算术或布尔运算指令执行后,立即判断目的操作数最低有效字节中1的个数是否为偶数。
●辅助进位标志位置1,意味着目的操作数最低有效字节中位3有进位。
1.无符号数运算:零标志位、进位标志位和辅助进位标志位
INC和DEC指令不会影响进位标志位。在非零操作数上应用NEG指令总是会将进位标志位置1。
4.3 与数据相关的运算符合伪指令
●OFFSET运算符返回的是一个变量与其所在段起始地址之间的距离
●PTR运算符可有重写操作数默认的大小类型
●TYPE运算符返回的是一个操作数或数组中每个元素的大小(按字节记)
●LENGTHOF运算符返回的是数组中元素的个数
●SIZEOF运算符返回的是数组初始时使用的字节数
●LABEL伪指令可以用不同的大小类型来重新定义同一个变量
4.3.1 OFFSET运算法
可以用一个变量的偏移量来初始化另一个双字变量,从而有效地创建一个指针。
.data
bigArray DWORD 500 DUP(?)
pArray DWORD bigArray
下面的指令把该指针的值加载到ESI中,因此,这个ESI寄存器就可以指向数组的起始地址:
.code
mov esi,pArray
4.3.2 ALIGN 伪指令
ALIGN伪指令把一个变量对齐到字节边界、字边界、双字边界或段落边界。
ALIGN bound
Bound可取值有:1、2、4、6、16。
当取值为1时,则下一个变量对齐于1字节边界(默认情况)。当取值为2时,则下一个变量对齐于偶数地址,当取值为4/16时,则下一个变量地址为4/16的倍数。
为什么要对齐数据?因为,对于存储于偶地址和奇地址的数据来说,CPU处理偶地址数据的速度要快得多。
bVal BYTE ? ;00404000h
ALIGH 2
wVal WORD ? ;00404002h
bVal2 BYTE ? ;00404004h
ALIGN 4
dVal DWORD ? ;00404008h
dVal DWORD ? ;0040400Ch
dVal的偏移量原本是0040 4005,但是ALIGN 4伪指令使它的偏移量成为0040 4008.
4.3.3 PTR运算符
PTR运算符可以用来重写一个已经被声明过的操作数的大小类型。只要试图用不同于汇编器设定的大小属性来访问操作数,那么这个运算符就是必需的。
注意,PTR必须与一个标准汇编数据类型一起使用,这些类型包括:BYTE、SBYTE、WORD、SWORD、DWORD、SDWORD、FWORD、QWORD或TBYTE。
将较小的值送入较大的目的操作数
.data
wordList WORD 5678h,1234h
.code
mov eax,DWORD PTR wordList ;EAX=12345678h
将第一个字节复制到EAX的低半部分,第二个字复制到高半部分。
4.3.4 TYPE运算法
4.3.7 LABEL伪命令
LABEL伪指令可以插入一个标号,并定义它的属性大小属性,但是不为这个标号分配存储空间
.data
val16 LABEL WORD
val32 DWORD 12345678h
.code
mov ax,val16 ;AX=5678h
mov dx,[val16+2] ;DX=1234h
.data
LongValue LABEL DWORD
val1 WORD 5678h
val2 WORD 1234h
.code
move eax,LongValue
4.4 间接寻址
直接寻址很少用于数组处理,因为,用常数偏移量来寻址多个数组元素时,直接寻址不实用。
反之,会用寄存器作为指针(称为间接寻址)并控制该寄存器的值。如果一个操作数使用的是间接寻址,就称之为间接操作数。
4.4.1 间接操作数
保护模式 任何一个32位通用寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP和ESP)加上括号就能构成一个间接操作数。寄存器中存放的是数据的地址。
示例如下,ESI存放的是byteVal的偏移量,MOV指令使用间接操作数作为源操作数,解析ESI中的偏移量,并将一个字节送入AL:
.data
byteVal BYTE 10h
.code
mov esi,offset byteVal
mov al,[esi] ;AL=10h
如果目的操作数也是间接操作数,那么新值将存入由寄存器提供地址的内存位置。
在下面的例子中,BL寄存器的内容复制到ESI寻址的内存地址中:
mov [esi],bl
PTR与间接操作数一起使用
一个操作数的大小可能无法从指令中直接看出来。
汇编器不知道ESI指针的类型是字节、字、双字,还是其他的类型。而PTR运算符则可以确定操作数的大小类型:
inc BYTE PTR[esi]
4.4.2 数组
4.4.3 变址操作数
变址操作数是指,在寄存器上加上常数产生一个有效地址。每个32位通用寄存器都可以用作变址寄存器。
MASM可以用不同的符号来表示变址操作数(括号是表示符号的一部分):
constant [reg]
[constant + reg]
4.4.4 指针
如果一个变量包含另一个变量的地址,则该变量称为指针。指针是控制数组和数据结构的重要工具,因为,它包含的地址在运行时是可以修改的。比如,可以使用系统调用来分配(保留)一个内存块,再把这个块的地址保存在一个变量中。指针的大小受处理器当前模式(32位或64位)的影响。下例为32位的代码,ptrB包含了arrayB的偏移量:
.data
arrayB byte 10h,20h,30h,40h
ptrB dword arrayB
还可以用OFFSET运算符来定义ptrB,从而使得这种关系更加明确:
ptrB dword OFFSET arrayB
使用TYPEDEF运算符
TYPEDEF运算符可以创建用户定义类型,这些类型包含了定义变量时内置类型的所有状态。它是创建指针变量的理想工具。
比如,下面声明创建的一个新数据类型PBYTE就是一个字节指针:
PBYTE TYPEDEF PTR BYTE
这个声明通常放在靠近程序开始的地方,在数据段之前。然后,变量就可以用PBYTE来定义:
.data
arrayB BYTE 10h,20h,30h,40h
ptr1 PBYTE ? ;未初始化
ptr2 PBYTE arrayB ;指向一个数组
4.5 JMP和LOOP指令
●无条件转移:无论什么情况都会转移到新地址。新地址加载到指令指针寄存器,使得程序在新地址进行执行吗。JMP指令实现这种转移。
●条件转移:满足某种条件,则程序出现分支。各种条件转移指令还可以组合起来,形成条件逻辑结构。CPU基于ECX和标志寄存器的内容来解释真/假条件。
4.5.1 JMP指令
JMP指令无条件跳转到目标地址,该地址用代码标号来标识,并被汇编器转换为偏移量。
JMP destionation
4.5.2 LOOP指令
LOOP指令,正式称为按照ECX计数器循环,将程序块重复特定次数。ECX自动成为计数器,每循环一次计数值减1.语法如下所示:
LOOP destionation
循环目标必须距离当前地址计数器-128到+127字节范围内。LOOP指令的执行有两个步骤:第一步,ECX减1,第二步,将ECX与0比较。如果ECX不等于0,则跳转由目标给出的标号。否则,如果ECX等于0,则不发生跳转,并将控制传递到循环后面的指令。
实地址模式中,CX是LOOP指令的默认循环计数器。同时,LOOPD指令使用ECX为循环计数器,LOOPW指令使用CX为循环计数器。
下面的例子中,每次循环是将AX加1。当循环结束时,AX=5,ECX=0:
mov ax,0
mov ecx,5
第5章过程
5.1 堆栈操作
5.1.1 运行时堆栈(32位模式)
运行时堆栈是内存数组,CPU用ESP (扩展堆栈指针,extended stack pointer)寄存器对其进行直接管理,该寄存器被称为堆栈指针寄存器(stack pointer register)。32位模式下,ESP寄存器存放的是堆栈中某个位置的32位偏移量。
1.入栈操作
32位入栈操作把栈顶指针减4,再将数值复制到栈顶指针指向的堆栈位置。
运行时堆栈是向下生长的,即从高地址向低地址扩展。
2.出栈操作
出栈操作从堆栈删除数据。数据弹出堆栈后,栈顶指针增加(按栈顶元素大小),指向堆栈中下一个最高位置。
ESP之下的堆栈域在逻辑上是空白的,当前程序下一次执行任何数值入栈操作指令都可以覆盖这个区域。
3.堆栈应用
•当寄存器用于多个目的时,堆栈可以作为寄存器的一个方便的临时保存区。在寄存器被修改后,还可以恢复其初始值。
•执行CALL指令后,CPU在堆栈中保存当前过程的返回地址。
•调用过程时,输入数组也被称为参数,通过将其压入堆栈实现参数传递。
•堆栈也为过程局部变量提供了临时存储区域。
5.1.2 PUSH和POP指令
1.PUSH指令
PUSH指令首先减少ESP的值,再将源操作数复制到堆栈。操作数是16位的,则ESP减2,操作数是32位的,则ESP减4。
PUSH reg/mem16
PUSH reg/mem32
PUSH imm32
2.POP指令
POP指令首先把ESP的值,再将源操作数复制到堆栈。如果操作数是16位的,则ESP加2,操作数是32位的,则ESP加4。
POP reg/mem16
POP reg/mem32
3.PUSHFD和POPFD指令
PUSHFD指令把32位EFLAGS寄存器内容压入堆栈,而POPFD指令则把栈顶单元内容弹出到EFLAGS寄存器:
pushfd
popfd
通常用PUSHFD和POPFD封闭一段代码:
pushfd ;保存标志寄存器
;
;任意语句序列
;
popfd
当用这种方式使用入栈和出栈指令时,必须确保程序的执行路径不会跳过POPFD指令。
.data
saveFlags DWORD ?
.code
pushfd
pop saveFlags
push saveFlags
popfd
4.PUSHAD,PUSHA,POPAD和POPA