汇编语言笔记

Assemby Language (MASM)

涉及操作系统,编写编译器

机器级思维方式处理编程问题

课程配套资料:http://asmirvine.com/

VS2019汇编环境配置

操作数不是数

irvine32配置(作者写的……作者是Kip Irvine)

https://blog.csdn.net/fuhanghang/article/details/112408348

测试irvine的验证代码

; This program adds and subtracts 32-bit integers
; and stores the sum in a variable.
 
INCLUDE Irvine32.inc
 
.data
val1     dword  10000h
val2     dword  40000h
val3     dword  20000h
finalVal dword  ?
 
.code
main PROC
 
    mov    eax,val1            ; start with 10000h
    add    eax,val2            ; add 40000h
    sub    eax,val3            ; subtract 20000h
    mov    finalVal,eax        ; store the result (30000h)
    call    DumpRegs            ; display the registers
 
    exit
main ENDP
END main

有的网站已经书的内容搬运过去了

http://c.biancheng.net/view/3295.html

X86汇编常见的寄存器

offset 偏移量

1基本概念

1.2 虚拟机概念

L0 电子电路

L1 数字逻辑

L2 指令集架构(IAS)-机器语言

L3 汇编语言

L4 高级语言

层与层之间解释翻译

解释在执行前需要译码(解释程序一定是L0)

翻译将L1层程序转为L0层程序

虚拟机可以定义为一个软件程序,用来模拟物理或虚拟计算机

VM1执行L1语言编写的命令

Java程序相对而言独立于系统

1.3 数据表示

必须善于检查内存和寄存器

1.3.1 二进制整数

自右向左,从0编号

左边MSB(最高有效位)

右边LSB(least significant bit)(最低有效位)

二进制与十进制间的转换

1.3.2 二进制加法

加法时要预留存储位

1.3.3 整数储存大小

所有存储的基本单位都是字节

一个字节8位,有字(两个字节),双字,四字

1.3.4 十六进制整数

二进制1111 即 十六进制的 F

提供一种简便的方式阅读大的二进制数

1.3.5 十六进制加法

内存地址用十六进制表示

更换十进制加法的基数,求16的余数,然后按十进制的方向相加

1.3.6 有符号二进制整数

补码:按位取反后加1

十六进制一样适用,但每个位变为15减去数字

1.3.7 二进制减法

1.3.8 字符存储

ASCII码为十六进制

ASCII只用字节的低7位,最高位被用来创建专有字符集

ASCII字符串结尾处有一个为0的字节 ,是\0(反斜杠)

ASCII控制字符好像是一种很牛逼的东西,输出控制字符会执行一些预定义的动作

二进制整数是指以原始格式保存在内存中得整数,以备计算

数字字符串是一串ASCII字符

1.4 布尔表达式

就是 与 或 非

OR AND NOT

NOT具有最高优先级

AND和NOT级别一样

1.5 本章小结

KEY!!!!

汇编器是一种程序,将汇编语言转化为机器语言

链接器将生成的单个文件组合成可执行程序

还有就是调试器,用于追踪程序

汇编语言和机器语言是一对一的关系

2 x86处理器架构

2.1 一般概念

2.1.1 基本微机设计

寄存器,高频时钟,控制单元,算数逻辑单元

时钟:CPU内部操作系统其他组件进行同步

控制单元:协调参与机器指令执行

算数逻辑单元:执行算数运算逻辑运算

内存存储单元用于程序运行时保存指令与数据

这东西不是RAM

接受CPU数据请求,将数据从RAM运往CPU

???

ALU是算数逻辑单元

总线是并行线

数据总线 CPU与内存之间传输指令和数据

IO总线 CPU与系统输入/输出设备之间传输数据

控制总线 用二进制信号对所有连接在总线上的设备的行为进行同步

地址总线 保持指令和数据的地址(Cpu和内存间通信时)

由于不同组件组件的运行速度存在差异

访问内存指令往往需要空时钟周期

也就是等待状态

2.1.2 指令执行周期

1.CPU从指定内存区域(指令队列)获得指令,之后立即增加指令指针的值

2.CPU对指令的二进制位模式进行编码???

3.如果有操作数,CPU就从寄存器和内存中取得操作数,有时还包括地址计算

4.CPU使用从步骤三获得的操作数,CPU执行指令,同时更新状态位

零标志(Zero),进位标志(Carry)和溢出标志(Overflow)

5.如果有输出操作数的话,CPU需要对其存放

简化为三个步骤:取值,译码,执行

图2-2是什么东西??

2.1.3 读取内存

速度比较:内部寄存器>内存

从内存读取数据一般要4个时间周期,而内部寄存器一般只要1个时间周期

所以需要高速存储器辅助

高速存储器cache

一级cache在CPU

二级cache通过高速数据总线与CPU相联

cache是SRAM,即固态

2.1.4 加载并执行程序

程序执行前,需要一种工具程序将其加载到内存

程序加载器

认为解释不详细???

2.2 32位x86

2.2.1 操作系统

2.2.2 基本执行环境

寄存器直接位于CPU内的高速存储位置

来自知乎的形象比喻

如果把被储存的东西比作能量:
1. 寄存器就是 ATP,可以随时拿来用,性能高,但数量有限;
2. 内存就是葡萄糖,性能一般,但是存量可以比较多;
3. 外存(比如硬盘)就是脂肪,容量可以非常大,性能很差,要先转化为葡萄糖(存进内存),然后转化为 ATP(放到寄存器)才能直接利用(存取)。

作者:Pluveto
链接:https://www.zhihu.com/question/20539463/answer/724173258
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
寄存器的功能是存储二进制代码,它是由具有存储功能的触发器组合起来构成的。一个触发器可以存储1位二进制代码,故存放n位二进制代码的寄存器,需用n个触发器来构成

百度百科

基本程序执行寄存器
通用寄存器

用于算数运算数据传输

AH(8位)+AL(8位)=AX(16位)

EAX(32位)

●乘除指令默认使用EAX。它常常被称为扩展累加器(extended accumulator)寄存器。 ●CPU默认使用ECX为循环计数器 ●ESP用于寻址堆栈(一种系统内存结构)数据。它极少用于一般算术运算和数据传输,通常被称为扩展堆栈指针(extended stack pointer)寄存器 ●ESI和EDI用于高速存储器传输指令,有时也被称为扩展源变址(extended souree index)寄存器和扩展目的变址(extended destination index)寄存器

●高级语言通过EBP来引用堆栈中的函数参数和局部变量。除了高级编程,它不用于一般算术运算和数据传输。它常常被称为扩展帧指针(extended frame pointer)寄存器

段寄存器

存放预先分配的内存区域的基址,这些内存区域就是

实地址模式中, 16 位段寄存器表示的是预先分配的内存区域的基址,这个内存区域称为段。保护模式中,段寄存器中存放的是段描述符表指针。一些段中存放程序指令(代码).其他段存放变量(数据),还有一个堆栈段存放的是局部函数变量和函数参数

指令指针寄存器

存放下一条要执行指令的地址

标志(flags)寄存器

包含了独立的 二进制位,用于控制CPU的操作,或是反映一些CPU操作的结果。有些指令可以测试和控制这些单独的处理器标志位

设置标志位时,该标识位=1,清除(或重置)标识位时,该标志位=0。

包含的独立二进制位用于控制CPU操作,并反应ALU操作的结果

控制标志位

程序能够通过设置EFLAGS寄存器中的单独位来控制CPU的操作,比如,方向标志位中断标志位

状态标志位

●进位标志位(CF).与目标位置相比,无符号算术运算结果太大时,设置该标志位。 ●溢出标志位(OF). 与目标位置相比,有符号算术运算结果太大或太小时,设置该标志位。

●符号标志位(SF), 算术或逻辑操作产生负结果时, 设置该标志位。 ●零标志位(ZF). 算术或逻辑操作产生的结果为零时,设置该标志位。 ●辅助进位标志位(AC), 算术操作在8位操作数中产生了位3向位4的进位时,设置该标志位。 ●奇偶校验标志位(PF), 结果的最低有效字节包含偶数个1时,设置该标志位,否则,清除该标志位。一般情况下, 如果数据有可能被修改或损坏时,该标志位用于进行错误检测。

MMX寄存器

intel专属

XMM 寄存器

浮点单元

以上内容在P28-30

2.2.3 x86内存管理

2.3 64位的x86-64位处理器

比x86多了8个通用寄存器

2.4 典型X86计算机组件

2.5 基本输入输出系统

2.5.1 I/O访问层次

通用操作系统极少允许应用程序直接访问硬件

3 汇编语言基础

3.1 基本语言元素

3.1.1 第一个汇编语言程序

.data
sum dword 0

.code
main PROC
	mov eax,5
	add eax,6
	moc sum,eax

	INVOKE ExitProcess,0
main ENDP

这个示范不可运行

变量sum申明大小为32为,关键字是dword

.code .data 为段,后面还要命名一种段,是堆栈

分号开头为注释

需要借助调试器运行(初期没有调试器什么都干不了)

3.1.2 整数常量

26d ;十进制
11010011b ;二进制
42o ;八进制
1Ah
0A3h ;十六进制

3.1.3 整数常量表达式

是一种算数表达式,包含整数常量和算数运算符

3.1.4 实数常量

至少需要一个数字和一个十进制小数点

编码实数表示的是十六进制实数

IEEE浮点数格式表示短实数,暂时用不到,也没讲清楚

3.1.5 字符常量

汇编器在内存中以二进制ASCII形式存储字符常量

3.1.6 字符串常量

在内存中的保存形式为整数字节数值序列

还是ASCII形式存储

3.1.7 保留字

无大小写区分(好耶!)

3.1.8 标识符

由程序员自行选择,用于表示变量,常数,子程序和代码标签

(类似但本质完全不同于C语言的自定义变量)

3.1.9 伪指令

嵌入源代码中的命令,由汇编器识别和执行,不在运行时执行

myvar dword 26  这是伪指令
mov eax,myvar 这是指令

定义段

.stack 定义运行时的堆栈,并设置大小

3.1.10 指令

指令是一种语句,它在程序汇编编译时变得可执行

汇编器将指令翻译为机器语言字节(区别于伪指令)

1.标号

指一种标识符,是指令和数据位置的标记

数据标号

位于指令的前端,表示指令的地址

位于变量的前端,表示变量的地址(确实如此,应为没有变量名)

count dword 100
标号  数据定义类型(指令?)  变量

代码标号

就举下面这个例子

target:
	mov ax,bx ;寄存器间是可以互相移动的
	jmp target
2.指令助记符

英文是用来帮助记忆的,指令本质应当是二进制(?)

3.操作数

(应该是翻译的锅,operation是操作,操作数不是数)

操作数可以是寄存器,内存操作数,整数表达式和输入输出端口(统称操作数)

有多个操作数时,第一个操作数为目的操作数,第二个操作数为源操作数

一般情况下目的操作数的内容由指令修改

stc 指令没有操作数(进位标志位置1)

stc

inc 指令只有一个操作数

inc eax ;EAX加1

imul有3个操作数

imul eax,ebx,5 ;ebx与5相乘,结果存放在eax

eax 为寄存器

count 为内存

4.注释

单行注释,使用分号

块注释,使用comment伪指令定义符号实现

5.NOP空操作指令

占一个字节,用于对齐位置

3.2 整数加减法

3.2.1 AddTwo程序

.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

第一行

.386表明这是个32位程序

第二行选择内存模式(flat)

stdcall确定了子程序的调用规范(32位Windows服务的要求)

第三行

运行时堆栈保留4096字节的存储器

第四行

exitprocess函数函数的原型

原型包括函数名,proto关键字,一个逗号

第4行声明了ExitProcess函数的原型,它是一个标准的Windows服务。原型包含了函数名、PROTO关键字、一个逗号,以及一个输人参数列表。ExitProcess 的输入参数名称为dwExitCode。可以将其看作为给Windows操作系统的返回值,返回值为零,则表示程序执行成功;而任何其他的整数值都表示了一个错误代码。因此,程序员可以将自己的汇编程序看作是被操作系统调用的子程序或过程。当程序准备结束时,它就调用ExitProcess,并向操作系统**返回一个整数(!)**以表示该程序运行良好。

最后一行

end伪指令标记了程序的入口(main)。标号main在前一行进行了声明,它标记了程序开始执行的地址。

汇编伪指令回顾

.stack 4096

4096字节是一个内存页的大小 数值4096可能比将要用的字节数多,但是对处理器的内存管理而言,它正好对应了一个内存页的大小。所有的现代程序在调用子程序时都会用到堆栈——首先, 用来保存传递的参数;其次,用来保存调用函数的代码的地址。函数调用结束后,CPU利用这个地址返回到函数被调用的程序点。此外,运行时堆栈还可以保存局部变量,也就是,在函数内定义的变量。

main endp

标记进程结束

END main

表示程序结束

END后面可以放代码注释,代码副本

3.2.2 运行和调试AddTwo

调出registers,可以看到很多信息

右键flag,可以看到标志位信息

0是清除,1是置位,有修改就会变红

便于理解程序运行

3.2.3 程序模板

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
	;这里声明变量
.code
main PROC
	;这里编写代码

	invoke ExitProcess,0
main ENDP
END main

使用注释 在注释中包括程序说明、 程序作者的名字、创建日期,以及后续修改信息,是一个非常好的主意。这种文档对任何阅读程序清单的人(包括程序员自己,几个月或几年之后)都是有帮助的。许多程序员已经发现了,程序编写几年后,他们必须先重新熟悉自己的代码才能进行修改。如果读者正在上编程课,那么老师可能会坚持要求使用这些附加信息。

3.3 汇编,链接和运行程序

3.3.1 汇编-链接-执行周期

汇编语言和C语言一样,也需要链接

3.3.2 列表文件

列表文件里有符号表

若想告诉 Visual Studio生成列表文件,则在打开项目时按下述步骤操作:在Project菜单中选择Properties,在Configuration Properties 下,选择Microsoft Macro Assembler。然后选择ListingFile。在对话框中,设置Generate Preprocessed Source Listing为Yes,设置List All Avilable Information为Yes

(选项打开以后,VS2019就没法调试程序了)

表现了源文件和源文件生成的机器代码,可检查程序是否正常生成机器代码

数值B8 被称为操作代码,表示特定的机器指令

3.4 定义数据

3.4.1 内部数据类型

