汇编语言基础

掌握汇编语言是学习逆向分析和免杀技术的关键,文章对以往所学习的汇编知识进行了梳理和总结,基于Win32汇编,详尽记录了学习汇编所需基础知识和汇编指令及用法,旨在巩固汇编基础,夯实基本功。

参考书籍 《Win32汇编语言程序设计》

一、基础知识储备

(一) 计算机语言

计算机语言分为机器语言、汇编语言、高级语言三大类:

  • 机器语言:由0和1组成的指令 (采取01二进制原因是便于底层晶体管逻辑实现),也是CPU唯一能解释执行的命令,可辨度差,难以调试排查故障。
  • 汇编语言:机器语言的助记符,由汇编指令、伪指令和其他符号组成,汇编语言经过编译转换成机器语言,汇编指令与机器指令一一对应,伪指令和其他符号帮助编译器将汇编指令转换为机器指令,但本身并不对应产生机器指令。汇编语言增加了可读性,但其依赖于底层硬件,通用性较差,大量助记符的记忆和辨识仍有一定难度。
  • 高级语言:语法和结构类似英文,不依赖于底层硬件,多层封装,通用性强,根据应用领域和需求设计有多种高级语言:C、C++、Java、Python、JavaScript等等,功能实现方便,便于记忆和辨识。高级语言分为编译型和解释型两种:编译型语言经过编译、汇编、链接最终形成可执行二进制文件,一次编译就可以脱离开发环境运行,执行效率高,但不同平台通用性差,不便移植;解释型语言由相应解释器逐行解释运行,不同平台只要有解释器即可运行,通用性强,但因为要边解释边运行,导致执行效率低。

(二) CPU架构和指令集

CPU架构即CPU内部硬件架构,也称为微架构,是厂商对某一系列CPU所定义的规范,它规定了CPU内部硬件之间的逻辑运算方式,CPU架构和指令集之间有着对应关系。目前CPU指令集主要分为复杂指令集 (CISC) 和精简指令集 (RISC) 两类:CISC指令数量众多且复杂,每条指令长度不同,运行时间较长,但每条指令可以处理的工作较多;RISC指令精简,每个指令运行时间较短,完成的工作也比较简单,执行性能佳,功耗少。

CISC架构CPU以x86为主,RISC架构CPU以ARM / MIPS / PowerPC为主,如下介绍:

  • x86:x86是一个Intel通用计算机系列的标准编号缩写 (8086、80286、80386、80486、80586等),1978年Intel推出了首款基于x86架构的16位处理器:8086CPU (x86-16或IA-16架构),1985年又推出了首款32位的处理器:80386 (IA-32架构),其同样基于x86架构。市场上巨大的成功使得x86架构迅速成为PC业界标准,x86系列采用复杂指令集 (CISC),具有向下兼容的特点,包括后续由AMD先发制人发明的AMD64,或称x86-64架构,可以看成是x86架构的超集。Intel为了与AMD64竞争,发明了不向下兼容x86的IA64架构,但市场经营惨淡,IA64最终退出市场,Intel也妥协于AMD64。
  • ARM:ARM指 Advanced RISC Machines,采用精简指令集 (RISC),第一款RISC微处理器由英国Acorn公司设计,1991年Acorn成立为新的ARM公司,与一般商业模式不同,ARM公司既不生产芯片也不销售芯片,它只出售芯片技术授权,ARM架构以其低成本、高性能、低功耗以及兼容16位 (Thumb) / 32位 (ARM) 指令集的优势,迅速占领了移动端和嵌入式产品市场。
  • MIPS:MIPS指 Microprocessor without Interlocked Piped Stages,其机制是尽量利用软件办法避免流水线中的数据相关问题,采用精简指令集 (RISC),它最早在80年代初由斯坦福大学Hennessy教授领导的研究小组研制而出,目前占领诸如电视盒子、打印机等一定规模的小众嵌入式产品市场。
  • PowerPC:PowerPC指 Performance Optimization With Enhanced RISC – Performance Computing,或简称为PPC,采用精简指令集 (RISC),二十世纪九十年代,由IBM、Apple和Motorola公司共同开发,目前在通信、工控、航天国防等要求高性能和高可靠性的领域得到广泛应用。

(三) 寄存器

CPU主要由运算器、控制器以及多个寄存器组成,寄存器是CPU内部的信息存储单元,不同架构CPU之间寄存器类型有所区别。有很多寄存器如IR、MAR、MDR等是CPU内部使用的,对程序员而言是透明的,我们无法直接对其进行操作,而我们需要关注的是编程可以直接操作的寄存器,这类寄存器主要包括通用寄存器、段寄存器、指令指针寄存器和标志寄存器,这里先以8086寄存器为基础对x86系列寄存器进行介绍。

1.8086 寄存器

8086 CPU是8086系列CPU的基础,它有16根数据线和20根地址线,可寻址空间为1MB,虽然后续出现32、64位的高性能CPU,但其寄存器主要都是在8086寄存器上的扩展,向下兼容。8086 CPU中对程序员可见的寄存器分为通用寄存器、段寄存器、指令指针和标志寄存器4类共14个,均为16位,具体如下:

(1) 通用寄存器 (8个)

通用功能为传送和暂存数据,也可参与算术逻辑运算,并保存运算结果,除此之外每个寄存器还有一些特殊用途。

① 数据寄存器

  • AX (Accumulator):累加寄存器,可分为两个独立的8位寄存器,高8位AH,低8位AL。在除法运算中,AX存储被除数,乘法运算中,AX存储被乘数,如果运算数或运算结果是32位,则低16位存在AX中,高16位存在DX中
  • BX (Base):基地址寄存器,可分为两个独立的8位寄存器,高8位BH,低8位BL,用于寻址,存放某个存储单元地址的偏移,格式为:段:[偏移],不指明段则默认为DS,如:MOV AH,DS:[BX]MOV AH,[BX]等效
  • CX (Count):计数器寄存器,可分为两个独立的8位寄存器,高8位CH,低8位CL,用于循环计数
  • DX (Data):数据寄存器,可分为两个独立的8位寄存器,高8位DH,低8位DL。在运算中,如果运算数或运算结果是32位,则低16位存在AX中,高16位存在DX中

