标签 8086CPU 下的文章

前言
两年前,我写过一篇《【汇编学习随记】汇编中栈的定义》,当时是初次接触汇编语言,对于栈的理解仅限于理解这是一种数据结构,自己却没有理解怎么用栈。当时确实是肤浅了。两年后的今天,我总算是搞清楚了汇编中栈究竟是什么,写下来分享给大家。

栈是什么
数据结构上理解栈,我想不必过多解释了,本质就是一块具有特定进出规则的内存空间,在这块空间内遵循数据先进后出的原则。
但我们作为汇编学习者,仅有这些理解是不够的,让我们从8086CPU的角度看看栈。在8086CPU中,提供了一对段寄存器SS和寄存器SP,用于指向栈顶,CPU对栈的两个操作push(入栈)和pop(出栈)都取决于这两个(段)寄存器,但却没有寄存器能用于标识栈底,这意味着如果我们不提前计算好哪些内存单元是栈空间,栈空间到哪里,那么就很容易出现栈顶超界的问题,可能影响到其他内存空间的数据,从而导致程序异常。(这里也体现了汇编语言和高级语言的不同:汇编语言要求你手动把内存空间抠的很细,但是用高级语言编程时肯定不用考虑这些,因为操作系统已经代为安排好了。想想C++ STL库里的那个栈,根本不用想栈顶是否超界对吧)。对此,我们只能在设计程序时就预留好栈。
因为这一特点,通常情况下,我们不会把栈和代码放在一个段里,而是给栈单独开一个段。
化学上常说,性质决定用途。由于栈这一独特的结构,(至少在汇编中)栈通常用于多层循环时,进入内层循环前暂存外层循环的数据,内层循环结束了再将这些数据恢复出来。这是一个编程时的惯例。

实际应用
上一篇文章中的双层循环就是一个不错的例子,这里就不新出题目了,直接搬过来,要求用栈暂存数据~
编程,将datasg段中每个单词改成小写字母(用栈)

assume cs:codesg,ds:datasg,ss:stacksg

stacksg segment
stacksg ends

datasg segment
  db 'Far             '
  db 'You             '
  db 'HJC             '
  db 'BBS             '
datasg ends

codesg segment
  start:
codesg ends
ends start

直接上答案:

assume cs:codesg,ds:datasg,ss:stacksg;三个段寄存器cs、ds、ss分别关联三个段:代码段、数据段、栈段

stacksg segment
  dw 0,0,0,0,0,0,0,0;定义出16个位,刚刚
stacksg ends

datasg segment
  db 'Far             '
  db 'You             '
  db 'HJC             '
  db 'BBS             '
datasg ends

codesg segment

  start:mov bx,0
        mov cx,4
        mov sp,0;标记当前栈底
        
     s0:push cx;进入内层循环前,cx入栈
        mov si,0
        mov cx,3;重置cx,进内层循环
        
      s:mov al,[bx+si]
        or al,00100000b
        mov [bx+si],al
        inc si
        loop s
        
        add bx,16
        pop cx
        loop s0
        
        mov ax,4c00h
        int 21h
codesg ends
end start

结语
汇编中的栈和高级语言的栈还是有差别的,虽然是同一种数据结构,但汇编的栈直接由底层硬件(电路)提供支持,而高级语言中的栈其实是用程序模拟的。我是faryou,下期见!

前言
(没有前言,因为写不出来)

段是什么
我们的程序在运行的时候,需要调用各种数据,这些数据在内存中,如果和代码混在一起,那么显然,调用会十分麻烦,代码则极度混乱。
段的出现就是为了解决这个问题。字面意思理解段,就是把程序分成“一段一段”,每段有自己的用处。8086CPU中,有三种段:代码段(存放你程序的代码)、栈段(作为栈使用,通常只有一段)、数据段(存放你的静态资源,即长文本、图片、视频)。

如何使用段
汇编语言中,用segment和ends分别表示段的开始与结束。格式如下:

段名 segment
段内代码
段名 ends

在汇编中,可以直接引用段名代表引用其段地址,以下语句是合法的:

mov 寄存器,段名

通常情况下,我们会用三组寄存器来标记三个地址:CS:IP标记代码段,DS:BX标记数据段,SS:SP标记栈段,CPU以这三组寄存器为依据来确定操作对象。
由此,我们只需要改变这些寄存器的内容,即可以改变CPU的操作对象。
需要注意的是,对于CS:IP,我们无法使用mov直接改变其值,而应使用jmp等转移指令。

结语
本文主要讲解了一下什么是段,及段的基本用法,实际操作将在之后几节讲解,我是faryou,下期见!

前言
写程序的过程中,我们会用到许多数据,包括图片、视频、长文本等,这些数据在程序运行前就应该同代码一起暂存至内存中,同时,栈空间也需要预先分配。今天我们来学习一下汇编中存放数据的方式。

操作符X ptr
8086CPU支持处理尺寸为1byte或1word。在汇编指令中,如果存在寄存器,那么可以由寄存器名直接指明操作数据的尺寸。例如:

mov ax,[0];字操作(ax为16位寄存器)
mov al,[0];字节操作(al为8位寄存器)

但是如果没有寄存器名存在,那我们可以用操作符X ptr指明内存单元长度,例:

mov word ptr [0],1
inc word ptr [0]
add word ptr [0],1
;用word ptr指明操作的是一个字
mov byte ptr [0],1
inc byte ptr [0]
add byte ptr [0],2
;用byte ptr指明操作的是一个字节

伪指令db、dw、dd
这三个指令都是伪指令,为了方便程序员快速向内存中写入数据而存在,格式:

db/dw/dd 数据1,数据2,...;可以写入任意个数字节型/字型/双字型数据

同时,db也可以用于写入字符串,每个字符占1个字节,例:

db 'faryou';从该处开始的6个内存单元存放字符串'faryou'的ASCLL码

dup操作符
dup操作符的存在可以让我们更方便地写入重复数据,它在使用时与db/dw/dd联用,格式:
db/dw/dd 重复次数 dup (重复的数据)
例如:

db 5 dup (0);相当于:db 0,0,0,0,0
db 5 dup (0,1,2,3,4);相当于:db 0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,0,1,2,3,4,0,1,2,3,4
dw 5 dup ('faryou ','HJCBBS ');相当于:dw 'faryou HJCBBS faryou HJCBBS faryou HJCBBS faryou HJCBBS faryou HJCBBS'

div除法指令
div是除法指令,但是其格式与add、sub不同,其需要与ax寄存器或dx&ax寄存器联合使用,格式:

div 寄存器名/内存单元

div可以进行两种除法:
第一种(在div指令中用byte ptr指明):
被除数(16位):存放于ax寄存器中
除数(8位):存放于div指令中指明的寄存器/内存单元中
商(8位):存放于al寄存器中
余数(8位):存放在ah寄存器中

第二种(在div指令中用word ptr指明):
被除数(32位):存放于dx&ax寄存器中,其中dx存放高16位,ax存放低16位
除数(16位):存放于div指令中指明的寄存器/内存单元中
商(16位):存放于ax寄存器中
余数(16位):存放于dx寄存器中
下面举个例子:
计算114514/123
代码如下:

;先将114514转到16进制:19842H
mov dx,0001H
mov ax,9852H
mov bx,123
div bx

执行后,ax=007cH,dx=0001H。

mul乘法指令
使用格式与div类似,这里不详细介绍了。说明一下各寄存器的存放内容:
如果是8位乘法,则乘数一个在al寄存器中,另一个在指定的8位寄存器或内存单元中,结果在ax寄存器中。如果是16位乘法,则乘数一个在ax寄存器中,另一个在指定的16位寄存器或内存单元中,结果高16位在dx寄存器中,低16位在ax寄存器中。

结语
本文介绍了汇编语言中存数据的一些方式与格式,希望能帮到读者。我是faryou,下次见!

前言
本来已经编辑好了汇编语言后面的几篇教程,但最终考虑了一下还是决定写一下这篇基础教程,如果读者愿意看我的教程学习的话建议后面几篇反复来回看,有助于理解,看书也是如此~

什么是寄存器
从物理层面上看,寄存器位于CPU中,位数一般与地址总线相同(因为用地址总线在内存和寄存器之间通信)。寄存器的读写速度非常快,用处很多,有些寄存器用来存地址,帮助CPU实现一些功能,有的寄存器则用作循环,如下节课的bx。

什么是内存
不知道大家有没有见过内存条,外观上看,内存条是一块薄片,而实际上我们从物理的角度看内存条,它存储的数据确实是条状分布,即便是你在高级语言编程时创建的数组,在内存上也是“一条”,高级语言中的多维数组并不是多维,只不过是用指针标记的罢了。
内存的读写速度相比寄存器慢些(毕竟寄存器本身就在CPU里面),但相比硬盘这些还是要快不少的。应用程序运行时的数据都放在内存中(包括程序的机器代码数据本身、图片、视频、长文本等静态资源,和一些临时数据(就是高级语言中的变量))。

mov指令
mov指令从CPU的角度看就是用地址总线传输一组数据,有以下六种格式:

mov 寄存器,数据(常数)
mov 寄存器,寄存器
mov 寄存器,内存单元
mov 内存单元,寄存器
mov 段寄存器,寄存器
mov 寄存器,段寄存器

写成C语言方便理解就是这样:

void mov(int *a,int b){
    *a = b;
    return ;
}

使用mov时有三个需要注意的点:

  1. 要向段寄存器中传数据时,必须用寄存器中转。
  2. 要在内存空间之间中转数据,必须用寄存器中转。
  3. mov指令的两个数据必须位数相同

如违反会导致编译错误,因为CPU没有这些功能。

add&sub指令
add指令和sub指令的使用格式类似,其功能是向前一个操作单元加上/减去(add是加,sub是减)。以下是使用格式:

add/sub 寄存器,数据
add/sub 寄存器,寄存器
add/sub 寄存器,内存单元
add/sub 内存单元,寄存器

写成C语言方便理解就是这样:

void add(int *a,int b){
    *a += b;
    return ;
}
void sub(int *a,int b){
    *a -= b;
    return ;
}

需要注意的是,add/sub指令不能对段寄存器操作。

结语
本文介绍了汇编中的一些基础知识,方便后面的学习。我是faryou,下期见!

前言
我们在使用高级语言编程的时候都用过函数(有些语言叫“方法”),函数能够帮助我们将程序分块,实现结构化,从而让代码更为清晰可懂。而在汇编语言中,由于我们是直接对硬件编程,CPU提供了一套指令帮助我们快速实现程序的结构化。

基本思路
首先,我们要回想一下函数中有哪些要素:参数、执行代码和返回值(可看我写的C++函数教程)。高级语言中的数据存在变量中,但是我们使用的汇编并没有这一抽象出来的概念。我们可以使用寄存器或内存单元来存放参数及返回值,至于程序编写,在程序的主体实现部分,我们已经有了相当多的经验。而在调用函数时,应使用CPU提供的call和ret/retf指令。

call和ret/retf
我们先看这两条指令的执行过程。
call 标号 指令执行时,步骤如下:
将IP压栈
转移到标号处(原理同jmp near ptr)
call far ptr 标号指令:
将CS:IP压栈
转移到标号处(原理同jmp far ptr)
call 16位寄存器 指令:
将IP压栈
转移到指定的地址(原理同jmp 16位寄存器)
call word ptr 指令:
将IP入栈
jmp word ptr 内存单元
call dword ptr 指令:
将CS:IP入栈
jmp dword ptr 内存单元
ret指令:将IP出栈(从而实现近转移)
retf指令:将CS:IP出栈(从而实现段间转移)
可以看出,call和ret/retf配合使用,即可完成函数的调用与返回。

代码过程
在执行函数代码时,我们不希望丢失主程序的数据,因此,在使用主程序占用的寄存器之前,应将其先入栈。由此,我们得到了函数的代码过程:
主程序数据入栈
函数实现过程
主程序数据出栈
返回主程序(ret/retf)

总结
本文介绍了在汇编中设计函数的过程,有关实操的内容在后面的文章中会详细介绍。我是faryou,下期见!