汇编器识别一组基本的内部数据类型,按照数据大小(字节、字、双字等等)、是否有符号、是整数还是实数来描述其类型。这些类型有相当程度的重叠——例如,DWORD类型(32位,无符号整数)就可以SDWORD类型(32位,有符号整数)相互交换。可能有人会说,程序员用SDWORD告诉读程序的人,这个值是有符号的,但是,对于汇编器来说这不是强制性的。汇编器只评估操作数的大小。因此,举例来说,程序员只能将32位整数指定为DWORD、SDWORD或者REAL4类型。表3-2给出了全部内部数据类型的列表,有些表项中的IEEE符号指的是IEEE计算机学会出版的标准实数格式。

微信图片_20201011230711

3.4.2 数据定义语句

数据定义语句:在内存为变量留出存储空间,并赋予一个可选的名字

count dword 12345

名字 遵守标识符规定即可

伪指令 上述指令外,还有DB,DW,DD,DQ,DT

初始值 数据定义至少要有一个初始值,即使该值为0

3.4.3 向AddTwo程序添加一个变量

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
sum DWORD 0

.code
main PROC
	mov eax,5
	add eax,6
	mov sum,eax

	invoke ExitProcess,0
main ENDP
END main

3.4.4 定义BYTE和SBYTE数据

byte 定义字节

sbyte 定义有符号字节

value6 byte ?

?表示未初始化,意味着运行时分配数值到该变量

可选名字是一个标号,标识从变量包含段的开始到该变量的偏移量。比如,如果 value1在数据段偏移量为0000处,并在内存中占一个字节,则value2就自动处于偏移量为0001处

value2 BYTE 20h
value1 BYTE 10h

DB伪指令也可以定义有符号或无符号的8位变量:

val1 DB 255
val2 DB -128
1.多初始值

(为什么有这个存在?数组吗?)

单个数据定义时,初始值可以使用不同的基数

list byte 10,20,30,40

10的偏移量是0000,20的偏移量是0001

在单个数据定义中,其初始值可以使用不同的基数。字符和字符串常量也可以自由组合。在下面的例子中,listl和list2有相同的内容:

1ist1 BYTE 10.32,41h,00100010b
1ist2 BYTE 0Ah. 20h,"A'.22h

(!是不是最后都要存成二进制的缘故?)

2.定义字符串

2.定义字符串 定义一个字符申,要用单引号或双引号将其括起来。最常见的字符中类型是用-一个空字节(值为0)作为结束标记,称为以空字节结束的字符串,很多编程语言中都使用这种类型的字符串:

greetingl BrtE "Good afternoon".o
grcecing2 BrTE 'Good night".0

每个字符占一个字节的存储空间。对于字节数值必须用逗号分隔的规则而言,字符申是 个例外。如果没有这种例外,greeting1就会被定义为:

每个字符占一个字节的存储空间。对于字节数值必须用逗号分隔的规则而言,字符申是个例外。如果没有这种例外,greeting1就会被定义为:

greeting1 BYtE 'o'.o'."o'.'d'....etc

字符串可以分为多行,并且不用为每一行都添加标号:

十六进制代码0Dh和0Ah也被称为CRLF(回车换行符)或行结束字符。在编写标准输出时,它们将光标移动到当前行的下一行的左侧,行连续字符()把两个源代码行连接成一条语句,它必须是一行的最后一个字符。

3.DUP操作符

DUP操作符使用一个整数表达式作为计数器,为多个数据项分配存储空间

BYTE 20 DUP(0) ;20个字节,值全部为0
BYTE 20 DUP(?) ;20个字节,非初始化
BYTE 4 DUP("STACK")

3.4.5 定义word和sword数据

word(定义字)

sword(定义有符号字)

DD 32位整数或实数(双字)

是否是有无符号型,取决于数字赋值是否有负号

16位字数组

mylist word 1,2,3,4,5
mylist word 5 dup(?)

3.4.6 定义dword和dsword

dword(定义双字)

sdword(定义有符号双字)

dword还可以声明一种变量,这种变量包含的是另一个变量的32位偏移量

pval dword val3

32位双字

mylist dword 1,2,3,4,5

3.4.7 定义qword数据

qword(定义四字)伪指令为64位数值分配空间

quadl qword 12345678h

不然就是DQ

quadl DQ 12345678h

3.4.8 定义压缩BCD(TBYTE)数据

Intel把一个压缩的二进制编码的十进制(BCD,Binary Coded Decimal)整数存放在一个10字节的包中。每个字节(除了最高字节之外)包含两个十进制数字。在低9个存储字节中,每半个字节都存放了一个十进制数字。最高字节中,最高位表示该数的符号位。如果最高字节为80h,该数就是负数;如果最高字节为00h,该数就是正数。整数的范围是-999 999 999 999 999 999到 +999 999 999 999 999 999

示例下表列出了正、负十进制数1234的十六进制存储字节,排列顺序从最低有效字。

第二个例子无效的原因是MASM将常数编码为二进制整数,而不是压缩BCD整数。如果想要把一个实数编码为压缩BCD码,可以先用FLD指令将该实数加载到浮点寄存器堆栈,再用FBSTP指令将其转换为压缩BCD码,该指令会把数值舍人到最接近的整数

3.4.9 定义浮点类型

real4定义4字节单精度浮点变量(短实数),real8定义8字节双精度浮点变量(长实数),real10定义10字节扩展精度。每个伪指令都需要一个或多个实常数初始值(扩展精度实数)

rval real4 -1.2
rval real8 3.2e-260

3.4.10 变量加法程序

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
firstval dword 20002000h
secondval dword 11111111h
thirdval dword 22222222h
sum dword 0

.code
main PROC
	mov eax,firstval
	add eax,secondval
	add eax,thirdval
	mov sum,eax

	invoke ExitProcess,0
main ENDP
END main

x86指令集不允许将一个变量直接与另一个变量相加,但是允许一个变量与一个寄存器相加

最终数是5333 5333(16进制)

3.4.11 小端顺序

这很重要!

x86处理器在内存中按小端(little-endian)顺序(低到高)存放和检索数据。最低有效字节存放在分配给该数据的第一个内存地址中,剩余字节存放在随后的连续内存位置中。

考虑一个双字12345678h(占用4个字节)。如果将其存放在偏移量为0000的位置,则78h存放在第一个字节(0000),56h存放在第二个字节(0001),余下的字节存放地址偏移量为0002和0003

然而有些计算机用的是大端顺序

3.4.12 声明未初始化数据

.data ? 伪指令声明未初始化数据,减小了编译程序的大小

.data
smallarray dword 10 dup(0)
.data?
bigarray dword 5000 dup(?)

需要有比较,上面的节省空间(20000个字节)

.data
smallarray dword 10 dup(0)
bigarray dword 5000 dup(?)

代码可以与数据混合

.code
mov eax,ebx
.data
temp DWORD ?
.code
mov temp,eax

temp打断了可执行执行流,可以但不推荐

3.5 符号常量

通过为整数表达式或文本指定标识符来创建符号常量(symbolic constant)(也称符号定义(symbolic definition))。符号不预留存储空间。它们只在汇编器扫描程序时使用,并且在运行时不会改变。下表总结了符号与变量之间的不同:

符号:不使用内存运行时数值不改变

3.5.1 等号伪指令(包含当前地址计数器)

(这些东西都是放在.code里的喽?好像不一定)

把一个符号名称与一个整数表达式连接起来

整数表达式 既是

count = 500

可以被重定义,即量是可以改变的!

当前地址计数器 $

下面的语句声明了一个变量selfPtr,并将其初始化为该变量的偏移量

selfPtr dword $

3.5.2 计算数组和字符串的大小

List byte 10,20,30,40
listsize = ($ - list)

ListSize必须紧跟在list的后面。下面的例子中,计算得到的ListSize值(24)就过大

list BYTE 10,20.30,40
var2 BYTE 20 DUP(?)
Listsize = ($ - list)

不需要手动计算字符串的长度,让汇编器完成这个工作:

mystring BYtE "This is a long string, containing"
BYTE "any number of characters"
myString_len = ($ - myString)

字数组和双字数组

字数组和双字数组当要计算元素数量的数组中包含的不是字节时,就应该用数组总的大小(按字节计)除以单个元素的大小。比如,在下例中,由于数组中的每个学要上占2个字节(16位),因此,地址范围应该除以2:

1ist woRD 1000h,2000h,3000h,4000h
Listsize = ($ - list)/2

双字数组

DWORD 10000000h,20000000h,30000000h,40000000h
Listsize = ($ - list)/4

3.5.3 EQU伪指令

把一个符号名称与一个整数表达式或一个任意文本连接起来

下面示例将一个符号和一个字符串连接起来,然后用该符号定义一个变量

presskey equ <"Prees any key",0>
.data
prompt byte presskey
matrix1 equ 10*10
matrix2 equ <10*10>
.data
M1 word matrix1
M2 word matrix2

等价于

m1 word 100
m2 word 10*10

这东西不能重定义

不能重定义

3.5.4 TEXTQU伪指令

TEXTEQU伪指令,类似于EQU,创建了文本宏(text macro)。它有3种格式:第一种 为名称分配的是文本;第二种分配的是已有文本宏的内容;第三种分配的是整数常量表达式: name TEXTEQU

name TExTEQU textmacro

name TExTEQU $constExpr

例如,变量prompt1使用了文本宏 continueMsg:

continueMsg TEXTEQU c"Do you wish to continue (Y/N)?"s
.data
promptl BYTE continueMsg

文本宏可以相互构建。如下例所示,count被赋值了一个整数表达式,其中包含rowSize 然后,符号move被定义为mov。最后,用move和count创建setupAL:

rowsize = 5
count TEXTEQU %(rowsize*21)
move TEXTEQU <mov>
setupAL TEXTEQU <move al. count>

因此,语句 Setupal 就会被汇编为 mov al,10

用TEXTEQU定义的符号随时可以被重新定义

3.6 64位编程(程序模板)

ExitProcess PROTO

.data
sum dword 0
.code
main PROC
	mov eax,5
	mov sum,eax

	mov ecx,0
	call ExitProcess
main ENDP
END 

执行不了,不知道哪儿出错了

使用64位的寄存器

dword变为qword

eax变为rax

3.7 本章小结

整型常量表达式是算术表达式,包括了整数常量。符号常量和算术运算符。优先级是指当表达式有两个或更多运算符时,运算符的隐含顺序

字符常量是用引号括起来的单个字符。汇编器把字符转换为一个字节,其中包含的是该字符的二进制ASCI码。字符事常量是用引号括起来的字符序列,可以选择用空字节标记结束。

汇编语言有一组保留字。它们含义特殊且只能用于正确的上下文中。标识符是程序员选择的名称。用于标识变量、符号常量、子程序和代码标号。不能用保留字作标识符

伪指令是嵌在源代码中的命令,由汇编器进行转换。指令是源代码语句,由处理器在运行时执行。指令助记符是短关键字,用于标识指令执行的操作。标号是一种标识符,用作指令或数据的位置标记

操作数是传递给指令的数据。一条汇编指令有0一~3个操作数,每一个都可以是寄存器、内存操作数、整数表达式成输人/输出端口号

4 数据传送,寻址和算数运算

本章介绍了数据传送和算术运算的若干必要指令,用大量的篇幅说明了基本寻址模式,如直接寻址、立即寻址和可以用于处理数组的间接寻址。同时,还展示了怎样创建循环和怎样使用一些基本运算符,如OFFSET,PTR和LENGTHOF。阅读本章后,将会了解除条件语句之外的汇编语言的基本工作知识。

4.1 数据传送指令

4.1.1引言

用Java或C++这样的语言编程时,编译器产生的大量语法错误信息很容易让初学者感到心烦。编译器执行严格类型检查,以避免可能出现诸如不匹配变量和数据的错误。另一方面,只要处理器指令集允许,汇编器就能完成任何操作请求。换句话说,汇编语言就是将程序员的注意力集中在数据存储和具体机器细节上。编写汇编语言代码时,必须要了解处理器的限制。而x86处理器具有众所周知的复杂指令集(complex instruction set),因此,可以用许多方法来完成任务如果花时间深入了解本章介绍的材料,则阅读本书其他内容会更加顺利。随着示例程序越来越复杂,需要依赖对本章介绍的基础工具的掌握。

4.1.2 操作数类型

指令包含的操作数个数可以是:0个,1个,2个或3个。这里,为了清晰起见,省略掉标号和注释: meronic mnemonic ldestinationl mnemonic Idestinaclon],[sourcel mnemonic [destination],[source-l],[tsource-2]

操作数有3种基本类型: ●立即数——使用数字文本表达式 ●寄存器操作数——使用CPU内已命名的寄存器 ●内存操作数——引用内存位置

这些符号来自intel手册

AH+AL=AX

4.1.3 直接内存操作数

变量名引用的是数据段内的偏移量。例如,如下变量varl的声明表示,该变量的大小类型为字节,值为十六进制的10:

.data
varl BYTE 10h

可以编写指令,通过内存操作数的地址来解析(查找)这些操作数。假设varl的地址偏移量为10400h。如下指令将该变量的值复制到AL寄存器中:

mov a1 varl

指令会被汇编为下面的机器指令: A0 00010400

这条机器指令的第一个字节是操作代码(即操作码(opcode))。剩余部分是var1的32位十六进制地址。虽然编程时有可能只使用数字地址,但是如同var1一样的符号标号会让使用内存更加容易。

4.1.4 MOV指令

左边操作数是目标操作数,右边操作数是源操作数

。两个操作数必须是同样的大小 。两个操作数不能同时为内存操作数 。指令指针寄存器(IP、EIP或RIP)不能作为目标操作数

内存到内存

单条MOV指令不能用于直接将数据从一个内存位置传送到另一个内存位置。相反,在将源操作数的值赋给内存操作数之前,必须先将该数值传送给一个寄存器

书本示例无法直接运行

覆盖值

下述代码示例演示了怎样通过使用不同大小的数据来修改同一个32位寄存器。当oneWord字传送到AX时,它就覆盖了AL中已有的值。当oneDword传送到EAX时,它就覆盖了AX的值。最后。当0被传送到AX时,它就覆盖了EAX的低半部分。

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
onebyte byte 78h
oneword word 1234h
onedword dword 12345678h

.code
main PROC
	mov eax,0
	mov al,onebyte
	mov ax,oneword
	mov eax,onedword
	mov ax,0

	invoke ExitProcess,0
main ENDP
END main

就是al管低2 byte(8位),ax管低4 byte(16位),eax管8 byte(32位)

4.1.5 整数的全零/符号扩展

1.把一个较小的值复制到一个较大的操作数

尽管MOV指令不能直接将较小的操作数复制到较大的操作数中。但是程序员可以想办法解决这个问题。假设要将count(无符号,16位)传送到ECX(32位)。可以先将ECX设置为0,然后将count传送到CX:

.data
count WORD 1
.code
mov ecx,0
nov cx,count

如果对一个有符号整数-16进行同样的操作会发生什么呢?

.data
signedval swORD -16
.code
mov ecx,0
nov cx,oignedVal

ECX中的值(+65520)与-16完全不同。但是,如果先将ECX设置为FFFFFFh,然后再把signedVal复制到CX,那么最后的值就是完全正确的:

(见书本)

本例的有效结果是用源操作数的最高位(1)来填充目的操作数ECX的高16位,这种技术称为符号扩展(signextension)。当然,不能总是假设源操作数的最高位是1,幸证的是。Intel的工程师在设计指令集时已经预见到了这个问题,因此,设置了MOVZX和MOVSX指令来分别处理无符号整数和有符号整数。

2.MOVZX 指令

MOVZX(进行全零扩展并传送)(zero ex),将源操作数复制到目的操作数,并把目的操作数0扩展到16位或32位。该指令只用于无符号整数

3.MOVSX 指令

MOVSX(进行符号扩展并传送)

如果一个十六进制常数的最大有效数字大于7,那么它的最高位等于1。如下例所示,传送到BX的十六进制数值为A69B,因此,数字“A”就意味着最高位是1。(A69B前面的0是一种方便的表示法,用于防止汇编器将常数误认为标识符)

mov bx,0A69Bh
movsx eax,bx    ;EAX=FFFFA69Bh
movsx eDx,bl    ;EDX=FFFFFF9Bh
movsx cx,bl     ;CX=GG9Bh

4.1.6 LAHF和SAHF指令

LAHF(加载状态标志位到AH)指令将EFLAG寄存器的低字节复制到AH。被复制

4.1.7 XCHG指令

XCHG(交换数据)指令交换两个操作数的内容

xchg var1,bx  ;交换16位内存操作数与BX寄存器的内容

如果要交换两个内存操作数,需要用寄存器作为临时容器

mov ax,bx
xchg ax,val2
mov vall,ax

4.1.8 直接-偏移量操作数

arrayb byte 10h,20h,30h,40h,50h
mov a1,arrayB
mov al,[arrayB+1]

以下属于事件案例

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
arrayb byte 10h,20h,30h,40h,50h

.code
main PROC
mov al,arrayB
mov ah,[arrayB+1]

	invoke ExitProcess,0
main ENDP
END main

eax的显示就是2010h

形如arrayB+1一样的表达式通过在变量偏移量上加常数来形成所谓的有效地址。有效地址外面的括号表明,通过解析这个表达式就可以得到该内存地址指示的内容。汇编器并不要求在地址表达式之外加括号,但为了清晰明了,本书还是强烈建议使用括号。 MASM没有内置的有效地址范围检查。在下面的例子中,假设数组arrayB有5个字节,面指令访问的是该数组范围之外的一个内存字节。其结果是一种难以发现的逻辑错误,因此,在检查数组引用时要非常小心

字和双字数组在16位的字数组中,每个数组元素的偏移量比前一个多2个字节。这就是为什么在下面的例子中,数组ArrayW加2才能指向该数组的第二个元素:

.data
arrayw word 100h,200h,300h
.code
mov ax,arrayw ;AX = 100h
mov ax,[arrayW+2] ;AX=200h

同样,如果是双字数组,则第一个元素偏移量加4才能指向第二个元素

4.1.9 示例程序(Moves)

.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+4]

	invoke ExitProcess,0
main ENDP
END main

0是清除,1是置位,有修改就会变红

4.2 加法和减法

乘法和除法在第7章,浮点运算在第12章

4.2.1 INC和DEC

inc(increase)和dex(decrease)指令分别表示寄存器或内存操作数加1减一

.data
myword word 1000h
.code
inc myword
mov bx,myword
dec bx

4.2.2 add指令

add指令将长度相同的源操作数和目的操作数进行相加操作

标志位会变化

4.2.3 sub指令

add指令将长度相同的源操作数和目的操作数进行相剪操作

标志位会变化

4.2.4 NEG指令

neg(非)指令通过把操作数转换为其二进制补码,将操作数符号取反

将目标操按位取反再加1,就可以的到这个数的二进制补码

标志位会变化

4.2.5 执行算术表达式

Rval = -Xval + (Yval - Zval)

等价于

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
Rval sdword ?
Xval sdword 26
Yval sdword 30
ZVal sdword 40

.code
main PROC
mov eax,Xval
neg eax

mov ebx,Yval
sub ebx,Zval

add eax,ebx
mov Rval,eax


	invoke ExitProcess,0
main ENDP
END main

计算器调成Dword才能显示负数

4.2.6 加减法影响的标志位(!)

执行算数运算指令时,常常想要了解结果。它是负数,正数还是零对目的操作数来说,它是太大,还是太小?这些问题的答案有助于发现计算错误,否则可能会导致程序的错误行为。检查算术运算结果使用的是CPU状态标志位的值,同时,这些值还可以触发条件分支指令,即基本的程序逻辑工具。下面是对状态标志位的简要概述:

●进位(CY)标志位意味着无符号整数溢出。比如,如果指令目的操作数为8位,而指令产生的结果大于二进制的111111,那么进位标志位置1。 ●溢出(OV)标志位意味着有符号整数溢出。比如,指令目的操作数为16位,但其产生的负数结果小于十进制的-32768,那么溢出标志位置1。

●零标志位(ZR)意味着操作结果为0。比如,如果两个值相等的操作数相减,则零标志位置1。 ●符号标志位(PL)意味着操作产生的结果为负数。如果目的操作数的最高有效位(MSB)置1,则符号标志位置1

●奇偶标志位(PE)是指,在一条算术或布尔运算指令执行后,立即判断目的操作数最低有效字节中1的个数是否为偶数

●辅助进位标志位置1,意味着目的操作数最低有效字节中位3有进位。要在调试时显示CPU状态标志位,打开Register窗口,右键点击该窗口,并选择Flags。

OV 溢出 UP 方向 EI中断 PL符号 ZR 零 AC 辅助进位

PE 奇偶性 CY 进位

eip寄存器存储着我们cpu要读取指令的地址,没有了它,cpu就无法读取下面的指令(通俗点讲cpu就无法执行。每次相应汇编指令执行完相应的eip值就会增加)

1.无符号数运算:零标志位、进位标志位和辅助进位标志位

加法和进位标志位

加法和进位标志位如果将加法和减法分开考虑,那么进位标志位的操作是最容易解释的。两个无符号整数相加时,进位标志位是目的操作数最高有效位进位的副本。直观地说,如果和数超过了目的操作数的存储大小,就可以认为CF=1。在下面的例子里,ADD指令将进位标志位置1,原因是,相加的和数(100h)超过了AL的大小

图4-3演示了在0FFh上加1时,操作数的位是如何变化的。AL最高有效位的进位复制到进位标志位。

另一方面,如果AX的值为00FFh,则对其进行加1操作后,和数不会超过16位,那么进位标志位清0

但是,如果AX的值为FFFFh,则对其进行加1操作后,AX的高位就会产生进位

mov ax,0FFFFH
add ax,1 ;AX=0000

减法和进位标志位

mov al,1
sub al,2

0001变为FFFF,ac标志位变为1

INC和DEC不会影响进位标志位,在非零操作数上NEG指令总是会将进位标志位置1

辅助进位标志位

辅助进位AC

mov al,oFh
add al,1   ;AC=1

最后一个十六进制位进位了

奇偶标志位

mov al,10001100b
add al,00000010b   ;PF(PE Flag)=1
sub al,10000000b   ;PE=0
2.有符号运算:符号标志位和溢出标志位

符号标志位

有符号数算数操作结果为负数,则符号标志位置1

mov eax,4
sub eax,5  ;PL标志位变为1

溢出标志位

有符号数算数操作结果与目的操作数相比,如果发生上溢或下溢,则溢出标志位为1

mov al,+127
add al,1  ;OV标志位变为1

加法测试

两个正数相加,结果为负数

两个负数相加,结果为正数

两个加数的符号相反,则不可能发生溢出

NEG指令

mov al,-128
neg al  ;EAX没有变,CY标志位变为1

AL储存不了有符号的正128,AL的值不会改变,AL的值置为1

CPU不知道一个算数运算符是有符号的还是无符号的

4.2.7 示例程序(AddSubTest)

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
Rval sdword ?
Xval sdword 26
Yval sdword 30
ZVal sdword 40

.code
main PROC
;INC和DEC
mov ax,100h
inc ax
dec ax

;Rval=-Xval(Yval-Zval)

;零标志符
mov cx,1
sub cx,1
mov ax,0FFFFh
inc ax

;符号标志位
mov cx,0
sub cx,1
mov ax,0FFFFh
inc ax

;进位标志位
mov al,0FFh
add al,1

;溢出标志位
mov al,+127
add al,1
mov al,-128
sub al,1


	invoke ExitProcess,0
main ENDP
END main

4.3 与数据相关的运算符和伪指令

运算符与伪指令不是可执行指令,反之,它们由汇编器进行分析。使用一些汇编语言伪指令可以获取数据地址和大小的信息: OFFSET运算符返回的是一个变量与其所在段起始地址之间的距离(就相当于数组的首元素)。

PTR运算符可以重写操作数默认的大小类型

TYPE运算符返回的是一个操作数或数组中每个元素的大小(按字节计)。

LENGHTOF运算符返回的是数组中元素的个数

SIZEOF运算符返回的是数组初始化时使用的字节数

此外,LABEL伪指令可以用不同的大小类型来重新定义同一个变量。本章的运算符和伪指令只代表MASM支持的一小部分运算符

4.3.1 OFFSET运算符

OFFSET运算符返回数据标号的偏移量。这个偏移量按字节计算,表示的是该数据标号距离数据段起始地址的距离。

.data
bval byte ?
wval word ?
dval dword ?
dval2 dword ?

.code
main PROC
mov esi,offset bval
mov esi,offset wval
mov esi,offset dval
mov esi,offset dval2

还可以用一个变量的偏移量来初始化另一个双字变量,从而有效地创建一个指针。(!)如下例所示,PArray就指向bigArray的起始地址:

.data
bighrray DNORD 500 DUP(?)
pArray DMoRD bigArray

下面的指令把该指针的值加载到ESI中,因此,这个ESI寄存器就可以指向数组的起始地址:

mov esi,pArray

4.3.2 ALIGN运算符

ALIGN伪指令将一个空量对齐到字节边界、字边界,以字边界或段落边界。语法如下

ALIGN bound

Bound可取值有:1、2、4,8、16,当敢值为1时,则下一个变量对齐于1字节边界(默认情况)。当取值为2时,则下一个变量对齐于偶数地址,当此值为4时,则下一个变量地址为4的倍数。当取值为16时,则下个小量地址为16的倍数,哪一个段落的边界为了满足对齐要求,汇编器会在变量前插人一个成第个空字节。为什么要对齐数据?因为,对于存储于偶地址和奇地址的数据来说,CPU处理偶地址数据的速度要快得多

4.3.3 PTR运算符

PTR运算符可以用来重写一个已经被声明过的操作数大小类型

PTR必须与一个标准汇编数据类型一起使用

将较小的值送入较大的目的操作数

.data
wordlist word 5678h,1234h

.code
main PROC
mov eax,dword ptr wordlist ;EAX=12345678

第一个字复制到EAX的低部分,第二字复制到EAX的高部分

4.3.4 TYPE运算符

TYPE运算符返回变量单个元素的大小,大小以字节为单位进行计算

4.3.5 LENGTHOF运算符

LENGTHOF运算符计算数组中的元素个数

myArray byte 10,20,30,40,50
		byte 60,70,80,90,100

length of my arrray 返回值为5

myArray byte 10,20,30,40,50, 
		byte 60,70,80,90,100

添加了逗号以后,length of my arrray返回值为10

4.3.6 SIZEOF运算符

SIZEOF的运算符返回值等于LENGTHOF与TYPE返回值的乘积

跟c语言的sizeof效果一样

4.3.7 LABEL伪指令

LABEL伪指令可以插人一个标号,并定义它的大小属性,但是不为这个标号分配存储空间。LABEL中可以使用所有的标准大小属性,如BYTE、WORD、DWORD、QWORD或TBYTE。LABEL常见的用法是,为数据段中定义的下一个变量提供不同的名称和大小属性。如下例所示,在变量val32前定义了一个变量,名称为val16,属性为WORD

.data
va116 LADRL WORD
val32 DWORD 12345678h
.code
mov ax,val16
mov dx, [val16+2]

vall6与val32共享同一个内存位置LABEL伪指令自身不分配内存

有时需要用两个较小的整数组成一个较大的就收。如下例所示,两个16位变量组成一个32位变量并加载到EAX中:

.data
LongValue LABEL DMORD
val1 WORD 5678h
va12 WORD 1234h
.code
mov eax, LongValue

4.4 间接寻址

直接寻址很少用于数组处理,因为,用常数偏移量来寻址多个数组元素时,直接寻址不实用。反之,会用寄存器作为指针(称为间接寻址)并控制该寄存器的值。如果一个操作数使用的是间接寻址,就称之为间接操作数

ESI储存的就是偏移量

4.4.1 间接操作数

保护模式

任何一个32位通用寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP和 ESP)加上括号就能构成一个间接操作数。寄存器中存放的是数据的地址。示例如下,ESI存放的是byteVal的偏移量,MOV指令使用间接操作数作为源操作数,解析ESI中的偏移量,并将一个字节送入AL:

.data
byteVal BYTE 10h

.code
main PROC
mov esi,oFFsET byteval
mov al,[esi] ;访问的是地址,AL=10h
PTR与间接操作数一起使用

如果目的操作数也是间接操作数,那么新值将存入由寄存器提供地址的内存位置(!)

4.4.2 数组

间接操作数是步进遍历数组的理想工具。下例中,arrayB有3个字节,随着ESI不断加1,它就能顺序指向每一个字节

.data
arrayb byte 10h,20h,30h

.code
main PROC
mov esi,oFFsET arrayb
mov al,[esi]
inc esi
mov al,[esi]
inc esi
mov al,[esi]

实现了步进!

示例:32位整数相加

.code
main PROC
mov esi,oFFsET arrayd
mov eax,[esi]
add esi,4
add eax,[esi]
add esi,4
add eax,[esi]

4.4.3 变址操作数

变址操作数是指,在寄存器(而不是变量)上加上常数产生一个有效地址。每个32位通用寄存器都可以用作变址寄存器。MASM可以用不同的符号来表示变址操作数(括号是表示符号的一部分),下面给出的是两种符号形式的例子:

arrayB[esi][arrayB+esi]
arrayD[esi][arrayD+esi]

第一种形式是变量名加上寄存器。变量名由汇编器转换为常数,代表的是该变量的偏移量。数组元素之前,变址寄存器需要初始化为0:

.data
arrayB BYTE 10h,20h,30h
.code
mov esi,1
mov al,arrayB[esi]

byte+1,即20h会进到al里头

增加位移量变址寻址的第二种形式是寄存器加上常数偏移量:变址寄存器保存数组或结构的基址

.data
arrayw word 1000h,2000h,3000h

.code
main PROC
mov esi,offset arrayw
mov ax,[esi]
mov ax,[esi+2]
mov ax,[esi+4]

变址操作数的比例因子

就是用TYPE,使步进空间变得灵活

.data
arrayd dword 1000h,2000h,3000h

.code
main PROC
mov esi,2*type arrayd
mov eax,arrayd[esi]

INTEL的人员想出了比例因子这种东西。比例因子是数组元素的大小(字=2,双字=4,四字=8)

.data
arrayd dword 1000h,2000h,3000h

.code
main PROC
mov esi,2
mov eax,arrayd[esi*type arrayd]

他们认为这样让变换地址更加灵活

4.4.4 指针

如果一个变量包含另一个变量的地址,则该变量称为指针。指针是控制数组和数据结构的重要工具,因为,它包含的地址在运行时是可以修改的。比如,可以使用系统调用来分配(保留)一个内存块,再把这个块的地址保存在一个变量中。指针的大小受处理器当前模式(32位或64位)的影响。下例为32位的代码,ptrB包含了arrayB的偏移量:

.data
arrayB byte 10h,20h,30h,40h
ptrB dword arrayB
; 等价于 ptrB dword offset arrayB

本书中的32位模式程序使用的是近指针,因此,它们保存在双字变量中

使用TYPEDEF运算符

typedef运算符可以创建用户定义类型,这些类型包含了定义变量时内置类型的所有状态。创建指针的理想工具

下面声明创建一个新的数据类型就是一个字节指针

pbyte typedef ptr byte

这个声明放在靠近程序开始的地方,在数据段之前,然后,变量就可以用pbyte来定义

.data
arrayb byte 10h,20h,30h,40h
ptr1 pbyte ?
ptr2 pbyte arrayB

示例程序Pointers

指针定义放在.data前面,然后在.data中定义变量,加ptr是因为指针地址是32位的

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

pbyte typedef ptr byte ;字节指针
pword typedef ptr word
pdword typedef ptr dword
.data
arrayb byte 10h,20h,30h
arrayw word 1,2,3
arrayd dword 4,5,6
ptr1 pbyte arrayb
ptr2 pword arrayw
ptr3 pdword arrayd

.code
main PROC
	mov esi,ptr1
	mov al,[esi]
	mov esi,ptr2
	mov ax,[esi]
	mov esi,ptr3
	mov eax,[esi+8] ;6,因为一个Dword是4

	invoke ExitProcess,0
main ENDP
END main

4.4.5 本节回顾

任何一个32位通用寄存器都可以用作间接操作数

至少EBX可以,但默认是esi

4.5 JMP和LOOP指令

无条件转移:无论什么情况都会转移到新地址。新地址加载到指令指针寄存器,使得程序在新地址进行执行。JMP指令实现这种转移。

条件转移:满足某种条件,则程序出现分支。各种条件转移指令还可以组合起来,形成条件逻辑结构。CPU基于ECX和标志寄存器的内容来解释真/假条件。

4.5.1 JMP指令

JMP指令无条件跳转到目标地址,该地址用代码标号来标识,并被汇编器转换为偏移量。语法如下所示:

JMP destination

创建一个情环JMP指令提供了一种简单的方法来创建循环,即跳转到循环开始时的标号:

top:
.
.
jmp top;不断地循环

JMP是无条件的,因此循环会无休止地进行下去,除非找到其他方法退出循环。

明明可以举一个跳转到别处的例子,为什么要自己跳自己?

4.5.2 LOOP指令

LOOP指令,正式称为按照ECX计数器循环,将程序块重复特定次数。ECX自动成为计数器,每循环一次计数值减1。语法如下所示:

LooP deatination

循环目标,必须距离当前地址计数器-128到+127字节范围内。LOOP指令的执行有两个步骤:第一步,ECX减1,第二步,将ECX与0比较。如果ECX不等于0,则跳转到由目标给出出的标号。否则,如果ECX等于0,则不发生跳转,并将控制传递到着环后面的指令。

实地址模式下,CX是LOOP指令的默认循环计数器,同时,LOOPD指令使用ECX为循环计数器,LOOPW指令使用CX为循环计数器

**不能将ECX初始化为0!!**不然循环次数会非常大(-1后跳到65536),编译器会报错

.code
main PROC
	mov ax,0
	mov ecx,5
L1:
	inc ax
	loop L1

如果必须要修改ECX,那最好将ECX先用变量储存起来

涉及到循环嵌套

对于一般规则,多余两重的循环嵌套难以编写,需要多重循环的话,则需要用子程序实现

4.5.3 在Visual Studio调试器中显示数组

Debug->Memory->Memory

在内存窗口上端的Address栏里,键入符号和数组名称,然后ENTER

例如&arrayb

右键有更多选项

4.5.4 整数数组求和

在刚开始编程时,几乎没有任务比计算数组元素总和更常见了。汇编语言实现数组求和步骤如下: 1)指定一个寄存器作变址操作数,存放数组地址 2)循环计数器初始化为数组的长度。 3)指定一个寄存器存放累积和数,并赋值为0. 4)创建标号来标记循环开始的地方。 5)在循环体内,将和数与一个数组元素相加。 6)指向下一个数组元素。 7)用LOOP指令重复循环 步骤1到步骤3可以按照任何顺序执行。下面的短程序实现对一个16位整数数组求和。

.data
intarray dword 10000h,20000h,30000h,40000h

.code
main PROC
	mov edi,offset intarray
	mov ecx,lengthof intarray
	mov eax,0
L1:
	add eax,[edi]
	add edi,type intarray
	loop L1

4.5.5 复制字符串

在汇编语言里,用循环来复制一个字符串,而字符串表示为带有一个空终止值的字节数组

从一个内存空间到另外一个内存空间

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
source byte "This is the source string",0
target byte sizeof source DUP(0)

.code
main PROC
	mov esi,0
	mov ecx,sizeof source
L1:
	mov al,source[esi]
	mov target[esi],al
	inc esi
	loop L1

	invoke ExitProcess,0
main ENDP
END main

4.6 64位编程

(放在以后)

内存数据格式由dword变qword

5 过程

两个代码库,Irvine32和Irvine64 包含有用工具进行简化输入输出

5.1 堆栈操作

堆栈是FILO,后进先出

5.1.1 运行时堆栈(32位模式)

直接由CPU的硬件支持,是过程调用与返回的基本机制

运行时堆栈是内存数组,CPU用ESP(扩展堆栈指针,extended stack pointer)寄存器对其进行直接管理,该寄存器被称为堆栈指针寄存器(stack pointer register)。32位模式下,ESP寄存器存放的是堆栈中某个位置的32位偏移量。ESP基本上不会直接被程序员控制,反之,它是用CALL、RET、PUSH和POP等指令间接进行修改。

1.入栈操作

32位入栈操作把栈顶指针减4,再将数值复制到栈顶指针指向的堆栈位置。图5-3展示了把000000A5压入堆栈的结果,堆栈中已经有一个数值(00000006)。注意,**ESP寄存器总是指向添加,或压入到栈顶的最后一个数值。图中显示的堆栈顺序与之前示例给出的盘堆栈顺序相反,这是因为运行时堆栈在内存中是向下生长的即从高地址向低地址扩展。

2.出栈操作

出栈操作从堆栈删除数据。数值弹出堆栈后,栈顶指针增加(按堆栈元素大小)

ESP之下的堆栈域在逻辑上是空白的,当前程序下一次执行任何数值入栈操作指令都可以覆盖这个区域。(!)

3.堆栈应用

运行时堆栈在程序中有一些重要用途: 当寄存器用于多个目的时,堆栈可以作为寄存器的一个方便的临时保存区。在寄存器被修改后,还可以恢复其初始值。 执行CALL指令时,CPU在堆栈中保存当前过程的返回地址。 调用过程时,输入数值也被称为参数,通过将其压入堆栈实现参数传递。

​ 堆栈也为过程局部变量提供了临时存储区域。

5.1.2 PUSH和POP指令

1.PUSH指令

PUSH指令首先减少ESP的值,再将源操作数复制到堆栈。操作数是16位的,则ESP减2,操作数是32位的,则ESP减4。

2.POP指令

POP指令首先把ESP指向的堆栈元素内容复制到一个16位或32位目的操作数中,再增加ESP的值。如果操作数是16位的,ESP加2,如果操作数是32位的,ESP加4

3.PUSHFD和POPFD指令

PUSHFD指令把32位EFLAGS寄存器内容压入堆栈,而POPFD指令则把栈顶单元内容弹出到EFLAGS寄存器

不能用MOV指令把标识寄存器内容复制给一个变量(?),因此,PUSHFD可能就是保存标志位的最佳途径。有些时候保存标志寄存器的副本是非常有用的,这样之后就可以恢复标志寄存器原来的值。

当用这种方式使用入栈和出栈指令时,必须确保程序的执行路径不会跳过POPFD指令。当程序随着时间不断修改时,很难记住所有入栈和出栈指令的位置。因此,精确的文档就显得至关重要! 一种不容易出错的保存和恢复标识寄存器的方法是:将它们压入堆栈后,立即弹出给一个变量:

.data 
saveFlags DWORD ?
.code
pushfd ;标识寄存器内容入栈
pop saveFlags;复制给一个变量

下述语句从同一个变量中恢复标识寄存器内容

push saveFlags ;被保存的标识入栈
popfd ;复制给标识寄存器
4.PUSHAD,PUSHA,POPAD,POPA

PUSHAD指令按照EAX、ECX、EDX、EBX、ESP(执行PUSHAD之前的值)、EBP、ESI和EDI的顺序,将所有32位通用寄存器压入堆栈

POPAD指令按照相反顺序将同样的寄存器弹出堆栈

PUSHA指令按序(AX、CX、DX、BX、SP、BP、SI和DI)将16位通用寄存器压入堆栈。POPA指令按照相反顺序将同样的寄存器弹出堆栈

必须要指出,上述示例有一个重要的例外:过程用一个或多个寄存器来返回结果时,不应使用PUSHA和PUSHAD

示例:字符串反转

现在查看名为RevStr的程序:在一个字符串上循环,将每个字符压入堆栈,再把这些字符从堆栈中弹出(相反顺序),并保存回同一个字符串变量。由于堆栈是LIFO(后进先出)结构,字符串中的字母顺序就发生了翻转

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
aname byte "Mocus EZ",0
namesize = ($-aname)-1

.code
main PROC
	mov ecx,namesize
	mov esi,0
L1:
	movzx eax,aname[esi]
	push eax
	inc esi
	loop L1

	mov ecx,namesize
	mov esi,0

L2:
	pop eax
	mov aname[esi],al
	inc esi
	LOOP L2

	invoke ExitProcess,0
main ENDP
END main

5.2 定义并使用过程

如果读者已经学过了高级编程语言,那么就会知道将程序分割为子过程(subroutine)是多么有用。一个复杂的问题常常要分解为相互独立的任务,这样才易于被理解、实现以及有效地测试。在汇编语言中,通常用术语过程(procedure)来指代子程序。在其他语言中,子程序也被称为方法或函数。 就面向对象编程而言,单个类中的函数或方法大致相当于封装在一个汇编语言模块中的过程和数据集合。汇编语言出现的时间远早于面向对象编程,因此它不具备面向对象编程中的形式化结构。汇编程序员必须在程序中实现自己的形式化结构。(啊这)

5.2.1 PROC伪指令

1.定义过程

过程可以非正式地定义为:由返回语句结束的已命名的语句块。过程用PROC和ENDP伪指令来定义,并且必须为其分配一个名字(有效标识符)。到目前为止,所有编写的程序都包含了一个名为main的过程

当在程序启动过程之外创建一个过程时,就用RET指令来结束它RET强制CPU返回到该过程被调用的位置(??)

2.过程中的标号

默认情况下,标号只在其被定义的过程中可见。这个规则常常影响到跳转和循环指令。在下面的例子中,名为Destination的标号必须与JMP指令位于同一个过程中: jmp Destination解决这个限制的方法是定义全局标号,即在名字后面加双冒号(::):

    Destination::

​ 就程序设计而言,跳转或循环到当前过程之外不是个好主意。过程用自动方式返回并调整运行时堆栈。如果直接跳出一个过程,则运行时堆栈很容易被损坏。(这是什么东西?)

用高级语言,如C和C++,编写的函数,通常用AL返回8位的值,用AX返回16位的值,用EAX返回32位的值。

5.2.2 CALL和RET指令

CALL指令调用一个过程,指挥处理器从新的内存地址开始执行。过程使用RET(从过程返回)指令将处理器转回到该过程被调用的程序点上。从物理上来说,CALL指令将其返回地址压入堆栈,再把被调用过程的地址复制到指令指针寄存器。当过程准备返回时,它的RET指令从堆栈把返回地址弹回到指令指针寄存器。32位模式下,CPU执行的指令由EIP(指令指针寄存器)在内存中指出。16位模式下,由IP指出指令。

调用和返回示例

假设在main过程中,CALL指令位于偏移量为00000020处。通常,这条指令需要5个字节的机器码,因此,下一条语句(本例中为一条MOV指令)就位于偏移量为00000025处:

MySub的地址加载到EIP。执行MySub中的全部指令直到RET指令。当执行RET指令时,ESP指向的堆栈数值被弹出到EIP。在步骤2中,ESP的数值增加,从而指向堆栈中的前一个值(步骤2)。

当CALL指令执行时,调用之后的地址(00000025)被压入堆栈,MySub的地址加载到EIP。执行MySub中的全部指令直到RET指令。当执行RET指令时,ESP指向的堆栈数值被弹出到EIP。在步骤2中,ESP的数值增加,从而指向堆栈中的前一个值(步骤2)。

下面这个做了一个示范样例,不涉及ESP和ESP的直接调用

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
aname byte "Mocus EZ",0
namesize = ($-aname)-1

.code
sumof proc
	add eax,ebx
	add eax,ecx
	ret
sumof endp


main PROC
	mov eax,5
	mov ebx,3
	mov ecx,6
	call sumof

	invoke ExitProcess,0
main ENDP
END main

5.2.3 过程调用嵌套

书本图太多,类比C语言里的函数调用,ESP和堆栈的弹入和弹出

一般说来,堆栈结构用于程序需要按照特定顺序返回的情况。

5.2.4 向过程传递寄存器参数

向过程传递数组的偏移量以及指定数组元素个数的整数。这些内容被称为参数(或输入参数)。在汇编语言中,经常用通用寄存器来传递参数。 在前面的章节中创建了一个简单的过程SumOf,计算EAX、EBX和ECX中的整数之和。在main调用SumOf之前,将数值分配给EAX、EBX和ECX

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
sum dword ?

.code
sumof proc
	add eax,ebx
	add eax,ecx
	ret
sumof endp


main PROC
	mov eax,5
	mov ebx,3
	mov ecx,6
	call sumof
	mov sum,eax

	invoke ExitProcess,0
main ENDP
END main

5.2.5 示例:整数数组求和

下面这个案例可通用

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
array dword 10000h,20000h,30000h,40000h,50000h
thesum dword ?

.code
arraysum proc
	push esi
	push ecx
	mov eax,0
L1:
	add eax,[esi]
	add esi,type dword
	loop L1
	pop ecx
	pop esi
	ret
arraysum endp


main PROC
	mov esi,offset array
	mov ecx,lengthof array 
	;所以为什么说lengthof有用的原因
	;当时写了一个错的add esi,type dword
	call arraysum
	mov thesum,eax

	invoke ExitProcess,0
main ENDP
END main

5.2.6 保存和恢复寄存器

在ArraySum示例中,ECX和ESI在过程开始时被压入堆栈,在过程结束时被弹出堆栈。这是大多数过程修改寄存器的典型操作。总是保存和恢复被过程修改的寄存器,将使得调用程序确保自己的寄存器值不会被覆盖。

USES运算符USES运算符与PROC伪指令一起使用,让程序员列出在该过程中修改的所有寄存器名。USES告诉汇编器做两件事情:

第一,在过程开始时生成PUSH指令,将寄存器保存到堆栈;

第二,在过程结束时生成POP指令,从堆栈恢复寄存器的值。

USES运算符紧跟在PROC之后,其后是位于同一行上的寄存器列表,表项之间用空格符或制表符(不是逗号)分隔。(妙啊!)

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
array dword 10000h,20000h,30000h,40000h,50000h
thesum dword ?

.code
arraysum proc uses esi ecx
	mov eax,0
L1:
	add eax,[esi]
	add esi,type dword
	loop L1
	ret
arraysum endp


main PROC
	mov esi,offset array
	mov ecx,lengthof array 
	call arraysum
	mov thesum,eax

	invoke ExitProcess,0
main ENDP
END main

节省掉push和pop

例外情况!!!

当寄存器要返回数值时,就不能这么做(比如EAX要带着运算结果会main,汇编没用形式参数这一说)

5.3 链接到外部库

使用Irvine32链接库,简化控制台应用编写

Irvine32链接库只能用于32位模式下运行的程序。它包含了链接到MS-Windows API的过程,生成输入输出。对64位应用程序来说,Irvine64链接库的限制更多,它仅限于基本显示和字符串操作。

5.3.1 背景知识

当程序进行汇编时,汇编器将不指定CALL指令的目标地址,它知道这个地址将由链接器指定。链接器在链接库中寻找WriteString,并把库中适当的机器指令复制到程序的可执行文件中。同时,它把WriteString的地址插入到CALL指令。如果被调用过程不在链接库中,链接器就发出错误信息,且不会生成可执行文件。

链接命令选项

链接器工具把一个程序的目标文件与一个或多个目标文件以及链接库组合在一起。

32位程序链接kernel32.lib文件是Microsoft Windows平台软件开发工具(SoftwareDevelopment Kit)的一部分,它包含了kernel32.dll文件中系统函数的链接信息。kernel32.dll文件是MS-Windows的一个基本组成部分,被称为动态链接库(dynamic link library)。它含有的可执行函数实现基于字符的输入输出。

链接到在第1章到第10章中,程序都链接到Irvine32.lib或可以链接到者Irvine64.obj。第11章说明了如何将程序直接链接到kermel32.lib执行kernel32.lib。

5.4 Irvine32 链接库

5.4.1 创建库的动机

注意,数字是按照逆序生成,插入缓冲区,从后往前移动。然后,数字按照正序写到控制台。虽然这段代码简单到足以用C/C++实现,但是如果是在汇编语言中,它还需要一些高级技巧。

电子PDF142页,有详细过程

5.4.2 概述控制台窗口

控制台窗口(console window)(或命令窗口command window)是显示命令提示符时,由MS-Windows生成的一个纯文本窗口。

文件句柄(file handle)是一个32位整数,Windows操作系统用它来标识当前打开的文件。当用户程序调用一个Windows服务来打开或创建文件时,操作系统就创建一个新的文件句柄,并使其对用户程序可用。每当程序调用OS服务方法来读写该文件时,就必须将这个文件句柄作为参数传递给服务方法

**注意:**如果用户程序调用Irvine32链接库中的过程,就必须总是将这个32位数值压入运行时堆栈;如果不这样做,被库调用的Win32控制台函数就不能正常工作。(!)

5.4.3 过程详细说明

CloseFile

CloseFile 过程关闭之前已经创建或打开的文件(参见CreateOutputFile和OpenInputFile)。该文件用一个32位整数的句柄来标识,句柄由EAX传递。如果文件成功关闭,EAX中的返回值就是非零的

(电子PDF145页开始,到154页,太多了,不写了)

5.4.4 库测试程序

这些库是书作者写的,难道微软官方就不给库嘛?

测试1

写一个程序,演示用屏幕颜色输入/输出整数

(居然不是标准格式,那些.386 flat全部没有)

; Library Test #1: Integer I/O   (TestLib1.asm)

; Tests the Clrscr, Crlf, DumpMem, ReadInt, 
; SetTextColor, WaitMsg, WriteBin, WriteHex, 
; and WriteString procedures.

INCLUDE Irvine32.inc
.data
arrayD     DWORD 1000h,2000h,3000h
prompt1    BYTE "Enter a 32-bit signed integer: ",0
dwordVal   DWORD ?

.code
main PROC
; Set text color to yellow text on blue background:
	mov	eax,yellow + (blue * 16)
	call	SetTextColor
	call	Clrscr			; clear the screen

; Display the array using DumpMem.
	mov	esi,OFFSET arrayD	; starting OFFSET
	mov	ecx,LENGTHOF arrayD	; number of units in dwordVal
	mov	ebx,TYPE arrayD	; size of a doubleword
	call	DumpMem			; display memory
	call	Crlf				; new line

; Ask the user to input a signed decimal integer.
	mov	edx,OFFSET prompt1
	call	WriteString
	call	ReadInt			; input the integer
	mov	dwordVal,eax		; save in a variable

; Display the integer in decimal, hexadecimal, and binary.
	call	Crlf				; new line
	call	WriteInt			; display in signed decimal
	call	Crlf
	call	WriteHex			; display in hexadecimal
	call	Crlf
	call	WriteBin			; display in binary
	call	Crlf
	call	WaitMsg			; "Press any key..."

; Return console window to default colors.
	mov	eax,lightGray + (black * 16)
	call	SetTextColor
	call	Clrscr
	exit
main ENDP
END main
测试3
; This program adds and subtracts 32-bit integers
; and stores the sum in a variable.
 
INCLUDE Irvine32.inc
 
.data
outer=3
start dword ?
msg1 byte "Please wait",0dh,0ah,0
msg2 byte "milliseconds:"

.code
main PROC
    mov edx,offset msg1
    call writestring

    call getmseconds
    mov start,eax

    mov ecx,outer
L1: call innerloop
    loop L1

    call getmseconds
    sub eax,start

    mov edx,offset msg2
    call writestring
    call writedec
    call crlf
    exit
main ENDP

innerloop proc
    push ecx
    mov ecx,0FFFFFFFh
L1: mul eax
    mul eax
    mul eax
    loop L1
    pop ecx
    ret
innerloop ENDP
END main

5.5 64位汇编编程

(未完待续)

6 条件处理

使用本章的工具实现理论计算机科学最根本的结构之一:有限状态机

6.1 条件分支

高级语言的if和swit

6.2 布尔和比较指令

XOR 异或操作

NOT 非操作

TEST 源操作数和目的操作数

6.2.1 CPU状态标志

布尔指令影响零标志位、进位标志位、符号标志位、溢出标志位和奇偶标志位。下面简单回顾一下这些标志位的含义: ·操作结果等于0时,零标志位置1。 ·操作使得目标操作数的最高位有进位时,进位标志位置1。 ·符号标志位是目标操作数高位的副本,如果标志位置1,表示是负数;标志位清0,表示是正数。(假设0为正。)

·指令产生的结果超出了有符号目的操作数范围时,溢出标志位置1。 ·指令使得目标操作数低字节中有偶数个1时,奇偶标志位置1。

6.2.2 AND指令符

AND指令可以清除一个操作数中的1个位或多个位,同时又不影响其他位。这个技术就称为位屏蔽,就像在粉刷房子时,用遮盖胶带把不用粉刷的地方(如窗户)盖起来。例如,假设要将一个控制字节从AL寄存器复制到硬件设备。并且当控制字节的位0和位3等于0时,该设备复位。

将字符转换为大写

AND指令提供了一种简单的方法将字符从小写转换为大写。如果对比大写A和小写a的ASCIⅡ码,就会发现只有位5不同

测试样例(陷入死循环)

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
array byte 50 DUP(?)

.code
main PROC
	mov ecx,lengthof array
	mov esi,offset array
	L1:
	and byte ptr [esi],11011111b
	inc esi
	loop L1

	invoke ExitProcess,0
main ENDP
END main

6.2.3 OR指令

标志位OR指令总是清除进位和溢出标志位,并根据目标操作数的值来修改符号标志位、零标志位和奇偶标志位。比如,可以将一个数与它自身(或0)进行OR运算,来获取该数值的某些信息

(见电子PDF172页,纸质151页)

6.2.4 位映射集

应用可以用位向量(或位映射)把一个二进制数中的位映射为数组中的对象

补集

可以用NOT指令生成

mov eax,SetX
not eax
交集

AND指令可以生成位向量来表示两个集合的交集

mov eax,SetX
and eax,SetY

很难想象还有更快捷的方法生成交集。对于更大的集合来说,它所需要的位超过了单个寄存器的容量,因此,需要用循环来实现所有位的AND运算。

并集

OR指令生成位图表示两个集合的并集

6.2.5 XOR指令

同0输出1 同1输出0 相异输出1

两次XOR,输出为原有值

检查奇偶标志位

一个既能检查数的奇偶性,又不会修改其数值的有效方法是,将该数与0进行异或运算:

mov al,10110101b;5个1,奇校验
xor al,0;奇偶标志位清0(奇)
mov al,11001100b;4个1,偶校验
xor al,0;奇偶标志位置1(偶)

Visual Studio用PE=1表示偶校验,PE=0表示奇校验

16为奇偶性以此类推

对16位整数来说,可以通过将其高字节和低字节进行异或运算来检测数的奇偶性:

mov ax,64C1h ;0110010011000001
xor ah,al;奇偶标志位置1(偶)

奇偶标志位置1(偶)将每个寄存器中的置1位(等于1的位)想象为一个8位集合中的成员。XOR指令把两个集合交集中的成员清0,并形成了其余位的并集。这个并集的奇偶性与整个16位整数的奇偶性相同。

6.2.6 NOT 指令

可翻转操作数的所有为,结果被成为反码

6.2.7 TEST指令

TEST指令在两个操作数的对应位之间进行AND操作,并根据运算结果设置符号标志位、零标志位和奇偶标志位。

TEST指令与AND指令唯一不同的地方是,TEST指令不修改目标操作数。TEST指令允许的操作数组合与AND指令相同。

在发现操作数中单个位是否置位时,TEST指令非常有用。

多位测试

这个例子里,提到了位掩码

6.2.8 CMP指令

X86汇编语言用CMP指令比较整数。字符代码也是整数,因此可以用CMP指令。浮点数需要特殊的比较指令,相关内容将在第12章介绍。 CMP(比较)指令执行从目的操作数中减去源操作数的隐含减法操作,并且不修改任何操作数

无符号数和有符号数在该指令下标志位结果不同

6.2.9 置位和清除单个CPU标志位

涉及细节操作,看电子书的P176页,纸质的P155

6.2.10 64位模式下的布尔指令

6.3 条件跳转

6.3.1 条件结构

执行一个条件语句需要两个步骤:

第一步,用CMP、AND或SUB操作来修改CPU状态标志位

第二步,用条件跳转指令来测试标志位,并产生一个到新地址的分支。

即:根据状态标志位决定跳转

6.3.2 Jcond指令

cond是指确定一个或多个标志位状态的标志位条件。下面是基于进位和零标志位的例子

jz,为0跳转(零标志位1)

jnz,非0跳转(零标志位清0)

jc,进位跳转(进位标志位1)

jnc,无进位跳转(进位标志位清0)

cmp eax,5
je L1;如果相等则跳转(euqall)
jl L1;小于则跳转
jg L1;大于则跳转

6.3.3 条件跳转指令类型

image-20210208170631447

image-20210208170654346

image-20210208171125093

image-20210208171207038

6.3.4 条件跳转应用

测试状态位
两个数中的较大数
三个数的最小数

6.4 条件循环指令

6.4.1 LOOPZ和LOOPE指令

LOOPZ(为0跳转)

LOOPE(相等跳转)

6.4.2 LOOPNZ和LOOPNE指令

LOOPZ(非0跳转)

LOOPE(不等跳转)

这里会用到pushfd和popfd,因为add可能修改标志位

6.5 条件结构

该部分讨论高级语言的条件结构如何转化为低级语言

6.5.1 块结构的IF语句

相同的高级语言在汇编语言中的实现不一定相同

方案1是条件跳转+CMP指令

方案2是JE实现==(没有方案1来的紧凑)

白盒测试

复杂条件语句可能有多个执行路径,这使得它们难以进行调试检查(查看代码)。程序员经常使用的技术称为白盒测试,用来验证子程序的输入和相应的输出。白盒测试需要源代码,并对输入变量进行不同的赋值。对每个输入组合,要手动跟踪源代码,验证其执行路径和子程序产生的输出(主要面对大量if-else嵌套)

6.5.2 复合表达式

从电子PDF P188页开始

6.5.3 while循环

6.5.4 表驱动选择

用查表替代多路选择的方法(即switch case)

表驱动选择有一些初始化开销,但是它能减少编写的代码总量。一个表就可以处理大量的比较,并且与一长串的比较、跳转和CALL指令序列相比,它更加容易修改。甚至在运行时,表还可以重新配置

6.6 应用:有限状态机(FSM)

