首页 > 技术文章 > 汇编语言 - 王爽

ztw1002 2021-03-09 18:37 原文

前言

学习汇编的两个最根本的目的:充分获得底层编程的体验,深刻理解机器运行程序的机理。

任何不以循序渐进的方式进行的学习,都将出现盲目探索和不成系统的情况,最终学习到的也大都是相对零散的知识,并不能建立起一个系统的知识结构。非循序渐进的学习,也达不到循序渐进学习所能达到的深度,因为后者是步步深入的,每一步都以前一步为基础

必须遵守的原则:
①没有通过监测点不要向下学习
②没有完成当前的实验不要向下学习
③每一个实验都是后续内容的基础,实验的任务必须独立完成
④本书的教学重心是:通过学习关键指令来深入理解机器工作的基本原理,培养底层编程意识和思想

我们必须通过一定的编程实践,体验一个裸机的环境,在一个没有操作系统的环境中直接对硬件编程

第1章 基础知识

汇编语言是直接在硬件之上工作的编程语言

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

寄存器是CPU中可以存储数据的器件

编译器能够将汇编指令转换成机器指令的翻译程序

汇编语言由以下3类指令组成:
1.汇编指令:机器码的助记符,由对应的机器码(核心) <汇编指令是机器指令便于记忆的书写格式>
2.伪指令:没有对应的机器码,由编译器执行,计算机并不执行
3.其他符号:+ - * /

要想让一个CPU工作,就必须向它提供指令和数据。指令和数据在存储器中存放,也就是我们平时所说的内存

CPU要从内存中读数据,首先要指定存储单元的地址,CPU在读写数据时还要指明。它要对哪个器件进行哪种操作
所以CPU要想进行数据的读写,必须和外部器件进行下面3类信息的交互->

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

指令和数据没有任何区别,都是二进制信息。CPU在工作的时候把有的信息看作数据,为同样的信息赋予了不同的意义

1 Byte = 8 bit = 8个二进制位 = 8位二进制数据

在计算机中专门有连接CPU和其他芯片的导线,通常称为总线,总线从逻辑上分为3类,地址总线、控制总线、数据总线

(1)CPU通过地址线将地址信息3发出
(2)CPU通过控制线发出内存读命令,选中存储器芯片并通知它将要从中读取数据
(3)存储器将3号单元中的数据8通过数据线送入CPU

CPU是通过地址总线来指定存储器单元的
10根导线可以传送10位二进制数据。而10位二进制数可以表示\(2^10\)个不同的数据
->一个CPU有N根地址线,则可以说这个CPU的地址总线的宽度为N

CPU与内存或其他器件之间的数据传送是通过数据总线来进行的
数据总线的宽度决定了CPU和外界的数据传送速度,1根数据总线一次传送一个1位二进制数据
1 Byte = 8位二进制数据

8088CPU的数据总线宽度为8,8086CPU的数据总线宽度为16

8088CPU数据总线上的数据传送情况:8088只有8根数据线,一次只能传8位数据,所以向内存写入数据89D8H时需要进行两次数据传送

8086CPU数据总线上的数据传送情况:8088有16根数据线,一次可以传16位数据,所以可一次传送数据89D8H

1.10 控制总线

CPU对外部器件的控制是通过控制总线来进行的,在这里控制总线是个总称,控制总线是一些不同控制线的集合。控制总线的宽度决定了CPU对外部器件的控制能力

1.1~1.10小结
(1)CPU可以直接使用的信息在存储器中存放

(2)地址总线的宽度决定了CPU的寻址能力

(3)数据总线的宽度决定了CPU与其他期间进行数据传送时的一次数据传送量

(4)控制总线的宽度决定了CPU对系统中其他期间的控制能力

检测点1.1
① 1个CPU的寻址能力为8KB,那么它的地址总线的宽度为---13
②8080、8088、8086、80286、80386的地址总线宽度分别为8根、8根、16根、16根、32根。则他们一次可以传送的数据为---1B、1B、2B、2B、4B
③从内存中读取1024字节的数据,8086至少要读512次,80386至少要读256次

CPU通过总线向接口卡发送命令,接口卡根据CPU的命令控制外设进行工作

1.14 各类存储器芯片

一台PC机装有多个存储器芯片,从读写属性上看分为两类:随机存储器(RAM)和只读存储器(ROM)
RAM可读可写,但必须带电存储,关机后存储的内容消失
ROM只能读取不能写入

存储器从功能和连接上又可分为以下几类:

  • RAM
  • BIOS(Basic Input Output System):基本输入输出系统
  • 接口卡上的RAM:显示卡上的RAM一般称为显存。我们将需要显示的内容写入显存,就会出现在显示器上

CPU在操控存储器的时候,把他们都当作内存来对待,把他们总的看做一个由若干存储单元组成的逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间

可以将所有的物理存储器看作一个由若干存储单元组成的逻辑存储器,每个物理存储器在这个逻辑存储器中占有一个地址段,即一段地址空间。CPU在这段地址空间中读写数据,实际上就是在相对应的物理存储器中读写数据

内存地址空间的大小受CPU地址总线宽度的限制

不同的计算机系统的内存地址空间的分配情况是不同的

系统中所有存储器中的存储单元都处于一个统一的逻辑存储器中,它的容量受CPU寻址能力的限制。这个逻辑存储器即是我们所说的内存地址空间

第2章 寄存器

在CPU中:

  • 运算器进行信息处理
  • 寄存器进行信息存储
  • 控制器控制各种器件进行工作
  • 内部总线连接各种器件,在它们之间进行数据的传送

对于一个汇编程序员来说,CPU中的主要部件是寄存器,寄存器是CPU中程序员可以用指令读写的部件。程序员通过改变各种寄存器中的内容来实现对CPU的控制

2.1 通用寄存器

8086CPU 的所有寄存器都是16位的

AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,被称为通用寄存器,且都可分为两个可独立使用的8位寄存器来用

2.2 字在寄存器中的存储

1 word = 2 byte

十六进制数的一位相当于二进制数的四位

**以后的课程中,我们多用十六进制来表示一个数据。为了区分不同的进制,在十六进制表示的数据的后面加 H ,在二进制表示的数据后面加 B **

2.3 几条汇编指令

在进行数据传送或运算时,要注意指令的两个操作对象的位数应当是一致的

2.4 物理地址

CPU访问内存单元时,要给出内存单元的地址。每一个内存单元在这个空间中都有唯一的地址,我们将这个唯一的地址称为物理地址

16位结构的CPU描述了一个CPU具有下面几方面的结构特性:

  • 运算器一次最多可以处理16位的数据
  • 寄存器的最大宽度为16位
  • 寄存器和运算器之间的通路位16位

2.6 8086CPU给出物理地址的方法

8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力,8086CPU又是16位结构,在内部一次性处理、传输、暂时存储的地址为16位,如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出的寻址能力只有64KB

8086采用一种在内部用两个16位地址合成的方法来形成一个20位的物理地址

物理地址 = 段地址*16 + 偏移地址

一个X进制的数据左移1位,相当于乘以X

2.7 段地址*16 + 偏移地址 = 物理地址的本质含义

  • 8086CPU的这种寻址能力时"基础地址 + 偏移地址 = 物理地址",段地址*16可看作是基础地址

2.8 段的概念

内存并没有分段,段的划分来自于CPU,我们可以用分段的方式来管理内存。以后我们可以根据需要,将若干地址连续的内存单元看作一个段

段地址*16必然是16的倍数,所以一个段的起始地址也一定是16的倍数;偏移地址为16位,16位地址的寻址能力为64KB,所以一个段的长度最大为64KB

内存单元地址小结
CPU在访问内存单元时,必须向内存地址提供内存单元的物理地址
段地址SA(Segment Address) 偏移地址OA(Offset Address) 有效地址EA(Effective Address)

检测点2.2
①有一数据存放在内存20000H单元中,现给定段地址为SA,若想用偏移地址寻到此单元。则SA应满足的条件是:最小为 1001H*,最大为 2000H

2.9 段寄存器

段地址再8086CPU的段寄存器中存放,8086有4个段寄存器:CS、DS、SS、ES

2.10 CS和IP

CS为代码段寄存器,IP为指令指针寄存器

8086机中,任意时刻,设CS中的内容为M,IP中的内容为N,8086CPU将从内存M*16 + N单元开始,读取一条指令并执行

8086机中,任意时刻,CPU将CS:IP指向的内容当作指令执行 <区别指令和数据的方法>

读取一条指令后,IP中的值自动增加,以使CPU可以读取下一条指令。因当前读入的指令B82301长度为3个字节,所以IP中的值+3。此时CS:IP指向内存单元2000:0003

8086CPU的工作过程可以简要描述如下:
(1)从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
(2)IP = IP + 所读取指令的长度,从而指向下一条指令
(3)执行指令。转到步骤(1),重复这个过程

CPU将CS:IP指向的内存单元中的内容看作指令。如果说内存中的一段信息曾被CPU执行过的话,那么它所在的内存单元必然被CS:IP指向过

2.11 修改CS、IP的指令

程序员可以通过改变CS、IP中的内容来控制CPU执行目标指令

若想同时修改CS、IP的内容,可用形如"jmp段地址:偏移地址"的指令完成
jmp 2AE3:3,执行后:CS = 2AE3H,IP = 0003H,CPU将从2AE33H处读取指令

"jmp 段地址:偏移地址" 指令的功能为:用指令中给出的段地址修改CS,偏移地址修改IP

"jmp 某一合法寄存器"指令的功能为:用寄存器中的值修改IP jmp ax 在含义上相当于 mov IP,ax

若想仅修改IP中的内容,可用形如"jmp 某一合法寄存器"的指令完成,如:
jmp ax,指令执行前:ax = 1000H,CS = 2000H,IP = 0003H
指令执行后:ax = 1000H,CS = 2000H,IP = 1000H

CS、IP中的内容送入地址加法器(地址加法器完成:物理地址 = 段地址16 + 偏移地址)*

要让CPU执行我们放在代码段中的指令,必须将CS:IP指向所定义的代码段中的第一条指令的首地址

检测点2.3
下面的3条指令执行后,CPU几次修改IP?都是在什么时候?最后IP中的值是多少?
mov ax,bx
sub ax,ax
jmp ax

一共修改4次
第一次:读取mov ax,bx后
第二次:读取sub ax,ax后
第三次:读取jmp ax后
第四次:执行jmp ax修改IP
最后IP的值为0000H,因为最后ax中的值为0000H,所以IP中的值也为0000H

实验一 查看CPU和内存,用机器指令和汇编指令编程


用 R 命令查看、改变CPU寄存器的内容
要修改AX中的值:

  • 输入“r ax”后按 Enter 键,将出现“:”,在后面输入要写入的数据后按 Enter 键,即完成对AX中内容的修改

(1)用 Debug 的 D 命令查看内存中的内容

  • 要查看内存10000H处的内容,可以用“d 短地址:偏移地址”的格式来查看,输入“d 1000:0”即可查看内存10000H处的内容

  • 使用“d段地址:偏移地址”,debug将列出从指定内存单元开始的128个内存单元的内容

  • 使用“d段地址:偏移地址”之后,接着使用D命令,可列出后续的内容;也可以指定D命令的查看范围,此时采用的“d段地址:起始偏移地址 结尾偏移地址”的格式

(2)用 Debug 的 E 命令改写内存中的内容

  • 要将内存1000:0~1000:9单元中的内容分别写为0、1、2、3、4、5、6、7、8、9,可以用“e 起始地址 数据 数据 数据...”的格式来进行

  • 也可以一个一个地改写内存中的内容
    输入 e 1000:10 按 Enter 键 -> 输入数据(如果输入空格则不对当前内存单元进行改写)

  • 可以用E命令向内存中写入字符/字符串:-e 1000:0 1 'a' 2 'b' 3 'c'

用E命令向内存中写入机器码,用U命令查看内存中机器码的含义,用T命令执行内存中的机器码

  • 若要用T命令控制CPU执行我们写到 1000:0 的指令,必须先让CS:IP指向1000:0;接着用R命令修改CS、IP中的内容,使CS:IP 指向1000:0;再使用T命令来执行我们写入的指令

用 Debug 的 A 命令以汇编指令的形式在内存中写入机器指令

总结
R命令:查看、修改CPU中寄存器的内容
D命令:查看内存中的内容
E命令:修改内存中那个的内容
U命令:将内存中的内容解释为机器指令和对饮的汇编指令
T命令:执行CS:IP指向的内存单元处的指令
A命令:以汇编指令的形式向内存中写入指令

在汇编语言学习中,Debug是一个经常用到的工具,在学习预备知识中,应该一边看书一边在机器上操作

第3章 寄存器(访问内存)

CPU中,用16位寄存器来存储一个字。高8位存放高位字节,低8位存放低位字节。

字节是内存单元(一个单元存放一个字节),则一个字要用两个地址连续的内存单元来存放,这个字的低位字节存放在低地址单元中,高位字节存放在高地址单元中。

字单元:存放一个字型数据(16位)的内存单元,由两个地址连续的内存单元组成
N地址字单元:起始地址为N的字单元

问题3.1

根据图3.1:
①0地址字单元中存放的字节型数据是多少?

  • 20H
    ②0地址字单元中存放的字型数据是多少?
  • 4E20H
    ③1地址字单元中存放的字形数据是多少?
  • 124EH

3.2 DS和[address]

ds:段寄存器 通常用来存放要访问的段地址

要读取10000H单元的内容,可以用如下的程序段进行:
mov bx,1000H
mov ds,bx
mov al,[0]
上面的3条指令将10000H(1000:0)中的数据读到al中。

[...]表示一个内存单元,[...]中的0表示内存单元的偏移地址

指令执行时,8086CPU自动取ds中的数据为内存单元的段地址

8086CPU不支持将数据直接送入段寄存器的操作,要将1000H送入ds,只能用一个寄存器来进行中转:现将1000H送入一个一般的寄存器,如bx,再将bx中的内容送入ds

问题3.2
如何将al中的数据送入内存单元10000H中?
解:从内存单元到寄存器的格式是:"mov 寄存器名,内存单元地址";从寄存器到内存单元是:"mov 内存单元地址,寄存器名"
10000H可以表示为1000:0,用ds存放段地址1000H,偏移地址是0,则mov [0],al可完成从al到10000H的数据传送。
完整指令为:
mov bx,1000H
mov ds,bx
mov [0],al

3.3 字的传送

8086是16位结构,有16根数据线,所以可以一次性传送16位的数据/1个字,只要在mov指令中给出16位的寄存器就可以进行16位数据的传送了

ds:段寄存器


mov ax,1000H
mov ds,ax //前两条指令的目的是将ds设为1000H

mov ax,[0] //1000:0处存放的字形数据送入ax,1000:1单元存放字型数据的高8位:11H;1000:0单元存放字型数据的低8位:23H,所以1000:0处存放的字型数据为1123H

[0]对应0和1
[1]对应1和2,以此类推

写出下面的指令执行后内存中的值

mov [0],ax //ax中的字型数据送到1000:0处

bx = 2C34H
sub bx,[2]
bx = 1B12H //bx = bx中的字型数据-1000:2处的字形数据=2C34H-1122H=1B12H

3.4 mov、add、sub指令

  • 不能将常数放在段寄存器
  • 不能两个段寄存器
  • 不能两个内存

mov [0000],cs 执行后,CS中的数据(0B39H)被写入1000:0处,1000:1单元存放0BH,1000:0单元存放39H

3.5 数据段

比如将123B0H~123B9H的内存单元定义为数据段,现在要累加这个数据段中的前3个单元的数据
mov ax,123BH
mov ds,ax
mov al,0 //用al存放累加结果
add al,[0] //将数据段第一个单元(偏移地址为0)中的数值加到al中
add al,[1] //将数据段第二个单元(偏移地址为0)中的数值加到al中
add al,[2] //将数据段第三个单元(偏移地址为0)中的数值加到al中

一个字型数据占两个单元

3.1~3.5小结*
1.字节在内存中存储时,要用两个地址连续的内存单元来存放,字的低位字节存放在低地址单元中,高位字节存放在高地址单元中
2.用 mov 指令访问内存单元,可以在mov指令中只给出单元的偏移地址,此时段地址默认在DS寄存器中
3.[address]表示一个偏移地址为address的内存单元
4.在内存和寄存器之间传送字型数据时,高地址单元和高8位寄存器、低地址单元和低8位寄存器相对应
5.mov、add、sub是具有两个操作对象的指令。jmp是具有一个操作对象的指令

检测点3.1

为什么AX = 2662H?
解:[0000]表示的是,而"d 0:0 1f"表示的是"d 段地址:起始偏移地址 结尾偏移地址",[0000]对应的两个单元为62和26,然后1f就对应下面那行

3.6 栈

在这里我们对栈的研究仅限于这个角度:栈是一种具有特殊的访问方式的存储空间。它的特殊性在于:最后进入这个空间的数据,最先出去

栈有两个基本的操作:入栈和出栈。入栈就是将一个新的元素放到栈顶,出栈就是从栈顶取出一个元素

3.7 CPU提供的栈机制

在基于8086CPU编程的时候,可以将一段内存当作栈来使用

8086提供入栈(PUSH)和出栈(POP)指令

push ax表示将寄存器ax中的数据送入栈中;pop ax表示从栈顶取出数据送入ax。8086CPU的入栈和出栈都是以为单位进行的

字型数据用两个单元存放,高地址单元存放高8位,低地址单元存放

CPU执行 push 和 pop 指令时,将对这段空间按照栈的后进先出的规则进行访问

CPU如何知道当前要执行的指令所在的位置?
解:CS、IP中存放着当前指令的段地址和偏移地址

CPU如何知道栈顶的位置?
解:任意时刻,SS:SP指向栈顶元素,8086CPU中有2个寄存器,段寄存器SS和寄存器SP

push ax的执行,由以下两步完成
(1)SP = SP - 2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
(2)将ax中的内容送入SS:SP指向的内存单元处,SS:SP此时指向新栈顶


入栈时,栈顶从高地址向低地址方向增长


任意时刻SS:SP指向栈顶元素,当栈为空的时候,栈中没有元素,就没有栈顶元素,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的偏移地址为栈最底部的子单元的偏移地址+2。栈最底部字单元的地址为1000:000E,所以栈空时,SP = 0010H

** pop ax 的执行过程和 push ax 刚好相反,由以下两步完成**
(1)将SS:SP指向的内存单元处的数据送入ax中
(2)SP = SP + 2,SS:SP指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶


出栈后,SS:SP 指向新的栈顶1000EH,pop操作前的栈顶元素。1000CH处的2266H依然存在,但是它已不在栈中。当再次执行push等入栈指令后,SS:SP 移至1000CH,并在里面写入新的数据,它将被覆盖

3.8 栈顶超界问题

当栈满时再使用push指令入栈,或栈空的时候再使用pop指令出栈,都将发生栈顶超界的问题。所以在编程的时候要自己操心栈顶超界的问题

3.9 push、pop指令

push 和 pop 指令的格式可以是如下形式:
push 寄存器/段寄存器; //将一个寄存器/段寄存器中的数据入栈
pop 寄存器/段寄存器; //出栈,用一个寄存器/段寄存器接收出栈的数据

也可以是如下形式:
push 内存单元; //将一个内存单元处的字入栈(注意:栈操作都是以字为单位)
pop 内存单元; //出栈,用一个内存字单元接收出栈的数据

比如:
mov ax,1000H
mov ds,ax; //内存单元的短地址要放在ds中
push [0]; //将1000:0处的字压入栈中
pop [2]; //出栈,出栈的数据送入1000:2处

指令执行前,CPU要知道内存单元的地址,可以在push、pop指令中只给出内存单元的偏移地址,段地址在指令执行时,CPU从ds中取得

问题3.7
编程将10000H~1000FH这段空间当作栈,初始状态栈是空的,将AX、BX、DS中的数据入栈

分析:
mov ax,1000H //
mov ss,ax //设置栈的段地址 SS = 1000H,不能直接向段寄存器SS中送入数据,所以用ax中转
mov sp,0010H //设置栈顶的偏移地址,因栈为空,所以SP = 0010H,如果对栈为空时SP的设置还有疑问,需要复习3.7节、问题3.6
上面的3条指令设置栈顶地址,编程中要自己注意栈的大小
push ax
push bx
push ds

问题3.8
编程:
(1)将10000H~1000FH 这段空间当作栈,初始状态栈是空的
(2)设置AX = 001AH,BX = 001BH
(3)将AX、BX中的数据入栈
(4)将AX、BX清零
(5)从栈中恢复AX、BX原来的内容

分析:
设置好栈顶位置->
mov ax,1000H
mov ss,ax
mov sp,0010H

mov ax,001AH
mov bx,001BH

push ax
push bx //ax,bx入栈

sub ax,ax //将ax清零,也可以用mov ax,0
//sub ax,ax的机器码为2个字节
//mov ax,0的机器码为3个字节

sub bx,bx

pop bx
pop ax //从栈中恢复ax、bx原来的数据,当前栈顶的内容是bx中原来的内容:001BH,
//ax中原来的内容001AH在栈顶的下面,所以要先pop bx,然后再pop ax


用栈暂存以后需要恢复的寄存器中的内容时,出栈的顺序要和入栈的顺序相反<后进先出>

问题3.9
编程:
(1)将10000H~1000FH 这段空间当作栈,初始状态栈是空的 <栈是空的:1000F + 1 = 10010>
(2)设置AX = 001AH,BX = 001BH
(3)利用栈,交换AX和BX中的数据

分析:先初始化栈顶
mov ax,1000H
mov ss,ax
mov sp,0010H

mov ax,001AH
mov bx,001BH

push ax
push bx //ax、bx入栈

pop ax //当前栈顶的数据是bx中原来的数据(因为bx后入栈):001BH,所以先pop ax, ax = 001BH
pop bx //执行pop ax后,栈顶的数据为ax原来的数据,所以再pop bx,bx = 001AH

问题3.10
如果要在10000H处写入字型数据2266H,可以用以下的代码完成
mov ax,1000H
mov ds,ax
mov ax,2266H
mov [0],ax

补全下面的代码,使它能够完成同样的功能:在10000H处写入字型数据2266H。要求:不能使用"mov 内存单元,寄存器"这类指令

mov ax,1000H
mov ss,ax
mov sp,2
mov ax,2266H
push ax

解析:题目最后两行,将ax中的2266H压入栈中,就是说最终应该是push ax将2266H写入10000H处,问题是:如何使push ax访问的内存单元是10000H
push ax是入栈指令,它将在栈顶之上压入信的数据。它的执行过程是,先将记录栈顶偏移地址的SP寄存器中的内容-2,使得SS:SP指向新的栈顶单元,然后再将寄存器中的数据送入SS:SP指向的新的栈顶单元

所以要在执行push ax之前,将SS:SP指向10002H,这样在执行push ax的时候,CPU先将SP = SP - 2,使得SS:SP指向10000H,再将ax中的数据送入SS:SP指向的内存单元处(即10000H处)

push、pop实质上就是一种内存传送指令,可以在寄存器和内存之间传送数据,与mov指令不同的是,push和pop指令访问的内蕴单元的地址不是在指令中给出的,而是由SS:SP指出的。同时push和pop指令还要改变SP中的内容

push和pop指令同mov指令不同,CPU执行mov指令只需一步操作,就是传送,而执行push、pop指令却需要两步操作。
执行push时,CPU的两步操作是:先改变SP,后向SS:SP处传送。
执行pop时,CPU的两步操作是:先读取SS:SP处的数据,后改变SP

push、pop等栈操作指令,修改的只是SP,所以栈顶的变化范围最大是0~FFFFH

**8086CPU提供了栈操作机制,方案如下:
(1)在SS、SP中存放栈顶的段地址和偏移地址,提供入栈和出栈指令,他们根据SS:SP指示的地址,按照栈的方式访问内存单元
(2)push指令的执行步骤:
1.SP = SP - 2
2.向SS:SP指向的字单元中送入数据
(3)pop指令的执行步骤:
1.从SS:SP指向的字单元中读取数据
2.SP = SP + 2
(4)任意时刻,SS:SP指向栈顶元素
(5)8086只记录栈顶,栈空间的大小我们要自己管理
(6)用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈的顺序相反
(7)push、pop实质上是一种内存传送指令,注意它们的灵活运用

栈是一种非常重要的机制,一定要深入理解,灵活掌握

3.10 栈段

我们可以将长度为N(N≤64KB)的一组地址里连续、起始地址为16的倍数的内存单元,当作栈空间来用,从而定义了一个栈段
比如我们将10010H~1001FH这段长度为16字节的内存空间当作栈来用,以栈的方式进行访问。这段空间就可以称为一个栈段,段地址为1001H,大小为16字节

问题3.11
如果将10000H~1FFFFH这段空间当作栈段,初始状态栈是空的,此时SS = 1000H,SP = ?
解析:因为SS:SP指向栈顶元素,栈又是空的,所以SS:SP只能指向栈的最底部单元下面的单元,该单元的地址为栈最底部的字单元的地址+2。栈最底部字单元的地址为1000:FFFE,所以栈空时,SP = 0000H

栈顶的变化范围是0~FFFFH,从栈空的时候SP = 0,一直压栈,直到栈满时 SP = 0;如果再次压栈,栈顶将环绕,覆盖了原来栈中的内容。

段的综述

  • 对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当作数据来访问
  • 对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令
  • 对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中,这样CPU在需要进行栈操作的时候,比如执行push、pop指令等,就将我们定义的栈段当作栈空间来用

可见,不管我们如何安排,CPU将内存中的某段内容当作代码,是因CS:IP指向了那里;CPU将某段内存当作栈,是因为SS:SP指向了那里。我们一定要清楚CPU的工作机理,才能控制CPU按照我们的安排运行

实验2 用机器指令和汇编指令编程
Debug的T命令在执行修改寄存器SS的指令时,下一条指令也紧接着被执行

第四章 第一个程序

现在我们将开始编写完整的汇编语言程序,用编译和连接程序将他们编译连接成为可执行文件(如*.exe文件),在操作系统中运行

4.1 一个源程序从写出到执行的过程

1.编写汇编源程序
2.对源程序进行编译连接
3.执行可执行文件中的程序

4.2 源程序

汇编语言源程序中,包含两种指令:汇编指令和伪指令
汇编指令是有对应的机器码的指令,可以被编译为机器指令,可以被编译为机器指令,最终为CPU所执行
伪指令没有对应的机器指令,最终不被CPU所执行。伪指令是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作

(1)XXX segment
segment和ends是一对成对使用的伪指令,这是在写可被编译器编译的汇编程序时,必须要用到的一对伪指令。
功能是定义一个段,segment说明一个段开始,ends说明一个段结束。一个段必须有一个名称来标识
格式:
段名 segment
...
...
段名 ends

(2)end
end是一个汇编程序的结束标记。编译器遇到end就结束对源程序的编译

(3)assume(含义为“假设”)
在需要的情况下,编译程序可以将段寄存器和某一个具体的段相联系
比如用assume cs:codesg将用作代码段的段codesg和CPU中的段寄存器cs联系起来

编程运算\(2^3\),用源程序写:
(1)定义一个段,名称为abc
(2)在这个段中写入汇编指令,来实现我们的任务
(3)指出程序在何处结束
(4)abc被当作代码来用,所以应该将abc和cs联系起来(也不是非做不可)

最终程序:
assume cs:abc
abc segment

mov ax,2
add ax,ax
add ax,ax

abc ends

end

程序返回
一个程序结束后,将CPU的控制权交还给使他得以运行的程序
如何返回?要在程序的末尾添加返回的程序段:
mov ax,4c00H
int 21H
这两条指令所实现的功能就是程序返回

语法错误和逻辑错误
程序在编译时被编译器发现的错误是语法错误
在源程序编译后,在运行时发生的错误是逻辑错误

4.3 编译源程序

可以用任意的文本编辑器来编辑源程序,只要将其存储为纯文本文件即可

目标文件(.obj)是我们最终要得到的结果

4.5 连接

连接的作用有以下几个:
1.当源程序很大时,可以将它分为多个源程序文件来编译,每个源程序编译成为目标文件后,再用连接程序将他们连接到一起,生成一个可执行文件
2.程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件
3.一个源程序编译后,得到了存有机器码的目标文件,目标文件中的有些内容还不能直接用来生成可执行文件,连接程序将这些内容处理为最终的可执行信息。所以只要有一个源程序文件,而又不需要调用某个库中的子程序的情况下,也必须用连接程序对目标文件进行处理,生成可执行文件

操作系统的外壳
任何通用的操作系统,都要提供一个称为shell(外壳)的程序,用户使用这个程序来操作计算机系统进行工作
DOS中有一个程序command.com,这个程序在DOS中称为命令解释器,就是DOS系统的shell

问题4.1
此时有一个正在运行的程序将1.exe中的程序加载入内存,这个正在运行的程序是什么?它将程序加载入内存后,如何使程序得以运行?
程序运行结束后,返回到哪里?
解:
(1)在DOS中直接执行1.exe时,是正在运行的command,将1.exe中的程序加载入内存
(2)command设置CPU的CS:IP指向程序的第一条指令(即程序的入口),从而使程序得以运行
(3)程序运行结束后返回到command中,CPU继续运行command

DOS系统中.EXE文件中的程序的加载过程

推荐阅读