汇编学习笔记

汇编语言是为了解决早期机器指令过于复杂难记发明的,本质就是使用一些特殊字母代替机器指令,运行前由编译器翻译为机器指令。所以的汇编是最接近机器语言的语言,它可以面向硬件编程,我们使用高级语言,如 c,c++,java到最后都必须转为汇编。通常情况下我们并不会使用汇编编程,因为它太复杂,很难进行大型项目编写,而且汇编基本都是针对硬件编程,完全不可移植。但是学习汇编却也是必要的,它可以大大提升我们对计算机、对操作系统、对其他语言的理解。我就是因为想要森如学习操作系统才开始学习汇编,以下是简要的学习笔记(学习笔记而不是教程,只是一些总结归纳,而且基本都是概念性的东西,不能当做教程,甚至可能只有本人能看懂,请做好心理准备)。 

汇编的学习资料不是很丰富,但是好书也还是不少的,这里推荐王爽老师的《汇编语言》,内容详略有度通俗易懂,我的学习基本就是基于这本书,此外再推荐一个网站 http://c.biancheng.net/asm/ ,王爽老师的书侧重原理,此网站侧重用法,请结合使用

 

学习环境准备

  因为早期的书都是基于windows系统,所以如果在windows系统上学习汇编就方便的很多,本人因为某些原因装的是linux 系统,与书本上的环境对不上,所以环境准备就多了一个门槛。linux上当然也是可以直接编写汇编的,但是其语法规则和windows上差别很大,十分诡异,而且和书本对应不上,而且我本质不是为了深入研究汇编,所以不想过多研究不同的环境,就在linux上搭建了一个dos环境,如果有人和我一样,请往下看

  linux上运行.exe程序基本都是基于虚拟机,wine ,freedos,dosemu等等,但是并不是所有的虚拟机都适合学习汇编,这里推荐dosbox,学习汇编因为要使用到debug.exe,dosbox对这个调试工具的支持很好,而且它十分小巧,也是同类型运行速度最快的虚拟机。美中不足是界面太丑了,分辨率不可调(我的电脑不可调整)。如果对此不介意的话dosbox是首选,速度够快,dosemu 也非常好用,功能比dosbox更强大,重要的是界面好看

  学习汇编最基本的需要使用到三个工具,汇编器,链接器,调试工具,这三个工具在的windows 和linux上都有,也有跨平台的,因为我的学习是基于dos 的,所以我的使用的分别是masm.exe \ link.exe \ debug.exe 

  实际上汇编并不像c语言,Java那样可以一招鲜吃遍天,除了平台会导致汇编形式不同,计算机的位数也会导致汇编的代码不同(本文就是16位汇编)操作系统就使用了好几种汇编语言,如果不是为了深入研究汇编, 就没必要纠结于学习哪种好或者是不是都要学的问题,按照书本或者自己喜欢的语法来学习,以学习原理为主

 

计算机基本知识

1、计算机基本组成

1)计算机有存储器 | 运算器 | 控制器| 输入设备 |输出设备 五部分组成
2)计算机的五部分是概念分类,实际的物理设备有很多,比如输入设备就有键盘和鼠标,存储设备有磁盘、内存、寄存器等等,这些物理设别互相配合使计算机能完成复杂的任务。既然需要配合,部件之间就必须要进行通信,计算机的所有部件之间的通信斗依赖总线
3)总线是计算机的一个概念,实际上就是一个集成电路。计算机的所有部件都连接在一个集成电路上,互相之间可以通过集成电路传递信息
4)计算机之间并不是随意的通信,随意的配合,而是需要有人调度,这个调度者就是CPU

 2、CPU基本知识 

1)CPU是计算机的核心, 负责数据的运算和计算机的控制。显然CPU不是一个单一的设备,构成CPU的主要有三个部件:控制器、运算器和寄存器。
2)控制器是负责计算机资源调度的,比如数据的读写,网络请求的收发。当cpu收到一个指令后,控制器就通过总线将指令发送给相关的部件
3)计算机的总线分为三种
 *地址总线:主要负责内存的寻址,每个cpu都存在多个内存总线,每个总线都只能传递0或者1,那么地址总线的组合就可以传递一个二进制码, 如果一个cpu存在n个地址总线,它最多能访问2^n方个地址码,那此计算机的最大可用内存也就只能是 2^n 字节。可使用内存的大小受此限制
 *数据总线:负责cpu和其他组件的数据传递,原理和地址总线一样,如果数据总线个数为8,每次就能传递1字节数据
 *控制总线:负责cpu对其他组件的控制,控制总线是一系列总线的统称,cpu有很多控制总线,每个控制总线斗代表一种基础控制指令,比如读数据、写数据,指令的组合使用可以完成一些复杂控制
