写个dos弹球小动画

前段时间把汇编复习了一遍,心里感觉踏实多了。。。最近一直在看数电和数据结构,结果好像用眼过度了,老毛病又犯了,只能连着休息了两天,没敢用电脑。今天刚打开计算机也不知道干什么,就写个简陋的dos动画吧,纯属练练手。
一个碰到四壁会反弹的小球,纯dos下可以看到闪烁的效果,v86自然就木有闪烁了。其实进一步编写应该可以写出一个小游戏来,不(但)过(是)考(我)虑(是)的(个)东(水)西(平)就(很)比(次)较(的)多(渣)了(啊)。
下面是源码:

assume cs:code
data segment
data ends
code segment
start:
	mov ax,data
	mov ds,ax
	mov ax,0
	mov bx,1
	mov cx,1
	mov dh,5
	mov dl,12
	
	call boder
	
game:
	call display
	call delay
	call clear
	call cmprc
	
	add dh,bl
	add dl,cl
	jmp game
	
	mov ax,4c00h
	int 21h

cmprc:                                      ;判断小球是否到达边界
	push ax
	cmp dh,1
	jz s1
	cmp dh,23
	jz s1
	jmp k1
s1:	mov al,0
	sub al,bl
	mov bl,al
	
k1:	cmp dl,1
	jz s2
	cmp dl,78
	jz s2
	jmp k2
s2:	mov al,0
	sub al,cl
	mov cl,al
k2:	pop ax
	ret
display:
	push ax
	push bx
	push cx
	
	mov ah,2
	mov bh,0
	int 10h			
	mov ah,9
	mov al,2
	mov bl,84h
	mov bh,0
	mov cx,1
	int 10h

	pop cx
	pop bx
	pop ax
	ret
clear:
	push ax
	push bx
	push cx
			
	mov ah,9
	mov al,' '
	mov bl,7
	mov bh,0
	mov cx,1
	int 10h

	pop cx
	pop bx
	pop ax
	ret
delay:
	push ax
	push dx
	mov dx,500h
	mov ax,0
s:	sub ax,1
	sbb dx,0
	cmp ax,0
	jne s
	cmp dx,0
	jne s
	pop dx
	pop ax
	ret
boder:                              ;边界绘制
	push ax
	push bx
	push cx
	push dx
	mov dh,0
	mov dl,0
	mov cx,80
up:	
	mov ah,2
	mov bh,0
	int 10h			
	mov ah,9
	mov al,8
	mov bl,2
	mov bh,0
	push cx
	mov cx,1
	int 10h
	pop cx
	inc dl
	loop up
	
	mov dh,24
	mov dl,0
	mov cx,80
down:
	mov ah,2
	mov bh,0
	int 10h			
	mov ah,9
	mov al,8
	mov bl,2
	mov bh,0
	push cx
	mov cx,1
	int 10h
	pop cx
	inc dl
	loop down
	
	mov dh,0
	mov dl,0
	mov cx,25
left:
	mov ah,2
	mov bh,0
	int 10h			
	mov ah,9
	mov al,8
	mov bl,2
	mov bh,0
	push cx
	mov cx,1
	int 10h
	pop cx
	inc dh
	loop left
	
	mov dh,0
	mov dl,79
	mov cx,25
right:	
	mov ah,2
	mov bh,0
	int 10h			
	mov ah,9
	mov al,8
	mov bl,2
	mov bh,0
	push cx
	mov cx,1
	int 10h
	pop cx
	inc dh
	loop right
	
	pop dx
	pop cx
	pop bx
	pop ax
	ret
code ends
end start

下面附上exe程序:
ball
话说MIT的算法导论公开课不错,大二争取要在这方面有所进步啊,算法菜真是道硬伤。。

..2012.9.22
今天在win7下测试了一下这段代码,发现有时在快到边界时(还没到)就可能反弹,不知道什么原因,可能是代码问题,也可能是延时过短问题。。

未启动分页机制的保护模式(1)

这篇笔记写在8086汇编一栏不太合适,但是这和前一篇包括接下来我要写的几篇都是一个系列。放在一起对自己的复习比较方便。。

未启动分页机制的保护模式
1.保护模式下,内存依然分段
2.seg:offset只能访问20位内存,如何访问32位内存?
答:将内存段信息存入一张表中,用16位段寄存器索引这张表,从而找到各个段。

此时的段寄存器:段选择子
表中每个表示32位内存段信息的项:段描述符
整张表:段描述符表
3.段选择子:

描述符索引13位,因此段描述符最大个数为:2^13=8096个
描述符索引定位描述符表中的某个段描述符
TI:用来表示是从全局描述符表中读取描述符还是从局部描述符表中读取
RPL:特权检查
为什么有TI位?
答:有些段是进程私有的,段信息放在局部描述符表中,如进程的数据段
有些段是公有的,段信息放在全局描述符表中,如系统库函数所在段
4.物理地址的形成(再次声明,未开启分页机制)

5.段描述符:
(1)包含段的开始地址和段的界限(长度)
(2)8个字节
(3)段描述符结构:

