汇编入门学习
Drunkbaby Lv6

https://www.bilibili.com/video/BV1Rs411c7HG

前期内容还是挺理论的,要先搞清楚电脑都有什么组成,都负责哪些功能,是怎么样运作的。

基础知识

汇编语言是直接在硬件之上工作的编程语言,首席按要了解硬件系统的结构,才能有效的应用汇编语言对其编程。

汇编的研究重点放在如何利用硬件系统的编程结构和指令集有效灵活的控制系统进行工作。

机器语言

机器语言是机器指令的集合。

机器指令展开来讲就是一台机器可以正确执行的命令。比如这个指令 01010000 (PUSH AX)—— 把 AX 推进堆栈。

而机器码只认识 01,所以在很多时候非常不方便,这就产生了汇编语言

汇编语言

汇编语言的主体是汇编指令,汇编语言和机器语言其实是一一对应的,也就是直接把 01 翻译成了对应能被识别的东西。如下的一个例子就是很好的说明。

寄存器

简单的讲是 CPU 中可以存储数据的器件,一个 CPU 中有多个寄存器。

AX 是其中一个寄存器的代号,BX 则是另一个寄存器的代号。

  • 然而这里又涉及到一个概念,计算机能读懂的只有机器语言,怎么样让计算机读懂汇编语言呢?

中间其实是通过一个编译器,它会将汇编指令翻译为机器码。

汇编语言的组成

汇编语言由以下三类组成

1、汇编指令(机器码的助记符)
2、伪指令(由编译器执行,编译器认识,计算机不认识)
3、其他符号(由编译器识别,比如加减乘除)

汇编语言的核心是汇编指令,它决定了汇编语言的特性。

存储器

CPU 是计算机的核心部分,它控制了整个计算机的运作并进行运算,要想让一个 CPU 工作,就必须要向它提供指令和数据。

  • 指令和数据在存储器中存放,也就是平时所说的内存。
  • CPU 是基于内存运行的,离开了内存,性能再好的 CPU 也无法工作。

磁盘不同于内存,磁盘上的数据或程序如果不被读到内存中,就无法被 CPU 使用。

指令和数据

指令和数据是应用上的概念。在内存或磁盘上,指令和数据没有任何区别,都是二进制信息。

比如目前有个二进制 1000100111011000

对应的数据为 ——> 89D8H(数据)
同样可以表示为一个指令 ——> MOV AX,BX(程序)

存储单元

存储器被划分为若干个存储单元,每个存储单元从 0 开始顺序编号。例如一个存储器有 128 个存储单元,编号从 0 ~ 127,如图所示

对于大容量的存储器一般还用以下单位来计量容量,磁盘上的容量单位同内存的一样,实际上以上单位是微机中常用的计量单位。

1KB = 1024B
1MB = 1024KB
1GB = 1024MB
1T = 1024GB

CPU 对存储器的读写

CPU 想要进行数据的读写,必须和外部器件(标准的说法是芯片)进行三类信息的交互

  • 存储单元的地址(地址信息,比如内存、硬盘、显卡等)
  • 器件的选择,读或写命令(控制信息)
  • 读或写的数据(数据信息)

由于电子计算机能处理、传输的信息都是电信号,电信号是用导线传送的,这也是 CPU 传输地址、数据、控制信息的通道。

在计算机中有专门连接 CPU 和其他芯片的导线,通常称为总线。

总线:是指计算机组件间规范化的交换数据(data)的方式,即以一种通用的方式为各组件提供数据传送和控制逻辑。

简单来说,总线是计算机硬件设备之间用来通信的

总线是单向的 例如:不能同时进行读取和写入的操作

物理上:一根根导线的集合;
逻辑上:地址总线、数据总线、控制总线

用下图来表示

  • 数据总线(Data Bus):在 CPU 与 RAM 之间来回传送需要处理或是需要储存的数据。总线是宽度决定了 CPU 与其它器件进行数据传送时一次数据的传送量。这也就决定了传送速度。
  • 地址总线(Address Bus):用来指定在 RAM(Random Access Memory)之中储存的数据的地址。总线宽度决定了 CPU 的寻址能力。一个 CPU 有 N 根地址总线,则可以说这个 CPU 的地址总线的宽度为 N。这样的 CPU 最多可以寻找 2 的 N 次方个内存单元。
  • 控制总线(Control Bus):将微处理器控制单元(Control Unit)的信号,传送到周边设备,一般常见的为 USB Bus 和 1394 Bus。总线宽度决定了 CPU 对系统中其它器件的控制能力。

