前言

一年多前曾经看过汇编,但是当时的编程知识和硬件知识都很薄弱,导致很多内容是死记硬背很快就忘记了,最近重新找到了王爽编写的《汇编语言》pdf,打算重新回顾一下主要内容,夯实自己的编程基础。

关键点

机器语言是一堆二进制码,它可以操纵CPU完成指定的操作,但是很难记忆而且不便于排错,因此发明了汇编语言,汇编语言是与机器语言一一对应的,它的唯一区别就是更符合人的记忆特点,例如mov ax,bx;但汇编语言最终还需要通过编译器编译成机器语言才能交给计算机执行。汇编语言中主要有三个部分构成:汇编指令、伪指令、其他操作符,后两者是给编译器使用的,前者是与机器语言一一对应的。
CPU从内存中读取数据时需要指定三个信息:地址信息,控制信息,数据信息;CPU通过地址总线传输地址信息指定数据存在哪里,通过控制总线传输控制信息决定是读还是写或是其他操作,数据总线则是用来传输实际的数据。地址总线的宽度决定CPU的寻址能力,例如32位只能支持4GB的内存寻址,数据总线宽度决定数据传输速度,控制总线和前两者不同,它是一系列控制线的总称,例如一根输出控制线拉低则代表写数据,一根输入控制线拉低则代表读数据。

内存地址空间:
【基础】汇编语言学习笔记
将实际上独立的各个模块的存储器看成一个逻辑存储器,对应一个一维的内存地址空间,向指定的地址读写数据就是向相应的外部存储器读写数据。这些存储器以各种方式连接在主板上,但逻辑上都是和CPU通过三个总线连接的。

一个典型的CPU由运算器(运算数据、控制器(控制器件进行工作、寄存器(存储数据)等器件组成。以8086CPU为例,寄存器都是16位的,首先看通用寄存器:AX,BX,CX,DX,他们都可以分成H和L两个8位的寄存器以满足向下兼容。

字(word)是由两个字节构成,当年看到这些东西还不是很敏感,现在看到就会很自然的想到,分成高低字节,并且使用16进制表示会非常直观。这里面还会涉及到大小头的问题,印象中当年是看阮一峰的文章学习的;

前面提到过地址总线传输的是地址信息,那么这个地址信息是怎么形成的呢?对于8086这一16位CPU,之所以是16位:寄存器为16位,一次处理数据的能力是16位,运算器和寄存器之间的总线宽度是16位;有这些可以看出,地址总线的宽度不影响CPU的位数定义。
【基础】汇编语言学习笔记
物理地址就是指在内存地址空间中唯一的那个内存地址。16位8086能够传输20位地址信息的原理就是将物理地址分成:段地址+偏移地址实现的。如上图。
物理地址=段地址X16+偏移地址。这个本质上也很好理解,实际上就是因为CPU是16位的,所以要传输20位的地址信息,只能先传输16位的段地址再传输16位的偏移地址,最终根据约定合并成一个20位的物理地址;而且很明显可以看出,段地址一定是16的倍数,当然不是说是先传输的那个16位段地址,而是在逻辑分析时的那个段地址;偏移地址是16位,因此一个段最大为64K。8086CPU有4个段寄存器,CS,DS,ES,SS。

下面重点看一下CS+IP这两个寄存器,在ucos中的任务切换也曾经提到过相关的内容,这也是为什么我会跑回来再过一遍汇编的原因之一。CS——代码段寄存器,IP——指令指针寄存器。8086CPU将要执行的指令就存放在这两个寄存器组成的物理地址中。因此它俩直接决定CPU的执行流程。简单写:CPU将CS:IP存放的内容当作指令执行
【基础】汇编语言学习笔记
在ucos中也提到过,CS和IP是不可以直接写入数值的,不像其他寄存器可以通过mov进行操作。但是最简单的一个方法就是使用jmp指令实现写入。例如:jmp 2AE3:3。
【基础】汇编语言学习笔记
关于段地址寄存器DS的使用方法:
【基础】汇编语言学习笔记
mov指令的几种形式:
【基础】汇编语言学习笔记

这一次复习我不打算去过多地使用汇编进行实践操作,一是曾经做过,二是现在的目的是去回顾一下汇编的思想,让自己对它不再畏惧;三是回顾一下接近硬件底层的一些数据结构例如栈,在学习ucos时任务堆栈是个非常常见的东西。

栈:
在学习ucos的任务切换时,我比较好奇的是为什么它在入栈弹栈时能够对指定的栈空间进行操作?这也是我回顾的一个重点。
【基础】汇编语言学习笔记
这一块是关于栈空间的主要内容。总的来说,关键就是在与SS,SP这两个寄存器,它们时刻指向栈空间的栈顶。(这里需要自己模拟一下push和pop过程中栈顶指针的变化)其实在使用push和pop这两个汇编指令的时候,背后不仅是对数据的读写,而且包括了对SP的维护。也正式因为这样才能实现栈空间的各种功能。
还有一点比较重要,对于8086CPU,栈空间的增长方向是从高地址向低地址增长。所以对于一个空的栈空间,SS:SP应当指向栈底的后一个地址,因为在push时需要先对SP-2,然后才能放入数据。而8086CPU是不提供任何栈越界的防护措施的,也就是说push和pop都有可能威胁到栈空间外部的数据。
【基础】汇编语言学习笔记
我觉得下面这张图讲的很好,的确,对于内存而言里面就是存放的一堆数据,CPU把它当作什么,怎么操作完全是根据寄存器来决定的。这里我注意到一个问题就是栈段,如果使用的是一个大小为64K的段,那么就不会出现栈越界的问题,就像一个环形缓冲池一样。
【基础】汇编语言学习笔记

源程序

一个非常普通的常识:我们编写的源代码到一个可执行文件,首先编译器要将源代码编译成机器码,即目标文件;接下来编译器将目标文件与一些必要的信息连接最终生成一个可以在操作系统中运行的exe文件。连接的必要信息主要是一些描述信息,比如程序有多大,之类的。最终exe在运行的时候会根据这些描述信息将程序和数据载入内存并且初始化CPU使其能够执行程序中的指令。

一些要点:

xxx segment
xxx ends
这是汇编程序中必须要有的一个段。非常重要;
assume cs:xxx将某一个段寄存器与某一个段关联起来,比如这一段是将一段汇编指令作为代码段看待;
end:标志着一个源程序的末尾;
前面提到的xxx,最终会被编译器和连接器翻译成一个段的段地址,即物理地址的一部分;

源程序中程序返回相关内容:
【基础】汇编语言学习笔记

连接的作用
【基础】汇编语言学习笔记
【基础】汇编语言学习笔记

[BX] 和Loop

现在看[bx]其实没什么特别的,它就是一个偏移地址,每次取数据时会自动的和ds寄存器中的段地址做计算最终得到实际的物理地址。而Loop指令则与cx寄存器有关。
【基础】汇编语言学习笔记

关于Loop的初级应用看到下面的代码就可以了,其中可以看到一些细节的处理。总的来说,Loop指令和CX寄存器紧密相连,loop s 会首先将cx中的数值减1,看是否为0,不为0则跳转至S,为0则继续往下执行。这里面还有一个地方值得注意的就是,mov al,[bx],[bx]是物理地址中的数据,但是提取一个字还是字节,完全根据另一个寄存器来定。
【基础】汇编语言学习笔记

包含多个段的程序

【基础】汇编语言学习笔记
上面这张图中比较重要的概念是:assume cs:code与下面的code segment+code ends对应的,在编译的过程中,code会被解释成一个地址的段地址信息,而下面的那些代码将会被存放在这个段中。此外,将该地址与CS关联的目的就是说明该程序在运行的时候会从这个段地址开始,将下面的内容全部解释成指令。实际上,假设code被解释称0BD3,那么dw分配的这16个字节的数据的起始地址就是0BD3:0000~0BD3:000F。所以上述代码是不能正确运行的,因为前16个字节并不是指令而是单纯的数据,下面的方法是改进后的:
【基础】汇编语言学习笔记
当看到这里的时候我有一个疑惑,IP是在哪里指定的?这本书不愧为经典,接下来就解释了这个问题:在这本书前面曾经讲到,我们编写的源代码会被编译成目标文件然后附加上一些描述信息才能形成exe。而上述代码中的”start“,”end start“本身并不是汇编指令而是伪指令,也就是说是交给编译器用的。因此编译器在编译连接的时候就可以根据上述内容将程序的入口地址的IP信息写进描述信息中,供程序真正运行的时候读取。如下便是需要使用到大量数据时的程序架构:
【基础】汇编语言学习笔记
下面这段代码是比较重要的,涉及到栈的使用,其中最重要的是使用dw申请了一段空间,当然最开始使用它的目的是在一段空间中存放一些数据,但是实际上它的另一个作用是开辟一段内存空间。而且如何把它当作一个栈来使用?由一次讲到了前面提到的内容:就是将SS:SP指向栈底,然后使用push和pop进行入出栈,栈顶指针是不需要自己维护的,在这两个指令中已经内嵌好。
【基础】汇编语言学习笔记
很明显,这么做有很多弊端,程序架构混乱,数据容量受限等等问题,所以设计一个具有多个段的程序是必须的。考虑前面的内容可以很容易的得到”assume cs:code,ds:data,ss:stack"。这么做将程序分成多个段,code,stack,data本质上就是一个标号最后都是变成一个地址,因此在代码中就是把它当作一个地址数据,例如:
【基础】汇编语言学习笔记
明确一点:这三个段的名称设计完全是为了方便人阅读,它只是一个过度的标记,因此CPU还是不知道从哪里开始执行。所以还是和前面的一样,需要使用end后面的标号指定程序的入口,写入描述信息中。

更灵活的地址定位

一种新的概念是将[bx]改进为[bx+idata]。这个[bx+idata]本质上很像高级语言中的一个数组。另外两种写法是[bx].idata/idata[bx]。所以可以看出来可能数组的底层实现原理就是这个吧。

SI和DI寄存器:(不能拆分成两个8位寄存器)。它们的作用本质上和bx是一样的。可以通过[si]以及[di]实现对指定内存地址的访问。
【基础】汇编语言学习笔记
【基础】汇编语言学习笔记
例题:
【基础】汇编语言学习笔记
答案如下:可以看到,上述例题涉及到了循环嵌套,首先CS寄存器保存了Loop的次数。所以循环嵌套的时候外层循环的CX会被内嵌循环的CX覆盖,因此需要暂存外部CX,最简单的方法就是使用栈空间,申请一段空间并且将SS和SP初始化为空栈状态,这样接下来只需要进行push和pop即可。此外循环嵌套意味着[]中要使用两个变量,一个控制行一个控制列。
【基础】汇编语言学习笔记

数据处理的两个基本问题

【基础】汇编语言学习笔记
【基础】汇编语言学习笔记
【基础】汇编语言学习笔记

从上面两张图可以看出,对于通用寄存器只有bx是可以放在[]中代指一个内存地址的;此外,bp,si,di,bx之间的关系上一张图可以看出来,bx可以与si,di组合,bp可以和si,di组合,其他组合均不可。应该是由于CPU本身的结构决定的,具体的我觉得不需要深究或者记忆。下面这张图比较重要,它说明[bp]对应的段地址寄存器不是ds而是ss。
【基础】汇编语言学习笔记
对于指令要处理的数据,有三种类型,1:立即数,即在汇编指令中给的;2:寄存器;3.内存地址;下图就是3中对应的常见寻址方式
【基础】汇编语言学习笔记
前面已经知道,汇编指令中从内存地址提取的数据是字节还是字取决于目标寄存器;但当没有寄存器参与时,就需要指明是word还是byte。
【基础】汇编语言学习笔记
【基础】汇编语言学习笔记
上面这段话将的很好,将汇编语言和C等高级语言中常见的结构体联系在了一起,bx确定后就可以知道结构体的起始地址。idata可以定位某一项的内容,有点像数据元素中的数据项;而这个项本身可能还是一个数组或者字符串,那么修改它的内容就交给最后的si来做。

8.7节 div指令,看到这个我忍不住想起html中的div块。谁想到只是个除法运算符,其中有很多细节我暂时不打算去看了

此外就是datasegement中常见的内存申请指令:db,dw,dd的含义需要理解。

转移指令

转移指令其实就是修改CS:IP中的数值的指令,例如loop就是一个转移指令。常见的还有jmp。下图是一个小插曲,将了offset操作符的作用,就是提取指定标号的偏移地址。

【基础】汇编语言学习笔记
下面开始详细介绍jmp指令的功能:

  1. 段内短转移:关键词short,转移的偏移数值只能是前后7位即128个字节左右。首先可知,指令的执行过程中,是将指令读取到指令缓冲中后,IP立即转移到下一个待执行的指令。这意味着什么?在执行jmp的指令时,IP指向add ax,1;这里最关键的是:jump short s在被编译后会变成一个偏移地址,也就是说CPU并不知道jmp的目标地址是多少,它只知道需要从add ax,1的地址开始偏移多少字节。
    【基础】汇编语言学习笔记
  2. 段内近转移:关键词near唯一的不同就是可以实现前后15位的转移
  3. 远转移:关键词far。这个指令可以显性的看到跳转的目标地址CS:IP。
    【基础】汇编语言学习笔记
    此外jmp的执行对象也可以在内存中,字对应着IP即段内转移,双字对应着CS:IP可以实现段间转移。
    jcxz指令,如果不是看到这个指令我已经完全想不起来了。
    【基础】汇编语言学习笔记
    【基础】汇编语言学习笔记

call和ret指令

本质上也是转移指令,可以修改CS:IP的数值,ret不同的地方在于,它是从栈中提取出的数据给IP,或者同时给CS和IP;
【基础】汇编语言学习笔记
下面这个程序作了如下几件事:首先有一个栈空间,在end后面的start告诉编译器将start标号所在的指令地址写入exe的描述信息中使得程序运行时CS:IP指向start的指令。程序开始运行后首先设置stack的参数,然后将cs段寄存器以及0入栈。在使用retf时,做了如下的事情,弹出一个字给IP,弹出一个字给CS,这个过程中的SP的维护在指令中已经设计好。所以执行完后CS:IP指向的是code代码段中偏移地址为0处的指令,继续执行到程序结束。

【基础】汇编语言学习笔记
下面是关于call指令的相关内容:
【基础】汇编语言学习笔记
根据位移来实现的近转移,原理和jmp类似
【基础】汇编语言学习笔记

【基础】汇编语言学习笔记

【基础】汇编语言学习笔记
【基础】汇编语言学习笔记
这里很容易想到如果转移的地址在内存中,就存在两种情况,call word ptr ds:[0]/call dword ptr ds:[0]。分别代表在使用call的时候会从指定的地址取出IP或CS+IP。在这之前都会先将当前的CS:IP入栈。因此可以想到,call可以实现类似函数的调用,而ret则可以在函数执行完成后返回到函数调用时的CS:IP,可以想到,是call后面那条指令的CS:IP。所以这里也就可以明白,不带参数的函数调用中,从调用-执行-返回的整个底层过程,当然,也许会存在函数嵌套,但是原理是一样的。

标志寄存器

中断

先前听到的最多的就是中断向量、中断源等,这本书讲得比较浅显易懂。
中断的宏观过程是CPU接收到中断信息,停下当前的操作去执行中断服务程序。但这其中包含了很多细节:

  • CPU接收到的中断信息中包含有中断类型码;
  • 中断向量表中,不同的中断类型码对应着不同的中断向量;
  • 中断向量就是指终端服务程序的地址,即CS:IP
  • CPU如何找到中断向量表?中断向量表的地址是默认确定的。

下面是进入中断前的一系列操作:
【基础】汇编语言学习笔记
这一步解释了很多问题:CPU在处理中断前只涉及到CS、IP、标志寄存器的入栈。而其他通用寄存器的数据则交由中断服务程序进行处理。
【基础】汇编语言学习笔记

相关文章: