咸鱼暄

咸鱼暄的学习空间!

导航

汇编语言入门学习 | 2 - 汇编语言代码基本结构

从一个例子开始

根据个人习惯,我更愿意从一个实例开始某种语言的学习。


这里以一个 16 位汇编程序为例:
我们在 xp 虚拟机中新建文件 hello.asm,用记事本编辑:

 1 data segment
 2 abc db "hello, world!", 0Dh, 0Ah, "$"
 3 data ends
 4 ;这是一条注释
 5 code segment
 6 assume cs:code, ds:data
 7 main:
 8   mov ax, data
 9   mov ds, ax
10   mov ah, 9
11   mov dx, offset abc
12   int 21h
13   mov ah, 4Ch
14   int 21h
15 code ends
16 end main


将其放在 \Masm 目录中。该目录中同时包含了 LINK.EXE 以及 MASM.EXE。我们在 command 中进入对应目录,输入指令:

masm hello;
link hello;
hello

我们通过运行 masm.exe 来编译 hello[.asm],然后通过 link.exe 来连接 hello[.obj],最后运行 hello[.exe]。结果显示: hello, world!


下面,我们对代码进行逐句解析:

    • 对于 8086PC 机,在编程时可以根据需要将一组内存单元定义为一个段(机器语言代码也存储在内存中)。例如第 1 行和第 5 行就分别定义了名为 data 的段和名为 code 的段。第 3 行和第 15 行分别是这两个段的结束。

    • 8086CPU 要求每个段的容量不能超过 64KB。这与计算机的寻址方式有关:

      • 计算机对内存的编码是线性的。例如内存为 256M,则地址就应该为 0~(256M-1) 。这个地址称为 物理地址 或 绝对地址 。
      • 8086CPU 可以传送 20 位的地址 0~(1M-1),但是由于 8086CPU 是 16 位结构的,因此如果采用简单的方法传递物理地址,那么它的寻址能力只有 0~(64K-1)。因此 8086CPU 采用两个 16 位地址合成的方法来形成一个 20 位的物理地址。比如 12ABh:34DEh 就是一个地址(其中 12ABh 表示 16 进制下的 12AB。汇编语言中用末尾的一个 h 来表示 16 进制数。汇编语言中数字的表示不区分大小写。如果一个 16 进制数是字母开头的,则需要在它前面增写一个 0,如 ABCDh 应写为 0ABCDh,因为字母开头的字符串表示的是变量的名称),它由两个 16 位的地址组成,分别称为 段地址 (Segment) 和 偏移地址 (Offset)。这样表示的地址称为 逻辑地址 。
      • 地址加法器采用 物理地址 = 段地址 * 16 + 偏移地址 的方法合成物理地址。
      • 需要说明的是,同一个物理地址可以表示成多个逻辑地址。如 123BC = 123B:000C = 122A:011C。
      • 因此,我们如果要用 段地址:偏移地址 的方式寻址,偏移地址的范围为 0h~FFFFh,即 10000h 个字节,即 64KB。
    • 伪指令

      • 在汇编语言源程序中,包含 2 种指令,一种是汇编指令,一种是伪指令。汇编指令是与机器码一一对应的,而伪指令由编译器来执行,编译器会进行相关的编译工作。
      • 例如,segment 和 ends 就是一对成对使用的伪指令。
  • 定义数组

    • 第 2 行 abc db "hello, world!", 0Dh, 0Ah, "$" 定义了一个字节类型的变量(db: define byte,byte 类型实际上等价于 C 语言中的 char 类型),名为 abc,内容为 "hello, world!", 0Dh, 0Ah, "$" ,相当于 C 语言中的 char abc[] = "hello, world!\x0D\x0A$"; ,即逗号隔开的内容会被连接成一个变量。其中 0Dh, 0Ah 分别是回车(光标回到行首)和换行(光标向下移动一行)的 ASCII 码。在汇编语言中,$ 是字符串结束的标志。

    • 我们可以通过 ans db 100 dup(0) 定义一个定长的数组,相当于 C 语言中的 char ans[100] = {0}; 。dup 是 duplicate 的简写,表示重复。我们可以通过这种方法来取得内存空间存放数据。

    • 汇编语言将所有的变量定义放在一起,即 data segment 区域中。

    • 类似地,我们可以用 dw(define word) 定义字型数据(16位)。

  • 注释 

    • 如第 4 行,汇编语言源代码中,可以用分号 ; 表示本行中后面的内容均为注释。这与 C 中的 // 类似。
  • 寄存器

    • CPU 本身只负责运算,不负责存储数据。数据一般存放在存储器中,CPU 需要使用数据时就会去存储器中调用数据。然而,CPU 的运算速度远高于内存的读写速度,因此为了提高效率,CPU 自带缓存和 寄存器 (register)。缓存可以看做读写速度较快的内存,而寄存器是 "fastest, smallest and most expensive" 的,用来存储最常用的数据。

    • 寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称。
      8086CPU 有 14 个寄存器,分别为 AX, BX, CX, DX, SI, DI, SP, BP, IP, CS, SS, DS, ES, PSW,它们都是 16 位的。

    • 本文中,我们用加括号的寄存器名称来表示寄存器中存储的数据。例如, (ax) 表示 ax 寄存器中存储的数据。

    • AX, BX, CX, DX 这 4 个寄存器通常用来存放一般性数据,称为 通用寄存器 。为了与上一代 CPU 兼容,每个通用寄存器都可以拆成两个 8 位寄存器独立使用,如 AX 可拆分为 AH 和 AL,BX 拆分为 BH 和 BL 等。H 和 L 分别表示高 8 位和低 8 位。
      计算机存储信息的基本单位是一个 二进制位(bit),一位可存储一个二进制数 0 或 1。每 8 位组成一个 字节(Byte)。每两个字节组成一个 字(word),这两个字节分别称为高位字节和低位字节。

    • 代码段寄存器 CS(code segment) 和指令指针寄存器 IP(Instruction Pointer) 是 8086CPU 中最关键的两个寄存器。它们分别用来提供当前指令的段地址和偏移地址。即任意时刻,8086CPU 将 CS:IP 指向的内容当做命令执行。每条指令进入指令缓冲器后、执行前,IP += 所读取指令的长度,从而指向下一条指令。

    • 其余寄存器将在用到时再做记录。

  • 伪指令 assume

    • 第 6 行 assume cs:code, ds:data 将段寄存器和段名建立了关系。即 assume 使得段寄存器储存了对应段的段地址。
  • 标号 

    • 第 7 行 main: 是一个标号。标号在程序中的主要用途是方便跳转语句的执行。跳转语句将在后面再做学习。
  • 传送指令 mov 

    • 传送指令 mov 的一般格式为 mov A, B ,用于将 B 的内容赋给 A(如果合法)。
    • 传送指令在本文文末专门记录。
  • offset 

    • 操作符 offset 的功能是取得标号的偏移地址。第 11 行 mov dx, offset abc  的作用就是将 abc 的偏移地址赋给 dx。 
  • 中断 

    • 第 12 和 14 行的 int 21h 调用了中断。中断在本文文末专门记录。
    • 在这里,mov ah, 9;mov dx, offset abc;int 21h 调用了中断 21h 的 09h 号功能,实现了对字符串 abc 的输出。中断 21h 的 09h 号功能实现的是:将自 ds:dx 开始、到 '$' 为止的字符串输出到标准输出设备上。
    • 程序返回:每个可执行文件的类型都来自于某一个正在运行的程序的调用。可执行文件运行完毕后,它要将 CPU 的控制权交还给调用它的程序,这个过程称为程序返回。 mov ah, 4Ch   int 21h 完成的就是这个过程。
  • end 

    • end 指令用于通知编译器:程序运行结束了。
    • 在 end 后面加上一个标号,如 end main,则在起到上述效果的同时还会通知编译器程序的入口在什么地方(即偏移地址)。即,程序的入口由 end 指出。在本代码中,程序自标号 main 开始运行。