有限状态机(FSM)指一种被称为有向图的更一般结构特例

6.6.1 验证输入字符串

相当多的细节,从电子PDF193开始

6.6.2验证有符号整数

6.7 条件控制流伪指令

32位模式下,MASM包含了一些高级条件控制流伪指令(conditional control flowdirectives),这有助于简化编写条件语句。遗憾的是,这些伪指令不能用于64位模式(简化汇编的开发流程)

image-20210208180321748

image-20210208180341845

6.7.1 新建IF语句

在调试的时候,在伪指令上右键到diassembly,就可以看到伪代码实现过程

6.7.2 有符号数和无符号数的比较

基于伪代码的汇编

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
array dword 5
result dword ?

.code
main PROC
	mov eax,6
	.if eax>val1
	mov result,1
	.endif

	invoke ExitProcess,0
main ENDP
END main

汇编器用JBE(无符号跳转)指令对其进行扩展

有符号比较

汇编器用JLE指令生成代码,即基于有符号比较的跳转

寄存器比较

下面生成的代码表示汇编器将其默认为无符号数比较(注意使用的是JBE指令):

6.7.3 复合表达式

6.7.4 用.REPEAT和.WHILE创建循环

repeat对应while……if

.repeat
	statements
.until condition
.while condition
	statement
.ENDW

6.8 本章小结

TEST指令对目的操作数执行隐含的AND操作,并正确地设置标志位。目的操作数不变。 NOT指令将目的操作数的每一位取反。

表6-4列出了基于无符号数比较的条件跳转,例如:JA(大于则跳转)、JB(小于则跳转)和JAE(大于等于则跳转)。

32位模式下,若零标志位等于1,且ECX大于零,则LOOPZ(LOOPE)指令重复循环。
若零标志位等于0,且ECX大于零,则LOOPNZ(LOOPNE)指令重复循环。在64位模式下,LOOPZ和LOOPNZ指令使用的是RCX寄存器。

流程图是用视图展示程序逻辑的一种有效工具。利用流程图作模型,可以很容易地编写汇编语言代码。给流程图中每一个符号都赋予一个标号,并在汇编源代码中使用同样的标号是很有帮助的。

7 整数运算

本章将介绍汇编语言最大的优势之一:基本的二进制移位和循环移位技术。

7.1 移位和循环移位指令

bit shifting(位移动)

image-20210208183955337

这些指令会影响溢出标志位进位标志位

左移和算数左移为什么不一样?

7.1.1 逻辑以为和算数移位

第一种是逻辑移位(logic shift),空出来的位用0填充

第二种是算术移位(arithmetic shift),空出来的位用原数据的符号位填充

7.1.2 SHL指令

SHL(左移)指令使目的操作数逻辑左移一位,最低位用0填充

位元乘法

7.1.3 SHR指令

位元除法

7.1.4 SAL和SAR指令

SAL(算术左移)指令的操作与SHL指令一样。每次移动时,SAL都将目的操作数中的每一位移动到下一个最高位上。最低位用0填充;最高位移入进位标志位,该标志位原来的值被丢弃

适用于有符号数除法

AX符号扩展到EAX

设AX中为有符号数,现将其符号位扩展到EAX。首先把EAX左移16位,再将其算术右移16位

7.1.5 ROL指令

以循环方式来移位即为位元循环(Bitwise Rotation)。一些操作中,从数的一端移出的位立即复制到该数的另一端。还有一种类型则是把进位标志位当作移动位的中间点。

位循环不会丢弃位。从数的一端循环出去的位会出现在该数的另一端。

进位标志位保存的是最后循环移出MSB的位

7.1.6 ROR指令

进位标志位保存的是最后循环移出LSB的位

7.1.7 RCL和RCR指令

RCL(带进位循环左移)指令把每一位都向左移,进位标志位复制到LSB,而MSB复制到进位标志位

RCR指令RCR(带进位循环右移)指令把每一位都向右移,进位标志位复制到MSB而LSB复制到进位标志位

7.1.8 有符号数溢出

如果有符号数循环移动一位生成的结果超过了目的操作数的有符号数范围,则溢出标志位置1。换句话说,即该数的符号位取反

7.1.9 SHLD/SHRD 指令

SHLD(双精度左移)指令将目的操作数向左移动指定位数。移动形成的空位由**源操作数(?)**的高位填充。源操作数不变,但是符号标志位、零标志位、辅助进位标志位、奇偶标志位和进位标志位会受影响

还没有熟练掌握

7.2 移位和循环的应用

7.2.1 多个双字的移位

小端顺序:将数组的最低字节存放到它的起始地址

小端字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处

具体案例放下面

http://www.cppblog.com/aaxron/archive/2011/02/28/140786.html

#include <stdio.h>
#include <assert.h>

int main(void)
{
        short test;
        FILE* fp;
         
        test = 0x3132; /* (31ASIIC码的'1', 32ASIIC码的'2') */
        if ((fp = fopen("c:\\test.txt", "wb")) == NULL)
              assert(0);
        fwrite(&test, sizeof(short), 1, fp);
        fclose(fp);
        return 0;
}

而书本给的操作没有看懂,感觉很迷

7.2.2 二进制乘法

快速幂

部分情况下代替MUL

7.2.3 显示二进制位

将二进制整数转换为ASCIⅡ码的位串,并显示出来是一种常见的编程任务。SHL指令适用于这个要求,因为每次操作数左移时,它都会把操作数的最高位复制到进位标志位

运行时报错(2021.2.8)

(先保留,留到以后微机接口看)

7.2.4 提取文件日期字段

位0位4表示的是131内的日期;位5位8表示的是月份;位9~位15表示的是年份

在这种情况下,可以通过位遮蔽和右移得到想要的数据

7.3 乘法和除法指令

7.3.1 MUL指令

MUL无符号数乘法,可执行8,16,32位的乘法

默认被乘数是AL,AX,EAX

所以MUL的参数只有一个

乘数和被乘数的大小必须保持一致,乘积的大小则是它们的一倍

如果出现溢出,16位的高位会放在DX,32位的高位会放在EDX,64位会在RDX

7.3.2 IMUL指令

IMUL(有符号数乘法)

该指令支持单操作数,双操作数,三操作数

单操作数类似MUL

双操作数格式会按照目的操作数的大小来截取乘积

因此,在执行了有两个操作数的IMUL操作后,必须检查这些标志位(进位或溢出)中的一个

三操作数格式

32位模式下的三操作数格式将乘积保存在第一个操作数中。第二个操作数可以是16位寄存器或内存操作数,它与第三个操作数相乘,该操作数是一个8位或16位立即数

双操作数和三操作数IMUL指令的目的操作数大小与乘数大小相同。因此,有可能发生有符号溢出。执行这些类型的IMUL指令后,总要检查溢出标志位

7.3.3 测量程序运行的时间

Microsoft WindowsAPI为此提供了必要的工具,Irvine32库中的GetMseconds过程可使其变得更加方便使用

处理器可以通过位移法加快MUL计算

7.3.4 DIV指令

DIV(无符号除法)

商放在低位(AL),余数放在高位(AH)

7.3.5 有符号数除法

有符号除法几乎与无符号除法相同,只有一个重要的区别:在执行除法之前,必须对被除数进行符号扩展

符号扩展 是指将一个数的最高位复制到包含该数的变量或寄存器的所有高位中

一个例子

-101在byte计算器中是9Bh,可在word计算器里,155是9B(无符号的,由此表明符号扩展的必要性)

而解决该问题的正确方法是使用CWD(字转双字)指令,在进行除法之前在DX:AX中对AX进行符号扩展

1.符号扩展指令(CBW、CWD、CDQ)

Intel提供了三种符号扩展指令:CBW(字节转字)、CWD(字转双字)和CDQ(双字转四字)

2.IDIV指令

IDIV(有符号除法)指令执行有符号整数除法,其操作数与DIV指令相同。执行8位除法之前,被除数(AX)必须完成符号扩展。余数的符号总是与被除数相同。

执行DIV和IDIV后,所有算术运算状态标志位的值都不确定

3.除法溢出

如果除法操作数生成的商不适合目的操作数,则产生除法溢出(divide overflow)。这将导致处理器异常并暂停执行当前程序。

对此有个建议:使用32位除数64位被除数来减少出现除法溢出条件的可能性

cmp b1,0 ;检查除数
je NoDividezero 为零?显示错误

7.3.6 实现算数表达式

看C++的编译代码,看看能不能从中有所启发

7.4 扩展加减法

7.4.1 ADC指令

ADC(带进位加法)

7.4.2 扩展整数加法示例

7.4.3 SBB指令

SBB(带借位减法)指令从目的操作数中减去源操作数和进位标志位的值。允许使用的操作数与ADC指令相同。

7.5 ASCII和非压缩十进制运算

7.5节讨论的指令只能用于32位模式编程

ASCII加减法运行操作数为ASCII格式或非压缩十进制格式,但是乘除法只能使用非压缩十进制数

尽管ASCII运算执行速度比二进制运算要慢很多,但是它有两个明显的优点: ·不必在执行运算之前转换串格式。 ·使用假设的十进制小数点,使得实数操作不会出现浮点运算的舍入误差的危险。

7.5.1 AAA指令

在32位模式下,AAA(加法后的ASCII调整)指令调整ADD或ADC指令的二进制运算结果。设两个ASCII数字相加,其二进制结果存放在AL中,则AAA将AL转换为两个非压缩十进制数字存入AH和AL。一旦成为非压缩格式,通过将AH和AL与30h进OR运算,很容易就能把它们转换为ASCIT码。

计算带有小数点的十进制数时,这种做法比较好

7.5.2 AAS指令

32位模式下,AAS(减法后的ASCII调整)指令紧随SUB或SBB指令之后,这两条指令执行两个非压缩十进制数的减法,并将结果保存到AL中。AAS指令将AL转换为ASCII码的数字形式。只有减法结果为负时,调整才是必需的。

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
val1 byte '8'
val2 byte '9'
.code
main PROC
	mov	ah,0	
	mov al,val1
	sub al,val2
	aas
	pushf
	or al,30h
	popf
main ENDP
END main

7.5.3 AAM指令

32位模式下,MUL执行非压缩十进制乘法,AAM(乘法后的ASCIⅡ调整)指令转换由其产生的二进制乘积。乘法只能使用非压缩十进制数

表现形式是16进制(结果带有h),但是出来的却是十进制

7.5.4 AAD指令

32位模式下,AAD(除法之前的ASCII调整)指令将AX中的非压缩十进制被除数转换为二进制,为执行DIV指令做准备。

7.6 压缩十进制计算

(7.6节讨论的指令仅用于32位编程模式)

压缩十进制数的每个字节存放两个十进制数字,每个数字用4位表示。如果数字个数为奇数,则最高的半字节用零填充。

DAA(加法后的十进制调整)和DAS(减法后的十进制调整)这两条指令调整压缩十进制数加减法的结果。

可惜的是,目前还没有与乘除法有关的相似指令。在这些情况下,相乘或相除的数必须是非压缩的,执行后再压缩。

7.6.1 DAA指令

mov al,35h 
add al,48h ;AL=7Dh 
daa			;AL=83h(调整后的结果)

7.6.2 DAS指令

mov bl,48h 
mov al,85h 
sub al,b1;AL=3Dh 
das;AL=37h(调整后的结果)

8 高级过程

本章学习的详细内容与C++和Java知识相关,将展示:

如何以数值或引用的形式来传递参数;如何定义和撤销局部变量;以及如何实现递归。

在本章结束时,将解释MASM使用的不同的内存模式和语言标识符。参数既可以用寄存器传递也可以用堆栈传递。

编程语言用不同的术语来指代子程序。例如,在C和C++中,子程序被称为函数(functions)。在Java中,被称为方法(methods)。在MASM中,则被称为过程(procedures)。本章目的是说明典型子程序调用的底层实现,就像它们在C和C++中展现的那样。在本章开始提到一般原则时,将使用泛称:子程序。而在提到具体汇编语言代码示例时,通常会使用术语过程来指代子程序。

8.2 堆栈帧

8.2.1 堆栈参数

堆栈帧(stack frame)(或活动记录(activation record))是一块堆栈保留区域,用于存放被传递的实际参数、子程序的返回值、局部变量以及被保存的寄存器

几乎所有高级语言都会用到他们

子程序调用时,有两种常见类型的参数会入栈: ·值参数(变量和常量的值)

·引用参数(变量的地址)

值传递

传递的是变量的副本

引用传递

传递的是变量的地址(offset)

8.2.3 访问堆栈参数

push ebp
mov ebp,esp ;做到这一步,EBP成为了函数的基址指针
1.显式的堆栈参数

若堆栈参数的引用表达式形如[ebp+8],则称它们为显式的堆栈参数(explicit stackparameters)。这个名称的含义是:汇编代码显式地说明了参数的偏移量是一个常数。

2.清除堆栈

子程序返回时,必须将参数从堆栈中删除。否则将导致内存泄露,堆栈就会被破坏。

8.2.4 32位调用规范

本节将给出Windows环境中两种最常用的32位编程调用规范。

首先是C语言发布的C调用规范,该语言用于Unix和Windows。

然后是STDCALL调用规范,它描述了调用Windows API函数的协议。

这两种规范都很重要,因为在C和C++程序中会调用汇编函数,同时汇编语言程序也会调用大量的WindowsAPI函数。

C调用规范
AddTWO(A,B);

B先入栈,然后A再入栈

C调用规范用一种简单的方法解决了清除运行时堆栈的问题:程序调用子程序时,在CALL指令的后面紧跟一条语句使堆栈指针(ESP)加上一个数,该数的值即为子程序参数所占堆栈空间的总和。下面的例子在执行CALL指令之前,将两个参数(5和6)入栈

STDCALL调用规范

如下所示的AddTwo过程给RET指令添加了一个整数参数,这使得程序在返回到调用过程时,ESP会加上数值8。这个添加的整数必须与被调用过程参数占用的堆栈空间字节数相等

8.2.5 局部变量

局部变量创建于运行时堆栈,通常位于基址指针(EBP)之下。尽管不能在汇编时给它们分配默认值,但是能在运行时初始化它们。可以使用与C和C++相同的方法在汇编语言中新建局部变量。

经典案例

MySub PROC 

push ebp 
mov ebp,esp 

sub esp,8;创建局部变量 
moV DWORD PTR [ebp-4],10;x 
mov DWORD PTR [ebp-8],20;Y 

mov esp,ebp
pop ebp 

ret 
MySub ENDP

可以使用局部变量使用使程序易读

X_local EQU DWORD PTR [ebp-4] 
Y_local EQU DWORD PTR [ebp-8]

