最近在看《汇编语言(第2版)》来学习汇编。

之所以要学习汇编语言,主要是因为在看《CSAPP》的时候,感觉涉及到汇编的部分都看不懂,影响了对知识点的理解,于是决定先把汇编语言做一番了解之后再继续看《CSAPP》。

1.CPU如何对存储器中的数据进行操作

我们在这里讨论的都是8086系列的Intel处理器,首先我们要了解CPU的三个属性。

  • 地址总线
  • 数据总线
  • 控制总线

其中地址总线的根数 N 被称之为CPU的地址总线宽度,该CPU最多可以寻找 2 的 N 个次方的内存单元。地址总线的宽度决定了CPU的寻址能力

数据总线是CPU和存储器进行数据传输的硬件通道,有多少根数据总线,在一个时钟周期,CPU就可以与存储器传输多少位的数据。数据总线的宽度决定了CPU与其它器件进行数据传送时以此数据传送量。

控制总线用来控制数据的读写操作。控制总线的宽度决定了CPU对其他器件的控制能力。

2.寄存器 - CPU中可以存储数据的器件

一个CPU中有多个寄存器,例如AX,BX等等,这些字母是寄存器的代号。

首先我们来了解一下AX,BX,CX,DX这四个通用寄存器,他们均为16位寄存器。

AX,BX,CX,DX这四个寄存器我们称之为通用寄存器,8086CPU为了与之前的 8 位寄存器系列CPU兼容,它的 16 位通用寄存器又可以被拆分为两个互相独立的 8 位寄存器,他们分别是AH,AL,BH,BL,CH,CL,DH,DL,分别占据了 16 位寄存器的高八位和低八位,它们可以被当作为独立的 8 位寄存器来使用。

操作一个寄存器的值的方式为:mov ax,0000h,即把一个16进制的16位的数据存入ax寄存器中。同样的,mov al,88h,表示将一个8位的数据存入al寄存器中。

我们在前面说到,一个CPU的寻址能力 = 2地址总线宽度,8086CPU 的地址总线为16根,那么它的寻址范围就为 216 = 64k,但是当时的机器内存是远大于64K的,也就是说超出了CPU的寻址能力。那怎么办,那么我们多余出来的内存不就会被浪费掉了吗?8086的设计者通过一个巧妙的方法解决了这个问题。

在8086CPU中,地址总线的数据被人为的划分成了段地址偏移地址这两种数据,也就是说,我们可以把上一个时钟周期的地址总线数据认为是段地址,而这个时刻的数据认为是偏移地址,并且:物理地址 = 段地址 x 16 + 偏移地址。这样一来,CPU的寻址能力大大提高,也就能够扩大CPU的访问内存了。

在8086CPU中有4个段寄存器,顾名思义,他们就是用来存段地址的,它们分别为CS、DS、SS、ES,其中的S就是segment的缩写。

我们首先来讨论一下CS:

CS被我们称之为代码段寄存器,也就是说,它对应的是代码的段地址,这里的代码段和接下来的数据段等等,都是我们人为区分的,为了方便人的操作,对CPU而言所有的数据都是一样的。有了段地址还不够,我们还要有偏移地址才能得到完整的内存地址,IP(指令指针寄存器)就是与CS对应的一个偏移地址,也就是说,如果我们有了CS:IP,那么我们就可以让CPU确定内存中的一个确定的位置了(CS x 16 + IP即为它对应的物理地址)。那么CS:IP所指向的位置有什么特殊的吗?为什么我们单独把他们拿出来说呢?其实,该物理地址对应的位置通常是存放代码的,而CPU会根据CS:IP来获得该地址,并且取出该位置的代码来执行。所以你会想了,我们可以通过mov cs,ffffh来修改这个寄存器值并且让CPU执行指定位置的的代码,很遗憾,这种方式是不行的。这8086中我们无法直接操作段寄存器,而是要通过
mov ax,ffffh
mov cs,ax
这种方式来改变段寄存器。另一个更通用的修改CS:IP的方法是 JMP 命令,你可以通过使用 jmp ffffh:ffffh来修改CS和IP的值为ffff。又或者,你可以通过jmp 一个合法的寄存器来修改IP为该寄存器中的值(相当于mov IP,AX 但是直接使用该指令是非法的)

DS寄存器,通常是用来存放数据地址的寄存器。比如,我们要把内存中的数据一个内容送入寄存器,那么是哪一个内存单元中的数据呢?我们可以通过MOV AX,[0]来实现把指定内存中的数据转入到AX寄存器中,其中 [0] 是内存单元的偏移地址,而段地址CPU会自行的取DS中的数据([]表示操作对象是一个内存单元)。8086不支持将数据直接送入段寄存器,要通过通用寄存器来转接,即要通过 mov ax,ffffh ,mov ds,ax 来实现把数据存到ds中。
[BX]:内存数据的偏移地址,和[0]的效果一样,只不过是取了BX中的值,也就说我们可以灵活的控制数据偏移地址了。
inc bx指令:bx的内容自加一。

3.在汇编语言中实现循环(loop)

要想在汇编语言中实现循环,我们先要知道如何书写一个完整的汇编程序。下面给出了一个完整的汇编程序示例:

assume CS:code ;CS意味着code段是一个代码段,我们可以声明多个段,中间用逗号连接
code segment ;code段开始

start:     mov ax,0111h ;start代表程序的入口,并不一定要是start,只要是end后面的单词即可
        mov ax,4c00h
        int 21h ;以上两句用来声明一个程序已经结束
code ends ;段结束
end start ;声明start代表着该程序的入口,CPU会从start出开始运行程序

loop所执行的操作:

  1. (CX) = (CX) -1
  2. 检测(CX),如果(CX) != 0,转至标号处执行程序,否则向下执行

语法如下:

...
mov cx,6
s:  add ax,1111h ;add表示 ax += 1111h
loop s
...

4.汇编语言中栈的实现

PUSH AX 把AX压入栈顶,高位位于高位,低位位于低位

POP AX 把栈顶的元素弹出来交给AX

SS寄存器:存放栈顶段地址

SP寄存器:存放栈顶偏移地址

任意时刻,SS:SP指向栈顶元素,PUSH或POP指令执行时,CPU从SS:SP中获得栈顶地址

PUSH:先改变SP(SP = SP - 2),然后把值压入SS:SP

POP:先把值弹出,然后改变SP(SP = SP + 2)

当栈中没有元素的时候,SS:SP指向栈底的下一个字的位置

5.其他

DW:define word,定义若干个字,中间用逗号连接

一个程序可以命名多个段,mov ax.data表示将名称为data的段的段地址送入ax

mov ax,data
mov ds,ax
mov bx,ds:[6] 将data中的第六个字转入bx

end start ;表示程序从start指令开始执行

and 和 or 指令:进行二进制位运算

mov al,’a’ 汇编器会将’’里面的内容转化为其对应的ASCII码,然后交给CPU

db ‘unIX’ 相当于 db 75h,6eh,49h,58

mov al,’f’ 相当于 mov al,61h

一个字母,不管他原来是大写还是小写,只要把他的第 5 位置0,它就会变成大写;将他的第 5 位置 1,他将会变为小写(二进制下)

mov ax,[200+bx]
mov ax,200[bx]
mov ax,[bx].200

以上三种表示偏移地址是 bx + 200,三种写法意义是一样的

SI、DI寄存器功能类似于BX寄存器

[bx+si] 与 [bx+di]:偏移地址相加,也可以写成[bx][si]

[bx+si+idata]也可以写成:

[bx+200+si]
[200+bx+si]
200[bx][si]
[bx].200[si]
[bx][si].200

一般来说,当我们想保存临时数据的时候,就应该使用栈

6.附录

汇编语言的编译器和链接器的使用。

  1. 把汇编器和链接器加入环境变量。
  2. 在DOS环境下(Windows中可以在Command Prompt中操作)输入MSAM,进入编译器,输入文件名 + ;,敲回车,如果没有错误会在当前目录生成一个文件名.obj文件
  3. 输入LINK,进入连接模式,然后输入 文件名 + ;,敲回车,可以生产可执行文件。