80×86 实模式入门(3):程序结构

正文索引 [隐藏]

80x86 实模式入门(3)程序结构

除了指令系统,一个完整的汇编程序还需要一些伪操作,这些伪操作是写给汇编工具看的,汇编工具根据这些伪操作自动生成一些指令,构成了程序的总体框架,并让整个程序的可读性大大上升,下面简单总结一下


1. 处理器选择

由于80X86的所有处理器都支持8086/8088系 统,但每一种高档的机型又都增加了一些新的 指令,因此在编写程序时要对所用指令集的处 理器有一个确定的选择。也就是说,要告诉汇编程序:应该选择哪一种处理器的指令系统,默认情况下是 8086 指令系统,可以不用管他,用默认的就行


2. 段定义

程序开始前首先就要划分下存储器的段,对应的伪操作就叫段定义伪操作,一般需要定义的段有 数据段、栈段、代码段,代码段中存在着程序主体,将在后面介绍,段的定义格式如下:

用 <> 括起来的部分都是需要自己填的

<segment_name> segment <定位类型><组合类型><使用类型><类别>
...
<segment_name> ends
  • 定位类型:可以填 BYTE, WORD, DWORD, PARA, PAGE
    • PARA:段从小段地址开始(缺省默认)
    • BYTE:段从任意字节开始
    • WORD:段从下一字地址开始,偶数地址开始
    • DWORD:段从下一双字地址开始,开始地址最低2位为0, 4的倍数
    • PAGE:段从下一页地址开始(能被256整除)
  • 组合类型:可以填 PRIVATE, PUBLIC, COMMON, AT, STACK, MEMORY
    • PRIVATE:该段为私有段,不与其他模块中的同名段合并(缺省默认)
    • PUBLIC:同名段连接成一个物理段,连接次序由连接命令指定
    • COMMON:同名段启始地址相同,重叠在一起形成一个段,可以覆盖, 连接长度是各分段中的最大长度
    • AT <expression>:指定段地址,段地址是表达式的值,但不能指定代码段
    • STACK:该段运行时为堆栈的一部分,各堆栈段紧接,组成一个堆栈段
    • MEMORY:与PUBLIC同义,该段装入模块的最高地址
  • 使用类型
    • USE16: 16位寻址方式 (缺省默认),段长64KB
    • USE32: 32位寻址方式,段长4GB
    • 实模式下,应该使用USE16
  • 类别
    • ‘CLASS‟,给出连接时组成段组的类型名
    • 同类别的段装配在相邻的位置,组成段组
    • 如‘code‟, „data‟,‟bss‟ Segment

(1) 基础数据段定义

上面那些对于初学者比较多余,其实定义一个数据段全部用默认的对菜鸡就够用了

举例

DATA SEGMENT
    ARRAY_1 DB 1,2,3,4,5,6
    ARRAY_2 DW 1,2,3,4,5,6
    STRING  DB "Hello world!"
DATA ENDS

解释一下,上面定义了两个数组和一个字符串,DB、DW 分别表示Byte 类型和 Word 类型,数组 1 和 数组 2 的虽然存的都是 1,2,3,4,5,6,但是占用存储空间是不同的,程序运行时要按照类型进行存取。

另外,注意,字符串必须是 Btye 类型


(2)基础栈段定义

举例

STACK SEGMENT
    DB 256 DUP(?)
TOS LEBEL WORD
STACK ENDS

这样一个简单的栈段就定义好了,解释一下:

  • <type> <number> DUP(<char>):这个就是在存储器里面申请number 个 char(type 类型)的存储空间
  • TOS:这个名字随意自己起,是标号,标记着栈底,等会儿要把它绑定到 SP 上
  • LEABEL WORD:将 TOS 改为 WORD 类型(因为 SP 是 16 位的)

3. 程序开始与结束

定义好数据段(DS)、栈段(SS)之后,就该定义代码段(CS)了,这个是程序真正运行的代码,所有操作指令都是写在代码段里面的

(1)代码段主体结构

CODE SEGMENT
    ASSUME DS:DATA, SS:STACK, CS:CODE
...
CODE ENDS

ASSUME 伪操作:写给汇编工具,汇编工具将根据这个约定查找标号之类的东西,也便于查错,这个操作不会真的把 DATA 段绑定到 DS 上,之后还需要手工添加


(2)过程定义与程序入口

过程定义格式

<process_name> PROC [类型]
...
RET
<process_name> ENDP
  • 类型(这个会关系到 RET的方式)
    • NEAR(或缺省):说明该过程只能在段内被调用,RET 时从栈中取出 IP
    • FAR:说明该过程可以在段间被调用,也可以在段内被调用,RET 时从栈中取出 CS:IP

程序入口

这个要借助伪操作 end ,格式如下

start: ...
...
end start

这样程序就会从 start 进入,到 end start 终止,所以最好把 start 放到一个过程的最上面,这个过程就成为了主过程,如下:

MAIN PROC
START:
...
MAIN ENDP
...
END START

(3)正式程序开始前需要的套路

总体的框架有了,现在要开始写程序了,当然直接动手写还会出现一些问题,这里就需要一些固定的套路,写完这些指令之后再开始写真正的功能代码。


I. 栈绑定

如果之前定义了栈段,第一步就是要绑定到栈寄存器中去(因为仅仅之前仅仅定义了一个 SEGMENT,程序并不知道这个 SEGMENT 是什么段),也就是要告诉系统这个段是栈段

STACK SEGMENT
    DB 256 DUP(?)
TOS LEBEL WORD 
STACK ENDS
... 
CODE SEGMENT
MAIN PROC FAR
    ASSUME ...
    MOV AX, STACK ; 获取 STACK 段的首地址
    MOV SS, AX ; 赋给 SS (绑定栈寄存器)
    LEA SP, TOS; 将栈顶指针指向栈底  
    ... 

简单解释一下栈的摆放,和数据段中不同,栈底是在整个栈段的末尾的,每次入栈栈顶指针(SP)自动减少,当 SP 到达栈段首地址就代表栈满,这也是为什么用 TOS LEBEL WORD 来绑定 SP 的原因


II. DOS 状态保存

首先要明白,将 .exe 文件在 windows 中执行之前,CS:IP 就有其值,这个值和 DOS 操作系统正在处理的任务有关,此时执行程序相当于 DOS 调用了 MAIN PROC 从而进入到我们所写的程序之中,那么很自然的就想到要程序执行完成之后要跳回 DOS,这个时候就需要在程序执行之前保存一下 DOS 的状态了

方法1(通过子程序返回的方式,需要在程序开头将 CS:IP 入栈)

CODE SEGMENT
    ASSUME ...
MAIN PROC FAR
    START:
    ... ; 栈绑定
    PUSH DS ; 程序开始前将 DS 压入栈
    XOR AX, AX ; AX 清零
    PUSH AX ; AX 压入栈
    ... ; 程序主体
    RET ; 返回 DOS
MAIN ENDP
CODE ENDS
    END START

方法2(通过 DOS 中断的方式,这个不用在程序开头写指令)

CODE SEGMENT
    ASSUME ...
MAIN PROC FAR
    ... ; 程序主体
    MOV AX, 4C00H ; DOS 操作码
    INT 21H ; 调用 DOS 中断
MAIN ENDP
CODE ENDS
    END START

III. 数据段寄存器绑定

栈寄存器需要绑定一样,数据段也是(先绑定栈段是因为 DOS 状态保存可能需要 PUSH、POP 这类操作)

CODE SEGMENT
MAIN PROC FAR
    ASSUME ...
    START:
    ...
    MOV AX, DATA
    MOV DS, AX
    ...
MAIN ENDP
CODE ENDS
    END START

 

那么到这里整个程序的总体框架就有了,总结一下:

DATA SEGMENT
...
DATA ENDS
;
STACK SEGMENT
DB 256 DUP(?)
STACK ENDS
;
CODE SEGMENT
    ASSUME DS:DATA, SS:STACK, CS:CODE
MAIN PROC FAR
START:
    MOV AX, STACK
    MOV SS, AX
    LEA SP, TOS
    ; 栈寄存器绑定
    MOV AX, DATA
    MOV DS, AX
    ; 数据寄存器绑定

    ... ; 代码主体

    MOV AX, 4C00H ; DOS 返回
    INT 21H
MAIN ENDP
CODE ENDS
    END START

 


(4)其他伪操作

这些伪操作初学时可能和指令系统弄混,但是要明白这些操作都不是给操作系统的指令,而是给汇编工具的命令,在翻译的时候汇编工具会将这些伪操作全部转化为系统指令然后进行编译。

I. 数据定义及存储器分配

  • 功能:汇编工具为变量(数据)分配存储单元,并设置初始值或者只预留空间
  • 格式:[Variable] Mnemonic Operand, … , Operand [;Comments]
    • Variable:变量名,可有可无,是变量的符号地址,如果指令使用了变
      量名,表示(指向)第一个字节的偏移地址
    • Mnemonic:伪操作助记符,是数据类型的符号表示
      字节定义伪指令 DB
      字定义伪指令 DW
      双字定义伪指令 DD
      四字定义伪指令 DQ
      10个字节定义伪指令 DT
    • Operand:
      • 如给出具体数值常量、数值表达式、字符串常量、
        地址表达式,表示形成单元的初始化数据;
      • ?: 该单元内容未定,即未初始化数据
    • Comments:注释,必须以“; ” 开始
  • 举例
    DATA SEGMENT
    X DB 10,4,10H
    Y DW 100,100H,-5
    Z DD 3*20,0FFFFDH
    DATA ENDS
    

标号

  • 定义:表示指令地址,也叫符号地址 ,直接在指令助记符前加上标识符,必须以冒号“: ” 结束,如 NEXT:
  • 举例:NEXT: MOV AX, DATA

II. 数值回送

  • OFFSET / SEG 变量(或标号)
    功能:回送变量或标号的偏址 / 段址

    1234:5678(word_array 首地址) 61H
    62H
    MOV AX, WORD_ARRAY ; MOV AX, [5678H]
    mov AX, OFFSET WORD_ARRAY ; MOV AX, 5678H
    mov AX, SEG WOEG_ARRAY ; MOV AX, 1234H
    
  • TYPE变量(或标号)
    变量: DB DW DD DQ DT 标号: NEAR FAR
    值: 1 2 4 8 10 -1 -2
  • LENGTH
    • 功能:回送由DUP定义的变量的数据个数,其它情况回送1
    • 格式: LENGTH 变量
  • SIZE
    • 功能: LENGTH*TYPE
    • 格式:SIZE 变量

III. 属性

  • PTR
    • 功能:指定变量的类型属性
    • 格式:TYPE PET 变量名
    • 举例:MOV AL, BYTE PRT X
  • LABEL
    • 功能:定义变量类型属性
    • 格式:NAME LABEL TYPE
    • 举例
      DATA SEGMENT
      X LABEL BYTE
      DATA ENDS
      
  • THIS
    • 功能:为存储器操作数指定类型。该操作数地址与下一个存储单元具有相同的段基址和偏移量
      TA EQU THIS BYTE
      TB DW 100 DUP (?)
      
  • SHORT
    • 功能:用来修饰JMP指令中转向地址的属性,指出转向地址是在下一条指令地址的-128~+127字节范围之内
    • JMP SHORT NEXT
  • HIGH & LOW
    • 功能:这两个操作符被称为字节分离操作符,它接收一个数字或地址表达式,HIGH取其高字节, LOW取其低字节
      ARRAY DW 1,2,3,4,5,6,7
      MOV AL, LOW ARRAY
      ; 汇编后, MOV AL, BYTE PTR [ARRAY]
      MOV AL, HIGH ARRAY
      ; 汇编后, MOV AL, BYTE PTR [ARRAY]
      

IV. ASSUME

  • 功能:明确段和段寄存器之间的关系
  • 格式:ASSUME 段寄存器名:段名
  • 注意:
    • 若一个段寄存器与NOTHING关联,则表示取消前边对该段寄存器的假设
      DS:NOTHING ; 取消原来段寄存器DS的预约分配
      
    • ASSUME语句并不给段寄存器赋值,只是约定,便于汇编程序查错

V. ORG & EVEN

ORG

  • 功能:用来设置当前地址计数器的值,即分配后续数据、指令的存储器开始地址
  • 格式:ORG 常数表达式(n)
  • 举例
    vectors segment
    org 10 ; 10=000AH
    vect1 dw 4567h ; 偏移地址值为000AH
    org 20
    vect2 dw 9876h ; 偏移地址值为0014H
    vectors ends
    

EVEN

  • 功能:使下一个变量或指令地址开始于偶字节地址
  • 格式:EVEN
  • 举例
    A DB "morning"
    EVEN
    B DW 2 DUP (?)
    

举例

   ORG 50H
A1 DB 3
    EVEN
A2 DW 5
地址 内容
DS:0050H 03
DS:0052H 05
00

VI. RADIX

  • 功能:用于改变汇编语言默认的基数(范围为: 2~16)
  • 注意:汇编语言默认(不带后缀)的数为十进制数
  • 举例
    RADIX 16
    MOV BL, 0FF 
    ; 等价于 MOV BL, 0FFH
    

VII. EQU & =

  • 功能:构造表达式
  • 格式:表达式名称 EQU 表达式
  • 举例
    K EQU 1
    K EQU K + 5 ; 非法,EQU 伪操作中的表达式名是不允许重复定义
    A = 2
    A = A + 2 ; 合法
    
  • 注意
    • 不分配占用存储单元,只是相当定义了一个常数的数值
    • 表达式中的变量名是指变量的数值 BETA EQU ALPHA-2
    • 字符串、变址引用都可以赋以符号名 B EQU [BP+8],MOV AL, B 汇编后等同 MOV AL, [BP+8]
    • EQU 伪操作中的表达式名是不允许重复定义的,而 “= ” 伪操作中的表达式名则允许重复定义

$

  • 功能:保存当前正在汇编的基本处理单位在存储器中的首地址(如指令的首地址,操作数首地址),地址计数器的值用 $ 表示,可以用 $ 引用地址计数器的值
  • 举例ARRAY DW 1 , 2, $ + 4, 3, 4, $ + 4
    地址 内容
    0074H 01
    00
    02
    00
    0078H 7C
    00
    03
    00
    04
    00
    007EH 82
    00

VIII. +、 -、 *、 / 和 MOD

  • 举例
  • ARRAY DW 1,2,3,4,5,6,7
    ARYEND DW ?
    MOV DX,ARRAY+(6-1)*2
    ;汇编后, MOV DX,[00FA]
    ;执行后, 06→DX
    MOV CX,(ARYEND-ARRAY)/2 ;计算数组长度
    ;汇编后, MOV CX,7
    ;汇编时将ARRAY认为地址偏移量来计算
    

IX. 逻辑与移位操作

  • 举例
    OPR1 EQU 25 ; 00011001
    OPR2 EQU 7 ; 00000111
    AND AX, OPR1 AND OPR2 ; 汇编后等同 AND AX, 0001H
    
  • 注意:
    • 上例中第一个 AND 是指令系统的,第二个是伪操作

X. 关系操作符

  • 关系运算符有: EQ(等于)、 NE(不等于)、 LT(小于)、 GT(大于)、 LE(小于等于)、 GE(大于等于)
  • 关系运算符的两个操作数是数字或同一段内的两个存储器地址 ,计算的结果应为逻辑值;结果为真,逻辑值=0ffffh;结果为假,逻辑值0000H
  • 举例
    MOV FID, (OFFSET Y - OFFSET X) LE 128