X86汇编语言期末复习

汇编

1. 第一章序言

编译器: 用于将汇编语言源程序转化为机器语言。
链接器: 它把汇编器生成的单个文件(目标模块)组合 为一个可执行文件。

源程序 -> 目标模块 -> 可执行文件

寄存器: CPU中的存储位置。(保存操作的中间结果)

Q: 为什么要学汇编语言?

A: 汇编语言:以花费大量时间进行调试为代价,换来 访问底层 数据(内存)

,从自由访问底层,优化速度,代码体积小,和实际嵌入式或驱动开发需要分析

L0 : 计算机可以用电子电路来执行机器语言(l0由数字组成)的每条 指令,这种语言称为L0。

L1: 程序员可以自由编写的语言(L1)

2. x86处理架构

2. 基本组成与总线

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

控制单位(CU): 协调参与机器指令执行的步骤序列

算术逻辑单元(ALU): 执行算术、逻辑运算。

内存存储单元 :程序运行时保存指令及数据

总线: 一组并行线,将数据从计算机的一个部分传到另一部分。

数据总线: 在内存和CPU之间传递指令和数据。

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

地址总线: 保持指令和数据的地址。

I/O总线: 在系统输入/输出设备和CPU之间传输数据。

指令执行步骤:取指、译码、执行

2.2 寄存器分类

了解下面分类即可

处理器内部的高速存储单元

用于暂时存放程序执行过程中的代码和数据

透明寄存器 : 对应用人员不可见、不能编程直接控制

可编程(Programmable)寄存器: 具有引用名称、供编程使用

通用寄存器(General-Purpose Register) 具有多种用途 数量较多、使用频度较高

专用寄存器 各自只用于特定目的

EIP是专用寄存器: 顺序执行时自动增量(加上该指令的字节数),指 向下一条指令 。在分支、调用等操作时,执行控制转移指令修改,引起程序转移到指定的指令执行,出现中断或异常时被处理器赋值而相应改变

是用于安排相关代码或数据的一个主存区域,分为 代码段 (存放程序中指令代码 ), 数据段 (存放当前运行程序所用数据), 堆栈段 (指明程序使用的堆栈区域)

下面是常用的通用寄存器寄存器的作用,EAX 应用在做加法运算上,ECX 应用在循环的计数上,ESP 是当前地址的堆栈指针

3.汇编语言基础

3.1 基础定义

先看一个简单的例子,这里有数据段,用来存放数据,代码段,用来存放程序,在这里需要程序的入口,调用退出函数,程序的结尾

assembly 复制代码
.386 ;这是一个32位程序
.model flat , stdcall ;选择flat内存模式,确认子程序调用规范(stdcall规范)
.stack 4096 ;堆栈保留4096字节存储空间
ExitProcess PROTO, dwExitCode:DWORD ;给windows系统的返回值

.data
sum DWORD 0
                                                                                                                                      
.code
main PROC ;main声明
    mov eax , 5
    add eax , 6
INVOKE ExitProcess , 0
main ENDP

下面是其中涉及到量的说明:

整数常量: 需要说明相应的进制,十进制(d),十六进制(h)

整数常量表达式: 需符合相应的运算顺序

字符/字符串常量: 单双引号都可

标识符: 程序员自定义名称,注意不与保留字相同

伪指令: 不在运行时执行,但可以用来定义变量、宏;为内存段分配名称,就是前面的.data 之类的,还有后面的 WORD之类的

指令:(一般就是指令助记符 + 操作数)

​ 标号:可选

​ 指令助记符:必须 ,汇编语言操作 数的个数是0-3

​ 操作数:通常必须

​ 注释:可选,单行注释,用分号(;)开始,多注释:COMMENT ! 注释内容 !

3.2 数据定义

有下面的数据类型

定义数据的格式如下:

名字 伪指令 初始值

初始值必须至少1个,可以是 ?

多个数据定义示例 : list1 BYTE 10,20,30,40

定义字符串:greeting1 BYTE "Good afternoon", 0

在最后一行使用 \ 可以把多行进行拼接

申请空间 DUP

可以重复的申请指定的空间

下面就是名字为count的 双字类型的 重复40次 申请 1,2,3,4,5

asm 复制代码
count    DW     40      DUP(1,2,3,4,5)