两个函数用了两个堆栈(每个函数都有自己的栈空间)

使用Move指令的原因是,那两个数还未赋值,最后做到的效果跟铺设差不多

8.2.6 引用参数

引用参数通常是由过程用基址一偏移量寻址(从EBP)方式进行访问。由于每个引用参数都是一个指针,因此,常常作为一个间接操作数放在寄存器中

数组偏移量——数组指针地址

数组长度——数组大小

最后通过访问堆栈的形式访问参数

8.2.7 LEA指令

LEA指令返回间接操作数的地址

会在运行时计算这些操作数的偏移量(用于不确定数组大小的情况)

面对这样的数组时,offset没法使用,只能用offset

虽然数组只有30个字节,但是ESP还是递减了32以对齐双字边界

8.2.8 ENTER和LEAVE指令

ENTER指令为被调用过程自动创建堆栈帧。它为局部变量保留堆栈空间,把EBP入栈。具体来说,它执行三个操作: ·把EBP入栈(push ebp)

·把EBP设置为堆栈帧的基址(mov ebp,esp)

·为局部变量保留空间(sub esp,numbytes)ENTER有两个操作数:

第一个是常数,定义为局部变量保存的堆栈空间字节数;

第二个定义了过程的词法嵌套级。

如果要使用ENTER指令,那么本书强烈建议在同一个过程的结尾处同时使用LEAVE指令。否则,为局部变量保留的堆栈空间就可能无法释放。这将会导致RET指令从堆栈中弹出错误的返回地址

LEAVE指令LEAVE指令结束一个过程的堆栈帧。它反转了之前的ENTER指令操作: 恢复了过程被调用时ESP和EBP的值。

(没有参数)

8.2.9 LOCAL伪指令

Microsoft创建LOCAL伪指令是作为ENTER指令的高级替补

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
.code
MySub PROC 
	local num:byte
	mov num,1
	mov eax,1
	ret
MySub ENDP

main PROC
	mov eax,10
	push eax
	call MySub
	pop eax
	add eax,3
main ENDP
END main

8.2.10 Microsoft×64调用规范

8.3 递归

用寄存器的话,不许要局部变量

无限递归
Endless PROC 
mov edx,OFFSET endlessStr 
call WriteString 
call Endless 
ret;从不执行

这个程序会在堆栈溢出时终止程序

8.3.1 递归求和

即使是一个简单的递归过程也会使用大量的堆栈空间。每次过程调用发生时最少占用4字节的堆栈空间,因为要把返回地址保存到堆栈。

calsum中的定义


CalcSum PROC
; Calculates the sum of a list of integers
; Receives: ECX = count
; Returns: EAX = sum
	cmp  ecx,0	; check counter value
	jz   L2		; quit if zero
	add  eax,ecx	; otherwise, add to sum
	dec  ecx		; decrement counter
	call CalcSum	; recursive call
L2:	ret
CalcSum ENDP

8.3.2 计算阶乘

每一次都建立了新函数,所以每次堆栈的位置都不一样(但栈与栈之间都挨得很近)

main PROC
	push 5			; calculate 5 factorial
	call Factorial		; calculate factorial (eax)
main ENDP

Factorial PROC
	push ebp
	mov  ebp,esp
	mov  eax,[ebp+8]	; get n
	cmp  eax,0		; n < 0?
	ja   L1			; yes: continue
	mov  eax,1		; no: return 1
	jmp  L2

L1:	dec  eax
	push eax			; Factorial(n-1)
	call Factorial

; Instructions from this point on execute when each
; recursive call returns.

ReturnFact:
	mov  ebx,[ebp+8]   	; get n
	mul  ebx          	; ax = ax * bx

L2:	pop  ebp			; return EAX
	ret  4			; clean up stack
Factorial ENDP
END main

为什么L2包在RetutrnFact里面?原因是RetutrnFact里用到了L2退出堆栈

Returnfact在程序的内部以正常运算顺序内执行到

读者可能已经注意到之前的EAX,即第一次调用时分配给Factorial的值,被新值覆盖了。这说明了一个重要的事实:在过程进行递归调用时,应该小心注意哪些寄存器会被修改。如果需要保存这些寄存器的值,就需要在递归调用之前将其入栈,并在调用返回之后将其弹出堆栈。幸运的是,对Factorial过程而言,在递归调用之间保存EAX并不是必要的。

8.4 INVOKE、ADDR、PROC和PROTO

在32位模式中,INVOKE、PROC和PROTO伪指令是过程定义和调用的强大工具。 ADDR运算符与这些伪指令一起使用,是定义过程参数的重要工具。

从教学的角度来看,它们的使用是有争议的,因为它们屏蔽了运行时堆栈的底层结构。因此,在使用这些伪指令之前,详细了解子程序调用的底层机制是非常明智的。

8.4.1 INVOKE伪指令

INVOKE伪指令,只用于32位模式,将参数入栈(按照MODEL伪指令的语言说明符所指定的顺序)并调用过程。INVOKE是CALL指令一个方便的替代品,因为,它用一行代码就能传递多个参数。

将参数逆序压入堆栈当中

电子PDF P267页

覆盖EAX和EDX如果向过程传递的参数小于32位,那么在将参数入栈之前,INVOKE为了扩展参数常常会使得汇编器覆盖EAX和EDX的内容。有两种方法可以避免这种情况: 其一,传递给INVOKE的参数总是32位的;其二,在过程调用之前保存EAX和EDX,在讨程调用之后再恢复它们的值。

8.4.2 ADDR伪指令

ADDR运算符同样可用于32位模式,在使用INVOKE调用过程时,它可以传递指针参数

必须是汇编常数且只能与INVOKE一起使用

8.4.3 PROC伪指令

电子PDF P269页

3.指定参数传递协议

一个程序可以调用Irvine32链接库过程,反之,也可以包含能被C++程序调用的过程。 为了提供这样的灵活性,PROC伪指令的属性域允许程序指定传递参数的语言规范,并且能覆盖.MODEL伪指令指定的默认语言规范。下例声明的过程采用了C调用规范:

 Example1 PROCC,parm1:DWORD,parm2:DWORD

8.4.4 PROTO伪指令

prototype

64位模式中,PROTO伪指令指定程序的外部过程

32位模式中,PROTO是一个更有用的工具,因为它可以包含过程参数列表

相当于一个事先声明

8.4.5 参数类别

过程参数可分为输入类,输出类,输入输出类

电子PDF P274页

8.4.7 调试提醒

1.参数大小不匹配

就是一个word应该是+4,+1的话就会出错

2.传递错误类型的指针

指针类型不匹配

3.传递立即数

如果过程有一个引用参数,就不要向其传递立即数参数,因为很可能把数据当成地址传递,从而出错

8.4.8 WriteStackFrame过程

Irvine32链接库有个很有用的过程WriteStackFrame,用于显示当前过程堆栈帧的内容,其中包括过程的堆栈参数、返回地址、局部变量和被保存的寄存器。

8.5 创建多模块程序

把一个程序按模块(module)(汇编单位)分割。每个模块可以单独汇编,因此,对一个模块源代码的修改就只需要重汇编这个模块。链接器将所有汇编好的模块(OBJ文件)组合为一个可执行文件的速度是相当快的,链接大量目标模块比汇编同样数量的源代码文件花费的时间要少得多

新建多模块程序有两种常用方法:

其一是传统方法,使用EXTERN伪指令,基本上它在不同的x86汇编器之间都可以进行移植。

其二是使用Microsoft的高级伪指令INVOKE和PROTO,这能够简化过程调用,并隐藏一些底层细节。

8.5.1 隐藏和导出过程名

默认情况下,MASM使所有的过程都是public属性,即允许它们能被同一程序中任何其他模块调用。使用限定词PRIVATE可以覆盖这个属性

mysub proc private

如果程序的启动模块使用了OPTIONPROC:PRIVATE,那么就应该将它(通常为main)指定为PUBLIC,否则操作系统加载器无法发现该启动模块

8.5.2 调用外部过程

调用当前模块之外的过程时使用EXTERN伪指令,它确定过程名和堆栈帧大小。

过程名的后缀@n确定了已声明参数占用的堆栈空间总量(参见8.4节扩展PROC伪指令)。如果使用的是基本PROC伪指令,没有声明参数,那么EXTERN中的每个过程名后缀都为@0。若用扩展PROC伪指令声明一个过程,则每个参数占用4字节。

有时候可以用PROTO代替extern

8.5.3 跨模块使用标量和符号

1.导出变量和符号
2.访问外部变量和符号

对符号(由EQU和=定义)而言,type应为ABS。对变量而言,type是数据定义属性,如BYTE、WORD、DWORD和SDWORD,可以包含PTR。

3.使用带 EXTERNDEF的INCLUDE文件

MASM中一个很有用的伪指令EXTERNDEF可以代替PUBLIC和EXTERN。它可以放在文本文件中(类似C语言的.h文件),并用INCLUDE伪指令复制到每个程序模块。

电子PDF P279

8.5.5 用Extern新建模块

为了使源代码更加友好,用EQU伪指令再次定义了过程名

8.5.6 用INVOKE和PROTO新建模块

与前面的PromptForlntegers版本比较,语句enter0,0和leave不见了,这是因为当MASM遇到PROC伪指令及其声明的参数时,会自动生成这两条语句。同样,RET指令也不需要自带常数参数了(PROC会处理好)

这些伪指令简化了很多细节,并为Windows API函数调用进行了优化

8.5.7 课程回顾

链接OBJ模块比汇编ASM源文件快得多(对的)

8.6 参数的高级用法(可选主题)

在查看由C和C++编译器创建的代码时,就有可能发现其中用到了将在下面说明的技术

8.6.1 受USES运算符影响的堆栈

由于汇编器在过程开头插入了ECX和EDX的PUSH指令,使得堆栈参数的偏移量发生变化,从而导致结果错误,原本的EBP+8要改为EBP+16

使用PROC的高级语法就不会造成该问题

8.6.2 向堆栈传递8位和16位参数

16位操作数入栈,可能会使EBP不能对齐双字边界,从而可能导致出现页面失效、降低运行时性能。

PUSH指令不允许操作数为8位

8.6.3 传递64位参数

32位模式中,通过堆栈向子程序传递64位参数时,先将参数的高位双字入栈,再将其低位双字入栈。这样就使得整数在堆栈中是按照小端顺序(低字节在低地址)存放的,因而子程序容易检索到这些数值

8.6.4 非双字局部变量

在声明不同大小的局部变量时,LOCAL伪指令的操作会变得很有趣。每个变量都按照其大小来分配空间:

8位的变量分配给下一个可用的字节,

16位的变量分配给下一个偶地址(字对齐),

32位变量分配给下一个双字对齐的地址。

由于32位模式中,堆栈偏移量默认为32位

虽然SwapFlag只是一个字节变量,但是ESP还是会下移到堆栈中下一个双字的位置。

对嵌套调用来说,不论程序执行到哪一步,运行时堆栈(用.stack指令进行声明)都必须大到能够容纳下全部的活跃局部变量。

若过程为递归调用,则堆栈空间大约为其局部变量参数总的大小乘以预计的递归次数

8.7 Java字节码(可选主题)

Java虚拟机的指令集与×86处理器系列的指令集有很大的不同。它采用面向堆栈的方法实现计算、比较和分支,与×86指令经常使用寄存器和内存操作数形成了鲜明的对比。

8.8 本章小结

过程参数有两种基本类型:寄存器参数和堆栈参数

堆栈帧(或活动记录)是为过程返回地址、传递参数、局部变量和被保存寄存器预留的堆栈区域。运行中的程序在开始执行过程的时候就会创建堆栈帧

LOCAL伪指令在过程内部声明一个或多个局部变量,它必须紧跟在PROC伪指令的后面。

9 字符串和数组

9.2 字符串基本指令

X86指令集有五组指令用于处理字节、字和双字数组。虽然它们被称为字符串原语(string primitives),但它们并不局限于字符数组。

字符串原语能高效执行,因为它们会自动重复并增加数组索引。

示例:复制字符串

c1d ;清除方向标志位 
mov esi,OFFSET stringl;ESI指向源串 
mov edi,OFFSET string2;EDI执行目的串 
mov ecx,10;计数器赋值为10 
rep movsb;传送10个字节

方向标志位根据方向标志位的状态,字符串基本指令增加或减少ESI和EDI(参见表9-2)。可以用CLD和STD指令显式修改方向标志位

CLD;正向

SLD;反向

9.2.1 MOVSB、MOVSW和MOVSD

MOVSB、MOVSW和MOVSD指令将数据从ESI指向的内存位置复制到EDI指向的内存位置。(根据方向标志位的值)这两个寄存器自动地增加或减少

Mov string byte

Mov string word

Mov string double word

数组复制完成后,ESI和EDI将分别指向两个数组范围之外的一个位置(双字就会超出4字节)

9.2.2 CMPSB、CMPSW和CMPSD

CMPSB、CMPSW和CMPSD指令比较ESI指向的内存操作数与EDI指向的内存操作数

.data 
source DWORD 1234h 
target DWORD 5678h 
.code 
mov esi,OFFSET source
mov edi,OFFSET target 
cmpsd;比较双字 
ja L1;若source>target则跳转

REPE前缀重复比较操作,并自动增加ESI和EDI,直到ECX等于0,或者发现了一对不相等的双字。

9.2.3 SCASB、SCASW和SCASD

SCASB、SCASW和SCASD指令分别将AL/AX/EAX中的值与EDI寻址的一个字节/字/双字进行比较

用于扫描是否有匹配字符,配合REPE(equal)或REPZ(zero),未发现的话使用jnz强制退出

9.2.4 STOSB、STOSW和STOSD

STOSB、STOSW和STOSD指令分别将AL/AX/EAX的内容存入由EDI中偏移量指向的内存位置。EDI根据方向标志位的状态递增或递减。与REP前缀组合使用时,这些指令实现用同一个值填充字符串或数组的全部元素

实际上是重复填充

9.2.5LODSB、LODSW和LODSD

LODSB、LODSW和LODSD指令分别从ESI指向的内存地址加载一个字节或一个字到AL/AX/EAX,加载到累加器的新值会覆盖其原来的内容

LODS相当于两条指令

mov al,[esi]
inc esi

9.3 部分字符串过程

本节将演示用Irvine32链接库中的几个过程来处理空字节结束的字符串

到电子PDF P312结束

9.4 二位数组

9.4.1 行列顺序

本章主要讲的是行主序实现

9.4.2 基址-变址操作数

其中的方括号是必须的

二维数组

按行访问一个二维数组时,行偏移量放在基址寄存器中,列偏移量放在变址寄存器中。

只要不涉及堆栈,EBP就不会动,EBX是基址