mov 指令(传送指令)

将数据直接送入寄存器

指令 mov ax, 4E20h 表示将 4E20h 送入寄存器 AX。等价于高级语言中的 AX = 4E20h; (此后文中会大量使用高级语言的语法描述汇编指令)。

将一个寄存器中的内容送入另一个寄存器

类似地, mov ax, bx 表示 AX = BX; 。

将一个内存单元中的内容送入一个寄存器

之前我们提到,8086CPU 中的地址由段地址和偏移地址组成。8086CPU 中有一个 DS 寄存器(段寄存器),用来存放要访问数据的段地址。
例如,我们要读取 10000h 单元的内容,可以用以下的程序段进行:

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

上面的三条指令将 10000h (1000:0) 中的数据读到了 al 中。可见,我们可以通过 mov register, [ address ] 的方式来将内存中 DS:address 的数据读到合法的寄存器 register 中。
值得注意的是,我们通过 1,2 两行将 1000h 放入了 DS,这是因为 8086CPU 不支持将数据直接送入段寄存器(ds, ss, cs, es)的操作。


我们还可以显式地规定我们调用的内存地址的段地址,如我们可以用 ds:[0] 来表示我们调用的内存单元为 ds:0。这允许了我们使用 ds 以外的段地址。这样的 "ds:" "cs:" 等被称为 段前缀。逻辑地址中的偏移地址可以用常数表示,但是段地址必须用段寄存器表示。