小端: 低位数据存储在低地址,低位指定是右边开始

地址计数器$

这个表达当前的偏移量,可以用来计算数据的大小

asm 复制代码
list BYTE 10,20,30,40
ListSize =($-list)

4. 数据传送、寻址和算术运算

**操作数有三种类型: ** 立即数,寄存器操作数,内存操作数

4.1 MOV指令

我们发现,不能内存到内存

最后总结主要一下几点:

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

当我们把较小的值赋值到较大的值,需要涉及拓展

无符号拓展: MOVZX

有符号拓展: MOVSX (就是使用最高位补全)

4.2 状态标志保存

状态标志位(EFLAGS)与低8位寄存器(AH),通过寄存器保存标志位,如下示范

4.3 其余操作数

XCHG: 指令交换两个操作数的内容

偏移量操作数: 通过加上偏移量来进行寻址,像数组访问一样,注意单次偏移大小和数据类型有关

OFFSET运算符: 得到数据标号的偏移量,单位字节

ALIGN伪指令: 对其偶数内存地址。原因:偶数地址更快

PTR运算符: 强制转换

type运算符: 返回变量单个元素的大小,单位:字节

lengthof运算符: 计算数组中元素的个数,不关心每个元素的大小

sizeof运算符: = type * lengthof

4.4 加法减法指令集

4.4.1 自增自减

inc (自增)和dec(自减):寄存器或内存加1减1,不会影响进位标准位(CF)

4.4.2 加减法

add:源操作数不变,相加之和存在目的操作数中

sub:目的操作数减去源操作数

neg: 把符号进行取反

4.5 标志位

对于有符号和无符号来说,区别除了符号位外,区别最大的是,无符号数溢出时是CF位置1,但是对于有符号来说,OF表示溢出,CF表示进位

4.6 寻址

直接寻址: 直接通过数组类似的方式访问

asm 复制代码
.data
arrayB Byte 10h, 20h, 30h, 40h
.code
mov al, arrayB+0
mov bl, [arrayB+1]
mov cl, [arrayB+2]

间接寻址: 通过偏移量地址进行访问

下面这段代码自增的是 10,

asm 复制代码
.data
arrayB Byte 10h, 20h, 30h
.code
mov esi, offset arrayB
mov al, [esi]
inc  [esi]
;报错:系统不知道操作数的大小。
inc   BYTE PTR  [esi]

下面是通过改变esi的方法实现偏移

注意:因为是byte,所以esi增加1,就可以寻到下一个,如果是其余类型,移动一位需要增减相应数据类型的字节数

asm 复制代码
.data
arrayB Byte 10h, 20h, 30h
.code
mov esi, offset arrayB
mov al, [esi]
方法1:inc自增
inc esi
mov  al ,[esi]
inc  esi
mov  al , [esi]

法二:使用变址操作数,通用注意数据类型

asm 复制代码
.data
arrayB Byte 10h, 20h, 30h
.code
;方法2:变址操作数(寄存器+常数)
mov  esi    0
mov  al , arrayB[esi]
mov  al , arrayB[esi+1]
mov  al , arrayB[esi+2]

4.7 指针

前面我们知道了可以通过offset得到地址,所以可以自定义指针,来完成保存某个地址,如下:

4.8 跳转

跳转分为jmp 和 loop 两类,前者是根据标志位进行条件跳转,后者是通过ecx进行计算跳转

jmp:去看书上的跳转指令表

loop: 到达这会执行下面的步骤

  1. ECX减1。
  2. ECX与0比较,不等于0的话调到标号处。
  3. 如果ECX=0,不发生跳转,执行循环后面的指令。

如果内部还有循环,需要先把ecx的值保存在变量,或者压栈,内部循环结束后,再进行恢复

5. 过程

栈: 先进后出的一种结构体

入栈出栈指令:

asm 复制代码
;入栈指令:push
PUSH reg/mem16
PUSH reg/mem32
PUSH imm32

;出栈指令:pop
POP   reg/mem16
POP   reg/mem32

衍生指令: 看看就行

asm 复制代码
;入栈指令:pushfd(将标志寄存器压入堆栈)
pushad ;(按照顺序将所有的32位通用寄存器压入堆栈)
pusha  ;(按照顺序将所有的16位通用寄存器压入堆栈)

;出栈指令:popfd(将桟中的内容弹回标志寄存器)
popad ;(按照相反的顺序将所有的32位通用寄存器弹出堆栈)
popa  ;(按照相反的顺序将所有的16位通用寄存器弹出堆栈)

定义子程序:

通过 PROC 和 ENDF 定义

asm 复制代码
sample PROC
..
ret
sample ENDP

Call/ret

完成函数的跳转,分别体会call和ret是什么作用

call:

1.将调用call的函数的call的下一条指令地址压入堆栈

2.将子程序的地址给EIP

就是我本来马上执行call,call执行了该执行call的下一条,这个时候先保存下一条的位置,然后执行call的时候,跑到call里函数的位置了

ret:

1.将主程序里下一条指令地址给EIP

call里执行完成之后,使用ret,得到调用call的函数的下一条指令位置,给EIP

给子程序传递参数

需要保证原来的数不改变,所以通过入栈和出栈进行保护

当然可以使用

USES运算符

  1. 列出要修改的寄存器

  2. 子程序开始时自动调用push、子程序结束时自动调用pop指令

和上面的等价

6.条件分支与跳转

布尔指令: and(test) ,or, not, xor
比较指令: cmp

test 与 and的不同之处不改变目标操作

cmp:目的操作数减去源操作数,查看结果的ZF,CF,OF标志位

6.1 条件跳转

分为了以下四大类:

  1. 基于特定标志位的值跳转。(零标志位、进位标志位、奇偶标志位等)

  2. 基于两数是否相等,或是否等于ECX寄存器的值跳转

  3. 基于无符号操作数的比较跳转

  4. 基于有符号操作数的比较跳转

loopz、loope(为零时跳转,相等时跳转)

loopnz、loopne(不为零时跳转,不相等时跳转)

下面是一个例子,通过条件跳转实现短路求值

下面是来实现switch,有点难

7.整数运算

7.1 移位

下面先看所有的移位指令

SHL左移

就是向左移动一位,注意CF标志,如果移位的那一位是1的话,会置位CF标志位

SHR右移

就是向右移动一位,注意CF标志,是把最低位移给CF,最高位补零

SAL指令(算术左移)

和逻辑移位的区别在于带了符号(+、-)

SAR指令(算术右移)

和逻辑移位的区别在于带了符号(+、-),SHR前面补0,遇到负数右移的情况下前面补1

循环移位

循环进位移位

双精度移位

注意右移:目的操作数的最高位,来源于源操作数的低位

小结:

各种移位的差别就是,移走的,和移来的,怎么处理,算术位移要补符号位,循环位移的移来的位,来源于移走的位,双精度位移,移来的位来源于另一个

7.2 乘法

先看下面的表

1、乘积是被乘数与乘数大小的2倍。(为了结果不溢出)

2、要用到CF(进位)标志寄存器(乘积的高半部分不为零,则会把CF设置为1.)

3、乘数和被乘数的大小一样

有符号乘法

使用的是imul,和MUL指令的区别为:如果乘积的高半部分不是其低半部分符 号的扩展,则CF=1、OF=1

7.3 除法

DIV(无符号)

iDIV指令(有符号数除法)

执行该指令前必须进行符号扩展(把数的高位复制所有高位中)

CBW、CWD、CDQ 是有符号拓展指令,中间那位是数据类型

7.4 拓展加减法

ADC是拓展加法,SBB是拓展减法

ADC指令执行逻辑: 目的操作数 = 目的操作数 + 源操作数 + 进位标志位的值

SBB指令执行逻辑: 目的操作数 =目的操作数 - 源操作数 - 进位标志位的值

asm 复制代码
mov edx,100h
mov eax,8000 0000h
sub eax, 9000 0000h
sbb edx,0 ;最后得到0x100 - 1(来源于进位) = 0x0ff

8.堆栈

8.1 寄存器传参数与堆栈传参数

寄存器传递参数: 将参数值复制到寄存器

寄存器传递参数的缺点:可能无法恢复寄存器,使代码混乱,让系统无法正常运行。

载体是寄存器

堆栈传参数:

把参数压入堆栈,这里有值传递和引用传递,引用传递本质上是offset后得到的地址

堆栈传递离不开堆栈帧,创建如下

1)被传递的实际参数。如果有,压入堆栈。

2)当子程序被调用时,使该子程序的返回值压入堆栈。

3)子程序开始执行时,EBP(扩展帧指针寄存器)压入堆栈。

4)设置EBP等于ESP。(基址的设立)。

5)如果有局部变量,修改ESP,(在堆栈中为变量留空间)。

6)如果有另外的寄存器要保存,压入堆栈

核心最后是通过基址来完成访问

注意:堆栈清除

如下图,当ret时,会弹出下条要执行指令的地址,如果不进行清除,就会去找地址为5的下一条指令

解决办法:

如下,把esp指针更新位置就行,这样就能返回正确的下一条指令

当然下面这样也是一样的

对于5,6点,也是在后面存储到堆栈里,更改esp指针

如下图,最后也会进行回收,保证不影响esp

如下,添加了变量,也可以复原esp指针

9.字符串和数组

下面是字符串的指令:

  • MOVSB、MOVSW和MOVSD:移动字符串数据指 令,将由ESI寻址的内存地址处的数据复制到EDI寻址的 内存地址处

  • CMPSB、CMPSW和CMPSD:比较字符串,比较由 ESI和EDI寻址的两个内存地址处的值

  • SCASB、SCASW和SCASD:扫描字符串,比较累加 器与EDI寻址的内存地址处的内容

  • STOSB、STOSW和STOSD:存储字符串数据,存储 累加值的内容至EDI寻址的内存地址处  LODSB、LODSW和LODSD:字符串装入累加器,加 载ESI寻址的内存地址处的数据至累加器

下面是指令特点:

  • 源串指针为ESI,目的串指针为EDI
  • 串长度在ECX中
  • 指针与计数器自动修改
  • 加减由DF确定:CLD、STD
  • 修改长度 B=BYTE、W=WORD、D=DWORD
  • 重复前缀有REP、REPE/REPZ、 REPNE/REPNZ
  • 允许源和目的操作数都是存储单元

操作流程:

执行串操作之前,应先设置:

(1)源串首地址(末地址)→ ESI

(2)目的串首地址(末地址)→ EDI

(3)串长度 → ECX

(4)建立方向标志(CLD或STD)

执行串操作指令后,ESI和EDI已经越界或 超过目标

MOVSB、MOVSW和MOVSD

  • B:BYTE、W:WORD、D:DWORD
  • 三条指令会自动修改ESI和EDI的值
    • 自动设置增加还是减小的数值的多少
  • 连续的字符串移动指令使用方法
    • 设置ESI和EDI的方向:增加或者减少
      • CLD:清除方向标值位,也就是正向,ESI和EDI增加
      • STD:设置方向标值位,也就是反向,ESI和EDI减小
    • 设置ECX寄存器内容为要复制的字符串的数量
    • 设置源字符串偏移ESI和目的字符串偏移EDI两个寄存器
    • 使用重复前缀指令rep设置重复执行的指令(三条指令之一)
      • rep的使用类似于loop,会按照ECX的值重复执行指令


STOSB、STOSW和STOSD

  • 分别将AL、AX或EAX中的值存储到EDI寻址的目标内存单元中

  • 特别适用于用填充字符串或者数组

LODSB、LODSW和LODSD

  • 从ESI寻址的内存单元中将对应尺寸的内容复制到AL、 AX或EAX寄存器中
相关推荐
Darkershadow3 小时前
蓝牙学习之unprovision beacon
python·学习·ble
报错小能手3 小时前
线程池学习(六)实现工作窃取线程池(WorkStealingThreadPool)
开发语言·学习
星火开发设计5 小时前
二叉树详解及C++实现
java·数据结构·c++·学习·二叉树·知识·期末考试
xiaoxiaoxiaolll5 小时前
《Light: Science & Applications》超表面偏振态与偏振度完全独立控制新范式
学习
sww_10265 小时前
JVM基础学习
jvm·学习·测试工具
num_killer6 小时前
小白的Jenkins学习
运维·python·学习·jenkins
Every exam must be6 小时前
12.2 vue学习02
学习
sunfove6 小时前
照度 (E) 与亮度 (L) 的关系
学习
HL_风神6 小时前
设计原则之单一职责原则
c++·学习·设计模式·单一职责原则
CS创新实验室7 小时前
正态分布的深入学习:从数学发现到自然法则的演变
学习·数据挖掘·数据分析·统计学·正态分布