7. 高级汇编语言技术
文章目录
- 7. 高级汇编语言技术
- 7.0 导学
- 7.1 子程序的另外一种写法-segment/ends-proc/endp
- 7.2 程序的多文件组织
- 7.3 汇编指令汇总
- 7.4 汇编伪操作汇总
- 7.5 汇编操作符汇总
- 7.6 汇编过程
- 7.7 宏汇编
- 7.8 宏库
- 7.9 条件汇编
- 7.10 重复汇编
- 7.11 80x86汇编
- 7.12 汇编语言集成开发环境
- 7.13 用Visual studio开发汇编程序
- 7.14 高级语言的指令级调试
- 7.15 反汇编(逆向工程)
- 7.16 混合编程
- 7.17 文件结构分析
- 7.18 完结撒花
- 参考视频:烟台大学贺利坚老师的网课《汇编语言程序设计系列专题》,或者是B站《汇编语言程序设计 贺利坚主讲》,大家一起看比较热闹。
- 中文教材:《汇编语言-第3版-王爽》(课程使用)、《汇编语言-第4版-王爽》(最新版)。
- 老师的博客:《迂者-贺利坚的专栏-汇编语言》
- 检测点答案参考:《汇编语言》- 读书笔记 - 各章检测点归档
本篇笔记对应课程第七章(下图倾斜),章节划分和教材对应关系如下。
7.0 导学
目前我们已经跟随《汇编语言-王爽》完整系统的学完了8086汇编,该教材体系清晰,但是并没有提及汇编语言的可替代方案、以及一些汇编语言高级技术,本章就对这部分没有提到的内容进行介绍。
本章参考教材:
- 《IBM-PC汇编语言程序设计(第2版)》-沈美明-2018年出版
- 《汇编语言:基于x86处理器(原书第7版)》-[美]基普·欧文(Kip Irvine)著,贺莲等译
8086扩展:模块化功能设计支持的功能。
【7.1 子程序的另外一种写法】
【7.2 程序的多文件组织】将源程序写在多个文件中,最后再组织在一起。8086总结:总结前面所有学过的指令、伪操作、操作符,并介绍没用过的。
【7.3 汇编指令汇总】
【7.4 汇编伪操作汇总】
【7.5 汇编操作符汇总】
【7.6 汇编过程】宏汇编:在工程中常用,类似于C语言中的宏定义。可以看到,汇编语言对于工程上的支持还是比较强的。
【7.7 宏汇编】
【7.8 宏库】
【7.9 条件汇编】
【7.10 重复汇编】32位汇编:8086是16位汇编,虽然目前的主流消费PC都是32位/64位,但许多嵌入式等设备仍旧使用16位汇编,另外16位汇编也可以迁移到32位/64位汇编,所以学习16位汇编很有必要。基于前面的汇编语言工作方式,进一步介绍32位汇编的80x86。
【7.11 80x86汇编】
【7.12 汇编语言集成开发环境】
【7.13 用Visual studio开发汇编程序】实用技术:对于汇编掌握熟练的人才能理解。
【7.14 高级语言的指令级调试】指令级调试。
【7.15 反汇编(逆向工程)】游戏外挂、文件解密、软件汉化等。
【7.16 混合编程】将汇编语言和高级语言混合进行编程,同时发挥汇编语言执行效率高、高级语言开发效率高的优势。
【7.17 文件结构分析】通过debug等工具观察文件的每一个数据,对照文件结构解析理解文件构成。以后即使遇到未知的文件结构,也可以通过数据组成推导出文件结构。
7.1 子程序的另外一种写法-segment/ends-proc/endp
【
名称 segment
、名称 ends
】
功能:用于标记一个 段 的开始和结束。【
名称 proc
、名称 endp
】
功能:用于标记一个 过程/子程序 的开始和结束。
前面我们直接定义一个子程序的地址标号(带冒号标号)来调用子程序,但是总觉不够美观。实际上,仿照C语言的方式,我们可以进一步使用 proc
/endp
来进一步封装程序。比如下面给出的封装子程序的方式,中间是正常调用,使用ret
返回;右侧是远调用,返回使用retf
:
7.2 程序的多文件组织
目前为止,我们写的汇编程序都是将所有程序放在一个程序文件中。但实际上,对于大型程序来说,更加常见的做法是将不同的程序放在不同的程序文件中,并且每个程序文件又会包含若干程序,如上图所示。汇编语言中也很容易做到这点,只需要在每个程序文件的最开始,用 extrn
声明要调用的外部程序、用 public
声明可以被外部调用的程序,最后再“分开编译、一起链接”即可(和C++中class
的感觉有点像)。具体操作见下面的示例:
【代码示例】将上一小节“图7-1”所示的程序拆分成两个文件。
只存放主程序的程序文件 p-2-1.asm:extrn subp:far ; 表示要调用的程序在其他文件,far是调用属性assume cs:code, ss:stackstack segment stackdb 16 dup (0) stack endscode segment main procstart: ; 初始化mov ax,stackmov ss,axmov sp,16mov ax,1000; 调用子程序call far ptr subp; 程序退出mov ax,4c00hint 21h main endp code ends end start
只存放子程序 subp 的程序文件 p7-2-2.asm:
public subp ; 列出本程序中可以被外部调用的程序assume cs:codecode segment subp proc; s: add ax,axadd ax,ax ; 子程序的开始也可以不加标号retf subp endp code ends end ; 注意子程序的最后就不需要标号了
7.3 汇编指令汇总
本小节总结一下汇编语言的所有指令,并且简单介绍一下之前没学过的。但是要注意汇编指令很多,没有必要全部记下来,本节只是大概介绍一下有个印象,后续用到的时候查手册就行。如上图给出常见的汇编指令分类,下面进行简单的介绍:
- 指令详解见:《汇编语言指令大全》(Github、我的百度网盘)——根据字母表排序。
【一、数据传送指令】
【1-通用数据传送指令】mov、push、pop、xchg
xchg 寄存器1,寄存器2
:交换指令,交换(exchange)两个寄存器的内容。不影响标志位、寄存器不允许是使用段寄存器。【2-累加器专用传送指令】in、out、xlat
xlat
:换码指令,将ds:[bx+al]中的内容(8bit)放到al当中。显然,这里的bx存储首地址、al存储偏移地址(显然不超过256)。换码指令不影响标志位。【3-地址传送指令】lea、lds、les
lea reg,src
:有效地址送寄存器指令。src 通常是标号,这条指令就是将 src 的偏移地址送给寄存器 reg 中。只取两个字节。lds reg,src
:指针送寄存器和DS指令。将 src 作为偏移地址送给寄存器 reg、再将 (src+2) 的内容作为首地址送给 ds。总共传送四字节。les reg,src
:指针送寄存器和ES指令。将 src 作为偏移地址送给寄存器 reg、再将 (src+2) 的内容作为首地址送给 es。总共传送四字节。【4-标志寄存器传送指令】lahf、sahf、pushf、popf
lshf
:标志送AH指令(load ah flag)。将标志寄存器的低8位送给ah。sahf
:AH送标志寄存器指令(save ah flag)。将ah的内容保存到标志寄存器的低8位。pushf
:标志进栈指令。将标志寄存器压栈(16bit)。popf
:标志出栈指令。将标志寄存器出栈(16bit)。【5-类型转换指令】cbw、cwd
cbw
:将al扩展成ax(convert byte word)。ah的内容等于al的最高位。cwd
:将ax扩展成 dx,ax(convert word double word)。dx的内容等于ax的最高位。- 共性:都是无操作符指令,不影响标志寄存器。变化原则可保证有符号数不发生变化。
【二、算术指令】
【1-加法指令】add、adc、inc
【2-减法指令】sub、sbb、dec、neg(取负)、cmp
- 注:cmp 的本质是减法,只不过不影响操作数,而是只影响标志寄存器。
【3-乘法指令】mul(无符号数乘法)、imul(有符号数乘法)
【4-除法指令】div(无符号数除法)、idiv(有符号数除法)
【5-十进制调整指令】daa、das、aaa、aas、aam、aad
前置知识1:将一个十进制数用4位二进制进行表示称为“压缩的BCD码”,比如(59)十进制=(0101_1001)BCD;用8位二进制进行表示就是“非压缩的BCD码”,比如(59)十进制=(0000_0101__0000_1001)BCD。ASCII码就是一种特殊的非压缩BCD码,只不过高4位固定为0011b而不是0000b。
前置知识2:现在做十进制加法。但是压缩的BCD码做二进制加法后,需要再加 6d=110h,才能得到十进制加法结果。比如(19+8)十进制=(0001_1001)BCD+(0000_1000)BCD+0110=0010_0001。可见十进制加法和二进制加法可以通过加减一个常数相互转化,这个过程就是“调整”。
daa
:直接调用,将二进制加法结果(al)调整为十进制。
das
:直接调用,将二进制减法结果(al)调整为十进制。
aaa
:直接调用,将ASCII码的十进制加法结果(al)调整为二进制。
aas
:直接调用,将ASCII码的十进制减法结果(al)调整为二进制。
aam
:直接调用,将ASCII码的十进制乘法结果(ax)调整为二进制。
aad
:直接调用,将ASCII码的十进制除法结果(ax)调整为二进制。
【三、逻辑指令】
【1-逻辑运算指令】and、or、not、xor、test
test
:本质上是进行and
操作。类似于cmp,结果不保存只影响标志位,常用于按位运算类型的判断。【2-移位指令】shl/shr、sal/sar、rol/ror、rcl/rcr
【四、串处理指令】
【1-串方向标志】cld、std
【2-串重复前缀】rep、repe/repz、repne/repnz
【3-串处理指令】movsb/movsw、stosb/stosw(save)、lodsb/lodsw(load)、cmpsb/cmpsw、scasb/scasw(scan)【代码示例】从附加段字符串"computer"中查找一个指定的字符"t"。
mess db 'COMPUTER'lea di,mess mov al,'T' mov cx,8 ; 字符串长度为8,所以最大重复8次 cld ; 自增方向 repne scasb ; 若当前内存单元=='a',则退出扫描 ; 退出后,若cx不为0,则证明找到了指定字符't',并且cx此时指明了位置。
注:课程首次出现见“5.14节-DF标志和串传送指令-movsb/movsw”。
【五、控制转移指令】
【1-无条件转移指令】jmp
【2-条件转移指令】jz/jne、je/jne、js/jns、jo/jno、jp/jnp、jb/jnb、jl/jnl、jbe/jnbe、jle/jnle、jcxz注:课程首次出现见“5.12节-cmp和条件转移指令”。
【3-循环指令】loop、loopz/loope、loopnz/loopne
【4-子程序调用和返回指令】call、ret
【5-中断与中断返回指令】int、into、iret
【六、处理机控制与杂项操作指令】
【1-标志处理指令】clc、stc、cmc、cld、std、cli、sti
【2-其他处理机控制与杂项操作指令】nop、hlt、wait、esc、lock
- nop:无操作。机器码占一个字节。
- hlt:暂停机。等待一次外中断,之后继续执行程序。
- wait:等待。等待外中断,之后仍继续等待。
- esc:换码。
- lock:封锁。维持总线的锁存信号,直到其后的指令执行完。
7.4 汇编伪操作汇总
“汇编指令”对应机器指令,在程序运行期间由计算机执行。而“伪操作”则是在汇编程序对源程序汇编期间,由汇编程序处理的操作,可以完成如数据定义、分配存储区、指示程序结束等功能。也就是说,本节介绍的“伪操作”都是对汇编指令的简化,伪操作本身并不对应机器指令,但可以有效的分配内存空间,改善程序可读性。
【一、处理器选择伪操作】
.8086
:选择 8086 指令系统。.286
/.286P
:选择 80286 指令系统、选择保护模式下的 80286 指令系统。.386
/.386P
:选择 80386 指令系统、选择保护模式下的 80386 指令系统。.486
/.486P
:选择 80486 指令系统、选择保护模式下的 80486 指令系统。.586
/.586P
:选择 Pentium 指令系统、选择保护模式下的 Pentium 指令系统。
【二、段定义伪操作】
之前一直在使用的段定义伪操作 assume
有很多选项,下面介绍完整的段定义伪操作。
.MODEL 存储模式 [,其他选项]assume 段寄存器:段名[,其他段声明]
段名 segment [定位类型] [组合类型] [使用类型] ['类别']
...
段名 ends
- 存储模式:指定在内存中如何安放各段。可选参数有:
tiny
:所有代码和数据在同一个段内,全部加起来小于64K。small
:一个代码段和一个数据段,都小于64K,于是可以使用“近访问”。medium
:一个代码段和多个数据段。compact
:多个代码段和一个数据段。large
:多个代码段和多个数据段,都小于64K,都支持“远访问”。huge
:多个代码段和多个数据段,并且允许单个数据结构超过64KB。flat
:用于保护模式下的平面内存模型。适用于32位或64位的操作系统,如Windows,所有代码和数据在一个4GB的线性地址空间内。
- 定位类型(align_type):段空间的对齐策略,可选参数有
BYTE
(以字节为单位)、WORD
(字)、DWORD
(双字)、PAGE
(页)。- 组合类型(combine_type):多文件组织时,对于存储空间共享、段空间共享的策略。可选参数有
PRIVATE
(私有段)、PUBLIC
(公有段)、COMMON
、STACK
(栈段)、AT
、exp
。- 使用类型(use_type):段使用内存的长度。可选参数有
USE16
()、USE32
。- 类别(class)
于是便可以使用伪操作对段的定义进行简化:
; 代码段 .code [name]; 数据段 .data ; 定义并初始化数据段。 .data? ; 定义但不初始化数据段(没有给初始化值)。 .fardata [name] ; 需要远访问的数据段 .fardata? [name]; 常量数据段 .const; 栈段 .stack [size]
【代码示例】使用伪操作简化 Hello world! 程序。
【三、程序开始和结束伪操作】
TITLE text
:给程序加标题NAME module_name
:给程序加模块名END [label]
:程序结束标志,里面的’label’就是标志程序开始的标号。.STARTUP
:取得数据段的段地址,标志着程序的开始。masm5.0/5.1不支持,需要更高版本。.EXIT [return_value]
:等价于调用21h中断的4ch号中断。masm5.0/5.1不支持,需要更高版本。
【四、数据定义及存储器分配伪操作】
前面我们其实已经使用很多次数据段的定义,定义时需要使用“助记符”指明数据类型。格式和常见示例如下:
助记符:
DB
(byte)、DW
(16bit)、DD
(双字)、DF
(三字)、DQ
(四字)、DT
(五字)。
; 格式:[变量] 助记符 操作数 [,操作数, …] [;注释]; 示例1:定义不同类型的数据
DATA_BYTE DB 10,4,10H,? ; ? 表示随机值
DATA_WORD DW 100,100H,-5,? ; ? 表示随机值; 示例2:数据的直接定址表
PAR1 DW 100,200
PAR2 DW 300,400
ADDR_TABLE DW PAR1,PAR2; 示例3:使用dup快速定义数据
VAR DB 100 DUP (?) ; ? 表示随机值
DB 2 DUP (0,2 DUP(1,2),3) ; 嵌套使用dup
但上述同一个内存空间定义好后,就只能使用一种数据类型访问,这显然非常不方便。于是还有一个伪操作 label
,可以使同一变量(同一空间)具有不同的类型。也就是从不同的角度使用同一段内存。
; 变量名 LABEL type; 示例:label需要写在数据段定义的上方
BYTE_ARRAY LABEL BYTE
WORD_ARRAY DW 50 DUP (?)
; 使用标号WORD_ARRAY时,以“字(16bit)”为单位操作内存。
; 使用标号BYTE_ARRAY时,以“字节(8bit)”为单位操作同一段内存。
【五、表达式赋值伪操作】
汇编语言的伪操作 EQU
可以将数值赋给表达式。重复定义时,以最新定义为准。注意这些值都是编译前就能确定,而不是需要执行机器指令才能得到。但注意,EQU
的功能没有C语言中的 #define
,具体见 “7.7节-宏汇编”。
; 表达式名 EQU 表达式
ALPHA EQU 9 ; 使用ALPHA表示9
BETA EQU ALPHA+18 ; 使用BETA表示9+18=27
BB EQU [BP+8] ; 使用BB表示偏移地址[bp+8]内存单元中的值; 表达式名 = 表示式(伪操作,允许重复定义)
EMP = 7 ; 使用EMP表示7
EMP = EMP+1 ; 重复定义,更新EMP为8; 实际应用:下面三行等价
mov ax,beta+emp ; 上面定义了beta=27,emp=8
mov ax,35
mov ax,23h
【六、地址计数器与对准伪操作】
- ORG伪操作:设置当前地址计数器的值。
- 地址计数器 $ :保存当前正在汇编的指令的地址。
- ALIGN伪操作:保证数组边界从2的整数次幂地址开始。对于16位/32位/64位操作系统有很大作用。
- EVEN伪操作:使下一个变量或指令开始于偶数字节地址。
seg1 segment; org伪操作ORG 10 ; 偏移地址指定为10,否则段开头默认为0VAR1 DW 1234HORG 20 ; 偏移地址指定为20VAR2 DW 5678H; 地址计数器$ORG $+8 ; 偏移地址后移8VAR3 DW 1357H; align伪操作ALIGN 4 ; 偏移地址后移,直到能被4整除ARRAY db 100 DUP(?); even伪操作A DB 'morning' ; 奇数个地址EVEN ; 偏移地址后移,直到第一个偶数地址B DW 2 DUP (?) ; 地址从偶数开始
SEG1 ENDS
【七、基数控制伪操作】
.RADIX 表达式
:可以修改基数。不修改时,计算过程默认使用十进制,基数为十。
; 默认基数是十进制
mov bx,0ffh ; 十六进制必须加后标,显式声明
mov bx,178 ; 默认是十进制; 修改基数为十六进制
.RADIX 16
mov bx,0ff ; 此时默认是十六进制的00ffh
mov bx,178d ; 十进制必须加后标,显式声明
7.5 汇编操作符汇总
“操作符”用于在操作数中,通过操作符,将常数、寄存器、标号、变量等,组合成表达式,实现求值的目的。这些值在汇编期间确定,而不是在运行期间调用机器指令计算得到的。分类如上图,现在来简单介绍。
【一、算术操作符】
- 五个操作符:
+
、-
、*
、/
、Mod
。
; 利用立即数计算
BLOCK DB 25*80*2 DUP(?) ; 空间大小为4000个字节; 利用标号的偏移地址计算
ARRAY DW 1,2,3,4,5,6,7
ARYEND DW ?
MOV CX, (ARYEND-ARRAY)/2; 常见应用
ADD AX, BLOCK+2 ; 符号地址±常数,常用
MOV AX, BX+1 ; 编译时无法确定,错误的代码!
MOV AX, [BX+1] ; 寄存器间接寻址
【二、逻辑和移位操作符】
- 六个操作符:
AND
、OR
、XOR
、NOT
、SHL
、SHR
。
; 应用实例一
OPR1 EQU 25 ; 也就是00011001B
OPR2 EQU 7 ; 也就是00000111B
AND AX, OPR1 AND OPR2 ; 等价于AND AX,1
MOV AX, 0FFFFH SHL 2 ; 等价于MOV AX,0FFFCH; 应用示例二
PORT_VAL = 61H
IN AL, PORT_VAL ; 等价于 in al,61H
OUT PORT_VAL AND 0FEH, AL ; 只有最低位变成0,也就是60H
【三、关系操作符】
- 六个操作符:
EQ
、NE
、LT
、LE
、GT
、GE
。- 计算结果:真为 0FFFFH,假为 0000H。
MOV FID, (OFFSET Y - OFFSET X) LE 128
X: 代码...
Y: 代码...
; 若 ≤128(真) 汇编结果等价于:MOV FID,-1(有符号数)
; 若 >128(假) 汇编结果等价于:MOV FID,0
【四、数值回送操作符】
- 五个操作符:
OFFSET
(取偏移地址)、SEG
(取段地址)、TYPE
(判断类型)、LENGTH
、SIZE
。
type
可能返回的值:1(DB)、2(DW)、4(DD)、6(DF)、8(DQ)、10(DT)、-1(Near)、-2(Far)、0(常数)。length
的功能:回送由DUP定义的变量的单元数,其它情况回送1。size
的功能:返回length*type
的值。
ARRAY DW 100 DUP (?)
TABLE DB 'ABCD'ADD SI, TYPE ARRAY ; 等价于 ADD SI, 2
ADD SI, TYPE TABLE ; 等价于 ADD SI, 1
MOV CX, LENGTH ARRAY ; 等价于 MOV CX, 100
MOV CX, LENGTH TABLE ; 等价于 MOV CX, 1
MOV CX, SIZE ARRAY ; 等价于 MOV CX, 200
MOV CX, SIZE TABLE ; 等价于 MOV CX, 1
【五、属性操作符】
- 八个操作符:
PTR
、段操作符冒号:
、SHORT
、THIS
、HIGH
、LOW
、HIGHWORD
、LOWWORD
。
; ptr指明数据类型属性
; 调用格式:类型 PTR 表达式
MOV WORD PTR [BX], 5; 段操作符“:”
MOV ES:[BX], AL ; bx的段地址默认时ds,若不是,必须指明段地址。; short标明是短转移
; 调用格式:SHORT 标号
JMP SHORT NEXT; 将紧跟在this后面的数据类型作为当前数据
; 调用格式:THIS 类型
TA EQU THIS BYTE
TD DW 1234H
NEXT EQU THIS FAR
MOV AX,2; HIGH和LOW可以分别取出高字节、低字节
CONS EQU 1234H
MOV AH, HIGH CONS
MOV AL, LOW CONS
7.6 汇编过程
- 目标文件OBJ:包含了可重定位的机器代码和符号信息。
- 交叉引用文件CRF:包含标识符(段名、过程名、变量名、标号)在源程序中定义的位置和被引用的位置,对源程序所用的各种符号进行前后对照的文件。CRF文件 是一个二进制文件不能直接读取,可以通过 cref 软件将其转换成文本文件查看。
- 列表文件LST:将源程序、目标程序、错误信息列表,以供检查程序用。信息全面,用于早期的调试。
注:“3.2节-由源程序到程序运行”已经介绍过。
前面已经介绍过.exe文件由汇编、连接得到,如上图,那么具体的汇编过程是什么样子呢?汇编过程将源文件转换成目标文件,需要经过两次扫描,如下:
- 第一次汇编:确定地址,翻译成各条机器码,字符标号原样写出。
- 第二次汇编:标号代真,将字符标号用计算出的地址值或偏移量代换。
注:伪指令不产生机器码,汇编指令与机器指令一一对应。
7.7 宏汇编
【预处理含义】
- 在对程序进行编译之前,根据预处理命令对程序作相应处理。
- 经过预处理后编译程序才可以对程序进行编译等处理,得到可供执行的目标代码。
编译预处理发生在编译之前。C语言中的以“#”开头的一些预处理指令非常好用,可以帮助我们一键修改代码中的参数、控制代码结构等,比如宏定义 #define
、文件包含 #include
、条件编译 #ifdef
等。汇编语言中也有类似于 #define
的宏定义,支持用户自定义的宏指令。宏必须先定义后调用,编译前汇编程序会把宏调用展开,也就是将宏定义体复制到宏指令位置,并使用实参代替虚参,然后才进行编译。如上图汇编语言指令分类便可以扩展为三个。宏定义使用 MACRO
、ENDM
完成,具体格式如下:
; 宏定义
macro_name MACRO [哑元表] ; 形参/虚参; 宏定义体
ENDM; 宏调用
macro_name [实元表] ; 实参
注:编译程序 masm.exe 名称中的“asm”表示汇编语言,第一个 “m” 就是 宏(macro)。所以 masm 就意为“宏汇编”。
下面给出宏定义和子程序的区别:
- 宏定义:编译时会直接进行宏展开。优点是参数传送简单,执行效率高;缺点是代码占用内存空间大。
- 子程序:每次调用都会跳转到相应内存 。优点是模块化,省内存;缺点是程序开销大。
最后直接给出几个使用“宏定义”简化代码的示例:
;;;;;;;;;;;;;;;;;寄存器压栈;;;;;;;;;;;;;;;;;
; 宏定义
push_reg macropush axpush bcpush cxpush dxpush sipush di
endm; 宏调用
push_reg
;;;;;;;;;;;;;;;;编写乘法函数;;;;;;;;;;;;;;;;
; 宏定义
multiply macro opr1,opr2,resultpush axpush dxmov ax,opr1imul opr2 ; 有符号数乘法mov result,ax ; 默认舍弃了高16位(dx)的运算结果pop dxpop ax
endm; 宏调用
multiply cx,var,xyz[bx]
;;;;;;;;;;;;;;;;转化成绝对值;;;;;;;;;;;;;;;;
; 宏定义
absolate macro oprlocal next ; 声明为局部标号,防止干扰其他程序cmp opr,0jge nextneg opr ; 若为负数,求反
next:
endm; 宏调用
absolate ax
;;;;;;;;;;变元也是操作码的一部分!!;;;;;;;;;
; 宏定义
leap macro cond,labj&cond lab ; 注意看这里的奇妙用法!!!!
endm; 宏调用
leap z,there ; 等价于 jz there !!
leap nz,here ; 等价于 jnz here !!
7.8 宏库
我们可以将宏定义分类放到不同的宏库文件中,也就是“宏库”,文件后缀为.mac。然后在汇编源程序文件中,使用伪指令 include
将宏库包含进行,即可调用。于是我们显然可以直接调用别人写好的宏库,有利于构建汇编生态。
7.9 条件汇编
在汇编过程中,根据条件把一段源程序包括在汇编语言程序内或者排除在外,称之为“条件汇编”。汇编语言中的条件汇编格式为 ifxxx 条件 ... [else] ... endif
,其中 else
可以省略不写,并且没有 elseif
之类的伪指令。下面给出常见的if伪指令:
IF 表达式 ; 表达式不为0,则汇编
IFE 表达式 ; 表达式=0,则汇编IF1 ; 在第一遍扫视期间满足条件
IF2 ; 在第二遍扫视期间满足条件IFDEF 符号 ; 符号已定义,则汇编
IFNDEF 符号 ; 符号未定义,则汇编IFB <自变量> ; 自变量为空,则汇编
IFNB <自变量> ; 自变量不为空,则汇编IFIDN <字符串1>,<字符串2> ; 串1与串2相同
IFDIF <字符串1>,<字符串2> ; 串1与串2不同
7.10 重复汇编
“重复汇编”用于连续产生完全相同或基本相同的一组代码。本节来介绍三个重复汇编的伪操作REPT
、IRP
、IRPC
,本质上都是宏定义。
【重复伪操作
REPT
】; REPT调用格式 REPT 表达式; 重复块 ENDM
【代码示例1】将字符’A’到’Z’的ASCII码按顺序填入数组TABLE。
CHAR = 'A' TABLE LABEL BYTE ; 重复块 REPT 26DB CHARCHAR = CHAR+1 ENDM
【不定重复伪操作
IRP
】; IRP调用格式 IRP 哑元,<自变量表>; 重复块 ENDM
【代码示例2】生成一组入栈指令。
; 根据自变量表重复相应次数 IRP REG,<AX,BX,CX,DX>PUSH REG ENDM; 展开后的等价效果 push ax push bx push cx push dx
【不定重复伪操作
IRPC
】; IRPC调用格式 IRPC 哑元,字符串; 重复块 ENDM
【代码示例3】生成存储字符串的汇编语句。
; 根据字符串的字符数量重复操作 array label byte IRPC K, 12345 ; 注意这个字符串不加引号!!db 'NO.&K' ENDM; 实现效果 db 'NO.1' db 'NO.2' db 'NO.3' db 'NO.4' db 'NO.5'
7.11 80x86汇编
CPU | 地址总线宽度 | 寻址能力 | 数据总线宽度 | 一次传送数据 |
---|---|---|---|---|
8080 | 16 | 640KB | 8 | 1B |
8088 | 20 | 1MB | 8 | 1B |
8086 | 20 | 1MB | 16 | 2B |
80286 | 24 | 1 6MB | 16 | 2B |
80386 | 32 | 4GB | 32 | 4B |
之前一直介绍的是8086汇编,本节介绍一下80x86汇编。80x86指的是Intel公司一系列的CPU,如上表中CPU性能一览中的系列,地址总线宽度由16位晋升到32位,到现在已经是64位(表中未给出)。本节就从下图所给的这六个方面,来介绍80386相比于8086所带来的提升。
【80x86的寄存器结构】
80x86的寄存器显而易见的比8086位宽更大、数量更多。如下图左侧的“程序可见寄存器”中,粉色表示8086/8088/80286的寄存器,绿色则表示80386扩展后的寄存器。可以看到,其中“通用寄存器”、“专用寄存器”的位宽展宽(加上了Extend前缀);“段寄存器”则是新增两个。而下图右侧给出了“专用寄存器”中的“标志寄存器”的扩展细节。如下图所示:
【80x86的寻址方式】
在寻址方式,80386兼容之前所有的寻址方式,但同时也引入了新的“比例寻址”,方便操作更大的元素,而不是像8086那样寻址单位只能是一个字节:
地址成分 | 16位寻址(8086) | 32位寻址(80386) |
---|---|---|
基址寄存器 | BX、BP | 任何32位通用寄存器 |
变址寄存器 | SI、DI | 除ESP外的任何32位通用寄存器 |
比例因子 | 1 | 1、2、4、8 |
- 8086的寻址方式
- 立即寻址,例:
MOV AX, 3069H
。- 寄存器寻址,例:
MOV AL, BH
。- 直接寻址,例:
MOV AX, [2000H]
。- 寄存器间接寻址,例:
MOV AX, [BX]
。- 寄存器相对寻址,例:
MOV AX, COUNT [SI]
。- 基址变址寻址,例:
MOV AX, [BP][DI]
。- 相对基址变址寻址,例:
MOV AX, MASK [BX][SI]
。
- 80386新增
- 基址比例变址寻址方式,例:
MOV ECX, [EAX][EDI*4]
。- 相对基址比例变址寻址方式,例:
MOV EAX, TABLE [EBP][EDI*4]
。
注:对于元素大小为2、4、8字节的数组,可以在变址寄存器中给出数组元素的下标,依靠比例因子,直接将下标转换为变址值。
【80x86的指令系统】
不仅指令集进行了扩展,在指令的使用方式上也进行了扩展:
- 指令集的32位扩展
- 所有16位指令都可扩展到32位,比如
MOV EAX,1
。- 可使用32位的存储器寻址方式,比如
MOV EAX, [EDX]
。
- 使用方式的扩展
IMUL
:将 单操作数乘法指令 扩展成 双操作数指令、三操作数指令,比如双操作数格式是IMUL REG, SRC
。PUSH
:允许使用立即数寻址方式,比如PUSH 36H
就是对立即数压栈。移位指令
:移位次数可用8位立即数(1~31),而不再需要cl指示移动位数,比如SHL EAX, 16
。
【80x86新增指令】
下面简单列出,详细描述可以查找手册。
新增指令 | 描述 | 新增指令(测试类) | 描述 |
---|---|---|---|
MOVSX | 带符号扩展传送 | BT | 位测试 |
MOVZX | 带零扩展传送 | BTS | 位测试并置1 |
PUSHA/PUSHAD | 所有寄存器进栈 | BTR | 位测试并置0 |
POPA/POPAD | 所有寄存器出栈 | BTC | 位测试并变反 |
LFS/LGS/LSS | 取地址 | BSF | 正向位扫描 |
PUSHFD | 标志进栈 | BSR | 反向位扫描 |
POPFD | 标志出栈 | SHLD | 双精度左移 |
CWDE | 字转换为双字EAX | SHRD | 双精度右移 |
CDQ | 双字转换为4字EDX EAX | INSB/INSW/INSD | 串输入 |
BSWAP | 32位寄存器的字节次序变反 | OUTSB/OUTSW/OUTSD | 串输出 |
XADD | 交换加 | ||
CMPXCHG | 比较并交换(486) | ||
CMPXCHG8B | 比较并交换8字节(Pentium)A |
【条件设置指令】
之前使用串传送指令时,要使用 cld
/std
指明传送更新的方向,下面的“条件设置指令”功能类似,也是通过改变相应的标志位设置条件,下面简单列出:
- 根据单个条件标志的值把目的字节置 1
SETZ
/SETE
、SETNZ
/SETNE
SETS
/SETNS
、SETO
/SETNO
SETP
/SETPE
、SETNP
/SETPO
SETC
/SETB
/SETNAE
、SETNC
/SETNB
/SETAE
- 比较两个无符号数,根据比较结果把目的字节置 1
SETB
/SETNAE
/SETC
、SETNB
/SETAE
/SETNC
SETBE
/SETNA
、SETNBE
/SETA
- 比较两个带符号数,根据比较结果把目的字节置 1
SETL
/SETNGE
、SETNL
/SETGE
SETLE
/SETNG
、SETNLE
/SETG
【Intel系列微处理器的3种工作模式】
工作模式 | 芯片类型 | 工作特点 |
---|---|---|
实模式 | 8086 | 8086/ 8088支持的单任务工作模式,优势在于程序可以直接访问系统内存和硬件设备。 实模式现在仍然用于小型的嵌入式设备。 |
保护模式 | 80286 | 程序获得独立的内存段,也会阻止使用自身段范围之外的内存,提供对多任务环境的支持。 缺点是不兼容上一代的实模式。 |
虚拟8086模式 | 80386以上 | 可以从保护模式切换到实模式,提供对原生实模式程序的支持。 |
注:Windows xp及之前的版本都支持MS-DOS实模式,但是现代的win8、win10也不再支持实模式,因为太久远了。所以前面的DOS开发只能使用纯软件模拟,而不能开虚拟机。
最后总结一下,上述虽然介绍了很多新特性,但是不要觉得学习8086汇编没有用。8086的设计思想仍然是核心,熟练掌握8086汇编的原理,对于后续扩展到32位、64位的汇编有很大帮助。
7.12 汇编语言集成开发环境
之前在开发汇编程序时,我们一直使用的是DOS沙盒(如上图红色),本节按照上图,总结一下可以支持汇编语言的开发环境。
- DOSBox:命令行窗口。
之前按一直在用的,命令行的形式有利于理解底层原理。
- emu8086:图形窗口。
EMU8086集编辑器、汇编器、调试器于一身,提供了一个8086CPU的模拟器,用窗口形式,提供了对8086汇编的支持,此外,还提供了循序渐进的教程和大量的示例。
下载连接:https://emu8086-microprocessor-emulator.en.softonic.com/
教程:软件菜单栏“Help”选项,或者 https://yassinebridi.github.io/asm-docs/
- masm32
MASM32是一个由个人开发的包含了不同版本工具组建的汇编开发工具包。
MASM32的汇编编译器是MASM6.0以上版本中的Ml.exe,资源编译器是Microsoft Visual Studio中的Rc.exe,32位链接器是Microsoft Visual Studio中的Link.exe,同时包含有其他的一些如Lib.exe和DumpPe.exe等工具。
官网:http://www.masm32.com/
- visual studio系列
微软自己开发的开发工具,下一节介绍。
7.13 用Visual studio开发汇编程序
Microsoft Visual Studio(简称VS)是微软公司的开发工具包系列产品。VS是一个基本完整的开发工具集,它包括了整个软件生命周期中所需要的大部分工具,如UML工具、代码管控工具、集成开发环境(IDE)等等。VS支持多种程序设计语言,不仅是常见的C/C++、Web语言,汇编也是支持的!
视频“14-用Visual studio开发汇编程序”中演示了如何使用 Visual Studio 2008 创建项目、编译汇编程序、单步调试。
7.14 高级语言的指令级调试
如上图所示,“汇编语言”与“机器语言”是一对一关系,每一条汇编语言指令对应一条机器语言指令。“高级语言”与“汇编语言”则是一对多关系,C语言的一条语句会扩展为多条汇编语言指令或机器指令。
于是根据这样的对应关系,显然我们在调试高级语言时,也可以直接查看底层的汇编指令。在Visual Studio中创建好Cpp项目后,首先需要在“工具”>“选项”>“调试”下,选择“启用地址级调试”,然后在调试期间选择“窗口”>“反汇编”,即可打开反汇编窗口,如下图所示。最后给出
7.15 反汇编(逆向工程)
和上一小节将高级语言翻译成汇编语言不同,反汇编(逆向工程)指的是将机器语言代码转换为汇编语言代码,由低级转高级。反汇编常用于软件破解(例如找到它是如何注册的,从而解出它的注册码或者编写注册机)、外挂技术、病毒分析、逆向工程、软件汉化等领域。从正面来说, 学习和理解反汇编,对软件调试、漏洞分析、OS的内核原理及理解高级语言代码都有相当大的帮助,在此过程中我们可以领悟到软件作者的编程思想。总之一句话:软件一切神秘的运行机制全在反汇编代码里面。下面给出反汇编的工具:
- 在DOS环境中:可以直接使用debug工具中的
u
命令,会将二进制代码翻译成汇编代码。- 在Windows环境中:可以使用微软开发的 windbg 软件,使用方法和上述DOS中的debug工具相同。
7.16 混合编程
“混合编程”是指使用两种或两种以上的程序设计语言来开发应用程序的过程。之所以使用混合编程?是因为程序设计语言有多种,它们有各自的优势和不足,混合编程可以充分利用各种程序设计语言的优势。如上图所示,没有引入混合编程之前,每个编程语言只能调用自己的库,但是引入混合编程,任意语言的主程序都可以调用任意语言的子程序库,只要专注于中间的目标程序.obj和程序库.lib能够连接在一起即可。于是,混合编程的难点就是不同语言模块之间的接口需要保持一致,也就是“编程接口规范”。
最后给出一个C++与汇编语言混合编程的一个方案,在.cpp文件中使用 __asm
标出汇编程序的位置,核心的循环功能使用汇编语言完成:
【代码示例】将字符串 “abcd” 中的每个字符都转换成大写。
#include <iostream> using namespace std; int main() {char a[10] = "abcd";//// 汇编程序起始标志__asm{ ; 初始压栈push ebxpush eaxpush ecx; 循环将小写字符都转换成大写lea ebx, amov ecx, 4tran:mov al,byte ptr [ebx]sub al,20Hmov [ebx], alinc ebxloop tran; 出栈推出pop ecxpop eaxpop ebx}/printf("%s\n",a);getchar();return 0; }
7.17 文件结构分析
- BYTE是8bit、UINT是16bit、DWORD是32bit、LONG是64bit。
BMP(Bitmap,位图)是Windows操作系统中的标准图像文件格式,其格式如上图所示。显然根据上述格式,我们可以使用汇编语言、C语言等读取bpm文件的任意信息。下面给出查看图像二进制数据的工具:
- DOS环境:万年不变的Debug工具。使用
debug xxx.bmp
将图像加载到内存,然后就可以使用d
指令观察图像文件的所有数据了。但显然比较原始。- Windows环境:Binary Viewer 可以用来查看文件的二进制数据,下载连接:https://binary-viewer.en.softonic.com/
当然图像数据格式还有很多,比如png、tiff、jpg之类的。当我们对这些格式的二进制文件头了如指掌后,是不是直接根据二进制数据就可以反推出图像格式呢?唯手熟尔。进一步扩展到其他领域,也就掌握了一项根据二进制数据推演文件格式这个非常了不起的技能。
7.18 完结撒花
都不说话是吧,看我直接一手手动完结撒花~~🥳🥳😎