Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

汇编语言 第四版 王爽著(上) #45

Open
Ray-56 opened this issue Sep 3, 2021 · 0 comments
Open

汇编语言 第四版 王爽著(上) #45

Ray-56 opened this issue Sep 3, 2021 · 0 comments
Labels
StudyNotes 读书笔记

Comments

@Ray-56
Copy link
Owner

Ray-56 commented Sep 3, 2021

汇编语言 读书笔记

汇编语言 第三版 王爽著

[TOC]

进制计算转化验证时可以使用计算机系统自带计算器开启编程模式,eg:MacOS

MacOS 计算器

1 基础知识

  1. 汇编指令是机器指令的助记符,同机器指令一一对应
  2. 每一种 CPU 都由自己的汇编指令集
  3. CPU 可以直接使用的信息在存储器中存放
  4. 在存储器中指令和数据没有任何区别,都是二进制信息
  5. 存储单元从零开始顺序编号
  6. 一个存储单元可以存储8个 bit,即8位二进制数
  7. 1Byte=8bit 1KB=1024B 1MB=1024KB 1GB=1024MB

Byte 字节,bit 比特
1KB = 1024Byte(B) = 8 * 1024bit

2 寄存器

2.1 通用寄存器

AX = AH + AL,高字节位,低字节位

2.2 字在寄存器中的存储

十六进制,汇编H结尾,C语言0x开头
Byte 字节, word 字,一个字由两个字节组成

2.3 几条汇编指令

AH AL 在使用中都是作为独立的 8 位寄存器

检测点2.1

62627 = 0xf4a3

(1) 写出每条汇编指令执行后相关寄存器中的值:

mov ax,62627; # AX=f4a3h
mov ah,31h;   # AX=31a3h
mov al,23h;   # AX=3123h
add ax,ax;    # AX=6246h
mov bx,826ch; # AX=6246h BX=826ch
mov cx,ax;    # AX=6246h BX=826ch CX=6246h
mov ax,bx;    # AX=826ch
add ax,bx;    # AX=04d8h 这里AX为16位寄存器,只能存放4位16进制的数据,所以最高位的1无法保存
mov al,bh;    # AX=0482h
mov ah,bl;    # AX=6c82h
add ah,ah;    # AX=d882h
add al,6;     # AX=d888h
add al,al;    # AX=d810h
add ax,cx;    # AX=6246h

(2) 只能使用目前学过的汇编指令,最多使用4条指令,编程计算2的4次方:

mov ax,2;     # AX=0002h
add ax,ax;    # AX=0004h
add ax,ax;    # AX=0008h
add ax,ax;    # AX=0016h

2.4 物理地址

CPU 通过地址总线送入存储器的必须是一个内存单元的物理地址

2.5 十六位结构的CPU

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

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

  • CPU 相关部件提供两个16位的地址,分别为段地址偏移地址
  • 段地址和偏移地址通过内部总线送入地址加法器的部件
  • 地址加法器将两个16位地址合成一个20位的物理地址
  • 地址加法器同工内部总线将20位物理地址送入输入输出控制电路
  • 输入输出控制电路将20位物理地址送上地址总线
  • 20位物理地址被地址总线传送到存储器

地址加法器:物理地址=段地址x16+偏移地址

“段地址x16” 的常用说法是左移4位(通常指二进制位)

由观察数据2H二进制形式10B,对其进行左移运算可以得到:一个X进制的数据左移1位,相当于乘以X

2.7 “段地址x16+偏移地址=物理地址”的本质含义

本质含义:CPU 在访问内存时,用一个基础地址(段地址x16)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址也就是“基础地址+偏移地址=物理地址”

学校、体育馆、图书馆的位置关系,摘自汇编语言第四版

三种方式表示图书馆的地址:

  • 从学校走2826到图书馆。2826可以认为是图书馆的物理地址
  • 从学校走2000基础地址到体育馆,再从体育馆走826偏移地址(以基础地址为起点)到图书馆
  • 加限制两张三位数据纸条,第一张写上200段地址,第二张写上826偏移地址,再运算200x10+826=2826

8086CPU 就相当于提供两张3位数据纸条的 CPU

2.8 段的概念

小结:

CPU 可以使用不同的段地址和偏移地址形成同一个物理地址。eg:
    物理地址    段地址  偏移地址
    21F60H      2000H   1F60H
                2100H   0F60H
                21F6H   0000H
                1F00H   2F60H
                
偏移地址 16 位,变化范围为0~FFFFH,仅用偏移地址来寻址最多可寻到 64KB 个内存单元。

检测点2.2

公式:段地址(SA)x16 + 偏移地址(EA) = 物理地址
这里的 16 是十进制,计算时需要先进行转换

  1. 0010H ~ 1000FH

    由题意可知偏移地址范围 0000H ~ FFFFH
    min = 0001H x 16 + 0000H, min = 0010H
    max = 0001H x 16 + FFFFH, max = 1000FH

  2. 1001H ~ 2000H TODO: 不是很明白,需要后续再回头研究

    偏移地址(EA)范围 0000H ~ FFFFH
    带入公式得到段地址(SA)范围 1000H ~ 2000H,答案是这样么?
    根据提示反过来思考 SA = 0000H 时,1000Hx16+FFFFH=1FFFFH,得到结果并不是 20000H
    因为这不是一道计算题,要考虑到逻辑位移
    因此 1001Hx16+FFF0H=20000H

2.9 段寄存器

8086CPU 有四个段寄存器:CS、DS、SS、ES。需要访问内存时提供内存单元的段地址。本章只看 CS

2.10 CS 和 IP

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

在内存中,指令和数据没有任何区别,都是二进制信息,CPU 在工作的时候把有的信息看作指令,有的看作数据。**CPU 将 CS、IP 中的内容当作指令的段地址和偏移地址,用它们合成指令的物理地址,到内存中读取指令码,执行。**如果说,内存中的一段信息曾被 CPU 执行过的话,那么,它所在的内存单元必然被 CS:IP 指向过

2.11 修改 CS、IP 的指令

jmp指令可以修改 CS、IP。

  • jmp 段地址:偏移地址(段内转移):用指令中给出的段地址修改 CS,偏移地址修改 IP
  • jmp 某一合法寄存器(段间转移):用寄存器中的值修改 IP

问题2.3

指令执行顺序为:

mov ax,6622H
jmp 1000:3
mov ax,0000
mov bx,ax
jmp bx
mov ax,0123H; # 后面再执行 mov ax,0000

2.12 代码段

对于 8086PC 机,可以根据需求,将一组内存单元定义为一个段。
我们可以将长度为 N(N<=64KB) 的一组代码,存在一组地址连续、起始地址为 16 的倍数的内存单元中,我们可以认为,这段内存是用来存放代码的,从而定义了一个代码段。

2.9~2.12 小结

  1. 段地址在 8086CPU 的段寄存器中存放。当 8086CPU 要访问内存时,由段寄存器提供内存单元的段地址。8086CPU 有 4 个段地址,其中 CS 用来存放指令的段地址。
  2. CS 存放指令的段地址,IP 存放指令的偏移地址

    8086 机中,任意时刻,CPU 将 CS:IP 指向的内容当作指令执行

  3. 8086CPU 的工作过程:
    1. 从 CS:IP 执行的内存单元读取指令,读取的指令进入指令缓冲器
    2. IP 指向下一条指令
    3. 执行指令。(转到步骤1,重复这个过程)
  4. 8086CPU 提供转移指令修改 CS、IP 的内容

检测点2.3

下面指令执行后,CPU 几次修改 IP?都是在什么时候?最后 IP 中的值是多少?

mov ax,bx
sub ax,ax
jmp ax

遵循指令被送入指令缓冲器 -> IP值自动增加 -> 指令被执行的顺序:四次修改

  1. mov ax,bx被送到指令缓冲器内,IP 自动增加【第一次修改】
  2. mov ax,bx被执行
  3. sub ax,ax被送到指令缓冲器内,IP 自动增加【第二次修改】
  4. sub ax,ax被执行,ax=ax-ax,ax中的内容为0000H
  5. jmp ax被送到指令缓冲器中,IP 自动增加【第三次修改】
  6. jmp ax被执行,将 IP 的内容修改成0000H【第四次修改】

