跳转至

4 章 源程序格式⚓︎

2926 个字 130 行代码 预计阅读时间 16 分钟

⚓︎

定义⚓︎

段定义的一般格式为:

segmentname segment [use] [align] [combine] ['class']
    statements
segmentname ends
  • 关键字:segment表示段定义的开始,ends表示段定义的结束,它们是必需的
  • segmentname表示段名,遵循下面提到的命名规则。注意段定义的开始和结束的段名必须一致
  • statements表示汇编语言的语句(指令 ~、伪指令 ~、汇编指示 ~
  • 可选部分(用方括号括起来的,一般情况下用不到
    • use:段内偏移地址宽度
      • 可用关键字有use16use32,分别表示 16 位和 32 位段内地址宽度
      • 若源程序开头有语句.386,表示接下来每个段的偏移地址宽度默认为use32,否则的话默认为use16
    • align:对齐方式
      • 可用关键字有:byteworddwordpara(节,16 字节,默认对齐方式page(页,256 字节,用于规定所定义段的边界宽度
      • 段首地址能够被对齐方式(段的边界宽度)整除
    • combine:合并类型
      • 可用关键词有:
        • public:用于代码段或数据段的定义。凡是段名相同、类别名相同、合并类型为public的段,在链接时将合并成一个段
        • stack:用于堆栈段的定义。凡是段名相同、类别名相同、合并类型为stack的段,在链接时将合并成一个段;且在程序载入内存准备运行时,sssp会自动初始化为该堆栈段的段址和长度
      • 如果不存在同名的代码段或数据段,则可省略合并类型
      • 如果定义了堆栈段,则必须指定该段的合并类型为stack,否则编译器会把它当作一个普通的数据段,因而sssp会被分别初始化为首段的段地址和 0
    • 'class':类别名
      • 名称可变,且必须被单引号括起来
      • 相同类别名的段在链接时会被链接器重新安排顺序,使它们在可执行文件中是邻近的

假设⚓︎

汇编指示语句assume可以用来建立编译器所需的段和段寄存器之间的关联。格式如下:

assume segreg:segmentname
  • segreg表示四个段寄存器(csdsesss)中的一种
  • segmentname表示某个段的段名

一般来说,段和段寄存器的匹配关系如下所示:

asusme cs:code, ds:data, es:extra, ss:stk

注意“建立关联”意味着并不是将段地址直接赋值给段寄存器,而是提醒编译器在编译时将段地址替换为关联的段寄存器。

在前一章中,我们知道dses在程序开始执行时被赋值为 PSP 段址。因此若想在程序中正确引用数据段内的变量或数据元素,必须在代码段一开始对ds进行以下赋值:

mov ax, data
mov ds, ax

引用⚓︎

  • 段名
  • seg 变量名/标号名

语句⚓︎

汇编语言的语句可分为以下三类:

  • 指令语句(instruction statement):源程序的核心成分,编译后变成机器码
  • 伪指令语句(pseudo-instruction statement):
    • 用于定义变量或数组
    • 编译后仅剩下变量或数组的初始值,名称及类型均在编译后消失
  • 汇编指示语句(assembler directive statement):
    • 它的作用是告诉编译器如何编译源程序
    • 编译后自动消失
例子
.386
data segment use16
c db 0FFh
s db "ABCD", 0
i dw 1234h, 5678h
d dd 8086C0DEh
data ends

code segment use16
assume cs:code, ds:data, ss:stk
main:
mov ax, data
mov ds, ax
mov eax, [d]
rol eax, 16
push eax
pop dword ptr [i]
mov ah, 4Ch
int 21h
code ends

stk segment use16 stack
db 100h dup('S')
stk ends
end main

其中:

  • 指令语句:12-19
  • 伪指令语句:3-6, 11, 23
  • 汇编指示语句:1, 2, 7, 9, 10, 20, 22, 24, 25

格式⚓︎

汇编语句的一般格式为:

name mnemonic operand   ; comment
; 例如:
; main: mov ax, data   ; 把 data 段地址赋值给 ax
  • name:名字项
    • 可以表示变量名、标号名、段名、过程名
    • 该项不是必需的,大多数语句并不需要
  • mnemonic:助记符项,包括 80x86 指令(movaddjmp、汇编指示指令(segmentassumeend、伪指令(dbdwdd
  • operand:操作数项,作为助记符项的参数
    • 操作数的个数取决于助记符,可以有 0 个或多个,没有助记符就没有操作数
  • comment:注释项

    • 源程序编译时,注释项会被全部忽略,因此注释仅对源程序的作者、读者有意义
    • 以分号开始,只能用于单行注释
    • 多行注释:
    ; 法1
    ; #号可以换成其他字符,比如%、@、|,但要保证开始和结束标记一定要相同,且注释内容中不得包含标识符
    comment #
      注释
    #
    
    ; 法2
    IF 0
    注释
    ENDIF
    

四个项之间可以用一个或多个空白字符(空格、制表符、回车)间隔。

常数⚓︎

  • 整数常数

    ; 以下4条语句等价
    mov ah, 83
    mov ah, 01010011B
    mov ah, 123Q
    mov ah, 53h
    
  • 浮点型常数

    x dd 3.14          ; float
    y dq 1.6E-307      ; double
    z dt 3.14159E4096  ; long double
    
  • 字符常数

    • 可用单引号或双引号括起来
    • 数值上等于该字符的 ASCII 码值
  • 字符串常数
    • 可用单引号或双引号括起来(所以汇编语言中单引号和双引号没有区别)
    • 不同于 C 语言,字符串末尾并没有结束符00h
    • 将字符串常量拆成一个个字符,用逗号间隔,这样构成的字符数组与原字符串等价
      s db 'H', 'e', 'l', 'l', 'o'
      ; 等价于 s db "Hello"
      

常数表达式⚓︎

常数与运算符结合就构成了常数表达式。下面列出汇编语言中常数表达式可用的运算符

运算符 格式 含义
+ +表达式(一元) 或 表达式 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=定义符号常数,格式如下:

symbol equ expression
symbol  =  expression
; symbol:符号名,expression:表达式
  • =的操作数只能是数值类型或字符类型的常数或常数表达式,可以对同一个符号进行多次定义
  • equ的操作数还可以是字符串或汇编语句,但它不允许对同一个符号进行多次定义
例子
char     =    'A'
exitfun  equ  <mov ah, 4Ch>
dosint   equ  <int 21h>
code segment
assume cs:code
main:
    mov ah, 2
    mov dl, char
    dosint
    char = 'B'     ; 重新定义char
    mov ah, 2
    mov dl, char
    dosint
    exitfun
    dosint
code ends
end main

变量和标号⚓︎

命名规则⚓︎

  • 变量名和标号名的可用字符有:大小写字母、数字、符号@$?_
  • 不得以数字开头
  • $?不能单独作为名称
  • 名称长度不超过 31 个字符
  • 在缺省情况下,变量名及标号名不区分大小写
  • 相同名称不得重复定义
  • 不能与 80x86 指令、伪指令、汇编指示指令名相同

变量⚓︎

变量定义的格式如下:

varname   db | dw | dd | dq | dt   value
  • varname表示变量名
  • dbdwdddqdt是伪指令,分别表示不同位宽的数据(具体含义见 2
  • value表示初始值

    • 定义数组时,有时需要多个相同的初始值,可以使用dup运算符获取重复 (duplicate) 值,格式为:
    varname pseudo-inst n dup(x1[, x2, ..., xm])
    

    其中n表示重复的次数,x1, x2, ..., xm表示重复项,可以有 1 个或多个,且允许嵌套

    例子
    y db 2 dup('A', 3 dup('B'), 'C')
    ; 等价于
    y db 'A', 'B', 'B', 'B', 'C', 'A', 'B', 'B', 'B', 'C'
    

    变量的引用:

  • 单个变量 / 数组首元素的引用:var[var]

  • i个数组元素的引用:a[i * n][a + i * n],其中a是宽度为n字节的数组(与 C 语言略有不同)
  • 在数据段中
    • varoffset var都可以作为伪指令dw的操作数,表示var的偏移地址(近指针)
    • var还可以作为伪指令dd的操作数,表示该变量的偏移地址的远指针
  • 在代码段中,只能用offset var引用该变量的偏移地址,用seg var或数据段名引用该变量的段地址

位置计数器(location counter):一个用于记录当前段内变量或标号的偏移地址

  • 在段定义开始时,编译器会自动把位置计数器清零
  • 每编译完一条指令或伪指令语句时,编译器会把该语句的宽度(即对应机器码的字节数)加到位置计数器中
  • 一种特殊的操作数$,它表示当前位置计数器的值,可以用它来计算数组的长度

标号⚓︎

标号是符号形式的跳转目标地址,既可作为跳转指令(比如jmpjnzloop等)的目标地址,也可作为call指令的目标地址。标号的定义格式如下:

; 定义1
labelname:

;定义2
labelname label near|far|byte|word|dword|qword|tbyte
; label: 伪指令
; label后面所跟关键词为标号的类型
  • 2 个(nearfar)为标号类型,分别表示近标号远标号
  • 5 个为变量类型
  • 可使用label定义变量

    data segment
        ; 常规定义
        abc db 1, 2, 3, 4
    
        ; 等价的label定义
        xyz label byte
        db 1, 2, 3, 4   ; 这两句话实际上是连在一起的
    data ends
    
    • 好处:可以在同一地址上同时定义字节、字等多种类型的变量
    例子
    b label byte
    w label dword
    d label dword
    db 12h, 34h, 56h, 78h
    
    ; b == 12h
    ; w == 3412h
    ; d == 78563412h
    ; 这3个变量地址相同而值不同
    

关于近标号和远标号:

  • 两者取决于以该标号为目标的jmpcall指令是否与该标号落在同一个段内
  • 近标号:jmpcall与标号位于同一个段内
    • 格式:labelname:labelname label near
    • 会转化为该标号所在段中的偏移地址,可看作一个仅含偏移地址的近指针
  • 远标号:jmpcall与标号不在同一个段内
    • 格式:labelname label far
    • 会转化为该标号所在段的段地址以及它所在段中的偏移地址,可看作一个含段地址和偏移地址的远指针
  • 标号修饰:强制将指令中的标号编译成指定指针
    • far ptr:强制为远指针
    • near ptr:强制为近指针
    • 何时使用:
      • jmpcall指令引用不在同一个段内近标号时,或者当jmpcall指令向前引用(forward reference)(源程序上方的语句引用下方的变量或标号)不在同一个段内远标号时,必须在该标号前加far ptr修饰
      • jmpcall指令向后引用不在同一个段内远标号时,far ptr可省略
      • 若某个标号既被同一个段内的calljmp指令引用,又被其他段内的calljmp指令引用,
        • 将该标号定义为近标号
        • 同一段内的calljmp指令可加near ptr修饰,也可以省略
        • 不同段内的calljmp指令必须加far ptr修饰

标号的引用:若lab为标号名,则laboffset lab均可作为该标号的偏移地址

程序的结束⚓︎

源程序的结束用汇编指示语句end表示,格式如下:

end labelname
;labelname 是标号名,用来指定程序首条指令的位置
  • 当源程序被编译成可执行程序并开始运行时,寄存器ip被赋值为该标号的偏移地址,cs被赋值为该标号的段地址即代码段的段地址
  • end后省略labelname,则程序开始运行时ip = 0cs =代码段的段地址(代码段首条指令的位置)

然而,源程序的结束并不意味着可执行程序的结束,因为end是一条汇编指示语句,编译后会消失。要想让程序真正中止,通常需要调用 DOS 4Ch号功能,调用格式如下:

mov ah, 4Ch
mov al, 返回码
int 21h
  • al的返回码用于将本程序的运行状态传递给父程序,即当前运行程序的调用者
  • 比如在 DOS 命令行下执行可执行程序时,DOS 就是该程序的父程序。但 DOS 并不使用返回码,因此中间的语句可以省略
  • 如果不调用4Ch号功能,那么 CPU 会继续执行当前程序后面的内存空间中的指令,而这些指令往往是一堆随机的机器码,因此 CPU 极有可能会因为无法解释这些指令而死机

评论区

如果大家有什么问题或想法,欢迎在下方留言~