长沙建网站企业,网站备案需要收费么,中投建设官方网站,网站开发毕业设计评审表开始#xff1a;
在学习王爽的《汇编语言》的过程中#xff0c;我就真切地体会到编程实践对于理解的帮助。起初我没有安装书中的实验环境#xff0c;看到100页左右就开始感觉无趣、吃力#xff0c;看了后面忘前面#xff0c;差点就要放弃这本书的学习。好在我后来还是装好…开始
在学习王爽的《汇编语言》的过程中我就真切地体会到编程实践对于理解的帮助。起初我没有安装书中的实验环境看到100页左右就开始感觉无趣、吃力看了后面忘前面差点就要放弃这本书的学习。好在我后来还是装好了环境这才开始在实际的编程练习中感受到一些乐趣。
学习完《汇编语言》一书后我又开始阅读朋友给我推荐的《x86汇编语言 从实模式到保护模式》。没错王爽书介绍的只是实模式下编程的内容。读x86一书过程中前面接近于复习读起来还比较轻快但随着渐渐深入我每次对照书阅读代码都感觉很是吃力。到了第15章“程序的动态加载和执行”时已经常常会将保护模式和前面实模式中的内容搞混。
于是我决定自己设计并编写一个汇编程序以进一步理顺清晰前面所学习的内容——本文便缘起于此。需要声明的一点是我并没有打算将这个程序做成一个教程因为那好累我有点懒。前面一小段内容算是我的日记后面则是笔记。
但如果真的有人对我的程序或代码感兴趣我也在后面提供了我的代码仓库链接欢迎大家下载我的代码。然而你想运行它确实会有点麻烦同样的在后面我也大致介绍了程序的运行环境。 Gitee源代码仓库清风莫追/Typing_asm (gitee.com) 文章目录 开始介绍1 程序简述2 涉及知识3 运行环境 程序结构复习知识点1 常数定义2 用户程序中段的重定位3 关于vstart的三种情况4 显存模块5 命令行编译源代码6 中断向量表7 退出VBox对鼠标和键盘的独占8 发布到Gitee 指令记录1 伪指令2 其它 Bug记录1 主引导扇区程序未成功加载用户程序2 安装的int 9中断例程未正常运行3 按一个键后生成字符串的程序的不动了4 按下字母变绿失效当数据段加个字节5 伪随机数循环了6 数字显示不出来7 环绕模块卡死 介绍
1 程序简述
如果只允许用四个字来概括我的程序那我只能对你说打字游戏。可很遗憾今天的我有点话痨请允许我更详细地描述一下这个小游戏
运行程序后首先映入你眼帘的会是一个干净的黑框框。什么你问我怎么不做个哪怕简单点的导引界面其实原因你猜得到懒。那么接着你需要任意地按下一个键这个程序就要正式开始了。
屏幕顶部会开始在随机的位置掉下随机的大写字母而且总会有个字母被一圈环绕着不要犹豫它就是你的目标请按下对应的字母键吧如果你按下了正确的字母它就会变成绿色同时屏幕的左下角会默默地记录着你的得分。描述到这里就结束了毕竟它本就只是一个简单的小程序。
下面是我录制的一段程序运行时的GIF 2 涉及知识
程序看着简单玩起来也简单但做起来可难了而且最难的地方就是我需要使用汇编语言来完成它以至于它的源代码多达500行。关于指令的知识我就不展开说了。除了会使用基本的指令外想要写出本程序你还需要了解以下方面的知识
显存操作。在汇编语言中我们通过直接往显存写入数据来让它显示我们想要的内容。与此同时你还需要了解ascii编码和字符的显示属性。中断。你需要知道如何在系统中安装我们自己编写的中断例程以及键盘扫描码相关的内容。程序的加载。程序编译好后首先是写入到磁盘里的如何将它加载到内存中并运行如果你是编写一个在Dos操作系统上运行的汇编程序其实不需要这一步。但我将程序运行在一个没有操作系统的虚拟机裸机上就需要了。
除了上面三点我还使用“线性同余法”用汇编实现了伪随机数的功能。不然随机的字母、随机的下落位置其中的随机从何而来呢汇编并不像C语言那样有着丰富的库函数。
当然BIOS中也是提供了一些中断例程以供用户调用的我并不知道里面有没有随机数的功能。然而自己尝试实现一个也挺有意思的不是吗这费不了多少功夫——如果你没有花很多时间调试bug的话。
3 运行环境
1、汇编语言
因为与计算机硬件实现的强相关性汇编语言并不像其它高级语言那样标准统一。也就是说存在多种不同的汇编语言它们各自对应着不同的指令系统、体系结构。
好吧其实没必要讨论那么多概念我只是想说本程序使用的是8086汇编语言。更进一步地说我使用了Nasm编译器该编译器是开源的你可以在GitHub上找到它而另一款大家常用的编译器是Masm。
主要是伪指令的不同会导致一些代码框架结构上的差异如果你以前只用过Masm可能还需要去单独了解一些Nasm的内容。
2、虚拟环境
最简单的环境搭建是使用DOSBox它是一个包含Dos操作系统的虚拟机可以运行8086的汇编代码。如果你打算用它的话我的代码需要进行一些修改才能运行。
说实在用DOSBox学习汇编语言挺方便的然而它并没有提供虚拟的磁盘后来我安装了VisualBox和Bochs《…从实模式式到保护模式》的书籍配套资料中有相关的安装教程包括Nasm编译器 http://www.lizhongc.com/ Bochs除了运行程序还能debug而VisualBox只能运行程序效率要高一些。这两个虚拟机都没有操作系统所以你要自己想办法将程序从磁盘加载到内存中来这就是为什么前面“涉及的知识”中会说到程序的加载问题。
我的代码仓库中有两个.asm文件其中程序typing_mbr.asm所做的事情就是把用户程序也就是我的小游戏加载到内存中而typing.asm是真正的游戏程序代码。
- typing.asm
- typing_mbr.asmGitee源代码仓库清风莫追/Typing_asm (gitee.com) 好吧如果只是想用汇编语言编写并运行一个小游戏这确实多走了不少弯路。也许我有空会尝试出个DOSBox中能跑的版本但大概率会鸽因为我懒。 最后还得吐槽一下汇编语言的生产力太低了这个小程序花了我整整16个小时。不过复习的效果感觉也不错只是我也不太确定是否划算。可是自己制作小作品时的那种投入感是平常阅读技术书籍的过程中难以找到的。
接下来的部分是我编写代码过程中的一些笔记就比较无聊了Bug记录部分倒也可以参考一下。但我就不陪大家一起了有缘再见 程序结构
- 主引导程序将用户从磁盘加载到内存中。
- 用户程序- 头部- 程序长度- 程序入口- 段重定位表- 代码段- 数据段主引导程序包含信息 用户程序在磁盘的位置即起始逻辑扇区号。用户程序被加载到内存的物理起始地址。 需要用到的例程 显示 显示一个字母将屏幕一行内容左移一位 键盘中断 获取键盘字符的扫描码 时钟中断 控制字符移动速度作为随机数种子(已鸽目前采用固定种子1) 障碍屏幕移动与响应键盘中断是两个线程吗
答不需要重写键盘中断即可。
复习知识点
1 常数定义
hello equ 100定义一个值为100的常数hello若继续mov ax,hello则执行后ax寄存器的值为100。
在代码中引用常数或标号都是在编译阶段发挥作用。但注意与标号使用时相区分如果是hello db 100然后mov ax,hello则执行后ax的值为标号hello处的汇编地址。
2 用户程序中段的重定位
SECTION header vstart0 ;定义用户程序头部段 program_length dd program_end ;程序总长度[0x00];用户程序入口点code_entry dw start ;偏移地址[0x04]dd section.code_1.start ;段地址[0x06] realloc_tbl_len dw (header_end-code_1_segment)/4 ;段重定位表项个数[0x0a];段重定位表 code_1_segment dd section.code_1.start ;[0x0c]code_2_segment dd section.code_2.start ;[0x10]data_1_segment dd section.data_1.start ;[0x14]data_2_segment dd section.data_2.start ;[0x18]stack_segment dd section.stack.start ;[0x1c]header_end:如果使用主引导扇区加载用户程序则用户程序需要描述头部信息。头部代码示例如上记录的每个段的信息都是汇编地址在主引导扇区程序中会转换为在物理内存中的段地址以便之后用户程序可以通过
mov ax, [cs:data_1_segment]
mov ds, ax这样的方式在内存中正确地访问到对应的段。
3 关于vstart的三种情况
略了
4 显存模块
常用的是显存的文本模式物理地址空间为0xB800~0xBFFFF。可以显示25行每行80个字符使用ASCII码。
5 命令行编译源代码
nasm -f bin exam.asm -o exam.bin-f bin是要求nasm生成“纯二进制”的内容即不包含操作系统所需要的加载和重定位信息。
6 中断向量表
对于8080PC机中断向量表存放在0000:0000~0000:03FF的一共1024个内存单元中每个表项占两个字故最多可以有256个中断例程。表项中是中断处理程序的入口地址其中高地址字存放段地址低地址字存放偏移地址。
注中断号从0开始。
一般0000:0200~0000:02FF这段内存是空闲的操作系统和其他应用程序都不占用。
7 退出VBox对鼠标和键盘的独占
按右边的组合键Alt Ctrl按左边的这两个键没效果。
8 发布到Gitee
如果你使用的是vscode且以前已经配置过与Gitee的ssh公钥连接那么发布代码到远程Gitee仓库的步骤如下
vscode进入“源代码管理”栏初始化仓库并提交所有更改。登录Gitee账号创建一个空的仓库并复制仓库地址。在vscode中“添加远程存储库”然后输入仓库地址以及仓库名。最后发布分支。此时点开Gitee仓库将看到你的代码已经被上传。 指令记录
1 伪指令
section.段名称.start获取段的汇编地址。注意与段地址的概念相区分 段地址 ∗ 16 段地址*16 段地址∗16得到的才是段的汇编地址。
2 其它
jmp far [0x04]从内存中取出两个字低地址的字放入IP寄存器高地址的字放入CS寄存器。
Bug记录
1 主引导扇区程序未成功加载用户程序
在没有操作系统的虚拟机上目前我每次运行都会启动两个程序一个是主引导扇区程序它用于将用户程序从磁盘加载到内存中另一个就是我们用来完成任务的用户程序了。
当在加载阶段就出现问题时我习惯性地就认为是主引导程序的代码出现了问题而实际上加载用户程序的任务是由引导程序与用户程序的头部合作完成的。
举个例子我在用户程序的头部定义了用户程序的长度信息
;头部 用户程序长度
program_length dd program_end ;[0x00];栈段
SECTION stack align16 vstart0 ;...程序结尾
program_end:2 安装的int 9中断例程未正常运行
下面是程序的部分代码片段
int9_new:;说明新的int 9中断例程在屏幕固定位置显示按下的字符push dxpush ax mov al, kSECTION data align16 vstart0text db ABCDEFGHIJKLMNOPQRSTUVWXYZint9_new_store db 512 dup 0 ;存放新的int 9中断例程程序运行后会自动逐个字符得显示text标号处的内容我将mov al,k修改为mov al,e后显示的内容也响应地变化说明新的中断例程代码肯定是被成功写入到了data数据段的。 那么不能正常运行的可能原因如下
中断向量表填写不正确。又分为新中断例程表项和旧int 9中断例程的表项。int 9中断其实不会在按键盘时自动触发
经过两小时的调试都没能发现问题所在已经开始怀疑是不是VisualBox和Bochs不支持键盘中断、或者中断号并不是0x09毕竟现在换成了虚拟的x86处理器而不是之前看王爽书时用的8086处理器了。期间我将王爽书的实验15代码改编后在Bochs上也无法正常运行暂时还不知道原因
但搜索资料发现键盘中断的支持和中断号都应该是没有问题的 一文讲透Windows内核 x86中断机制详解 - 知乎 (zhihu.com) 可后来我发现了一个现象
代码正在Bochs运行时我随便按下一个键程序都会突然终止。而将新中断例程改写成以下样子后它是可以正常运行的按键后屏幕的左上角意料之外地正常出现了小写的字母k。
int9_new:;说明新的int 9中断例程在屏幕固定位置显示按下的字符push ax push esmov ax,0xb800 mov es,ax mov byte es:[0], kpop espop ax iret那么很显然之前的问题应该来自我编写的新中断例程本身。
我之前在中断例程中使用了形如call appear_char的指令而appear_char是我编写在用户程序中的子程序。那么问题来了call 标号指令采用的是相对近调用编译后指令的操作数是call指令相对于标号的偏移量。可是中断程序中cs寄存器的值已经改变哪里还找得到原来的子程序appear_char只能按着原来编译出来的偏移量跳转到一个莫名的位置罢了。
可能的解决方案 将用户程序和中断例程共用的子程序同样安装为中断例程。 在内存中保存用户程序的cs段地址中断例程中使用call far进行间接绝对远调用。 问题子程序采用ret返回只能实现近转移从中断例程中调用之后就回不来了。 将需要用到的子程序源码复制拷贝到中断例程中。有点无脑但看起来很方便 我之前在数据段准备了一段空间用来安装复制中断例程现在想来完全多此一举。我直接将用户程序代码段的int9_new地址填入中断向量表就可以作为中断例程了。 由于和用户程序本来就在同一个段调用其中的子程序肯定也没啥问题。Good idea!
成功解决
3 按一个键后生成字符串的程序的不动了
参考王爽第17章关于键盘缓冲区的介绍。p300
按下字符键会产生键盘中断并将字符的通码和ascii码写入字符缓冲区中而使用int 0x16中断例程可以从键盘缓冲区中取出一个键盘输入。但问题是只有按下按键时会产生int 9中断吗不松开时也会产生一个中断不要忘了每个键都对应这一个通码和一个断码。
于是每按下一个键并松开的过程如下
按键中断将键盘输入写入缓冲区 -- 从缓冲区读取一个键盘输入 -- 松开中断 -- int 0x16因读取不到键盘输入进入等待
经过尝试如果按下一个键之后不松开继续按第二个、第三个键生成字符串的程序不受影响继续运行。
int 128 ;在安装本中断前已将旧int 9的中断号修改为128
mov ah,0
int 0x16 ;(ah)扫描码, (al)ascii码mov dh,24
mov dl,79
call put_char可能的解决方案
自己从端口读取扫描码然后解析为ascii码就不会有等待的过程了。可以采用查表。
成功解决
4 按下字母变绿失效当数据段加个字节
我在记录头字母的标号后添加了一个字节定义random_seed db ...然后前面能跑的按键模块功能突然就寄了。
;...数据段中
head_letter db 0 ;持续记录屏幕上当前最左侧字母的位置(行内偏移)
random_seed db 0x4f ;以此生成下一个随机数每次生成后会更新发现问题在这里
mov bx,[head_letter]
mov es:[0xa0*12 bx 1], ah ;将头字符的属性修改为绿色因为目的操作数是bx因此从有效地址处取出两个字节即将random_seed处的字节放到了bx的高字节bh中。
5 伪随机数循环了 参考随机数大家都会用但是你知道生成随机数的算法吗 - 知乎 (zhihu.com) 起初使用的是平方取中法但总是很容易陷入短循环参数调了几次循环右移多少位后取右8位仍然效果极差。 后面感觉在汇编中观察数据的随机性还是太抽象了。于是我改用了python尝试了线性同余法 x ( a ∗ x b ) % c x(a*xb)\%c x(a∗xb)%c取参数组合为 ( a 217 , b 11 , c 253 ) (a217,b11,c253) (a217,b11,c253) 。生成散点图如下左图横轴是次数纵轴是生成的随机数值。可以很明显地看出周期性周期长度在100左右对于我的小游戏差不多能用了。
将参数c修改为327682的15次方后效果如右图所示。 6 数字显示不出来
明明感觉代码逻辑没啥问题pop dx取得的是待显示的一位数字的值可数字就是显示不出来。
后发现还是字符的值与属性的问题在显存中每个字符占一个字即两个字节第字节是ascii码值高字节是显示属性。代码中mov es:[di], dx直接写入一个字dl 是ascii值没错但dh是零解释成属性就是黑底黑字。
好吧原来不是没显示字符只是显示了而我看不见。
put_one:pop dxadd dx, 0mov es:[di], dxadd di, 2loop put_one7 环绕模块卡死
;2 样式表surround_style显示环绕效果
mov ax, style_end - surround_style
mov bx, 2
div bx
mov ah,0 ;有点多余但比较保险
mov cx,ax 在Bochs中调试时到div bx这一条指令就会一直卡在这里不知道是什么情况。 当我将bx修改为bl后程序可以顺畅地运行下去了。那么应该是因为dx不为0 ( d x , a x ) / b x (dx,ax)/bx (dx,ax)/bx中 bx 值太小2发生了除法溢出。很好这个问题算是解决了但又出现了一个新的问题
kkk kk
k k -- k k
kkk kkkk也不知道这个k是怎么独自跑到下面去的。 surround_style:db -1,-1, -1,0, -1,1db 0,-1, 0,1db 1,-1, 1,0, 1,1style_end:发现了一个问题我将相对坐标读取到dx中高地址dh为行号低地址dl为列号即每一对(x,y)应该将行号写在后面列号写在前面。虽然和我预想的方式不太一样然而其实没有什么区别因为都只是空缺了(0,0)这一个组合而已无法解释最左边一列为什么会错位。
最后我在胡乱调试中以一种我认为错误的方式得到了我认为正确的结果。 surround_style:db -1,-2, 0,-1, 1,-1db -1,-1, 1,0,db -1,0, 0,1, 1,1style_end: