第 4 章 源程序格式⚓︎
约 2926 个字 130 行代码 预计阅读时间 16 分钟
段⚓︎
定义⚓︎
段定义的一般格式为:
- 关键字:
segment
表示段定义的开始,ends
表示段定义的结束,它们是必需的 segmentname
表示段名,遵循下面提到的命名规则。注意段定义的开始和结束的段名必须一致statements
表示汇编语言的语句(指令 ~、伪指令 ~、汇编指示 ~)- 可选部分(用方括号括起来的,一般情况下用不到
) :use
:段内偏移地址宽度- 可用关键字有
use16
、use32
,分别表示 16 位和 32 位段内地址宽度 - 若源程序开头有语句
.386
,表示接下来每个段的偏移地址宽度默认为use32
,否则的话默认为use16
- 可用关键字有
align
:对齐方式- 可用关键字有:
byte
、word
、dword
、 para
(节,16 字节,默认对齐方式) 、 page
(页,256 字节) ,用于规定所定义段的边界宽度 - 段首地址能够被对齐方式(段的边界宽度)整除
- 可用关键字有:
combine
:合并类型- 可用关键词有:
public
:用于代码段或数据段的定义。凡是段名相同、类别名相同、合并类型为public
的段,在链接时将合并成一个段stack
:用于堆栈段的定义。凡是段名相同、类别名相同、合并类型为stack
的段,在链接时将合并成一个段;且在程序载入内存准备运行时,ss
和sp
会自动初始化为该堆栈段的段址和长度
- 如果不存在同名的代码段或数据段,则可省略合并类型
- 如果定义了堆栈段,则必须指定该段的合并类型为
stack
,否则编译器会把它当作一个普通的数据段,因而ss
和sp
会被分别初始化为首段的段地址和 0
- 可用关键词有:
'class'
:类别名- 名称可变,且必须被单引号括起来
- 相同类别名的段在链接时会被链接器重新安排顺序,使它们在可执行文件中是邻近的
假设⚓︎
汇编指示语句assume
可以用来建立编译器所需的段和段寄存器之间的关联。格式如下:
segreg
表示四个段寄存器(cs
、ds
、es
、 ss
)中的一种segmentname
表示某个段的段名
一般来说,段和段寄存器的匹配关系如下所示:
注意
在前一章中,我们知道ds
和es
在程序开始执行时被赋值为 PSP 段址。因此若想在程序中正确引用数据段内的变量或数据元素,必须在代码段一开始对ds
进行以下赋值:
引用⚓︎
段名
seg 变量名/标号名
语句⚓︎
汇编语言的语句可分为以下三类:
- 指令语句(instruction statement):源程序的核心成分,编译后变成机器码
- 伪指令语句(pseudo-instruction statement):
- 用于定义变量或数组
- 编译后仅剩下变量或数组的初始值,名称及类型均在编译后消失
- 汇编指示语句(assembler directive statement):
- 它的作用是告诉编译器如何编译源程序
- 编译后自动消失
例子
其中:
- 指令语句:12-19 行
- 伪指令语句:3-6, 11, 23 行
- 汇编指示语句:1, 2, 7, 9, 10, 20, 22, 24, 25 行
格式⚓︎
汇编语句的一般格式为:
name
:名字项- 可以表示变量名、标号名、段名、过程名
- 该项不是必需的,大多数语句并不需要
mnemonic
:助记符项,包括 80x86 指令(mov
、add
、jmp
等) 、汇编指示指令(segment
、assume
、 end
) 、伪指令(db
、dw
、 dd
)operand
:操作数项,作为助记符项的参数- 操作数的个数取决于助记符,可以有 0 个或多个,没有助记符就没有操作数
-
comment
:注释项- 源程序编译时,注释项会被全部忽略,因此注释仅对源程序的作者、读者有意义
- 以分号开始,只能用于单行注释
- 多行注释:
四个项之间可以用一个或多个空白字符(空格、制表符、回车)间隔。
常数⚓︎
-
整数常数
-
浮点型常数
-
字符常数
- 可用单引号或双引号括起来
- 数值上等于该字符的 ASCII 码值
- 字符串常数
- 可用单引号或双引号括起来(所以汇编语言中单引号和双引号没有区别)
- 不同于 C 语言,字符串末尾并没有结束符
00h
- 将字符串常量拆成一个个字符,用逗号间隔,这样构成的字符数组与原字符串等价
常数表达式⚓︎
常数与运算符结合就构成了常数表达式。下面列出汇编语言中常数表达式可用的运算符
运算符 | 格式 | 含义 |
---|---|---|
+ |
+ 表达式(一元) 或 表达式 1 + 表达式 2(二元) |
正(一元)或加(二元) |
- |
- 表达式(一元) 或 表达式 1 - 表达式 2(二元) |
负(一元)或减(二元) |
* |
表达式 1 * 表达式 2 |
乘 |
/ |
表达式 1 / 表达式 2 |
除 |
mod |
表达式 1 mod 表达式 2 |
求余 |
shl |
表达式 1 shl 表达式 2 |
左移 |
shr |
表达式 1 shr 表达式 2 |
右移 |
not |
not 表达式 2 |
非 |
and |
表达式 1 and 表达式 2 |
与 |
or |
表达式 1 or 表达式 2 |
或 |
xor |
表达式 1 xor 表达式 2 |
异或 |
seg |
seg 变量名或标号名 |
取段地址 |
offset |
offset 变量名或标号名 |
取偏移地址 |
- 常量表达式可用于变量定义,也可作为指令的操作数
- 常量表达式只能包含运算符和常数
例子
data segment
abc dw 80*10_20
x dw offset abc
y dw seg abc
var db (7 shl 3) or (not 0FEh)
data ends
code segment
assume cs:code, ds:data
main:
mov ax, seg abc
mov ds, ax
mov bx, offset var
mov dl, 5 mod 3
add dl, -2
add dl, [bx]
mov ah, (7/2) xor 1
int 21h
mov ah, 4Ch
code ends
end main
其中高亮行用到了常量表达式。
符号常数⚓︎
符号常数(symbolic constant) 是以符号形式表示的常数,可用equ
和=
定义符号常数,格式如下:
=
的操作数只能是数值类型或字符类型的常数或常数表达式,可以对同一个符号进行多次定义equ
的操作数还可以是字符串或汇编语句,但它不允许对同一个符号进行多次定义
例子
变量和标号⚓︎
命名规则⚓︎
- 变量名和标号名的可用字符有:大小写字母、数字、符号
@
、$
、?
、_
- 不得以数字开头
$
和?
不能单独作为名称- 名称长度不超过 31 个字符
- 在缺省情况下,变量名及标号名不区分大小写
- 相同名称不得重复定义
- 不能与 80x86 指令、伪指令、汇编指示指令名相同
变量⚓︎
变量定义的格式如下:
varname
表示变量名db
、dw
、dd
、dq
、dt
是伪指令,分别表示不同位宽的数据(具体含义见第 2 章)-
value
表示初始值- 定义数组时,有时需要多个相同的初始值,可以使用
dup
运算符获取重复 (duplicate) 值,格式为:
其中
n
表示重复的次数,x1, x2, ..., xm
表示重复项,可以有 1 个或多个,且允许嵌套
变量的引用:
- 定义数组时,有时需要多个相同的初始值,可以使用
-
单个变量 / 数组首元素的引用:
var
或[var]
- 第
i
个数组元素的引用:a[i * n]
或[a + i * n]
,其中a
是宽度为n
字节的数组(与 C 语言略有不同) - 在数据段中
var
或offset var
都可以作为伪指令dw
的操作数,表示var
的偏移地址(近指针)var
还可以作为伪指令dd
的操作数,表示该变量的偏移地址的远指针
- 在代码段中,只能用
offset var
引用该变量的偏移地址,用seg var
或数据段名引用该变量的段地址
位置计数器(location counter):一个用于记录当前段内变量或标号的偏移地址
- 在段定义开始时,编译器会自动把位置计数器清零
- 每编译完一条指令或伪指令语句时,编译器会把该语句的宽度(即对应机器码的字节数)加到位置计数器中
- 一种特殊的操作数
$
,它表示当前位置计数器的值,可以用它来计算数组的长度
标号⚓︎
标号是符号形式的跳转目标地址,既可作为跳转指令(比如jmp
、jnz
、loop
等)的目标地址,也可作为call
指令的目标地址。标号的定义格式如下:
; 定义1
labelname:
;定义2
labelname label near|far|byte|word|dword|qword|tbyte
; label: 伪指令
; label后面所跟关键词为标号的类型
- 前 2 个(
near
、 far
)为标号类型,分别表示近标号和远标号 - 后 5 个为变量类型
-
可使用
label
定义变量data segment ; 常规定义 abc db 1, 2, 3, 4 ; 等价的label定义 xyz label byte db 1, 2, 3, 4 ; 这两句话实际上是连在一起的 data ends
- 好处:可以在同一地址上同时定义字节、字等多种类型的变量
关于近标号和远标号:
- 两者取决于以该标号为目标的
jmp
和call
指令是否与该标号落在同一个段内 - 近标号:
jmp
、call
与标号位于同一个段内- 格式:
labelname:
或labelname label near
- 会转化为该标号所在段中的偏移地址,可看作一个仅含偏移地址的近指针
- 格式:
- 远标号:
jmp
、call
与标号不在同一个段内- 格式:
labelname label far
- 会转化为该标号所在段的段地址以及它所在段中的偏移地址,可看作一个含段地址和偏移地址的远指针
- 格式:
- 标号修饰:强制将指令中的标号编译成指定指针
far ptr
:强制为远指针near ptr
:强制为近指针- 何时使用:
- 当
jmp
、call
指令引用不在同一个段内近标号时,或者当jmp
、call
指令向前引用(forward reference)(源程序上方的语句引用下方的变量或标号)不在同一个段内远标号时,必须在该标号前加far ptr
修饰 - 当
jmp
、call
指令向后引用不在同一个段内远标号时,far ptr
可省略 - 若某个标号既被同一个段内的
call
、jmp
指令引用,又被其他段内的call
、jmp
指令引用,- 将该标号定义为近标号
- 同一段内的
call
、jmp
指令可加near ptr
修饰,也可以省略 - 不同段内的
call
、jmp
指令必须加far ptr
修饰
- 当
标号的引用:若lab
为标号名,则lab
或offset lab
均可作为该标号的偏移地址
程序的结束⚓︎
源程序的结束用汇编指示语句end
表示,格式如下:
- 当源程序被编译成可执行程序并开始运行时,寄存器
ip
被赋值为该标号的偏移地址,cs
被赋值为该标号的段地址即代码段的段地址 - 若
end
后省略labelname
,则程序开始运行时ip = 0
,cs =
代码段的段地址(代码段首条指令的位置)
然而,源程序的结束并不意味着可执行程序的结束,因为end
是一条汇编指示语句,编译后会消失。要想让程序真正中止,通常需要调用 DOS 的4Ch
号功能,调用格式如下:
al
的返回码用于将本程序的运行状态传递给父程序,即当前运行程序的调用者- 比如在 DOS 命令行下执行可执行程序时,DOS 就是该程序的父程序。但 DOS 并不使用返回码,因此中间的语句可以省略
- 如果不调用
4Ch
号功能,那么 CPU 会继续执行当前程序后面的内存空间中的指令,而这些指令往往是一堆随机的机器码,因此 CPU 极有可能会因为无法解释这些指令而死机
评论区