② 指针寄存器

  • SP (Stack Pointer):堆栈指针寄存器,SS:SP 指向栈顶元素,使用PUSH指令压栈时 SP=SP-1,使用POP指令弹栈时 SP=SP+1 (栈向下增长)
  • BP (Base Pointer):基指指针寄存器,用于栈寻址,不指明则使用SS作为默认段基地址

③ 变址寄存器

  • SI (Source Index):源变址寄存器,用于寻址,可存放源操作数的偏移地址,不指明则使用DS作为默认段基地址
  • DI (Destination Index):目的变址寄存器,用于寻址,可存放目的操作数的偏移地址,不指明则使用DS作为默认段基地址

(2) 段寄存器 (4个)

因为8086 CPU数据总线宽度为16位 (寻址64Kb),但是地址总线宽度为20位 (寻址1Mb),因此采用 [段地址*16+偏移地址] 的方式进行寻址,即段地址左移4位 (空缺补0) 后加上偏移地址,段寄存器的意义就在于为不同的段提供段地址。

  • CS (Code Segment):代码段寄存器,存放代码段基地址,CS:IP 指向CPU当前将要读取的指令的地址,当一个可执行文件加载到内存中以后,CS:IP 便指向了这个可执行文件的起始地址
  • DS (Data Segment):数据段寄存器,存放数据段基地址,除了BP寄存器外,其余寄存器都默认使用DS寄存器的值作为默认段基地址
  • SS (Stack Segment):堆栈段寄存器,存放堆栈段基地址,SS:SP 指向栈顶元素,使用PUSH指令压栈时 SP=SP-1,使用POP指令弹栈时 SP=SP+1
  • ES (Extra Segment):附加段寄存器,存附加段基地址

32/64位CPU地址总线和数据总线宽度保持一致,直接使用寄存器就可以寻址整个内存空间,因此 WIN32及之后汇编中不再通过段寄存器将内存进行分段,也无需再使用段寄存器进行寻址。

(3) 指令指针和标志寄存器: (2个)

  • IP (Instruction Pointer):指令指针寄存器,CS:IP 指向CPU当前将要读取的指令的地址,当一个可执行文件加载到内存中以后,CS:IP 便指向了这个可执行文件的起始地址
  • FLAG:标志寄存器,每一个位都表示不同的状态,存储的信息通常又被称作程序状态字 (PSW)
    在这里插入图片描述
    CF:进位标志,运算结果最高位进位时置1
    PF:奇偶标志,运算结果低8位中“1”的个数为偶数时置1
    AF:辅助进位标志,运算结果低4位产生进位时置1
    ZF:零标志,运算结果为0时置1
    SF:符号标志,运算结果为负时置1
    TF:跟踪标志,置1时进入单步调试状态
    IF:中断标志,置1时开中断,置0时关中断,缺省值为1
    DF:方向标志,置1时串操作为减地址方式,置0时为增地址方式,缺省值为0
    OF:溢出标志,运算结果超出范围时置1
2.32/64位CPU寄存器

第一款应用于桌面计算机的32位CPU是Intel公司的80386,这也是x86系列中第一个32位CPU,采用IA-32架构,80386除了包含8086所有的寄存器,并把通用寄存器、指令指针寄存器、标志寄存器从16位扩充成32位之外 (名字变为EAX、EBX、ECX…),还增加了两个16位的段寄存器FS和GS:

  • FS (Flag Segment):标志段寄存器
  • GS (Global Segment):全局段寄存器

32位之后又发展出64位的CPU,第一款应用于桌面计算机的64位CPU是AMD公司的Athlon64,其寄存器在80386基础上把通用寄存器、指令指针寄存器、标志寄存器从32位扩充成64位 (名字变为RAX、RBX、RCX…),同时又增加了8个通用寄存器R8-R15 (因为通用寄存器原先有8个0-7,所以新增加的从编号8开始),x86系列寄存器对比如下图所示:
请添加图片描述

(四) CPU工作模式

8086是在纯DOS系统下运行的,之后80386+ CPU (IA32) 为向下兼容,让旧程序仍然可以在新的CPU上执行,设置了3种操作模式:实模式、保护模式、虚拟8086模式。纯DOS操作系统运行在实模式,Windows操作系统运行在保护模式,Windows操作系统下运行纯DOS应用程序属于虚拟8086模式,3种操作模式基本区别如下:

1.实模式

实模式下的存储器寻址方式和8086完全一致,采用 [段地址*16+偏移地址] 的方式进行寻址,即使是32位环境,也只能使用低20位,寻址空间为1MB。实模式是纯DOS环境,不支持多任务和优先级,用户程序可以随便修改系统中的数据,相当于工作在特权级ring0。所有操作系统在刚启动时都是实模式,运行自检和载入引导程序 (从BIOS中读取) 后再跳转到保护模式继续接下来的工作。

2.保护模式

Windows环境运行在保护模式下,使用32位地址,寻址空间达4GB,支持多任务和优先级,操作系统运行在ring0级别,用户程序运行在ring3级别 (操作系统只用到ring0和ring3),只有ring0级别才可以修改段寄存器,因此Win32汇编一般不再操作段寄存器。我们编写的汇编指令编译成可执行文件,在操作系统中直接运行就是在保护模式。

3.虚拟8086模式

虚拟8086模式是为了在Windows环境下 (保护模式) 运行纯DOS程序 (8086程序) 而设置的,如果DOS程序中有特权指令,则在Windows环境下运行会产生异常,这些指令可能被模拟实现,也可能被忽略。

