《汇编语言》——笔记(一)

基础知识

在讲汇编语言之前,先介绍下机器语言。机器语言是机器指令的集合。电子计算机的机器指令是一列二进制数字,计算机将转变高低电平,来驱动电子器件。

计算机是可以执行机器指令,进行运算的机器。这是早期的概念。现在,有一个芯片来完成上面所说的计算机的功能。这个芯片便是CPU(Central Processing Unit,中央处理单元),CPU是一种微处理器。

每一种微处理器,由于硬件设计和内部结构的不同,就需要不同的电平脉冲来控制,使它工作。所以每一种微处理器都有自己的机器指令集,也就是机器语言。

早起程序员通过在纸带上打孔来进行输入,1打孔,0不打孔。

一旦程序出错,由无数0和1构成的机器指令无疑令人头大,甚至给整个产业的发展带来了障碍。于是汇编语言产生了

汇编指令和机器指令的差别在于指令的表示方法上。汇编语言是机器指令便于记忆的书写格式。

计算机能读懂的只有机器指令,需要有一个能够将汇编指令转换为机器指令的翻译程序,我们称之为编译器。用汇编语言写出的源程序,经过汇编编译器编译为机器码,由计算机最终执行。

汇编语言发展至今,由以下3类指令组成:

  • 汇编指令:机器码的助记符,有对应的机器码
  • 伪指令:没有对应的机器码,由编译器执行,计算器不执行
  • 其他符号:如+、-、*、/ 等,有编译器识别,没有对应的机器码。

存储器

要想让一个CPU工作,就必须向它提供指令和数据。指令和数据在存储器中存放,也就是我们平时说所的内存。

指令和数据的

在内存中,指令和数据没有任何区别,都是二进制信息。

存储单元

存储器被划分为若干个单元,每个存储单元从0开始顺序编号。电子计算机的最小信息单位是bit(比特),也就是一个二进制位。8个bit组成一个Byte,也就是通常讲的一个字节。

CPU对存储器的读写

CPU要想进行数据的读写,必须和芯片进行3类信息的交互:

  • 地址信息:存储单元的地址
  • 控制信息:读写,器件选择
  • 数据信息:数据

电子计算机只能理解电信号,电信号用导线传送。在计算机中专门有链接CPU和其他芯片的导线,通常称为总线。

根据物理信息的不同,逻辑上分为3类,地址总线、控制总线和数据总线。

地址总线的宽度决定了CPU的寻址能力;

数据总线的宽度决定了CPU与其他部件进行数据传送的一次数据传送量;

控制总线的宽度决定了CPU对系统其他期间的控制能力;

各类存储器芯片

一台PC机中,装有多个存储器芯片这些芯片从物理连接上看是独立的、不同的器件。从读写属性分为两类:RAM(随机存储器)和ROM(只读存储器),RAM可读可写,关机后内容丢失;ROM只读,关机后内容不丢失。

从功能和连接上有课分为以下几类。

  • 随机存储器
  • 装有BIOS的ROM:例如主板的ROM,网卡的ROM,显卡的ROM……
  • 接口卡上的RAM:列入显卡上的RAM,一般称为显存

内存地址空间

上述存储器,在物理上都是独立的器件,但是在以下两点上相同。

  • 都和CPU的总线相连
  • CPU对它们进行读写时都通过控制线发出内存读写命令

也就是说,CPU在操控他的时候,都当做内存来对待,把他们总的看成一个有若干存储单元组成的逻辑存储器,这个逻辑存储器就是我们所说的内存地址空间。对一段地址空间的读写,实际上就是在相对应的存储器中读写数据。

寄存器

一个电信的CPU由运算器、控制器、寄存器等器件组成,这些器件靠内部总线相连。前面所说的总线,相对于CPU内部来说是外部总线。简单的说,在CPU中:

  • 运算器进行信息处理
  • 寄存器进行信息存储
  • 控制器控制各个器件进行工作
  • 内部总线连接各种器件

对于汇编程序员来说,CPU的主要部件是寄存器,通过改变各种寄存器的内容来实现对CPU的控制。

不同的CPU,寄存器的数目、机构不像同。8086CPU有14个寄存器,这些寄存器是AX、BX、CS、DX、SI、DI、SP、BP、IP、CS、SS、DS、ES、PSW。

通用寄存器

8086寄存器都是16位的,可以存放两个字节,AX、BX、CX、DX这四个通常用来存放一般性的数据,称为通用寄存器。

8086为了兼容上一代CPU的程序,上面的四个通用寄存器都可分为两个独立的8为寄存器使用。

  • AX可分为AH和AL;
  • BX可分为BH和BL;
  • CX可分为CH和CL;
  • DX可分为DH和DL。

字在寄存器的存储

出于兼容性考虑,8086CPU可以一次性处理两种尺寸的数据。

  • 字节:byte,一个字节由8个bit组成,存在为寄存器中。
  • 字:word,一个字有两个字节是个,分别称为高位字节和低位字节

以AX为例,一个字型数据的高八位存储在AH中,低八位存储在AL中。

另外:通常以B(binary)、D(Decimal)、H(Hexadecimal)来表示二进制、十进制、十六进制,而4位二进制可以与一位十六进制相互转换。

mov ax,bx
mov ah,al
;不区分大小写,将寄存器BX的数据送入寄存器AX
;注意指令的两个操作对象应当是一致的

物理地址

8086是16为结构的CPU,也就是说,能够一次性处理、传输、暂时存储的信息最大长度为16位。

8086CPU有20位地址总线,可以传送20位数据,达到1MB的寻址能力。

如果将地址从内部简单发出,只能送出16位的地址,只有64KB的寻址能力。

8086CPU采用一种在内部用两个16位地址合成的方法形成一个20位的物理地址。

80806CPU要读写内存时,提供的两个16位地址被地址加法器合成一个20位的物理地址。

地址加法器采用物理地址=段地址*16+偏移地址的方法合成物理地址。

注意:这里说段地址并不是说内存被划分成了一个一个的段,每一个段有一个短地址。段的划分来源于CPU,因为物理地址的合成方式,使我们能以分段的方式管理内存。

段寄存器

8086访问内存时,需要相关的部件提供内存单元的短地址和偏移地址,那么,是什么部件提供提供短地址。段地址保存在8086CPU的段寄存器中存放,有4个段寄存器:CS、DS、SS、ES。这里只介绍CS.

CS和IP

CS为代码段寄存器,IP为指令指针寄存器。CS提供的为段地址,IP提供了偏移地址。

8086CPU的工作过程如下。

  • 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器;
  • IP=IP+所读指令的长度,从而指向下一条指令;
  • 执行指令,从头开始重复这个过程。

8080CPU加电启动后,CS和IP被设置为CS=FFFFH,IP=0000H,那么,FFFF0H单元中的指令是8086CPU开机后执行的第一条指令。

任意时刻,CS:IP所指向的指令就是下一条指令所在的段地址和偏移地址。

修改CS:IP的值

我们能够用指令读写的部件只有寄存器,通过实现改变寄存器中的内容实现对CPU的控制。CPU从何处执行指令是由CS:IP中的内容决定的,修改CS:IP的值就可以控制CPU执行目标指令。

前面提到过 mov指令, mov ax,123ax中的值设为 123,8086CPU大部分寄存器的值,都可以用 mov指令来改变, mov指令被称为传送指令。

但是, mov指令不能用于设置CS、IP的值。8086CPU提供了另外的指令来改变它们的值。

若想同时修改CS、IP的内容,可用 jmp短地址:偏移地址的指令来完成。

若仅想修改IP,可用 jmp某一合法寄存器的指令来完成。jmp ax,在含义上好似:mov IP,AX

前面说过,在内存中,指令和数据都是二进制信息。CPU也无法区分指令和数据,CPU只认被CS:IP所指向的内存单元中的内容为指令。

实验环境

笔者使用的为Ubuntu server 18.08,通过SSH链接。

sudo apt install dosbox
mkdir MASM
vim .dosbox/dosbox-0.74.conf #文件末添加如下指令
mount c: ~/MASM
c:
debug

MASM文件下至少放着masm.exe,link.exe,debug.exe。

下面是debug常用调试命令说明。

  • R:观看的修改寄存器的值;
  • D:查看内存的内容;
  • E:修改内存的内容;
  • U:将内存中的机器指令翻译为汇编指令;
  • T:执行一条机器指令;
  • A:以汇编指令的格式向内存写入一条机器指令。
  • Q:退出DEBUG,回到DOS状态

笔者日常的操作是这样,使用A 写入某内存单元,使用R修改CS,IP的值,使用T执行命令。也可以使用E写入机器码,较为复杂。

寄存器(内存访问)

8086CPU用16寄存器存储一个字,高8位存放高位字节,低8位存放低位字节。在内存中存储时,由于内存单元都是字节单元,则一个字需要两个地址连续的内存单元存放,这个字的地位字节都存放在低地址单元,高位字节存放在高地址单元中。

DS和[address]

CPU读写一个内存单元的时候,必须给出内存单元的地址,内存单元由段地址和偏移地址组成。8086CPU中有一个DS寄存器,通常用来存放要访问数据的短地址。

mov bx,1000H
mov ds,bx
mov al,[0]
;[address]表示一个便宜地址为address的内存单元
;用上述指令实现读取10000H(1000:0)单元中的数据独到AL中

由于8086CPU的硬件设计缺陷,不支持数据直接送入段寄存器。

上面实现了内存单元到寄存器,寄存器到内存单元是怎样的呢。

;H是指名16进制,实际不需要输入
mov bx,1000H
mov ds,bx
mov [0],al

回忆 mov这个命令,含有两个操作对象,这里介绍另外两条汇编指令, addsub,一个相加,一个相减。

到现在,我们知道,mov指令可以有以下集中形式。

mov 寄存器,数据
mov 寄存器,寄存器
mov 寄存器,内存单元

mov 段寄存器,寄存器
;可以推测既然有"mov 段寄存器,寄存器”,是不是应该也有"mov 寄存器,段寄存器,是的
mov 寄存器,段寄存器
mov 内存单元,寄存器
;是否也有"mov 段寄存器,内存单元"呢,是的
mov 段寄存器,内存单元
;注意内存单元有DS和指令中的便宜地址给出

数据段

对于8086PC机,编程时,可以根据需要,将一组内存单元定义为一个段。我们都可以将一组长度为N (N≤64)、地址连续、起始地址为16的倍数的内存单元当做专门存储数据的内存空间。

如何访问数据段中的数据呢?可以在具体操作的时候,用DS存放数据段的段地址,在根据需要,用相关指令访问数据段中的具体单元。

栈是一种具有特殊的访问方式的存储空间。他的特殊性就在于,最后进入这个空间的数据,最先出去。

使用一个盒子和3本书来描述栈这种操作方式。叠起来的三本书依次放入入盒子中,注意此过程中的顺序。

现在的问题是,一次只允许取一本,我们如何将3本书从盒子中取出来?一本本取出即可。取出的顺序正好和放入的顺序相反。

从程序化的角度来说,应该有一个标记,一直指示着盒子最上边的书。

从栈的角度描述上述过程,放书和取书的过程是入栈和出栈,标记着盒子最上边的为栈顶标记。

栈的这种操作规则称为:LIFO(last In First Out,后进先出)。

CPU提供的栈机制

8086CPU提供入栈和出栈的指令,最基本的两个分别是PUSH(入栈)和POP(出栈)。

那么,CPU如何知道内存空间当做栈使用?PUSH、POP操作时栈顶标记会发生改变,CPU如何知道这栈顶标记呢?

回想另一个问题,CPU如何知道当前要执行的指令所在的位置?CS、IP所指的指令存放着当前指令的短地址和偏移地址。

是否有其它寄存器存放栈顶的位置呢?是的,8086CPU中,段寄存器SS和储存器SP,栈顶的短地址存放在SS中,偏移地址存放在SP中,任意时刻,SS:SP指向栈顶元素

注意:PUSH只对字操作(不允许字节进栈)时,PUSH导致SP减2,POP导致加2。

为什么呢?栈本质上也是一段内存空间,还记得数据在内存中的存放方式吗。字型数据有两个单元组成,高地址存放高8位,低地址存放低8位。例如,定义一段内存区域为栈,内存区域从低地址向高地址增长,PUSH的数据将被放在这段区域尾部(此时数据任然以高位地址单元存放高位字节,低位地址单元存放低位字节),继续PUSH,栈顶标记应该向上移动,也就是从高地址单元箱低地址单元移动,PUP也同理。

栈顶越界的问题

不知道你有没这样一个疑惑,SS:SP虽然指出了栈顶的位置,但是,进行PUSH或POP完栈后,继续PUSH或POP呢?8086CPU只知道栈顶在何处,而不知道我们安排的栈空间有多大。

我们在编程的时候要操心栈超界的问题,要根据可能用到的最大栈空间,来安排栈的大小,防止入栈的数据太多导致超界;执行出栈操作的时候也要注意,防止栈空的时候继续出栈导致超界。

利用栈溢出来执行攻击指令的的例子是莫里斯蠕虫,它利用C语言标准库中gets()函数并未限制输入数据长度的漏洞,从而实现了栈溢出。

PUSH,POP指令

push和pop指令的格式可以是如下形式:

push 寄存器;
pop 寄存器;
push 段寄存器;
pop 段寄存器;
push 内存单元;
pop 内存单元;
;PUSH和POP指令访问的内存单元的地址来源于SS:IP。同时PUSH和POP指令也改变了SP中的内容。

我们需要明确的是。执行PUSH时,CPU的两步操作时,先改变SP,后想SS:IP出传送。执行POP时,CPU的两步操作是:先读取SS:IP处的数据,后改变SP。也就是说,POSH,POP等栈操作指令,只修改了SP,即栈顶的变化范围为:0~FFFFH。

栈段

如果将1000H~1FFFFH这段空间当做栈段,初始栈段位空,此时SS=1000H,SP=?,这里是作者的两种思路

  • 任意时刻,SS:IP指向栈顶单元,但栈中只有一个元素时,SS=1000,SP=FFFE,加2后SP=0,所以,栈为空时,SP=0
  • 任意时刻,栈为空时,SS:IP只能指向栈最底部单元,该单元的地址为最底部的字单元+2。栈最底部字单元的地址为1000:FFFE,所以栈空时,SP=1000H。

段的综述

我们可以将一段内存定义为一个段,用段地址指示段,用偏移地址访问段内的单元。

用一个段存放数据,定义为”数据段“;

用一个段存放代码,定义为”代码段“;

用一个段当做栈,定义为“栈段”;

对于数据段,段地址存放在DS中,用[address]来指示段地址寄存器中的偏移位置;

对于代码段,段地址存放在CS中,将段中的第一条偏移地址存放在IP中;

对于栈段,段地址存放在SS中,将栈顶单元的偏移地址放在SP中。

原文发布于微信公众号 - 渗透云笔记(shentouyun)

原文发表时间:2019-06-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券