汇编干货第三章

释放双眼,带上耳机,听听看~!

是时候

关注

我们一波了

包含多个段的程序

前面说道,如果要使用安全的内存空间,0:200~0:2FF是相对安全得内存空间,可是这段空间只有256字节,如果需要的空间大于256字节该怎么办呢?

在操作系统允许的情况下,程序可以取得任意容量的空间。

取得空间的方法有两种。

  • 加载程序时为程序分配
  • 执行过程中向系统申请(这里不讨论)

若要一个程序在加载时取得所需的空间,则必须在源程序做出说明。

上面是从内存空间获取的角度上,谈定义段的问题。为了可读性、功能设计,一般一额定义不同的段来存放。

关于段的问题,我们将以这样的顺序讨论多个段的问题:

  • 在一个段中存放数据、代码、栈;
  • 将数据、代码、栈放入不同的段中。

在代码段中使用数据

我们可以在程序中,定义我们希望处理的数据,数据作为程序的一部分一同被编译、链接写到可执行文件中。

考虑这样一个问题,编程计算8个数据的和,结果存放在AX寄存器中,下面是用我们前面知识写出的代码。

这里出现了一个新的指令dwdw即“define word”,在这里,定义了8个字型数据,占用16字节的内存空间。

使用Debug调试程序,不运行,发现一个问题,程序所在的内存区为075C:0(DS=075C),前256字节存放着PSP,程序的存放位置应为076C:0,使用U指令查看确发现有点不对。

实际上,看到其实是有dw定义的数据,从第16字节开始才是汇编指令对应的机器码。

怎样执行程序中的指令呢?在Debug中,可以手动修改IP寄存器的值,从而使CS:IP指向程序的另一条指令。

这样一来,在系统运行时就会出现问题,程序的入口不是我们希望执行的指令。

借助伪指令可以通知编译器程序的入口。

有了这个指令,可以仿照这个模板写出更多的程序,start上面安排数据,startend start之间安排代码。

在代码段中使用栈

这里的检测点没做出来,看视频才想通的,后来发现这个题目第一眼没看懂。

检测点考察dw定义的数据在内存空间的位置,理解了这一点,题目就可以做出来了。

注释未知的指令,在debug模式中运行可以直观的感受到到这一点。

将数据、代码、栈放入不同的段

前面的内容中,我们将数据、栈和代码都放到了一个段里面,编程的时候需要注意何处是数据,何处是栈,何处是代码。显然这样有问题:

  • 程序混乱
  • 8086的段空间限制只有64KB

下面的程序用不同的段实现了上面的功能

  • 对于不同的段,使用不同的段名联系不同的段寄存器。

  • 注意红线的部分,段名相当于一个标号,代表了段地址,8086CPU不允许将一个数值送入段寄存器,因此使用其它寄存器中转

  • “代码段”、“数据段”、“栈段”完全是我们的安排

    CPU如何处理定义的段的内容,取决与程序中具体的汇编指令。

    • 段名只是为了阅读性
    • cs:code等代码将段名和寄存器联系起来
    • end start指明了程序的入口,CS:IP指向这个入口,从而执行程序的第一条指令

编写、调试具有多个段的程序

这里是两个检测点,为了理解不同段在内存空间中的排列,一个段在内存空间中最小单位为16字节。

限于篇幅原因,我这里介绍比较最后的一个实验。

程序如下,编写code段的代码,用push指令将a段中的前8个字型数据逆序存储在b段中。

写出程序很容易,不过这不是我要说的重点。

程序运行完之后,查看内存空间,注意我这里查看的DS。

从数据对应关系不难判断,76C:00~76C:1F是我们定义的数据段, 076C:20~76C:2E是我们定义的栈段,76C:30之后是代码段。

查看对应的寄存器,也验证了这一点,

$076C \times 10H+0=76C0H=76C \times 10H+0$

$076C \times 10H+20=76E0H=76E \times 10H+0$

$076C \times10H+30=76F0H=76F \times 10H+0$

这里主要涉及到了一些段编译的规则,编译的规则影响了内存的分配。我们在使用SSD格式化的时候,有一个选项为4K对齐,4K对齐是为了让操作系统的最小分配单元和闪存的一个页对应,提高读写效率,实际使用过程中,即使文件没有那么大,实际占用的均为4KB的倍数,在这一点上和编译规则有些相似。

更灵活的定位内存地址的方法

前面,我们用[0]、[bx]的方法,定位内存单元的地址。本章介绍更为灵活的定位内存地址和相关的编程方法。

and和or指令

1
2
3
4
5
6
7
8
;and指令:逻辑与指令,按位进行与运算
mov al,00001111B
and al,11110000B
;执行后AL=00000000B  相应位设为0
;or指令:理解与指令,按位进行或运算
mov al,00001111B
or  al,11110000B
;执行后 AL=11111111B 相应位设为1

以字符形式给出的数据

计算机中所有的信息都是二进制,而人能理解的信息是具有约定意义的字符。将字符存储在计算机中,就要对其进行编码。计算机存储的信息展示给我们看时,就要对其进行解码。

ASCII是基于拉丁字母的一套编码系统。例如,文件编辑过程中,按一下按键的“a”键,计算机用ASII码规则编码为61H存储在内存中;文件编辑器从内存中取出61H,送入显卡上的显存中;显卡用ASII码的规则解释显存的内容,显卡驱动显示器,我们在显示器看到了字符“a”。

我们可以在汇编程序中,用‘……‘的方式指明数据是以字符的形式给出,编译器将转化为相对应的ASCII码。

大小写转换的问题

在codesg中填写代码(我这里写好了),将datasg中的第一个字符串转化为大写,第二个字符转化为小写。

查看字母的ASCII表。

可以发现,大写字母到小写字母在于寄存器中第5个字符的不同(我没说错,从右往左数,从0到7),那么这道题的关键在于将第5个字符置0的转换了,写出上面的代码就很简单了。

[bx+idata]

前面使用[bx]的方式来指明一个内存单元,还可以使用[bx+idata]来表示内存单元,他的偏移地址为[bx]+idata。

有了这个特性,前面我们做过一道将a段和b段的内容相加到c段中的题目,可以将代码优化。

可以看出灵活的内存访问方式,减少了指令,加快的程序运行速度。

SI+DI

SI和DI是8086CPU中和BX功能相近的寄存器,SI和DI不能分成两个8为寄存器来使用

[bx+si]和[bx+di]

在前面,我们用[bx]和[bx+idata]的方式来指明一个内存单元,还可以使用更为灵活的方式:[bx+si]和[bx+di]

[bx+si]表示一个内存单元,它的偏移地址为(bx)+(si)(即bx中的数值加上si的数值)

[bx+si+idata]和[bx+di+idata]

[bx+si+idata]表示一个内存单元,偏移地址为(bx)+(si)+(idata)。

不同的寻址方式的应用

  • [idata]用一个常量表示内存地址,可直接定位一个内存单元
  • [bx]用一个变量表示内存地址,可间接定位一个内存单元
  • [bx+idata]用一个变量和常量表示内存地址,可在一个起始位置的基础上用变量间接定位一个内存单元
  • [bx+si]用两个变量表示地址
  • [bx+si+idata]用两个变量和一个常量便是地址。

如下图,将datasg段中的每个单词改为大写字母

db指令和dw指令类似,不过他定义的是字节型数据

总共数据有4行,每行有3个字母需要更改,也就是$4\times3$此二重循环,有限的循环可以使用loop指令,这里需要存储两个循环次数,经过艰苦的思考(并没有,我想不出来,看书上思路了),可以使用空寄存器DX暂存,循环完成后又拿回(下图左)。

程序中进场需要进行数据的暂存,寄存器的数量有限,如果不适用寄存器,只能使用内存了,我们开辟了新的一块内存,先存放在内存中,需要的时候在从内存单元中恢复(下图中)。

我们使用内存来暂存数据,这是比较聪明的选择,但是值得推敲的是,我们用怎样的结构来保存这些数据,从而使程序更为清晰。

一般来说,在需要暂存数据的时候,都应该使用栈。

我们使用栈暂存数据,采用相关的指令将数据入栈,需要时在出栈(下图右)。

为什么要用[bx+si+data]的形式来表示?

  • 为了可阅读性,理解数据的起始,体现了偏移的思想

程序如何改进?

  • 更多的数据入栈,比如上面的BX进行入栈

数据处理的两个基本问题

  • 处理的数据在什么地方
  • 要处理的数据有多长

reg和sreg

定义描述性的符号reg和sreg,reg表示一个寄存器,sreg表示一个段寄存器。

reg:x、bx、cx、dx、ah、al、bh、bl、cx、bl、dh、dl、sp、bp、si、di;

sreg:ds、ss、cs、es

bx、si、di和bp

  • 8086CPU中,只有这4个寄存器可以用在[…]中进行内存单元寻址
  • […]可以单个出现,或只能以4中组合出现:bx和si、bx和di、bp和si、bp和di.
  • 指令没有显性给出段地址,段地址就默认在SS中。

处理的数据在什么地方

机器指令不关心数据的值多少,而关心指令执行前一刻,它将要处理的数据所在的位置。指令执行前,处理的数据可以在3个地方:CPU内部、内存、端口(后面介绍)

汇编语言中数据的表达

  • 立即数:直接在包含在机器指令中的数据(执行前在CPU的指令缓冲器中),称为立即数(idata)
  • 寄存器:数据在寄存器中,给出寄存器名使用
  • 段地址(SA)和偏移地址(EA):段地址默认在DS中,若BP做为有效地址的一部分,段地址默认在SS中,可以显性给出段寄存器

寻址方式

数据存放在内存中的时候,可是用多种方式给定这个内存单元的偏移地址,这种定位内存单元的方法一般被称为寻址方式。

指令的数据有多长

8086CPU中,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明,指令进行的是字操作还是字节操作。

  • 通过寄存器指明要处理的数据的尺寸,mov ax,1mov al,1
  • 没有寄存器时,用操作符指明:X ptr指明内存单元长度,X可以为word或byte

顺便说一下,[bx].10h[si]=[bx+16+si]

div指令

div是触发指令

  • 除数:有8位和16位两种,在一个reg或内存单元中
  • 被除数:默认放在AX或DX和AX中
    • 如果除数为8位,则被除数则为16位,默认在AX中存放
    • 如果除数为16位,被除数为32位,在DX和AX中存放,DX存放高16位,AX存放低16位
  • 结果
    • 如果除数为8位,AL存储除法操作的商,AH存储除法操作的余数
    • 如果除数为16位,AX存储除法操作的商,DX存储除法操作的余数

单纯看这段话容易看懵,在debug模式中试验下。

说明,这里演示的是$\frac{16}{3}=5\cdots\cdots1 $,其中16的部分在指令中我使用的是十六进制”10H“。

伪指令dd

前面使用dbdw定义字节型数据和字型数据。dd用来定义dword(double word,双字)型数据

伪指令dup

dup是一个操作符,同db、dw、dd等一样,也是由编译器识别处理的符号。配合db、dw、dd等数据定义伪指令使用,用来进行数据的重复。

使用的格式如下

1
2
3
db 重复的次数  dup (重复的字节型数据)
dw 重复的次数  dup (重复的字型数据)
dd 重复的次数  dup (重复的双字型数据)

实验七

这里基本将所有的知识都运用起来了,笔者自己做的时候感觉自己好渣,想不出来。还是需要进行分析,理解数据从哪里来,到哪里去,中间做了什么。

题目是将data中的年、收入、计算的人均收入写到table段中。直接给代码。

  • 年份的传递,可以使用寄存器,使用栈明显是更好的选择
  • 使用相应的寄存器存储偏移量,可以使用[bx+idata]访问数据节省寄存器
  • 除法运算中被除数为双字,使用AX、DX分别存储低16位和高16位,将AX中的商传递到Table段

实验的反思

这个实验的段名仿佛在暗示什么,回顾我们做了什么,我们将零散的数据结构化,使数据阅读性提升,使用偏移地址访问非常遍历。换句话说,如果编写程序时,将数据结构化,程序效率也将提升。程序的效率与数据组织的合理不合理有关,有一门课程叫数据结构,讲的就是这么个问题。

本质上可以归纳为对数据的组织,而下一章转移指令的原理,本质上是对代码的组织。

总结

前两章的介绍都是为了理解数据在内存中如何排列,后一章理解数据在处理过程中的细节。

戳原文,更有料!

本文源自微信公众号:渗透云笔记

人已赞赏
0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
今日签到
有新消息 消息中心
有新私信 私信列表
搜索