4)CPU的运算器负责数据的运算,既然要运算数据就必须有数据的来源和结果的存储位置,这就是寄存器的作用,作为cpu计算的数据来源和结果的存储位置.当然寄存器的作用远不止如此
5)每个CPU 都包含多个寄存器,每个寄存器各有作用,有的作为cpu运算数据来源,有的保存指令的存储位置,而且每个cpu的寄存器数量和分类都不同,这里以8086cpu 介绍

3、存储器基本知识

1)存储器是一个统称,磁盘、内存、寄存器都是存储器,这里主要介绍内存
2)CPU进行数据计算的直接数据来源是寄存器,而寄存器的数据来源是内存了。cpu需要计算时就将数读取到寄存器,然后从寄存器再获取到数据进真正的计算。
3)cpu想要使用数据首先必须要找到数据,内存就好象存储数据的小房间,每个房间都有一个独一无二的门牌号,这个门牌号就是内存的物理地址。cpu要进行数据读写就必须给内存提供一个物理地址,告诉内存需要哪块数据或者数据要存储在哪块内存,这就是寻址。
4)cpu从内存读写数据分为三步 (以数据读取举例)向内存发出一个内存区编号 > 发出指令告知内存需要读取这个编号的内容 > 内存将数据发送给cpu
5)内存其实也是多种存储器的统称,这些存储器包括主存储器,RAM存储器,ROM存储器,显卡存储器,网卡存储器等等,这些存储器都是独立的设备,但是它们都和总线相连,于是CPU在逻辑上把它们当作一个内存,用地址空间加以区分,每个存储器占用特殊的一段地址空间

4、段和栈

1)我们在编程时经常会听到数据段、代码段、堆、栈等概念,这些概念都是方便内存管理的,对于计算机来说并没有这么复杂的概念,cpu只知道存储空间,一些高级的抽象概念只不过是人们为了内存管理方便,把某些区域的内存规定为某类数据的专门存储地。换句话说你可以规定任意内存作为专门存储源代码的地方,这块内存就是代码段
2)计算机中有一个基础的概念,栈。栈是一种数据结构,特点是后进先出,市面上集合所有的cpu都通过某种方式实现了这种数据结构。cpu只有存储空间的概念, 自然不会天然的存在这种复杂的数据结构,栈的本质还是一段存储空间,只不过计算机通过某种方式保证,后进入这段空间的数据先出去,先进入这段空间的数据后出去。只要能实现这个特点,就能说这是一个栈
3)计算机实现栈结构的方式也比较简单,使用一个寄存器保存栈顶的物理地址,在存取数据时更新此寄存器的数据,保证这个寄存器的物理地址时刻指向栈顶,虽然cpu能时刻记录栈顶元素的位置,但是的它并不记录栈的大小, 也就是说你可以一直向栈里面压入元素,就很可能造成栈越界,所以使用栈的时候,栈越界是必须要考虑的 

寄存器专题

寄存器是学习汇编的核心,在汇编中,程序员能够用指令读写的只有寄存器,并以此实现对cpu的控制。

在学习寄存器之前需要学习两个基本概念:段地址和偏移地址

段地址和偏移地址

cpu的寻址能力是由寄存器的宽度决定,我们常说的16位cpu其实就是指它的寄存器宽度是16位,一个16位宽度的cpu每次寻址最多能找到2^16内存单元。

计算机的内存管理能力是由地址总线数量决定的,一个cpu含有16根地址总线,计算机就可以管理2^16个内存单元

8086CPU 寄存器有16字节,但是cpu的地址总线却有20根,这就造成了一个矛盾,根据地址总线的数量计算,cpu的寻址空间最大为2^20,但是根据寄存器的宽度计算,cpu每次寻址最大只能找到2^16的位置。为了解决这个问题人们发明了段地址和偏移地址的概念。其实概念很简单,既然单个寄存器无法满足寻址要求,那就使用两个寄存器组合好了,我们可以用一个寄存器保存一个基础地址(段地址),用另一个寄存器保存一个补充地址(偏移地址)。两个组合就可以得到一个更大的地址
例子:
问题:用两个三位数AAA,BBB表示一个四位数CCCC(2538)
实现:我们可以约定两个三位数的组合规则是 AAA * 10 + BBB,那么我们就可以把两者组合写作 250 * 10 + 038

基于以上例子的原理得到
物理地址 = 段地址 * 16 + 偏移地址

 

寄存器分类与用途介绍

每种CPU 的寄存器数量和用途都是不同,8086cpu含有14个寄存器 ax | bx | cx | dx | ds | ss | cs | es |  sp | bp | si | di | ip | psw,下面介绍这些寄存器的用途

1、被用来存放一般性数据的寄存器被称为数据寄存器:ax | bx | cx | dx。8086cpu寄存器都是16位的,而上一代寄存器都是8位的,为了向下兼容,这四个16位通用寄存器都可以被分成两个独立的8位寄存器使用,高8位用H表示,低8位用L表示。如AX 可以被分为 AH,AL。数据寄存器没有特殊用途,一般就是存取数据,或者作为数据中转站

2、存储段地址的寄存器被称为段寄存器:cs | ds | ss | es ,8086cpu寻址是依靠段地址加偏移地址的,以上的四个寄存器就是保存段地址,一般使用一个类型段寄存器保存一种数据的段地址,比如cs段寄存器专门用于保存代码段,ds用于保存数据段,ss保存栈段, 但是这并不是强制的

  • cs(Code Segment): 代码段寄存器,cs 是8086cpu中非常重要的寄存器,它和ip寄存器(指令指针寄存器,保存偏移地址)组合形成代码段物理地址,指示cpu将要运行的下一句代码的物理地址。
  • ds(Data Segment):数据段寄存器。汇编中进场需要将内存中的数据移动到寄存器计算,如mov  ax, [0] ,这个命令是将偏移地址为0 的 内存单元的数据移动至ax,我们知道一个单独的便宜地址是无法定位物理地址的,那么这个指令是不是错了呢。并不是, cpu默认将ds寄存器内的数据当做段地址,所以要使用上面这个指令一般还会加上 mov  ds,xxx ,初始化ds的值
  • ss(Stack Segment):栈段寄存器。cpu中栈的实现就是依靠寄存器保存栈顶元素的物理地址,当栈数据更新时,该寄存器的值也同步更新,这个寄存器就是ss,与之配合的保存偏移地址的寄存器就是sp和bp,ss:sp需要时刻指向栈顶元素,不可更改,那我们想要访问栈中的其他元素怎么办呢,将sp的地址传递给bp,由 ss:bp 去寻找元素
  • es(Extra  Segment):  扩展段寄存器,我们寻址是必须使用段急寄存器的,但是段寄存器数量有限,可能在有些程序中会不够用,此时就可以使用es寄存器

3、变址寄存器:si  | di。si是源变址寄存器,di是目的变址寄存器,常用于串指令操作,定位串的原始位置和目标位置 。比如将一个字符串从一个内存地址拷贝到另一个内存地址,可以使用ds:si 记录此字符串原来的地址, ds:di 记录数据将要被拷贝到的目标地。除了这种单独的使用方式,si | di还可以和bx , bp 结合使用用于寻址。形如 mov ax, [bx + si] ,这里 [bx + si] 代表偏移地址,其值等于bx内的数据 +  si内的数据,它的默认段地址在ds中。当si | di 与bp集合使用,段地址默认在es中

4、最特殊的寄存器标志寄存器:psw。标志寄存器名副其实, 此寄存器不像其他寄存器一样存储一个数据,它是按位起作用的,也就是说此寄存器的每一位都含有特殊的含义的,8086cpu标志寄存器有16位,按理说它可以标示16个特殊的含义,事实却不是如此,它只使用了0,2,4,6,7,8,9,10,11这9个位,其他的位没有意义,下面讲每一个位的含义

ZF标志:位于第6位,与计算指令相关,如果指令计算结果为0,ZF = 1,计算结果不为0,结果为0

PF标志:位于第2位,奇偶标志位,奇偶标志位,记录相关指令执行结束后,其结果的二进制表示中1的个数是否为偶数,为偶数 pf =1,否则 pf = 0

SF标志:位于第7位,符号标志位,记录相关指令结束后结果是否为负数,结果为负,sf=1,否则,sf=0

OF标志:位于第11位,计算溢出标志位,记录相关指令结束后,结果是否发生了溢出,发生溢出,of=1,否则of=0

 

端口

  计算机中的各种硬件都链接在总线上,以此互相通信,接受cpu调度,这些部件都通过芯片连接在总线上,这些芯片大体分为存储芯片(各种存储器)和接口芯片(网卡,显卡等)。cpu把所有的存储芯片当做一个整体,全部看做内存,然后进行统一编号,在访问的时候就不需要区分他们的种类了,只要根据编号来区分就可以了。接口芯片与存储芯片不同,每个芯片中都含有一个可由cpu读写的寄存器,cpu不会直接 操作接口芯片,而且通过与每个芯片的寄存器沟通,间接控制芯片。为了管理方便,cpu把这些外部寄存器也进行编号, 通信时直接根据编号定位寄存器。这个编号就是端口

 

中断 

中断是CPU的一种能力,简单来说就是cpu可以在执行一段程序的中间暂停,转去执行其他的程序,任何一个通用的cpu都具有这种中断能力。中断分为内中断和外中断,下面分开介绍

内中断: 

  当cpu内部发生某些特殊的事情(比如除法错误),需要运算器立即进行处理, 计算机会产生一个“中断信息”发送给cpu,cpu执行完当前正在执行的指令后会根据这个中断信息的描述转去执行处理这个特殊事情的处理程序,这就是内中断。触发中断的事件被称为中断源, 处理中断的程序称为中断处理程序。中断源是固定的是计算机事先定义好的,而中断处理程序是可以由程序员编写的

  cpu是如何识别接收到的中断源是什么,由如何定位中断处理程序的呢。其实在计算机内部,每个中断源都有一个独一无二的中断类型码(8位),比如除发错误的中断类型码是0,当程序发生除零错误,计算机就会生成中断信息,此中断信息就包含了中断类型码。cpu接收到中断类型码就知道了中断类型,然后去寻找中断处理程序。那么cpu是如何通过8位的中断类型码找到16位的中断处理程序地址的呢 ,或许你会想中断处理程序的地址也包含在中断信息中,事实却不是这样。其实计算机中实现存在一个中断向量表,这个表保存着终端类型和它对应的处理程序的地址,cpu获取到了中断类型,在这个表里面查询一下就知道中断处理程序的地址了。中断类型码是8位的,标示计算机中最多能存在256个中断类型和的256个 中断处理程序, 不用担心,现在计算机的中断类型还远没有达到256个

  中断处理程序是可以由我们自定义编写的, 具体的做法是 1)编写某个中断处理程序 2)将程序存到某块内存3)用这块内存的起始地址替换中断向量表对应中断类型的处理程序地址(安装) 

  除了自然产生的中断,我们也可以编程手动引发中断,使用  int n 指令就可以引发一个中断让cpu来处理, n 即为中断类型编号

外中断:  

  计算机除了运算,还要对其他的部件进行控制做一些 其他的事,比如打字,于是cpu必须能接受外部设备的请求,并处理请求。当点击键盘,键盘的芯片就会通过寄存器给cpu发送一个中断指令让cpu尽快来处理这个事件,终于中断处理的过程和内中断没什么太大差别

 

编程

 1、基本指令介绍

汇编语言简单来说就是用一些单词指令代替机器指令,运行时由翻译软件将单词翻译成机器指令,所以每个机器指令在汇编中都有对应的汇编指令,类似获取数据,加减乘除。当然,汇编语言锁含有的指令比机器指令多很多,主要分为三种

1)汇编指令:机器码的助记码,每个有对应的机器码
2)伪指令:用于编译器操作的辅助指令,没有对应机器码
3)符号体系:基本运算符号,供编译器使用,没有对应机器码

如下是一个最简单的汇编代码,中间是常用指令的介绍, 其他的就是一个最简单的汇编程序的基本结构

assume cs:code ;将cs寄存器和代码段关联(会使用就好,不必深究)

code segment ;声明一个段(类似c语言的一个函数名加左括号)
start: ;设置程序开始位置
; --------------------- 代码编写开始 -----------------------

;数据定义
db 0,0,0,0 ;以字节方式定义数据
dw 0,0,0,0 ;以字符方式定义数据
dd 0,0,0,0 ;以双字符方式定义数据
db 100 dup(0) ;直接定义100个值为0的字节型数据
db 100 dup('a') ;直接定义100个值为 'a' 的字节型数据
db 100 dup('ab') ;直接定义100个值为 'a' 的字节型数据
db 100 dup('ab',1) ;直接定义100个值为 'a' 的字节型数据

;数据的移动
mov ax,1111B ;将二进制数到寄存器ax
mov ax,1 ;将十进制数移动到寄存器ax
mov ax,1111H ;将十六进制数移动到寄存器ax
mov ax,'a' ;字符写法
mov ax,bx ;将bx中的数据移动到ax
mov ax,[0] ;将 (段地址为ds,偏移地址为0) 地址处的数据移动到ax
mov ax,[bx] ;将 (段地址为ds,偏移地址为bx寄存器中的内容) 处的数据移动到ax
mov ax,[bx+1] ;将 (段地址为ds,偏移地址为bx寄存器中的内容 + 1) 处的数据移动到ax
mov ax,[bx+si+10*2] ;将 (段地址为ds,偏移地址为bx寄存器中的内容 + si 寄存器的内容 + 1)处的数据移动到ax
;加减乘除
add ax,1 ;ax自增 1 ,相当于 ax = ax + 1
add ax,bx ;ax自增bx ,相当于 ax = ax + bx
sub ax,1 ;ax自减 1 ,相当于 ax = ax - 1
sub ax,bx ;ax自减bx ,相当于 ax = ax - bx
inc ax ;ax自增 1 ,相当于 ax = ax + 1
dec ax ;ax自减 1 ,相当于 ax = ax - 1
;乘法规则:两个乘数的位数要么都是8位要么都是16位.8位乘法,一个乘数默认放在ah,结果放在ax中;16位乘法,一个乘数默认放在ax中,结果的高位放在dx,低位放在ax
mul bl ;8位乘法,相当于 ah * bl
mul byte ptr ds:[0] ;8位乘法,byte ptr表示取一个字节的数据作为乘数
mul bx ;16位乘法,相当于 ax * bx
mul word ptr ds:[0] ;16位乘法,word ptr表示取一个字符的数据作为乘数
;除法规则:被除数默认放在ax.除数为8位,al存储商,ah存储余数;16位除法,ax存储商,bx存储余数
div bl ;8位除法,相当于 ah * bl
div byte ptr ds:[0] ;8位除法,byte ptr表示取一个字节的数据作为除数
div bx ;16位除法,相当于 ax * bx
div word ptr ds:[0] ;16位除法,word ptr表示取一个字符的数据作为除数

;与|或,将两个值的二进制数进行与或操作
and al,11H ;将al和十六进制数11H与操作,结果保存在al
and al,ah ;将al和ah与操作,结果保存在al
or al,11H ;或的规则和与规则一致
or al,ah

;转移指令,可以修改寄存器cs和ip的指令被称为转移指令
jmp short 标号 ;段内短转移,cs不变,ip改变, ip的变化范围为 -128 - 127
jmp near ptr 标号 ;段内近转移,cs不变,ip改变, ip的变化范围为 -32768 - 32767
jmp far ptr 标号 ;段间转移,cs变化
jmp word ptr 内存单元地址 ;以内存中存储的值作为转移的偏移地址
jmp dword ptr 内存单元地址 ;以内存中存储的值作为转移的偏移地址
jcxz ;条件转移,当cx=0时ip值变化
ret ;使用栈中的数据修改ip内容实现近转移,相当于:ip = ss*16+sp ;sp=sp+2
retf ;使用栈中的数据修改cs和ip内容实现远转移,相当于:ip=ss*16+sp; sp=sp+2; cs=ss*16+sp; sp=sp+2
call             ;将当前指令压如栈中,然后转移,不能实现短转移

; --------------------- 代码编写结束 ------------------------
code ends ;代声明码段结束(类似c函数的右括号)
end ;整个程序结束

 

 2、规结构化编程以及特殊关键字介绍

assume ds:data,ss:stack,cs:code                       ;把某个段寄存器与特定的段关联,表明这个寄存器专门用于保存一种特定的数据

;定义数据段
data segment
    db 100,dup(0)
data ends

;定义栈段
stack segment
    db 100,dup(0)   
stack ends
;注意:无论是数据段还是栈段,本质都是计算机为程序分配的一块内存,本程序中我们并没有指定这块内存在什么位置,操作系统在加载程序的时候会为程序自动的分配一块空间
;一般操作系统分配的这块内存空间以 ss:0 为起始位置,但是我们在程序中想要获取这个位置的话不需要使用ss寄存器的形式,直接使用 mov ax,data 就可以获取到数据的段地址
;至于数据的偏移地址是需要我们根据实际情况计算的

;定义代码段
code segment
start:        mov ax,offset  start                    ;offset 用于获取标号相对寄存器ip保存值的偏移地址,相当于 :mov ax,0
second:       mov ax,offset  second                   ;相当于mov ax,3 (前一个指令占用三个字节)
              
              mov cx,10
s:            add ax,2                                ;loop 用于循环,寄存器cx保存的数字是循环次数,每循环一次,cx减1,,cx等于0时跳出循环
              loop s                                  ;loop 跳转到标号s 处执行程序

              jmp short s
              jmp  a 
              

code ends

end start

 

posted @ 2019-07-07 10:13  这个世界需要我  阅读(905)  评论(0编辑  收藏  举报