(五) 计算机体系结构

篇幅较多,见文章:计算机体系结构简析

二、指令及用法

汇编语言由汇编指令、伪指令和其他符号三部分组成,汇编指令相当于机器指令的助记符,与机器指令一一对应,伪指令和其他符号帮助编译器将汇编指令转换为机器指令,但本身不产生机器指令。

(一) 汇编格式

汇编指令具有两种标准的格式:Windows下一般为Intel格式,Linux/Unix下一般为AT&T格式,一些典型区别如下表所示:

IntelAT&T
寄存器标识ax%ax
立即数格式1234h$0x1234
操作数格式mov ax,1234h
mov al, byte ptr 12h
mov $0x1234, %ax
movb $0x12, %al
寻址方式[ebx + 4*eax + section]
section[ebx + 4*eax]
section(%ebx, %eax, 4)
跳转格式jmp far section:offset
call far section:offset
ljump $section, $offset
lcall $section, $offset

(二) 汇编指令

指令是指编译后能生成机器码的语句,一条指令由4个部分组成:标号: 助记符 操作数 ;注释,这里基于Win32汇编,介绍指令的助记符和操作数的格式及用法,以Intel格式汇编指令为例,常用指令分类如下:

1.数据传送类指令
指令作用例子
mov dst,src数据传送mov ax,25 将立即数25传送到寄存器ax
xchg oprd1,oprd2数据交换oprd1/2同时发生变化,oprd可以是寄存器或存储器操作数,但不能是立即数或段寄存器,不能同时是存储器操作数。
xhcg [ax],bx ax指向的数据与bx进行交换
xlat[b]字节查表转换隐含包括两个操作数:ebx和al,将以ebx为字符数组首地址、al值为下标的元素的值传递给al,即al<–[ebx+al]
push src进栈指令push ax 将寄存器ax中的数据压栈
pusha[d]进栈指令依次将[e]ax、[e]cx、[e]dx、[e]bx、[e]sp、[e]bp、[e]si、[e]di等8个通用寄存器的值入栈
pop dst退栈指令pop ax 将栈顶数据传送给寄存器ax
popa[d]退栈指令依次将栈顶数据弹出并传送给8个通用寄存器[e]ax、[e]cx、[e]dx、[e]bx、[e]sp、[e]bp、[e]si、[e]di
lea reg,mem取地址指令reg必须是寄存器,mem必须是存储器操作数,指令lea ebx,var 将变量var的偏移地址赋予ebx,相当于mov ebx,offset var;指令lea bx,[si] 将si指向数据的地址 (也就是si自身) 传送给bx
lds/les/lfs/lgs/lss reg,mem取段地址指令reg必须是寄存器,mem必须是存储器操作数,把内存单元的偏移地址传给指定的寄存器,并把段地址传给相应的段寄存器DS、ES、FS、GS、SS
cld/std清0 / 置1方向位DF为1时,串操作按减地址方式取字符,为0时按增地址方式取字符,缺省值为0
cld DF<–0
std DF<–1
clc/stc/cmc清0 / 置1 / 取反进位位cld CF<–0
std CF<–1
cmc取反CF位
cli/sti关中断 / 开中断IF值为1时,恢复可屏蔽外部中断响应,为0时不允许可屏蔽中断响应,缺省值为1
cli IF<–0
sti IF<–1
2.整数算数运算指令
指令作用例子
add oprd1,oprd2加法指令add bx,ax 令bx=bx+ax,影响flag标志位
adc oprd1,oprd2带进位加法指令adc bx,ax 令bx=bx+ax+cf
inc oprd自增指令inc bx 令bx=bx+1,不影响flag标志位
sub oprd1,oprd2减法指令sub bx,ax 令bx=bx-ax,影响flag标志位
sbb oprd1,oprd2减法指令sub bx,ax 令bx=bx-ax-cf
dec oprd自减指令dec bx 令bx=bx-1,不影响flag标志位
mul / imul oprd无符号 / 有符号乘法指令无符号的数据最高位作为数值位,有符号的数据最高位作为符号位。只有乘数在指令中显示,被乘数隐含固定用eax,结果存在edx|eax中。如果是8位或16位,则被乘数改为al或ax,结果存在ax或dx|ax中
div / idiv oprd无符号 / 有符号除法指令无符号的数据最高位作为数值位,有符号的数据最高位作为符号位。只有除数在指令中显示,被除数隐含固定用edx|eax,商存在eax中,余数放在edx中。如果除数是8位或16位,则被除数改为ax或dx|ax,商存在al或ax中,余数存在ah或dx中
cmp dst,src比较指令目的操作数减源操作数,按运算结果设置相关标志位,但不保存结果,为其后条件转移或循环指令提供转移用依据。影响标志位 af、cf、of、pf、sf、zf
3.逻辑运算指令
指令作用例子
and dst,src逻辑与两个操作数按位相与,结果存在目的操作数中,受影响标志位包括 cf(0)、of(0)、pf、sf、zf
and al,00001111b 如果al是数字字符,则该命令可实现将其转换为数值
or dst,src逻辑或两个操作数按位相或,结果存在目的操作数中,受影响标志位包括 cf(0)、of(0)、pf、sf、zf
or al,00110000b 如果al是数值,则该命令可实现将其转换为数字字符
not oprd逻辑非将操作数按位取反,不影响任何标志位
xor dst,src逻辑异或两个操作数按位异或,结果存在目的操作数中,受影响标志位包括 cf(0)、of(0)、pf、sf、zf
xor al,11110000b 将al高4位取反
test oprd1,oprd2逻辑比较测试两个操作数按位相与,但不保存结果,受影响标志位包括 cf(0)、of(0)、pf、sf、zf,通常其后会紧跟jcc跳转指令。
test ecx,ecx 检测ecx是否为0
4.跳转指令

(1) 无条件跳转指令jmp

(2) 条件跳转指令:根据标志寄存器中一个或多个标志位来决定是否转移,分为三大类:

  • 无符号数条件转移
  • 有符号数条件转移
  • 特殊算数标志位条件转移

跳转指令有很多种,最重要的是记住E(Equal)、N(Not Equal)、A(Above)、B(Below)、G(Greater)、L(Less)这六个字母的含义,条件跳转指令即这些字母的组合,其中AB用于无符号数条件转移,GL用于有符号数条件转移,如下命令:

指令作用检测条件
jmp label无条件转移无条件
je/jz label= 等于时转移ZF=1
jne/jnz label≠ 不等于时转移ZF=0
ja/jnbe label> 大于时转移 (无符号数比较)CF=0 && ZF=0
jae/jnb label≥ 大于等于时转移 (无符号数比较)CF=0
jb/jnae label< 小于时转移 (无符号数比较)CF=1
jg/jnle label> 大于时转移 (有符号数比较)SF=OF && ZF=0
jge/jnl label≥ 大于等于时转移 (有符号数比较)SF=OF
jl/jnge label< 小于时转移 (有符号数比较)SF≠OF && ZF=0
5.串操作指令

包括移串 (MOVS)、取串 (LODS)、存串 (STOS)、输入串、输出串、串比较、串扫描等7种数据处理指令,任一操作指令前缀重复指令REPREPE/REPZREPNE/REPNZ后可实现对ECX个连续的存储单元按字节、字或双字进行操作,如果没有前缀重复指令,则串操作只执行一次,存储单元地址由ESI或EDI指定,在处理后ESI或EDI地址自动增减1、2或4,具体如下:

  • 当DF=0时 (执行CLD指令),ESI或EDI数据处理后地址递增1、2或4
  • 当DF=1时 (执行STD指令),ESI或EDI数据处理后地址递减1、2或4

(1) 重复串操作 REP[E|Z|NE|NZ]

① 无条件重复串操作 REP

指令格式为 REP 串操作指令,可用于无条件重复执行的串操作指令有移串、取串、存串、输入串、输出串五种,执行逻辑如下:

步骤1:若ECX≠0
步骤2:则ECX=ECX-1,并执行串操作指令,执行后回步骤1
步骤3:否则退出REP循环,顺序执行下一条指令

② 条件重复串操作 REP[N]E / REP[N]Z

判断条件在无条件重复串操作的基础上,多了对标志位ZF的判断,可用于条件重复执行的串操作指令有串扫描和串比较两种,执行逻辑如下:

i) 相等重复前缀指令 REPE / REPZ

步骤1:若ECX≠0且相等 (ZF=0)
步骤2:则ECX=ECX-1,并执行串操作指令,执行后回步骤1
步骤3:否则退出REP循环,顺序执行下一条指令

ii) 不等重复前缀指令REPNE / REPNZ

步骤1:若ECX≠0且不等 (ZF≠0)
步骤2:则ECX=ECX-1,并执行串操作指令,执行后回步骤1
步骤3:否则退出REP循环,顺序执行下一条指令

(2) 串操作指令

指令作用说明
movs dst,src移串操作只是告诉编译器数据传递类型 (字节、字、双字),不能决定传送位置,指令movs byte ptr es:[edi], byte ptr ds:[esi]作用与movb相同,注意edi在串操作时默认与es段联用
movs[b|w|d]移字节 / 字 / 双字串操作把以esi值为起始地址的一个字节/字/双字数据传送到以edi值为起始地址的内存空间,并在传送完根据DF值对esi和edi作相应增减,是唯一一条实现直接从存储单元送到存储单元的指令。一次只能传送一个数据,与rep搭配实现传送多个数据。
lods src取串操作只是告诉编译器数据传递类型 (字节、字、双字),不能决定传送位置
lods[b|w|d]取串操作把以esi值为起始地址的一个字节/字/双字数据传送到al/ax/eax,并在传送完根据DF值对esi作相应增减。一次只能取一个数据,与LOOP等循环指令结合可取多个数据。与rep指令结合使用没有意义,因为只能保存最后一个数据。
stos dst存串操作只是告诉编译器数据传递类型 (字节、字、双字),不能决定传送位置
stos[b|w|d]存串操作把al/ax/eax中的值传送到以edi值为起始地址的存储单元,并在传送完根据DF值对edi作相应增减。一次只能传送一个数据,与rep搭配实现传送多个数据。
ins dst输入串操作只是告诉编译器数据传递类型 (字节、字、双字),不能决定传送位置
ins[b|w|d] dst输入串操作从dx指定的端口接收一个字节/字/双字,并存入以edi值为起始地址的内存单元中,并在接收数据后根据DF值对edi作相应增减。一次只能输入一个数据,与rep搭配实现输入多个数据。若当前任务没有执行I/O的权限,则发生异常。
outs src输出串操作只是告诉编译器数据传递类型 (字节、字、双字),不能决定传送位置
outs[b|w|d]输出串操作把以edi值为起始地址的内存单元中的一个字节/字/双字数据从dx指定的端口输出,并在输出数据后根据DF值对esi作相应增减。一次只能输出一个数据,与rep搭配实现输出多个数据。若当前任务没有执行I/O的权限,则发生异常。
scas dst串扫描操作只是告诉编译器数据传递类型 (字节、字、双字),不能决定传送位置
scas[b|w|d]串扫描操作在目的串中查找指定值的数据,用al/ax/eax的值和以edi值为起始地址的字节/字/双字进行相减,并在运算后根据df值对edi进行增减。受影响标志位包括af、cf、of、pf、sf、zf。该指令执行一次只比较一次,与reperepne结合可以比较多个数据,循环直到结束或比较值相等,与rep结合没有意义,因为只保存最后一次比较结果到标志位。
cmps dst,src串比较操作只是告诉编译器数据传递类型 (字节、字、双字),不能决定传送位置
cmp[b|w|d]串扫描操作用以esi值为起始地址的字节/字/双字和以edi值为起始地址的字节/字/双字进行相减,并在运算后根据df值对esi和edi进行增减。受影响标志位包括af、cf、of、pf、sf、zf。该指令执行一次只比较一次,与reperepne结合可以比较多个数据,循环直到结束或比较值相等,与rep结合没有意义,因为只保存最后一次比较结果到标志位。

以移串操作为例:

.386
.model flat,stdcall
include kernel32.inc
includelib kernel32.lib
includelib msvcrt.lib
printf PROTO C:DWORD,:vararg

.data 
Src db 'Hello world!'
len equ $-Src           ; $表示当前地址,当前地址减Src地址得到的是Src串的长度常量
Dst db len dup(0)       ; 定义目的串空间

.code
start:
cld                     ; 复位DF,让串操作由低至高进行
lea esi,Src             ; esi指向源串地址
lea edi,Dst             ; edi指向目的串地址
mov ecx,len             ; 串长度len赋予ecx,要重复操作len次
rep movsb               ; 将源串Src开始的len个数据传送到Dst位置
invoke printf,addr Dst  ; 输出目的串
invoke ExitProcess,0    ; 退出进程,返回值为0
end start
6.循环指令

循环指令指多种LOOP 标号指令,指令循环次数保存在CX或ECX中,没执行一次循环CX或ECX自动减1,最后判断CX或ECX是否为0,以决定循环是否退出,使得LOOP标号之间的指令循环执行CX或ECX次。执行LOOP[N][E|Z]循环指令时,标志位ZF也能决定是否退出循环。

使用LOOP循环无论CX或ECX初值为多少,循环体至少要执行1次,然后CX或ECX减1再与0作比较,如果CX或ECX初值为0,减1后就会变为循环执行65536 (216) 或4294967296次 (232),这样运算结果就会出错,为此用J[E]CXZ 标号指令来判断[E]CX是否为0,如果为0则转移到标号处,此命令一般用于循环的开始处。

指令作用说明
loop[w|d] label循环指令loop指令自动选择计数器为,16位环境计数器为cx,32位环境为ecx。loopw计数器为cx,loopd计数器为ecx
loop[e|z][w|d] label相等或为零循环计数器cx/ecx选择同上,loopeloopz指令循环结束的条件除了cx减到0以外,还有zf=0 (两个数值不相等)
loopn[e|z][w|d] label不等或不为零循环计数器cx/ecx选择同上,loopneloopnz指令循环结束的条件除了cx减到0以外,还有zf=1 (两个数值相等)
j[e]cxz label[e]cx为零时跳转到标号处在循环开始前判断[e]cx是否为0,如果为0则直接跳到标号处执行,越过循环体

如下例计算 1+2+…+n 的和:

.386
.model flat,stdcall
include kernel32.inc
includelib kernel32.lib
includelib msvcrt.lib
scanf PROTO C:DWORD,:vararg
printf PROTO C:DWORD,:vararg

.data
infmt DB '%d',0
outfmt DB '1+2+...+%d=%d',0
n DB ?

.code
start:
invoke scanf,addr infmt,addr n
mov ecx,n
mov eax,0
jecxz done
again:
add eax,ecx
loopd again
done:
invoke printf,addr outfmt,n,eax
invoke ExitProcess,0           
end start
7.CPU控制指令
指令作用说明
nop空操作指令什么都不做,但同样会产生机器码

注意:不能直接对两个存储单元数据进行操作 (包括运算和数据传送),所以经常用寄存器作为临时存储空间实现操作。

(三) 伪指令

1.程序基本结构

主要包括处理器选择、定义程序存储模型、引用头文件和库文件、函数原型声明、变量定义、数据段代码段定义以及标号等涉及程序基本结构的内容,具体如下:

(1) 处理器选择

选择执行指令的CPU,不同型号的CPU可执行的命令不同,32位汇编程序至少要选择80386,如下:

.8086    ; 只接收8086指令 
.386     ; 接收除特权指令外的80386指令 
.386P    ; 接收全部80386指令
(2) 定义程序存储模型

格式为 .model 内存模型[,函数调用方法],如下:

.model flat,stdcall
  • flat 表示内存使用平展模型,使用32位地址,程序代码、数据和堆栈共用一个连续的4GB大小的线性地址空间,与之对应的是分段模型,分段内存模型中存储空间是分段使用的,Win16编程使用的是分段模型,Win32及之后编程使用的一般都是平展模型
  • stdcall 表示函数调用时实参入栈顺序,堆栈恢复是由子程序完成还是由调用者完成,以及是否允许使用VARARG
  • VARARG 表示实参个数是可变的

不同语言区别如下表所示:

语言类型入栈顺序堆栈恢复者是否可用VARARG
C从右到左调用者
sysCall从右到左子程序
stdcall从右到左子程序
BASIC从左到右子程序
FORTRAN从左到右子程序
PASCAL从左到右子程序
(3) 引用头文件和库文件

引用头文件和库文件以使用系统定义的函数,引用头文件用include,引用库文件用includelib,如下:

include kernel32.inc    
includelib kernel32.lib 
includelib msvcrt.lib   ; C语言定义的库函数大多在msvcrt.lib库中
(4) 函数原型PROTO声明

要调用外部函数,如C函数,就必须对函数进行声明,声明的格式有两种:PROTO声明和EXTRN声明,用PROTO声明的函数,既可以用CALL伪指令调用,也可以用INVOKE伪指令调用,用EXTRN声明的函数,只能用CALL伪指令调用。

PROTO声明格式为 函数名 PROTO [距离] [语言类型] [[参数]:类型]...[,[参数]:类型],其中:

  • 距离:函数被调用类型,可以是NEAR、FAR、NEAR16、NEAR32等,这些标识应用于分段的内存模型,NEAR指在本段调用函数,FAR指在其他段调用函数,对32位以上CPU而言意义不大,一般省略
  • 语言类型:表示参数传递顺序和堆栈恢复方式,可以是stdcall、C、BASIC等,如果忽略则使用头部.model定义的值
  • 参数:指参数名,一般省略
  • 类型:参数类型,Win32中默认DWORD,可省略,最后一个参数类型可以是VARARG,指不确定参数个数。类型名前缀ptr表示指针,因为Win32下指针是32位,所以也可以用DWORD代替指针类型
; 以下两个函数原型声明作用相同
printf PROTO C :ptr sbyte,:vararg
printf PROTO C :DWORD,:vararg
(5) 函数原型EXTRN声明

在不指定参数个数与类型的情况下,可用EXTRN声明,格式为 EXTRN 函数名1:类型1[,函数名2:类型2[,...]],其中类型指函数被调用类型,包括NEAR、FAR、NEAR16、NEAR32等,这里不能省略

EXTRN printf:near,scanf:near
(6) 变量定义

变量包括全局变量、局部变量和形式参数,全局变量在数据段中定义,在伪指令.data之后,常量在伪指令.const之后。全局变量定义格式如:变量名 数据类型 数据1,...,数据n,表示在指定变量名和数据类型条件下,定义了n个数据。其中变量名代表的是变量的首数据,而不是地址,这一点要与标号区分开,数据类型用于决定每个数据要分配的内存字节数,常用数据类型如下:

  • 整型数据类型:BYTE (DB)、WORD (DW)、DWORD (DD)、QWORD (DQ)、TBYTE (DT),字节数依次为1、2、4、8、10
  • 实型数据类型:REAL4 (DWORD、DD)、REAL8 (QWORD、DQ)、REAL10 (TBYTE、DT),字节数分别为4、8、10
  • 字符型数据类型:BYTE (DB),字节数为1
a DWORD 3               ; 相当于int a=3
a DWORD 3,4,5,6  
a DWORD ?               ; 数值不确定的话可以用?代替

b BYTE 'asdf',0         ; 多个字符型变量既可以用引号把字符串括起来,结尾用加上结束标志0
b DB 'a','s','d','f',0  ; 也可以逐个字符表示

局部变量和形参在函数中定义,局部变量定义需要用到伪指令LOCAL,意为临时的局部变量,格式为 LOCAL 变量名1[[数量]][:数据类型][,变量名2[[数量]][:数据类型]]...,如下所示:

LOCAL d[20]:BYTE,n:DWORD,i    ; 定义一个20个字节长的变量d (数组),一个双字长的变量n,以及一个未定义类型的i
(7) 段定义

有初值的数据一般定义在 .data 段中 (会增大可执行文件),未初始化数据定义在 .data? 段 (不会增加可执行文件大小),常量定义在 .const 段中,堆栈放在 .stack 段中,而代码只能放在 .code 段,如下例所示:

.data               ; 在和.model伪指令联用时,表示已初始化数据段的开始,段名为:_DATA
a BYTE 1

.data?              ; 在和.model伪指令联用时,表示未初始化数据段的开始,段名为:_BSS
b DD ?,10 DUP(?)    ; 10 DUP(?)表示10个未知数值

.const              ; 在和.model伪指令联用时,表示常量数据段的开始,段名为:CONST
PI REAL8 3.14

.stack [[size]]     ; 创建一个堆栈,在和.model伪指令联用时,表示栈顶位置,段名为:stack,可选择堆栈大小,默认为1KB

.code [[name]]      ; 在和.model伪指令联用时,表示代码段的开始,段名为:_TEXT,可选择为其单独起名字,@code可表示代码段的名称
LABEL:              ; 入口点标号
...
invoke ExitProcess,0
end LABEL           ; 指明程序入口点

以上段定义的格式为简化版,完整的段定义格式如下:

name SEGMENT [READONLY] [align] [combine] [use] [‘class’]

name ENDS

  • align为段起始边界的对齐方式,包括:BYTE、WORD、DWORD、PARA、PAGE,默认为PARA;
  • combine为段组合方式,包括:PUBLIC、STACK、COMMON、MEMORY、AT address、PRIVATE,缺省为PRIVATE;
  • use为使用模式,包括:USE16、USE32、FLAT,在代码段中可定义默认的操作数大小, 在数据段中可限制该段的数据长度;
  • class为段类型,包括:‘data’、‘data?’、‘code’、‘const’、‘stack’、‘extra’。
(8) 段分配ASSUME

作用一:用来关联段名与段寄存器,一般与段定义语句一起使用,格式为 ASSUME segreg:name[,segreg:name],这种语句主要是针对采用“段地址*16+段偏移”寻址方式的8086 DOS汇编,但是对于80386+ CPU,.model flat模式中,程序代码、数据和堆栈共用一个连续的4GB大小的线性地址空间,32位的寄存器直接可以4GB范围内寻址,MASM默认定义:ASSUME CS:flat, DS:flat, ES:flat, SS:flat, FS:ERROR, GS:ERROR,因此寄存器可以直接使用,而涉及到FS和GS时,默认定义关联到ERROR,如果不用ASSUME重新声明则会报错。

...
code_seg segment 'code'          ; 定义代码段,段名为code_seg
assume cs:code_seg               ; 声明code_seg是代码段
assume fs:nothing, gs:nothing    ; nothing表示不作任何声明,取消段寄存器和所有段名的关联
code_seg ends
...

作用二:用来指定数据寄存器的类型,格式为 ASSUME datareg:type[,datareg,type],如下将edi声明为结构体指针,这样就可以按照[edi].的方式取数据:

assume edi:ptr NM_TREEVIEW  
.if [edi].hdr.code==TVN_BEGINDRAG  
    ...
.endif 
(9) 标号

指令由4个部分组成:标号: 助记符 操作数 ;注释,其中标号是指令所在的地址,用来标明程序转移的目标地址,格式为 标识符:,如下所示:

mov eax,0
mov ecx,100
again:
add eax,ecx
loop again       

标识符可以是@@,用作@F@B@F向前转移到最近的@@:@B向后转移到最近的@@:,如下所示:

; 实现功能:计算|ecx|+|ecx-1|+...+1
mov eax,0
cmp ecx,0
jg  @F
neg ecx
@@:
add eax,ecx
loop @B
2.函数定义及调用
(1) invoke伪指令调用函数

函数声明后就可以调用函数,有两种调用方法:invoke和call,invoke调用函数的格式为 invoke 函数名[,参数1][,参数2][...],如下所示:

invoke printf,ADDR fmt,ADDR s     ; 以fmt格式输出字符串s
invoke printf,OFFSET fmt,OFFSET s ; 传递的参数如果是地址,可以用ADDR,也可以用OFFSET

注意:用invoke调用的函数只能用PROTO声明,不能用EXTRN声明。

(2) call指令调用函数

call调用函数的格式稍复杂些,因为要借助栈传参,格式如下:

push 参数n

push 参数1
call 函数名
add esp,4*n

注意参数入栈顺序,如果是C、syscall、stdCall调用方式,则右边的参数即参数n先入栈,左边的参数即参数1后入栈;如果是C调用,调用完还要由程序调用者 (即调用函数的程序) 用 add esp,压栈距离 来恢复堆栈;而如果是stdCall或syscall调用,则要由函数在其内部恢复堆栈,可以使用命令 add esp,压栈距离 ,也可以使用命令ret 压栈距离命令恢复堆栈,后者相当于在ret返回的同时又执行了add esp,压栈距离命令。下面以C调用方式,按fmt格式输出字符串s为例,代码如下:

push OFFSET s
push OFFSET fmt
call printf
add esp,8           ; 压入2个地址,每个地址4字节,所以退栈距离是8

注意:
① 用call调用的函数既可以用PROTO声明,也可以用EXTRN声明;
② 所传递的参数如果是地址,入栈时只能用OFFSET,不能用ADDR;
③ invoke伪指令编译后也是转换为call指令,最好用call指令调用函数。

(3) 函数定义

汇编中的函数又称为子程序或过程,两者本质相同,定义函数的格式如下:

函数名 PROC [距离] [可视区域] [语言类型] [形参:数据类型][,形参:数据类型]…[VARARG]
    LOCAL 变量名[[数量]][:数据类型][,变量名[[数量]][:数据类型]]…
                 ; 程序体
    RET           ; 如果有返回值,则存在EAX寄存器或st(0)浮点寄存器中
函数名 ENDP

其中可视区域可以是PRIVATE、PUBLIC、EXPORT,PRIVATE表示只有本模块可见,PUBLIC (缺省值) 表示所有模块可见,EXPORT指导出的函数,如编写DLL时,若要将函数导出,则使用EXPORT。PROC和ENDP标识了函数定义的起始和结束位置,函数定义部分放置在代码段,如下所示:

...
.data
a DWORD 1
b DWORD 2

.code
FunTemp PROC x:DWORD,y:DWORD
mov eax,x
add eax,y
RET
FunTemp ENDP
...

start:
invoke FunTemp,a,b
end start

段内调用CALL NEAR (NEAR可省略) 采用近返回RETNRET,段间调用CALL FAR采用远返回RETF

  • CALL相当于将调用指令的下一条指令地址压栈,然后跳转到函数位置执行,即:PUSH 下一条指令地址MOV EIP,函数位置,相应地,RETNRET相当于POP EIP
  • CALL FAR相当于将段寄存器压栈,然后将调用指令的下一条指令地址压栈,最后再跳转到函数位置执行,即PUSH CSPUSH 下一条指令地址MOV EIP,函数位置,相应地,RETF相当于POP EIPPOP CS

WIN32及之后汇编中不再通过段寄存器将内存进行分段,因此只有段内调用和段内返回。

若函数有参数或局部变量,程序会在函数开始和结束位置 (RET指令之前) 自动加上保护主程序堆栈的指令,使堆栈在函数调用前后保持平衡,如下:

push ebp       ; 保护主程序基址指针寄存器
mov ebp,esp    ; 保护主程序堆栈指针寄存器

mov esp,ebp    ; 恢复主程序堆栈指针寄存器
pop ebp        ; 恢复主程序基址指针寄存器
3.选择结构伪指令

汇编指令实现分支选择结构,可以使用条件转移指令Jcc (cc指各种条件) 和无条件转移指令JMP实现,也可以使用.IF .ELSEIF .ELSE .ENDIF伪指令实现,后者比较简单,但并不是最终表现形式,汇编后都要转换成相应的条件转移指令和无条件转移指令。

若在条件表达式中要检测标志位信息,可以使用的符号名有:ZERO? (ZF==1)、CARRY? (CF==1)、OVERFLOW? (OF==1)、PARITY? (PF==1)、SIGN? (SF==1)。(标志位?表示标志位的值,当其为真时即标志位XF==1)

分支选择结构格式如下所示:

.IF 条件表达式1
     指令序列1                ; 满足条件1时执行指令序列1
[.ELSEIF 条件表达式2          ; 满足条件2时执行指令序列2
     指令序列2
     […]                     ; 满足其他条件时执行其他指令序列
[.ELSE
     指令序列3]               ; 都不满足时执行指令序列3
]

.ENDIF

注意:
① 汇编语言可以使用逻辑运算符&&||!,但不能使用运算符&|~^,要实现运算符功能需要用ANDORNOTXOR指令;
② 运算符&&前后要用空格隔开,'A'<=var<='Z'只能写成var>='A' && var<='Z',而不能写成'A'<=var && var<='Z'

如下例所示:

.if var>='A' && var<='Z'    ; 判断var如果是大写字母则转换成小写
    add c,20h
.endif

.if carry? && eax!=ebx      ; 检测cf==1且eax≠ebx是否成立
    ...