另外,我们可以通过 [bx] 表示内存单元 ds:bx,即段地址由 ds 提供,偏移地址由 bx 提供。
需要注意的是,8086CPU 中只有 bx, si, di, bp 这四个寄存器可以用来在 [] 中进行内存单元的寻址,其他寄存器进行这样的操作都是非法的。
同时,在 [] 中,这四个寄存器只能单独出现或以 bx, si / bx, di / bp, si / bp, di 的组合出现,如 [bx+si+1] 是合法的,而 [bx+bp] 就是非法的。两个寄存器只能相加,不能相减
只要在 [] 中用到寄存器 bp,而指令中没有显式给出段地址,那么段地址就默认在 ss 中而不是 ds 中。


由于 8086CPU 是 16 位结构,因此可以一次性传送 16 位数据。比如:

内存情况:
10000H 11
10001H 22

指令:
mov ax, 1000h
mov ds, ax
mov ax, [0]

结果:
ax = 2211H

这是因为,我们将 1000:0 处存放的字数据(由两个字节组成)送入 ax 时,1000:0 处存放的是字数据的低 8 位,即 11;1000:1 处存放的数字数据的高 8 位,即 22。(小端规则:对于 8 位以上的变量,先存放低位,再存放高位,即低位的内存地址低于高位的内存地址。)执行 mov 时,字数据的低 8 位送入 al,高 8 位送入 ah,因此 ax = 2211H。


这也说明,mov 操作的内存单元的长度由其他操作对象(寄存器)指出。但是,两个不同长度的寄存器之间的传递是非法的,如 al 和 bx。


另外,下面的代码反映了一种常见的错误:

data segment
xyz dw 1234h, 0ABCDh
data ends
...... ;略去
    
code segment
...... ;略去
mov ax, xyz[1]
...... ;略去

在 C 语言的理解中,short int xyz[2] = {1234h, 0ABCDh}; 定义出的数组,xyz[1] 的值应该为 0ABCDh。而实际上在汇编语言中,xyz[1](即 [xyz + 1])指向的就是 xyz 物理地址 +1 的地址。假设 xyz 地址为 10000H,那么内存情况为(小端规则):

10000h 10001h 10002h 10003h
  34     12     CD     AB

实际上 xyz + 1 即 10001h,那么实际上程序认为 mov ax, xyz[1] 调用了以 10001h 为低八位的 16 个字节,根据小端规则,ax 被赋值为 0CD12h。
因此,如果希望引用 0ABCDh,实际上要写的是 mov ax, xyz[2] 。这是需要特别注意的。


中断

posted on 2020-04-15 00:14  咸鱼暄  阅读(632)  评论(0编辑  收藏  举报