Debug 的一些功能

  • R 查看、改变 CPU 寄存器的内容
  • D 查看内存中的内容
  • E 改写内存中的内容
  • U 将内存中的机器指令翻译成汇编指令
  • T 执行一条机器指令
  • A 以汇编指令的格式在内存中写入一条机器指令

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

由于使用 MAC 所有下面都用实验楼中的环境编码

汇编语言(第 2 版,郑晓薇著)配套实验 免费

3 寄存器(内存访问)

从访问内存角度继续学习几个寄存器

3.1 内存中字的存储

任何两个地址连续的内存单元,N 号单元和 N+1 号单元,可以将它们看成两个内存单元,也可以看成一个地址为 N 的字单元中的低位字节单元高位字节单元

例如:AX = 4E20HAH = 4EH(高位),AL = 20H(低位),AX 存放在2地址字单元(2单元+3单元)中,2单元存放低位20H,3单元存放高位4EH

3.2 DS 和[address]

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

问题3.2

写几条指令,将 al 中的数据送入内存单元 10000H 中

mov bx,1000h
mov ds,bx
mov [0],al

分析:10000H = 1000H x 16 + 0H 可以表示为 1000:0,地址段 1000H,偏移地址 0

3.3 字的传送

mov bx,1000h
mov ds,bx
mov ax,[0];  1000:0 处的字型数据送入 ax
mov [0],cx;  cx 中的 16 为数据送到 1000:0 处

3.4 mov、add、sub 指令

mov 寄存器,数据 例如:mov ax,8
mov 寄存器,寄存器 例如:mov ax,bx
mov 寄存器,内存单元 例如:mov ax,[0]
mov 内存单元,寄存器 例如:mov [0],ax
mov 段寄存器,寄存器 例如:mov ds,ax

add,sub 不能对段寄存器进行操作,其它与 mov 一致

3.5 数据段

将一组长度为 N(N<=64KB)、地址连续、起始地址为 16 倍数的内存单元当作专门储存数据的内存单元,从而定义一个数据段

比如,将 123B0H~123B9H 的内存单元定义为数据段。现在要累加这个数据段中的前 3 个单元中的数据,代码如下:

mov ax,123BH
mov ds,ax           ; 将 123BH 送入 ds 中,做为数据段的段地址
mov al,0            ; 用 al 存放累加结果
add al,[0]          ; 将数据段的第一个单元(偏移地址为0)中的数值加到 al 中
add al,[1]          ; 将数据段的第二个单元(偏移地址为1)中的数值加到 al 中
add al,[2]          ; 将数据段的第三个单元(偏移地址为2)中的数值加到 al 中

问题 3.5

写几条指令,累加数据段中的前 3 个字型数据,注意,一个字型数据占两个单元,所以偏移地址为 0、2、4

mov ax,123BH
mov ds,ax
mov ax,0
add ax,[0]; 数据段中的第一个字
add ax,[2]
add ax,[4]

3.1~3.5 小结

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

检测点 3.1

(2):内存中的情况如图所示。
各寄存器的初始值:CS=2000H,IP=0,DS=1000H, AX=0,BX=0;

  1. 写出 CPU 执行的执行序列(用汇编指令写出)
  2. 写出 CPU 执行的每条指令后,CS、IP 和相关寄存器中的值
  3. 再次体会:数据和程序有区别吗?如何确定内存中的信息哪些是数据?哪些是程序?
    内存中的情况示意
mov ax, 6622H;      CS:IP=2000:0000, DS=1000H, AX=6622H, BX=0000H
jmp 0ff0:0100;      CS:IP=0ff0:0100, DS=1000H, AX=6622H, BX=0000H, 得到地址为 0ff0x16+0100 = 10000
mov ax,2000H;       CS:IP=0ff0:0103, DS=1000H, AX=2000H, BX=0000H
mov ds,ax;          CS:IP=0ff0:0105, DS=2000H, AX=2000H, BX=0000H
mov ax,[0008];      CS:IP=0ff0:0108, DS=2000H, AX=C389H, BX=0000H
mov ax,[0002];      CS:IP=0ff0:010B, DS=2000H, AX=EA66H, BX=0000H

数据和程序没有区别,本质都是二进制 01 码,当内存单元被 CS:IP 指定时其存储就被当作程序执行。当内存单元被 DS:[address] 指定时其存储的就是数据。

3.6 栈

LIFO(Last In First Out,后进先出)

3.7 CPU 提供的栈机制

PUSH入栈,POP出栈,8086CPU 的入栈和出栈都是以字为单位进行的。
字型数据用两个单元存放,高地址单元存放高 8 位,低地址单元存放低 8 位。
任意时刻,SS:SP 指向栈顶元素。 SS 段寄存器存放段地址,SP 寄存器存放偏移地址

3.8 栈顶超界的问题

8086CPU不保证我们对栈的操作不会超界。它只知道栈顶在何处(由 SS:SP 指示),而不知道栈空间有多大。这点就好像它只知道当前要执行的指令在何处(由 CS:IP 指示),而不知道要执行的指令有多少。

从这两点可以看出 8086CPU 的工作机理,只考虑当前的情况:当前栈顶在何处、要执行的指令是哪一条

3.9 push、pop 指令

可以在寄存器和内存(栈空间也是内存空间的一部分,它只是一段可以以特殊方式今昔访问的内存空间)之间传递数据

push 寄存器     ; 将一个寄存器中的数据入栈
pop 寄存器      ; 出栈,用一个寄存器接收出栈的数据
push 段寄存器   ; 将一个段寄存器中的数据入栈
pop 段寄存器    ; 出栈,用一个段寄存器接收出栈的数据
push 内存单元   ; 将一个内存字单元处的字入栈(注意:栈操作都是以字为单位的)
pop 内存单元    ; 出栈,用一个内存字单元接收出栈的数据

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

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

问题 3.7

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

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

问题 3.8

编程:

  1. 将 10000H~1000FH 这段空间当作栈,初始状态栈为空
  2. 设置 AX=001AH,BX=001BH
  3. 将 AX、BX 中的数据入栈
  4. 然后将 AX、BX 清零
  5. 从栈中恢复 AX、BX 原来的内容
mov ax,1000H
mov ds,ax
mov sp,0010H    ; 栈顶为空,sp=栈底(栈空间高位)+16=000FH=+16=0010H
mov ax,001AH
mov bx,001BH
push ax
push bx

sub ax,ax       
sub bx,bx       ; 清零也可以使用 mov bx,0
                ; sub bx,bx 的机器码为 2 个字节
                ; mov bx,0 的机器码为 3 个字节
pop bx          ; 后进先出,所以先送入 bx
pop ax

栈的综述

  1. 8086CPU 提供了栈操作机制,方案如下

    在 SS、SP 中存放栈顶的段地址和偏移地址:提供入栈和出栈指令,它们根据 SS:SP 指示的地址,按照栈的方式访问内存单元

  2. push 指令的执行步骤:(1)SP=SP-2;(2)向 SS:SP 指向的字单元中送入数据
  3. pop 指令的执行步骤:(1)从 SS:SP 指向的字单元中读取数据;(2)SP=SP+2
  4. 任意时刻,SS:SP 指向栈顶元素
  5. 8086CPU 只记录栈顶,栈空间的大小要自己管理
  6. 用栈来暂存以后需要恢复的寄存器的内容时,寄存器出栈的顺序要和入栈顺序相反
  7. push、pop 实质上是一种内存传送指令,注意它们的灵活应用

3.10 栈段

我们可以将长度为 N(N<=64KB)的一组地址连续、起始地址为 16 的倍数的内存单元,当作栈空间来用,从而定义了一个栈段

段的综述

将一段内存定义为一个段,用一个段地址指示段,用偏移地址访问段内的单元。

  • 用一个段存放数据,将它定义为“数据段”;
  • 用一个段存放代码,将它定义为“代码段”;
  • 用一个段当作站,将它定义为“栈段”;

若要让 CPU 按照我们的安排来访问这些段,就要

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

