faryou 发布的文章

前言&端口简介

(本文有点长,相比之前的内容也更有意思,请耐心食用~)
电脑作为一个由各零件拼凑成的整体,以CPU为核心,管理其他的硬件。端口,就是联系CPU与其他硬件的一个中间规范。各硬件把功能封装好,CPU通过端口访问,实现交互。本文以CMOS RAM为例,介绍端口的使用方法。

in指令和out指令

在8086CPU中,只有in指令和out指令这两条,能够对端口进行操作,其使用格式如下:
in指令(从端口读入数据)

in al,端口号(若为0~255号直接用常数指明,若为256~65535号则先将号码放在dx中,再在此处填写dx);本指令用于访问8位端口
in ax,端口号(同上);本指令用于访问16位端口

out指令(输出数据到端口)

out 端口号(同上),al;本指令用于访问8位端口
out 端口号(同上),ax;本指令用于访问16位端口

需要注意的是,在使用in指令和out指令时,读入和读出的数据都存放在固定寄存器中(8位端口对应al,16位端口对应ax),如果改动会出错。

CMOS RAM简介

CMOS RAM是一种芯片,存放了电脑中的时间信息及其他一些系统配置,其中有一块128字节的RAM可以读取。其只有70h和71h两个端口。我们作为初学者可以用它作为端口学习的一个简单实例,今天我们写一个小程序,用端口读入其时间并显示在屏幕上。下面说明其使用:
70h:地址端口,存放要访问的CMOS RAM单元的地址。
71h:数据端口,存放70h端口中指定地址的数据。
由此可见,我们如果要读取CMOS RAM的x(x为0~255的正整数)号单元,只需要先将x送入端口70h,再从71h读取结果即可。
在CMOS RAM中,秒、分、时、日、月、年信息依次存放在0、2、4、7、8、9单元中。用BCD码的形式存放,即用8位二进制分割为两个4位,高4位存十位,低4位存个位。例如,00010100b表示14。

如何输出内容到屏幕上

今天我们即将写出第一个输出结果到屏幕上的程序~下面介绍显示缓冲区:
在80*25彩色字符模式下,内存中B8000h~BFFFFh为显示缓冲区,分为8页,通常情况下显示第0页,即B8000h~B8F9Fh中的字符,显示时会将显示缓冲区中的内容转为ASCLL码后输出。
一个字符的信息存在一个字中,其中前一个字节放字符,后一个字节放属性
下面为一个属性字节所包含的信息:

位数(二进制位)76543210
含义闪烁背景背景背景高亮前景前景前景
对应颜色 绿

例如:
红底绿字:01000010B 红底闪烁黄字:11000110B
紫底黑字:01010000B 蓝底高亮白字:01001111B
以此类推,可以根据光学三原色互相合成调出8种颜色。

移位指令

shl和shr为逻辑移位指令,其中shl为左移,shr为右移。功能类似C语言的<<及>>。使用格式:

shl/shr 原数据(存放在的寄存器名),移位量(0~8)。

程序编写

终于介绍完全部的知识了,下面整理一下思路:

  1. 从CMOS RAM中读出一个字节
  2. 将该字节以十进制输出(原数+30h)
  3. 重复以上步骤,并将其格式化为“年/月/日 时:分:秒”的格式(前面日期用红底白字,后面时间用黑底高亮黄字)

以下为代码:

assume cs:code
code segment
start:
      mov bx,0b800h
      mov es,bx
      mov cl,4
      
      mov al,9
      out 70h,al
      in al,71h
      mov ah,al
      shr ah,cl
      and al,00001111b
      add ah,30h
      add al,30h
      mov byte ptr es:[0],ah
      mov byte ptr es:[1],01000111b
      mov byte ptr es:[2],al
      mov byte ptr es:[3],01000111b
      
      mov al,2Fh
      mov byte ptr es:[4],al
      mov byte ptr es:[5],01000111b
      
      mov al,8
      out 70h,al
      in al,71h
      mov ah,al
      shr ah,cl
      and al,00001111b
      add ah,30h
      add al,30h
      mov byte ptr es:[6],ah
      mov byte ptr es:[7],01000111b
      mov byte ptr es:[8],al
      mov byte ptr es:[9],01000111b
      
      mov al,2Fh
      mov byte ptr es:[10],al
      mov byte ptr es:[11],01000111b
      
      mov al,7
      out 70h,al
      in al,71h
      mov ah,al
      shr ah,cl
      and al,00001111b
      add ah,30h
      add al,30h
      mov byte ptr es:[12],ah
      mov byte ptr es:[13],01000111b
      mov byte ptr es:[14],al
      mov byte ptr es:[15],01000111b
      
      mov al,4
      out 70h,al
      in al,71h
      mov ah,al
      shr ah,cl
      and ah,00001111b
      add ah,30h
      add al,30h
      mov byte ptr es:[20],ah
      mov byte ptr es:[21],00001110b
      mov byte ptr es:[22],al
      mov byte ptr es:[23],00001110b
      
      mov al,3Ah
      mov byte ptr es:[24],al
      mov byte ptr es:[25],00001110b
      
      mov al,2
      out 70h,al
      in al,71h
      mov ah,al
      shr ah,4
      and 00001111b
      add ah,30h
      add al,30h
      mov byte ptr es:[26],ah
      mov byte ptr es:[27],00001110b
      mov byte ptr es:[28],al
      mov byte ptr es:[29],00001110b
      
      mov al,3Ah
      mov byte ptr es:[30],al
      mov byte ptr es:[31],00001110b
      
      mov al,0
      out 70h,al
      in al,71h
      mov ah,al
      shr ah,cl
      and ah,00001111b
      add ah,30h
      add al,30h
      mov byte ptr es:[32],ah
      mov byte ptr es:[33],00001110b
      mov byte ptr es:[34],al
      mov byte ptr es:[35],00001110b
      
      mov ax,4c00h
      int 21h

code ends
end start

(没有用循环,导致出现了挺多冗余代码的,各位可以试试用循环+子程序简化程序~)

前言

在8086CPU中,有一个特殊的寄存器,用来存放各种指令产生的临时信息,它被称为flag,即标志寄存器。本文将介绍该寄存器的使用,在编程时应灵活使用其特性。

标志寄存器简介

标志寄存器可以视为几个单二进制位的寄存器所拼凑出来的一个寄存器(几个各具自己含义的单二进制位的寄存器挤在同一个寄存器里面),存放一些指令执行时产生的附加信息(如加减法时的进位、借位,溢出等)。在标志寄存器中,每个二进制位代表一个信息。下面将详细介绍各标志寄存器的用途~

标志寄存器组成

标志寄存器由以下各位组成:
  位数  15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0
  表示              OF DF IF TF SF ZF    AF    PF    CF
其中空的位没有实际意义,有意义的位将进行介绍——

标志寄存器使用

DF标志(第10位,方向标志位)与串传送指令

DF标志控制串传送指令每次操作后si、di的增减。若DF值为0,则每次操作后si、di递增;若DF值为1,则每次操作后si、di递减。
串传送指令用于在内存中移动一批数据。其中movsb针对一个字节,movsw针对一个字。movsb的功能如下:

(1)将ds:si指向的内存单元字节送入es:di处。
(2)若DF值为0,则si、di各自增1;
    若DF值为1,则si、di各自减1。

同理,movsw的功能如下:

(1)将ds:si指向的内存单元字送入es:di处。
(2)若DF值为0,则si、di各自增2;
    若DF值为1,则si、di各自减2。

串传送指令一般和rep配合使用,如:

rep movsb

其等价于:

s:movsb
  loop s

易知rep的作用:可以实现(cx)个字节/字的传送。
由此,不难想到,CPU应提供直接更改DF值的方式。事实确实如此。在8086CPU中,我们可以通过cld/std指令将DF的值设置为0/1,格式:

cld/std

ZF标志(第6位,零标志位)

ZF标志用于记录相关指令执行后,其结果是否为0。若结果为0,则ZF值为1;反之则ZF值为0。其中,add、sub、mul、div、inc、or、and等运算指令会影响ZF的值。例如,执行如下指令后,ZF值为1:

mov ax,1
sub ax,1

PF标志(第2位,奇偶标志位)

PF标志用于记录一些指令执行后,其结果的所有单字节位中1的个数是否为偶数。若为偶数,则PF值为1;反之则PF值为0。例如,执行如下指令后,PF值为0:

mov al,1
or al,1

SF标志(第7位,符号标志位)

SF标志记录一些指令执行后,其结果是否为负。若结果为负,则SF值为1;否则SF值为0。需要注意的是,计算机中存放的数字有无符号数和有符号数两种,而一个字节单元存放的数有无符号由编程者意识决定。当你认为运算对象是有符号数时,8086CPU会将运算结果视为补码,并将其转为原码后判断其符号位的正负来更新SF位的值;当你认为运算对象是无符号数时,CPU也会以相同方式更新SF位,但此时符号对你没有意义,所以请无视SF位。例如,执行如下指令后,SF值为0(运算结果换成原码后为0,即非负数):

mov al,10000001b
add al,01111111b

CF标志(第0位,进退位标志位)

CF标志记录add、sub执行后最高位是否产生进、退位。若产生了进、退位,则CF值为1;反之则CF值为0。需要注意的是,inc和loop指令不影响CF位。
abc、sbb指令
abc、sbb指令是带借位加、减法指令。相当于在操作目标上再加、减一个CF的值,例如:

add al,bl
abc ah,bh

以上指令等效于:

add ax,bx

由此可见,我们可以通过abc和sbb指令进行大数据相加。例如,以下代码能计算1EF0001000H+2010001EFH的值,其中高、中、低16位分别存放在ax、bx、cx寄存器中:

mov ax,001EH
mov bx,0F000H
mov cx,1000H
add cx,1EF0H
abc bx,1000H
abc ax,0020H

OF标志(第11位,溢出标志位)

我们在进行有符号数运算时,可能会发生溢出,这会导致运算结果不正确。例如,我们要将两个8位二进制数(范围-128~127)98和99相加,正确结果是197。但是,由于机器无法存下,发生了溢出,我们得到了-59,显然,这与我们的希望不符。
OF标志位就是用于记录溢出的位。如果发生溢出,OF值为1;反之OF值为0。上面的98+99运算后,OF值为1。但需要注意OF和CF的区别。CF针对无符号数,而OF针对有符号数。

cmp指令和检测比较结果的条件跳转指令

cmp指令为比较指令,格式:
cmp 操作对象1,操作对象2
当执行cmp指令时,CPU会对两个操作对象作减法,但不保存在操作对象中,只影响标志寄存器的值。例如,执行如下指令后,ZF值为0,PF值为1,SF值为0,CF值为0,OF值为0:

mov ax,8
mov bx,3
cmp ax,bx

则执行cmp指令后,我们通过标志寄存器的值即可判断两个操作对象的大小。由逻辑推理,我们可以得到:

当ZF值为1时,(ax)=(bx)
当CF值为1时,(ax)<(bx)
当CF值为0且ZF值为0时,(ax)>(bx)

对于有符号数,由于其靠补码表示,故情况较为复杂:

当SF值与OF值不同时,(ax)<(bx)
当SF值与OF值相同时,(ax)≥(bx),然后根据ZF的值判断是否相等

通过上面的讨论,我们知道了不同比较结果下cmp指令的实际意义,下面介绍常用的使用cmp比较结果的方法——检测比较结果的条件转移指令。对于无符号数,其条件转移指令如下:

je/jz:等于(ZF值为1)则转移
jne/jnz:不等于(ZF值为0)则转移
ja/jnbe:高于(CF、ZF值均为0)则转移
jna/jbe:不高于(CF、ZF中至少一个值为1)则转移
jb/jnba:低于(CF值为1)则转移
jnb/jae:不低于(CF值为0)则转移

对于有符号数,其条件转移指令如下:

je/jz:等于(ZF值为1)则转移
jne/jnz:不等于(ZF值为0)则转移
jg/jnle:大于(SF、OF值相等且ZF值为1)则转移
jng/jle:不大于(SF、OF值不等或ZF值为0)则转移
jl/jnge:小于(SF、OF值不等)则转移
jnl/jge:不小于(SF、OF值相等)则转移

如果只想要用其中一个标志位,则可以使用下面针对某一个位的指令:

jc/jo/jp/js:CF/OF/PF/SF值为1则转移
jnc/jno/jnp/jns:CF/OF/PF/SF值为0则转移

以ja为例,以上全部转移指令的格式:

ja 标号

将ja替换为其他指令也可。注意,以上条件转移指令均为短转移。

AF标志(第四位,辅助进退位标志位)

AF标志位用于BCD码运算相关操作。BCD码是一种用一组四个二进制位表示十进制数的编码。若两数相加,其中低四位的最高位有进位,则AF值为1,反之AF值为0。

直接获得标志寄存器中的数据

8086CPU支持直接访问标志寄存器的数据。pushf能够将标志寄存器压栈,popf刚能够从栈中弹出数据到标志寄存器中,格式:

pushf/popf

同时,还有一个lahf指令,能够将标志寄存器的低8位送入ah寄存器。格式:

lahf

在得到标志寄存器中的数据后,我们即可通过位运算指令,建立值-指令映射关系,同样可以实现类似条件转移指令的功能(高级语言中if语句的底层逻辑就是映射)。

前言

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

栈是什么

数据结构上理解栈,我想不必过多解释了,本质就是一块具有特定进出规则的内存空间,在这块空间内遵循数据先进后出的原则。
但我们作为汇编学习者,仅有这些理解是不够的,让我们从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

前言

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

段是什么

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

如何使用段

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

段名 segment
段内代码
段名 ends

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

mov 寄存器,段名

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

前言

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

操作符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寄存器中。