段界限(segment limit):共20位,被分成两部分。第一部分保存在第1、2字节。第二部分保存在第7字节的低4位。
段基地址:共32位,也分成两部分,如图。
段属性:指明段是否可写,是代码段还是数据段等。。
具体见下图:

关于DT位:系统段:系统程序的段。存储段:用户程序的段

关于DPL位:访问该段需要什么特权级。

注意:段界限只有20位,段大小岂不是最大为1M?
答:事实上limit部分的20位并非直接填入段大小,而是按段界限公式转化后的值。
段界限公式:
段界限=limit*4k+0fffh
即:limit=(段界限-0fffh)/4k

》》》》》》》》》》》》》》》》》华丽的分割线》》》》》》》》》》》》》》》

实例:如果段基地址是0,大小为8M。如何填写段描述符

下面是相应汇编代码:

引导程序编写小记

1.引导程序:
bios将启动磁盘第一扇区(512Byte)载入内存0000:7c00处。如果第一扇区最后一个字是dw 0aa55h,那么它就是引导程序
2.nasm的语法:
(1)$:编译后的当前行地址
(2)$$:编译后一个节的开始处的地址
(3)任何不被[]括起来的标签或变量名都被认为是地址。没有offset这个关键词
(4)org 07c00h 告诉编译器程序的开始地址是07c00h
(5)times 字节数 db 0 从该行开始,将指定长度空间填0
(6)小技巧:jmp $ 死循环
3.用nasm编译:nasm *.asm -o *.bin
4.反编译:ndisasm *.bin
5.开机时实模式下的1M内存:

因为试了很久都没能给centos纯字符界面装上vbox的增强包,自然也就不能设置共享文件夹,所以果断放弃centos,改用win下的nasm。。。
步骤:
1.写好引导程序
2.用nasm编译
3.用winimage新建个虚拟软盘文件
4.写个c程序copy_image,将写好的引导程序copy到空白软盘镜像boot.vfd
5.vpc挂载boot.vfd,启动
下面先给出int 10h中断,13h号功能:
功能:在Teletype模式下显示字符串
入口参数:AH=13H
BH=页码
BL=属性(若AL=00H或01H)
CX=显示字符串长度
(DH、DL)=坐标(行、列)
ES:BP=显示字符串的地址 AL= 显示输出方式
0—字符串中只含显示字符,其显示属性在BL中。显示后,光标位置不变
1—字符串中只含显示字符,其显示属性在BL中。显示后,光标位置改变
2—字符串中含显示字符和显示属性。显示后,光标位置不变
3—字符串中含显示字符和显示属性。显示后,光标位置改变
出口参数: 无
下面给出这个可以引导的程序:

org 07c00h
	mov ax,cs
	mov ds,ax
	mov es,ax
	call display
	jmp $

display:
	mov ax,msg
	mov bp,ax
	mov cx,12
	mov dh,12
	mov dl,36
	mov bh,0
	mov al,1
	mov bl,0ch
	mov ah,13h
	int 10h
	ret
msg: db 'hello my os!'
	times 510-($-$$) db 0
dw 0aa55h

这个程序很简单,懂汇编都明白,就不再注释。
下面给出用于将上述编译出来的boot.bin拷贝到boot.vfd的c程序

#include<stdio.h>
#include<string.h>
#include<fcntl.h>
#include<sys/types.h>
#include<sys/stat.h>
int main()
{
    int fd_source;
    int fd_dest;
    int read_count=0;
    char buffer[512]={0};
    fd_source=open("boot.bin",O_RDONLY);
    if(fd_source<0)
    {
        perror("open boot.bin error:");
        return 0;
    }
    fd_dest=open("boot.vfd",O_WRONLY);
    while((read_count=read(fd_source,buffer,512))>0)
    {
        write(fd_dest,buffer,read_count);
        memset(buffer,0,512);
    }
    printf("write image ok!\n");
    return 0;
}

完成后可用Uedit打开boot.vfd验证是否写入成功:

ok,写入成功。
下面附上清晰美图。。。:

(二叉)树笔记

1.二叉树分类:
(1)一般二叉树
(2)满二叉树:在不增加树的层数的前提下不能再添加一个节点的二叉树
完全二叉树:如果只是删除满二叉树最底层最右边的连续若干个节点,这样形成的树是完全二叉树。
完全二叉树包括满二叉树
2.二叉树的存储:
(1)连续存储
必须转化为完全二叉树,否则无法还原原来的树
缺点:耗内存
优点:可以快速找到指定编号节点的父节点和子节点
(2)链式存储
3,一般树的存储:
(1)双亲表示法:求父节点方便
(2)孩子表示法:求子节点方便
(3)双亲孩子表示法:求父节点和子节点都比较方便
(4)二叉树表示法:
一般树转成二叉树方法:
<1>使每个节点的左指针指向它的第一个孩子
<2>右指针指向它的右边相邻的兄弟
4.森林的存储:先转化为二叉树再存储
转化方法同上(树的根节点之间看成兄弟关系)
5.二叉树的操作:
遍历:
(1)前序遍历
先访问根节点
再先序遍历左子树
再先序遍历右子树

快速求先序遍历:用笔从根节点开始从左侧开始绕二叉树外围画一圈,按笔到达的顺序写出节点,即为先序序列
(2)中序遍历
中序遍历左子树
访问根节点
中序遍历右子树
(3)后序遍历
后序遍历左子树
后序遍历右子树
访问根节点
已知两种遍历序列,求原始二叉树:
已知先序遍历和中序遍历 或 已知中序或后序遍历 可以求出原始二叉树
已知先序和后序则无法推原始二叉树
6.已知先序和中序,求后序:
以中序为主体,在先序中最先出现的是根节点,然后在中序中分清了左右
对于左右部分,同样应用上述方法(在先序序列中最先出现为根)
7.已知中序和后序,求先序:
基本方法同上,只是在后序序列中,最后出现的是根。
8.二叉树的几个性质:
(1)第i层最多2^(i-1)个节点(i>=1)
(2)深度为k的二叉树最多有(2^k)-1个节点(k>=1)
(3)n0=n2+1 (n0是度为0的节点个数,n2是度为2的节点个数)
完全二叉树的性质
(4)

(5)


2)如果2i>n,则节点i无左孩子,i为叶子节点。否则其Lchild(i)=2i
3)如果2i+1>n,则节点i无右孩子,否则Rchild(i)=2i+1

9.最简单的二叉树及遍历

#include<stdio.h>
#include<malloc.h>
typedef struct BTnode{
	char data;
	struct BTnode*pLchild;
	struct BTnode*pRchild;
}BTNODE,*PBTNODE;
PBTNODE CreateBTree();
void pretraverse(PBTNODE);
void intraverse(PBTNODE);
void posttraverse(PBTNODE);
int main()
{
	PBTNODE pRoot=CreateBTree();
	posttraverse(pRoot);
	return 0;
}
PBTNODE CreateBTree()
{
	PBTNODE pa=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE pb=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE pc=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE pd=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE pe=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE pf=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE pg=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE ph=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE pi=(PBTNODE)malloc(sizeof(BTNODE));
	PBTNODE pj=(PBTNODE)malloc(sizeof(BTNODE));
	pa->data='A';
	pb->data='B';
	pc->data='C';
	pd->data='D';
	pe->data='E';
	pf->data='F';
	pg->data='G';
	ph->data='H';
	pi->data='I';
	pj->data='J';
	pa->pLchild=pb;
	pa->pRchild=pc;
	pb->pLchild=pd;
	pb->pRchild=NULL;
	pc->pLchild=ph;
	pc->pRchild=pi;
	pd->pLchild=NULL;
	pd->pRchild=pe;
	pe->pLchild=pf;
	pe->pRchild=pg;
	pf->pLchild=pf->pRchild=pg->pLchild=pg->pRchild=NULL;
	ph->pLchild=ph->pRchild=NULL;
	pi->pLchild=pj;
	pi->pRchild=NULL;
	pj->pLchild=pj->pRchild=NULL;
	return pa;
}
void pretraverse(PBTNODE pt)
{
	printf("%c\n",pt->data);
	if(pt->pLchild!=NULL)
		pretraverse(pt->pLchild);
	if(pt->pRchild!=NULL)
		pretraverse(pt->pRchild);
	return;
}
void intraverse(PBTNODE pt)
{
	if(pt->pLchild!=NULL)
		intraverse(pt->pLchild);
	printf("%c\n",pt->data);
	if(pt->pRchild!=NULL)
		intraverse(pt->pRchild);
	return;
}
void posttraverse(PBTNODE pt)
{
	if(pt->pLchild!=NULL)
		posttraverse(pt->pLchild);
	if(pt->pRchild!=NULL)
		posttraverse(pt->pRchild);
	printf("%c\n",pt->data);
	return;
}

关于汉若塔问题

这是一道非常经典的题,我想只要是将C语言的书一定都会写到递归,而谈到递归,汉若塔问题就不可能不出现。
记得第一次学C语言时看到它的解法,感觉很难,后来渐渐才理解,但总会突然又忘记,总之看着很别扭。于是这回再来看看这道经典的题。
首先是建模,n个盘子从1到n编号,最下面是n,最上面是1,初始都在A柱子上,目的是通过B柱子全部移到C柱子上。当然有一定限制,游戏规则大家应当都知道,不在累述。
下面是程序:

#include<stdio.h>
int han(int n,char x,char y,char z)
{
	if(n==1)
	{
		printf("把编号为%d的盘子从%c移到%c\n",n,x,z);//问题规模为1时终止递归
	}
	else                                                  //否则分三步走
	{
		han(n-1,x,z,y);                               //递归,子问题是同样的问题。
		printf("把编号为%d的盘子从%c移到%c\n",n,x,z);
		han(n-1,y,x,z);
	}
	return 0;
}
int main()
{
	int n;
	printf("请输入盘子数:");
	scanf("%d",&n);
	han(n,'A','B','C');
	return 0;
}

事实上可以发现,n个盘子需要移(2^n)-1次

静态队列的实现

1.静态队列一般必须是循环的。显然
2.循环队列需要两个参数确定。front、rear
3.队首删除元素,队尾泽插入元素。
4.两个参数在不同场合的不同含义:
(1)队列初始化:front=rear=0
(2)队列非空:front指向第一个元素,rear指向最后一个元素的下一个元素
(3)队列空:front=rear
5.出队伪算法:
a[rear]=val
rear=(rear+1)%数组长度
6.入队伪算法:
front=(front+1)%数组长度
7.如何判满:
少用一个元素法:
(rear+1)%数组长度=front
8.下面是具体的代码,其实是比较简单的:

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>
#define MAXQSIZE 100
typedef struct Queue{
	int *pBase;
	int front;
	int rear;
}QUEUE;
void init(QUEUE*);
bool empty(QUEUE*);
bool full(QUEUE*);
void en(QUEUE*,int val);
int out(QUEUE*);
void traverse(QUEUE*);
int QueueLength(QUEUE*);
int main()
{
	QUEUE q;
	init(&q);
	traverse(&q);
	en(&q,2);
	en(&q,5);
	en(&q,100);
	traverse(&q);
	out(&q);
	traverse(&q);
	system("pause");
	return 0;
}
void init(QUEUE*q)
{
	q->pBase=(int *)malloc(sizeof(int)*MAXQSIZE);
	q->front=0;
	q->rear=0;
	return;
}
bool empty(QUEUE*q)
{
	if(q->front==q->rear)
		return true;
	else
		return false;
}
bool full(QUEUE*q)
{
	if((q->rear+1)%MAXQSIZE==q->front)
		return true;
	else
		return false;
}
void traverse(QUEUE*q)
{
	int i=q->front;
	if(empty(q))
		return;
	while(i!=q->rear)
	{
	printf("%d ",q->pBase[i]);
	i=(i+1)%MAXQSIZE;
	}
	printf("\n");
	return;
}
void en(QUEUE*q,int val)
{
	if(full(q))
	return;
	q->pBase[q->rear]=val;
	q->rear=(q->rear+1)%MAXQSIZE;
	return;
}
int out(QUEUE*q)
{
	int val;
	if(empty(q))
		return -1;
	val=q->pBase[q->front];
	q->front=(q->front+1)%MAXQSIZE;
	return val;
}
int QueueLength(Queue*q)
{
	return (q->rear-q->front+MAXQSIZE)%MAXQSIZE;
}

一道有趣的题,关于宏

今天看到一道题目,感觉写不出程序来,于是看了答案,看到答案后先是不解,感觉答案不正确,结果发现是自己把变量的&操作跟寄存器的and操作混淆了,做了个小实验才弄清楚。c语言里面变量&操作不改变原变量,只改变赋值号(=)左边的值。。。囧~
之后理解了答案,然后就彻底震惊了。。。
原来程序竟然可以这么写!!这道题目改变了我对宏的看法。。
题目是这样的:
假设中国象棋棋盘上只有一个将和一个帅,显然,两者不能出现在同一纵列上。题目要求求出将和帅的所有可能位置组合。
只能使用一个变量。是的,只能用一个变量,对我来说有点难。想了一会没感觉。就看了答案,答案用1到9表示棋子可以移动的九个格子。如下

这样设计使判断是否在同一纵列简单化了,只需取模判断即可。关键是如何用一个变量构造两层循环和两个表示量(明显需要表示两个棋子的位置)
看了答案,总算自己也写出了程序,不过我感觉书上的答案不必要(针对这道题目),所以简化了宏,当然也降低代码的通用性。。
结果没想到这么一个小改动,让我足足调试了30分钟。。
下面给出个人修改的代码:

#include "stdio.h"
#define SETL(b,n) (b=((b & 0x0f)^(n << 4)))
#define SETR(b,n) (b=((b & 0xf0) ^ n))
#define GETL(b) ((b & 0xf0) >> 4)
#define GETR(b) (b & 0x0f)
int main()
{
	unsigned char b;
	for (SETL(b,1);GETL(b)<=9;SETL(b,GETL(b)+1))
		for(SETR(b,1);GETR(b)<=9;SETR(b,GETR(b)+1))
		{
			if(GETL(b)%3==GETR(b)%3)
				continue;
			printf("A=%d,B=%d\n",GETL(b),GETR(b));
		}
	return 0;
}

要是这样也就算了,更震惊的是这不是最优解。。。

#include"stdio.h"
int  main()
{
	char i=81;
	while(i--)
	{
		if(i/9%3==i%9%3)
		continue;
		printf("A=%d,B=%d\n",i/9+1,i%9+1);
	}
	return 0;
}
}

这段代码更加巧妙,用商和余数分别表示两个变量。。比前一个解更加简洁。事实上书上给出了第三个答案,效率更高,但我怎么看都认为用到了两个变量,不符题意,所以我就不贴了。。

386汇编基础知识笔记

寄存器
1.8个通用寄存器都可用于基址变址寻址,当做指针寄存器使用,这是不同于8086的(8086只能是bx、si、di、bp)
2.六个段寄存器:cs、ds、ss、es、fs、gs
3.32位标志寄存器eflags
新增4个标志:
(1)io特权标志iopl:2位宽,指定了执行i/o指令所需的特权级,如果当前特权级小于等于该值,则i/o=指令可被执行,否则引发保护异常。
(2)位14。嵌套任务标志nt:控制iret的执行
nt=0,常规返回(pop eflags 、pop eip、pop cs)
nt=1,通过任务转换实现中断返回(不太理解。。。)
(3)位16。重启动标志rf:控制是否接受调试故障。
rf=0    接受
rf=1    忽略
成功执行每一条指令后,cpu对rf位清0.接收到一个非调试性故障时置1.
(4)位17。虚拟8086方式标志vm:
vm=1:在虚拟8086方式下工作
vm=0:在保护模式下工作

存储器寻址:
1.保护模式下,段寄存器存储段选择子,间接指示段基地址
2.默认段寄存器的选择只受基址寄存器影响。
3.如果某一存储器操作数的地址是该操作数尺寸(所占字节数)的倍数,那么称该操作数对齐。访问对齐的数较快。

386指令集:
1.数据传送指令:
(1)通用传送指令:mov movsx movzx xchg push pusha pushad pop popa popad
movsx和movzx用法:
movsx dst,src
dst必须为reg16、reg32,src可以reg8、reg16,也可以是存储器单元
push指令与8086的不同点:可以push立即数,而且支持push 32位值
80386push sp或esp时:将push前的sp或esp值压栈。
pushad:将8个32位通用寄存器压入堆栈
顺序:eax ecx edx ebx esp ebp esi edi
然后esp减32,所以压入的esp值是pushad之前的esp值
popad:与pushad相反,但esp的恢复是通过加32实现,不是弹栈实现
(2)地址传送指令:
lea妙用:可以用lea做计算
lea eax,[ecx+esi*4+1234h]    lea的作用是将内存操作数的地址传入寄存器,在这里[]中是操作数地址,该指令事实上计算了ecx+esi*4+1234h,并存入eax。
装入指针指令:lds les lfs lgs lss
用法:lds reg,oprd
(3)标志传送指令:
pushfd    popfd    push和pop32位eflags
(4)累加器专用传送指令:in out xlat
in和out和8086一样,但可以输入输出一个双字
xlat    表转换指令,未学,,有待查资料了解用法。。。。
2.算术运算指令:
(1)加减指令:neg未学,有待查询。。
(2)乘除指令:mul div imul idiv
与8086相同,只是操作数位数相应变大
imul指令特殊用法:
imul dst,src
imul dst,src1,src2
格式1:两者相乘存入dst。格式2:src1和src2相乘存入dst
dst:reg16或reg32.
格式1:src长度必须和dst长度相同(8位立即数除外),可以是寄存器、存储器单元、立即数
格式2:src1只能是寄存器或存储单元,并且长度必须和dst长度相同,src2只能是立即数。
(3)符号扩展指令:cbw cwd cwde cdq
(4)十进制调整指令:daa das aaa aas aam aad
3.逻辑运算和移位指令:
(1)逻辑运算指令:not and or xor test
(2)一般移位指令:
算术移位:sal sar
逻辑移位:shl shr
sal和shl相同
和8086相比,移位位数可以是8位立即数。80386移位位数实际是移位位数的低5位。即范围:0——31
(3)循环移位指令:rol ror rcl rcr
用到再说。。。
(4)双精度移位指令:
用到再说。。。
4.控制转移指令:
(1)转移指令组:
jecxz    仍用1字节表示地址差值
(2)循环指令组:loop loopz loopnz loope
用ecx做计数器
(3)过程调用和返回指令
(4)中断调用和返回指令
5.串操作指令:
(1)基本串操作:lodsd stosd movsd scansd cmpsd
(2)rep repz/repe repnz/repne
(3)串输入:
insb    输入字节
insw    输入字
insd    输入双字
串输入指令从由dx给出端口地址的端口读入一字符,并送入es:edi所指的目的串中,同时根据df和字符类型调整edi。
一般与rep连用,实现连续输入,但必须主意端口的数据准备情况。
(4)串输出:
outsb
outsw
outsd
从ds:esi所指源串读取一个字符,输出到由dx给出端口地址的端口,同时根据df和字符类型调整esi。
6.高级语言支持指令:bound enter leave
enter cnt1,0相当于:
push ebp
mov ebp.esp
sub esp,cnt1
leave则相反
bound    检查数组下标是否越界,越界则产生5号中断。
bound    oprd1,oprd2
oprd1为reg32,oprd2为64位存储器单元,低dd是起始下标,高dd是结尾下标
下面是示例:
num=100
array db num dup (0)
stv dd 0
edv dd num-1
……
bound si,fword ptr stv    ;si中是下标,如果si不在范围中,则引发int 5
mov al,array[si]
7.条件字节设置指令
SETcc oprd
oprd只能是reg8或内存字节单元
根据测试条件,置oprd为1
8.位操作指令
(1)位测试与设置指令
bt    位测试
btc    位测试并取反
btr    位测试并复位
bts    位测试并复位
格式:
xxx oprd1,oprd2
oprd1:reg32或内存双字数据,要测试的数据
oprd2:8位立即数或者与oprd1长度相等的寄存器,用于指定要测试的位(具体规则见书)
后3个指令都是将要测试的位送cf,然后设置标志寄存器相关位
感觉这个指令没有太大用处,,要测试的位是哪一位的寻找规则比较繁琐。
(2)位扫描指令:bsf bsr
不解释。。。
8.处理器控制指令
(1)设置标志指令组
(2)空操作指令:nop:占一个字节
(3)外同步指令和前缀
wait
等待直到busy引脚为高(怎么感觉像单片机。。呵呵,不过本来就是个高级的单片机)
busy引脚由数值协处理器控制。所以该指令实际上是等待协处理器以便与它同步。
封锁前缀lock
锁定目的操作数所在内存单元
可用lock前缀的指令如下:
xchg
add,adc,sub,sbb,neg,inc,dec
and,or,not,xor
bt,bts,btr,btc

递归与可重入子程序

递归:
示例:计算阶乘(n<=8)
入口参数:ax=n
出口参数:ax=n!
fact proc
push dx ;每次都要保存dx的值,即n的值,否则在返回上一层时,dx的值错误
mov dx,ax
cmp ax,0
jz ok
dec ax ;n=n-1
call fact ;计算(n-1)!
mul dx ;n*(n-1)!=n!
pop dx
ret
ok: mov ax,1
ret
fact endp

可重入子程序:
如果子程序在执行到一半的时候,发生中断,而中断处理中又调用了该子程序,则执行完中断再继续执行原子程序,入口或出口参数很可能已被破坏

考虑以下子程序可以更好的理解

如果在(1)、(2)执行之前发生中断,并且中断处理过程调用该子程序,则很明显入口参数被破坏

如果(8)执行后中断,中断处理中调用该子程序,则出口参数result被破坏

解决方法:使用堆栈

如以下程序:

实现对字符串字符个数的计数,字符串以0结尾

计数变量在堆栈中,这时再来考虑重入的情况,很明显无论在哪发生中断(会调用该函数的中断),都不会破坏原执行过程,中断中对该函数的调用引发了再次压栈。计数变量会有多个副本,它们在“不同层次的栈”中(这个。。自创的名词,,易于理解就行)他们互不影响。

字符串操作指令相关笔记

源串:ds:si
目标串:es:di
1.    lodsb    将源串中的一个字符装入al中,同时根据df位si加1或减1
相当于
df=0时
mov al,[si]
inc si

lodsw 类似lodsb,只是传送一个字。

2.    stosb    将al中的值传入目标串中,并根据df为di加1或减1
相当于
df=0时
mov es:[di],al
inc di
stosw    类似stosb,只是传送一个字

3.    scasb    将al内容与di指向的字节数据相减方式比较(只影响标志位,不影响操作数),然后根据df位di加1或减1
scasw    类似

4.    cmpsb    将si指向字节数据与di指向字节数据用相减方式比较(只影响标志位,不影响操作数),然后根据df位si和di加1或减1
cmpsw    类似

其他指令movsb等见汇编入门笔记。

下面写个示例:

assume cs:code,ds:data
data segment
str db 'm& na%e is sh#dow',0
data ends
code segment
start:
	mov ax,data
	mov ds,ax
	mov es,ax
	xor si
	xor di
	cld
	
s1:	lodsb
	cmp al,0
	jz s3
	cmp al,'a'
	jb s2
	cmp al,'z'
	ja s2
	and al,11011111b
s2:	stosb
	jmp short s1
	
s3:	mov ax,4c00h
	int 21h
code ends
end start

键盘输入与显示笔记

关于直接定址表部分事实上能做的笔记较少,只能多看书,多写代码巩固用法。就不写笔记了。
1.cpu执行完int9之后,键盘输入放到了键盘缓冲区中。
2.键盘缓冲区有16个字单元,可以存储15个按键的扫描码和对应的ascii码
3.只有通码及相应ascii码会放入键盘缓冲区,断码不会。
4.一个按键占一个字,高位字节放扫描码,低位字节放ascii码
5.控制键(shift、ctrl等)不会放入键盘缓冲区,但会改变状态字节的对应位(具体见上上篇笔记)
6.int 16h
ah=0
从键盘缓冲区读取一个键盘输入
返回:ah=扫描码,al=ascii码
7.调用int 16h的0号功能后,已读取的键盘输入从缓冲区删除。
8.如果键盘缓冲区为空,则调用int 16h的0号功能会循环等待,直到有数据。
9.字符串的输入显示、删除、回车结束等是利用栈来实现的。
最近心情有些沮丧,没有写代码的欲望了,下次再写相关程序吧。
王爽的汇编入门书到这里终于算是复习完毕了。。接下来就是大量练习巩固了。

外中断笔记2

重写int9中断
目标:按下F1键改变屏幕颜色
这个程序没有运行过,因为要访问真实的硬件,必须在纯dos下运行,而我试了好久发现把exe文件加入dos7.1的虚拟磁盘文件中还是无法访问。。
这种改写中断的方式让我想到了windows编程中的api hook,api hook其中一种实现方式就是改掉api的开头几个字节,实现跳转。很像,有木有。。

assume cs:code
code segment
start:
	mov ax,0
	mov ds,ax
	push ds:[9*4]
	pop ds:[200h]
	push ds:[9*4+2]
	pop ds:[202h]
	cli
	mov word ptr ds:[9*4],204h
	mov word ptr ds:[9*4+2],0
	sti
	mov ax,cs
	mov ds,ax
	mov ax,0
	mov es,ax
	mov si,offset int9start
	mov di,204h
	mov cx,offset endint9-offset int9start
	cld
	rep movsb
	
	mov ax,4c00h
	int 21h
	
int9start:
	push ax
	push bx
	push cx
	push es
	in al,60h
	mov ax,0
	mov es,ax
	call dword ptr es:[200h]
	
	cmp al,3bh
	jne int9end
	mov ax,0b800h
	mov es,ax
	mov bx,00010000b
	mov cx,2000
s:	inc byte ptr es:[bx]
	add bx,2
	loop s
	
int9end:
	pop es
	pop cx
	pop bx
	pop ax
	iret
endint9:nop
code ends
end start

给blog搬个新家

昨天在host2ez买了虚拟主机,给自己的blog安了个新家,感觉还不错。虽然忙到很晚,但基本搞定了,心情还可以。

虽然host2ez的空间速度不怎么样,但对比国内国外其它主机商,最大的特点的是:确实便宜。而且售后服务真的很不错,昨天联系客服,不到5分钟就回复了。感觉花的钱还是值得的。

原先感觉kilu的免费德国主机挺不错,所以也就暂时放弃了付费空间,一段时间用下来,稳定性不错,速度较慢,这些都可以接受,最让我懊恼的是后来居然出现了广告,而且是大幅的浮动广告。。虽然可以去除,但是去除了估计网站也封了。于是在这个暑假决定掏钱了。。域名很悲剧,shadow.com就不用想了,退而求其次shadowxh.com,还好,空着。。

就这样吧,有个地方安家挺不错~

外中断笔记1

1.cpu——端口(外部寄存器)——外设芯片——外设硬件
2.外中断源:
(1)可屏蔽中断:
如果if=1,则响应中断
如果if=0,则不响应中断
与内中断的不同点:中断类型码通过数据线传入
设置if位的指令:sti        cli
(2)不可屏蔽中断:
中断类型码固定为2
与内中断的不同点:不用取中断类型码
3.谁发出外中断的问题:中断由各芯片(主板上的,不是外设硬件中的)发出
4.pc机键盘(个人认为就是矩阵键盘+扫描处理程序,即可以用单片机实现)的处理过程:
(1)键盘中的芯片对每个键的状态进行扫描
(2)按下一个键,开关接通,芯片产生扫描码,扫描码说明按键位置。
(3)扫描码被送入主板相关接口芯片的寄存器中,端口地址:60h
(4)松开按键,也产生扫描码,也送入60h
(5)扫描码长度1字节
通码:按下时的扫描码,第七位为0
断码:松开时的扫描码,第七位为1
断码=通码+80h
(6)int 9
是bios中断
主要工作:
<1>读出60h的扫描码
<2>如果是字符键的扫描码,则将扫描码和相应ascii码送入内存中的bios键盘缓冲区。
如果是控制键,则转换为状态字节,写入内存中0040:17字节单元
*关于键盘缓冲区:可存储15个键盘输入,一个键盘输入用一个字,低位字节ascii码,高位字节扫描码。
*0040:17信息:

5.下面是键盘处理过程的一个感性认识图。。:

6.通过以上知识点的总结,我发现了一个显而易见的结论:键盘先于操作系统启动(处理程序在bios中)。

 

dos绪论

0.msdos三模块:io.sys,msdos.sys,command.com
1.内部命令(30个左右):
常用:
dir
copy
type    显示文本文件
rename
del
date
time
cls
cd
md
用于批处理:
call    调用批文件
echo    命令显示开关
for        循环
goto    转向
if        条件
pause    暂停执行
shift    移动替换参数
内部命令的执行实际上是调用同名子程序。这些子程序位于command.com中。
接收内部命令后,command.com进程查表找到相应子程序的入口。
2.外部命令(50个左右):
format        磁盘格式化
diskcopy    软盘之间的复制
diskcomp    软盘之间的比较
comp        文件之间的比较
chkdsk        检查磁盘
backup        备份磁盘文件
restore        恢复磁盘文件
debug        调试程序
edlin        行编辑程序
edit        文本编辑程序
fdisk        硬盘分区
exe2bin        exe文件转换
外部命令实际上是一个存在于磁盘上的可执行文件。通常把与操作系统有关的磁盘可执行文件作为dos的外部命令。
3.command.com中有一个专门的批处理子程序负责解释执行.bat
4.dos中断服务程序都属于软中断,大部分位于msdos.sys模块中。
5.中断服务程序分两类:
n=21h,系统功能调用
n!=21h,中断调用
6.bios的rom中也有硬件和软件中断程序,即bios中断
7.bat文件:
(1)copy con命令:由键盘直接输入bat文件的各行语句。
如:copy con filedir.bat
ctrl+z结束编辑
(2)rem    注释,会以大写字母显示其后的字母
(3)autoexec.bat文件:dos启动最后阶段,会在引导盘的根目录下寻找并执行autoexec.bat。
(4)prompt    设置系统提示符为当前驱动器符加当前目录名再加上>符号。
(5)path    指出.com文件,.exe文件,.bat文件的查找路径
(6)set    设置一个环境变量
8.dos系统设置:config.sys
(1)功能和autoexec.bat类似,建立方法完全相同
(2)启动过程中由io.sys处理
(3)break=on    表示允许ctrl-c和ctrl-break检查
files=20    表示dos能打开的文件最多有多少,dos系统固定占用5个,20-5=15
buffers=20    设置磁盘缓冲区的数目。磁盘缓冲区用来存放访问磁盘过程中某些数据的副本,节省读盘时间。
(4)用于系统配置的命令分3组
<1>系统参数指派命令
1>break命令
外部设备一般分为字符设备(键盘、屏幕、打印机)和块设备(磁盘),字符设备工作时dos总要检查ctrl-c或ctrl-break是否被按下。而块设备工作时,若break=on,才检查。
2>files命令
打开文件两种方式:1.句柄方式。2.文件控制块方式
该命令指定程序以文件句柄方式总共可以同时打开多少个文件。
files=n
n范围    8——255    缺省8
dos内部,files每加1,系统文件表就增1个表项
3>fcbs命令
指定以文件控制块方式打开文件时的最大数
fcbs=n
n范围    1——255    缺省4
fcb源于CP/M系统,基本弃用。。
4>buffers命令
n范围:1——99    缺省:2——15(由RAM和磁盘类型不同造成)
5>country命令
设定各个国家不同的日期、时间和货币代号的表现格式
默认美国方式
6>lastdrive命令
设置dos承认的最后一个驱动器符
lastdrive=字母
7>drivparm命令:
dos允许带多种类型的块设备。
dos启动时为每个块设备分配一个驱动器符,并设置一个磁盘参数表DPB
若新驱动器规格dos无法识别,需用drivparm命令指明参数,以便建立相应DPB
drivparm=/D:drive [/F:form] [/H:heads] [/S:sectors] [/T:tracks]
drive代表驱动器号,A=0,B=1……。form是驱动器的形状系数(可参见dos手册),heads是磁头数,sectors是每磁道的扇区数,tracks是磁道数。
8>stacks命令:
设置堆栈池中栈区的个数和每个栈区的大小。
堆栈池:dos内部的一个区域,专为接管硬件中断而设置。
硬件中断在中断向量表中的地址指向内存低地址的dos内核。这样dos获得控制权,将ss寄存器指向堆栈池,然后串接到中断处理程序,隔离了应用程序和rom-bios
stacks=n,s
n指定栈个数,8——64,s指定栈的字节数,32——512
286以上机器:n=9,s=128
9>switches命令:
为了使用增强型键盘,书上写的不太理解。。主要是不形象,,~
<2>设备驱动程序安装命令
pc上每一种外设都有一个确定的设备驱动程序。
驱动程序分两类:
一类是标准设备驱动程序:在io.sys中,专门用于控制台(键盘和显示器)、打印机、串行口、实时钟、磁盘等
另一类是可安装设备驱动程序:其它驱动
1>device命令
device=[d:][path]filename[parameters]
filename:要装入的驱动程序名
parameters:传递给驱动程序的一组参数
2>devicehigh命令
作用类似于device,只是它把驱动程序装入上位内存
常规内存:0——640KB
上位内存(umb):640KB——1024KB
高位内存(hma):1024KB——1088KB
延伸内存:1024KB——16MB
<3>其他程序安装命令
专门用于安装那些不具备设备驱动程序标准格式的,并且需要常驻的程序。
1>shell命令
安装命令处理程序(即各种shell)
shell=[d:][path]filename[parameters]
如果安装的是command.com,且其不在系统盘下,则需在这里指出路径。
2>install命令
主要用于安装下面四种外部命令程序:
fastopen.exe    快速打开文件
keyb.com        扩展键盘
nlsfunc.exe        指定国别信息
share.exe        文件共享
<3>dos命令:
把dos的一部分搬到高位内存,从而增加用户使用的空间。
两个参数:第一个为high或low,表示是否将dos的一部分搬到1M地址以上的第1个64KB内存区
第二个参数可为umb或没有参数
(这个命令让我有点紊乱。。。)