由上可见:CS:IP 指向地址当作代码,SS:SP 指向地址当作栈

比如将 10000H~1001FH 安排为代码段,并在里面存储如下代码:

mov ax,1000H
mov ss,ax
mov sp,0020H    ; 初始化栈顶
mov ax,cs
mov ds,ax       ; 设置数据段段地址
mov ax,[0]
add ax,[2]
mov bx,[4]
add bx,[6]
push ax
push bx
pop ax
pop bx

设置 CS=1000H,IP=0,这段代码将执行。10000H~1001FH,即是代码段,又是栈段和数据段

一段内存,可以即是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么都不是。关键在于 CPU 中的寄存器设置,即 CS、IP、SS、SP、DS 的指向

检测点 3.2

(1)补全下面的程序,使其可以将 10000H1000FH 中的 8 个字,逆序复制到 20000H2000FH 中。逆序复制的含义如下图所示:
逆序复制示意图

mov ax,1000H
mov ds,ax
; ----补全代码开始----
mov ax,1000H
mov ss,ax
mov sp,0010H

; ----补全代码结束----
push [0]
push [2]
push [4]
push [6]
push [8]
push [A]
push [C]
push [E]

(2)由(1)得到的再逆序回去

代码段中使用 pop,所以 sp 为栈顶地址 0000H

实验2 用机器指令和汇编指令编程

TODO

第4章 第一个程序

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

一个汇编语言程序从写出到执行的过程

  1. 编写汇编源程序

    使用文本编辑器,用汇编语言编写汇编源程序。
    工作结果:产生了一个存储源程序的文本文件

  2. 对源程序进行编译连接

    使用汇编语言编译程序对源程序文件中的源程序进行编译,产生目标文件:再用连接程序对目标文件进行连接,生成可在操作系统中直接运行的可执行文件(包含下面两部分内容)。
    工作结果:产生了一个可在操作系统中运行的可执行文件

    • 程序(从源程序从的汇编指令翻译过来的机器码)和数据(源程序中定义的数据)
    • 相关的描述信息(比如,程序有多大、要占用多少内存空间等)
  3. 执行可执行文件中的程序

    操作系统依照可执行文件中的描述信息,将可执行文件中的机器码和数据加载入内存,并进行相关的初始化(比如设置 CS:IP 执行第一条要执行的指令),然后由 CPU 执行程序

4.2 源程序

assume cs:codesg

codesg segment
    mov ax,0123H
    mov bx,0456H
    add ax,bx
    add ax,ax
    
    mov ax,4c00h
    int 21H
    
codesg ends

end

1.伪指令

汇编语言源程序中包含两种指令

  • 汇编指令 - 有对应的机器码指令,可以被编译为机器指令,被 CPU 执行
  • 伪指令 - 没有对应的机器码指令,不被 CPU 执行,编译器来执行,根据伪指令来进行相关的编译工作

上面程序中出现了 3 中伪指令

  1. 段名 segment 段名 ends - 成对使用,定义一个段(用来存放代码、数据或栈空间来使用),ends的含义可理解为“end segment”
  2. end - 汇编程序的结束标记
  3. assume - “假设”,它假设某一段寄存器和程序从的某一个用segment...ends定义的段相关联

2.源程序中的“程序”

用汇编语言写的源程序,包括伪指令和汇编指令。
将源程序文件中的所有内容称为 源程序,源程序中最终由计算机执行、处理的指令或数据,称为 “程序”

3.标号

一个标号指代了一个地址。比如:codesgsegment的前面,作为一个段的名称,这个段的名称最终将被编译、连接程序处理为一个段的段地址

4.程序的结构

任务:编程运算 2^3。

(1)定义一个段,名称为 abc

+ abc segment

+ abc ends

(2)写入汇编指令,实现任务

abc segment
+   mov ax,2
+   add ax,ax
+   add ax,ax
abc ends

(3)指出在何处结束

abc segment
    mov ax,2
    add ax,ax
    add ax,ax
abc ends

+ end

(4)abc 被当作代码段来用,所以将 abc 和 cs 联系起来

+ assume cs:abc

abc segment
    mov ax,2
    add ax,ax
    add ax,ax
abc ends

end

5.程序返回

一个程序结束后,将 CPU 的控制权交还给使它运行的程序,这个过程为:程序返回

mov ax,4c00H
int 21H

上述代码中的两条指令所实现的功能就是程序返回

6.语法错误和逻辑错误

一般来说,编译时被编译器发现的错误是语法错误,运行时发生的错误是逻辑错误

4.3 编辑源程序

4.4 编译

使用 masm 1.asm->1.obj

如果编译的过程中出现错误,那么将得不到目标文件。一般来说,有两类错误使我们得不到目标文件

  • 程序中有“Severe Errors”
  • 找不到所给出的源程序文件

在编译过程中,提供一个输入(源程序文件)。最多可以得到 3 个输出:目标文件(.obj)、列表文件(.list)、交叉引用文件(.crf)。目标文件是要得到的最终结果

4.5 连接

使用 link 1.obj->1.exe

连接的作用:

  • 源程序很大时,可以分为多个源程序文件(编译成目标文件)来编译,再用连接程序将多个目标文件连接到一起,生成一个可执行文件
  • 程序中调用了某个库文件中的子程序,需要将这个库文件和该程序生成的目标文件连接到一起,生成一个可执行文件
  • 单个源程序编译后,得到了存有机器码的目标文件(中有些内容还不能直接用来生成可执行文件),连接程序将这些内容处理为最终的可执行信息

4.6 以简化的方式进行编译和连接

  • masm c:\1; 当前路径下生成 1.obj,并忽略中间文件的生成
  • link 1; 当前路径下生成可执行文件 1.exe,并忽略中间文件的生成

4.7 1.exe 的执行

4.8 谁将可执行文件中的程序装载进入内存并使它运行?

观察 1.exe 的执行过程,思考问题

  1. 在提示符c:\后面输入可执行文件的名字“1”,按 Enter 键。这时思考问题4.1
  2. 1.exe 中的程序运行
  3. 运行结束后,返回,再次显示提示符c:\。思考问题4.2

问题4.1

此时,有一个正在运行的程序将 1.exe 中的程序加载入内存,这个正在运行的程序是什么?将程序加载入内存后,如何是使程序运行?

在 DOS 中直接执行 1.exe 时,是正在运行的 command 将 1.exe 中的程序加载入内存

command 设置 CPU 的 CS:IP 指向程序的第一条指令(即程序的入口),从而使程序得以运行

问题4.2

程序运行结束后,返回到哪里?

返回到 command 中,CPU 继续运行 command

汇编程序从写出到执行的过程

编程(Edit) -> 1.asm -> 编译(masm) -> 1.obj -> 连接(link) -> 1.exe -> 加载(command) -> 内存中的程序 -> 运行(CPU)

操作系统的外壳(shell)

操作系统是由多个功能模块组成的庞大、复杂的软件系统。任何通用的操作系统,都要提供一个称为 shell(外壳)的程序,用户(操作人员)使用这个程序来操作计算机系统进行工作。

DOS 中有一个程序 command.com,这个程序在 DOS 中称为命令解释器,也就是 DOS 系统的 shell。

DOS 启动时,先完成其他重要的初始化工作,然后运行 command.com,执行完其它的相关任务后,在屏幕上显示出由当前路径组成的提示符,比如:“c:\”或“c:\windows”等,然后等待用户的输入。

用户可以输入要执行的命令,比如:cd、dir、type 等,这些命令由 command 执行,执行完这些命令后,再次显示由当前盘符和当前路径组成的提示符,等待用户的输入。

如果用户要执行一个程序,则输入改程序的可执行文件的名称,command 首先根据文件名找到可执行文件,然后将这个可执行文件加载入内存,设置 CS:IP 执行程序的入口。此后 command 暂停运行,CPU 运行程序。结束后返回到 command 中,command 再次显示由当前盘符和当前路径组成的提示符,等待用户输入。

在 DOS 中,command 处理各种输入:命令或要执行的程序的文件名。我们就是通过 command 来进行工作的。

