汇编语言学习笔记
最近在看 《汇编语言(第 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,ffffhmov 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 所执行的操作:
- (CX) = (CX) -1
- 检测 (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.附录
汇编语言的编译器和链接器的使用。
- 把汇编器和链接器加入环境变量。
- 在 DOS 环境下(Windows 中可以在 Command Prompt 中操作)输入 MSAM,进入编译器,输入文件名 + ;,敲回车,如果没有错误会在当前目录生成一个文件名.obj 文件
- 输入 LINK,进入连接模式,然后输入 文件名 + ;,敲回车,可以生产可执行文件。