.endif
4.循环结构伪指令

汇编指令实现循环结构,可以使用LOOPJECXZ指令实现,也可以使用.while .repeat .break .continue等伪指令实现,后者比较简单,但并不是最终表现形式,汇编后都要转换成LOOPJECXZ指令,或者条件转移指令实现。

(1) 当循环伪指令.while

先判断后执行,格式为:

.while 条件表达式
    指令序列
.endw
(2) 重复伪指令.repeat

先执行后判断,有两种格式,如下所示:

; 格式一
.repeat
    指令序列
.until 条件表达式         ; 直到条件表达式的值为真时,退出循环

; 格式二
.repeat
    指令序列
.untilcxz [条件表达式]    ; 直到ecx(不是cx)值为0,或条件表达式的值为真时,退出循环

两种格式虽然相似,但是本质不同,其中格式一汇编后会转换成相应的条件转移指令,而格式二汇编后会转换成LOOP指令,用ECX作计数器。

如下两段代码作用相同:

.repeat                        next:
    add eax,ecx     ----->          add eax,ecx
    dec ecx                         dec ecx
.until ecx==0                       jnz next

.repeat                        next:
    add eax,ecx     ----->          add eax,ecx
.untilcxz                           loop next
(3) 退出伪指令.break

作用与C语言的break相同,执行时强制退出循环。如果后接一个测试伪指令.break .if 条件表达式,则测试条件为真时才执行.break

(4) 短路伪指令.continue

作用与C语言的continue相同,执行时结束当前循环,直接进入下次循环的判断。如果后接一个测试伪指令.continue .if 条件表达式,则测试条件为真时才执行.continue

(四) 寻址方式

分为数据寻址和转移地址寻址两大类,数据寻址用于定位数据,转移地址寻址用于定位转移及调用指令指向的地址

1.数据寻址
(1) 立即数寻址

操作数直接包含在指令中,例如:

mov ax,41h 
mov ah,65
(2) 寄存器寻址

操作数保存在寄存器中,通过寄存器进行寻址,例如:

mov ax,bx
(3) 存储器寻址

通过操作数在存储器中的地址进行寻址,根据地址表示的方式具体分为以下几种:

① 直接寻址

操作数地址为立即数或变量,例如:

mov ax,[1234h]
mov bl,var     ; 从var的值对应的地址中取一个字节赋予bl,等效于 mov bl,[var]
mov bl,var+5   ; 从var+5对应的地址中取一个字节赋予bl,等效于 mov bl,[var+5] 
mov var,bl     ; 将bl的值写入var对应的地址,等效于 mov [var],bl

② 寄存器间接寻址

操作数地址保存在基址寄存器 (BX、BP) 或变址寄存器 (SI、DI) 中,例如:

mov ax,[bx]

③ 寄存器相对寻址

也称为直接变址寻址,操作数地址为基址寄存器 (BX、BP) 或变址寄存器 (SI、DI) 加上一个偏移量,偏移量可以是立即数或变量,例如:

mov ax,8[bx]
mov ax,[bx+8]
mov ax,var[bx] 
mov ax,[bx+var]

④ 基址变址寻址

操作数地址为基址寄存器 (BX、BP) 加上变址寄存器 (SI、DI),例如:

mov ax,[si][bx]
mov ax,[bx+si]

⑤ 相对基址变址寻址

通过操作数在存储器中的地址进行寻址,操作数地址为基址寄存器 (BX、BP) 加上变址寄存器 (SI、DI) 的值,例如:

mov al,8[bx][si] 
mov al,8[bx+si]
mov al,[8+bx+si]
mov al,var[bx][si] 
mov al,var[bx+si]
mov al,[var+bx+si]

⑥ 比例变址寻址

80386及以上寄存器才支持,通过操作数在存储器中的地址进行寻址,操作数地址为 基址寄存器+变址寄存器×比例因子+偏移量,例如:

mov ax,8[bx][4*si]
mov ax,8[bx+4*si]
mov ax,[bx+4*si+8]
mov ax,var[bx][4*si]
mov ax,var[bx+4*si]
mov ax,[bx+4*si+var]

注意:80386及以上CPU存储器寻址方式中,基址寄存器可以替换为任意通用寄存器,变址寄存器代表除ESP以外的任何一个32位寄存器。

2.转移地址寻址

当转移地址为8位或16位则为段内转移,段寄存器CS不变,指令指针寄存器IP变化;转移地址为32位则为段间转移,转移地址为32位双字,第一个字作为段地址取代CS,第二个地址作为偏移地址IP。进一步分为直接寻址和间接寻址,如下:

(1 ) 直接寻址

转移的目的地址以标号形式给出,例如:

jmp label
jmp short label       # 短转移,限定8位地址标签
jmp near ptr label    # 近转移,限定16位地址标签,near ptr是缺省值
jmp far ptr label     # 长转移,限定32位地址标签
(2) 间接寻址

转移的目的地址以变量、寄存器或两者结合的方式给出,例如:

jmp bx
jmp var
jmp var[bx]
jmp [bx+var]

三、程序调用

(一) 汇编程序调用C函数

INCLUDE[LIB]包含必要的库,用PROTOEXTRN定义外部函数,用INVOKECALL引用,上述伪指令部分已讲,不再赘述。

(二) C程序调用汇编子程序

基本步骤如下:

  • 在C程序中用extern声明调用的函数
  • 在汇编程序中定义该函数,定义时调用方式要与C程序中声明的调用方式一致
  • 编译汇编程序生成obj目标文件,将目标文件添加到IDE的工程项目中
  • 编译运行

汇编程序和C程序声明方式如下例所示:

汇编程序
.386
.model flat,C
.code
myfunc proc x:dword,y:dword
       ...
       ret
myfunc endp
end
==============================
c程序
#include <stdio.h>
extern int add(int a, int b);
void main(){
    ...
}
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值