4.9 程序执行过程的跟踪

debug 1.exe,Debug将程序从可执行文件加载入内存

U 命令查看其它指令

T 命令单步执行程序中的每一条指令,观察指令的执行结果

int 21时,要使用 P 命令执行(不做深入研究,记住int 21使用 P 命令执行即可)

实验3 编程、编译、连接、跟踪

(1)将下面的程序保存为 t1.asm 文件,生成可执行文件 t1.exe

在 DOSBox 中运行下面命令:

edit t1.asm
masm c:\t1;
link t1;

mydosbox的目录

(2)用 Debug 跟踪 t1.exe 的执行过程,写出每一步执行后,相关寄存器中的内容和栈顶的内容

使用debug t1.exe加载程序后:

ax=FFFFH;bx=0000H;
cx=0016H(表示程序长度是 22 个字节)
ds=075AH(表示操作系统给 t1.exe 分配的空间区域的段地址)
cs=076AH(cs=ds+10H)
ss=0769;sp=0000H; 使用d 0769:0 1命令查看栈顶内容为 0000H

运行结果显示的汇编指令(MOV AX,2000)为下一条即将执行的指令

  1. 单步执行第一条指令mov ax,2000H
    ax=2000H;栈顶内容为 0000H
  2. 单步执行第二条指令mov ss,ax后,自动连带执行第三条mov sp,0、第四条指令add sp,10
    这里注意第四条执行,10 的十六进制为 000AH;ss=2000H;栈顶内容为 0000H
  3. 单步执行第五条指令pop ax
    sp=000AH;栈顶内容为 0000H
  4. 单步执行第六条指令pop bx
    sp=sp+2=000CH;栈顶内容为 0000H
运行步骤结果显示

debug t1.exe载入程序后t查看寄存器的信息

d SS:SP SP+1查看SP(指定地址1)到SP+1(指定地址2)内存单元的内容。

  1. 单步执行第一条指令后
  2. 需要注意执行mov ss,ax后,自动连带执行mov sp,0

  3. 10 的十六进制为000AH

  4. 执行 pop 后,sp=sp+2

  5. 执行 push 后,sp=sp-2

使用p指令执行int 21H

(3)PSP 的头部两个字节是 CD 20,用 Debug 加载 t1.exe,查看 PSP 的内容

第五章 [BX]和 loop 指令

(1)[bx]和内存单元的描述

[bx]和[0]有些类似

  • [0]表示内存单元,它的偏移地址为 0。
  • [bx]同样也表示一个内存单元,它的偏移地址在 bx 中。
mov ax,[0]  ; 将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址为 0,段地址在 ds 中。
mov al,[0]  ; 将一个内存单元的内容送入 al,这个内存单元的长度为 1 字节(字节单元),存放一个字节,偏移地址为 0,段地址在 ds 中。
mov ax,[bx] ; 将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址在 bx 中,段地址在 ds 中。
mov al,[bx] ; 将一个内存单元的内容送入 ax,这个内存单元的长度为 1 字节(字节单元),存放一个字节,偏移地址在 bx 中,段地址在 ds 中。

完整地描述一个内存单元需要两种信息:1、内存单元的地址;2、内存单元的长度(类型)。
用 [0] 表示一个内存单元时,0 表示单元的偏移地址,段地址默认在 ds 中,单元的长度(类型)可以由指令中的其它操作对象(比如寄存器)指出。

(2)loop

这个指令和循环有关

(3)我们定义的描述性符号:“()”

以后我们将使用一个描述性符号“()”来表示一个寄存器或一个内存单元中的内容。比如:

  • (ax)表示 ax 中的内容、(al)表示 al 中的内容
  • 20000H表示内存 20000H 的内容(()中的内存单元地址为物理地址)
  • ((ds)*16+(bx))表示为:
    • ds 中的内容为 ADR1,bx 中的内容为 ADR2,内存 ADR1x16+ADR2 单元的内容
    • 也可以理解为:ds 中的 ADR1 作为段地址,bx 中的 ADR2 作为偏移地址,内存 ADR1:ADR2 单元的内容
      注意:"()"中的元素可以有 3 中类型:1、寄存器名;2、段寄存器名;3、内存单元的物理地址(一个 20 位数据)。比如:
  • (ax)(ds)(al)(cx)(20000H)((ds)*16+(bx))等是正确的用法
  • (2000:0)((ds):1000H)等是不正确的用法

(X)的应用,比如:

  • ax 中得内容为 0010H,描述:(ax)=0010H
  • 2000:1000 处的内容为 0010H,描述:(21000H)=0010H
  • 对于 mov ax,[2] 的功能,描述:(ax)=((ds)*16+2)
  • 对于 mov [2],ax 的功能,描述:((ds)*16+2)=(ax)
  • 对于 add ax,2 的功能,描述:(ax)=(ax)+2
  • 对于 add ax,bx 的功能,描述:(ax)=(ax)+(bx)
  • 对于 push ax 的功能,描述:(sp)=(sp)-2; ((ss)*16+(sp))=(ax)
  • 对于 pop ax 的功能,描述:(ax)=((ss)*16+(sp)); (sp)=(sp)+2

(X)所表示的数据有两种类型:1、字节;2、字;由寄存器名或具体的运算决定,比如:

  • (al)(bl)(cl)等得到的数据为字节型;
  • (ds)(ax)(bx)等得到的数据为字型;
  • (al)=(20000H)、则 (20000H) 得到的数据为字节型;
  • (ax)=(20000H)、则 (20000H) 得到的数据为字型;

(4)约定符号 idata 表示常量

在 Debug 中写过类似的指令:mov ax,[0],表示将 ds:0 处的数据送入 ax 中。指令中,在“[...]”里用一个常量 0 表示内存单元的偏移地址。以后,用 idata 表示常量。比如:

  • mov ax,[idata] 代表 mov ax,[1]、mov ax,[2]、mov ax,[3]
  • mov bx,idata 代表 mov ax,1、 mov ax,2、 mov ax,3
  • mov ds,idata 代表 mov ds,1、 mov ds,2 等,它们都是非法指令

5.1 [BX]

mov ax,[bx] ; bx 中存放的数据作为一个偏移地址 EA,段地址 SA 默认在 ds 中,将 SA:EA 处的数据送入 ax 中。(ax)=((ds)*16+(bx))
mov [bx],ax ; bx 中存放的数据作为一个偏移地址 EA,段地址 SA 默认在 ds 中,将 ax 中的数据送入内存 SA:EA 中。((ds)*16+(bx))=(ax)

5.2 Loop 指令

格式是:loop 标号,CPU 执行 loop 指令的时候,要进行两步操作:

  1. (cx)=(cx)-1
  2. 判断 cx 中的值,不为零则转至标号处执行程序,如果为零则向下执行

通常我们用 loop 指令来实现循环功能,cx 中存放循环次数

(1)编程计算 2^3

assume cs:code
code segment
    mov ax,2
    add ax,ax
    add ax,ax
    
    mov ax,4c00h
    int 21h
code ends
end

(2)编程计算 2^12

assume cs:code
code segment
    mov ax,2
    
    mov cx,11
s: add ax,ax
    loop s
    
    mov ax,4c00h
    int 21h
code ends
end

用 cx 和 loop 指令配合实现循环功能的程序框架如下:

mov cx,循环次数
s:
    循环执行的程序段
    loop s

问题5.2

编程,用加法计算 123*236,结果存在 ax 中。

assume cs:code
code segment
    mov ax,0
    mov cx,236
s:add ax,123
    loop s
    
    mov ax,4c00h
    int 21h
code ends
end

问题5.3

改进 5.2,提高 123*236 的计算速度。

程序 5.2 做了 236 次加法,可以将 236 加 123 次。先设 (ax)=0,然后循环 123 次 (ax)=(ax)+236,这样可以用 123 次加法实现相同的功能

5.3 在 Debug 中跟踪用 loop 指令实现的循环程序

计算 ffff:0006 单元中的数乘以 3,结果存储在 dx 中。

(1)运算后的结果是否会超出 dx 所能存储的范围?

ffff:0006 单元中的数是一个字节型的数据,范围在 0~255,则用它和 3 相乘结果不大于 65535,可以在 dx 中存放

(2)用循环累加来实现乘法,用哪个寄存器进行累加?

将 ffff:0006 单元中的数赋值给 ax,用 dx 进行累加。先设 (dx)=0,然后做了 3 次 (dx)=(dx)+(ax)

(3)ffff:5 单元是一个字节单元,ax 是一个 16 位寄存器,数据的长度不一样,如何赋值?

注意,这里“赋值”是说让 ax 中的数据的值(数据的大小)和 ffff:0006 单元中的数据的值(数据的大小)相等。8 位数据 01H 和 16 位数据 0001H 的数据长度不一样,但它们的值是相等的。

如何赋值?设 ffff:0006 单元中的数据是 XXH,若要 ax 中的数据的值和 ffff:0006 单元中的相等,ax 中的数据应为 00XXH。所以,若实现 ffff:0006 单元向 ax 赋值,应该令 (ah)=0, (al)=(ffff6H)

assume cs:code
code segment
    mov ax,0ffffh
    mov ds,ax
    mov bx,6        ; 以上,设置 ds:bx 指向 ffff:6
    mov al,[bx]
    mov ah,0        ; 以上,设置 (al)=((ds)*16+(bx)), (ah)=0
    mov dx,0        ; 累加寄存器清零
    mov cx,3        ; 循环 3 次
s:add dx,ax
    loop s          ; 以上累加计算 (ax)*3
    mov ax,4c00h
    int 21h         ; 程序返回
code ends
end

注意程序中的第一条指令 mov ax,0ffffh。大于 9FFFH 的十六进制数据 A000H、A001H...C000H、C001H...FFFEH、FFFFH 等,在书写的时候都是以字母开头的。而在汇编程序中,数据不能以字母开头,所以要在前面加 0。比如:9138h 在汇编程序中可以写为“9138h”,而 A000h 在汇编程序中要写为“0A000h”

使用 u 命令查看被 Debug 加载入内存的程序。图中 ds=075A,所以程序在 076A 处(如果不清楚,可以复习 4.9 的内容)。(cs)=076a, (ip)=0, cs:ip 正指向程序的第一条指令。

从 076a:0000~076a:001b 是我们的程序,076a:0014 处是源程序中的指令 loop s,此处标号 s 已经变为了一个地址 0012h。如果在执行“loop 0012”时,cx 减 1 后不为 0,“loop 0012”就把 IP 设置为 0012h,实现跳转

可以使用 g 命令,g 0012,表示执行程序到当前代码段(段地址在 CS 中)的0012h处

5.4 Debug 和汇编编译器 masm 对指令的不同处理

mov ax,[]指令的不同表示

  • 在 Debug 中表示将 ds:0 处的数据送入 ax 中
  • 在汇编程序中,被编译器当作指令mov ax,0处理

目前可以将偏移地址送入 bx 寄存器中,用[bx]的方式来访问内存单元。比如:

mov ax,2000h
mov ds,ax       ; 段地址 2000h 送入 ds
mov bx,0        ; 偏移地址 0 送入 bx
mov al,[bx]     ; ds:bx 单元中的数据送入 al

比较汇编源程序以下指令的含义:

mov al,[0]      ; 含义:(al)=0,将常量 0 送入 al 中,与 mov al,0 含义相同
mov al,ds:[0]   ; 含义:(al)=((ds)*16+0),将内存单元中的数据送入 al 中
mov al,[bx]     ; 含义:(al)=((ds)*16+(bx)),将内存单元中的数据送入 al 中
mov al,ds:[bx]  ; 含义:与 mov al,[bx] 相同

从上面代码可以看出:在汇编源程序中

  1. 如果用指令访问一个内存单元,则在指令中必须用“[...]”来表示内存单元,如果在“[]”里用一个常量 idata 直接给出内存单元的偏移地址,就要在“[]”的前面显式地给出段地址所在的段寄存器。比如 mov al,ds:[0],如果没有显示给出,比如mov al,[0]那么编译器 masm 将把指令中的“[idata]”解释为“idata”
  2. 如果在“[]”里用寄存器,比如 bx,间接给出内存单元的偏移地址,则段地址默认在 ds 中

5.5 loop 和[bx]的联合应用

计算 ffff:0~ffff:b 单元中的数据的和,结果存储在 dx 中。

分析:

  1. 运算后的结果是否会超出 dx 所存储的范围?

    ffff:0ffff:b 内存单元的数据是字节型数据,范围在 0255 之间,12 个这样的数据相加,结果不会大于 65535,dx 中可以存放的下

  2. 能否将 ffff:0~ffff:b 中的数据直接累加到 dx 中?

    不可以,ffff:0~ffff:b 中的数据是 8 位的,不能直接加到 16 位寄存器 dx 中

  3. 能否将 ffff:0~ffff:b 中的数据累加到 dl 中,并设置 (dh)=0,从而实现累加到 dx 中?

    也不可以,因为 dl 是 8 位寄存器,能容纳的数据的范围在 0255 之间,ffff:0ffff:b 中的数据也都是 8 位,如果仅向 dl 中累加 12 个 8 位数据,很有可能造成进位丢失

  4. 到底怎样将 ffff:0~ffff:b 中的 8 位数据,累加到 16 为寄存器 dx 中?
    1. (dx)=(dx)+内存中的 8 位数据,问题是两个运算对象的类型不匹配
    2. (dl)=(dl)+内存中的 8 位数据,问题是结果有可能超界

    目前的方法就是用一个 16 位寄存器来做中介。将内存单元中的 8 位数据赋值到一个 16 位寄存器 ax 中,再将 ax 中的数据加到 dx 上,从而使两个运算对象的类型匹配并且结果不会超界

assume cs:code
code segment
    mov ax,0ffffh
    mov ds,ax       ; 设置 (ds)=ffffh
    mov dx,0        ; 初始化累加寄存器,(dx)=0
    
    mov al,ds:[0]
    mov ah,0        ; (ax)=((ds)*16+0)=(ffff0h)
    add dx,ax       ; 向 dx 中加上 ffff:0 单元的数值
    
    mov al,ds:[1]
    mov ah,0        ; (ax)=((ds)*16+0)=(ffff0h)
    add dx,ax       ; 向 dx 中加上 ffff:1 单元的数值
    
    ;中间省略 ds:[2] 到 ds:[0bh] 的代码
    
    mov ax,4c00h
    int 21h
code ends
end

使用 loop 指令改写上面程序,可以看到有 12 个相似的程序段,描述:

mov al,ds:[X]   ; ds:X 指向 ffff:X 单元
mov ah,0        ; (ax)=((ds)*16+(X))=(ffffXh)
add dx,ax       ; 向 dx 中加入 ffff:X 单元的数值

可以得到最终程序:

assume cs:code
code segment
    mov ax,0ffffh
    mov ds,ax
    mov bx,0        ; 初始化 ds:bx 指向 ffff:0
    mov dx,0        ; 初始化累加寄存器 dx, (dx)=0
    mov cx,12       ; 初始化循环计数寄存器 cx,(cx)=12
s:  mov al,[bx]     ; 这里 bx 相当于一个变量
    mov ah,0
    add dx,ax       ; 间接向 dx 中加上((dx)*16+(bx))单元的数值
    inc bx          ; ds:bx 指向下一个单元
    loop s
    mov ax,4c00h
    int 21h
code ends
end

5.6 段前缀

mov ax,ds:[bx]  ; 将一个内存单元中的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址在 bx 中,段地址在 ds 中
mov ax,cs:[0]   ; 将一个内存单元中的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址为 0,段地址在 cs 中

上述访问内存单元的指令中,用于显式指明内存单元的段地址的“ds”、“cs”在汇编语言中成为段前缀

5.7 一段安全的空间

向一段内存中写入内容时:

  1. 这段内存空间不应该存放系统或其他程序的数据或代码,否则写入操作很可能引发错误
  2. DOS 方式下,一般情况,0:200~0:2ff 空间中没有系统或其他程序的数据或代码

以后需要直接向一段内存中写入内容时,就使用 0:200~0:2ff 这段空间

5.8 段前缀的使用

考虑一个问题,将内存 ffff:0ffff:b 单元中的数据复制到 0:2000:20b 单元中。

分析:

  1. 0:2000:20b 单元等同于 0020:00020:b 单元,它们描述的是同一段内存空间
  2. 复制的过程应用循环实现,简要描述:
    初始化:
    X=0
    循环12次:
    将 ffff:X 单元中的数据送入 0020:X(需要一个寄存器中转)
    X=X+1
    
  3. 在循环中,源始单元 ffff:X 和目标单元 0020:X 的偏移地址 X 是变量。用 bx 来存放
  4. 将 0:2000:20b 用 0020:00020:b 面熟,就是为了使目标单元的偏移地址和源始单元的偏移地址从同一数值 0 开始

程序 5.8 如下:

assume cs:code
code segment
    mov bx,0        ; (bx)=0,偏移地址从 0 开始
    mov cx,12       ; (cx)=12,循环 12 次
s:  mov ax,0ffffh
    mov ds,ax       ; (ds)=0ffffh
    mov dl,[bx]     ; (dl)=((ds)*16+(bx)),将 ffff:bx 中的数据送入 dl
    mov ax,0020h
    mov ds,ax       ; (ds)=0020h
    mov [bx],dl     ; ((ds)*16+(bx))=(dl),将 dl 中的数据送入 0020:bx
    inc bx          ; (bx)=(bx)+1
    loop s
    mov ax,4c00h
    int 21h
code ends
end

因源始单元 ffff:X 和目标单元 0020:X 相距大于 64KB,在不同的 64KB 段里,程序 5.8 中,每次循环要设置两次 ds,这样做法效率不高。可以使用两个段寄存器分别存放源始单元 ffff:X 和目标单元 0020:X 的段地址,这样就可以省略循环中需要重复做 12 次设置 ds 的程序段。改进的程序 5.9 如下:

assume cs:code
code segment
    mov ax,0ffffh
    mov ds,ax       ; (ds)=0ffffh
    mov ax,0020h
    mov es,ax       ; (es)=0020h
    mov bx,0        ; (bx)=0,此时 ds:bx 指向 ffff:0,es:bx 指向 0020:0
    mov cx,12       ; (cx)=12,循环 12 次
s:  mov dl,[bx]     ; (dl)=((ds)*16+(bx)),将 ffff:bx 中的数据送入 dl
    mov es:[bx],dl  ; ((es)*16+(bx))=(dl),将 dl 中的数据送入 0020:bx
    inc bx          ; (bx)=(bx)+1
    loop s
    mov ax,4c00h
    int 21h
code ends
end

程序 5.9 中,使用 es 存放目标空间 0020:00020:b 的段地址,用 ds 存放源始空间 ffff:0ffff:b 的段地址。访问内存单元的指令mov es:[bx],al中,显式地用段前缀“es”给出单元的段地址,这样就不必在循环中重复设置 ds

实验4 [bx]和 loop 的使用

(1)编程,向内存 0:2000:23F 依次传送数据 063(3FH)

先进行分析:

  1. 0:2000:23f 与 0020:00020:3f 内存空间一样。牢记公式内存地址=段地址x16+偏移地址
  2. 偏移地址是连续的内存单元,可以使用偏移地址作为变量
  3. 0~63(3FH),也就是需要循环 64 次(40H),因为 loop 指令 cx 为 0 则向下执行指令
assume cs:code
code segment
    mov ax,0020
    mov ds,ax       ; ds 段寄存器写入段地址 0020
    mov bx,0        ; bx 寄存器存放偏移地址
    mov dx,0        ; dx 寄存器作为要存入内存单元的值变量
    moc cs,40H      ; cs 寄存器存放循环次数
s:  mov [bx],dx     ; 向 ds:[bx] 内存单元写入 dx 中的数值
    inc bx
    inc dx
    loop s
    mov ax,4c00h
    int 21h
code ends
end

(2)编程,向内存 0:2000:23f 依次传送数据 063(3FH),程序中只能使用 9 条指令(包括“mov ax,4c00h”和“int 21h”)

先进行分析:上题中的 bx、dx 寄存器中的值在每次循环中都是一致的,去掉 dx 就可以了

assume cs:code
code segment
    mov ax,0020
    mov ds,ax
    mov bx,0
    mov cx,40H      ; 这里也可以写成 64
s:  mov [bx],bx     ; 这里 bx 既作为内存单元的地址,又作为数值
    inc bx
    loop s
    mov ax,4c00h
    int 21h
code ends
end

(3)下面程序的功能是将“mov ax,4c00h”之前的指令复制到内存 0:200 处,补全程序。上机调试,跟踪运行结果

assume cs:code
code segment
    mov ax,______   ; 填入 cs,查看分析1
    mov dx,ax
    mov ax,0020h
    mov es,ax
    mov bx,0
    mov cx,______   ; 填入 23 或者 17H,
s:  mov al,[bx]
    mov es:[bx], al
    inc bx
    loop s
    mov ax,4c00h
    int 21h
code ends
end
; 提示1、复制的是什么?从哪里到哪里?
; 提示2、复制的是什么?有多少个字节?你如何知道要复制的字节的数量?

先进行分析:

  1. 题目中“mov ax,4c00h”之前的指令是什么?指令也是数据,CS:IP 指向的就是指令,因此,mov ax,______这里填入 cs
  2. 寄存器 cx 是控制循环次数,那mov cx,______这里是要填入一个数字,代表循环的次数
    • 从下图中可以看到程序从mov ax,csmov ax,4c00h之前的内存占用为 076a:0~076a:17 也就是 17H=23
    • 具体占用的内存单元个数要通过上机操才可以看到,可以先随便写入一个数字让代码被编译、连接、执行跑起来,才可以看到真正的内存单元个数,我是使用 10 作为测试程序的循环个数

t5程序占用字节

第 6 章 包含多个段的程序

在操作系统环境中,合法地通过操作系统取得的空间都是安全的,因为操作系统不会让一个程序所用的空间和其它程序以及系统自己的空间相冲突。取得空间的方法有两种:

  1. 在加载程序的时候为程序分配
  2. 程序在执行的过程中相系统申请(课程中不讨论此方法)

一个程序在被加载的时候取得所需的空间,则必须要在源程序中做出说明。我们通过在源程序中定义段来进行内存空间的获取。

上面是从内存空间获取的角度上谈定义段的问题。我们再从程序规划的角度来谈一下定义段的问题。大多数有用的程序,都要处理数据,使用栈空间,当然也都必须有指令,为了程序设计上的清晰和方便,也都定义不同的段来存放它们。

6.1 在代码段中使用数据

考虑一个问题,编程计算以下 8 个数据的和,结果存在 ax 寄存器中:0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h

assume cs:code
code segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    mov bx,0
    mov ax,0
    mov cx,9
s:  add ax,cs:[bx]
    add bx,2
    loop s
    mov ax,4c00h
    int 21h
code ends
end

“dw”的含义是定义字型数据,即“define word”。在这里,定义了 8 个字型数据,占用空间大小为 16 个字节

程序中的指令就要对这 8 个数据进行累加**可这 8 个数据在哪里?**由于它们在代码段中,程序在运行的时候 CS 中存放代码段的段地址,所以可以从 CS 中得到它们的段地址。**它们的偏移地址是多少呢?**因为用 dw 定义的数据处于代码段的最开始,所以偏移地址为 0,这 8 个数据就在代码段的偏移 0、2、4、6、8、A、C、E 处。程序运行时,它们的地址就是段前缀+偏移,也就是 CS:0、CS:2 等。

程序中,用 bx 存放 2 递增的偏移地址,用循环来进行累加。

使用 Debug 来查看程序:

图中,通过“DS=075A”,得知程序从 076a:0 开始存放。用 u 命令查看程序,看到一些读不懂的指令。

在源程序中,在汇编指令前,有 16 个字节是用 dw 定义的数据,从 16 个字节后才是汇编指令对应的机器码。

用 d 命令清楚的查看程序中前 16 个字节的内容:

从 076a:0010 查看程序要执行的机器指令:

**怎样执行程序中的指令呢?**用 Debug 加载后,可以将 IP 设置为 10H,从而使 CS:IP 指向程序中的第一条指令。让后再用 t、p 或 g 命令执行。因为程序编译后的入口处不是所希望执行的指令,所以只能使用 Debug 来执行程序,正确的做法是指明程序中的入口所在,具体的做法:

assume cs:code
code segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    start:  mov bx,0
            mov ax,0
            mov cx,9
        s:  add ax,cs:[bx]
            add bx,2
            loop s
            mov ax,4c00h
            int 21h
code ends
end start

在程序的第一条指令的前面加上了一个标号 start,而这个标号在伪指令 end 的后面出现。**end 除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。**程序中用 end 指令指明了程序的入口在标号 start 处,也就是说mov bx,0是第一条指令。

在前面的课程中(参见 4.8 节),已经知道在单任务系统中,可执行文件中程序执行过程如下:

  1. 由其它的程序(Debug、command 或其它程序)将可执行文件中的程序载入内存
  2. 设置 CS:IP 指向程序的第一条要执行的指令(即程序的入口),从而使程序得以运行
  3. 程序运行结束后,返回到加载者

若要 CPU 从何处开始执行程序,要在源程序中用“end 标号”指明

6.2 在代码段中使用栈

利用栈将定义的数据逆序存放dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h

思路如下:

  1. 程序运行时,定义的数据存放在 cs:0~cs:F 单元中,共 8 个字单元
  2. 依次将这 8 个字单元中的数据入栈
  3. 再依次出栈到这 8 个字单元中

问题是,首先要有一段可以当作栈的内存空间。如前所述,这段空间应该由系统来分配。可以在程序中通过定义数据来取得一段空间,然后将这段空间当作栈空间来用。程序如下:

assume cs:codesg
codesg segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 
    ; 用 dw 定义 16 个字型数据,在程序加载后,将取得 16 个字的内存空间,存放这 16 个数据。在后面的程序中将这段空间当作栈来使用
start:  mov ax,cs
        mov ss,ax
        mov sp,30h      ; 将设置栈顶 ss:sp 指向 cs:30
        
        mov bx,0
        mov cx,8
    s:  push cs:[bx]
        add bx,2
        loop s          ; 以上将代码段 0~15 单元中的 8 个字型数据依次入栈
        
        mov bx,0
        mov cx,8
    s0: pop cs:[bx]
        add bx,2
        loop s0         ; 以上依次出栈 8 个字型数据到代码段 0~15 单元中
        
        mov ax,4c00h
        int 21h
codesg ends
end start               ; 指明程序的入口在 start 处

代码段中定义了 16 个字型数据,它们的数值都是 0。这 16 个字型数据的值是多少,对程序来说没有意义。我们用 dw 定义 16 个数据,即在程序中写入了 16 个字型数据,而程序在加载后,将用 32 个字节的内存空间来存放它们。这段内存空间是我们所需要的,程序将它用作栈空间。可见,定义这些数据的最终目的是,通过它们取得一定容量的内存空间。所以在描述 dw 的作用时,可以说它定义数据,也可以说用它开辟内存空间

检测点 6.1

(1)下面的程序实现一次用内存 0:0~0:15 单元中的内容改写程序中的数据,完成程序:

assume cs:codesg
codesg segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
start:  mov ax,0
        mov ds,ax   ; ds 指向 0000 段内存
        mov bx,0    ; bx 偏移地址归零
        mov cx,8    ; loop 的次数
    s:  mov ax,[bx] ; 将 ds:[bx] 内存单元中的内容送入 ax 中
        ______      ; 填入“mov cs:[bx],ax”,将 ax 中的内容写入 code 段的前 16 个字节单元中
        add bx,2
        loop s
        mov ax,4c00h
        int 21h
codesg ends
end start

(2)下面的程序实现依次用内存 0:0~0:15 单元中的内容改写程序中的数据,数据的传送用栈来进行。栈空间设置在程序内。补全程序:

assume cs:codesg
codesg segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
    dw 0,0,0,0,0,0,0,0,0,0      ; 10 个字单元用作栈空间
start:  mov ax,______       ; cs
        mov ss,ax
        mov sp,______       ; 24H = 18 个字单元 = 36 个字节单元
        
        mov ax,0
        mov ds,ax
        mov bx,0
        mov cx,8
    s:  push [bx]
        __________          ; pop ss:[bx]
        add bx,2
        loop s
        
        mov ax,4c00h
        int 21h
codesg ends
end start

6.3 将数据、代码、栈放入不同的段

前面的内容中,在程序中用到了数据和栈,将数据、栈和代码都放到一个段内。这样做有两个问题:

  1. 放在一个段内使程序显得混乱
  2. 前面的程序中处理的数据很少,用到的栈空间也小,加上代码量也不多,放在一个段内没有问题。如果数据、栈和代码需要的空间超过 64KB,就不能放在一个段中(一个段的容量不能大于 64 KB,是我们在学习中所用到的 8086 模式的限制,并不是所有的处理器都是这样)

程序 6.4:

assume cs:code,ds:data,ss:stack

data segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
    dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends

code segment
start:  mov ax,stack
        mov ss,ax
        mov sp,20h      ; 设置栈顶 ss:sp 指向 stack:20
        
        mov ax,data
        mov ds,ax       ; ds 指向 data 段
        
        mov bx,0        ; ds:bx 指向 data 段中的第一个单元
        
        mov cx,8
    s:  push [bx]
        add bx,2
        loop s          ; 以上将 data 段中的 0~15 单元中的 8 个字型数据依次入栈
        
        mov bx,0
        
        mov cx,8
    s0: pop [bx]
        add bx,2
        loop s0         ; 以上依次出栈 8 个字型数据到 data 段的 0~15 单元中
        
        mov ax,4c00h
        int 21h
code ends
end start

下面对程序 6.4 做出说明

(1)定义多个段的方法

定义一个段的方法和前面讲的定义代码段的方法没有区别,只是对于不同的段,要有不同的段名

(2)对段地址的引用

  • 程序中有多个段了,如何访问段中的数据呢?

    通过地址,而地址分为两部分,即段地址和偏移地址

  • 如何指明要访问数据的段地址呢?

    段名就相当于一个标号,它代表了段地址

  • 偏移地址呢?

    偏移地址要看它在段中的位置。程序中的“data”段中的数据“0abch”的地址就是:data:6

(3)“代码段”、“数据段”、“栈段”完全是开发者的安排

以一个具体的程序再次讨论一下所谓的“代码段”、“数据段”、“栈段”。在汇编程序中,可以定义许多的段,比如在程序 6.4 中,定义了 3 个段,“code”、“data”和“stack”。我们可以分别安排它们存放代码、数据和栈。那么如何让 CPU 按照我们的这种安排来执行这个程序呢?看看源程序中对这 3 个段所做的处理:

  1. 在源程序中为这 3 个段起了具有含义的名称,用来存放数据的段 data,存放代码的段 code,用作栈空间的段 stack
    这样命名了后 CPU 是否就去执行 code 段中的内容,处理 data 段中的数据,将 stack 当作栈了呢?
    当然不是,我们这样命名,仅仅是为了使程序便于阅读。这些名词同 start、s、s0 等标号一样,仅在源程序中存在,CPU 并不知道它们。
  2. 在源程序中用伪指令assume cs:code,ds:data,ss:stack将 cs、ds 和 ss 分别和 code、data、stack 段相连。这样做了后,CPU 是否会将 cs 指向 code,ds 指向 data,ss 指向 stack,从而按照我们的意图来处理这些段呢?
    当然也不是,要知道 assume 是伪指令,是由编译器执行的,也是仅在源程序中存在的信息,CPU 并不知道它们。不必深究 assume 的作用,只要知道需要用它将你定义的具有一定用途的段和相关寄存器联系起来就可以了
  3. 若要 CPU 按照开发者的安排行事,就要用机器指令控制它,源程序中的汇编指令是 CPU 要执行的内容。CPU 如何知道去执行它们?
    我们在源程序的最后用end start说明了程序的入口,这个入口将被写入可执行文件的描述信息,可执行文件中的程序被加载入内存后,CPU 的 CS:IP 被设置指向这个入口,从而开始执行程序中的第一条指令。标号start在 code 段中,这样 CPU 就将 code 段中的内容当作指令来执行了。我们在 code 段中,使用指令:
    mov ax,stack
    mov ss,ax
    mov sp,20h
    
    设置 ss 指向 stack,设置 ss:sp 指向 stack:20,CPU 执行这些指令后,将把 stack 段当作栈空间使用。CPU 若要访问 data 段中的数据,则可用 ds 指向 data 段,用其它的寄存器(如 bx)来存放 data 段数据的偏移地址。