内存地址空间

最终运行程序的是 CPU,我们用汇编的思维去理解程序,考虑问题。

首先思考如下一个场景,若我们电脑需要把一张图片显示出来,需要哪些过程或者说是步骤呢。

这里首先需要得到一个对应的内存地址,然后把数据放到对应的内存地址上面,最后再把数据的内容呈现到网卡上。而不同地址对应的其实是不同的器件,如内存条,显卡,网卡,RAM 主存储器等等。

而这些器件都分配了对应的地址,CPU 是根据地址传输对应需要的数据的。

接下来再看看一个例子,有如下程序,为什么得到的 q 的结果不是 6 + 7 + 8 = 21,而是 22 呢

1
2
3
4
5
6
7
void main {

int i=5,j=5,q,p;
p = (i++)+(i++)+(i++);
q = (++j)+(++j)+(++j);
printf("%d\n,%d\n,%d\n,%d\n", p,q,i,j)
}

这里其实可以编译完之后,通过汇编语言来看问题。(留给自己的实践任务)

寄存器(CPU 工作原理)

寄存器概述

X86 有 14 个寄存器,它们的名称为:AX、BX、CX、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。

寄存器有许多分类(以下为 x86)

  • 通用寄存器
  • 标志寄存器
  • 指令寄存器
  • 段寄存器
  • 控制寄存器
  • 调试寄存器
  • 描述符寄存器
  • 任务寄存器
  • MSR寄存器

AX、BX、CX、DX 通常用来存放一般性数据,被称为通用寄存器。下面以 AX 为例,来看一下它的逻辑结构。如下图

拿到一个数据,把它转换为二进制,再存储。

这四个通用寄存器都可以分为两个独立的 8 位寄存器使用,例如 AX 可以分为 AH 和 AL,H 就是 High,L 就是 Low。寄存器分高低,人不分贵贱(quoted by 小甲鱼

其实 AH 和 AL 分开和 AX 并不冲突,因为以前的系统是使用 8 位的,而不是 16 位,所以如果你要把 AX 向下兼容也是可以的,只要 8 - 15 都填 0 就可以,很好理解。

而如果在处理数据的时候,更常见的一种情况如下图所示。

当时看完了寄存器相关的部分,对于这些寄存器的功能和作用还是不太清楚,这里详细记录一下

通用寄存器

  • AX(Accumulator Register)

    • 功能:主要用于算术和逻辑运算,是许多指令默认使用的寄存器。在乘法和除法指令中,它有特殊用途。例如在进行 16 位乘法运算时,两个 16 位操作数相乘,结果的高 16 位存放在 DX 寄存器,低 16 位存放在 AX 寄存器。
    • 作用:它可以作为数据的临时存储位置,方便对数据进行快速处理。在一些输入 / 输出操作中,也用于传递数据。
  • BX(Base Register)

    • 功能:可以作为基址寄存器,用于在内存寻址中提供基地址。在一些指令中,它可以与偏移量组合来访问内存中的数据结构。
    • 作用:在访问数组或者结构体等复合数据类型时非常有用。例如,当需要遍历一个数组时,BX 可以存放数组的起始地址,通过与索引值相加就能访问数组中的每个元素。
  • CX(Count Register)

    • 功能:常用于循环操作中,作为循环计数器。许多循环指令会自动递减 CX 寄存器的值,并且在 CX 的值为 0 时结束循环。
    • 作用:在程序中实现循环逻辑时是不可或缺的。比如,要循环执行一段代码 10 次,可以将 10 赋值给 CX,然后在循环指令中,每执行一次循环,CX 的值就会自动减 1,直到 CX 为 0,循环结束。
  • DX(Data Register)

    • 功能:在进行 32 位的算术运算(如乘法和除法)时与 AX 配合使用,如前面提到的乘法运算存放高 16 位结果。也可用于一些输入 / 输出操作。
    • 作用:它扩展了数据处理的范围,特别是在处理大于 16 位的数据时发挥重要作用。在端口读写操作中,DX 用于指定 I/O 端口地址。

变址寄存器

  • SI(Source Index)和 DI(Destination Index)
    • 功能:这两个寄存器主要用于字符串操作和内存寻址。在字符串处理指令中,SI 通常指向源字符串的当前位置,DI 指向目标字符串的当前位置。它们也可以和基址寄存器或段寄存器一起用于内存寻址。
    • 作用:在复制、移动字符串等操作中很方便。例如,要将一个字符串从一个内存区域复制到另一个内存区域,SI 可以作为源字符串的索引,逐字节读取源字符串,DI 作为目标字符串的索引,将读取的数据逐字节写入目标区域。

指针寄存器

  • SP(Stack Pointer)
    • 功能:指向栈顶的位置。栈是一种后进先出(LIFO)的数据结构,在程序调用子程序、中断处理等过程中,用于保存和恢复现场数据。当数据压入栈(push 操作)时,SP 的值会自动减 2(16 位环境下),指向新的栈顶;当数据从栈中弹出(pop 操作)时,SP 的值会自动加 2。
    • 作用:它维护栈的完整性,确保数据按照正确的顺序进出栈。在函数调用过程中,函数的参数、返回地址等信息都通过栈来传递和保存,SP 精确地定位栈顶元素,保证这些操作的正确执行。
  • BP(Base Pointer)
    • 功能:主要用于在栈帧中访问局部变量和参数。它提供了一种相对稳定的基地址,通过与偏移量组合,可以方便地访问栈帧中的各种数据。
    • 作用:在函数调用时,栈帧中包含了函数的参数、局部变量等信息。BP 可以帮助程序员方便地在栈帧内定位这些数据,使得程序能够正确地访问和操作这些数据。

指令指针寄存器

  • IP(Instruction Pointer)
    • 功能:存放下一条要执行的指令的偏移地址。在程序执行过程中,CPU 会根据 IP 寄存器中的地址从内存中读取指令并执行,每执行完一条指令,IP 会自动更新,指向下一条指令的地址。
    • 作用:它控制着程序的执行流程,使得 CPU 能够按照正确的顺序从内存中读取和执行指令。通过改变 IP 的值,可以实现程序的跳转,如在条件分支语句或者函数调用时,程序会根据条件改变 IP 的指向,从而执行不同的代码段。

段寄存器

  • CS(Code Segment)
    • 功能:定义代码段的起始地址。代码段是存储程序代码的内存区域,CPU 通过 CS 寄存器和 IP 寄存器相结合来确定下一条要执行的指令的物理地址(物理地址 = CS×16 + IP)。
    • 作用:它划分了内存空间,使得程序代码能够在指定的区域存储和执行。在操作系统的内存管理和程序加载过程中,CS 寄存器的设置是至关重要的,它确保程序能够正确地运行在分配的代码段内存区域内。
  • SS(Stack Segment)
    • 功能:定义栈段的起始地址。栈段是用于存储栈数据的内存区域,与 SP 寄存器一起管理栈的操作。栈段的物理地址计算方式为 SS×16 + SP(16 位环境下)。
    • 作用:它确定了栈在内存中的位置,保证栈操作的正确执行。不同的程序或者进程可能有不同的栈段,通过 SS 寄存器来区分和管理这些栈空间,防止栈数据的混乱和冲突。
  • DS(Data Segment)
    • 功能:通常用于定义数据段的起始地址。数据段是存储程序中使用的数据的内存区域,在访问数据段中的内存单元时,默认情况下是通过 DS 寄存器来确定段基址。
    • 作用:它提供了一个默认的数据存储区域,方便程序对变量、数组等数据进行存储和访问。在程序初始化阶段,会设置 DS 寄存器,以便后续的数据访问操作能够顺利进行。
  • ES(Extra Segment)
    • 功能:作为附加的数据段寄存器。在一些字符串操作和内存块复制等操作中,可以作为目标数据段的段基址。
    • 作用:它为数据存储和处理提供了额外的灵活性。例如,在使用 REP MOVSB 指令(重复移动字节串)时,DS:SI 指向源字符串,ES:DI 指向目标字符串,ES 在这里就起到了指定目标数据段的作用。

标志寄存器(PSW - Program Status Word)

  • 功能:它包含了一系列的状态标志位,用于记录 CPU 执行算术和逻辑运算后的结果状态,以及一些控制标志。例如,进位标志位 CF(Carry Flag)用于表示无符号数运算是否产生进位或借位;零标志位 ZF(Zero Flag)在运算结果为 0 时被置 1;符号标志位 SF(Sign Flag)反映运算结果的符号,结果为负时 SF = 1;溢出标志位 OF(Overflow Flag)用于表示有符号数运算是否溢出等。
  • 作用:这些标志位可以被条件跳转指令用来改变程序的执行流程。例如,在比较两个数的大小后,根据标志位的状态可以决定是否执行某一段代码。如果比较结果相等,ZF 标志位会被置 1,程序可以根据这个标志位进行相等情况的处理。

存储器

CPU 可以直接使用的信息在存储器中存放,CPU 想要进行数据的读写必须和外部器件进行三类信息的交互

  • 存储单元的地址(地址信息)
  • 器件的选择,读或写命令(控制信息)
  • 读或写的数据(数据信息)

在存储器中指令和数据是没有区别的,它们都是二进制信息,存储器依靠传输二进制信息所使用的总线类型来进行判断是指令还是数据。

一个存储单元可以存储8个 bit

汇编语言基础指令

汇编指令示例 实际作用 等价代码
mov rax,rbx 用于赋值 rax=rbx
add/sub rax,rbx 用于加/减法 rax+=rbx/rax-=rbx
and/xor/or rax,rbx 用于与/异或/或 rax&=rbx/rax^=rbx/rax|=rbx
push rax 压栈 rsp-=8;*rsp=rax
pop rax 出栈 rax=*rsp;rsp+=8
call rax 调用函数 push rip;jmp rax;
ret 从函数返回 pop rip;
cmp rax,rbx 比较两个数 rax-rbx,不保留结果,只修改flags寄存器
test rax,rbx 比较两个数 rax&rbx,不保留结果,只修改flags寄存器

各种跳转指令,跳转指令不同于 mov 指令,他可以用于修改段寄存器 cs、ip 的值,从而修改 CPU 在内存中所读取的内容的地址

汇编指令示例 英文 实际作用
jmp jump 跳转
jz jump if zero 为0时跳转
jnz jump if not zero 不为0时跳转
jg jump if greater 有符号数大于跳转
jl jump if less 有符号数小于跳转

Debug 语法 && 运行一个汇编脚本

以前版本的 Windows 里面自带有 debug,但是 win10 之后的更高版本取消了,我们可以通过配置 debug 文件,下载 DOSbox 来体验 debug,并通过 debug 来初体验一下汇编语言。

Windows 10无法使用debug的解决方案 - Angel_Kitty - 博客园

通过命令mount <磁盘> <debug 安装路径>挂载磁盘

常见的 debug 命令 在 debug 中不区分大小写

  • R:查看、改变 CPU 寄存器的内容

    格式 -r 或 -r 寄存器名称 原始值 修改值

    查看寄存器

修改寄存器

  • D:查看内存中的内容

格式 -d 或 -d 起始地址 结束地址/长度

  • E:改写内存中的内容

格式 -e 起始地址 二进制命令

  • U:将机器指令翻译成汇编指令

格式 -u 起始地址 结束地址/长度

  • T:执行一条机器指令

格式 -t 或 -t 地址 指令条数

  • A:以汇编指令的格式向内存中存入一条机器指令

格式 -a 地址 存放的汇编指令

看起来似乎 Debug 语法是一个微型版的 gdb

接下来是运行汇编脚本,可以参考这篇文章,比较容易。

https://zhuanlan.zhihu.com/p/370786100

BX 和 Loop

BX

BX 是什么

bx是一个通用寄存器的代号,当 bx 变成 [bx] 时,bx 的值就不单单代表着数值,它还表示着偏移地址。

BX 如何运作

举个例子

1
2
3
4
5
mov ax, [bx]

功能:bx中所存放的数据作为一个偏移地址,然后再结合 ds 中段地址的数据计算出物理地址

物理地址=(ds)*16+bx

然后将该物理地址中的数据送入 ax 中。

Loop

也就是循环

格式以及运作原理

格式如下:

1
2
3
4
5
6
7
8
...  
...
s:xxx
xxx
loop s
xxx
...
...
  • 标号 s 要放在 loop 指令之前
  • 循环执行的程序段放在标号和 loop 指令之间

运作原理:

当 CPU 执行 loop命令时会执行一下两个步骤:

  1. (cx)=(cx)-1
  2. 判断 cx 的值是否为0,如果不为0,则跳转至 s 处继续执行代码;如果为零,那就继续执行下一条指令

两者的联合使用

当我们需要对连续性变化的内存地址进行重复性的操作时,如果我们不适用循环,那我们的源码会变得十分冗长,且有很多的重复性语句。

例如:计算 ffff:0~ffff:b 单元内数据的和,结果储存在 dx 中

首先我们最应该考虑的是数据是否会溢出,数据类型是不是匹配等问题。很显然,溢出的问题是不存在的,但是将 ffff:0 中的数据直接放入 bx 中是不可取的,因为前者的数据是8位,而后者的数据是16位。

通过如下步骤解决问题

1
2
mov al,ds:[0] //将目标数据放入低位的 ax 寄存器中  
mov ah,0 //将 ax 寄存器的高位置为0,此时(ax)=(ffff:0)且数据长度为16位

不利用循环的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
assume cs:code  
code segment

mov ax,0ffffh //汇编语言中不允许字母为开头的常量,所以我们添加0
mov ds,ax //设置(ds)=ffffh
mov dx,0 //初始化累加器

mov al,ds:[0]
mov ah,0 //获取目标内存中的数据且长度为16位
add dx,ax //进行数据累加

mov al,ds:[1]
mov ah,0
add dx,ax

.
.
.

mov al,ds:[0bh]
mov ah,0
add dx,ax

mov ax,4c00h //程序返回
int 21h

code ends
end

该程序中间的实现功能的主要部分大多都是相似的,变化的不过时偏移地址的值,因此我们在循环中改变偏移地址的值即可。

使用循环的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
assume cs:code  
code segment

mov ax,0ffffh //汇编语言中不允许字母为开头的常量,所以我们添加0
mov ds,ax //设置(ds)=ffffh
mov dx,0 //初始化累加器

mov cx,12 //设置循环为12次

s: mov al,[bx]
mov ah,0
add dx,ax //将目标数据存入ax中并累加
inc bx //改变偏移地址
loop s

mov ax,4c00h //程序返回
int 21h

code ends
end

多个段的程序

有了多个段之后,程序会变得更加有条理、更清晰

  • 直接以实际编程问题来看

将 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h 以下8个数据的和的结果放入寄存器 ax 中

如果用其他语言来写,应该是相加,然后让 ax = result;由汇编来写,则有一些不太一样的地方,先看实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:codesg  
codesg segment

dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

mov bx,0
mov ax,0

mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s

mov ax,4c00h
int 21h

codesg ends
end

dw 先定义字型数据,即“define word”。在这里定义了8个字型数据,它们所占用的空间大小为16个字节。

由于这八个数据在代码段中,程序在运行的时候会把代码段的短地址存放在 CS 中(codesg),所以我们可以从 CS 中得到它们的段地址。

接下来分析一下这 8 个数据的偏移地址是多少呢?

因为用 dw 定义的数据处于代码段的最开始,所以偏移地址为 0、2、4…… 程序在运行时,它们的地址就是 CS:0、CS:2、CS:4 ……

接着对这段代码开启动调看一下,会发现进入了循环,我们所想要写入的数据已经被连续地存放在了代码段的最开始处。

关注图中 -r 中显示的所要执行的下一条命令,可以发现是and ax,[bx+di]并不是我们所想要的mov bx,0

产生这种情况的原因在于,计算机认为我们程序执行的开始是 076a:0000 处,但是在此处之后的16个字节已经被我们用来存放我们所定义的字型数据了,而and ax,[bx+di]就是我们所存放的数据变成汇编语言的结果。

所产生的问题就是: 当我们的程序包含有自定义的数据时,由于计算机自动将 cs:ip 中的内容当程序执行的第一句话,我们所定义的数据会被当成命令来执行。

我们也可以推理出来,我们所希望计算机开始执行语句的地方是 076a:0010

解决方式

修改代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:codesg  
codesg segment

dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

start: mov bx,0
mov ax,0

mov cx,8
s:add ax,cs:[bx]
add bx,2
loop s

mov ax,4c00h
int 21h

codesg ends
end start

我们通过标识符 start 来告诉计算机我们的代码段所要开始执行语句的位置。start 标识符是我们可以进行自定义的,只要是符合规定的名称都可以,其次就是保证两处的标识符要保持一致

end 的作用: end 不但通知计算机程序的结束之处,还告诉了编译器程序的入口之处。

代码段中使用栈

  • 老问题,将这些数据0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h按照逆序存放

对应代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
assume cs:codesg  
codesg segment

dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0 //定义栈段

start: mov ax,cs
mov ss,ax //修改栈段
mov sp,20h //修改栈偏移指针,将其指向栈底-1处

mov bx,0
mov cx,8
s0: push cs:[bx]
add bx,2
loop s0 //数据入栈

mov bx,0
mov cx,8
s1: pop cs:[bx]
add bx,2
loop s1 //数据出栈

mov ax,4c00h
int 21h

codesg ends
end start

debug 后的初始状态

执行完第一个 loop 后的状态,所有数据入栈

执行完第二个 loop 后的状态,数据出栈并存放

小结: 以上是我们通过在代码段中自定义数据段和栈段来实现我们的功能,但当我们的程序变得复杂是,将这些段都糊在一起会显得很杂乱且容易出错。

使用多个段

为了解决上面的问题,我们使用多个段。所谓的多个段就是:将数据、代码、栈放入不同的段中。

使用多个段的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
assume cs:codesg,ds:data,ss:stack  

data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
dw 0,0,0,0,0,0,0,0
stack ends

codesg segment

start: mov ax,stack
mov ss,ax
mov sp,10h

mov ax,data
mov ds,ax

mov bx,0
mov cx,8
s0: push cs:[bx]
add bx,2
loop s0

mov bx,0
mov cx,8
s1: pop cs:[bx]
add bx,2
loop s1

mov ax,4c00h
int 21h

codesg ends
end start

观察初始状态的寄存器情况

一般来说寄存器中 DS 和 CS 的数值应当是相差 10h 的,但是这里它们相差了 12h,再观察图中的汇编语句我们可以知道我们所定义的 stack 段的段地址是 076b。

查看内存中的情况

我们所申请的数据段和栈段被安排了代码段之前,并且每一段的空间大小都为16个字节,这也就是为什么 DS 和 CS相差了12h的原因。

对多个段的顺序进行改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
assume cs:codesg,ds:data,ss:stack  

codesg segment

start: mov ax,stack
mov ss,ax
mov sp,10h

mov ax,data
mov ds,ax

mov bx,0
mov cx,8
s0: push cs:[bx]
add bx,2
loop s0

mov bx,0
mov cx,8
s1: pop cs:[bx]
add bx,2
loop s1

mov ax,4c00h
int 21h

codesg ends

data segment
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
dw 0,0,0,0,0,0,0,0
stack ends

end start

当我们在程序中改变各个段的次序时,可以发现在内存中它们的位置也发生了改变

灵活查看与使用内存地址

在前面我们讲了用 [0]、[bx] 的方法来定位内存单元的地址,在这一部分我们学习一些更为灵活的定位内存地址的方法,和相关的编程方法。

[bx+idata]

[bx] 可以用来指明一个内存单元,[bx+idata] 也具有相同的功能且更为灵活。

[bx+idata] 表示:偏移地址=(bx)+idata

例如:mov ax,[bx+200] 它的含义就是将一个内存单元的内容放入 ax 中,这个内存单元的长度为2个字节,偏移地址为 (bx)+200,段地址为 (ds)。将该命令用数学化的公式表达即为:(ax)=((ds)*16+(bx)+200)

指令mov ax,[bx+200]也可以用如下形式表示

1
2
3
mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200

[bx+idata] 与数组的关系

问题:将 datasg 中定义的字符串转为大写,第二个字符串转为小写,在 ASCII 码中,小写字母和大写字母的区别在于第 5 位(从右往左数),将第 5 位清零即可将小写字母转换为大写字母。

所以写出相对应代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
assume cs:codesg,ds:datasg  

datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends

codesg segment
start:mov ax,datasg
mov ds,ax
mov bx,0

mov cx,5
s:mov al,[bx]
and al,11011111b //通过 and 将小写改为大写
mov [bx],al
inc bx
loop s

mov bx,5
mov cx,5
s0:mov al,[bx]
or al,00100000b //通过 or 将大写改为小写
mov [bx],al
inc bx
loop s0

mov ax,4c00h
int 21h
codesg ends

end start

简单的逻辑解读

  • s: 是循环标签。
  • mov al,[bx]:将数据段中偏移地址为 BX 的字节数据加载到 AL 寄存器。
  • and al,11011111b:通过 AND 操作将 AL 中的字符转换为大写。在 ASCII 码中,小写字母和大写字母的区别在于第 5 位(从右往左数),将第 5 位清零即可将小写字母转换为大写字母。
  • mov [bx],al:将转换后的字符存回数据段原来的位置。
  • inc bx:将偏移地址 BX 加 1,指向下一个字符。
  • loop s:循环执行,直到 CX 减为 0。

而以上的方法并没有将 [bx+idata] 利用起来,我们依旧是通过两个循环实现我们的目的。

我们可以将这两个等长的字符串看成是2个数组,在一个循环中对2个数组同时进行处理。这两个数组的不同之处就是数组起始地址的偏移地址不一样,而每次处理时对于这两个数组的偏移地址地址都是相同的,而数组起始地址的偏移地址我们可以通过 [dx+idata] 的方式来改变。

使用 [dx+idata] 的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
assume cs:codesg,ds:datasg  

datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends

codesg segment
start:mov ax,datasg
mov ds,ax
mov bx,0

mov cx,5
s:mov al,0[bx] //0为第一个字符串起始地址的偏移地址
and al,11011111b
mov 0[bx],al

mov al,5[bx] //5为第二个字符串起始地址的偏移地址
or al,00100000b
mov 5[bx],al

inc bx
loop s

mov ax,4c00h
int 21h
codesg ends

end start

汇编语言和C语言相呼应的地方

1
2
汇编语言:0[bx],5[bx]
C语言:a[i],b[i]

SI和DI

它们是寄存器中和 bx 功能相近的寄存器,它们之间的区别就是:si 和 di 不能分成两个8位寄存器来使用。

1
2
3
[bx+si] 偏移地址=(bx)+(si)
[bx+si+idata] 偏移地址=(bx)+(si)+idata
[bx+200+si]=[200+bx+si]=200[bx][si]=[bx].200[si]=[bx][si].200

暂存数据

当我们程序变得复杂庞大时,需要对一些数据进行暂存,而寄存器数量有限,我们只能通过内存来帮助我们存储这些临时数据,但是如果只是通过简单地获取内存中的一个单元地址来存储我们的数据的方法是不可取的,因为我们必须要记住我们将数据放入了哪个内存单元中,这样程序会很容易混乱。

解决办法就是利用

一般来说,在需要缓存数据的时候,我们都应该使用栈来操作。

数据的处理

  • 前面讲了一些基本语法,现在回来看一看计算机在数据进行处理、运算的过程中,都是怎么一个过程

计算机处理数据、运算的过程中,有两个最基本的问题

  • 所要处理的数据放在是什么地方
  • 所要处理的数据有多长

这两个问题的答案在机器指令中必须给以明确或隐含的说明。

为了对问题研究的方便,我们定义了两个描述性符号:

reg——>寄存器

sreg——>段寄存器

reg 的集合包括

  • ax、ah、al 临时存放一些数据
  • bx、bh、bl bp si、di 都用于表示偏移地址
  • cx、ch、cl 控制 loop 循环的次数
  • dx、dh、dl 数据寄存器
  • sp 指向栈底指针的偏移地址

sreg 的集合包括

  • ds 数据段的段地址
  • ss 栈的栈段
  • cs 代码段的段地址
  • es 附加段寄存器

用于表示偏移地址的 bx、si、di、bp 所有正确的用法

我们可以将 bx 和 bp 理解为基本偏移地址寄存器,si 和 di 是用于灵活改变、使用这两个基本偏移地址寄存器的辅助偏移地址寄存器。

  • bx 的默认段地址为 ds 中的数值
  • bp 的默认段地址为 ss 中的数值

寻址

一个指令在执行前,指令所涉及的数据可以储存在3个地方:CPU内部、内存、端口

数据位置表达的三个概念

有3种概念来表达数据的位置:立即数(idata)、寄存器、段地址和偏移地址

  • 立即数

在机器指令中直接给出的数据

  • 寄存器

指令要处理的数据在寄存器中,在指令中指明了相应的寄存器名称

  • 段地址(SA)和偏移地址(EA)

所要处理的数据在内存中,用 [x] 方式给出 EA,SA 为某个段的寄存器。

默认段地址在 ds 中的情况

默认段地址在 ss 中

指明段地址

寻址方式

指令执行过程

  1. 在寄存器的 cs 和 ip 中获取代码段在内存中的地址
  2. 读取指令
  3. 将指令放入指令缓冲寄存器
  4. 获取指令中涉及的相关数据
  5. 执行指令

其中获取指令的相关数据时涉及到了寻址

处理数据的长度

8086CPU 的指令能够处理两种数据的长度

  • byte
  • word

其中 byte 占一个字节,word 占两个字节

有两种方式告诉计算机所要执行的指令长度

  • 寄存器
  • X ptr

寄存器指明

因为寄存器自带具有多少个字节的属性,因此计算机可以直接通过寄存器获取所要处理数据的长度

  • 对字进行操作
  • 对字节进行操作

div 指令

除法指令,使用时的注意点:

  • 除数:有8位和16位2种选择,位于 reg 或内存单元中
  • 被除数:默认放在 AX 或 DX 和 AX 中。
    • 除数为8位,被除数为16位,默认存储在 AX 中
    • 除数为16位,被除数为32位,默认高位存储在 DX 中,低位放在 AX 中
  • 结果:余数和商
    • 除数为8位,AL 中存储商,AH 中存储余数
    • 除数为16位,AX 中存储商,DX 中存储余数

dd 伪指令

之前使用过 db 和 dw 来定义字节型数据和字型数据,dd 用来定义 双字型数据(double word),dd 所定义的数据占了2个字的大小,也就是4个字节。

1
2
3
4
5
data segment  
db 1 //在 data:0 处,数据为 01H,占1个字节
dw 1 //在 data:1 处,数据为 0001H,占1个字
dd 1 //在 data:3 处,数据为 00000001H,占2个字
data ends

举个例子:

使用 div 计算 data 段中第一个数据除以第二个数据后的结果,商储存在第三个数据的存储单元内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data segment  
dd 100001
dw 100
dw 0
data ends

code segment
mov ax,data
mov ds,ax
mov ax,ds:[0] //将 ds:0 中的低16位存储在 ax 中
mov dx,ds:[2] //将 ds:2 中的高16位存储在 dx 中
div word ptr ds:[4] //用 dx:ax 中的32位数据除以 ds:[4] 字单元中的数据
mov ds:[6],ax //将商储存在 ds:[6] 字单元中
code ends

dup

dup 是一个操作符。

dup、db、dw、dd 都是由编译器识别处理的符号。

通常和 db、dw、dd 等数据定义伪指令配合使用,用来进行数据的重复

例如:

1
2
db 3 dup(0) //定义了3个字节,值都为0,相当于 db 0,0,0  
db 3 dup(0,1,2) //定义了9个字节,值分别为 0,1,2,0,1,2,0,1,2,相当于 db 0,1,2,0,1,2,0,1,2。

使用的格式如下:

  • db 重复的次数 dup (重复的字节型数据)
  • dw 重复的次数 dup (重复的字型数据)
  • dd 重复的次数 dup (重复的双字型数据)
 评论