.386 
.model flat,stdcall
.stack 4096
ExitProcess PROTO,dwExitcode:DWORD

.data
tableB  BYTE  10h,  20h,  30h,  40h,  50h
        BYTE  60h,  70h,  80h,  90h,  0A0h
        BYTE  0B0h, 0C0h, 0D0h, 0E0h, 0F0h
RowSize = 5
msg1	BYTE "Enter row number: ",0
msg2 BYTE "The sum is: ",0

.code
main PROC

; Demonstrate Base-Index mode:

	mov	  edx,OFFSET msg1			; "Enter row number:"
	;call  WriteString
	mov eax,1					; EAX = row number

	mov	  ebx,OFFSET tableB
	mov	  ecx,RowSize
	call  calc_row_sum				; EAX = sum
   
	mov	  edx,OFFSET msg2			; "The sum is:"
	;call  WriteString
	;call  WriteHex					; write sum in EAX
	;call  Crlf

main ENDP


;------------------------------------------------------------
calc_row_sum PROC uses ebx ecx edx esi
;
; Calculates the sum of a row in a byte matrix.
; Receives: EBX = table offset, EAX = row index, 
;		    ECX = row size, in bytes.
; Returns:  EAX holds the sum.
;------------------------------------------------------------

	mul	 ecx			; row index * row size
	add	 ebx,eax		; row offset
	mov	 eax,0		; accumulator
	mov	 esi,0		; column index

L1:	movzx edx,BYTE PTR[ebx + esi]		; get a byte
	add	 eax,edx						; add to accumulator
	inc	 esi							; next byte in row
	loop L1

	ret
calc_row_sum ENDP

END main
2.比例因子

如果是为数组编写代码,则需要将变址操作数乘以比例因子2。

使用type即可

mov eax,[ebx+esi*type tableD]

9.4.3 基址一变址一偏移量操作数

基址一变址一偏移量操作数用一个偏移量、一个基址寄存器、一个变址寄存器和一个可选的比例因子来生成有效地址。

base基址 index变址 displacement偏移量操作数

偏移量相当于移动基址

9.4.4 64位模式下的基址一变址操作数

9.5 整数数组的检索和排序

9.5.1 冒泡排序

;----------------------------------------------------------
BubbleSort PROC USES eax ecx esi,
	pArray:PTR DWORD,		; pointer to array
	Count:DWORD			; array size
;
; Sort an array of 32-bit signed integers in ascending order
; using the bubble sort algorithm.
; Receives: pointer to array, array size
; Returns: nothing
;-----------------------------------------------------------

	mov ecx,Count
	dec ecx			; decrement count by 1

L1:	push ecx			; save outer loop count
	mov	esi,pArray	; point to first value

L2:	mov	eax,[esi]		; get array value
	cmp	[esi+4],eax	; compare a pair of values
	jge	L3			; if [esi] <= [edi], don't exch
	xchg eax,[esi+4]	; exchange the pair
	mov	[esi],eax

L3:	add	esi,4		; move both pointers forward
	loop	L2			; inner loop

	pop	ecx			; retrieve outer loop count
	loop L1			; else repeat outer loop

L4:	ret
BubbleSort ENDP

9.5.2 对半查找

.code
;-------------------------------------------------------------
BinarySearch PROC USES ebx edx esi edi,
	pArray:PTR DWORD,		; pointer to array
	Count:DWORD,			; array size
	searchVal:DWORD			; search value
LOCAL first:DWORD,			; first position
	last:DWORD,				; last position
	mid:DWORD				; midpoint
;
; Search an array of signed integers for a single value.
; Receives: Pointer to array, array size, search value.
; Returns: If a match is found, EAX = the array position of the
; matching element; otherwise, EAX = -1.
;-------------------------------------------------------------
	mov	 first,0			; first = 0
	mov	 eax,Count			; last = (count - 1)
	dec	 eax
	mov	 last,eax
	mov	 edi,searchVal		; EDI = searchVal
	mov	 ebx,pArray			; EBX points to the array

L1: ; while first <= last
	mov	 eax,first
	cmp	 eax,last
	jg	 L5					; exit search

; mid = (last + first) / 2
	mov	 eax,last
	add	 eax,first
	shr	 eax,1 ;用右移代替除法
	mov	 mid,eax

; EDX = values[mid]
	mov	 esi,mid
	shl	 esi,2				; scale mid value by 4
	mov	 edx,[ebx+esi]		; EDX = values[mid]

; if ( EDX < searchval(EDI) )
;	first = mid + 1;
	cmp	 edx,edi
	jge	 L2
	mov	 eax,mid				; first = mid + 1
	inc	 eax
	mov	 first,eax
	jmp	 L4

; else if( EDX > searchVal(EDI) )
;	last = mid - 1;
L2:	cmp	 edx,edi
	jle	 L3
	mov	 eax,mid				; last = mid - 1
	dec	 eax
	mov	 last,eax
	jmp	 L4

; else return mid
L3:	mov	 eax,mid  				; value found
	jmp	 L9						; return (mid)

L4:	jmp	 L1						; continue the loop

L5:	mov	 eax,-1					; search failed
L9:	ret
BinarySearch ENDP
END

9.6 Java字节码:字符串处理(可选主题)

9.7 本章小结

数组操作是计算密集型的,因为一般它会涉及循环算法。大多数程序80%~90%的运行时间都用来执行其代码的一小部分。因此,通过减少循环中指令的条数和复杂度就可以提高软件的速度。由于汇编语言能控制每个细节,所以它是极好的代码优化工具。比如,通过用寄存器来代替内存变量,就能够优化代码块。或者可以使用本章介绍的字符串处理指令,而不是用MOV和CMP指令。

10 结构和宏

10.1 结构

结构(structure)是一组逻辑相关变量的模板或模式。结构中的变量被称为字段(fields)。程序语句可以把结构作为整体进行访问,也可以访问其中的单个字段。结构常常包含不同类型的字段。联合(union)也会把多个标识符组织在一起,但是这些标识符会在内存同一区域内相互重叠。联合将在10.1.7节介绍。

(就是结构体和共用体)

COORD结构

WindowsAPI中定义的COORD结构确定了屏幕的X和Y坐标。相对于结构起始地址,字段X的偏移量为0,字段Y的偏移量为2

10.1.1 定义结构

定义结构使用的是STRUCT和ENDS伪指令。在结构内,定义字段的语法与一般的变量定义是相同的。结构对其包含字段的数量几乎没有任何限制

对齐结构字段

汇编语言中的ALIGN伪指令会使其后的字段或变量按地址对齐:

ALIGN datatype

10.1.2 声明结构变量

.data 
point1 COORD<5,10>;X=5,Y=10 
point2 COORD<20>;x=20,Y=?
point3 COORD<>;X=?,Y=?
worker Employee<>;默认初始值

10.1.3 引用结构变量

使用TYPE和SIZEOF运算符可以引用结构变量和结构名称。

复习一下,type运算符返回的是标识符存储类型的字节数

1.引用成员

以下为对worker(一个Employee)的运行时引用:

.data 
worker Employee<> 
.code 
mov dx,worker.Years 
mov worker.SalaryHistory,20000;第一个工资 
mov [worker.SalaryHistory+4],30000;第二个工资 

使用OFFSET运算符使用OFFSET运算符能获得结构变量中一个字段的地址:

mov edx,OFESET worker.LastName
2.间接和变址操作数

下面的语句不能汇编,原因是Years自身不能表明它所属的结构:

mov ax,[esi].Years;无效

下述语句访问的是索引位置为1的雇员的Years字段:

.data 
department Employee 5 DUP(<>) 
.code 
mov esi,TYPE Employee;索引=1 
mov department[esi].Years,4
3.对齐的结构成员的性能

对齐肯定比不对齐要快,但实际还要看CPU如何处理数据,有些情况下相差不大

10.1.5 结构包含结构

结构还可以包含其他结构的实例。例如,Rectangle可以用其左上角和右下角来定义,而它们都是COORD结构:

Rectangle STRUCT
UpperLeft COORD<>
LowerRight COORD<>
Rectangle ENDS

下面是对其一个结构字段的直接引用:

mov rect1.UpperLeft.x,10
OFFSET运算符能返回单个结构字段的指针,包括嵌套字段:
mov edi,OFFSET rect2.LowerRight 
moV (COORD PTR [edi]).x,50 
mov edi,OFFSET rect2.LowerRight.X 
mov WORDPTR [edi],50
好好想想,过程是正确的

10.1.6 声明和使用联合

即:如何使用结构体

10.2 宏

10.2.1 概述

宏过程(macro procedure)是一个命名的汇编语句块。一旦定义好了,它就可以在程序中多次被调用。在调用宏过程时,其代码的副本将被直接插入到程序中该宏被调用的位置。

位置宏定义一般出现在程序源代码开始的位置,或者是放在独立文件中,再用INCLUDE伪指令复制到程序里。

宏在汇编器预处理(preprocessing)阶段进行扩展

10.2.2 定义宏

定义一个宏使用的是MACRO和ENDM伪指令

通过弹出和进入堆栈控制参数

10.2.3 调用宏

提示通常,与过程相比,宏执行起来更快,其原因是过程的CALL和RET指令需要额外的开销。但是,使用宏也有缺点:重复使用大型宏会增加程序的大小,因为,每次调用宏都会在程序中插入宏代码的一个新副本。

调试宏

调试使用了宏的程序相当具有挑战性。

电子PDF P344页

调试使用了宏的程序相当具有挑战性。程序汇编之后,检查其列表文件(扩展名为.LST)以确保每个宏都按照程序员的要求展开。

10.2.4 其他宏特性

1.规定形参

利用REQ限定符,可以指定必需的宏形参。如果被调用的宏没有实参与规定形参相匹配,那么汇编器将显示出错消息。

2.宏注释

宏定义中的注释行一般都出现在每次宏展开的时候。如果希望忽略宏展开时的注释,就在它们的前面添加双分号(;;)。

3.ECHO伪指令

在程序汇编时,ECHO伪指令写一个字符串到标准输出。

Visual Studio2012的控制台窗口不会捕捉ECHO伪指令的输出,需要特殊配置完以后才行

4.LOCAL伪指令

宏定义中常常包含了标号,并会在其代码中对这些标号进行自引用

使用local标记标号,预处理程序就会把标号名称转换为唯一标识符

汇编器生成的标号名使用了??nnnn的形式

5.包含代码和数据的宏

宏通常既包含代码又包含数据,数据也会附上唯一标识符

6.宏嵌套

被嵌套的宏(nested macro)

10.2.5使用本书的宏库(仅32位模式)

10.2.7本节回顾

1.(真/假):当一个宏被调用时,CALL和RET指令将自动插入到汇编程序中。

应该是真的,还需要细究

10.3 条件汇编伪指令

10.3.1 检查缺失的参数

宏能够检查其参数是否为空。通常,宏若接收到空参数,则预处理程序在进行宏展开时会导致出现无效指令

为了防止由于操作数缺失而导致的错误,可以使用IFB(if blank)伪指令,若宏实参为空,则该伪指令返回值为真。还可以使用IFNB(if not blank)运算符,若宏实参不为空,则返回值为真

EXITM伪指令告诉预处理程序退出宏,不再展开更多宏语句。

10.3.2 默认参数初始值设定

宏可以有默认参数初始值。如果调用宏出现了宏参数缺失,那么就可以使用默认参数。

mWriteln MACRO text:=<""> 
	mWrite text 
	call Crlf
ENDM

双引号中间必须有参数,否则汇编器会产生错误

10.3.3 布尔表达式

支持部分布尔表达式

电子PDF P356页

10.3.4 IF,ELSE和ENDIF

IF伪指令的后面必须跟一个常量布尔表达式。该表达式可以包含整数常量、符号常量或者常量宏实参,但不能包含寄存器或变量名

10.3.5 IFIDN和IFIDNI伪指令

IFIDNI伪指令在两个符号(包括宏参数名)之间进行不区分大小写的比较,如果它们相等,则返回真。

IFIDN伪指令执行的是区分大小写的比较。

如下面的宏mReadBuf(电子PDF 357页),其第二个参数不能用EDX

因为当buffer的偏移量被送入EDX时,原来的值就会被覆盖。

在如下修改过的宏代码中,如果这个条件不满足,就会显示一条警告消息

10.3.6 示例:矩阵行求和

由于没有宏于USES伪指令功能相当,因此插入push和pop指令

电子PDF P359页

若MOVZX右操作数为双字,那么指令不会汇编。所以,当eltType为DWORD时,需要用IFIDNI运算符另外编写一条MOV指令

带有条件汇编的片段需要标记为local

10.3.7 特殊运算符

&替换运算符

<>文本文字运算符

!文字字符运算符

% 条件展开符

1.替换运算符(&)

替换运算符(&)解析对宏参数名的有歧义的引用,添加了&运算符,它就会强制预处理程序在字符串文本中插入实参(如ECX)

2.展开运算符(%)

展开运算符(%)展开文本宏并将常量表达式转换为文本形式

如果用TEXTEQU编写包含(SIZEOF array)的文本宏,那么该宏就可以展开为之后的代码行,实现数字输出

有点迷惑,在电子PDF P361页

@LINE是一个预先定义的汇编运算符,其功能为返回当前源代码行的编号

3.文字文本运算符(<>)

文字文本(literal-text)运算符(>)把一个或多个字符和符号组合成一个文字文本

如果用文字文本运算符将字符串括起来,那么预处理程序就会把尖括号内所有的文本当作一个宏实参

4.文本字符运算符(!)

强制预处理程序把预先定义的运算符当作普通的字符

10.3.8 宏函数

宏函数与宏过程有相似的地方,它也为汇编语言语句列表分配一个名称。不同的地方在于,宏函数通过EXITM伪指令总是返回一个常量(整数或字符串)。

看的不太懂,好像是可以通过这个方案选择inc文件,书上举的案例,是在16位和32位之间让机器自动做出选择

电子pDF P364页

10.4 定义重复语句块

MASM有许多循环伪指令用于生成重复的语句块:WHILE、REPEAT、FOR和FORC。 与LOOP指令不同,这些伪指令只在汇编时起作用,并使用常量值作为循环条件和计数器: ·WHILE伪指令根据一个布尔表达式来重复语句块。 ·REPEAT伪指令根据计数器的值来重复语句块。 ·FOR伪指令通过遍历符号列表来重复语句块。 ·FORC伪指令通过遍历字符串来重复语句块。

基本上和C语言的循环是一致的

10.4.5 示例:链表

那里面的LABEL标签怎么就变成了Macro?

10.5 本章小结

结构自身不占内存空间,但是结构变量会占用内存