总之,CPU 到底如何处理我们定义的段中的内容,完全是靠程序中具体的汇编指令,和汇编指令对 CS:IP、SS:SP、DS 等寄存器的设置来决定的

实验5 编写、调试具有多个段的程序

这一章内容较少,有些知识需要在实践中掌握。必须完成这个实验。

(1)将下面程序编译、连接,用 Debug 加载、跟踪,然后回答问题

assume cs:code,ds:data,ss:stack

data segment
    dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
    dw 0,0,0,0,0,0,0,0
stack ends

code segment
start:  mov ax,stack
        mov ss,ax
        mov sp,16
        
        mov ax,data
        mov ds,data
        
        push ds:[0]
        push ds:[2]
        pop ds:[2]
        pop ds:[0]
        
        mov ax,4c00h
        int 21h
code ends
end start
  1. CPU 执行程序,程序返回前,data 段中的数据为多少?

    data 段中的数据不变

  2. CPU 执行程序,程序返回前,cs=______ ss=______ ds=______。

    076ch、076bh、076ah(不同机器得到结果可能不同)

  3. 设程序加载后,code 段的段地址为 X,则 data 段的段地址为______,stack 段的段地址为______。

    X-2,X-1;
    数据段为 dw 声明的 8 个字单位也就是 16 个 字节单位,栈段一致也为 16 个字节单位,由公式物理地址=段地址x16+偏移地址可得出上述答案

(2)将下面的程序编译、连接,用 Debug 加载、跟踪,然后回答问题

assume cs:code,ds:data,ss:stack

data segment
    dw 0123h,0456h
data ends

stack segment
    dw 0,0
stack ends

code segment
start:  mov ax,stack
        mov ss,ax
        mov sp,16
        
        mov ax,data
        mov ds,ax
        
        push ds:[0]
        push ds:[2]
        pop ds:[2]
        pop ds:[0]
        
        mov ax,4c00h
        int 21h
code ends
end start
  1. CPU 执行程序,程序返回前,data 段中的数据为多少?

    有 16 个字节单位空间,前两个字单位数据不变,其余用 00 补全

  2. CPU 执行程序,程序返回前,cs=______ ss=______ ds=______

    076ch, 076bh, 076ah

  3. 设程序加载后,code 段的段地址为 X,则 data 段的段地址为______,stack 段的段地址为______

    X-2, X-1; 这里可能有不懂,看 4 的详解

  4. 对于如下定义的段
    name segment
     .
     .
     .
    name ends
    
    如果段中的数据占 N 个字节,则程序加载后,该段实际占有的空间为______

    (N/16取整 + 1) * 16; 详解:
    虽然上面代码 data 段和 stack 段只初始化了 4 个字节的内存,但是在汇编中还是直接分配 16 个字节的空间,不足的按 00 补全

(3)将下面的程序编译、连接,用 Debug 加载跟踪,然后回答问题

assume cs:code,ds:data,ss:stack
code segment
start:  mov ax,stack
        mov ss,ax
        mov sp,16
        
        mov ax,data
        mov ds,ax
        
        push ds:[0]
        push ds:[2]
        pop ds:[2]
        pop ds:[0]
        
        mov ax,4c00h
        int 21h
code ends
data segment
    dw 0123h,0456h
data ends
stack segment
    dw 0,0
stack ends
end start
  1. CPU 执行程序,程序返回前,data 段中的数据为多少?

    有 16 个字节单位空间,前 4 个字节单位数据不变,其余用 00 补全

  2. CPU 执行程序,程序返回前,cs=______ ss=______ ds=______

    076ah, 076eh, 076dh;

  3. 设程序加载后,code 段的段地址为 X,则 data 段的段地址为______, stack 段的段地址为______

    X+3, X+4; 下图红色为 data 段,绿色为 stack 段

    图中可以看到 cx=0044h 的意思为次程序所有的机器码所占用的总空间是 44H=68字节。data 和 stack 都小于 16 字节,汇编还是会分配 16 字节单位空间,其余补 00。剩余 36 个字节就是 code 段可执行的机器码,36 字节不满足 48(3*16),所以加载时剩余空间也用 00 补齐

(4)如果将(1)、(2)、(3)题中的最后一条伪指令end start改为end(也就是说不指明程序的入口),则哪个程序仍然可以正确执行?请说明原因

(3)中代码可以正确执行。如果不指明入口程序会中加载内存的第一个内存单元开始执行,(1)(2)程序中开始是定义数据段,虽然将数据当作代码执行了,但是执行逻辑上是错误的

(5)程序如下,编写 code 段中的代码,将 a 段和 b 段中的数据依次相加,将结果存储到 c 段中。

assume cs:code
a segment
    db 1,2,3,4,5,6,7,8
a ends
b segment
    db 1,2,3,4,5,6,7,8
b ends
c segment
    db 0,0,0,0,0,0,0,0
c ends

code segment

; *********编写代码开始***********
start:  mov ax,a
        mov ds,ax       ; ds 指向 a 段
        mov ax,b
        mov es,ax       ; es 指向 b 段
        mov bx,0        ; 初始化下面取值的偏移地址
        mov cx,8        ; 循环次数,依据数据段 db 声明的 8 个字节单位长度
s:      mov dl,[bx]     ; 将 a:[bx] 内存单元的值放入 dl
        add dl,es:[bx]  ; 将 dl 内存单元的值与 es:[bx] 内存单元的值相加,此后 dl 内存单元的值为 a 与 b 相对应的内存单元的值相加的结果
        push ds         ; 将 ds 的值保护起来
        mov ax,c
        mov ds,ax       ; 因为这里用到了 ds,所以上面要保护起来
        mov [bx],dl     ; 将 dl 中的值写入 c 中,这里的 [bx]=ds:[bx] 也就是 c 段中的相对应的内存单元
        pop ds          ; 恢复 ds 的值
        inc bx          ; bx 递增
        loop s
        
        mov ax,4c00h
        int 21h
code ends
end start
; *********编写代码结束***********

code ends
end start

(6)程序如下,编写 code 段中的代码,用 push 指令将 a 段中的前 8 个字型数据,逆序存储到 b 段中

assume cs:code
a segment
    dw 1,2,3,4,5,6,7,8,9,0ah,0bh,0ch,0dh,0eh,0fh,0ffh
a ends
b segment
    dw 0,0,0,0,0,0,0,0
b ends
code segment

; *********编写代码开始***********
start:  mov ax,a
        mov ds,ax       ; ds 数据段指向 a 段
        
        mov ax,b
        mov ss,ax       ; ss 栈段指向 b 段
        mov sp,16       ; 初始化栈顶,ss:sp 指向栈顶,表示 b 段作为栈结构使用
        
        mov bx,0
        mov cx,8        ; 循环次数,依据题目中逆序存储前 8 个字型数据
    s:  push ds:[bx]    ; 将 a 段中的字单位内存单元值入栈,所有值入栈后的结构就是逆序的
        add bx,2
        loop s
        
        mov ax,4c00h
        int 21h
; *********编写代码结束***********

code ends
end start
@Ray-56 Ray-56 changed the title 汇编语言 第四版 (上) 汇编语言 第四版 王爽著(上) Sep 3, 2021
@Ray-56 Ray-56 added the StudyNotes 读书笔记 label Sep 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
StudyNotes 读书笔记
Projects
None yet
Development

No branches or pull requests

1 participant