【编程实践】黑框框里的打字小游戏,但是汇编语言(1):https://developer.aliyun.com/article/1407230
指令记录
1 伪指令
section.段名称.start
获取段的汇编地址。注意与段地址的概念相区分,段地址 ∗ 16 段地址*16段地址∗16得到的才是段的汇编地址。
2 其它
jmp far [0x04]
从内存中取出两个字,低地址的字放入IP寄存器,高地址的字放入CS寄存器。
Bug记录
1 主引导扇区程序未成功加载用户程序
在没有操作系统的虚拟机上,目前我每次运行都会启动两个程序,一个是主引导扇区程序,它用于将用户程序从磁盘加载到内存中;另一个就是我们用来完成任务的用户程序了。
当在加载阶段就出现问题时,我习惯性地就认为是主引导程序的代码出现了问题,而实际上加载用户程序的任务是由引导程序与用户程序的头部合作完成的。
举个例子,我在用户程序的头部定义了用户程序的长度信息
;头部: 用户程序长度 program_length dd program_end ;[0x00] ;栈段 SECTION stack align=16 vstart=0 ;...程序结尾 program_end:
2 安装的int 9中断例程未正常运行
下面是程序的部分代码片段:
int9_new: ;说明:新的int 9中断例程,在屏幕固定位置显示按下的字符 push dx push ax mov al, 'k'
SECTION data align=16 vstart=0 text db 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' int9_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 es mov ax,0xb800 mov es,ax mov byte es:[0], 'k' pop es pop 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 ,取参数组合为( a = 217 , b = 11 , c = 253 ) (a=217,b=11,c=253)(a=217,b=11,c=253) 。生成散点图如下左图,横轴是次数,纵轴是生成的随机数值。可以很明显地看出周期性,周期长度在100左右,对于我的小游戏差不多能用了。
将参数c修改为32768(2的15次方)后,效果如右图所示。
6 数字显示不出来
明明感觉代码逻辑没啥问题,pop dx取得的是待显示的一位数字的值,可数字就是显示不出来。
后发现还是字符的值与属性的问题,在显存中每个字符占一个字(即两个字节),第字节是ascii码值,高字节是显示属性。代码中mov es:[di], dx直接写入一个字,dl 是ascii值没错,但dh是零,解释成属性就是黑底黑字。
好吧,原来不是没显示字符,只是显示了而我看不见。
put_one: pop dx add dx, '0' mov es:[di], dx add di, 2 loop put_one
7 环绕模块卡死
;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 kkk k
也不知道这个k是怎么独自跑到下面去的。
surround_style: db -1,-1, -1,0, -1,1 db 0,-1, 0,1 db 1,-1, 1,0, 1,1 style_end:
发现了一个问题:我将相对坐标读取到dx中,高地址dh为行号,低地址dl为列号,即每一对(x,y)应该将行号写在后面,列号写在前面。虽然和我预想的方式不太一样,然而其实没有什么区别,因为都只是空缺了(0,0)这一个组合而已,无法解释最左边一列为什么会错位。
最后,我在胡乱调试中以一种我认为错误的方式,得到了我认为正确的结果。
surround_style: db -1,-2, 0,-1, 1,-1 db -1,-1, 1,0, db -1,0, 0,1, 1,1 style_end: