菜单 English Ukrainian 俄语 主页

面向爱好者和专业人士的免费技术图书馆 免费技术库


讲义、备忘单
免费图书馆 / 目录 / 讲义、备忘单

信息学和信息技术。 讲义:简而言之,最重要的

讲义、备忘单

目录 / 讲义、备忘单

文章评论 文章评论

目录

  1. 计算机科学导论 (信息学。信息。信息的表示和处理。数字系统。计算机中数字的表示。算法的形式化概念)
  2. 帕斯卡语言 (Pascal 简介。标准过程和函数。Pascal 运算符)
  3. 程序和功能 (辅助算法的概念。Pascal 中的过程。Pascal 中的函数。子程序的预期描述和连接。指令)
  4. 子程序 (例程参数。子例程参数类型。Pascal 中的字符串类型。字符串类型变量的过程和函数。记录。集合)
  5. 商品资料 (文件。文件操作。模块。模块类型)
  6. 动态内存 (引用数据类型。动态内存。动态变量。使用动态内存。无类型指针)
  7. 抽象数据结构 (抽象数据结构。堆栈。队列)
  8. 树数据结构 (树数据结构。树上的操作。操作实现示例)
  9. 计数 (图的概念。表示图的方法。通过关联列表表示图。图的深度优先遍历算法。将图表示为列表的列表。图的广度优先遍历算法)
  10. 对象数据类型 (Pascal 中的对象类型。对象的概念、其描述和使用。继承。创建对象实例。组件和范围)
  11. 方法 (方法。构造函数和析构函数。析构函数。虚拟方法。对象数据字段和形式方法参数)
  12. 对象类型兼容性 (封装。可扩展对象。对象类型兼容性)
  13. 汇编器 (关于汇编器。微处理器软件模型。用户寄存器。通用寄存器。段寄存器。状态和控制寄存器)
  14. 寄存器 (微处理器系统寄存器。控制寄存器。系统地址寄存器。调试寄存器)
  15. 装配程序 (汇编器程序结构。汇编器语法。比较运算符。运算符及其优先级。简化的段定义指令。由 MODEL 指令创建的标识符。内存模型。内存模型修饰符)
  16. 装配指令结构 (机器指令的结构。指定指令操作数的方法。寻址方法)
  17. Команды (数据传输命令。算术命令)
  18. 控制传输命令 (逻辑命令。逻辑非的真值表。逻辑包含 OR 的真值表。逻辑 AND 的真值表。逻辑异或的真值表。jcc 命令名称中缩写的含义。该命令的条件跳转命令列表。条件跳转命令和标志)

LECTURE No. 1. 计算机科学概论

1. 计算机科学。 信息。 信息的表示和处理

信息学致力于在科学、技术和生产的各个领域对对象及其关系结构进行形式化表示。 各种形式化工具用于对对象和现象进行建模,例如逻辑公式、数据结构、编程语言等。

在计算机科学中,信息等基本概念具有多种含义:

1) 外部形式信息的正式呈现;

2) 信息的抽象意义、其内在内容、语义;

3)信息与现实世界的关系。

但是,作为一项规则,信息被理解为它的抽象意义——语义。 通过解释信息的表示,我们得到它的意义、语义。 因此,如果我们要交换信息,我们需要一致的观点,这样才能不违反解释的正确性。 为此,信息表示的解释用一些数学结构来识别。 在这种情况下,可以通过严格的数学方法进行信息处理。

信息的数学描述之一是以函数 y =f(x, t) 的形式表示,其中 t 是时间,x 是在某个场中测量 y 值的点。根据chi函数的参数(信息可以进行分类。

如果参数是具有一系列连续值的标量,则以这种方式获得的信息称为连续(或模拟)。 如果给定参数一定的变化步长,则该信息称为离散的。 离散信息被认为是通用的,因为对于每个特定参数,都可以获得具有给定准确度的函数值。

离散信息通常用数字信息来标识,数字信息是字母表示的符号信息的一种特殊情况。 字母表是任何性质的有限符号集。 在计算机科学中,经常会出现一种情况,即一个字母表的字符必须由另一个字母表的字符表示,即执行编码操作。 如果编码字母的字符数小于编码字母的字符数,那么编码操作本身并不复杂,否则就需要使用编码字母的固定字符集进行无歧义正确编码。

正如实践所示,允许您编码其他字母表的最简单字母表是二进制的,由两个字符组成,通常用 0 和 1 表示。使用二进制字母表的 n 个字符,您可以编码 2n 个字符,这就足够了编码任何字母表。

二进制字母表中一个符号所能表示的值称为信息或比特的最小单位。 8 位序列 - 字节。 包含 256 个不同的 8 位序列的字母表称为字节字母表。

作为当今计算机科学的标准,采用了一种代码,其中每个字符由 1 个字节编码。 还有其他字母表。

2.数字系统

数字系统是一组命名和书写数字的规则。 有位置和非位置数字系统。

如果数字的数字值取决于数字在数字中的位置,则称为位置数字系统。 否则,它被称为非位置。 数字的值由这些数字在数字中的位置决定。

3. 计算机中的数字表示

32 位处理器最多可以使用 232-1 个 RAM,并且可以在 00000000 - FFFFFFFF 范围内写入地址。 但是,在实模式下,处理器使用高达 220-1 的内存运行,地址范围为 00000 - FFFFF。 内存字节可以组合成固定长度和可变长度的字段。 一个字是一个由 2 个字节组成的固定长度字段,一个双字是一个 4 个字节的字段。 字段地址可以是偶数或奇数,偶数地址执行操作更快。

定点数在计算机中表示为整数二进制数,它们的大小可以是 1、2 或 4 个字节。

二进制整数以二进制补码表示,定点数以二进制补码表示。此外,如果一个数字占用 2 个字节,则该数字的结构按照以下规则写入:最高有效位分配给该数字的符号,其余的分配给该数字的二进制数字。正数的补码等于该数本身,负数的补码可以通过以下公式获得:x = 10i - \x\,其中n为该数的位数。

在二进制数字系统中,通过反转位获得附加代码,即用零替换单元,反之亦然,并将最低有效位加一。

尾数的位数决定了数字表示的精度,机器序位数决定了浮点数的表示范围。

4. 算法的形式化概念

只有同时存在某个数学对象时,算法才能存在。 算法的形式化概念与递归函数、正常马尔可夫算法、图灵机的概念有关。

在数学中,如果对于任何一组参数,都有一个定律可以确定函数的唯一值,则该函数称为单值函数。 算法可以充当这样的法则; 在这种情况下,该函数被称为可计算的。

递归函数是可计算函数的子类,定义计算的算法称为伴随递归函数算法。 首先,基本的递归函数是固定的,伴随的算法是平凡的、明确的; 然后引入了三个规则——替换、递归和最小化算子,借助它们在基本函数的基础上得到更复杂的递归函数。

基本功能及其附带的算法可以是:

1) n 个自变量的函数,均等于 XNUMX。 那么,如果函数的符号是​​φn,那么不管参数的数量如何,函数的值都应该设置为零;

2) n 个形式为ψni 的自变量的恒等函数。 那么,如果函数的符号是​​ψni,那么函数的值应该作为第i个参数的值,从左到右计数;

3) Λ 是一个独立参数的函数。 那么,如果函数的符号是​​λ,那么函数的值应该取为参数值后面的值。 不同的学者已经提出了他们自己的形式化方法

算法的表示。 例如,美国科学家 Church 提出,可计算函数类已被递归函数耗尽,因此,无论将一组非负整数处理成另一组非负整数的算法是什么,都有一个伴随递归函数的算法是相当于给定的。 因此,如果不可能构造一个递归函数来解决给定的问题,那么就没有解决它的算法。 另一位科学家图灵开发了一种虚拟计算机,可以将输入的字符序列处理成输出。 对此,他提出了任何可计算函数都是图灵可计算的论点。

LECTURE No. 2. 帕斯卡语言

1. Pascal语言介绍

语言的基本符号——字母、数字和特殊字符——构成了它的字母表。 Pascal 语言包括以下一组基本符号:

1) 26 个拉丁小写字母和 26 个拉丁大写字母:

ABCDEFGHIJKLMNOPQRSTU VWXYZ

abcdefghijklmnopqrstuvwxyz;

2) _(下划线);

3)10位:0123456789;

4) 操作迹象:

+ - x / = <> < > <= >= := @;

5) 限制器:

., ' ( ) [ ] (..) { } (* *).. : ;

6) 说明符:^#$;

7)服务(保留)词:

ABSOLUTE,汇编器,和,数组,ASM,BEGIN,CASE,const,构造器,析构器,DIV,DO,DOWNTO,ELSE,END,EXPORT,外部,FAR,文件,FOR,FORWARD,FUNCTION,GOTO,IF,实现, IN、索引、继承、内联、接口、中断、标签、库、MOD、名称、NIL、NEAR、NOT、OBJECT、OF、OR、PACKED、私有、过程、程序、公共、记录、重复、居民、设置、 SHL、SHR、字符串、然后、到、类型、单位、直到、使用、VAR、虚拟、同时、与、异或。

除了列出的那些之外,基本字符集还包括一个空格。 双字符和保留字内不能使用空格。

数据的类型概念

在数学中,习惯上根据一些重要特征对变量进行分类。 对真实变量、复杂变量和逻辑变量,代表单个值的变量和一组值等进行严格区分。在计算机上处​​理数据时,这样的分类更为重要。 在任何算法语言中,每个常量、变量、表达式或函数都属于特定类型。

Pascal 中有一条规则:类型在使用之前的变量或函数的声明中明确指定。 Pascal 类型概念具有以下主要属性:

1)任何数据类型定义了一个常量所属的一组值,一个变量或表达式可以取,或者一个操作或函数可以产生;

2) 常量、变量或表达式给出的值的类型可以通过它们的形式或描述来确定;

3) 每个操作或函数都需要固定类型的参数并产生固定类型的结果。

因此,编译器可以使用类型信息来检查各种构造的可计算性和正确性。

类型定义:

1)属于给定类型的变量、常量、函数、表达式的可能值;

2)计算机中数据呈现的内部形式;

3)可以对属于给定类型的值执行的操作和功能。

需要注意的是,类型的强制描述会导致程序文本出现冗余,但这种冗余是开发程序的重要辅助工具,被认为是现代高级算法语言的必要属性。

Pascal 中有标量和结构化数据类型。 标量类型包括标准类型和用户定义类型。 标准类型包括整数、实数、字符、布尔值和地址类型。

整数类型定义常量、变量和函数,其值由给定计算机中允许的整数集实现。

实数类型定义了由给定计算机中允许的实数子集实现的那些数据。

用户定义的类型是枚举和范围。 结构化类型有四种类型:数组、集合、记录和文件。

除了列出的那些之外,Pascal 还包括另外两种类型——过程和对象。

语言表达式由常量、变量、函数指针、运算符符号和括号组成。 表达式定义了计算某个值的规则。 计算顺序由其中包含的操作的优先级(优先级)决定。 Pascal 具有以下运算符优先级:

1) 括号内的计算;

2)函数值的计算;

3) 一元运算;

4) 操作 *、/、div、mod 和;

5) 运算+、-、或、异或;

6)关系运算=、<>、<、>、<=、>=。

表达式是许多 Pascal 语言运算符的一部分,也可以是内置函数的参数。

2. 标准程序和功能

算术函数

1. 函数 Abs(X);

返回参数的绝对值。

X 是实数或整数类型的表达式。

2.函数ArcTan(X: Extended):扩展;

返回参数的反正切。

X 是实数或整数类型的表达式。

3.函数exp(X:Real):Real;

返回指数。

X 是实数或整数类型的表达式。

4.Frac(X: Real):实数;

返回参数的小数部分。

X 是一个实型表达式。 结果是 X 的小数部分,即

Frac(X) = X-Int(X)。

5.函数Int(X:Real):Real;

返回参数的整数部分。

X 是一个实型表达式。 结果是 X 的整数部分,即 X 向零舍入。

6.函数Ln(X:Real):Real;

返回实型表达式 X 的自然对数 (Ln e = 1)。

7.功能Pi:扩展;

返回 Pi 值,定义为 3.1415926535。

8.Function Sin(X: Extended):扩展;

返回参数的正弦值。

X 是一个实型表达式。 Sin 以弧度返回角度 X 的正弦值。

9.Function Sqr(X: Extended):扩展;

返回参数的平方。

X 是一个浮点表达式。 结果与 X 的类型相同。

10.Function Sqrt(X: Extended):扩展;

返回参数的平方根。

X 是一个浮点表达式。 结果是 X 的平方根。

值转换过程和函数

1. 程序 Str(X [: Width [: Decimals]]; var S);

根据以下将数字 X 转换为字符串表示形式

宽度和小数格式选项。 X 是实数或整数类型的表达式。 宽度和小数是整数类型的表达式。 如果允许扩展语法,S 是 String 类型的变量或以 null 结尾的字符数组。

2.函数Chr(X:Byte):Char;

返回 ASCII 表中序号为 X 的字符。

3.功能高(X);

返回参数范围内的最大值。

4.功能低(X);

返回参数范围内的最小值。

5 FunctionOrd(X): Longint;

返回枚举类型表达式的序数值。 X 是枚举类型表达式。

6. Function Round(X: Extended): Longint;

将实数值舍入为整数。 X 是一个实数类型表达式。 Round 返回一个 Longint 值,它是将 X 的值四舍五入到最接近的整数。 如果 X 恰好在两个整数之间,则返回具有最大绝对值的数字。 如果 X 的舍入值超出 Longint 范围,则会生成一个运行时错误,您可以使用 EInvalidOp 异常处理该错误。

7.函数Trunc(X:Extended):Longint;

将实类型值截断为整数。 如果 X 的舍入值超出 Longint 范围,则会生成一个运行时错误,您可以使用 EInvalidOp 异常处理该错误。

8. 过程 Val(S; var V; var Code: Integer);

将数字从字符串值 S 转换为数字

表示 V. S - 字符串类型表达式 - 形成整数或实数的字符序列。 如果 S 表达式无效,则将无效字符的索引存储在 Code 变量中。 否则代码设置为零。

序数值过程和函数

1. 过程 Dec(varX [; N: LongInt]);

从变量 X 中减去 1 或 N。Dec(X) 对应于 X:= X - XNUMX,Dec(X, N) 对应于 X:= X - N。X 是枚举类型或类型的变量如果允许扩展语法,则为 PChar,并且 N 是整数类型的表达式。 Dec 过程生成最佳代码,在长循环中特别有用。

2. 程序 Inc(varX [; N: LongInt]);

将变量 X 加一或 N。如果允许扩展语法,则 X 是枚举类型或 PChar 类型的变量,N 是整数类型的表达式。 Inc (X) 与指令 X:= X + 1 匹配,Inc (X, N) 与指令 X:= X + N 匹配。Inc 过程生成最佳代码,在长循环中特别有用。

3. FunctionOdd(X: LongInt): 布尔值;

如果 X 是奇数则返回 True,否则返回 False。

4.FunctionPred(X);

返回参数的前一个值。 X 是枚举类型表达式。 结果是同一类型。

5 函数 Succ(X);

返回下一个参数值。 X 是枚举类型表达式。 结果是同一类型。

3. Pascal 语言运算符

条件运算符

完整条件语句的格式定义如下: If B then SI else S2; 其中B是分支条件(决策)、逻辑表达式或关系; SI, S2 - 一个可执行语句,简单或复合。

执行条件语句时,首先对表达式 B 求值,然后分析其结果:如果 B 为真,则执行语句 S1 - then 的分支,跳过语句 S2; 如果 B 为假,则语句 S2 - 执行 else 分支,并跳过语句 S1。

条件运算符还有一种缩写形式。 写成:如果 B 则 S。

选择语句

运算符结构如下:

案例 S

c1:指令1;

c2:指令2;

...

cn:指令N;

否则指令

结束;

其中 S 是一个序数类型表达式,其值正在被计算;

с1、с2...、сп - 与表达式进行比较的序数类型常量

S; instructions1,...,instructionN - 执行常量与表达式 S 的值匹配的运算符;

指令 - 如果 Sylq 表达式的值与常量 c1、c2.... cn 都不匹配时执行的语句。

该运算符是条件 If 运算符的泛化,适用于任意数量的备选方案。 没有 else 分支的语句有一个缩写形式。

带参数的循环语句

以单词 for 开头的参数循环语句导致语句(可以是复合语句)在控制变量被分配升序值时重复执行。

for 运算符的一般视图:

for <loop counter> := <start value> to <end value> do <statement>;

当 for 语句开始执行时,start 和 end 值被确定一次,并且这些值在 for 语句的整个执行过程中都被保留。 for 语句主体中包含的语句对起始值和结束值之间的每个值执行一次。 循环计数器始终初始化为初始值。 当 for 语句运行时,循环计数器的值随着每次迭代而递增。 如果起始值大于结束值,则不执行包含在 for 语句体中的语句。 在循环语句中使用 downto 关键字时,控制变量的值在每次迭代时减一。 如果此类语句中的起始值小于结束值,则不执行循环语句主体中包含的语句。

如果包含在 for 语句体中的语句改变了循环计数器的值,那么这是一个错误。 在 for 语句执行后,控制变量的值变为未定义,除非 for 语句的执行被跳转语句中断。

带前置条件的循环语句

前置条件循环语句(以 while 关键字开头)包含一个控制语句重复执行的表达式(可以是复合语句)。 循环形状:

而B做S;

其中 B 是一个逻辑条件,它的真实性被检查(它是一个终止循环的条件);

S - 循环体 - 一个语句。

控制语句重复的表达式必须是布尔类型。 在执行内部语句之前对其进行评估。 只要表达式的计算结果为 True,内部语句就会重复执行。 如果表达式从一开始就计算为 False,则不执行包含在前置条件循环语句中的语句。

带有后置条件的循环语句

在带有后置条件(以单词 repeat 开头)的循环语句中,控制语句序列重复执行的表达式包含在 repeat 语句中。 循环形状:

重复 S 直到 B;

其中 B 是一个逻辑条件,它的真实性被检查(它是一个终止循环的条件);

S - 一个或多个循环体语句。

表达式的结果必须是布尔类型。 包含在 repeat 和 until 关键字之间的语句按顺序执行,直到表达式的结果为 True。 语句序列将至少执行一次,因为每次执行语句序列后都会计算表达式。

第 3 讲。程序和功能

1.辅助算法的概念

问题求解算法是通过将整个问题分解为单独的子任务来设计的。 通常,子任务被实现为子例程。

子程序是一些辅助算法,在主算法中重复使用,某些传入量的值不同,称为参数。

编程语言中的子例程是一系列语句,仅在程序中的一个地方定义和编写,但可以从程序中的一个或多个点调用执行。 每个子例程都由唯一的名称标识。

Pascal 中有两种类型的子程序,过程和函数。 过程和函数是声明和语句的命名序列。 使用过程或函数时,程序必须包含过程或函数的文本以及对过程或函数的调用。 描述中指定的参数称为形式参数,调用子程序中指定的参数称为实际参数。 所有形式参数都可以分为以下几类:

1) 参数-变量;

2)常数参数;

3) 参数值;

4)过程参数和函数参数,即过程类型参数;

5) 无类型变量参数。

过程和函数的文本放在过程和函数的描述部分。

将过程和函数名称作为参数传递

在许多问题中,特别是在计算数学中,需要将过程和函数的名称作为参数传递。 为此,TURBO PASCAL 引入了一种新的数据类型 - 过程或函数,具体取决于所描述的内容。 (过程和函数类型在类型声明部分中描述。)

函数和过程类型定义为过程的标题和具有形式参数列表但没有名称的函数。 可以定义不带参数的函数或过程类型,例如:

类型

过程 = 过程;

在声明了过程或函数类型之后,它可以用来描述形式参数——过程和函数的名称。 此外,有必要编写那些名称将作为实际参数传递的真实过程或函数。

2. Pascal 中的过程

每个过程描述都包含一个标题,后跟一个程序块。 过程头的一般形式如下:

过程 <名称> [(<形式参数列表>)];

使用包含过程名称和所需参数的过程语句激活过程。 运行过程时要执行的语句包含在过程模块的语句部分中。 如果过程中包含的语句使用过程模块内部的过程标识符,则该过程将递归执行,即在执行时将引用自身。

3. Pascal 中的函数

函数声明定义了计算和返回值的程序部分。 函数头的一般形式如下:

函数 <名称> [(<形式参数列表>)]: <返回类型>;

该函数在调用时被激活。 调用函数时,会指定函数标识符和对其求值所需的任何参数。 函数调用可以作为操作数包含在表达式中。 当表达式被计算时,函数被执行并且操作数的值成为函数返回的值。

功能块的操作符部分指定激活功能时必须执行的语句。 一个模块必须至少包含一个赋值语句,为函数标识符赋值。 该函数的结果是分配的最后一个值。 如果没有这样的赋值语句,或者没有执行,那么函数的返回值是未定义的。

如果在调用模块内的函数时使用了函数标识符,则函数将递归执行。

4.子程序的前向描述和连接。 指示

一个程序可能包含多个子程序,即程序的结构可能很复杂。 但是,这些子例程可以处于同一嵌套级别,因此必须先声明子例程,然后再调用它,除非使用了特殊的前向声明。

包含前向指令而不是语句块的过程声明称为前向声明。 在此声明之后的某处,必须通过定义声明来定义过程。 定义声明是使用相同过程标识符但省略形式参数列表并包含语句块的声明。 前向声明和定义声明必须出现在过程和函数声明的同一部分。 在它们之间,可以声明可以引用前向声明过程的其他过程和函数。 因此,相互递归是可能的。

前向描述和定义描述是过程的完整描述。 该过程被认为是使用前向描述来描述的。

如果程序包含相当多的子程序,那么程序将不再是可视化的,将难以在其中导航。 为了避免这种情况,一些例程作为源文件存储在磁盘上,如有必要,它们在编译阶段使用编译指令连接到主程序。

指令是一种特殊的注释,可以放置在程序中的任何地方,普通注释可以放在任何地方。 但是,它们的不同之处在于指令有一个特殊的符号:紧跟在没有空格的右括号之后,写入符号 S,然后,再次没有空格,指示指令。

例子

1) {SE+} - 模拟数学协处理器;

2) {SF+} - 形成远程类型的过程和函数调用;

3) {SN+} - 使用数学协处理器;

4) {SR+} - 检查范围是否超出范围。

一些编译开关可能包含一个参数,例如:

{$1 文件名} - 在已编译程序的文本中包含命名文件。

LECTURE No. 4. 子程序

1.子程序参数

过程或函数的描述指定了形式参数的列表。 在形式参数列表中声明的每个参数对于所描述的过程或函数都是局部的,并且可以在与该过程或函数相关联的模块中通过其标识符来引用。

参数分为三种类型:值、变量和无类型变量。 它们的特征如下。

1. 前面没有关键字的一组参数是一个值参数列表。

2. 前面有const关键字,后面跟一个类型的一组参数是一个常量参数列表。

3. 前面有 var 关键字,后面跟一个类型的一组参数是一个无类型变量参数的列表。

4. 以 var 或 const 关键字开头但不跟类型的一组参数是无类型变量参数的列表。

2. 子程序参数类型

值参数

形式值参数被视为过程或函数的局部变量,除非它在调用过程或函数时从相应的实际参数派生其初始值。 形式值参数所经历的更改不会影响实际参数的值。 value参数对应的实际值必须是表达式,其值不能是文件类型或任何包含文件类型的结构类型。

实际参数的类型必须与形式值参数的类型兼容。 如果参数是字符串类型,那么形参的大小属性将为 255。

常量参数

形式常量参数的工作方式类似于只读局部变量,该变量在从相应的实际参数调用过程或函数时获取其值。 不允许对形式常量参数赋值。 形式常量参数也不能作为实际参数传递给另一个过程或函数。 与过程或函数语句中的实际参数对应的常量参数必须遵循与实际参数值相同的规则。

如果形式参数在过程或函数执行期间不改变其值,则应使用常量参数而不是值参数。 常量参数允许执行过程或函数以防止意外分配给形式参数。 此外,对于结构和字符串类型参数,编译器在使用常量参数代替值参数时可以生成更高效的代码。

可变参数

当必须将值从过程或函数传递给调用程序时,使用可变参数。 过程或函数调用语句中对应的实参必须是变量引用。 当调用过程或函数时,形参变量被实际变量替换,形参变量值的任何变化都会反映在实参中。

在过程或函数中,对形式变量参数的任何引用都会导致访问实际参数本身。 实参的类型必须与形参的类型相匹配,但这个限制可以通过使用无类型的可变参数来规避)。

无类型参数

当形参是无类型变量参数时,对应的实参可以是对变量或常量的任何引用,无论其类型如何。 使用 var 关键字声明的无类型参数可以修改,而使用 const 关键字声明的无类型参数是只读的。

在过程或函数中,无类型变量参数没有类型,即它与所有类型的变量不兼容,直到通过变量类型赋值赋予特定类型。

尽管无类型参数提供了更大的灵活性,但使用它们存在一些风险。 编译器无法检查对无类型变量的操作的有效性。

程序变量

在定义了过程类型之后,就可以描述这种类型的变量了。 这样的变量称为过程变量。 与可以分配整数类型值的整数变量一样,可以为过程变量分配过程类型值。 当然,这样的值可以是另一个过程变量,但它也可以是过程或函数标识符。 在这种情况下,过程或函数的声明可以看作是对一种特殊类型的常量的描述,其值为过程或函数。

与任何其他赋值一样,左侧和右侧变量的值必须是赋值兼容的。 过程类型,为了赋值兼容,必须有相同数量的参数,并且对应位置的参数必须是相同的类型。 过程类型声明中的参数名称无效。

此外,为了确保赋值兼容性,如果要将过程或函数赋值给过程变量,则必须满足以下要求:

1) 它不应该是标准程序或功能;

2) 这样的过程或函数不能嵌套;

3) 这样的程序不能是内联程序;

4) 它不能是一个中断程序。

标准过程和函数是 System 模块中描述的过程和函数,例如 Writeln、Readln、Chr、Ord。 不能使用带有过程变量的嵌套过程和函数。 当一个过程或函数在另一个过程或函数中声明时,它被认为是嵌套的。

过程类型的使用不仅限于过程变量。 与任何其他类型一样,过程类型可以参与结构类型的声明。

当一个过程变量被赋予一个过程的值时,在物理层发生的事情是过程的地址被存储在变量中。 实际上,过程变量与指针变量非常相似,只是它不是指数据,而是指向过程或函数。 与指针一样,过程变量占用 4 个字节(两个字),其中包含一个内存地址。 第一个字存储偏移量,第二个字存储段。

程序类型参数

由于过程类型可以在任何上下文中使用,因此可以描述将过程和函数作为参数的过程或函数。 当您需要对多个过程或函数执行常见操作时,过程类型参数特别有用。

如果要将过程或函数作为参数传递,则它必须遵循与赋值相同的类型兼容性规则。 也就是说,这样的过程或函数必须用 far 指令编译,它们不能是内置函数,不能嵌套,不能用 inline 或 interrupt 属性描述。

LECTURE #5. 字符串数据类型

1. Pascal 中的字符串类型

一定长度的字符序列称为字符串。 字符串类型的变量通过指定变量名称、保留字字符串以及可选但不一定在方括号中指定最大大小(即字符串的长度)来定义。 如果您没有设置最大字符串大小,那么默认情况下它将是 255,即字符串将由 255 个字符组成。

字符串的每个元素都可以通过其编号来引用。 但是,字符串是作为一个整体输入和输出的,而不是像数组那样逐个元素地输入和输出。 输入的字符数不得超过最大字符串大小中指定的字符数,因此如果发生这种超出,则“多余”字符将被忽略。

2.字符串类型变量的过程和函数

1. Function Copy(S: String; Index, Count: Integer): String;

返回字符串的子字符串。 S 是字符串类型的表达式。

Index 和 Count 是整数类型的表达式。 该函数返回一个字符串,其中包含从索引位置开始的 Count 个字符。 如果 Index 大于 S 的长度,则该函数返回一个空字符串。

2.过程Delete(var S: String; Index, Count: Integer);

从字符串 S 中删除长度为 Count 的字符的子字符串,从位置 Index 开始。 S 是 String 类型的变量。 Index 和 Count 是整数类型的表达式。 如果 Index 大于 S 的长度,则不删除任何字符。

3. 过程插入(来源:字符串;var S:字符串;索引:整数);

从指定位置开始将子字符串连接成字符串。 Source 是 String 类型的表达式。 S 是任意长度的 String 类型的变量。 索引是整数类型的表达式。 Insert 将 Source 插入 S,从位置 S[Index] 开始。

4.函数长度(S:字符串):整数;

返回字符串 S 中实际使用的字符数。请注意,当使用以 null 结尾的字符串时,字符数不一定等于字节数。

5. 函数Pos(Substr: String; S: String): Integer;

在字符串中搜索子字符串。 Pos 在 S 中查找 Substr 并返回一个整数值,该整数值是 S 中 Substr 的第一个字符的索引。如果未找到 Substr,则 Pos 返回 null。

3. 录音

记录是属于不同类型的有限数量的逻辑相关组件的集合。 记录的组成部分称为字段,每个字段都由一个名称标识。 记录字段包含字段的名称,后跟冒号表示字段的类型。 记录字段可以是 Pascal 中允许的任何类型,文件类型除外。

Pascal 语言中的记录描述是使用服务词 RECORD 进行的,然后是对记录组件的描述。 条目的描述以服务字 END 结束。

例如,笔记本包含姓氏、首字母和电话号码,因此可以方便地将笔记本中的单独一行表示为以下条目:

键入行 = 记录

FIO:字符串[20];

电话:字符串[7];

结束;

var str:行;

记录描述也可以不使用类型名称,例如:

var str : 记录

FIO:字符串[20];

电话:字符串[7];

结束;

仅在赋值语句中允许在赋值语句的左侧和右侧使用相同类型的记录名称时才能引用整个记录。 在所有其他情况下,操作单独的记录字段。 要引用单个记录组件,您必须指定记录的名称,并以点分隔,指定所需字段的名称。 这样的名称称为复合名称。 记录组件也可以是记录,在这种情况下,可分辨名称将不包含两个,而是包含更多名称。

使用 with append 运算符可以简化引用记录组件。 它允许您仅用字段名称替换表征每个字段的复合名称,并在连接语句中定义记录名称。

有时,单个记录的内容取决于其中一个字段的值。 在 Pascal 语言中,记录描述是允许的,由通用部分和变体部分组成。 变体部分是使用 case P ofconstruct 指定的,其中 P 是记录公共部分的字段名称。 该字段接受的可能值的列出方式与variant语句中的相同。 但是,与在变体语句中所做的那样指定要执行的操作不同,变体字段在括号中指定。 变体部分的描述以服务词结尾。 字段类型 P 可以在变量部分的标题中指定。 记录使用类型化常量进行初始化。

4. 套装

Pascal 语言中的集合概念是基于集合的数学概念:它是不同元素的有限集合。 枚举或区间数据类型用于构造具体的集合类型。 构成集合的元素类型称为基本类型。

使用功能词集来描述多重类型,例如:

类型 M = B 组;

这里M是复数类型,B是基本类型。

变量是否属于复数类型可以直接在变量声明部分确定。

集合类型常量被写为基本类型元素或间隔的括号序列,用逗号分隔。 [] 形式的常量表示空子集。

集合包括一组基本类型的元素、给定集合的所有子集和空子集。 如果构建集合的基类型有 K 个元素,那么包含在这个集合中的子集的数量等于 2 的 K 次方。在常量中列出基类型的元素的顺序是无关紧要的。 多类型变量的值可以通过 [T] 形式的构造给出,其中 T 是基本类型的变量。

赋值 (:=)、联合 (+)、交集 (*) 和减法 (-) 操作适用于集合类型的变量和常量。 这些操作的结果是复数类型的值:

1) ['A','B'] + ['A','D'] 将给出 ['A','B','D'];

2) ['A'] * ['A','B','C'] 将给出 ['A'];

3) ['A','B','C'] - ['A','B'] 将给出 ['C']。

操作适用于多个值:身份(=)、非身份(<>)、包含在(<=)中、包含(>=)。 这些操作的结果具有布尔类型:

1) ['A','B'] = ['A','C'] 将给出 FALSE ;

2) ['A','B'] <> ['A','C'] 将给出 TRUE;

3) ['B'] <= ['B','C'] 将给出 TRUE;

4) ['C','D'] >= ['A'] 将给出 FALSE。

除了这些操作之外,为了处理集合类型的值,使用了 in 操作,它检查操作符号左侧的基本类型的元素是否属于操作符号右侧的集合. 此操作的结果是一个布尔值。 通常使用检查元素是否属于集合的操作来代替关系操作。

当程序中使用多种数据类型时,对数据的位串进行操作。 计算机内存中多重类型的每个值对应一个二进制数字。

多个类型的值不能是 I/O 列表的元素。 在 Pascal 语言编译器的每个具体实现中,构建集合的基本类型的元素数量是有限的。

多个类型值的初始化是使用类型化常量完成的。

以下是使用集合的一些程序。

1. 程序排除(var S: Set of T; I:T);

从集合 S 中删除元素 I。S 是“set”类型的变量,I 是与 S 的原始类型兼容的类型的表达式。Exclude(S, I) 与 S 相同:= S - [I] ,但会生成更高效的代码。

2. 过程包括(var S: Set of T; I:T);

将元素 I 添加到集合 S。S 是“set”类型的变量,I 是与类型 S 兼容的类型的表达式。 Include(S, I) 构造与 S 相同:= S + [ I],但会生成更高效的代码。

第 6 讲。文件

1. 文件。 文件操作

将文件类型引入 Pascal 语言是由于需要提供与设计用于输入、输出和数据存储的外围(外部)计算机设备一起工作的能力。

文件数据类型(或文件)定义了任意数量的相同类型组件的有序集合。 数组、集合和记录的共同属性是它们的组件数量在编写程序的阶段就确定了,而程序文本中文件组件的数量不是确定的,可以是任意的。

处理文件时,会执行 I/O 操作。 输入操作是指将数据从外部设备(从输入文件)传输到计算机的主存储器,输出操作是将数据从主存储器传输到外部设备(到输出文件)。 外部设备上的文件通常称为物理文件。 它们的名称由操作系统决定。

在 Pascal 程序中,文件名是使用字符串指定的。 要在程序中使用文件,您必须定义一个文件变量。 Pascal 支持三种文件类型:文本文件、组件文件、无类型文件。

在程序中声明的文件变量称为逻辑文件。 提供数据 I/O 的所有基本过程和功能仅适用于逻辑文件。 在执行文件打开过程之前,物理文件必须与逻辑文件相关联。

文本文件

Pascal 语言中的一个特殊位置是文本文件,其组件是字符类型的。 为了描述文本文件,该语言定义了标准类型 Text:

var TF1,TF2:文本;

文本文件是一系列行,而行是一系列字符。 行的长度是可变的,每行都以行终止符结束。

组件文件

组件或类型化文件是具有其组件声明类型的文件。 组件文件由变量值的机器表示组成;它们以与计算机内存相同的形式存储数据。

文件类型值的描述为:

类型 M = T 文件;

其中 M 是文件类型的名称;

T - 组件类型。

文件组件可以是所有标量类型,也可以是结构化类型——数组、集合、记录。 在几乎所有 Pascal 语言的具体实现中,“文件的文件”结构是不允许的。

对组件文件的所有操作均使用标准程序执行。

写(f,X1,X2,...XK)

无类型文件

无类型文件允许您将计算机内存的任意部分写入磁盘并将它们从磁盘读取到内存。 无类型文件描述如下:

var f:文件;

现在我们列出处理不同类型文件的过程和功能。

1. 过程赋值(var F; FileName: String);

AssignFile 过程将外部文件名映射到文件变量。

F 是任何文件类型的文件变量,FileName 是 String 表达式,如果允许扩展语法,则为 PChar 表达式。 使用 F 进行的所有进一步操作都使用外部文件执行。

您不能使用已打开文件变量的过程。

2. 程序关闭(varF);

该过程断开文件变量和外部磁盘文件之间的链接并关闭文件。

F 是任何文件类型的文件变量,由 Reset、Rewrite 或 Append 过程打开。 与 F 关联的外部文件被完全修改然后关闭,释放文件描述符以供重用。

{SI+} 指令允许您在程序执行期间使用异常处理来处理错误。 关闭 {$1-} 指令后,您必须使用 IOResult 检查 I/O 错误。

3.函数Eof(var F):布尔值;

{类型化或非类型化文件}

函数 Eof[(var F: Text)]: 布尔值;

{文本文件}

检查当前文件位置是否为文件末尾。

如果当前文件位置在文件的最后一个字符之后,或者文件为空,则 Eof(F) 返回 True; 否则 Eof(F) 返回 False。

{SI+} 指令允许您在程序执行期间使用异常处理来处理错误。 关闭 {SI-} 指令后,您必须使用 IOResult 检查 I/O 错误。

4. 程序擦除(var F);

删除与 F 关联的外部文件。

F 是任何文件类型的文件变量。

在调用 Erase 过程之前,必须关闭文件。

{SI+} 指令允许您在程序执行期间使用异常处理来处理错误。 关闭 {SI-} 指令后,您必须使用 IOResult 检查 I/O 错误。

5.函数FileSize(var F):整数;

返回文件 F 的大小(以字节为单位) 但是,如果 F 是类型文件,则 FileSize 将返回文件中的记录数。 该文件必须在使用 FileSize 函数之前打开。 如果文件为空,FileSize(F) 返回零。 F 是任何文件类型的变量。

6.Function FilePos(var F): LongInt;

返回文件在文件中的当前位置。

在使用 FilePos 函数之前,文件必须是打开的。 FilePos 函数不用于文本文件。 F 是任何文件类型的变量,文本类型除外。

7. 过程重置(var F [: File; RecSize: Word]);

打开现有文件。

F 是与使用 AssignFile 的外部文件关联的任何文件类型的变量。 RecSize 是一个可选表达式,如果 F 是无类型文件,则使用该表达式。 如果 F 是无类型文件,则 RecSize 确定传输数据时使用的记录大小。 如果省略 RecSize,则默认记录大小为 128 字节。

重置过程打开与文件变量 F 关联的现有外部文件。如果没有具有该名称的外部文件,则会发生运行时错误。 如果与 F 关联的文件已经打开,则先关闭然后重新打开。 当前文件位置设置为文件的开头。

8. 过程重写(var F: File [; Recsize: Word]);

创建并打开一个新文件。

F 是与使用 AssignFile 的外部文件关联的任何文件类型的变量。 RecSize 是一个可选表达式,如果 F 是无类型文件,则使用该表达式。 如果 F 是无类型文件,则 RecSize 确定传输数据时使用的记录大小。 如果省略 RecSize,则默认记录大小为 128 字节。

Rewrite 过程创建一个新的外部文件,其名称与 F 关联。如果同名的外部文件已经存在,则将其删除并创建一个新的空文件。

9. 过程 Seek(var F; N: LongInt);

将当前文件位置移动到指定组件。 您只能对打开的类型化或非类型化文件使用该过程。

文件 F 的当前位置移动到编号 N。文件的第一个组件的编号为 0。

Seek(F, FileSize(F)) 指令将当前文件位置移动到文件末尾。

10. 程序追加(var F: Text);

打开现有文本文件以将信息附加到文件末尾(附加)。

如果具有给定名称的外部文件不存在,则会发生运行时错误。 如果文件 F 已经打开,它将关闭并重新打开。 当前文件位置设置为文件末尾。

11.Function Eoln[(var F: Text)]: Boolean;

检查当前文件位置是否是文本文件中的行尾。

如果当前文件位置在行或文件的末尾,则 Eoln(F) 返回 True; 否则 Eoln(F) 返回 False。

12.过程Read(F,V1[,V2,...,Vn]);

{类型化和非类型化文件}

过程 Read([var F: Text;] V1 [, V2,..., Vn]);

{文本文件}

对于类型化文件,该过程将文件组件读入一个变量。 每次读取时,文件中的当前位置都会前进到下一个元素。

对于文本文件,将一个或多个值读入一个或多个变量。

使用字符串变量,Read 读取直到(但不包括)下一个行尾标记的所有字符,或者直到 Eof(F) 评估为 True。 结果字符串被分配给变量。

在整数或实数类型的变量的情况下,该过程等待根据 Pascal 语法规则形成数字的字符序列。 当遇到第一个空格、制表符或换行符时,或者当 Eof(F) 计算结果为 True 时,读取停止。 如果数字字符串与预期格式不匹配,则会发生 I/O 错误。

13. 过程 Readln([var F: Text;] V1 [, V2..., Vn]);

它是 Read 过程的扩展,是为文本文件定义的。 读取文件中的字符串,包括行尾标记,并移动到下一行的开头。 不带参数调用 Readln(F) 函数将当前文件位置移动到下一行的开头,如果有,则跳转到文件末尾。

14. 函数 SeekEof[(var F: Text)]: 布尔值;

返回文件结尾,只能用于打开的文本文件。 通常用于从文本文件中读取数值。

15. 函数 SeekEoln[(var F: Text)]: 布尔值;

返回文件中的行终止符,只能用于打开的文本文件。 通常用于从文本文件中读取数值。

16. 过程 Write([var F: Text;] P1 [, P2,..., Pn]);

{文本文件}

将一个或多个值写入文本文件。

每个入口参数必须是 Char 类型、整数类型之一(Byte、ShorInt、Word、Longint、Cardinal)、浮点类型之一(Single、Real、Double、Extended、Currency)、字符串类型之一( PChar、AisiString、ShortString)或布尔类型之一(Boolean、Bool)。

过程写入(F,V1,...,Vn);

{键入的文件}

将变量写入文件组件。变量 VI....、Vn 必须与文件元素具有相同类型。每次写入变量时,文件中的当前位置都会移动到下一个元素。

17. 过程 Writeln([var F: Text;] [P1, P2,..., Pn]);

{文本文件}

执行写入操作,然后在文件中放置一个行尾标记。

不带参数调用 Writeln(F) 会将行尾标记写入文件。 该文件必须打开才能输出。

2. 模块。 模块类型

Pascal 中的模块 (1Ж1Т) 是专门设计的子程序库。 与程序不同,模块不能自行启动执行,它只能参与构建程序和其他模块。 模块允许您创建程序和函数的个人库并构建几乎任何大小的程序。

Pascal 中的模块是一个单独存储和独立编译的程序单元。 通常,模块是供其他程序使用的软件资源的集合。 程序资源被理解为 Pascal 语言的任何元素:常量、类型、变量、子例程。 模块本身不是可执行程序,它的元素被其他程序单元使用。

该模块的所有程序元素可分为两部分:

1) 供其他程序或模块使用的程序元素,这些元素在模块外称为可见的;

2) 仅对模块本身的操作所必需的软件元素,它们被称为不可见的(或隐藏的)。

据此,该模块除了头文件外,还包含三个主要部分,称为接口、可执行文件和已初始化。

一般来说,一个模块具有以下结构:

单元<模块名称>; {模块标题}

接口

{模块可见程序元素的描述}

履行

{模块隐藏编程元素的描述}

开始

{模块元素初始化语句}

结束。

在特定情况下,模块可能不包含实现部分和初始化部分,则模块结构如下:

单元<模块名称>; {模块标题}

接口

{模块可见程序元素的描述}

履行

结束。

模块中过程和函数的使用有其自身的特点。 子程序头包含调用它所需的所有信息:名称、参数列表和类型、函数的结果类型。 此信息必须可供其他程序和模块使用。 另一方面,实现其算法的子程序的文本不能被其他程序和模块使用。 因此,程序和函数的标题放在模块的接口部分,正文放在实现部分。

模块的接口部分仅包含过程和函数的可见(其他程序和模块可访问)标题(没有服务字转发)。 过程或函数的全文放在实现部分,头部可能不包含形参列表。

模块的源代码必须使用 Compile 子菜单的 Make 指令编译并写入磁盘。 模块编译的结果是一个扩展名为 . TPU(涡轮帕斯卡单元)。 模块的基本名称取自模块的标头。

要将模块连接到程序,您必须在模块描述部分指定其名称,例如:

使用 Crt、Graph;

如果模块接口部分和使用该模块的程序中的变量名称相同,则引用程序中描述的变量。 要引用模块中声明的变量,您必须使用由模块名称和变量名称组成的复合名称,并用点分隔。 复合名称的使用不仅适用于变量名称,而且适用于模块接口部分中声明的所有名称。

禁止递归使用模块。

如果一个模块有一个初始化部分,那么该部分中的语句将在使用该模块的程序开始执行之前执行。

让我们列出模块的类型。

1.系统模块。

SYSTEM 模块为所有内置功能(如 I/O、字符串操作、浮点操作和动态内存分配)实现了较低级别的支持例程。

SYSTEM 模块包含所有标准和内置的 Pascal 例程和函数。 任何不属于标准 Pascal 并且在任何其他模块中找不到的 Pascal 子例程都包含在 System 模块中。 该模块在所有程序中自动使用,不需要在uses语句中指定。

2.DOS模块。

Dos 模块实现了许多 Pascal 例程和函数,它们等效于最常用的 DOS 调用,例如 GetTime、SetTime、DiskSize 等。

3. CRT模块。

CRT 模块实现了许多功能强大的程序,可以完全控制 PC 的功能,例如屏幕模式控制、扩展键盘代码、颜色、窗口和声音。 CRT 模块只能用于在个人计算机上运行的程序 IBM PC、PC AT、IBM 的 PS/2 并与它们完全兼容。

使用 CRT 模块的主要优点之一是屏幕操作的速度和灵活性更高。 不使用 CRT 模块的程序使用 DOS 操作系统在屏幕上显示信息,这会带来额外的开销。 使用 CRT 模块时,输出信息直接发送到基本输入/输出系统 (BIOS),或者为了更快的操作,直接发送到显存。

4.图形模块。

使用此模块中包含的程序和功能,您可以在屏幕上创建各种图形。

5.叠加模块。

OVERLAY 模块允许您减少实模式 DOS 程序的内存需求。 事实上,编写超出可用内存总量的程序是可能的,因为在任何给定时刻,只有部分程序会在内存中。

LECTURE No. 7. 动态记忆

1.引用数据类型。 动态记忆。 动态变量

静态变量(静态分配)是在程序中显式声明的变量,通过名称引用。 静态变量在内存中的位置是在编译程序时确定的。 与此类静态变量不同,Pascal 程序可以创建动态变量。 动态变量的主要属性是它们在程序执行期间被创建并为它们分配内存。

动态变量被放置在动态内存区域(堆区域)中。 动态变量没有在变量声明中明确指定,也不能通过名称引用。 使用指针和引用访问此类变量。

引用类型(指针)定义了一组值,这些值指向特定类型的动态变量,称为基类型。 引用类型变量包含内存中动态变量的地址。 如果基类型是未声明的标识符,则它必须在类型声明的同一部分中声明为指针类型。

保留字 nil 表示具有不指向任何内容的指针值的常量。

让我们举一个动态变量描述的例子。

变种 p1, p2 : ^ 真实的;

p3, p4 : ^整数;

2. 使用动态内存。 无类型指针

动态内存过程和函数

1. 过程 New(var p: Pointer)。

在动态内存区域分配空间以容纳动态变量 pЛ, 并将其地址分配给指针 p。

2. 过程 Dispose(varp: Pointer)。

释放 New 过程为动态变量分配分配的内存,指针 p 的值变为未定义。

3. 过程 GetMem(varp: Pointer; size: Word)。

在堆区域中分配一个内存段,将其起始地址分配给 p 指针,段的大小(以字节为单位)由 size 参数指定。

4.过程FreeMem(var p: Pointer; size: Word)。

释放内存区域,其起始地址由p指针指定,大小由size参数指定。 指针 p 的值变得未定义。

5. 过程标记(var p: Pointer)

在调用时将一段空闲动态内存的开头地址写入指针 p。

6. 过程释放(var p: Pointer)

释放一段动态内存,从 Mark 过程写入指针 p 的地址开始,即清除调用 Mark 过程后占用的动态内存。

7. MaxAvaikLongint 函数

返回最长空闲堆的长度(以字节为单位)。

8. MemAvaikLongint 函数

返回可用动态内存的总量(以字节为单位)。

9. 辅助函数 SizeOf(X):Word

返回 X 占用的字节数,其中 X 可以是任何类型的变量名或类型名。

内置类型 Pointer 表示无类型指针,即不指向任何特定类型的指针。 Pointer 类型的变量可以取消引用:在此类变量之后指定 ^ 字符会导致错误。

和 nil 表示的值一样,Pointer 值与所有其他指针类型兼容。

第 8 讲。抽象数据结构

1. 抽象数据结构

结构化数据类型,例如数组、集合和记录,是静态结构,因为它们的大小在程序的整个执行过程中不会改变。

通常要求数据结构在解决问题的过程中改变它们的大小。 这种数据结构称为动态的。 这些包括堆栈、队列、列表、树等。

使用数组、记录和文件来描述动态结构会导致计算机内存的浪费并增加解决问题的时间。

任何动态结构的每个组件都是包含至少两个字段的记录:一个字段类型为“指针”,第二个字段用于数据放置。 一般来说,一条记录可能包含的不是一个,而是几个指针和几个数据字段。 数据字段可以是变量、数组、集合或记录。

如果指向部分包含列表中一个元素的地址,则该列表称为单向(或单链接)。 如果它包含两个组件,则它是双重连接的。 您可以对列表执行各种操作,例如:

1) 向列表中添加一个元素;

2) 使用给定键从列表中删除一个元素;

3) 搜索具有给定键字段值的元素;

4)对列表的元素进行排序;

5) 将列表划分为两个或多个列表;

6) 将两个或多个列表合并为一个;

7) 其他操作。

但是,通常不会出现解决各种问题的所有操作的需要。 因此,根据需要应用的基本操作,存在不同类型的列表。 其中最流行的是堆栈和队列。

2. 堆栈

堆栈是一种动态数据结构,添加组件和删除组件都是从一端开始的,称为堆栈顶部。 堆栈的工作原理是 LIFO(后进先出)——“后进先出”。

通常对栈执行三种操作:

1)堆栈的初始形成(第一个组件的记录);

2) 向栈中添加一个组件;

3)组件的选择(删除)。

要形成堆栈并使用它,您必须有两个“指针”类型的变量,第一个确定堆栈的顶部,第二个是辅助的。

例子。 编写一个程序,形成一个堆栈,向其中添加任意数量的组件,然后读取所有组件并将它们显示在显示屏上。 将字符串作为数据。 数据输入——来自键盘,输入结束标志——一串字符END。

程序堆栈;

使用 Crt;

类型

阿尔法=字符串[10];

PComp = ^Comp;

比较 = 记录

标清:阿尔法

pNext:PComp

结束;

VAR

pTop:PComp;

sc:阿尔法;

创建 ProcedureStack(var pTop : PComp; var sC : Alfa);

开始

新的(顶部);

pTop^.pNext := 无;

pTop^.sD := sC;

结束;

添加 ProcedureComp(var pTop : PComp; var sC : Alfa);

var pAux : PComp;

开始

新(pAux);

pAux^.pNext := pTop;

pTop := pAux;

pTop^.sD := sC;

结束;

程序 DelComp(var pTop : PComp; var sC : ALFA);

开始

sC := pTop^.sD;

pTop := pTop^.pNext;

结束;

开始

清除;

writeln('输入字符串');

读入(SC);

CreateStack(pTop, sc);

重复

writeln('输入字符串');

读入(SC);

AddComp(pTop, sc);

直到 sC = 'END';

writeln('****** 输出 ******');

重复

德尔康普(pTop,sc);

写入(SC);

直到 pTop = NIL;

结束。

3. 队列

队列是一种动态数据结构,其中一个组件在一端添加并在另一端检索。 队列的工作原理是 FIFO(先进先出)——“先进先出”。

要形成一个队列并使用它,必须有三个指针类型的变量,第一个确定队列的开始,第二个 - 队列的结束,第三个 - 辅助。

例子。 编写一个程序,形成一个队列,向其中添加任意数量的组件,然后读取所有组件并将它们显示在显示屏上。 将字符串作为数据。 数据输入——来自键盘,输入结束标志——一串字符END。

程序队列;

使用 Crt;

类型

阿尔法=字符串[10];

PComp = ^Comp;

比较 = 记录

标清:阿尔法

p下一个:PComp;

结束;

VAR

pBegin、pEnd :PComp;

sc:阿尔法;

创建程序队列(var pBegin,pEnd:PComp; var sC:Alfa);

开始

新的(p开始);

pBegin^.pNext := NIL;

pBegin^.sD := sC;

pEnd := pBegin;

结束;

过程添加过程队列(var pEnd:PComp;var sC:Alfa);

var pAux : PComp;

开始

新(pAux);

pAux^.pNext := NIL;

pEnd^.pNext := pAux;

pEnd := pAux;

pEnd^.sD := sC;

结束;

过程 DelQueue(var pBegin : PComp; var sC : Alfa);

开始

sC := pBegin^.sD;

pBegin := pBegin^.pNext;

结束;

开始

清除;

writeln('输入字符串');

读入(SC);

CreateQueue(pBegin, pEnd, sc);

重复

writeln('输入字符串');

读入(SC);

添加队列(pEnd,sc);

直到 sC = 'END';

writeln(' ***** 显示结果 *****');

重复

DelQueue(pBegin, sc);

写入(SC);

直到 pBegin = NIL;

结束。

LECTURE No. 9. 树状数据结构

1. 树数据结构

树状数据结构是元素节点的有限集合,它们之间存在关系——源节点和生成节点之间的连接。

如果我们使用 N. Wirth 提出的递归定义,那么基类型为 t 的树数据结构要么是一个空结构,要么是一个类型为 t 的节点,其中一组具有基类型 t 的树结构的有限集合,称为子树,是联系。

接下来,我们给出操作树结构时使用的定义。

如果节点 y 位于节点 x 的正下方,则节点 y 称为节点 x 的直接后代,x 是节点 y 的直接祖先,即如果节点 x 位于第 i 层,则节点 y 相应地为位于第 (i + 1) 层。

树节点的最大级别称为树的高度或深度。 祖先不只有树的一个节点——它的根。

没有子节点的树节点称为叶子节点(或树的叶子)。 所有其他节点称为内部节点。 节点的直接子节点的数量决定了该节点的度数,给定树中节点的最大可能度数决定了树的度数。

祖先和后代不能互换,即原始与生成之间的联系仅在一个方向上起作用。

如果你从树的根到某个特定的节点,那么在这种情况下将遍历的树的分支数称为该节点的路径长度。 如果树的所有分支(节点)都是有序的,则称这棵树是有序的。

二叉树是树结构的一种特殊情况。 这些是每个孩子最多有两个孩子的树,称为左子树和右子树。 因此,二叉树是一种度数为二的树结构。

二叉树的排序由以下规则确定:每个节点都有自己的键域,并且对于每个节点,键值大于其左子树中的所有键,小于其右子树中的所有键。

度数大于 XNUMX 的树称为强分支树。

2. 树上的操作

此外,我们将考虑与二叉树相关的所有操作。

一、建树

我们提出了一种构造有序树的算法。

1. 如果树为空,则将数据传输到树的根。 如果树不为空,则它的一个分支以不违反树顺序的方式下降。 结果,新节点成为树的下一个叶子。

2. 要将节点添加到已经存在的树中,可以使用上述算法。

3. 当从树中删除一个节点时,你应该小心。 如果要移除的节点是叶子,或者只有一个孩子,那么操作很简单。 如果要删除的节点有两个后代,则需要在其后代中找到一个可以放置在其位置的节点。 这是必要的,因为需要对树进行排序。

您可以这样做:将要删除的节点与左子树中键值最大的节点交换,或者与右子树中键值最小的节点交换,然后将所需节点作为叶子删除。

二、 查找具有给定键字段值的节点

执行此操作时,需要遍历树。 有必要考虑不同形式的树表示法:前缀、中缀和后缀。

问题来了:如何表示树的节点以便使用它们最方便? 可以使用数组表示树,其中每个节点由组合类型的值描述,组合类型具有字符类型的信息字段和引用类型的两个字段。 但这不是很方便,因为树有大量未预先确定的节点。 因此,描述树时最好使用动态变量。 那么每个节点都用一个相同类型的值来表示,其中包含对给定数量的信息字段的描述,并且对应的字段的数量必须等于树的度数。 用 nil 定义没有后代是合乎逻辑的。 然后,在 Pascal 中,二叉树的描述可能如下所示:

类型树链接 = ^树;

树=记录;

Inf : <数据类型>;

左,右:TreeLink;

结束。

3. 操作执行实例

1. 构造一棵具有 n 个最小高度的节点的树,或者是一棵完全平衡的树(这种树的左右子树的节点数必须相差不超过一个)。

递归构造算法:

1)将第一个节点作为树的根。

2) nl 个节点的左子树以同样的方式构建。

3)nr个节点的右子树以同样的方式构建;

nr = n - nl - 1. 作为信息字段,我们将获取从键盘输入的节点号。 实现此构造的递归函数将如下所示:

函数树(n:字节):TreeLink;

变量 t:树链接; nl,nr,x:字节;

开始

如果 n = 0 那么 Tree := nil

其他

开始

nl := n 格 2;

nr = n - nl - 1;

writeln('输入顶点编号');

读入(x);

新的(t);

t^.inf := x;

t^.left := 树(nl);

t^.right := 树(nr);

树 := t;

结束;

{树}

结束。

2. 在二叉树中,找到给定键域值的节点。 如果树中没有这样的元素,则将其添加到树中。

搜索过程(x:字节;var t:TreeLink);

开始

如果 t = nil 那么

开始

新(t);

t^inf := x;

t^.left := 无;

t^.right := 无;

结束

否则,如果 x < t^.inf 那么

搜索(x, t^.left)

否则,如果 x > t^.inf 那么

搜索(x, t^.right)

其他

开始

{处理找到的元素}

...

结束;

结束。

3. 分别编写正向、对称和反向的树遍历过程。

3.1。 程序预购(t:TreeLink);

开始

如果 t <> nil 那么

开始

写入(t^.inf);

预购(t^.left);

预购(t^.right);

结束;

结束;

3.2. 过程 Inorder(t : TreeLink);

开始

如果 t <> nil 那么

开始

有序(t^.left);

写入(t^.inf);

有序(t^.right);

结束;

结束。

3.3. 程序 Postorder(t : TreeLink);

开始

如果 t <> nil 那么

开始

后序(t^.left);

后序(t^.right);

写入(t^.inf);

结束;

结束。

4. 在二叉树中,删除给定键域值的节点。

让我们描述一个递归过程,它将考虑树中所需元素的存在以及该节点的后代数量。 如果要删除的节点有两个子节点,则将其替换为其左子树中最大的键值,然后才会被永久删除。

过程 Delete1(x : Byte; var t : TreeLink);

Var p:树链接;

过程 Delete2(var q : TreeLink);

开始

如果 q^.right <> nil 则 Delete2(q^.right)

其他

开始

p^.inf := q^.inf;

p := q;

q := q^.left;

结束;

结束;

开始

如果 t = nil 那么

Writeln('没有找到元素')

否则,如果 x < t^.inf 那么

删除1(x, t^.left)

否则,如果 x > t^.inf 那么

删除1(x, t^.right)

其他

开始

P := 吨;

如果 p^.left = nil 那么

t := p^.对

其他

如果 p^.right = nil 那么

t := p^.左

其他

删除2(p^.left);

结束;

结束。

第 10 讲。计数

1. 图的概念。 表示图的方法

图是一对 G = (V,E),其中 V 是一组任意性质的对象,称为顶点,E 是一组对 ei = (vil, vi2)、vijOV,称为边。 在一般情况下,集合 V 和/或族 E 可能包含无限数量的元素,但我们将只考虑有限图,即 V 和 E 都是有限的图。 如果 ei 中包含的元素的顺序很重要,则该图称为有向图,缩写为有向图,否则称为无向图。 有向图的边称为弧。 在下文中,我们假设术语“图”,在没有说明(有向或无向)的情况下使用,表示无向图。

如果 e = ,则顶点 v 和 u 称为边的端点。 这里我们说边 e 与每个顶点 v 和 u 相邻(事件)。 顶点 v 和 和 也称为相邻(事件)。 在一般情况下,形式 e = ; 这样的边缘称为循环。

图中顶点的度数是与该顶点相关的边的数量,循环被计数两次。由于每条边都与两个顶点相关,因此图中所有顶点的度数之和等于边数的两倍: Sum(deg(vi), i=1...|V|) = 2 * | E|。

节点的权重是分配给给定节点的数字(实数、整数或有理数)(解释为成本、吞吐量等)。 权重、边长 - 一个或多个被解释为长度、带宽等的数字。

图中的路径(或有向图中的路径)是顶点和边(或有向图中的弧)的交替序列,其形式为 v0, (v0,v1), v1..., (vn - 1,vn ),vn。数字 n 称为路径长度。没有重复边的路径称为链;没有重复顶点的路径称为简单链。路径可以闭合 (v0 = vn)。没有重复边的闭合路径称为循环(或有向图中的轮廓);不重复顶点(第一个和最后一个除外) - 一个简单的循环。

如果图的任意两个顶点之间存在路径,则称为连通图,否则称为断开连接。 断开连接的图由几个连接的组件(连接子图)组成。

有多种表示图形的方法。 让我们分别考虑它们。

1. 发病矩阵。

这是一个维度为 nx n 的矩形矩阵,其中 n 是顶点数,am 是边数。矩阵元素的值确定如下:如果边xi和顶点vj重合,则对应的矩阵元素的值等于1,否则该值为零。对于有向图,关联矩阵按照以下原则构造:如果边 xi 来自顶点 vj,则元素的值等于 - 1,如果边 xi 进入顶点 vj,则元素的值等于 XNUMX,否则等于 XNUMX 。

2. 邻接矩阵。

这是一个维度为 nxn 的方阵,其中 n 是顶点数。如果顶点vi和vj相邻,即有边连接,则对应的矩阵元素等于1,否则等于0。有向图和无向图构造该矩阵的规则没有什么不同。邻接矩阵比关联矩阵更紧凑。需要注意的是,这个矩阵也非常稀疏,但在无向图的情况下,它相对于主对角线是对称的,因此你不能存储整个矩阵,而只能存储一半(三角矩阵) )。

3. 邻接(事件)列表。

它是一种数据结构,为图中的每个顶点存储与其相邻的顶点列表。 该列表是一个指针数组,其中第 i 个元素包含指向与第 i 个顶点相邻的顶点列表的指针。

邻接列表比邻接矩阵更有效,因为它消除了空元素的存储。

4. 列表列表。

它是一种树状数据结构,其中一个分支包含与每个图顶点相邻的顶点列表,第二个分支指向下一个图顶点。 这种表示图的方式是最优化的。

2. 用关联列表表示图。 图深度遍历算法

要将图表实现为关联列表,您可以使用以下类型:

类型列表 = ^S;

S=记录;

inf:字节;

下一个:列表;

结束;

那么图形定义如下:

Var Gr:列表的数组[1..n];

现在让我们转向图遍历过程。 这是一种辅助算法,可让您查看图形的所有顶点,分析所有信息字段。 如果我们深入考虑图的遍历,那么有两种算法:递归和非递归。

使用递归深度优先遍历算法,我们取任意顶点并找到与其相邻的任意未见(新)顶点 v。 然后我们将顶点 v 视为不是新的,并找到与其相邻的任何新顶点。 如果某个顶点没有更新的看不见的顶点,那么我们认为这个顶点被使用并返回一个更高级别的顶点,我们从中得到我们使用的顶点。 以这种方式继续遍历,直到图中没有新的未扫描顶点。

在 Pascal 中,深度优先遍历过程如下所示:

过程 Obhod(gr : Graph; k : Byte);

变量 g:图表; l :列表;

开始

新 [k] := 假;

克:=克;

而 g^.inf <> k 做

g := g^.下一个;

l := g^.smeg;

虽然 l <> nil 开始

如果 nov[l^.inf] 那么 Obhod(gr, l^.inf);

l := l^.下一个;

结束;

结束;

注意

在此过程中,当描述 Graph 类型时,我们的意思是通过列表列表来描述图。 数组 nov[i] 是一个特殊数组,如果第 i 个顶点未被访问,则其第 i 个元素为 True,否则为 False。

也经常使用非递归遍历算法。 在这种情况下,递归被堆栈替换。 一旦一个顶点被查看,它就会被压入堆栈,当没有更多新的顶点与之相邻时,它就会被使用。

3. 用列表列表表示图。 广度图遍历算法

可以使用列表列表定义图形,如下所示:

类型列表 = ^Tlist;

tlist=记录

inf:字节;

下一个:列表;

结束;

图 = ^TGpaph;

TGpaph = 记录

inf:字节;

smeg:列表;

下一个:图表;

结束;

当以广度遍历图时,我们选择一个任意顶点并一次查看与其相邻的所有顶点。 使用队列而不是堆栈。 广度优先搜索算法对于查找图中的最短路径非常方便。

这是在伪代码中遍历宽度图的过程:

程序 Obhod2(v);

{values spisok, nov - global}

开始

队列 = O;

队列 <= v;

新 [v] = 假;

While queue <> O do

开始

p <= 队列;

为你在 spisok(p) 做

如果 new[u] 那么

开始

nov[u] := 错误;

队列<=你;

结束;

结束;

结束;

LECTURE #11. 对象数据类型

1. Pascal 中的对象类型。 对象的概念、描述和使用

从历史上看,第一种编程方法是过程式编程,也称为自下而上编程。 最初,创建了用于计算机应用各个领域的标准程序的通用库。 然后,基于这些程序,创建了更复杂的程序来解决特定问题。

但是,计算机技术在不断发展,开始被用来解决生产、经济等各种问题,因此需要处理各种格式的数据,解决非标准问题(例如非数值问题)。 因此,在开发编程语言时,他们开始关注各类数据的创建。 这促成了combined、multiple、string、file等复杂数据类型的出现。在解决问题之前,程序员进行了分解,即将任务拆分为多个子任务,每个子任务都编写了一个单独的模块. 主要的编程技术包括三个阶段:

1)自上而下的设计;

2)模块化编程;

3)结构编码。

但从 60 世纪 XNUMX 年代中期开始,新的概念和方法开始形成,形成了面向对象编程技术的基础。 在这种方法中,现实世界的建模和描述是在要解决的问题所属的特定主题领域的概念级别上进行的。

面向对象编程是一种与我们的行为非常相似的编程技术。 这是早期编程语言设计创新的自然演变。 面向对象的编程比以前所有关于结构化编程的发展都更具结构性。 它也比以前在内部数据抽象和编程细节方面的尝试更加模块化和抽象。 面向对象的编程语言具有三个主要特性:

1)封装。 将记录与操作这些记录的字段的过程和函数结合起来形成一种新的数据类型——对象;

2)继承。 对象的定义及其进一步用于构建子对象的层次结构,使与层次结构相关的每个子对象能够访问所有父对象的代码和数据;

3) 多态性。 为操作指定一个名称,然后在对象层次结构中上下共享,层次结构中的每个对象以适合它的方式执行该操作。

说到对象,我们介绍一种新的数据类型——对象。 对象类型是由固定数量的组件组成的结构。 每个组件要么是包含严格定义类型的数据的字段,要么是对对象执行操作的方法。 类比变量的声明,字段的声明指定了该字段的数据类型和命名字段的标识符:类比过程或函数的声明,方法的声明指定了过程的标题,函数、构造函数或析构函数。

一个对象类型可以继承另一个对象类型的组件。 如果类型 T2 继承自类型 T1,则类型 T2 是类型 T1 的子类型,类型 T1 本身是类型 T2 的父级。 继承是可传递的,即如果 TK 继承自 T2,而 T2 继承自 T1,则 TK 继承自 T1。 对象类型的范围(域)由其自身及其所有后代组成。

下面的源码是一个对象类型声明的例子,type

类型

点=对象

X、Y:整数;

结束;

矩形 = 对象

A、B:T点;

程序初始化(XA,YA,XB,YB:整数);

过程复制(var R:TRectangle);

程序移动(DX,DY:整数);

程序增长(DX,DY:整数);

过程相交(var R:TRectangle);

过程联合(var R:TRectangle);

函数包含(P:点):布尔值;

结束;

字符串指针 = ^字符串;

FieldPtr = ^TField;

TField = 对象

X、Y、Len:整数;

名称:字符串指针;

构造函数 Copy(var F: TField);

构造函数 Init(FX, FY, FLen: Integer; FName: String);

析构函数完成; 虚拟的;

程序显示; 虚拟的;

程序编辑; 虚拟的;

函数GetStr:字符串; 虚拟的;

函数 PutStr(S: String): 布尔值; 虚拟的;

结束;

StrFieldPtr = ^TStrField;

StrField = 对象(TField)

值:PString;

构造函数 Init(FX, FY, FLen: Integer; FName: String);

析构函数完成; 虚拟的;

函数GetStr:字符串; 虚拟的;

函数 PutStr(S: String): 布尔值;

虚拟的;

函数获取:字符串;

程序放置(S:字符串);

结束;

NumFieldPtr = ^TNumField;

TNumField = 对象(TField)

私立

值、最小值、最大值:Longint;

国家

构造函数 Init(FX, FY, FLen: Integer; FName: String;

FMin, FMax: 长整型);

函数GetStr:字符串; 虚拟的;

函数 PutStr(S: String): 布尔值; 虚拟的;

函数获取:Longint;

函数 Put(N: Longint);

结束;

ZipFieldPtr = ^TZipField;

ZipField = 对象(TNumField)

函数GetStr:字符串; 虚拟的;

函数 PutStr(S: String): 布尔值;

虚拟的;

结束。

与其他类型不同,对象类型只能在程序或模块范围最外层的类型声明部分中声明。 因此,对象类型不能在变量声明部分或过程、函数或方法块内声明。

文件类型组件类型不能具有对象类型或任何包含对象类型组件的结构类型。

2. 继承

一种类型继承另一种类型特征的过程称为继承。 后代称为派生(子)类型,子类型继承自的类型称为父(父)类型。

以前已知的 Pascal 记录类型不能继承。 然而,Borland Pascal 扩展了 Pascal 语言以支持继承。 其中一个扩展是与记录相关的新数据结构类别,但功能更强大。 这个新类别中的数据类型是使用新的保留字“object”定义的。 对象类型可以通过描述 Pascal 项的方式定义为完整的、独立的类型,但也可以通过将父类型放在保留字“object”后面的括号中来定义为现有对象类型的后代。

3. 实例化对象

通过声明对象类型的变量或常量,或通过将标准 New 过程应用于“指向对象类型的指针”类型的变量来创建对象的实例。 生成的对象称为对象类型的实例;

VAR

F:T场;

Z:TZipField;

FP:P场;

ZP:PZipField;

给定这些变量声明,F 是 TField 的一个实例,Z 是 TZipField 的一个实例。 同理,对FP和ZP应用New后,FP会指向一个TField实例,ZP会指向一个TZipField实例。

如果一个对象类型包含虚方法,那么该对象类型的实例必须在调用任何虚方法之前通过调用构造函数来初始化。

下面是一个例子:

VAR

S:StrField;

伊金

S.Init(1, 1, 25, '名字');

S.Put('弗拉基米尔');

S.显示;

...

完成;

结束。

如果未调用 S.Init,则调用 S.Display 将导致此示例失败。

分配对象类型的实例并不意味着实例的初始化。 一个对象由编译器生成的代码初始化,该代码在构造函数的调用和执行实际到达构造函数代码块中的第一条语句的点之间运行。

如果对象实例未初始化并且启用了范围检查(通过 {SR+} 指令),则对对象实例的虚拟方法的第一次调用会产生运行时错误。 如果范围检查被禁用(通过 {SR-} 指令),那么第一次调用未初始化对象的虚拟方法可能会导致不可预知的行为。

强制初始化规则也适用于结构类型组件的实例。 例如:

VAR

注释:TStrField 的数组 [1..5];

一:整数

开始

对于 I := 1 到 5 做

注释 [I].Init (1, I + 10, 40, 'first_name');

.

.

.

for I := 1 到 5 do Comment [I].Done;

结束;

对于动态实例,初始化通常与放置有关,而清理与删除有关,这是通过 New 和 Dispose 标准过程的扩展语法实现的。 例如:

VAR

SP:StrFieldPtr;

开始

新的(SP,初始化(1, 1, 25, 'first_name');

SP^.Put('弗拉基米尔');

SP^.显示;

.

.

.

处置(SP,完成);

结束。

指向对象类型的指针与指向任何父对象类型的指针的赋值兼容,因此在运行时指向对象类型的指针可以指向该类型的实例或任何子类型的实例。

例如,ZipFieldPtr 类型的指针可以分配给 PZipField、PNumField 和 PField 类型的指针,并且在运行时,PField 类型的指针可以为 nil 或指向 TField、TNumField 或 TZipField 的实例,或任何TField 子类型的实例。

这些赋值指针兼容性规则也适用于对象类型参数。 例如,TField.Cop 方法可以传递 TField、TStrField、TNumField、TZipField 或 TField 的任何其他类型子级的实例。

4. 组件和范围

bean 标识符的范围超出了对象类型。 此外,bean 标识符的范围通过实现对象类型及其后代的方法的过程、函数、构造函数和析构函数块扩展。 基于这些考虑,组件标识符的拼写在对象类型及其所有后代以及所有方法中必须是唯一的。

类型声明的私有部分中描述的组件标识符的范围仅限于包含对象类型声明的模块(程序)。 换句话说,私有标识符 bean 就像包含对象类型声明的模块内的普通公共标识符一样,而在模块外部,任何私有 bean 和标识符都是未知且不可访问的。 通过将相关类型的对象放在同一个模块中,可以确保这些对象可以访问彼此的私有组件,而这些私有组件不会被其他模块所知道。

在对象类型声明中,方法头可以指定所描述的对象类型的参数,即使声明尚未完成。

第 12 讲。方法

一、方法

对象类型内的方法声明对应于前向方法声明(forward)。 因此,在对象类型声明之后的某个地方,但在与对象类型声明的范围相同的范围内,必须通过定义其声明来实现方法。

对于过程和函数方法,定义声明采用普通过程或函数声明的形式,但在这种情况下,过程或函数标识符被视为方法标识符。

对于构造函数和析构函数方法,定义声明采用过程方法声明的形式,除了保留字过程被保留字构造函数或析构函数替换。

定义方法声明可以但不必重复对象类型中方法头的形式参数列表。 在这种情况下,方法头必须与对象类型中的头在顺序、类型和参数名称上完全匹配,如果方法是函数,则必须与函数结果的返回类型完全匹配。

方法的定义描述总是包含一个标识符为 Self 的隐式参数,对应于对象类型的形式变量参数。 在方法块中,Self 表示其方法组件被指定为调用该方法的实例。 因此,对 Self 字段值的任何更改都会反映在实例中。

对象类型 bean 标识符的范围扩展到实现该对象类型方法的过程、函数、构造函数和析构函数块。 效果与在方法块的开头插入以下形式的 with 语句相同:

用自己做

开始

...

结束;

基于这些考虑,组件标识符、形式方法参数、Self 以及任何引入方法可执行部分的标识符的拼写必须是唯一的。

如果需要唯一的方法标识符,则使用限定的方法标识符。 它由一个对象类型标识符后跟一个点和一个方法标识符组成。 与任何其他标识符一样,合格的方法标识符可以可选地在包标识符和句点之前。

虚拟方法

默认情况下,方法是静态的,但除了构造函数之外,它们可以是虚拟的(通过在方法声明中包含 virtual 指令)。 编译器在编译过程中解析对静态方法调用的引用,而对虚拟方法的调用在运行时解析。 这有时称为后期绑定。

如果一个对象类型声明或继承了任何虚方法,那么该类型的变量必须在调用任何虚方法之前通过调用构造函数来初始化。 因此,描述或继承虚方法的对象类型也必须描述或继承至少一个构造方法。

对象类型可以覆盖它从其父对象继承的任何方法。 如果子项中的方法声明指定与父项中的方法声明相同的方法标识符,则子项中的声明将覆盖父项中的声明。 重写方法的范围扩展到引入该方法的子方法的范围,并将一直保持到方法标识符再次被重写。

覆盖静态方法与更改方法头无关。 相反,虚拟方法覆盖必须保留顺序、参数类型和名称以及函数结果类型(如果有)。 此外,重新定义必须再次包含虚拟指令。

动态方法

Borland Pascal 支持称为动态方法的附加后期绑定方法。 动态方法与虚拟方法的区别仅在于它们在运行时分派的方式。 在所有其他方面,动态方法被认为等同于虚拟方法。

动态方法声明等同于虚拟方法声明,但动态方法声明必须包含动态方法索引,该索引紧跟在 virtual 关键字之后。 动态方法的索引必须是介于 1 和 656535 之间的整数常量,并且在对象类型或其祖先中包含的其他动态方法的索引中必须是唯一的。 例如:

过程 FileOpen(var Msg: TMessage); 虚拟100;

动态方法的重写必须匹配参数的顺序、类型和名称,并且与父方法的函数的结果类型完全匹配。 覆盖还必须包括一个虚拟指令,后跟在祖先对象类型中指定的相同动态方法索引。

2. 构造函数和析构函数

构造函数和析构函数是方法的特殊形式。 与 New 和 Dispose 标准过程的扩展语法结合使用,构造函数和析构函数具有放置和删除动态对象的能力。 此外,构造函数能够对包含虚方法的对象执行所需的初始化。 像所有方法一样,构造函数和析构函数可以被继承,并且对象可以包含任意数量的构造函数和析构函数。

构造函数用于初始化新创建的对象。 通常,初始化是基于作为参数传递给构造函数的值。 构造函数不能是虚拟的,因为虚拟方法的调度机制取决于首先初始化对象的构造函数。

以下是构造函数的一些示例:

构造函数 Field.Copy(var F: Field);

开始

自我:= F;

结束;

构造函数 Field.Init(FX, FY, FLen: integer; FName: string);

开始

X := 外汇;

Y := 财政年度;

GetMem(名称,长度(FName)+ 1);

名称^ := FName;

结束;

构造函数 TStrField.Init(FX, FY, FLen: integer; FName: string);

开始

继承 Init(FX, FY, FLen, FName);

Field.Init(FX, FY, FLen, FName);

GetMem(值,Len);

值^ := '';

结束;

派生(子)类型的构造函数的主要操作,例如上面的 TStr 字段。 Init 几乎总是调用其直接父级的适当构造函数来初始化对象的继承字段。 执行此过程后,构造函数初始化对象的仅属于派生类型的字段。

析构函数与构造函数相反,用于在对象使用后进行清理。 通常,清理包括删除对象中的所有指针字段。

注意

析构函数可以是虚拟的,而且通常是。 析构函数很少有参数。

下面是一些析构函数的例子:

析构函数字段完成;

开始

FreeMem(姓名, 长度(姓名^) + 1);

结束;

析构函数 StrField.Done;

开始

FreeMem(值,Len);

现场完成;

结束;

子类型的析构函数,例如上面的 TStrField。 完成后,通常首先删除派生类型中引入的指针字段,然后,作为最后一步,调用直接父级的适当收集器-析构函数以删除对象的继承指针字段。

3. 析构函数

Borland Pascal 提供了一种特殊类型的方法,称为垃圾收集器(或析构函数),用于清理和删除动态分配的对象。 析构函数将删除对象的步骤与该类型对象所需的任何其他操作或任务相结合。 您可以为单个对象类型定义多个析构函数。

析构函数与对象类型定义中的所有其他对象方法一起定义:

类型

员工=对象

名称:字符串[25];

标题:字符串[25];

率:真实;

构造函数 Init(AName, ATitle: String; ARate: Real);

析构函数完成; 虚拟的;

函数GetName:字符串;

函数GetTitle:字符串;

函数GetRate:速率; 虚拟的;

函数GetPayAmount:真实; 虚拟的;

结束;

析构函数可以被继承,它们可以是静态的或虚拟的。 由于不同的终结器往往需要不同类型的对象,因此通常建议析构函数始终是虚拟的,以便为每种类型的对象执行正确的析构函数。

不需要为每个清理方法指定保留字析构函数,即使对象的类型定义包含虚方法。 析构函数实际上只对动态分配的对象起作用。

当清理动态分配的对象时,析构函数执行一个特殊的功能:它确保在动态分配的内存区域中始终释放正确数量的字节。 使用带有静态分配对象的析构函数不必担心; 事实上,通过不将对象的类型传递给析构函数,程序员剥夺了该类型对象在 Borland Pascal 中动态内存管理的全部好处。

当必须清除多态对象并且必须释放它们占用的内存时,析构函数实际上变成了它们自己。

多态对象是那些由于 Borland Pascal 的扩展类型兼容性规则而分配给父类型的对象。 分配给 TEmployee 类型变量的 THourly 类型对象的实例是多态对象的一个​​示例。 这些规则也可以应用于对象; 指向 Thourly 的指针可以自由地分配给指向 TEmployee 的指针,并且该指针指向的对象将再次成为多态对象。 术语“多态”是合适的,因为处理对象的代码在编译时“不知道”它最终需要处理什么类型的对象。 它唯一知道的是这个对象属于对象的层次结构,这些对象是指定对象类型的后代。

显然,对象类型的大小是不同的。 那么当需要清理堆分配的多态对象时,Dispose 是如何知道要释放多少字节的堆空间的呢? 在编译时,无法从多态对象中提取有关对象大小的信息。

析构函数通过引用写入此信息的位置 - 在 TCM 实现变量中来解决这个难题。 每个对象类型的 TBM 都包含该对象类型的字节大小。 任何对象的虚拟方法表都可以通过隐藏参数 Self 获得,在调用方法时发送给方法。 析构函数只是一种方法,因此当对象调用它时,析构函数会在堆栈上获取 Self 的副本。 因此,如果一个对象在编译时是多态的,那么由于后期绑定,它在运行时永远不会是多态的。

要执行此后期绑定释放,必须调用析构函数作为 Dispose 过程的扩展语法的一部分:

处置(P,完成);

(在 Dispose 过程之外调用析构函数根本不会释放任何内存。)这里真正发生的是 P 指向的对象的垃圾收集器作为正常方法执行。 但是,一旦最后一个动作完成,析构函数就会在 TCM 中查找其类型实现的大小,并将大小传递给 Dispose 过程。 Dispose 过程通过删除之前属于 P^ 的堆空间的正确字节数来终止进程。 无论 P 是否指向 TSalaried 类型的实例,或者它是否指向 TSalaried 类型的子类型之一,例如 TCommissioned,要释放的字节数都是正确的。

请注意,析构方法本身可以为空,并且只执行此功能:

析构函数对象。完成;

开始

结束;

在这个析构函数中有用的不是它的主体的属性,但是,编译器会生成尾声代码来响应析构函数的保留字。 它就像一个模块,不导出任何东西,但通过在启动程序之前执行其初始化部分来完成一些无形的工作。 所有动作都发生在幕后。

4. 虚方法

如果方法的对象类型声明后跟新的保留字 virtual,则该方法变为虚拟。 如果父类型中的方法被声明为虚拟,则子类型中具有相同名称的所有方法也必须被声明为虚拟以避免编译器错误。

以下是示例工资单中正确虚拟化的对象:

类型

PEmployee = ^TEmployee;

员工=对象

姓名,标题:字符串[25];

率:真实;

构造函数 Init(AName, ATitle: String; ARate: Real);

函数GetPayAmount:真实; 虚拟的;

函数GetName:字符串;

函数GetTitle:字符串;

函数GetRate:真实;

程序展示; 虚拟的;

结束;

每小时 = ^每小时;

Thourly = 对象(TEmployee);

时间:整数;

构造函数 Init(AName, ATitle: String; ARate: Real; Time: Integer);

函数GetPayAmount:真实; 虚拟的;

函数GetTime:整数;

结束;

PSalaried = ^TSalaried;

TSalaried = 对象(TEmployee);

函数GetPayAmount:真实; 虚拟的;

结束;

P 委托 = ^T 委托;

TCommissioned = 对象(受薪);

佣金:真实;

销售金额:真实;

构造函数 Init(AName, ATitle: String; ARate,

ACommission, ASalesAmount: Real);

函数GetPayAmount:真实; 虚拟的;

结束;

构造函数是一种特殊类型的过程,它为虚方法机制做一些设置工作。 此外,必须在调用任何虚拟方法之前调用构造函数。 不先调用构造函数就调用虚方法会阻塞系统,编译器无法检查方法调用的顺序。

每个具有虚方法的对象类型都必须有一个构造函数。

警告

必须在调用任何其他虚拟方法之前调用构造函数。 在未调用构造函数的情况下调用虚方法可能会导致系统锁定,并且编译器无法检查调用方法的顺序。

注意

对于对象构造函数,建议使用标识符 Init。

每个不同的对象实例都必须使用单独的构造函数调用进行初始化。 初始化一个对象的一个​​实例然后将该实例分配给其他实例是不够的。 其他实例,即使它们可能包含有效数据,也不会使用赋值运算符进行初始化,并且会阻止系统对其虚拟方法的任何调用。 例如:

VAR

FBee、GBee:蜜蜂; { 创建两个 Bee 实例 }

开始

FBee.Init(5, 9) { FBee 的构造函数调用}

GBee := FBee; { Gbee 无效! }

结束;

构造函数究竟创建了什么? 每个对象类型在数据段中都包含一个称为虚拟方法表 (VMT) 的东西。 TVM 包含对象类型的大小,并且对于每个虚拟方法,都包含一个指向执行该方法的代码的指针。 构造函数在对象的调用实现和对象的类型 TCM 之间建立关系。

重要的是要记住,每种对象类型只有一个 TBM。 对象类型的单独实例(即该类型的变量)仅包含与 TBM 的连接,但不包含 TBM 本身。 构造函数将此连接的值设置为 TBM。 正因为如此,在调用构造函数之前你无法开始执行。

5.对象数据字段和形式化方法参数

方法及其对象共享一个公共范围这一事实的含义是,方法的形式参数不能与对象的任何数据字段相同。 这不是面向对象编程施加的一些新限制,而是 Pascal 一直拥有的旧范围规则。 这与防止过程的形式参数与过程的局部变量相同:

程序 CrunchIt(Cru​​nchee: MyDataRec, Crunchby,

错误代码:整数);

VAR

A、B:字符;

错误代码:整数;

开始

.

.

.

一个过程的局部变量和它的形参共享一个共同的作用域,因此不能相同。 如果您尝试编译类似这样的内容,您将得到“错误 4:重复标识符”,当您尝试将正式方法参数设置为方法所属对象的字段名称时,也会发生同样的错误。

情况有些不同,因为将过程头放在数据结构中是对 Turbo Pascal 创新的一种认可,但 Pascal 范围的基本原则没有改变。

LECTURE No. 13. 对象类型的兼容性

1. 封装

对象中代码和数据的组合称为封装。 原则上,可以提供足够多的方法,使对象的用户永远不会直接访问对象的字段。 其他一些面向对象的语言,例如 Smalltalk,需要强制封装,但 Borland Pascal 有一个选择。

例如,TEmployee 和 Thourly 对象的编写方式绝对不需要直接访问它们的内部数据字段:

类型

员工=对象

姓名,标题:字符串[25];

率:真实;

过程 Init(AName, ATitle: string; ARate: Real);

函数GetName:字符串;

函数GetTitle:字符串;

函数GetRate:真实;

函数GetPayAmount:真实;

结束;

Thourly = 对象(TEmployee)

时间:整数;

过程初始化(AName,ATitle:字符串;ARate:

Real, Atime: 整数);

函数GetPayAmount:真实;

结束;

这里只有四个数据字段:名称、标题、速率和时间。 GetName 和 GetTitle 方法分别显示工作人员的姓氏和职位。 GetPayAmount 方法使用费率,如果是工作 Tourly 和 Time 来计算支付给工作的金额。 不再需要直接引用这些数据字段。

假设存在一个 THourly 类型的 AnHourly 实例,我们可以使用一组方法来操作 AnHourly 数据字段,如下所示:

每小时做一次

开始

Init (Aleksandr Petrov, Fork lift operator' 12.95, 62);

{显示姓氏、职位和付款金额}

节目;

结束;

需要注意的是,访问一个对象的字段只能在这个对象的方法的帮助下进行。

2. 扩展对象

不幸的是,标准 Pascal 没有提供任何工具来创建允许您使用完全不同的数据类型的灵活过程。 面向对象编程通过继承解决了这个问题:如果定义了派生类型,则继承父类型的方法,但如果需要,它们可以被覆盖。 要覆盖继承的方法,只需声明一个与继承方法同名的新方法,但具有不同的主体和(如果需要)不同的参数集。

让我们定义一个 TEmployee 的子类型,它代表在以下示例中按小时计酬的员工:

常量

支付周期 = 26; { 付款期限 }

加班阈值 = 80; { 付款期限 }

加班系数 = 1.5; { 每小时收费 }

类型

Thourly = 对象(TEmployee)

时间:整数;

过程初始化(AName,ATitle:字符串;ARate:

Real, Atime: 整数);

函数GetPayAmount:真实;

结束;

过程 THourly.Init(AName, ATitle: string;

Arate: Real, Atime: Integer);

开始

TEmployee.Init(AName, ATitle, ARate);

时间:= ATime;

结束;

函数 Thourly.GetPayAmount:真实;

VAR

超时:整数;

开始

加班:= 时间 - 加班阈值;

如果加班 > 0 那么

GetPayAmount := RoundPay(超时阈值 * 费率 +

加班率 * OvertimeFactor * 率)

其他

GetPayAmount := RoundPay(时间 * 费率)

结束;

一个按小时计酬的人是一名工人:他拥有用于定义 TEmployee 对象的所有东西(姓名、职位、费率),只有小时工收到的金额取决于他在此期间工作了多少小时应付期间。 因此,THourly 也需要一个时间字段。

因为 THourly 定义了一个新的 Time 字段,所以它的初始化需要一个新的 Init 方法来初始化时间和继承的字段。 与其直接给Name、Title、Rate等继承的字段赋值,不如重用TEmployee对象的初始化方法(以第一个THourly Init语句为例)。

调用被覆盖的方法并不是最好的风格。 通常,TEmployee.Init 可能会执行一个重要但隐藏的初始化。

调用重写方法时,您必须确保派生对象类型包含父对象的功能。 此外,父方法的任何更改都会自动影响所有子方法。

在调用 TEmployee.Init 之后,THourly.Init 可以执行它自己的初始化,在这种情况下,它只包括分配在 ATime 中传递的值。

重写方法的另一个示例是 THourly.GetPayAmount 函数,它计算小时工的支付金额。 事实上,每种类型的 TEmployee 对象都有自己的 GetPayAmount 方法,因为工作人员的类型取决于计算方式。 THourly.GetPayAmount 方法应该考虑员工工作了多少小时,是否有加班,加班的增加因素是什么等。

TS工资法。 GetPayAmount 应该只将员工的费率除以每年的付款次数(在我们的示例中)。

单位工人;

接口

常量

支付周期 = 26; {年份}

加班阈值 = 80; {对于每个付款期}

加班系数=1.5; {增加正常付款}

类型

员工=对象

姓名,标题:字符串[25];

率:真实;

过程 Init(AName, ATitle: string; ARate: Real);

函数GetName:字符串;

函数GetTitle:字符串;

函数GetRate:真实;

函数GetPayAmount:真实;

结束;

Thourly = 对象(TEmployee)

时间:整数;

过程初始化(AName,ATitle:字符串;ARate:

Real, Atime: 整数);

函数GetPayAmount:真实;

函数GetTime:真实;

结束;

TSalaried = 对象(TEmployee)

函数GetPayAmount:真实;

结束;

TCommissioned = 对象(TSalaried)

佣金:真实;

销售金额:真实;

构造函数 Init(AName, ATitle: String; ARate,

ACommission, ASalesAmount: Real);

函数GetPayAmount:真实;

结束;

履行

函数 RoundPay(Wages: Real) : Real;

{将支出四舍五入以忽略小于的金额

货币单位}

开始

RoundPay := Trunc(工资 * 100) / 100;

.

.

.

TEmployee 是我们对象层次结构的顶部,包含第一个 GetPayAmount 方法。

函数 TEmployee.GetPayAmount :真实;

开始

运行错误(211); {给出运行时错误}

结束;

该方法给出运行时错误可能会让人感到意外。 如果调用了 Employee.GetPayAmount,程序会出错。 为什么? 因为 TEmployee 是我们对象层次结构的顶部,并没有定义一个真正的工作者; 因此,没有一个 TEmployee 方法以特定的方式被调用,尽管它们可以被继承。 我们所有的员工都是按小时、受薪或计件工作。 运行时错误终止程序执行并输出 211,它对应于与抽象方法调用相关的错误消息(如果程序错误地调用了 TEmployee.GetPayAmount)。

下面是 THourly.GetPayAmount 方法,它考虑了加班费、​​工作时间等因素。

函数 Thourly.GetPayAMount :真实;

VAR

超时:整数;

开始

加班:= 时间 - 加班阈值;

如果加班 > 0 那么

GetPayAmount := RoundPay(超时阈值 * 费率 +

加班率 * OvertimeFactor * 率)

其他

GetPayAmount := RoundPay(时间 * 费率)

结束;

TSalaried.GetPayAmount 方法要简单得多; 打赌

除以付款次数:

函数 TSalaried.GetPayAmount :真实;

开始

GetPayAmount := RoundPay(费率 / PayPeriods);

结束;

如果查看 TCommissioned.GetPayAmount 方法,您会看到它调用 TSalaried.GetPayAmount,计算佣金,并将其添加到 TSalaried 方法返回的值中。 获取支付金额。

函数 TCommissioned.GetPayAmount :真实;

开始

GetPayAmount := RoundPay(TSalaried.GetPayAmount +

佣金*销售额);

结束;

重要提示:虽然可以覆盖方法,但不能覆盖数据字段。 一旦在对象层次结构中定义了数据字段,任何子类型都不能定义具有完全相同名称的数据字段。

3.对象类型的兼容性

继承在一定程度上修改了 Borland Pascal 的类型兼容性规则。 除其他外,派生类型继承其所有父类型的类型兼容性。

这种扩展类型兼容性采用三种形式:

1)对象的实现之间;

2)在指向对象实现的指针之间;

3) 形参与实参之间。

但是,非常重要的是要记住,在所有三种形式中,类型兼容性仅从子级扩展到父级。 换句话说,子类型可以自由地代替父类型使用,反之则不行。

例如,TSalaried 是 TEmployee 的子代,TSosh-missioned 是 TSalaried 的子代。 考虑到这一点,请考虑以下描述:

类型

PEmployee = ^TEmployee;

PSalaried = ^TSalaried;

P 委托 = ^T 委托;

VAR

AnEmployee:TEmployee;

薪俸:TS薪俸;

P委托:T委托;

TEmployeePtr:PEmployee;

TSalariedPtr:PSalaried;

TCommissionedPtr:PCommissioned;

在这些描述下,以下运算符是有效的

作业:

AnEmployee :=A薪金;

A薪水:= ACommissioned;

TCommissionedPtr := ACommissioned;

注意

可以为父对象分配其任何派生类型的实例。 不允许反向分配。

这个概念对 Pascal 来说是新概念,起初可能很难记住订单类型兼容性是什么。 你需要这样想:源必须能够完全填满接收器。 由于继承的属性,派生类型包含其父类型包含的所有内容。 因此,派生类型要么大小完全相同,要么(通常是这种情况)大于其父类型,但绝不会更小。 将父(父)对象分配给子(子)可能会使子对象的某些字段未定义,这是危险的,因此是非法的。

在赋值语句中,只有两种类型共有的字段才会从源复制到目标。 在赋值运算符中:

AnEmployee:= ACommissioned;

只有来自 ACommissioned 的 Name、Title 和 Rate 字段将被复制到 AnEmployee,因为这些是 TCommissioned 和 TEmployee 共有的唯一字段。 类型兼容性也适用于指向对象类型的指针,并遵循与对象实现相同的一般规则。 指向子的指针可以分配给指向父的指针。 鉴于前面的定义,以下指针分配是有效的:

TSalariedPtr:= TCommissionedPtr;

TEmployeePtr:= TSalariedPtr;

TEmployeePtr:= PCommissionedPtr;

请记住,不允许反向分配!

给定对象类型的形式参数(值或变量参数)可以将其自己类型的对象或所有子类型的对象作为其实际参数。 如果您定义这样的过程标头:

程序 CalcFedTax(受害者:TSalaried);

那么实际的参数类型可以是 TSalaried 或 TCommissioned,但不能是 TEmployee。 受害者也可以是可变参数。 在这种情况下,遵循相同的兼容性规则。

备注

值参数和可变参数之间存在根本区别。 值参数是指向作为参数传递的实际对象的指针,而可变参数只是实际参数的副本。 此外,此副本仅包括形式值参数类型中包含的那些字段。 这意味着实际参数从字面上转换为形式参数的类型。 在实际参数保持不变的意义上,可变参数更像是转换为模式。

类似地,如果形参是指向对象类型的指针,则实参可以是指向该对象类型或任何子类型的指针。 给定程序的标题:

程序 Worker.Add(AWorker: PSalared);

有效的实际参数类型将是 PSalaried 或 PCommissioned,但不是 PEmployee。

第 14 讲。汇编器

1. 关于汇编

曾几何时,汇编程序是一种语言,不知道它不可能使计算机做任何有用的事情。 渐渐地情况发生了变化。 出现了更方便的与计算机通信的方式。 但与其他语言不同,汇编程序并没有死;而且,它原则上不能这样做。 为什么? 为了寻找答案,我们将尝试了解汇编语言的一般含义。

简而言之,汇编语言是机器语言的符号表示。 最低硬件级别的机器中的所有进程仅由机器语言的命令(指令)驱动。 从中可以清楚地看出,尽管有通用名称,但每种计算机的汇编语言都是不同的。 这也适用于用汇编程序编写的程序的外观,以及这种语言所反映的思想。

如果没有汇编程序的知识,就不可能真正解决与硬件相关的问题(或者甚至更多与硬件相关的问题,例如提高程序的速度)。

程序员或任何其他用户可以使用任何高级工具,直到程序来构建虚拟世界,甚至可能甚至不怀疑计算机实际上执行的不是编写程序的语言的命令,而是它们转换后的表示以完全不同的语言——机器语言的枯燥乏味的序列命令的形式。 现在想象这样一个用户有一个非标准的问题。 例如,他的程序必须使用一些不寻常的设备或执行其他需要了解计算机硬件原理的操作。 无论程序员编写程序的语言多么好,他都离不开汇编程序。 几乎所有高级语言的编译器都包含将其模块与汇编器中的模块连接起来或支持访问汇编器编程级别的方法,这并非巧合。

一台计算机由若干物理设备组成,每一个物理设备都连接到一个单元,称为系统单元。 要了解它们的功能目的,让我们看一下典型计算机的框图(图 1)。 它并不假装绝对准确,仅旨在显示现代个人计算机元素的用途、关系和典型组成。

米。 一、个人电脑结构图

2.微处理器的软件模型

在当今的计算机市场上,有各种各样不同类型的计算机。 因此,可以假设消费者会有一个问题,即如何评估特定类型(或型号)计算机的功能及其与其他类型(型号)计算机的不同之处。 为了汇集所有描述计算机功能程序控制属性的概念,有一个特殊的术语——计算机体系结构。 计算机体系结构的概念第一次随着第三代机器的出现而被提及,进行对比评测。

只有在找出计算机的哪些部分是可见的并且可以使用该语言进行编程之后,才开始学习任何计算机的汇编语言是有意义的。 这就是所谓的计算机程序模型,其中一部分是微处理器程序模型,它包含三十二个寄存器,或多或少可供程序员使用。

这些寄存器可以分为两大类:

1)6个用户寄存器;

2) 16 个系统寄存器。

3. 用户注册

顾名思义,之所以调用用户寄存器,是因为程序员在编写程序时可以使用它们。 这些寄存器包括(图 2):

1)32个XNUMX位的寄存器可供程序员用来存储数据和地址(它们也被称为通用寄存器(RON)):

eax/ax/ah/al;

ebx/bx/bh/bl;

edx/dx/dh/dl;

ecx/cx/ch/cl;

电子血压计/血压计;

欧洲标准协会/国际标准协会;

编辑/迪;

特别是/ sp。

2)六个段寄存器:cs、ds、ss、es、fs、gs;

3) 状态和控制寄存器:

标志寄存器 eflags/标志;

eip/ip 命令指针寄存器。

米。 2. 用户注册

其中许多寄存器都带有斜线。 这些不是不同的寄存器——它们是一个大型 32 位寄存器的一部分。 它们可以在程序中作为单独的对象使用。

4. 一般登记册

该组的所有寄存器都允许您访问它们的“较低”部分。 只有这些寄存器的低 16 位和 8 位部分可用于自寻址。 这些寄存器的高 16 位不可用作独立对象。

让我们列出属于通用寄存器组的寄存器。 由于这些寄存器物理上位于算术逻辑单元 (AL>) 内部的微处理器中,因此它们也称为 ALU 寄存器:

1) eax/ax/ah/al(累加器寄存器)- 电池。 用于存储中间数据。 在某些命令中,该寄存器的使用是强制性的;

2)ebx/bx/bh/bl(基址寄存器)——基址寄存器。 用于存储某个对象在内存中的基地址;

3) ecx/cx/ch/cl(计数寄存器)——计数器寄存器。 它用于执行某些重复操作的命令中。 它的使用往往是隐含的,隐藏在相应命令的算法中。

例如,循环组织命令,除了将控制权转移到位于某个地址的命令外,还对esx/cx寄存器的值进行分析和减XNUMX;

4)edx/dx/dh/dl(数据寄存器)——数据寄存器。

就像 eax/ax/ah/al 寄存器一样,它存储中间数据。 有些命令需要使用它; 对于某些命令,这是隐式发生的。

以下两个寄存器用于支持所谓的链操作,即顺序处理元素链的操作,每个元素的长度可以是 32、16 或 8 位:

1)esi/si(源索引寄存器)——源索引。

链操作中的这个寄存器包含源链中元素的当前地址;

2) edi/di(Destination Index register)——接收者(recipient)的索引。 链操作中的该寄存器包含目标链中的当前地址。

在硬件和软件层面的微处理器架构中,都支持栈这样的数据结构。 为了在微处理器指令系统中使用堆栈,有特殊的命令,在微处理器软件模型中,有专门的寄存器:

1)esp/sp(堆栈指针寄存器)——堆栈指针寄存器。 包含指向当前堆栈段中堆栈顶部的指针。

2) ebp/bp (Base Pointer register)——栈帧基指针寄存器。 旨在组织对堆栈内数据的随机访问。

对某些指令使用寄存器的硬固定可以更紧凑地对其机器表示进行编码。 了解这些特性将在必要时节省至少几个字节的程序代码占用的内存。

5. 段寄存器

微处理器软件模型中有六个段寄存器:cs、ss、ds、es、gs、fs。

它们的存在是由于英特尔微处理器的组织和使用 RAM 的特殊性。 这在于微处理器硬件以三部分的形式支持程序的结构组织,称为段。 因此,这种内存组织称为分段。

为了指示程序在特定时间点可以访问的段,需要使用段寄存器。 事实上(稍加修正)这些寄存器包含相应段开始的内存地址。 处理机器指令的逻辑是这样构造的,即在获取指令、访问程序数据或访问堆栈时,隐式使用明确定义的段寄存器中的地址。

微处理器支持以下类型的段。

1.代码段。 包含程序命令。 要访问这个段,就要用到cs寄存器(代码段寄存器)——段代码寄存器。 它包含微处理器可以访问的机器指令段的地址(即,这些指令被加载到微处理器流水线中)。

2.数据段。 包含程序处理的数据。 为了访问这个段,使用了ds寄存器(数据段寄存器)——一个段数据寄存器,存储当前程序的数据段的地址。

3.堆栈段。 该段是称为堆栈的内存区域。 微处理器根据以下原则组织与堆栈的工作:首先选择写入该区域的最后一个元素。 为了访问这个段,使用了 ss 寄存器(堆栈段寄存器)——堆栈段寄存器包含堆栈段的地址。

4.附加数据段。 隐含地,执行大多数机器指令的算法假设它们处理的数据位于数据段中,其地址在 ds 段寄存器中。 如果程序没有足够的一个数据段,则它有机会使用另外三个额外的数据段。 但与主数据段不同,主数据段的地址包含在 ds 段寄存器中,当使用附加数据段时,必须在命令中使用特殊的段重定义前缀明确指定它们的地址。 附加数据段的地址必须包含在寄存器 es、gs、fs(扩展数据段寄存器)中。

6. 状态和控制寄存器

微处理器包括几个寄存器,这些寄存器不断地包含有关微处理器本身和其指令当前加载到流水线的程序的状态的信息。 这些寄存器包括:

1)标志寄存器eflags/flags;

2) eip/ip 命令指针寄存器。

使用这些寄存器,您可以获得有关命令执行结果的信息并影响微处理器本身的状态。 让我们更详细地考虑这些寄存器的用途和内容。

1. eflags/flags(标志寄存器)——标志寄存器。 eflags/flags 的位深度为 32/16 位。 该寄存器的各个位具有特定的功能用途,称为标志。 该寄存器的下部与 18086 的标志寄存器完全相同。图 3 显示了 eflags 寄存器的内容。

米。 3. eflags寄存器的内容

根据使用方式的不同,eflags/flags 寄存器的标志可以分为三组:

1) 八个状态标志。

这些标志可能会在机器指令执行后发生变化。 eflags 寄存器的状态标志反映了算术或逻辑运算执行结果的细节。 这使得分析计算过程的状态并使用条件跳转命令和子程序调用对其进行响应成为可能。 表 1 列出了状态标志及其用途。

2) 一个控制标志。

表示为 df(目录标志)。 它位于 eflags 寄存器的第 10 位,供链式命令使用。 df 标志的值决定了这些操作中逐元素处理的方向:从字符串的开头到结尾(df = 0)或反之,从字符串的结尾到开头(df = 1)。 使用 df 标志有一些特殊的命令:eld(删除 df 标志)和 std(设置 df 标志)。 使用这些命令可以让您根据算法调整 df 标志,并确保在对字符串执行操作时计数器自动递增或递减。

3) 五个系统标志。

控制 I/O、可屏蔽中断、调试、任务切换和 8086 虚拟模式。不建议应用程序不必要地修改这些标志,因为这在大多数情况下会导致程序终止。 表 2 列出了系统标志及其用途。

表 1. 状态标志表 2. 系统标志

2. eip/ip(Instraction Pointer register)——指令指针寄存器。 eip/ip 寄存器为 32/16 位宽,包含要执行的下一条指令相对于当前指令段中 cs 段寄存器内容的偏移量。 程序员不能直接访问该寄存器,但其值由各种控制命令加载和更改,包括条件和无条件跳转、调用过程和从过程返回的命令。 中断的发生也会修改 eip/ip 寄存器。

第 15 讲。寄存器

1.微处理器系统寄存器

这些寄存器的名字表明它们在系统中执行特定的功能。 系统寄存器的使用受到严格监管。 是他们提供保护模式。 它们也可以被认为是微处理器架构的一部分,故意让其可见,以便合格的系统程序员可以执行最低级别的操作。

系统寄存器可分为三组:

1)四个控制寄存器;

2) 四个系统地址寄存器;

3) 八个调试寄存器。

2. 控制寄存器

控制寄存器组包括四个寄存器:cr0、cr1、cr2、cr3。 这些寄存器用于一般系统控制。 控制寄存器仅适用于特权级别 0 的程序。

虽然微处理器有四个控制寄存器,但只有三个可用——不包括cr1,其功能尚未定义(保留以备将来使用)。

cr0 寄存器包含系统标志,这些标志控制微处理器的操作模式并在全局范围内反映其状态,而与正在执行的特定任务无关。

系统标志的目的:

1) pe (Protect Enable),位 0 - 启用受保护的操作模式。 该标志的状态显示微处理器在给定时间运行的两种模式中的哪一种——真实(pe = 0)或受保护(pe = 1);

2) mp (Math Present),第 1 位 - 协处理器的存在。 始终为 1;

3) ts (Task Switched),位 3 - 任务切换。 处理器在切换到另一个任务时自动设置该位;

4) am(对齐掩码),第 18 位 - 对齐掩码。 该位启用(am = 1)或禁用(am = 0)对齐控制;

5) cd (Cache Disable),第 30 位 - 禁用高速缓存。

使用该位,您可以禁用(cd =1)或启用(cd = 0)内部缓存(一级缓存)的使用;

6) pg (PaGing),第 31 位 - 启用 (pg = 1) 或禁用 (pg = 0) 分页。

该标志用于内存组织的分页模型。

cr2 寄存器用于 RAM 分页,用于记录当前指令访问当前不在内存中的内存页面中包含的地址时的情况。

在这种情况下,微处理器中出现异常编号 14,并且导致该异常的指令的线性 32 位地址被写入寄存器 cr2。 有了这个信息,异常处理程序14确定所需的页面,将其交换到内存中并恢复程序的正常操作;

cr3 寄存器也用于分页内存。 这就是所谓的一级页目录寄存器。 它包含当前任务页目录的 20 位物理基地址。 该目录包含 1024 个 32 位描述符,每个描述符都包含二级页表的地址。 反过来,每个二级页表包含 1024 个 32 位描述符,用于寻址内存中的页帧。 页框大小为 4 KB。

3. 系统地址寄存器

这些寄存器也称为内存管理寄存器。

它们旨在保护微处理器多任务模式下的程序和数据。 在微处理器保护模式下运行时,地址空间分为:

1) 全局——所有任务通用;

2) 本地 - 为每个任务分开。

这种分离解释了微处理器架构中存在以下系统寄存器:

1)全局描述符表gdtr(Global Descriptor Table Register)寄存器,大小为48位,包含全局描述符表GDT的32位(bits 16-47)基地址和16位(bits 0-15) 限制值,即 GDT 表的字节大小;

2)本地描述符表寄存器ldtr(Local Descriptor Table Register),大小为16位,包含本地描述符表LDT的描述符的所谓选择器,这个选择器是GDT表中的一个指针,它描述了包含本地描述符表 LDT 的段;

3)中断描述​​符表寄存器idtr(Interrupt Descriptor Table Register),大小为48位,包含一个32位(bits 16-47)的IDT中断描述符表基地址和一个16位(bits 0-15) 限制值,即IDT表的字节大小;

4)16位任务寄存器tr(Task Register),和ldtr寄存器一样,包含一个选择器,即指向GDT表中的一个描述符的指针,这个描述符描述了当前的任务段状态(TSS)。 该段是为系统中的每个任务创建的,具有严格规范的结构并包含任务的上下文(当前状态)。 TSS 段的主要目的是保存一个任务在切换到另一个任务时的当前状态。

4.调试寄存器

这是一组非常有趣的用于硬件调试的寄存器。 硬件调试工具最早出现在 i486 微处理器中。 在硬件方面,微处理器包含 XNUMX 个调试寄存器,但实际使用的只有 XNUMX 个。

寄存器 dr0、dr1、dr2、dr3 的宽度为 32 位,用于设置四个断点的线性地址。这种情况下使用的机制如下:将当前程序生成的任何地址与寄存器 dr0...dr3 中的地址进行比较,如果匹配,则生成编号为 1 的调试异常。

寄存器 dr6 称为调试状态寄存器。 该寄存器中的位根据导致最后一个异常编号 1 发生的原因进行设置。

我们列出了这些位及其用途:

1) b0 - 如果该位设置为 1,则最后一个异常(中断)是由于到达寄存器 dr0 中定义的检查点而发生的;

2) b1 - 类似于 b0,但用于寄存器 dr1 中的检查点;

3) b2 - 类似于 b0,但用于寄存器 dr2 中的检查点;

4) bЗ - 类似于 b0,但用于寄存器 dr3 中的检查点;

5) bd (bit 13) - 用于保护调试寄存器;

6) bs (bit 14) - 如果异常 1 是由 eflags 寄存器中的标志 tf = 1 的状态引起的,则设置为 1;

7) 如果异常 15 是由切换到 TSS t = 1 中设置了陷阱位的任务引起的,则 bt(第 1 位)设置为 1。

该寄存器中的所有其他位都用零填充。 异常处理程序 1,根据 dr6 的内容,必须确定异常的原因并采取必要的措施。

寄存器 dr7 称为调试控制寄存器。 它包含四个调试断点寄存器中的每一个的字段,允许您指定应在以下条件下生成中断:

1)检查点注册位置——仅在当前任务或任何任务中。 这些位占用寄存器 dr8 的低 7 位(每个断点(实际上是一个断点)分别由寄存器 dr2、dr0、dr1、dr2 设置 3 位)。

每对的第一位就是所谓的局部分辨率; 如果断点在当前任务的地址空间内,设置它会告诉断点生效。

每对中的第二位定义全局权限,表示给定断点在系统中所有任务的地址空间内有效;

2) 启动中断的访问类型:仅在获取命令、写入或写入/读取数据时。 确定中断发生的这种性质的位位于该寄存器的上部。 大多数系统寄存器都可以通过编程方式访问。

第 16 讲。汇编程序

1. 汇编程序的结构

汇编语言程序是称为内存段的内存块的集合。 一个程序可能由这些块段中的一个或多个组成。 每个段包含一组语言句子,每个句子占据单独的程序代码行。

汇编语句有四种类型:

1) 命令或指令,它们是机器命令的符号类似物。 在翻译过程中,汇编指令被转换为微处理器指令集的相应命令;

2) 宏。 这些是节目文本中以某种方式形式化的句子,在播放过程中被其他句子替换;

3) 指令,指示汇编器翻译器执行某些操作。 指令在机器表示中没有对应物;

4) 包含任何字符的注释行,包括俄文字母。 翻译者忽略注释。

2. 汇编语法

组成程序的句子可以是对应于命令、宏、指令或注释的句法结构。 为了让汇编翻译器识别它们,它们必须按照一定的句法规则构成。 为此,最好使用语言语法的正式描述,如语法规则。 以这种方式描述编程语言的最常见方法是语法图和扩展的 Backus-Naur 形式。 对于实际使用,语法图更方便。 例如,可以使用下图所示的语法图来描述汇编语言语句的语法。

米。 4.汇编语句格式

米。 5.指令格式

米。 6. 命令和宏的格式

在这些图纸上:

1)标签名称——一个标识符,其值是它所表示的程序源代码语句的第一个字节的地址;

2) 名称 - 将该指令与其他同名指令区分开来的标识符。 作为某个指令的汇编程序处理的结果,可以将某些特征分配给该名称;

3)操作码(COP)和指令是相应机器指令、宏指令或翻译器指令的助记符;

4) 操作数——命令、宏或汇编指令的一部分,表示对其执行操作的对象。 汇编器操作数由带有数字和文本常量、变量标签和标识符的表达式描述,使用运算符符号和一些保留字。

如何使用语法图? 这非常简单:您需要做的就是找到并遵循从图表的输入(左)到其输出(右)的路径。 如果存在这样的路径,那么句子或结构在句法上是正确的。 如果没有这样的路径,那么编译器将不会接受这种构造。 使用语法图时,请注意箭头指示的遍历方向,因为路径中可能存在从右到左的路径。 事实上,句法图反映了译者在解析程序输入语句时的逻辑。

编写程序文本时允许使用的字符有:

1) 所有拉丁字母:A - Z,a - z。 在这种情况下,大写和小写字母被认为是等价的;

2) 0 到 9 的数字;

3) 符号 ?、@、S、_、&;

4) 分隔符。

汇编语句由词位构成,词位是句法上不可分割的有效语言符号序列,对翻译者来说是有意义的。

令牌如下。

1. 标识符是用于指定程序对象的有效字符序列,例如操作码、变量名和标签名。 标识符的书写规则如下:一个标识符可以由一个或多个字符组成。 作为字符,您可以使用拉丁字母、数字和一些特殊字符 - _、?、$、@。 标识符不能以数字字符开头。 标识符的长度最多可达 255 个字符,但翻译器只接受前 32 个字符而忽略其余字符。 您可以使用 mv 命令行选项调整可能标识符的长度。 此外,可以告诉翻译器区分大小写字母或忽略它们的区别(这是默认完成的)。 /mu、/ml、/mx 命令行选项用于此目的。

2. 字符链——用单引号或双引号括起来的字符序列。

3. 以下数字系统之一的整数:二进制、十进制、十六进制。 在汇编程序中编写数字时的识别是根据一定的规则进行的:

1) 十进制数字不需要识别任何附加字符,例如25或139;

2) 识别程序源文本中的二进制数,需要在写出组成它们的10010101和XNUMX后加上拉丁文“b”,例如XNUMX b;

3) 十六进制数在书写时有更多约定:

a) 首先,它们由数字 0...9、拉丁字母的小写和大写字母 a、b、c、d、e、Gili D B、C、D、E、E 组成

b) 其次,翻译者可能难以识别十六进制数字,因为它们只能包含数字 0...9(例如 190845)或以拉丁字母开头(例如 efl5)。为了向翻译者“解释”给定的标记不是十进制数字或标识符,程序员必须以特殊方式突出显示十六进制数字。为此,请在组成十六进制数的十六进制数字序列的末尾写入拉丁字母“h”。这是必须的。如果十六进制数以字母开头,则在其前面写入前导零:0 efl5 h。

因此,我们弄清楚了汇编程序的语句是如何构造的。 但这只是最表面的看法。

几乎每个句子都包含对在其上或借助其执行某些动作的对象的描述。 这些对象称为操作数。 它们可以定义如下:操作数是受指令或指令影响的对象(一些值、寄存器或内存单元),或者它们是定义或细化指令或指令动作的对象。

操作数可以与算术、逻辑、按位和属性运算符组合以计算某个值或确定将受给定命令或指令影响的内存位置。

让我们更详细地考虑以下分类中操作数的特征:

1) 常量或直接操作数 - 具有某些固定值的数字、字符串、名称或表达式。 该名称不能是可重定位的,也就是说,它不能依赖于要加载到内存中的程序的地址。 例如,它可以用等于或 = 运算符来定义;

2)地址操作数,通过指定地址的两个组成部分:段和偏移量来指定操作数在内存中的物理位置(图7);

米。 7.地址操作数描述语法

3) 可重定位的操作数——代表一些内存地址的任何符号名称。 这些地址可能指示某些指令的内存位置(如果操作数是标签)或数据(如果操作数是数据段中内存位置的名称)。

可重定位操作数与地址操作数的不同之处在于它们不绑定到特定的物理内存地址。 被移动的操作数地址的段分量是未知的,将在程序加载到内存执行后确定。

地址计数器是一种特定类型的操作数。 用符号 S 表示。这个操作数的特殊性是当汇编翻译器在源程序中遇到这个符号时,它会代入地址计数器的当前值。 地址计数器的值,或有时称为位置计数器的值,是当前机器指令与代码段开头的偏移量。 在列表格式中,第二列或第三列对应于地址计数器(取决于列表中是否存在具有嵌套级别的列)。 如果我们以任何清单为例,那么很明显,当翻译器处理下一条汇编指令时,地址计数器会增加生成的机器指令的长度。 正确理解这一点很重要。 例如,处理汇编指令不会改变计数器。 与汇编命令不同,指令只是对编译器执行某些操作以形成程序的机器表示的指令,对于它们,编译器不会在内存中生成任何结构。

当使用这样的表达式跳转时,请注意使用该表达式的指令本身的长度,因为地址计数器的值对应于该指令的指令段中的偏移量,而不是它后面的指令. 在我们的示例中,jmp 命令占用 2 个字节。 但要小心,指令的长度取决于它使用的操作数。 具有寄存器操作数的指令将比其操作数之一位于内存中的指令短。 在大多数情况下,这些信息可以通过了解机器指令的格式并通过使用指令的目标代码分析列表列来获得;

4) 寄存器操作数只是一个寄存器名称。 在汇编程序中,您可以使用所有通用寄存器和大多数系统寄存器的名称;

5) 基数和索引操作数。 此操作数类型用于实现间接基址、间接索引寻址或它们的组合和扩展;

6) 结构操作数用于访问称为结构的复杂数据类型的特定元素。

记录(类似于结构类型)用于访问某个记录的位字段。

操作数是构成机器指令一部分的基本组件,表示对其执行操作的对象。 在更一般的情况下,操作数可以作为组件包含在称为表达式的更复杂的形式中。 表达式是操作数和运算符的组合,被视为一个整体。 表达式求值的结果可以是某个存储单元的地址或某个常数(绝对)值。

我们已经考虑了可能的操作数类型。 我们现在列出汇编运算符的可能类型和形成汇编表达式的语法规则,并简要说明这些运算符。

1.算术运算符。 这些包括:

1) 一元“+”和“-”;

2)二进制“+”和“-”;

3) 乘法“*”;

4)整数除法“/”;

5) 从除法“mod”中获取余数。

这些运算符位于表 6,7,8 中的优先级 4、XNUMX、XNUMX。

米。 8.算术运算的语法

2. 移位运算符将表达式移位指定的位数(图 9)。

米。 9.移位运算符的语法

3. 比较运算符(返回值“true”或“false”)用于形成逻辑表达式(图 10 和表 3)。 逻辑值“真”对应于数字单位,“假”对应于零。

米。 10.比较运算符的语法

表 3. 比较运算符

4. 逻辑运算符对表达式执行按位运算(图 11)。 表达式必须是绝对的,即这样的,其数值可以由译者计算。

米。 11. 逻辑运算符的语法

5.索引运算符[]。 括号也是一个运算符,翻译器将它们的存在视为将表达式_1 的值添加到这些括号后面的指令,表达式_2 括在括号中(图 12)。

米。 12. 索引运算符语法

请注意,汇编器的文献中采用了以下名称:当文本涉及寄存器的内容时​​,其名称放在括号中。 我们也将遵守这个符号。

6. ptr 类型重新定义运算符用于重新定义或限定由表达式定义的标签或变量的类型(图 13)。

该类型可以采用以下值之一:byte、word、dword、qword、tbyte、near、far。

米。 13. 类型重定义运算符的语法

7. 段重定义操作符“:”(冒号)使物理地址相对于特定段组件进行计算:“段寄存器名称”、来自相应 SEGMENT 指令的“段名称”或“组名”(图 14)。 16)。 在讨论分段时,我们谈到了硬件级别的微处理器支持三种类型的段——代码、堆栈和数据。 这是什么硬件支持? 例如,要选择执行下一条命令,微处理器必须查看段寄存器 cs 的内容,并且只查看它。 正如我们所知,这个寄存器包含指令段开头的(尚未移位的)物理地址。 为了获得特定指令的地址,微处理器需要将 cs 的内容乘以 20(这意味着移位 16 位),并将得到的 XNUMX 位值加到 ip 寄存器的 XNUMX 位内容中。 当微处理器处理机器指令中的操作数时,会发生大致相同的事情。 如果它看到操作数是一个地址(一个有效地址,它只是物理地址的一部分),那么它就知道在哪个段中查找它——默认情况下,它是起始地址存储在段寄存器 ds 中的段.

但是堆栈段呢? 在我们考虑的上下文中,我们对 sp 和 bp 寄存器感兴趣。 如果微处理器将这些寄存器中的一个视为操作数(或其中的一部分,如果操作数是表达式),则默认情况下它形成操作数的物理地址,使用 ss 寄存器的内容作为其段组件。 这是微程序控制单元中的一组微程序,每个微程序执行微处理器机器指令系统中的一条指令。 每个微程序都根据自己的算法工作。 当然,你不能改变它,但你可以稍微修正它。 这是使用可选的机器命令前缀字段完成的。 如果我们就命令的工作方式达成一致,则缺少该字段。 如果我们想对命令的算法进行修改(当然,如果允许特定命令),则有必要形成适当的前缀。

前缀是一个单字节的值,其数值决定了它的用途。 微处理器通过指定值识别该字节是前缀,并且考虑到接收到的指令来执行微程序的进一步工作以纠正其工作。 现在我们对其中之一感兴趣——段替换(重新定义)前缀。 它的目的是向微处理器(实际上是固件)指示我们不想使用默认段。 当然,这种重新定义的可能性是有限的。 命令段不能重新定义,下一个可执行命令的地址由cs:ip对唯一确定。 这里是堆栈和数据的片段 - 这是可能的。 这就是“:”运算符的用途。 处理此语句的汇编器翻译器生成相应的一字节段替换前缀。

米。 14. 段重定义运算符的语法

8. 结构类型命名运算符“.”(点)如果出现在表达式中,也会强制编译器执行某些计算。

9.获取表达式seg地址的段分量的操作符返回表达式段的物理地址(图15),可以是标签、变量、段名、组名或一些符号名.

米。 15. 段组件接收操作符的语法

10. 获取表达式偏移量的运算符允许您获取表达式的偏移量值(图 16),相对于定义表达式的段的开头,以字节为单位。

米。 16. offset get 操作符的语法

与高级语言一样,在评估表达式时,汇编运算符的执行是根据它们的优先级执行的(表 4)。 具有相同优先级的操作从左到右依次执行。 可以通过放置具有最高优先级的括号来更改执行顺序。

表 4. 运算符及其优先级

3. 分割指令

在前面的讨论过程中,我们发现了在汇编语言程序中编写指令和操作数的所有基本规则。 如何正确格式化命令序列以便翻译器可以处理它们并且微处理器可以执行它们的问题仍然悬而未决。

在考虑微处理器的架构时,我们了解到它有六个段寄存器,通过它们可以同时工作:

1) 一个代码段;

2) 一个堆栈段;

3) 一个数据段;

4) 带有三个附加数据段。

再次回忆一下,段物理上是由命令和(或)数据占用的内存区域,其地址是相对于相应段寄存器中的值计算的。

汇编器中段的句法描述是图 17 所示的结构:

米。 17.段描述语法

需要注意的是,段的功能比简单地将程序分解为代码块、数据块和堆栈要广泛得多。 分段是与模块化编程概念相关的更通用机制的一部分。 它涉及由编译器创建的目标模块设计的统一,包括来自不同编程语言的目标模块。 这允许您组合用不同语言编写的程序。 SEGMENT 指令中的操作数旨在实现此类联合的各种选项。

让我们更详细地考虑它们。

1.段对齐属性(对齐类型)告诉链接器确保段的开头放置在指定的边界上。 这一点很重要,因为正确对齐可以使 i80x86 处理器上的数据访问更快。 该属性的有效值如下:

1) BYTE - 不执行对齐。 一个段可以从任何内存地址开始;

2) WORD - 段的起始地址是 0 的倍数,即物理地址的最后一位(最低有效位)为 XNUMX(与字边界对齐);

3) DWORD - 段的起始地址是四的倍数,即最后两位(最低有效位)为 0(双字边界对齐);

4) PARA - 段的起始地址是 16 的倍数,即地址的最后一个十六进制数字必须是 Oh(与段落边界对齐);

5) PAGE - 段的起始地址是 256 的倍数,即最后两个十六进制数字必须是 00h(与 256 字节页面的边界对齐);

6) MEMPAGE - 段从 4 KB 的倍数的地址开始,即最后三个十六进制数字必须是 OOOh(下一个 4 KB 内存页的地址)。 默认对齐类型为 PARA。

2. combine 段属性(组合类型)告诉链接器如何组合不同模块的同名段。 段组合属性值可以是:

1) PRIVATE - 该段不会与该模块之外的其他同名段合并;

2) PUBLIC - 使链接器连接所有具有相同名称的段。 新的合并段将是完整且连续的。 对象的所有地址(偏移量),这可能取决于命令和数据段的类型,将相对于这个新段的开头进行计算;

3) COMMON - 将具有相同名称的所有段放在相同的地址。 具有给定名称的所有段将重叠并共享内存。 结果段的大小将等于最大段的大小;

4) AT xxxx——将段定位在段落的绝对地址(段落是内存量,16的倍数;因此,段落地址的最后一个十六进制数字为0)。 段落的绝对地址由 xxx 给出。 考虑到组合属性,链接器将段放置在给定的内存地址(例如,这可用于访问视频内存或 ROM> 区域)。 从物理上讲,这意味着当加载到内存中时,段将从段落的这个绝对地址开始定位,但是要访问它,必须将属性中指定的值加载到相应的段寄存器中。 这样定义的段中的所有标签和地址都相对于给定的绝对地址;

5) STACK - 堆栈段的定义。 使链接器连接所有具有相同名称的段,并计算这些段中相对于 ss 寄存器的地址。 组合型STACK(栈)与组合型PUBLIC类似,只是ss寄存器是栈段的标准段寄存器。 sp 寄存器设置为连接堆栈段的末尾。 如果未指定堆栈段,链接器将发出未找到堆栈段的警告。 如果已经创建了一个堆栈段并且没有使用组合的 STACK 类型,则程序员必须将段地址显式加载到 ss 寄存器中(类似于 ds 寄存器)。

组合属性默认为 PRIVATE。

3. 段类属性(类类型)是一个带引号的字符串,它帮助链接器在从多个模块段组装程序时确定适当的段顺序。 链接器在内存中将所有具有相同类名的段组合在一起(类名通常可以是任何名称,但最好能反映段的功能)。 类名的典型用途是将程序的所有代码段组合在一起(通常使用“代码”类)。 使用类类型机制,您还可以对已初始化和未初始化的数据段进行分组。

4. 段大小属性。 对于 i80386 和更高版本的处理器,段可以是 16 位或 32 位。 这主要影响段的大小和在其中形成物理地址的顺序。 该属性可以采用以下值:

1) USE16 - 这意味着该段允许 16 位寻址。 形成物理地址时,只能使用 16 位的偏移量。 因此,这样的段最多可以包含 64 KB 的代码或数据;

2)USE32 - 该段将是 32 位的。 形成物理地址时,可以使用 32 位偏移量。 因此,这样的段最多可以包含 4 GB 的代码或数据。

所有段本身都是平等的,因为 SEGMENT 和 ENDS 指令不包含有关段功能用途的信息。 为了将它们用作代码、数据或堆栈段,有必要提前通知翻译器,为此使用了一个特殊的 ASSUME 指令,其格式如图 18 所示。 18. 该指令告诉翻译器哪个段绑定到哪个段寄存器。 反过来,这将允许翻译器正确绑定在段中定义的符号名称。 段到段寄存器的绑定是使用该指令的操作数执行的,其中段名必须是段的名称,在程序的源文本中由 SEGMENT 指令或无关键字定义。 如果仅将关键字 nothing 用作操作数,则取消之前的段寄存器分配,并同时取消所有六个段寄存器。 但是可以使用关键字 nothing 代替段名参数; 在这种情况下,名称为段名的段与相应的段寄存器之间的连接将被选择性地断开(见图XNUMX)。

米。 18.假设指令

对于包含一段代码、数据和堆栈的简单程序,我们希望简化其描述。 为此,翻译器 MASM 和 TASM 引入了使用简化分段指令的能力。 但是这里出现了一个问题,即必须以某种方式补偿无法直接控制段的放置和组合。 为此,与简化分段指令一起,他们开始使用用于指定 MODEL 内存模型的指令,该指令部分开始控制段的放置并执行 ASSUME 指令的功能(因此,在使用简化分段指令时, ASSUME 指令可以省略)。 该指令将段(在使用简化分段指令的情况下,具有预定义名称)与段寄存器绑定(尽管您仍然必须显式初始化 ds)。

MODEL 指令的语法如图 19 所示。

米。 19. MODEL 指令的语法

MODEL 指令的强制参数是内存模型。 该参数定义了 POU 的内存分段模型。 假设程序模块只能有某些类型的段,这些段由我们前面提到的简化段描述指令定义。 这些指令如表 5 所示。

表 5. 简化的段定义指令

某些指令中存在 [name] 参数表明可以定义该类型的多个段。 另一方面,多种数据段的存在是由于需要保证与一些高级语言的编译器的兼容性,这些编译器为已初始化和未初始化的数据以及常量创建不同的数据段。

使用 MODEL 指令时,翻译器提供了几个可在程序操作期间访问的标识符,以获得有关给定存储器模型的某些特征的信息(表 7)。 让我们列出这些标识符及其值(表6)。

表 6. 由 MODEL 指令创建的标识符

我们现在可以完成对 MODEL 指令的讨论。 MODEL 指令的操作数用于指定定义程序段集、数据和代码段的大小以及链接段和段寄存器的方法的内存模型。 表 7 显示了 MODEL 指令的“内存模型”参数的一些值。

表 7. 内存模型

MODEL 指令的“修饰符”参数可以阐明使用所选内存模型的一些特征(表 8)。

表 8. 内存模型修饰符

可选参数“语言”和“语言修饰符”定义了过程调用的一些特性。 当用各种编程语言编写和链接程序时,就需要使用这些参数。

我们描述的标准和简化的分段指令并不相互排斥。 当程序员希望完全控制段在内存中的放置以及它们与来自其他模块的段的组合时,使用标准指令。

简化指令对于简单程序和旨在与用高级语言编写的程序模块链接的程序很有用。 这允许链接器通过标准化链接和管理来有效地链接来自不同语言的模块。

LECTURE No. 17. 汇编器中的命令结构

1.机器指令结构

机器命令是对微处理器的指示,根据某些规则编码,以执行某些操作或动作。 每个命令都包含定义:

1)怎么办? (这个问题的答案由称为操作代码(COP)的命令元素给出。);

2) 需要对其进行操作的对象(这些元素称为操作数);

3)怎么办? (这些元素称为操作数类型,通常是隐式指定的。)

图 20 所示的机器指令格式是最通用的。 机器指令的最大长度为 15 个字节。 一个真正的命令可能包含的字段数量要少得多,最多只有一个 KOP。

米。 20.机器指令格式

让我们描述机器指令字段的用途。

1. 前缀。

可选的机器指令元素,每个为 1 字节或可以省略。 在内存中,前缀位于命令之前。 前缀的目的是修改命令执行的操作。 应用程序可以使用以下类型的前缀:

1) 段替换前缀。 明确指定在该指令中使用哪个段寄存器来寻址堆栈或数据。 前缀覆盖默认的段寄存器选择。 段替换前缀的含义如下:

a) 2eh - 段 cs 的替换;

b) 36h - 段 ss 的更换;

c) 3eh - 段 ds 的替换;

d) 26h - 更换段 es;

e) 64h - 段 fs 的更换;

e) 65h - gs 段的更换;

2) 地址位数前缀指定地址的位数(32 位或 16 位)。 每条使用地址操作数的指令都被分配了该操作数地址的位宽。 该地址可以是 16 位或 32 位。 如果该命令的地址长度为 16 位,这意味着该命令包含一个 16 位偏移量(图 20),它对应于地址操作数相对于某个段的开头的 16 位偏移量。 在图 21 的上下文中,这个偏移量称为有效地址。 如果地址为 32 位,则表示该命令包含 32 位偏移量(图 20),它对应于地址操作数相对于段开头的 32 位偏移量,其值形成 32 - 段中的位偏移量。 地址位数前缀可用于更改默认地址位数。 此更改只会影响前缀前面的命令;

米。 21. 实模式下物理地址的形成机制

3)操作数位宽前缀与地址位宽前缀类似,但表示指令操作的操作数位长(32位或16位)。 默认设置地址和操作数位宽属性的规则是什么?

在实模式和虚拟 18086 模式下,这些属性的值都是 16 位。 在保护模式下,属性值取决于可执行段描述符中D位的状态。 如果D = 0,则默认属性值为16位; 如果 D = 1,则为 32 位。

操作数宽度 66h 和地址宽度 67h 的前缀值。 使用实模式地址位前缀,您可以使用 32 位寻址,但请注意 64 KB 段大小限制。 与地址宽度前缀类似,您可以使用实模式操作数宽度前缀来处理 32 位操作数(例如,在算术指令中);

4)重复前缀与链式命令(行处理命令)一起使用。 此前缀“循环”命令以处理链中的所有元素。 命令系统支持两种类型的前缀:

a) 无条件 (rep - OOh),强制链式命令重复一定次数;

b) 有条件的 (repe/repz - OOh, repne/repnz - 0f2h),它在循环时检查一些标志,并且作为检查的结果,可以提前退出循环。

2.操作码。

描述命令执行的操作的必需元素。 许多命令对应几个操作码,每个操作码决定了操作的细微差别。 机器指令的后续字段确定了操作中涉及的操作数的位置以及它们的使用细节。 这些字段的考虑与在机器指令中指定操作数的方式有关,因此将在后面进行。

3.寻址方式字节modr/m。

该字节的值决定了使用的操作数地址形式。 操作数可以在内存中的一个或两个寄存器中。 如果操作数在内存中,则 modr/m 字节指定用于计算其有效地址的组件(偏移量、基址和索引寄存器)(图 21)。 在保护模式下,sib 字节(Scale-Index-Base)还可用于确定操作数在内存中的位置。 modr/m 字节由三个字段组成(图 20):

1)mod字段决定了命令中操作数地址占用的字节数(图20,命令中的offset字段)。 mod字段与r/m字段配合使用,指定如何修改“指令偏移”操作数的地址。 例如,如果 mod = 00,这意味着命令中没有偏移字段,操作数的地址由基址和(或)索引寄存器的内容决定。 将使用哪些寄存器来计算有效地址由该字节的值决定。 如果 mod = 01,这意味着偏移字段存在于命令中,占用 1 个字节并由基址和(或)索引寄存器的内容修改。 如果 mod = 10,这意味着命令中存在偏移字段,占用 2 或 4 个字节(取决于默认或前缀定义的地址大小),并由基址和/或索引寄存器的内容修改。 如果 mod = 11,这意味着内存中没有操作数:它们在寄存器中。 当指令中使用立即操作数时,使用相同的 mod 字节值;

2) reg/cop 字段确定位于命令中代替第一个操作数的寄存器,或操作码的可能扩展;

3) r/m 字段与 mod 字段一起使用,并确定位于命令中第一个操作数位置的寄存器(如果 mod = 11),或者用于计算有效地址的基址和索引寄存器(连同命令中的偏移字段)。

4. 字节尺度-索引-基数(byte sib)。

用于扩展寻址操作数的可能性。 机器指令中sib字节的存在由mod字段的值01或10之一和r/m = 100字段的值的组合来表示。sib字节由三个字段组成:

1)规模领域ss。 该字段包含索引组件索引的比例因子,它占据 sib 字节的下 3 位。 ss 字段可以包含以下值之一:1、2、4、8。

计算有效地址时,变址寄存器的内容会乘以这个值;

2)索引字段。 用于存放索引寄存器号,用于计算操作数的有效地址;

3) 基础字段。 用于存储基址寄存器号,也用于计算操作数的有效地址。 几乎所有通用寄存器都可以用作基址和变址寄存器。

5. 命令中的偏移量字段。

一个 8 位、16 位或 32 位有符号整数,全部或部分(根据上述考虑)表示操作数的有效地址的值。

6.立即操作数的字段。

一个可选字段,它是一个 8 位、16 位或 32 位立即操作数。 当然,该字段的存在反映在 modr/m 字节的值中。

2. 指定指令操作数的方法

操作数在固件级别隐式设置

在这种情况下,指令明确地不包含操作数。 命令执行算法使用一些默认对象(寄存器、eflags 中的标志等)。

例如,cli 和 sti 命令隐式使用 eflags 寄存器中的 if 中断标志​​,而 xlat 命令隐式访问 al 寄存器和内存中由 ds:bx 寄存器对指定的地址处的一行。

操作数在指令本身中指定(立即操作数)

操作数在指令代码中,也就是说,它是指令代码的一部分。 为了在命令中存储这样的操作数,分配了一个长达 32 位的字段(图 20)。 直接操作数只能是第二个(源)操作数。 目标操作数可以在内存中,也可以在寄存器中。

例如: mov ax,0ffffti 将十六进制常数 ffff 移动到寄存器 ax 中。 add sum, 2 命令将地址 sum 处的字段内容与整数 2 相加,并将结果写入第一个操作数的位置,即写入内存。

操作数在其中一个寄存器中

寄存器操作数由寄存器名称指定。 可以使用寄存器:

1)32位寄存器EAX、EBX、ECX、EDX、ESI、EDI、ESP、EUR;

2)16位寄存器AX、BX、CX、DX、SI、DI、SP、BP;

3)8位寄存器AH、AL、BH、BL、CH、CL、DH、DL;

4)段寄存器CS、DS、SS、ES、FS、GS。

例如,add ax,bx 指令将寄存器 ax 和 bx 的内容相加,并将结果写入 bx。 dec si 命令将 si 的内容减 1。

操作数在内存中

这是指定操作数的最复杂同时也是最灵活的方式。 它允许您实现以下两种主要类型的寻址:直接和间接。

反过来,间接寻址具有以下变体:

1)间接基址寻址; 它的另一个名字是寄存器间接寻址;

2) 带偏移的间接基址寻址;

3) 带偏移的间接索引寻址;

4)间接基索引寻址;

5) 带偏移的间接基索引寻址。

操作数是一个 I/O 端口

除了 RAM 地址空间,微处理器还维护一个 I/O 地址空间,用于访问 I/O 设备。 I/O 地址空间为 64 KB。 为该空间中的任何计算机设备分配地址。 此空间内的特定地址值称为 I/O 端口。 在物理上,I/O 端口对应一个硬件寄存器(不要与微处理器寄存器混淆),使用特殊的汇编指令 in 和 out 来访问它。

例如:

在 60 小时内; 从端口 60h 输入一个字节

I/O 端口寻址的寄存器可以是 8,16、32 或 XNUMX 位宽,但寄存器位宽对于特定端口是固定的。 in 和 out 命令对固定范围的对象进行操作。 所谓的累加器寄存器EAX、AX、AL被用作信息的来源或接收者。 寄存器的选择由端口的位数决定。 端口号可以指定为 in 和 out 指令中的立即操作数,也可以指定为 DX 寄存器中的值。 最后一种方法允许您在程序中动态确定端口号。

操作数在栈上

指令可能根本没有操作数,可能有一个或两个操作数。 大多数指令需要两个操作数,一个是源操作数,另一个是目标操作数。 重要的是,一个操作数可以位于寄存器或内存中,而第二个操作数必须位于寄存器中或直接位于指令中。 立即操作数只能是源操作数。 在双操作数机器指令中,可能有以下操作数组合:

1)注册——注册;

2)寄存器——内存;

3)内存——寄存器;

4)立即操作数——寄存器;

5)立即操作数——内存。

此规则在以下方面有例外:

1)可以将数据从内存移动到内存的链式命令;

2)堆栈命令,可以将数据从内存传输到同样在内存中的堆栈;

3) 乘法类型的命令,除了命令中指定的操作数外,还使用第二个隐式操作数。

在列出的操作数组合中,最常用的是寄存器 - 内存和内存 - 寄存器。 鉴于它们的重要性,我们将更详细地考虑它们。 我们将通过汇编指令的示例进行讨论,这些示例将展示在应用一种或另一种类型的寻址时汇编指令的格式如何变化。 对此,再看图21,它显示了在微处理器的地址总线上形成物理地址的原理。 可以看出,操作数的地址是由两个分量之和构成的——段寄存器移位4位的内容和16位有效地址,一般计算为三个分量之和:基数,偏移量和索引。

3.寻址方式

我们列出并考虑内存中主要寻址操作数类型的特征:

1) 直接寻址;

2)间接基本(寄存器)寻址;

3) 带偏移的间接基本(寄存器)寻址;

4) 带偏移的间接索引寻址;

5)间接基索引寻址;

6) 带偏移的间接基索引寻址。

直接寻址

这是在内存中寻址操作数的最简单形式,因为有效地址包含在指令本身中,并且没有使用额外的源或寄存器来形成它。 有效地址直接取自机器指令偏移字段(参见图 20),其大小可以是 8、16、32 位。 该值唯一标识数据段中的字节、字或双字。

直接寻址可以有两种类型。

相对直接寻址

用于条件跳转指令,表示相对跳转地址。 这种转换的相关性在于机器指令的偏移字段包含一个 8 位、16 位或 32 位值,作为指令操作的结果,该值将添加到ip/eip 指令指针寄存器。 作为这种相加的结果,获得了执行转移的地址。

绝对直接寻址

在这种情况下,有效地址是机器指令的一部分,但该地址仅由指令中偏移字段的值构成。 为了形成内存中操作数的物理地址,微处理器将该字段与移位了 4 位的段寄存器的值相加。 这种寻址的几种形式可以在汇编指令中使用。

但这种寻址很少使用——程序中常用的单元格被分配了符号名称。 在翻译过程中,汇编器计算这些名称的偏移值并将其代入它在“指令偏移”字段中生成的机器指令中。 结果,机器指令直接寻址它的操作数,事实上,在它的一个字段中具有有效地址的值。

其他类型的寻址是间接的。 这些类型寻址名称中的“间接”一词意味着只有一部分有效地址可以在指令本身中,其其余部分在寄存器中,它们的内容由 modr/m 字节和,可能是同胞字节。

间接基本(寄存器)寻址

通过这种寻址,操作数的有效地址可以在任何通用寄存器中,除了 sp / esp 和 bp / ebp (这些是用于处理堆栈段的特定寄存器)。 从句法上讲,在命令中,这种寻址模式通过将寄存器名称括在方括号 [] 中来表示。 例如,指令 mov ax, [ecx] 将位于数据段地址处的字内容放入寄存器 ax 中,偏移量存储在寄存器 esx 中。 由于寄存器的内容在程序运行过程中很容易改变,这种寻址方法允许您为某些机器指令动态分配操作数的地址。 此属性非常有用,例如,用于组织循环计算和处理各种数据结构(如表或数组)。

带偏移的间接基址(寄存器)寻址

这种类型的寻址是对前一种寻址的补充,旨在访问具有相对于某个基地址的已知偏移量的数据。 这种寻址方式便于访问数据结构的元素,当预先知道元素的偏移量时,在程序开发阶段,必须动态计算结构的基(起始)地址,在程序执行阶段。 通过修改基址寄存器的内容,可以访问相同类型数据结构的不同实例中的同名元素。

例如,指令 mov ax,[edx+3h] 将字从内存区域传送到位于地址的寄存器 ax:edx + 3h 的内容。

mov ax,mas[dx] 指令将一个字移动到寄存器 ax 的地址:dx 的内容加上标识符 mas 的值(请记住,编译器为每个标识符分配一个值,该值等于该标识符与数据段的开头)。

带偏移的间接索引寻址

这种寻址与带有偏移的间接基址寻址非常相似。 这里也使用通用寄存器之一来形成有效地址。 但是索引寻址有一个有趣的特性,它非常便于处理数组。 它与索引寄存器内容的所谓缩放的可能性有关。 这是什么?

请看图 20。我们对 sib 字节感兴趣。 在讨论这个字节的结构时,我们注意到它由三个字段组成。 这些字段之一是 ss 比例字段,索引寄存器的内容乘以该字段。

例如,在mov ax,mas[si*2]指令中,第二个操作数的有效地址的值由表达式mas+(si)*2计算得出。 由于汇编器没有组织数组索引的方法,程序员必须自己组织它。

具有扩展能力有助于解决这个问题,但前提是数组元素的大小为 1、2、4 或 8 个字节。

间接基索引寻址

使用这种类型的寻址,有效地址由两个通用寄存器的内容之和形成:基址和索引。 这些寄存器可以是任何通用寄存器,并且经常使用索引寄存器内容的缩放。

带偏移量的间接基索引寻址

这种寻址是间接索引寻址的补充。 有效地址由三个部分的总和构成:基址寄存器的内容、索引寄存器的内容和指令中偏移字段的值。

例如,mov eax,[esi+5] [edx] 指令将一个双字移动到地址为 (esi) + 5 + (edx) 的 eax 寄存器。

add ax,array[esi] [ebx] 命令将寄存器 ax 的内容添加到地址处字的内容中:标识符数组的值 + (esi) + (ebx)。

第18讲。团队

1.数据传输命令

为方便实际应用和反映其具体情况,更便于根据其功能目的考虑该组命令,根据其可分为以下几组命令:

1) 通用数据传输;

2) 输入输出到端口;

3) 使用地址和指针;

4) 数据转换;

5) 使用堆栈。

通用数据传输命令

该组包括以下命令:

1) mov 是基本的数据传输命令。 它实现了多种运输选项。 注意这个命令的细节:

a) mov 命令不能从一个内存区域传输到另一个内存区域。 如果出现这种需求,那么任何当前可用的通用寄存器都应该用作中间缓冲区;

b) 不可能将值直接从内存加载到段寄存器中。 因此,要执行这样的加载,您需要使用中间对象。 这可能是一个通用寄存器或堆栈;

c) 你不能将一个段寄存器的内容传送到另一个段寄存器。 这是因为命令系统中没有对应的操作码。 但经常需要采取这种行动。 您可以使用与中间寄存器相同的通用寄存器来执行此类传输;

d) 不能将段寄存器 CS 用作目标操作数。 原因很简单。 事实上,在微处理器的体系结构中,cs:ip 对总是包含接下来应该执行的命令的地址。 用 mov 命令改变 CS 寄存器的内容实际上意味着跳转操作,而不是传输,这是不可接受的。 2) xchg - 用于双向数据传输。 对于这个操作,当然可以使用多个 mov 指令的序列,但是由于交换操作经常使用,微处理器指令系统的开发人员认为有必要引入单独的 xchg 交换指令。 自然,操作数必须是同一类型。 不允许(对于所有汇编程序指令)相互交换两个存储单元的内容。

端口 I/O 命令

请看图 22。它显示了高度简化的计算机硬件控制概念图。

米。 22.计算机设备控制概念图

从图 22 可以看出,最低级别是 BIOS 级别,硬件直接通过端口处理。 这实现了设备独立的概念。 更换硬件时,只需更正相应的 BIOS 功能,将它们重新定向到新地址和端口逻辑。

从根本上说,直接通过端口管理设备很容易。 有关端口号、它们的位深度、控制信息格式的信息在设备的技术描述中给出。 您只需要知道您的操作的最终目标、特定设备的工作算法以及对其端口进行编程的顺序,即实际上您需要知道您需要发送什么以及以什么顺序发送到端口(写入时)或从中读取(读取时)以及如何解释此信息。 为此,微处理器命令系统中存在的两个命令就足够了:

1) 在累加器中,port_number - 从端口号为 port_number 的端口输入到累加器;

2) out port, accumulator——将累加器的内容输出到编号为port_number的端口。

用于处理地址和内存指针的命令

在汇编程序中编写程序时,需要对内存中操作数的地址进行大量工作。 为了支持这种操作,有一组特殊的命令,其中包括以下命令:

1) lea destination, source——有效地址加载;

2) ids destination, source——将指针加载到数据段寄存器ds中;

3) les destination, source——将指针加载到附加数据段es的寄存器中;

4) lgs destination, source——将指针加载到附加数据段gs的寄存器中;

5) lfs destination, source——将指针加载到附加数据段fs的寄存器中;

6) lss 目标,源 - 将指针加载到堆栈段寄存器 ss。

lea 命令类似于 mov 命令,因为它也执行移动。 但是lea指令传输的不是数据,而是数据的有效地址(即数据从数据段开头的偏移量)到目的操作数所指示的寄存器。

通常,要在程序中执行某些操作,仅知道有效数据地址的值是不够的,还需要有一个指向数据的完整指针。 一个完整的数据指针由一个段组件和一个偏移量组成。 该组的所有其他命令都允许您在一对寄存器中获得指向内存中操作数的完整指针。 在这种情况下,放置地址的段组件的段寄存器的名称由操作码确定。 因此,偏移量被放置在由目标操作数指示的通用寄存器中。

但对于源操作数,并非一切都如此简单。 实际上,在命令中,作为源,您不能直接指定内存中操作数的名称,我们希望接收指向该操作数的指针。 首先,需要获取某个内存区域的完整指针的值,并在get命令中指定该区域名称的完整地址。 要执行此操作,您需要记住用于保留和初始化内存的指令。

在应用这些指令时,当在操作数字段中指定另一个数据定义指令的名称(实际上是变量的名称)时,可能会出现特殊情况。 在这种情况下,这个变量的地址是在内存中形成的。 将生成哪个地址(有效或完整)取决于应用的指令。 如果是dw,那么在内存中只形成有效地址的16位值;如果是dd,则将完整地址写入内存。 该地址在内存中的位置如下:低位字包含偏移量,高位字包含地址的 16 位段分量。

例如,在用字符链组织工作时,可以方便地将其起始地址放在某个寄存器中,然后在循环中修改该值,以便顺序访问链中的元素。

尤其是在使用链时,需要使用命令来获取内存中的完整数据指针,即段的地址和段内的偏移值。

数据转换命令

许多微处理器指令可以归属于该组,但它们中的大多数具有某些特性,需要将它们归属于其他功能组。 因此,在整套微处理器命令中,只有一个命令可以直接归属于数据转换命令:xlat [address_of_transcoding_table]

这是一个非常有趣和有用的团队。 其效果是它用内存表中的另一个字节替换 al 寄存器中的值,该内存表位于 recoding_table_address 操作数指定的地址。

“表”这个词是非常有条件的,其实它只是一串字节。 字符串中将替换 al 寄存器内容的字节地址由 sum (bx) + (al) 确定,即 al 的内容充当字节数组中的索引。

使用 xlat 命令时,请注意以下细微之处。 尽管该命令指定要从中检索新值的字节串的地址,但必须将该地址预加载(例如,使用 lea 命令)到 bx 寄存器中。 因此,lookup_table_address 操作数并不是真正需要的(操作数的可选性通过将其括在方括号中来显示)。 至于字节串(转码表),它是一个大小为 1 到 255 个字节的内存区域(8 位寄存器中无符号数的范围)。

堆栈命令

该组是一组专门的命令,专注于组织灵活高效的堆栈工作。

栈是专门为临时存储程序数据而分配的一块内存区域。 堆栈的重要性取决于程序结构中为其提供了一个单独的段这一事实。 如果程序员忘记在其程序中声明堆栈段,tlink 链接器将发出警告消息。

堆栈有三个寄存器:

1) ss——堆栈段寄存器;

2) sp/esp——堆栈指针寄存器;

3) bp/ebp - 堆栈帧基指针寄存器。

堆栈大小取决于微处理器的操作模式,限制为 64 KB(或在保护模式下为 4 GB)。

一次只有一个堆栈可用,其段地址包含在 SS 寄存器中。 该堆栈称为当前堆栈。 为了引用另一个堆栈(“切换堆栈”),需要将另一个地址加载到 ss 寄存器中。 处理器自动使用 SS 寄存器来执行堆栈上的所有指令。

我们列出了使用堆栈的更多功能:

1)栈上数据的读写是按照LIFO原则进行的,

2)随着数据写入堆栈,后者向低地址增长。 此功能嵌入在使用堆栈的命令算法中;

3)当使用esp/sp和ebp/bp寄存器进行内存寻址时,汇编器自动认为其中包含的值是相对于ss段寄存器的偏移量。

通常,堆栈的组织方式如图 23 所示。

米。 23.栈组织概念图

SS、ESP/SP 和 EUR/BP 寄存器设计为与堆栈一起使用。 这些寄存器的使用方式很复杂,每个寄存器都有自己的功能用途。

ESP/SP 寄存器总是指向栈顶,也就是说,它包含最后一个元素被压入栈的偏移量。 堆栈指令隐式地更改此寄存器,使其始终指向压入堆栈的最后一个元素。 如果堆栈为空,则 esp 的值等于为堆栈分配的段的最后一个字节的地址。 当一个元素被压入堆栈时,处理器会递减 esp 寄存器的值,然后将该元素写入新顶点的地址。 当从堆栈中弹出数据时,处理器复制位于顶点地址的元素,然后递增堆栈指针寄存器 esp 的值。 因此,事实证明堆栈在减少地址的方向上向下增长。

如果我们需要访问不在栈顶而是在栈内的元素怎么办? 为此,请使用 EBP 寄存器。EBP 寄存器是堆栈帧基址指针寄存器。

例如,进入子程序时的一个典型技巧是通过将所需参数压入堆栈来传递它们。 如果子程序也在积极地使用堆栈,那么访问这些参数就会有问题。 解决方法是在将必要的数据写入堆栈后,将堆栈顶部的地址保存在堆栈的帧(基)指针,EUR 寄存器中。 EUR 中的值稍后可用于访问传递的参数。

堆栈的开头位于较高的内存地址。 在图 23 中,此地址由对 ss:fffF 表示。 此处有条件地给出 wT 的偏移。 实际上,这个值是由程序员在他的程序中描述堆栈段时指定的值决定的。

为了组织使用堆栈的工作,有用于写入和读取的特殊命令。

1. push source——将source的值写入栈顶。

有趣的是这个命令的算法,它包括以下动作(图 24):

1) (sp) = (sp) - 2; sp的值减2;

2) 将来自源的值写入 ss:sp 对指定的地址。

米。 24. push 命令的工作原理

2.弹出赋值——将栈顶的值写入目标操作数指定的位置。 该值从堆栈顶部“弹出”。 pop 命令的算法与 push 命令的算法相反(图 25):

1) 将栈顶的内容写入目标操作数指示的位置;

2) (sp) = (sp) + 2; 增加 sp 的值。

米。 25. pop 命令的工作原理

3. pusha - 对堆栈的组写入命令。 通过该命令,寄存器 ax、cx、dx、bx、sp、bp、si、di 被顺序写入堆栈。 注意sp的原始内容是写的,也就是发出pusha命令之前的内容(图26)。

米。 26. pusha 命令的工作原理

4. pushaw 几乎是 pusha 命令的同义词,有什么区别? bitness 属性可以是 use16 或 use32。 让我们看看 pusha 和 pushaw 命令如何处理这些属性:

1)use16——pushaw算法类似于pusha算法;

2) use32 - pushaw 不会改变(即它对段宽度不敏感,并且总是与字大小的寄存器一起工作 - ax、cx、dx、bx、sp、bp、si、di)。 pusha 命令对设置的段宽度敏感,当指定 32 位段时,它与相应的 32 位寄存器一起工作,即 eax、esx、edx、ebx、esp、ebp、esi、edi。

5. pushad - 执行类似于 pusha 命令,但有一些特殊性。

以下三个命令执行上述命令的相反操作:

1)罗拉;

2)罂粟;

3)流行。

下面描述的指令组允许您将标志寄存器保存在堆栈上,并将一个字或双字写入堆栈。 请注意,下面列出的指令是微处理器指令集中唯一允许(并要求)访问标志寄存器的全部内容的指令。

1. pushf - 将标志寄存器保存在堆栈上。

此命令的操作取决于段大小属性:

1)使用16——2字节大小的标志寄存器写入堆栈;

2) use32 - 4字节的eflags寄存器被写入堆栈。

2. pushfw - 在堆栈上保存一个字大小的标志寄存器。 总是像 pushf 一样使用 use16 属性。

3. pushfd——根据段的位宽属性(即与pushf相同)将flags或eflags标志寄存器保存在堆栈上。

同样,以下三个命令执行与上述操作相反的操作:

1) 流行音乐;

2)popftv;

3) 流行音乐。

总而言之,当堆栈的使用几乎不可避免时,我们会注意到主要的操作类型:

1) 调用子程序;

2)暂存寄存器值;

3) 局部变量的定义。

2.算术命令

微处理器可以执行整数和浮点运算。 为此,它的架构有两个独立的模块:

1) 进行整数运算的装置;

2) 执行浮点运算的装置。

这些设备中的每一个都有自己的命令系统。 原则上,整数设备可以接管浮点设备的许多功能,但这在计算上会很昂贵。 对于使用汇编语言的大多数问题,整数运算就足够了。

一组算术指令和数据概述

一个整数计算设备支持十几个算术指令。 图 27 显示了该组中命令的分类。

米。 27.算术指令的分类

整数算术指令组适用于两种类型的数字:

1) 整数二进制数。 数字可能有也可能没有带符号的数字,即有符号或无符号数字;

2) 整数十进制数。

考虑存储这些数据类型的机器格式。

整数二进制数

定点二进制整数是在二进制数系统中编码的数字。

二进制整数的维数可以是 8、16 或 32 位。 二进制数的符号取决于如何解释数字表示中的最高有效位。 这是对应维度的数字的第 7,15 位或第 31 位。 同时,有趣的是,在算术命令中,真正将这个最高位作为符号的命令只有两个,它们是整数乘除命令 imul 和 idiv。 在其他情况下,有符号数的动作以及相应的符号位的责任在于程序员。 二进制数的取值范围取决于其大小和最高有效位的解释,无论是作为数字的最高有效位还是作为数字的符号位(表 9)。

表 9. 二进制数的范围 十进制数

十进制数是数字信息的一种特殊表示形式,它是根据一组四位对数字的每个十进制数字进行编码的原理。 在这种情况下,数字的每个字节都包含一个或两个十进制数字,即所谓的二进制编码十进制代码(BCD - Binary-Coded Decimal)。 微处理器以两种格式存储 BCD 数字(图 28):

1) 打包格式。 在这种格式中,每个字节包含两个十进制数字。 十进制数字是 0 到 9 之间的 4 位二进制值。 在这种情况下,数字的最高位的代码占据最高4位。 因此,一个十进制压缩数在 1 个字节中的表示范围是 00 到 99;

2) 未打包的格式。 在这种格式中,每个字节在四个最低有效位中包含一个十进制数字。 高 4 位设置为零。 这就是所谓的区。 因此,1个字节表示一个十进制解包数的范围是0到9。

米。 28. BCD数的表示

如何在程序中描述二进制十进制数? 为此,您只能使用两个数据描述和初始化指令 - db 和 dt。 之所以可以只用这些指令来描述BCD数,是因为“低地址低字节”的原则也适用于这类数,非常方便它们的处理。 而且一般来说,当使用 BCD 数字这样的数据类型时,这些数字在程序中的描述顺序和处理它们的算法是程序员的品味和个人喜好的问题。 在我们查看下面使用 BCD 数字的基础知识后,这一点将变得清晰。

二进制整数的算术运算

无符号二进制数的加法

微处理器根据二进制数相加规则执行操作数相加。 只要结果的值不超过操作数字段的大小,就没有问题。 例如,添加字节大小的操作数时,结果不得超过数字 255。如果发生这种情况,则结果不正确。 让我们考虑一下为什么会发生这种情况。

例如,让我们做加法:二进制的 254 + 5 = 259。 11111110 + 0000101 = 1 00000011。结果超出了 8 位,其正确值适合 9 位,而值 8 保留在操作数的 3 位字段中,这当然不是真的。 在微处理器中,这个加法的结果是被预测的,并且提供了特殊的手段来解决和处理这些情况。 因此,为了解决超出结果位网格的情况,在这种情况下,打算使用进位标志 cf。 它位于 EFLAGS/FLAGS 标志寄存器的位 0。 正是这个标志的设置确定了从操作数的高位转移一个的事实。 自然,程序员必须考虑到这种加法运算结果的可能性,并提供纠正的手段。 这涉及在解析 cf 标志的加法操作之后包含代码段。 这个标志可以用各种方式解析。

最简单和最容易使用的是使用 jcc 条件分支命令。 该指令将当前代码段中的标签名称作为其操作数。 如果由于前一个命令的操作,cf 标志设置为 1,则执行到该标签的转换。微处理器命令系统中有三个二进制加法命令:

1) inc operand - 递增操作,即将操作数的值加1;

2)addoperand_1,operand_2——加法指令,运算原理:operand_1 =operand_1 +operand_2;

3) adc 操作数_1,操作数_2 - 加法指令考虑进位标志cf。 命令运算原理:operand_1 =operand_1 +operand_2 + value_sG。

注意最后一个命令——这是加法命令,它考虑到从高位转移一个。 我们已经考虑过这种单位出现的机制。 因此,adc 指令是一个微处理器工具,用于添加长二进制数,其维度超过微处理器支持的标准字段长度。

有符号二进制加法

事实上,微处理器“不知道”有符号数和无符号数之间的区别。 相反,他有办法固定在计算过程中出现的特征情况的发生。 在讨论无符号加法时,我们介绍了其中一些:

1)cf进位标志,设置为1表示操作数超出范围;

2) adc 命令,它考虑了这种退出的可能性(从最低有效位开始)。

另一种方法是注册操作数的高位(符号)位状态,这是使用 EFLAGS 寄存器(位 11)中的溢出标志来完成的。

当然,您还记得数字在计算机中是如何表示的:正数 - 二进制,负数 - 二进制补码。 考虑添加数字的各种选项。 这些示例旨在显示操作数的两个最高有效位的行为以及加法运算结果的正确性。

例子

30566 = 0111011101100110

+

00687 = 00000010

=

31253 = 01111010

我们监控第 14 位和第 15 位的转账以及结果的正确性:没有转账,结果是正确的。

例子

30566 = 0111011101100110

+

30566 = 0111011101100110

=

1132 = 11101110

从第 14 类转移; 没有从第 15 类转移。 结果是错误的,因为存在溢出 - 结果证明该数字的值大于 16 位有符号数字 (+32 767) 的值。

例子

-30566 = 10001000 10011010

+

-04875 = 11101100 11110101

=

-35441 = 01110101 10001111

从第 15 位开始有转帐,从第 14 位开始没有转帐。 结果不正确,因为结果不是负数,而是正数(最高有效位为 0)。

例子

-4875 = 11101100 11110101

+

-4875 = 11101100 11110101

=

09750 = 11011001

从第 14 位和第 15 位进行传输。 结果是正确的。

因此,我们检查了所有情况,发现在传输过程中发生了溢出情况(将 OF 标志设置为 1):

1) 从第 14 位开始(对于有符号的正数);

2) 从第 15 位开始(负数)。

相反,如果两个位都有进位或两个位都没有进位,则不会发生溢出(即,OF 标志复位为 0)。

所以溢出是用溢出标志注册的。 除了标志之外,当从高位传输时,传输标志 CF 被设置为 1。由于微处理器不知道有符号数和无符号数的存在,程序员独自负责正确的动作结果数字。 您可以分别使用条件跳转指令 JC\JNC 和 JO\JNO 来解析 CF 和 OF 标志。

带符号的数字相加命令与不带符号的数字相同。

无符号二进制数的减法

与分析加法运算一样,我们将讨论执行减法运算时发生的过程的本质。 如果被减数大于被减数,那么没有问题——差为正,结果是正确的。 如果被减数小于被减数,就会出现问题:结果小于0,而这已经是有符号数了。 在这种情况下,结果必须被包装。 这是什么意思? 通过通常的减法(在一列中),他们从最高订单中借出 1。 微处理器做同样的事情,即它从操作数位网格中最高位之后的数字中取 1。 让我们用一个例子来解释。

例子

05 = 00000000

-10 = 00000000 00001010

做减法,让我们做

高级假想贷款:

100000000 00000101

-

00000000 00001010

=

11111111 11111011

因此,从本质上讲,行动

(65 + 536) - 5 = 10

这里的 0 相当于数字 65536。结果当然是不正确的,但是微处理器认为一切正常,尽管它通过设置进位标志 cf 修复了借用一个单元的事实。 但是再仔细看看减法运算的结果。 它是 -5 的补码! 让我们进行一个实验:将差异表示为 5 + (-10) 的总和。

例子

5 = 00000000

+

(-10)= 11111111 11110110

=

11111111 11111011

即我们得到了与上一个示例相同的结果。

因此,在执行减无符号数的命令后,需要分析 CE 标志的状态,如果置 1,则表示从高位借位,结果是在附加代码中得到的.

与加法指令一样,减法指令组由可能的最小集合组成。 这些命令根据我们现在考虑的算法进行减法,异常必须由程序员自己考虑。 减法命令包括:

1) dec operand - 递减操作,即将操作数的值减1;

2) suboperand_1,operand_2——减法指令; 其工作原理:operand_1 =operand_1 -operand_2;

3) sbb 操作数_1,操作数_2 - 考虑到贷款(ci 标志)的减法命令:操作数_1 = 操作数_1 - 操作数_2 - 值_sG。

如您所见,在减法命令中,有一个 sbb 命令考虑了进位标志 cf。 此命令与 adc 类似,但现在 cf 标志充当减数时从最高有效位借 1 的指示符。

有符号二进制减法

这里的一切都有些复杂。 微处理器不需要有两个设备——加法和减法。 只有一个就足够了 - 加法器。 但是对于通过在附加代码中添加带有符号的数字的方式进行减法,有必要表示两个操作数 - 减少的和减去的。 结果也应被视为二进制补码值。 但是这里出现了困难。 首先,它们与操作数的最高有效位被视为符号位这一事实有关。 考虑减去 45 - (-127) 的例子。

例子

有符号数的减法 1

45 = 0010

-

-127 = 1000 0001

=

-44 = 1010 1100

从符号位判断,结果是负数,这反过来表明该数字应被视为等于 -44 的补码。 正确的结果应该是 172。在这里,与有符号加法的情况一样,当数字的有效位改变操作数的符号位时,我们遇到了尾数溢出。 您可以通过溢出标志的内容来跟踪这种情况。 将其设置为 1 表示该大小的操作数的结果超出有符号数的范围(即最高有效位已更改),程序员必须采取措施纠正结果。

例子

有符号数的减法 2

-45-45 = -45 + (-45)= -90。

-45 = 11010011

+

-45 = 11010011

=

-90 = 1010 0110

这里一切正常,溢出标志位复位为0,符号位中的1表示结果值为二进制补码数。

大操作数的减法和加法

如果您注意到,加法和减法指令使用固定维度的操作数:8、16、32 位。 但是,如果您需要使用 48 位操作数添加更大维度的数字,例如 16 位,该怎么办? 例如,让我们添加两个 48 位数字:

米。 29. 添加大操作数

图 29 显示了逐步添加长数字的技术。 可以看出,添加多字节数字的过程与“在一列中”添加两个数字时的方式相同——如果需要,可以将 1 转移到最高位。 如果我们设法对这个过程进行编程,那么我们将显着扩大我们可以执行加法和减法运算的二进制数的范围。

表示范围超出操作数标准位格的数的减法原理与加法相同,即使用进位标志 cf。 你只需要想象一下在一列中减去的过程,并正确地将微处理器指令与 sbb 指令结合起来。

结束我们对加法和减法指令的讨论,除了 cf 和 of 标志之外,eflags 寄存器中还有一些其他标志可以与二进制算术指令一起使用。 这些是以下标志:

1) zf - 零标志,如果运算结果为1则设置为0,如果结果不等于1则设置为0;

2) sf - 符号标志,其值经过算术运算(不仅如此)与结果的最高有效位的值一致,即与第 7、15 或 31 位一致。因此,该标志可用于运算在有符号的数字上。

无符号数的乘法

无符号数相乘的命令是

多重因子_1

如您所见,该命令仅包含一个乘数操作数。 第二个操作数 factor_2 是隐式指定的。 它的位置是固定的,取决于因素的大小。 因为,一般来说,乘法的结果大于它的任何因子,它的大小和位置也必须唯一确定。 因子的大小和第二个操作数的位置以及结果的选项如表 10 所示。

表 10. 操作数的排列和乘法结果

从表中可以看出,乘积由两部分组成,并且根据操作数的大小,放置在两个位置 - 代替 factor_2(下部)和附加寄存器 ah、dx、edx(较高部分)。 那么,如何动态地(即在程序执行期间)知道结果小到足以放入一个寄存器中,或者它超出了寄存器的尺寸并且最高部分最终在另一个寄存器中? 为此,我们使用前面讨论中已知的 cf 和溢出标志:

1) 如果结果的前导部分为零,则在乘积运算后标志 cf = 0 和 of = 0;

2)如果这些标志不为零,那么这意味着结果已经超出了产品的最小部分并且由两部分组成,在进一步的工作中应该考虑到这一点。

乘以有符号数

将数字与符号相乘的命令是

[imul 操作数_1,操作数_2,操作数_3]

该命令的执行方式与 mul 命令相同。 imul 命令的一个显着特征只是符号的形成。

如果结果很小并且适合一个寄存器(即,如果 cf = of = 0),则另一个寄存器(高位)的内容是符号扩展 - 它的所有位都等于高位(符号位) 的结果的低部分。 否则(如果cf = of = 1),结果的符号是结果高位的符号位,低位的符号位是二进制结果码的有效位。

无符号数的除法

无符号数相除的命令是

div 分隔符

除数可以在内存中或寄存器中,大小为 8、16 或 32 位。 被除数的位置是固定的,并且与乘法指令一样,取决于操作数的大小。 除法命令的结果是商和余数。

除法操作数的位置和大小选项如表 11 所示。

表 11. 操作数的排列和除法的结果

除法指令执行后,标志的内容未定义,但可能会出现中断号 0,称为“除以零”。 这种类型的中断属于所谓的异常。 由于计算过程中的一些异常,这种中断发生在微处理器内部。 中断 O,“除以零”,在执行 div 命令时可能由于以下原因之一发生:

1) 除数为零;

2)商不包括在为其分配的位网格中,这可能发生在以下情况:

a) 当一个字的值被除数除以字节值的除数时,被除数的值比除数的值大256倍以上;

b) 双字值的被除数除以一个字的除数时,被除数比除数大65倍以上;

c) 四字值的被除数除以双字值的除数时,被除数比除数大4倍以上。

有标志的划分

用符号除数字的命令是

idiv 分隔线

对于此命令,所有考虑的有关命令和符号数的规定都是有效的。 我们只注意到出现异常 0 的特征,即“除以零”,在带有符号的数字的情况下。 由于以下原因之一执行 idiv 命令时会发生这种情况:

1) 除数为零;

2)商不包含在分配给它的位格中。

后者又可能发生:

1)当用带符号字节值的除数除以带符号字值的被除数时,被除数的值大于除数值的128倍(因此,商不应超出-128的范围至 + 127);

2)当被除数除以有符号双字值除以除数除以有符号字值时,被除数的值大于除数值的32倍(因此,商不得超出- 768 至 +32);

3)当被除数除以有符号四字值除以有符号双字除数时,被除数大于除数值的2倍(因此,商不得超出-147到+的范围483 648 2 147)。

整数运算辅助指令

微处理器指令集中有几条指令可以更容易地编写执行算术计算的算法。 其中可能会出现各种问题,微处理器开发人员提供了几个命令来解决这些问题。

类型转换命令

如果算术运算涉及的操作数大小不同怎么办? 例如,假设在加法运算中,一个操作数是一个字,另一个是一个双字。 上面说过,相同格式的操作数必须参与加法运算。 如果数字是无符号的,那么输出很容易找到。 在这种情况下,可以在原操作数的基础上形成一个新的(双字格式),其高位可以简单地用零填充。 有符号数的情况更复杂:如何在程序执行过程中动态考虑操作数的符号? 为了解决这些问题,微处理器指令集有所谓的类型转换指令。 这些指令将字节扩展为字,将字扩展为双字,将双字扩展为四字(64 位值)。 类型转换指令在转换有符号整数时特别有用,因为它们会自动用旧对象的符号位的值填充新构造的操作数的高位。 此操作导致与原始值相同符号和相同幅度的整数值,但格式更长。 这种变换称为符号传播操作。

有两种类型转换命令。

1. 没有操作数的指令。 这些命令适用于固定寄存器:

1) cbw (Convert Byte to Word) - 通过将高位 al 的值扩展到 ah 寄存器的所有位,将一个字节(在 al 寄存器中)转换为一个字(在 ah 寄存器中)的命令;

2) cwd (Convert Word to Double) - 通过将高位 ax 的值扩展到寄存器 dx 的所有位,将一个字(在寄存器 ax 中)转换为双字(在寄存器 dx:ax 中)的命令;

3) cwde (Convert Word to Double) - 通过将高位 ax 的值传播到 eax 寄存器上半部分的所有位,将一个字(在寄存器 ax 中)转换为双字(在寄存器 eax 中)的命令;

4) cdq (Convert Double Word to Quarter Word) - 通过将 eax 的最高有效位的值传播到所有位来将双字 (在 eax 寄存器中) 转换为四字 (在 edx: eax 寄存器中) 的命令edx 寄存器的位。

2、与字符串处理命令相关的命令movsx和movzx。 这些命令在我们的问题上下文中具有有用的属性:

1) movsx 操作数_1,操作数_2 - 使用符号传播发送。 将operand_8 的16 位或2 位值(可以是寄存器或内存操作数)扩展为其中一个寄存器中的16 位或32 位值,使用符号位的值填充operand_1 的较高位置。 该指令对于为算术运算准备带符号的操作数很有用;

2) movzx operand_1,operand_2 - 以零扩展发送。 将operand_8 的16 位或2 位值扩展为16 位或32 位,用零清除(填充)operand_2 的高位。 该指令对于为算术准备无符号操作数很有用。

其他有用的命令

1. xadd 目标、源 - 交换和添加。

该命令允许您按顺序执行两个操作:

1) 交换目标值和源值;

2) 用目标操作数代替总和:目标 = 目标 + 源。

2. neg 操作数 - 用二进制补码求反。

该指令反转操作数的值。 在物理上,该命令执行一项操作:

操作数 = 0 - 操作数,即从零中减去操作数。

可以使用 neg 操作数命令:

1) 更改标志;

2) 对常数进行减法运算。

二进制十进制数的算术运算

在本节中,我们将了解压缩和非压缩 BCD 数的四种基本算术运算中的每一种的细节。

问题可能会出现:为什么我们需要 BCD 数字? 答案可能是:业务应用程序中需要 BCD 数字,即数字需要大而精确的地方。 正如我们已经在二进制数的示例中看到的那样,使用这种数字的操作对于汇编语言来说是非常有问题的。 使用二进制数的缺点包括:

1)word和双字格式的取值范围有限。 如果该程序设计用于金融领域,那么将卢布金额限制在 65(单字)甚至 536(双字)将大大缩小其应用范围;

2) 存在舍入误差。 你能想象一个在银行某处运行的程序在处理二进制整数并处理数十亿时不考虑余额的价值吗? 我不想成为这样一个程序的作者。 使用浮点数不会保存 - 存在相同的舍入问题;

3) 以符号形式(ASCII 码)呈现大量结果。 商业程序不只是做计算; 使用它们的目的之一是及时向用户提供信息。 当然,要做到这一点,信息必须以符号形式呈现。 将数字从二进制转换为 ASCII 需要一些计算工作。 浮点数更难转换成符号形式。 但是,如果您查看 ASCII 表中未压缩十进制数字及其对应字符的十六进制表示,您会发现它们相差 30h。 因此,转换为符号形式(反之亦然)更加容易和快捷。

您可能已经看到至少掌握十进制数字操作的基础知识的重要性。 接下来,考虑使用十进制数执行基本算术运算的特征。 我们立即注意到一个事实,即 BCD 数的加法、减法、乘法和除法没有单独的命令。 这样做的原因很容易理解:这些数字的维度可以任意大。 BCD 数可以加减,压缩和未压缩,但只有未压缩的 BCD 数可以除法和乘法。 为什么会这样,将在进一步的讨论中看到。

解压 BCD 数的算术运算

添加未打包的 BCD 号码

让我们考虑两种加法情况。

例子

相加结果不超过9

6 = 0000

+

3 = 0000

=

9 = 0000

没有从初级到高级四联体的转移。 结果是正确的。

例子

加法的结果大于 9:

06 = 0000

+

07 = 0000

=

13 = 0000

我们不再收到 BCD 号码。 结果是错误的。 解压 BCD 格式的正确结果应该是二进制的 0000 0001 0000 0011(或十进制的 13)。

在分析了添加 BCD 数时的这个问题(以及执行其他算术运算时的类似问题)和可能的解决方法之后,微处理器命令系统的开发人员决定不引入处理 BCD 数的特殊命令,而是引入几个纠正命令.

这些指令的目的是在操作数为 BCD 数的情况下更正普通算术指令的运算结果。

以例10的减法为例,可以看出得到的结果需要进行修正。 为了更正微处理器命令系统中将两个单位数字解包的 BCD 数相加的操作,有一个特殊的命令——aaa(ASCII Adjust for Addition)——对加法结果的更正,以符号形式表示。

该指令没有操作数。 它仅对 al 寄存器隐式工作并解析其下四分体的值:

1)如果这个值小于9,那么标志cf被复位为XNUMX,并且执行到下一条指令的转移;

2)如果该值大于9,则执行以下动作:

a) 将 6 添加到下四分体的内容中(但不添加到整个寄存器的内容中!)因此,小数结果的值在正确的方向上被纠正;

b) 标志 cf 设置为 1,从而将传输固定到最高有效位,以便在后续操作中可以考虑到它。

所以,在例子10中,假设和值0000 1101在al中,在aaa指令之后,寄存器会有1101 + 0110 = 0011,即二进制0000 0011或十进制3,cf标志位会设置为1,即传输已存储在微处理器中。 接下来,程序员将需要使用 adc 加法指令,该指令将考虑前一位的进位。

未压缩 BCD 数的减法

这里的情况与加法非常相似。 让我们考虑相同的情况。

例子

减法的结果不大于 9:

6 = 0000

-

3 = 0000

=

3 = 0000

如您所见,高级笔记本没有贷款。 结果是正确的,不需要更正。

例子

减法的结果大于 9:

6 = 0000

-

7 = 0000

=

-1 = 1111 1111

减法是根据二进制算术规则进行的。 因此,结果不是 BCD 数。

解压 BCD 格式的正确结果应该是 9(二进制的 0000 1001)。 在这种情况下,假设从最高有效位借位,与正常的减法命令一样,即在 BCD 数的情况下,实际上应该执行 16 - 7 的减法。因此,很明显,如在在加法的情况下,减法结果必须被纠正。 为此,有一个特殊的命令 - aas(ASCII 减法调整) - 修正减法结果以符号形式表示。

aas 指令也没有操作数,对 al 寄存器进行操作,解析其最小四分体如下:

1) 如果其值小于 9,则 cf 标志复位为 0,控制转移到下一条命令;

2)如果al中的四分体值大于9,那么aas命令执行如下动作:

a) 从寄存器 al 的下四分体的内容中减去 6(注意 - 不是从整个寄存器的内容中);

b) 重置寄存器 al 的上四分体;

c) 将 cf 标志设置为 1,从而固定虚高位借位。

很明显,aas 命令与基本的 sub 和 sbb 减法命令一起使用。 在这种情况下,只使用一次 sub 命令是有意义的,当减去操作数的最低位时,应该使用 sbb 命令,这将考虑到可能从最高位开始的贷款。

解压 BCD 数的乘法

以未压缩数字的加减为例,很明显没有标准算法可以对 BCD 数字执行这些操作,程序员必须根据其程序的要求自己实现这些操作。

剩下的两个运算——乘法和除法——的实现更加复杂。 在微处理器指令集中,只有一位数未压缩的 BCD 数的乘法和除法的产生方法。

为了将任意维数相乘,您需要自己实现乘法过程,以一些乘法算法为基础,例如“在一列中”。

为了将两个一位 BCD 数字相乘,您必须:

1)将其中一个因子放入AL寄存器中(根据mul指令的要求);

2) 将第二个操作数放入寄存器或内存中,分配一个字节;

3) 使用 mul 命令将因子相乘(结果如预期的那样在 ah 中);

4) 结果当然是二进制代码,所以需要更正。

为了更正乘法后的结果,使用了一个特殊命令 - aam(用于乘法的 ASCII 调整) - 更正乘法结果以以符号形式表示。

它没有操作数,对 AX 寄存器的操作如下:

1) 将 al 除以 10;

2) 除法的结果写成:al 中的商,ah 中的余数。 结果,在执行 aam 指令后,AL 和 ah 寄存器包含两位数乘积的正确 BCD 位。

在结束对 aam 命令的讨论之前,我们需要注意它的另一个用途。该命令可将 AL 寄存器中的二进制数转换为解包的 BCD 数,并将其放入 ah 寄存器中:结果的最高有效位在 ah 中,最低有效位在 al 中。显然,二进制数必须在 0...99 范围内。

解压 BCD 数的除法

对两个未压缩的 BCD 数执行除法运算的过程与之前考虑的其他运算有些不同。 这里也需要更正操作,但必须在直接将一个 BCD 数除以另一个 BCD 数的主操作之前执行。 首先,在寄存器啊,你需要得到两个未打包的被除数的BCD数字。 这在某种程度上让程序员对他感到舒服。 接下来,您需要发出命令 aad - aad (ASCII Adjust for Division) - 符号表示的除法校正。

该指令没有操作数,将 ax 寄存器中的两位解包 BCD 数转换为二进制数。这个二进制数随后将在除法运算中起到被除数的作用。除了转换之外,aad 命令还将生成的二进制数放入 AL 寄存器中。被除数自然是 0...99 范围内的二进制数。

aad 命令执行此转换的算法如下:

1)将ah中原始BCD数的最高位(AH的内容)乘以10;

2)进行AH+AL的加法,将其结果(二进制数)输入AL;

3)重置AN的内容。

接下来,程序员需要发出一个普通的 div 除法命令来执行 ax 的内容除以位于字节寄存器或字节存储器位置的单个 BCD 数字。

与 aash 类似,aad 命令也可用于将解压缩的 BCD 数字从 0...99 范围转换为其二进制等效值。

要划分更大容量的数字,以及在乘法的情况下,您需要实现自己的算法,例如“在一列中”,或者找到更优化的方法。

压缩 BCD 数的算术

如上所述,压缩 BCD 数字只能加减。 要对它们执行其他操作,它们必须另外转换为解压缩格式或二进制表示。 由于压缩的 BCD 数字不是很感兴趣,我们将简要考虑它们。

添加压缩 BCD 数字

首先,让我们进入问题的核心并尝试将两个两位数的压缩 BCD 数字相加。 添加压缩 BCD 数字的示例:

67 = 01100111

+

75 = 01110101

=

142 = 1101 1100 = 220

如您所见,二进制的结果是 1101 1100(或十进制的 220),这是不正确的。 这是因为微处理器不知道 BCD 数的存在,而是按照二进制数相加的规则进行相加。 实际上,BCD 的结果应该是 0001 0100 0010(或十进制的 142)。

可以看出,对于未打包的 BCD 数,对于打包的 BCD 数,需要以某种方式纠正算术运算的结果。

微处理器为此命令提供 daa - daa(加法的十进制调整) - 加法结果的校正,以便以十进制形式表示。

daa命令根据daa命令描述中给出的算法将al寄存器的内容转换成两个压缩十进制数字,得到的单位(如果加法的结果大于99)存储在cf标志中,从而考虑到最高有效位的传输。

压缩 BCD 数的减法

与加法类似,微处理器将打包的 BCD 数视为二进制,并相应地减去 BCD 数作为二进制。

例子

压缩 BCD 数的减法。

让我们减去 67-75。 由于微处理器以加法的方式进行减法,所以我们将按照以下方式进行:

67 = 01100111

+

-75 = 10110101

=

-8 = 0001 1100 = 28

如您所见,十进制的结果是 28,这是荒谬的。 在 BCD 中,结果应该是 0000 1000(或十进制的 8)。

在对压缩 BCD 数的减法进行编程时,程序员以及在减去非压缩 BCD 数时,必须自己控制符号。 这是使用 CF 标志完成的,它修复了高阶借位。

BCD 数的减法本身是通过简单的 sub 或 sbb 减法命令执行的。 结果的更正通过命令 das - das(减法的十进制调整)执行 - 以十进制形式表示的减法结果的更正。

das 命令根据 das 命令描述中给出的算法将 AL 寄存器的内容转换为两个压缩十进制数字。

LECTURE No. 19. 控制传输命令

1. 逻辑命令

除了算术计算的手段外,微处理器指令系统还具有逻辑数据转换的手段。 通过逻辑手段这种数据转换,这是基于形式逻辑的规则。

形式逻辑在真假陈述的层面上运作。 对于微处理器,这通常分别表示 1 和 0。 对于计算机而言,XNUMX 和 XNUMX 的语言是本机语言,但机器指令使用的最小数据单位是一个字节。 但是,在系统级别,通常需要能够在尽可能低的级别(位级别)上运行。

米。 29. 逻辑数据处理手段

逻辑数据转换的手段包括逻辑命令和逻辑运算。 汇编指令的操作数通常可以是表达式,而表达式又是运算符和操作数的组合。 在这些运算符中,可能有对表达式对象实现逻辑运算的运算符。

在详细考虑这些工具之前,让我们考虑一下逻辑数据本身是什么以及对它们执行了哪些操作。

布尔数据

逻辑数据处理的理论基础是形式逻辑。 有几个逻辑系统。 其中最著名的是命题演算。 命题是可以说是真或假的任何陈述。

命题演算是一组规则,用于确定某些命题组合的真假。

命题演算与计算机原理及其编程的基本方法非常和谐地结合在一起。 计算机的所有硬件组件都建立在逻辑芯片上。 在计算机中以最低级别表示信息的系统是基于位的概念。 只有两种状态(0(假)和 1(真))的位自然适合命题演算。

根据该理论,可以对语句(对位)执行以下逻辑操作。

1. 否定(逻辑非)- 对一个操作数的逻辑运算,其结果是原始操作数的值的倒数。

该操作的独特之处在于以下真值表(表 12)。

表 12. 逻辑否定的真值表

2. 逻辑加法(逻辑或)——对两个操作数的逻辑运算,如果一个或两个操作数为真(1),结果为“真”(1),如果两个操作数都是“假”(0)假(0)。

使用以下真值表(表 13)描述此操作。

表 13. 逻辑包含 OR 的真值表

3. 逻辑乘法(逻辑与) - 对两个操作数的逻辑运算,仅当两个操作数都为真 (1) 时,其结果才为真 (1)。 在所有其他情况下,操作的值为“假”(0)。

使用以下真值表(表 14)描述此操作。

表 14. 逻辑与真值表

4. 逻辑异加(逻辑异或)——对两个操作数进行逻辑运算,如果两个操作数中只有一个为真(1),则其结果为“真”(1),假(0),如果两个操作数都是假 (0) 或真 (1)。 使用以下真值表(表 15)描述此操作。

表 15. 逻辑异或的真值表

微处理器指令集包含五个支持这些操作的指令。 这些指令对操作数的位执行逻辑运算。 当然,操作数的维度必须相同。 例如,如果操作数的维数等于字(16 位),则首先对操作数的零位执行逻辑运算,并将其结果写入结果的位 0。 接下来,该命令对从第一个到第十五的所有位按顺序重复这些操作。

逻辑命令

微处理器命令系统具有以下支持处理逻辑数据的命令集:

1)和operand_1、operand_2——逻辑乘法运算。 该命令对操作数operand_1 和operand_2 的位执行按位逻辑与运算(合取)。 结果代替operand_1写入;

2) og operand_1,operand_2 - 逻辑加法运算。 该命令对操作数operand_1 和operand_2 的位执行按位逻辑或运算(析取)。 结果代替operand_1写入;

3) xor operand_1,operand_2 - 逻辑异加运算。 该命令对操作数operand_1 和operand_2 的位执行按位逻辑异或运算。 结果代替操作数写入;

4)测试operand_1、operand_2——“测试”运算(使用逻辑乘法)。 该命令对操作数operand_1 和operand_2 的位执行按位逻辑与运算。 操作数的状态保持不变,只是改变了标志 zf、sf 和 pf,这样就可以在不改变其状态的情况下分析操作数各个位的状态;

5) 非操作数——逻辑否定的操作。 该命令对操作数的每一位执行按位反转(将值替换为相反的值)。 结果被写入操作数的位置。

要了解逻辑命令在微处理器指令集中的作用,了解它们的应用领域以及它们在编程中的典型使用方法非常重要。

在逻辑命令的帮助下,可以选择操作数中的各个位来设置它们、重置它们、反转它们或简单地检查某个值。

为了用位来组织这样的工作,operand_2 通常扮演掩码的角色。 借助设置在位 1 中的此掩码位,可以确定特定操作所需的 operand_1 位。 让我们展示哪些逻辑命令可用于此目的:

1) 将某些数字(位)设置为 1,使用命令 ogoperand_1,operand_2。

在该指令中,充当掩码的操作数_2 必须包含1 位来代替操作数_1 中应设置为XNUMX 的那些位;

2)将某些数字(位)重置为0,使用命令和operand_1,operand_2。

在此指令中,充当掩码的操作数_2 必须包含零位,以代替操作数_0 中必须设置为1 的位;

3) 应用命令异或操作数_1,操作数_2:

a) 找出operand_1 和operand 中的哪些位不同;

b) 反转操作数_1 中指定位的状态。

执行 xor 命令时我们感兴趣的掩码位(operand_2)必须为单个,其余的必须为零;

命令 test operand_1,operand_2(检查操作数_1)用于检查指定位的状态。

掩码 (operand_1) 中操作数 2 的检查位必须设置为 1。 test命令的算法与and命令的算法类似,但不改变operand_XNUMX的值。 该命令的结果是设置零标志 zf 的值:

1)如果zf = 0,则作为逻辑乘法的结果,得到零结果,即掩码的一个单位位,与操作数对应的单位位不匹配;

2)如果zf = 1,则作为逻辑乘法的结果,得到一个非零的结果,即掩码的至少一个单位位与operand_1的相应单位位重合。

要对测试命令的结果做出反应,建议使用跳转命令 jnz label (Jump if Not Zero) - 如果零标志 zf 非零则跳转,或者反向操作命令 - jz label (Jump if Zero ) - 如果零标志 zf = 0,则跳转。

以下两个命令搜索设置为 1 的第一个操作数位。 可以从操作数的开头和结尾进行搜索:

1) bsf operand_1,operand_2 (Bit Scanning Forward) - 向前扫描位。 该指令从最低有效位到最高有效位(从位 2 到最高有效位)搜索(扫描)operand_0 的位,以搜索设置为 1 的第一位。如果找到,则将operand_1 填充为该位为整数值。 如果operand_2的所有位都为0,则零标志zf置1,否则zf标志置0;

2) bsr operand_1,operand_2(位扫描复位)——按相反顺序扫描位。 该指令从最高有效位到最低有效位(从最高有效位到第 2 位)搜索(扫描)operand_0 的位,以搜索设置为 1 的第一位。如果找到,则将operand_1 填充为该位为整数值。 重要的是,左侧第一个单位位的位置仍然相对于位 0 进行计数。如果操作数_2 的所有位都为 0,则将零标志 zf 设置为 1,否则将 zf 标志重置为 0。

在最新型号的英特尔微处理器中,逻辑指令组中出现了更多指令,允许您访问操作数的一个特定位。 操作数可以在内存中,也可以在通用寄存器中。 位位置由相对于操作数最低有效位的位偏移量给出。 偏移值可以指定为直接值或包含在通用寄存器中。 您可以使用 bsr 和 bsf 命令的结果作为偏移值。 所有指令都将选定位的值分配给 CE 标志。

1) bt操作数,bit_offset(Bit Test)——位测试。 该指令将位值传送到 cf 标志;

2) bts 操作数,offset_bit(位测试和设置)——检查和设置位。 该指令将位值传送到CF标志位,然后将要检查的位设置为1;

3) btr 操作数,bit_offset(位测试和复位)——检查和复位位。 指令将该位值传送到 CF 标志位,然后将该位设置为 0;

4) btc 操作数,offset_bit(位测试和转换)——检查和反转位。 该指令将位的值包装在 cf 标志中,然后反转该位的值。

换档命令

该组中的指令还提供对操作数的各个位的操作,但方式与上面讨论的逻辑指令不同。

所有移位指令都根据操作码向左或向右移动操作数字段中的位。 所有移位指令都具有相同的结构——复制操作数,shift_count。

要移位的位数 - counter_shifts - 位于第二个操作数的位置,可以通过两种方式设置:

1) 静态,包括使用直接操作数设置固定值;

2)动态,即在执行移位指令之前将移位计数器的值输入到cl寄存器中。

根据 cl 寄存器的维度,很明显移位计数器的值可以在 0 到 255 之间。但实际上,这并不完全正确。 为了优化目的,微处理器只接受计数器的五个最低有效位的值,即该值在 0 到 31 的范围内。

所有移位指令都设置进位标志 cf。

当位移出操作数时,它们首先命中进位标志,将其设置为等于操作数外下一位的值。 该位的下一个位置取决于移位指令的类型和程序算法。

移位命令按工作原理可分为两种:

1) 线性移位指令;

2) 循环移位命令。

线性移位指令

这种类型的命令包括根据以下算法转换的命令:

1) 下一个被压入的位设置 CF 标志;

2) 从另一端进入操作数的位值为0;

3)当下一位移位时,它进入CF标志,而前一位移位的值丢失! 线性移位命令分为两种子类型:

1) 逻辑线性移位指令;

2)算术线性移位指令。

逻辑线性移位命令包括以下内容:

1) shl 操作数,counter_shifts(逻辑左移)——逻辑左移。 操作数的内容左移 shift_count 指定的位数。 在右边(在最低有效位的位置)输入零;

2)shr操作数,shift_count(逻辑右移)——逻辑右移。 操作数的内容右移 shift_count 指定的位数。 在左侧(最高有效符号位的位置)输入零。

图 30 显示了这些命令是如何工作的。

米。 30. 线性逻辑移位指令工作方案

算术线性移位指令与逻辑移位指令的不同之处在于它们以特殊方式对操作数的符号位进行操作。

1)sal操作数,shift_counter(算术左移)——算术左移。 操作数的内容左移 shift_count 指定的位数。 在右边(在最低有效位的位置),输入零。 sal 指令不保留符号,但在下一位提前更改符号的情况下使用 / 设置标志。 否则,sal 命令与 shl 命令完全相同;

2)sar操作数,shift_count(算术右移)——算术右移。 操作数的内容右移 shift_count 指定的位数。 零被插入到左侧的操作数中。 sar 命令保留符号,在每次位移后恢复它。

图 31 显示了线性算术移位指令的工作原理。

米。 31. 线性算术移位指令的运算方案

旋转命令

循环移位指令包括存储移位位的值的指令。 循环移位指令有两种类型:

1)简单的循环移位命令;

2) 通过进位标志的循环移位命令 cf。

简单的循环移位命令包括:

1) rol 操作数,shift_counter (Rotate Left)——循环左移。 操作数的内容左移 shift_count 操作数指定的位数。 左移位从右边写入同一个操作数;

2) gog 操作数,counter_shifts (Rotate Right)——循环右移。 操作数的内容右移 shift_count 操作数指定的位数。 右移位被写入左侧的相同操作数。

米。 32. 简单循环移位命令的操作方案

从图 32 可以看出,简单循环移位的指令在其工作过程中执行了一个有用的动作,即:循环移位的位不仅从另一端压入操作数,同时它的value 成为 CE 标志的值。

通过进位标志 CF 的循环移位命令与简单循环移位命令的不同之处在于,移位后的位不会立即从另一端进入操作数,而是首先写入进位标志 CE 仅在下一次执行该移位命令时(前提是它在循环中执行)导致先前的高级位被放置在操作数的另一端(图 33)。

以下与通过进位标志的循环移位命令有关:

1) rcl 操作数 shift_count (Rotate through Carry Left) - 通过进位循环左移。

操作数的内容左移 shift_count 操作数指定的位数。 移位的位又成为进位标志 cf 的值。

2) rsg 操作数 shift_count (Rotate through Carry Right) - 通过进位循环右移。

操作数的内容右移 shift_count 操作数指定的位数。 移位的位又变成进位标志CF的值。

米。 33. 通过进位标志 CF 循环指令

图 33 显示,当通过进位标志进行移位时,会出现一个中间元素,借助它,特别是可以替换循环移位的比特,特别是比特序列的不匹配。

在下文中,位序列的失配是指允许以某种方式定位和提取该序列的必要部分并将它们写入另一个位置的动作。

附加换档命令

从 i80386 开始,最新 Intel 微处理器型号的命令系统包含额外的移位命令,这些命令扩展了我们之前讨论的功能。这些是双精度移位命令:

1) shld operand_1、operand_2、shift_counter - 双精度左移。 shld 命令通过将 operand_1 的位向左移动来执行替换,并用从 operand_2 移出的位的值填充其右侧的位,如图 34 所示。 0. 要移位的位数由shift_counter 值决定,该值的范围为31... 2. 该值可以指定为立即数操作数或包含在cl 寄存器中。 operand_XNUMX 的值未更改。

米。 34. shld命令的方案

2) shrd operand_1、operand_2、shift_counter - 双精度右移。该指令通过将 operand_1 操作数的位向右移位来执行替换,并根据图 2 中的图表用从 operand_35 移位的位的值填充其左侧的位。要移位的位数为由shift_counter的值确定,其范围为0...31。该值可以由立即操作数指定或包含在cl寄存器中。 operand_2 的值未更改。

米。 35. shrd命令的方案

正如我们所指出的,shld 和 shrd 命令最多可移动 32 位,但由于指定操作数和运算算法的特殊性,这些命令可用于处理长达 64 位的字段。

2.控制转移命令

我们熟悉了一些形成程序线性部分的命令。 它们中的每一个通常都执行一些数据转换或传输,然后微处理器将控制权转移到下一条指令。 但是很少有程序以这种一致的方式工作。 程序中通常有一些点必须决定接下来将执行哪条指令。 该解决方案可能是:

1)无条件 - 此时,有必要将控制权转移到下一个命令,而不是转移到与当前命令有一定距离的另一个命令;

2) 有条件的——根据对某些条件或数据的分析来决定接下来执行哪个命令。

程序是占用一定 RAM 空间的一系列命令和数据。 该内存空间可以是连续的,也可以由多个片段组成。

接下来应该执行哪条程序指令,微处理器从 cs: (e) ip 寄存器对的内容中学习:

1)cs——代码段寄存器,包含当前代码段的物理(基)地址;

2) eip/ip - 指令指针寄存器,它包含一个值,表示要执行的下一条指令在内存中相对于当前代码段开头的偏移量。

将使用哪个特定寄存器取决于设置的寻址模式 use16 或 use32。 如果指定 use 16,则使用 ip,如果指定 use32,则使用 eip。

因此,控制转移指令改变了 cs 和 eip / ip 寄存器的内容,结果微处理器选择执行的不是按顺序排列的下一条程序指令,而是程序其他部分中的指令。 微处理器内部的流水线被复位。

根据操作原理,在程序中提供转换组织的微处理器命令可分为 3 组:

1.无条件传递控制命令:

1)无条件分支命令;

2) 调用过程并从过程返回的命令;

3) 调用软件中断并从软件中断返回的命令。

2. 控制权有条件转移的命令:

1) 根据比较指令 p 的结果跳转指令;

2)根据某个标志的状态转换命令;

3)跳转esx/cx寄存器内容的指令。

3、循环控制命令:

1) 使用计数器 ехх/сх 组织循环的命令;

2) 用于组织带有计数器 ех/сх 的循环的命令,可以通过附加条件提前退出循环。

无条件跳转

前面的讨论已经揭示了过渡机制的一些细节。 跳转指令修改 eip/ip 指令指针寄存器和可能的 cs 代码段寄存器。 究竟需要修改什么取决于:

1)关于无条件分支指令中操作数的类型(near or far);

2)从在跳转地址之前指定一个修饰符(在跳转指令中); 在这种情况下,跳转地址本身可以直接位于指令中(直接跳转),也可以位于寄存器或存储单元中(间接跳转)。

修饰符可以采用以下值:

1) near ptr - 直接转换到当前代码段内的标签。 仅根据命令中指定的地址(标签)或使用取值符号-$的表达式修改eip/ip寄存器(取决于指定的use16或use32代码段类型);

2) far ptr - 直接转换到另一个代码段中的标签。 跳转地址指定为立即操作数或地址(标号),由16位选择器和16/32位偏移量组成,分别加载到cs和ip/eip寄存器中;

3) word ptr - 间接转换到当前代码段内的标签。 只有 eip/ip 被修改(通过命令中指定地址的内存或寄存器的偏移值)。 偏移大小 16 或 32 位;

4) dword ptr - 间接转换到另一个代码段中的标签。 两个寄存器 - cs 和 eip / ip - 都被修改(通过内存中的值 - 并且仅来自内存,来自寄存器)。 这个地址的第一个word/dword代表偏移量,加载到ip/eip中; 第二个/第三个单词被加载到 cs 中。 jmp 无条件跳转指令

无条件跳转的命令语法是 jmp [modifier] jump_address - 无条件跳转,不保存有关返回点的信息。

Jump_address 是标签形式的地址或跳转指针所在的内存区域的地址。

总之,在微处理器指令系统中,无条件跳转 jmp 的机器指令代码有好几种。

它们的差异由过渡距离和指定目标地址的方式决定。 跳转距离由 jump_address 操作数的位置决定。 该地址可能在当前代码段中或在某个其他段中。 在第一种情况下,过渡称为段内或接近,在第二种情况下 - 段间或远距离。 段内跳转假设只有 eip/ip 寄存器的内容被改变。

jmp 命令的段内使用有三个选项:

1)直短;

2) 笔直的;

3) 间接的。

程序

汇编语言有几个工具可以解决重复代码段的问题。 这些包括:

1) 程序机制;

2) 宏汇编器;

3)中断机制。

过程,通常也称为子例程,是分解(分成几个部分)任务的基本功能单元。 过程是用于解决特定子任务的一组命令,具有从更高级别调用任务的点接收控制并将控制返回到该点的方法。

在最简单的情况下,程序可能由单个过程组成。 换句话说,一个过程可以定义为一组格式良好的命令,这些命令被描述一次,如果需要,可以在程序的任何地方调用。

为了将命令序列描述为汇编语言中的过程,使用了两个指令:PROC 和 ENDP。

过程描述语法如下(图 36)。

米。 36.程序中程序描述的语法

图 36 显示在过程头(PROC 指令)中,只有过程名称是强制性的。 在 PROC 指令的大量操作数中,应该突出显示 [distance]。 该属性可以取值近或远,并表征从另一个代码段调用过程的可能性。 默认情况下,[distance] 属性设置为 near。

该过程可以放置在程序中的任何位置,但不能随意控制。 如果程序只是简单地插入到通用指令流中,那么微处理器将把程序的指令视为该流的一部分,并因此执行程序的指令。

条件跳转

微处理器有 18 条条件跳转指令。 这些命令允许您检查:

1)带符号的操作数之间的关系(“大于-小于”);

2)不带符号的操作数之间的关系(“高-低”);

3) 算术标志 ZF、SF、CF、OF、PF(但不是 AF)的状态。

条件跳转命令具有相同的语法:

jcc 跳转标签

可以看到,所有命令的助记码都以“j”开头——从单词跳转(jump)开始,它——决定了命令所分析的具体条件。

对于 jump_label 操作数,该标签只能位于当前代码段内;不允许条件跳转中的段间控制转移。 在这方面,无条件跳转命令的语法中存在修饰符,这是毫无疑问的。 在微处理器的早期模型(i8086、i80186 和 i80286)中,条件分支指令只能执行短跳转 - 从条件分支指令之后的指令开始从 -128 字节到 +127 字节。 从微处理器型号 80386 开始,这个限制被删除,但是,正如您所看到的,只在当前代码段内。

为了决定在何处将控制转移到条件跳转命令,必须首先形成一个条件,在此基础上做出转移控制的决定。

这种情况的来源可以是:

1) 任何改变算术标志状态的命令;

2)比较指令p,比较两个操作数的值;

3) esx/cx 寄存器的状态。

cmp比较命令

页面比较命令有一种有趣的工作方式。 它与减法命令完全一样——子操作数,operand_2。

p 指令与 sub 指令一样,减去操作数并设置标志。 它唯一不做的就是写减法的结果来代替第一个操作数。

命令语法 str - stroperand_1,operand_2 (compare) - 比较两个操作数并根据比较结果设置标志。

p 命令设置的标志可以通过特殊的条件分支指令进行分析。 在我们看它们之前,让我们稍微注意一下这些条件跳转指令的助记符(表 16)。 理解形成条件跳转命令名称(jcc命令名称中的元素,我们指定)时的符号,将有助于它们的记忆和进一步的实际使用。

表 16. jcc 命令名称中缩写的含义 表 17. 命令 p operand_1、operand_2 的条件跳转命令列表

不要对条件分支命令的几个不同助记符对应相同的标志值感到惊讶(在表 17 中它们之间用斜线隔开)。 名称上的差异是由于微处理器开发人员希望更容易结合某些指令组使用条件跳转指令。 因此,不同的名称反映了不同的功能定位。 然而,这些命令响应相同的标志这一事实使它们在程序中绝对等价和平等。 因此,在表 17 中,它们不是按名称分组,而是按它们响应的标志(条件)的值进行分组。

条件转移指令和标志

一些条件跳转指令的助记符名称反映了它们所使用的标志的名称,其结构如下:第一个字符是“j”(Jump,跳转),第二个是标志标志或否定字符“ n",后跟标志的名称。 这种团队结构反映了它的目的。 如果没有字符“n”,则检查标志的状态,如果等于 1,则转换到跳转标签。 如果存在字符“n”,则检查标志状态是否等于 0,如果成功,则跳转到跳转标签。

命令助记符、标志名和跳转条件如表18所示。这些命令可以在任何修改指定标志的命令之后使用。

表 18. 条件跳转指令和标志

如果仔细查看表 17 和表 18,您会发现其中的许多条件跳转指令是等价的,因为它们都是基于对相同标志的分析。

条件跳转指令和 esx/cx 寄存器

微处理器的架构涉及许多寄存器的具体使用。 例如,EAX/AX/AL 寄存器用作累加器,BP、SP 寄存器用于堆栈。 ECX / CX 寄存器也有一定的功能用途:它在循环控制命令和处理字符串时充当计数器。 从功能上讲,与 esx/cx 寄存器相关的条件分支指令可能更正确地归因于这组指令。

此条件分支指令的语法是:

1)jcxz jump_label(如果ex为零则跳转)-如果cx为零则跳转;

2) jecxz jump_label (Jump Equal ех Zero) - 如果 ех 为零则跳转。

这些命令在循环和处理字符串时非常有用。

应该注意的是,jcxz/jecxz 命令有一个固有的限制。 与其他条件传输指令不同,jcxz/jecxz 指令只能从其后的指令处理短跳转 -128 字节或 +127 字节。

循环的组织

如您所知,循环是一种重要的算法结构,如果不使用它,可能没有程序可以做到。 您可以组织程序某个部分的循环执行,例如,使用控制命令的条件转移或无条件跳转命令 jmp。 对于这样的循环组织,其组织的所有操作都是手动执行的。 但是,考虑到循环这样的算法元素的重要性,微处理器的开发人员将一组三个命令引入指令系统,这有助于循环编程。 这些指令还使用 esx/cx 寄存器作为循环计数器。

我们来简单描述一下这些命令:

1) 循环transition_label(Loop)——重复循环。该命令允许您组织类似于高级语言中的 for 循环的循环,并自动递减循环计数器。该团队的工作是执行以下操作:

a) ECX/CX 寄存器的递减;

b) 将 ECX/CX 寄存器与零进行比较:如果 (ECX/CX) = 0,则控制转移到循环后的下一条命令;

2) loope/loopz jump_label

loope 和 loopz 命令是绝对同义词。 命令的工作是执行以下操作:

a) ECX/CX 寄存器的递减;

b) 将 ECX/CX 寄存器与零进行比较;

c) 分析零标志 ZF 的状态,如果 (ECX/CX) = 0 或 XF = 0,则控制转移到循环后的下一个命令。

3) loopne/loopnz jump_label

命令 loopne 和 loopnz 也是绝对同义词。 命令的工作是执行以下操作:

a) ECX/CX 寄存器的递减;

b) 将 ECX/CX 寄存器与零进行比较;

c) 零标志 ZF 的状态分析:如果 (ECX/CX) = 0 或 ZF = 1,则控制转移到循环后的下一个命令。

loope/loopz 和 loopne/loopnz 命令在它们的操作中是相互的。 它们通过额外解析 zf 标志来扩展循环命令的操作,这使得可以使用该标志作为指示来组织提前退出循环。

循环命令 loop、loope/loopz 和 loopne/loopnz 的缺点是它们只实现短跳转(从 -128 到 +127 字节)。 要使用长循环,您需要使用条件跳转和 jmp 指令,因此请尝试掌握两种组织循环的方法。

作者:茨维特科娃 A.V.

我们推荐有趣的文章 部分 讲义、备忘单:

管理。 婴儿床

社会心理学。 婴儿床

简述 XNUMX 世纪的俄罗斯文学。 婴儿床

查看其他文章 部分 讲义、备忘单.

读和写 有帮助 对这篇文章的评论.

<< 返回

科技、新电子最新动态:

用于触摸仿真的人造革 15.04.2024

在现代科技世界,距离变得越来越普遍,保持联系和亲密感非常重要。萨尔大学的德国科学家最近在人造皮肤方面的进展代表了虚拟交互的新时代。萨尔大学的德国研究人员开发出了超薄膜,可以远距离传输触觉。这项尖端技术为虚拟通信提供了新的机会,特别是对于那些发现自己远离亲人的人来说。研究人员开发的超薄膜厚度仅为 50 微米,可以融入纺织品中并像第二层皮肤一样穿着。这些薄膜充当传感器,识别来自妈妈或爸爸的触觉信号,并充当将这些动作传递给婴儿的执行器。父母触摸织物会激活传感器,对压力做出反应并使超薄膜变形。这 ... >>

Petgugu全球猫砂 15.04.2024

照顾宠物通常是一项挑战,尤其是在保持房屋清洁方面。 Petgugu Global 初创公司推出了一种有趣的新解决方案,这将使猫主人的生活变得更轻松,并帮助他们保持家中干净整洁。初创公司 Petgugu Global 推出了一款独特的猫厕所,可以自动冲掉粪便,让你的家保持干净清新。这款创新设备配备了各种智能传感器,可以监控宠物的厕所活动并在使用后激活自动清洁。该设备连接到下水道系统,确保有效清除废物,无需业主干预。此外,该厕所还具有较大的可冲水存储容量,非常适合多猫家庭。 Petgugu 猫砂碗专为与水溶性猫砂一起使用而设计,并提供一系列附加功能 ... >>

体贴男人的魅力 14.04.2024

长期以来,女性更喜欢“坏男孩”的刻板印象一直很普遍。然而,英国莫纳什大学科学家最近进行的研究为这个问题提供了新的视角。他们研究了女性如何回应男性的情感责任和帮助他人的意愿。这项研究的结果可能会改变我们对男性对女性吸引力的理解。莫纳什大学科学家进行的一项研究得出了有关男性对女性吸引力的新发现。在实验中,女性看到了男性的照片,并附有关于他们在各种情况下的行为的简短故事,包括他们对遇到无家可归者的反应。一些人无视这名无家可归的人,而另一些人则帮助他,比如给他买食物。一项研究发现,与表现出同理心和善良的男性相比,表现出同理心和善良的男性对女性更具吸引力。 ... >>

来自档案馆的随机新闻

元界可能比社交网络更糟糕 01.12.2021

现代技术正在转向虚拟现实和增强现实,创造空间来容纳虚拟世界。 怀疑论者对这一趋势持否定态度,并认为元宇宙会导致人们所知道的现实的终结。 正如第一个增强现实系统的发明者路易斯·罗森伯格所说,虚拟世界可能比社交网络更糟糕。

增强现实和元宇宙力求以最自然的方式呈现内容,从而“改变现实感”,消除人们心中的界限,扭曲对日常体验的解释。

“就我个人而言,这让我感到害怕。增强现实将从根本上改变社会的方方面面,而且不一定会变得更好,”罗森伯格解释道。

据专家介绍,社交网络通过过滤显示给用户的内容来操纵现实。 人们越来越依赖企业在人们和他们的日常生活之间提供和维护无数层的技术。

正如罗森伯格所相信的那样,增强现实将成为生活中不可或缺的一部分,人们将无法简单地摘下增强现实眼镜并处理实际问题。 删除积分意味着一个人将处于不利的社会、经济和智力地位。 罗森伯格敦促每个人都要小心,因为增强现实很容易被用来分裂社会并在人与人之间制造不和。

其他有趣的新闻:

▪ 低噪声 38V LDO 稳压器 ST Microelectronics LDO40L

▪ 对聚乙烯的蜂蛾

▪ 2 端口 PCIe 3.0 融合总线适配器

▪ 磁场以不寻常的方式影响石墨烯

▪ 地球轨道将由卫星看门人清理

科技、新电子资讯

 

免费技术图书馆的有趣材料:

▪ 电工网站的部分。 英语口语考试。 文章精选

▪ 沃尔特·本杰明的文章。 名言警句

▪ 文章什么是毒蛇? 详细解答

▪ 文章线索结。 旅游小贴士

▪ 文章数字荧光示波器。 无线电电子电气工程百科全书

▪ 文章 电压高达 1 kV 的架空输电线路。 前言。 无线电电子电气工程百科全书

留下您对本文的评论:

Имя:


电子邮件(可选):


点评:





本页所有语言

主页 | 图书馆 | 用品 | 网站地图 | 网站评论

www.diagram.com.ua

www.diagram.com.ua
2000-2024