具有相同值的两个变量正在分配不同的值来注册?ASM(还有其他一些问题)



我目前正在为课堂做一个项目。 不幸的是,这学期我在家里遇到了一些个人问题,所以我一直在努力赶上包括这门课,所以很抱歉,如果这个问题听起来很愚蠢或代码看起来很糟糕。

我必须做一个游戏。我画了边框,游戏网格等,我基本上想打印出数字,包括超过10的值到屏幕上。我记下了如何转换为 ascii 代码。

我打算为游戏提供一系列值。我想用 1 个值 0 进行测试,在我输入一堆数字之前先在屏幕上打印。 顺便说一句,我在 emu8086 上这样做 我的数据细分受众群下有五个变量,我指的是两个变量

numbers dw 0 ; this was gonna have multiple values (an array), started off with 1 value
digitOne dw 0 ; this was suppose to represent a digit, I can make it also ? instead

请注意,它们都是 dw 并且具有相同的值,0

现在,我有一个工作 for 循环,可以在屏幕上的正确位置打印值。我处于文本模式,80x25(确切地说是 ax 0003h)

下面的代码在循环中。我在数据段中还有另一个变量,称为计数器,它计算我在网格上填充的框数,然后在计数器达到一定数量后移动到网格上的第二行,依此类推,直到我填充最后一个框。

mov dx, [numbers]      ; assign variable value
add dx, 48              ; gives the ascii code 48 to the lower bit
mov dh, 2fh               ; gives attribute color
mov ptr es: [bx], dx      ; displays on screen

当我这样做时,dx 在为其分配数字值时收到一个无符号值 205(这是在添加 48 之前)

但是当我使用

mov dx, [digitOne]      ; assign variable value
add dx, 48              ; gives the ascii code 48 to the lower bit
mov dh, 2fh               ; gives attribute color
mov ptr es: [bx], dx

dx 得到一个值 0 ,然后加上 48 得到 ASCII 代码 48,打印 0。前一个是获得一个 ascii 值 -3,它打印的是微小的 2。

知道发生了什么吗?

另外,我的另外两个问题是 我的另一个变量,数字二。当我将其从 db 切换到 dw 时,我的游戏不再以 80x25 运行,而是以 40x25 运行。我不知道为什么。再次将其设置为db会将其恢复到80x25。如果我只是尝试声明一个新变量,也会发生同样的事情。

另一个问题是,当我将其编译为com文件以使用TASM在dosbox上运行时,假设0打印成功,它们都向右缩进了一定量。为什么?

我不知道这是否正确,但我觉得我的内存可能用完了,或者我没有正确清理?

我的其余代码基本上只是用于在屏幕周围创建边框的循环,然后为游戏创建一个网格。那些画得很好。

我会尝试使用 TASM 和 dosbox 来做到这一点,但大多数程序似乎都会崩溃,甚至是我在网上找到的示例程序。我读到它可能与我在 64 位机器上有关。这就是我下载emu8086的原因

感谢您的阅读

首先是大多数"奇怪"行为发生的主要原因。

您正在将其组装+运行为COM文件,这是两种DOS可执行文件类型之一。

COM文件是原始二进制文件(最长65280字节(65536-256)),从偏移量加载到100h的某个可用内存段中(前256个字节包含命令行(参数)和其他DOS环境值之类的东西,无法回忆细节,IIRC那个区域称为PSP)。DOS 加载二进制文件后,它将跳转到该偏移量100h

您的代码确实是从数据开始的,因此它们由 CPU 作为指令执行,从而导致其他一些损坏/差异(如 40x25 文本模式等)。

当使用 EXE 类型的二进制文件时,它可以正常工作,因为 EXE 包含有关入口点的更多元数据(完整源中的end start指定 EXE 的入口点,但不指定 COM 的入口点),并且可以将数据加载到单独的数据段中。并且还可以包含多个代码/数据段,因此EXE的总大小可以大得多(如果您的代码库很大,不适合单个段)。


现在关于语法问题,使你的源代码可以使用 TASM 编译,以及一些关于代码本身的提示。


segment .data- TASM无法识别,您必须仅使用.data- 快捷方式来定义数据段,或者使用成熟的segment定义,包括ends等(查看TASM文档)。

segment .code- 同样的故事,只是 TASM 中.code是正确的

但是,如果您的目标是 COM 二进制文件,则不需要.data.code,那么只有org 100h很重要(并立即从代码开始,即使它只是jmp start后跟数据)。反之亦然,对于 EXE 目标,org 100h不应该存在,让链接器指定 EXE 可执行文件的内存映射,它有一些合理的默认值。所以选择一种类型,并坚持下去。在 TASM 中,您可以通过对 EXE 使用.model small或对 COM 使用.model tiny来预先选择汇编程序行为(也有更大的模型,但您不需要这些模型)。在 emu8086 中有一些帮助/文档文件,其中解释了 COM/EXE 的指令,我不记得了,甚至不想知道,因为 emu8086 是 IMO 相当残暴(但是嘿,只要它对有用......

顺便说一句,这些.data/.code修复程序很可能也适用于 emu8086,因为它基本上忽略了几乎所有它不完全理解的内容,不强制执行任何特定的语法。


mov ax, 0003h ; Size of 80x25 (al=00, ah=03)

错误的注释,AL 是较低的字节,AH 是较高的字节,所以0003h是 AH=0,AL=3。


清除屏幕:哇,这是对"向上滚动活动页面"服务的滥用,令人印象深刻。但是,只有一种更简单的方法可以通过覆盖视频内存来清除屏幕:

start:
mov ax, @data               ; ds = data segment
mov ds, ax
mov ax, 0B800h              ; es = text mode video memory segment
mov es, ax
mov ax, 0003h ; Size of 80x25 (al=00, ah=03)
int 10h
; clear screen
xor di, di                  ; di = video memory offset 0
mov ax, 02F00h + ' '        ; write space with green background and white text
mov cx, 80*25               ; 80*25 character+attribute pairs
rep stosw                   ; overwrite video memory (clears screen)

关于手动绘制电路板...让我们从语法问题开始。

mov ptr es: [bx], dx

独立的ptr指令在这种情况下没有任何意义(甚至不确定在MASM/TASM中是否有任何上下文意味着什么)。如果你想说应该写内存,方括号就足够了,即mov es:[bx],dx.如果要指定要写入的数据大小,则必须mov byte ptr es:[bx],dx指定它,要求汇编程序生成指令,该指令将存储byte来自dx的地址es:bx- 这将在TASM中报告为错误,因为dx的大小word,并且不存在这样的指令。(工作替代方案将是mov word ptr es:[bx],dxmov byte ptr es:[bx],dl)。

通常,当要存储的值取自寄存器时,大小说明符不会写入源代码中,因为从所使用的寄存器大小来看,它有点"明显"。 即 写入dx= 写入word

但是,然后使用如下说明绘制网格:

mov ptr es: [bx], 240

如果汇编程序不能扣除240的大小,所以需要大小修饰符,并强烈建议使用(有些汇编程序会默默地通过值猜测大小,但这样的源很难读取+调试,因为你不知道哪个值确实希望程序员存储)。因此,请使用:mov byte ptr es: [bx], 240(因为您只想写入 ASCII 部分,而不是仅写入属性 -> 字节大小)。

最后是代码风格,我将仅剖析第一个循环:

mov bx, 2                   ; start at offset 2 (top horizontal line of border)

我对这个几乎很满意,尽管评论并不完全"像意图"。我宁愿将其描述为; offset of [1, 0] character ([x,y])...我可能会做mov bx, ( 0*80 + 1)*2这样我以后就可以在代码中使用相同的行(我什至在 0 和 1 之前放了额外的空间以允许稍后的两位数坐标),但只更改坐标,见下文。

mov di, 3842                ; start offset 3842 (bottom horizontal line of border)

现在这里变得平淡无奇..."底部水平"有点省钱,但仍然很神秘,考虑一下这个替代方案:

mov di, (24*80 +  1)*2      ; offset of [1, 24] character ([x,y])

通过这种方式,您可以使汇编程序在组装过程中为您完成计算工作。生成的机器代码当然是相同的,两种情况下的值3842,但通过这种方式,您可以阅读源代码并了解偏移量应该代表什么。

l1:
mov byte ptr es: [bx], 240    ; display fake at "coordinate" bx
mov byte ptr es: [di], 240    ; same as above
add bx, 2                ; move 2 horizontally
add di, 2                ; same as above

这很好。可以只使用单个偏移寄存器bx第二次写入+24行,如下所示:

mov byte ptr es: [bx + (24*80*2)], 196    ; bottom line

这将允许您删除所有多余的di内容,但我最终会以不同的方式做整个事情,正如我将在下面发布的那样,这只是一个想法,当某些东西只是偏移你已经拥有的东西时,你可以在汇编中使用更少的寄存器的频率。

cmp bx, 158              ; compare to the width of the window (160-2 because want room for a box corner)
jl l1                    ; loop if less than the width

再次"什么是 158"和jl.jl是"少跳"的缩写,这是有符号算术。但偏移量是无符号的。在文本模式下,您不会遇到问题,因为即使屏幕底部也只有大约 4k 偏移量,但在像素 320x200 模式下,底部像素的偏移量约为 64000,当您将其视为 16b 符号时,这已经是负值,因此在这种情况下jl不会循环。jb是比较内存偏移量更正确的方法。

cmp bx, ( 0*80 +  79)*2  ; write up to [79, 0] character (last line at [78,0], leaving room for corner)
jb l1                    ; loop if below that

然后稍后当你修复角落时,你有这样的代码:

mov bx, 3998
mov byte ptr es: [bx], 188

这有点过于复杂,x86 确实也有MOV r/m8,imm8变体,所以你可以直接做:

mov byte ptr es:[3998], 188

查看此答案,了解在 x86 x86 16 位寻址模式的 16b 实数模式下寻址操作数的所有合法变体。(在 32/64 位保护模式下,还有更多可用的变体,如果您看到类似mov al,[esi+edx*4]的东西,那是 86b 模式下的合法 x22 指令,但在 16b 模式下不是)

最后是大量重复的代码,使用一些子例程使其更短怎么样?我的尝试(哇,仍然大约 ~100 行,但与您的相比):

... after screen is set and cleared...
; big box around whole screen
mov al,196                  ; horizontal line ASCII
mov cx,78                   ; 78 characters to draw (80-2, first and last are for corners)
mov di, ( 0*80 +  1)*2      ; offset of [1, 0] character ([x,y])
call FillCharOnly_horizontal    ; top line
mov di, (24*80 +  1)*2      ; offset of [1, 0] character ([x,y])
call FillCharOnly_horizontal    ; bottom line
mov al,179                  ; vertical line ASCII
mov cx,23                   ; 23 characters to draw (25-2, first and last are for corners)
mov di, ( 1*80 +  0)*2      ; offset of [0, 1] character ([x,y])
call FillCharOnly_vertical  ; left line
mov di, ( 1*80 + 79)*2      ; offset of [79, 1] character ([x,y])
call FillCharOnly_vertical  ; right line
; corners are done separately
mov byte ptr es:[( 0*80 +  0)*2],218
mov byte ptr es:[( 0*80 + 79)*2],191
mov byte ptr es:[(24*80 +  0)*2],192
mov byte ptr es:[(24*80 + 79)*2],217
; draw 4x4 boxes (13x5 box size) with 5 horizontal and 5 vertical lines
; 5 horizontal lines first
mov ax, 196 + (5*256)       ; horizontal line in AL + counter=5 in AH
mov cx, 4*13                ; each box is 13 characters wide
mov di, ( 2*80 +  2)*2      ; offset of [2, 2] character ([x,y])
push di                     ; will be also starting point for vertical lines
boxes_horizontal_loop:
push di                     ; store the offset
call FillCharOnly_horizontal    ; horizontal edge of boxes
pop di
add di, ( 5*80 +  0)*2      ; next horizontal is [+0, +5] away
dec ah
jnz boxes_horizontal_loop
; 5 vertical lines then
mov ax, 179 + (5*256)       ; vertical line in AL + counter=5 in AH
mov cx, 4*5                 ; each box is 5 characters tall
pop di                      ; use same starting point as horizontal lines
boxes_vertical_loop:
push di                     ; store the offset
call FillCharOnly_vertical  ; horizontal edge of boxes
pop di
add di, ( 0*80 + 13)*2      ; next vertical is [+13, +0] away
dec ah
jnz boxes_vertical_loop
; fix corners and crossings - first the 4 main corners
mov byte ptr es:[( 2*80 +  2)*2],218
mov byte ptr es:[( 2*80 + 54)*2],191
mov byte ptr es:[(22*80 +  2)*2],192
mov byte ptr es:[(22*80 + 54)*2],217
; now the various crossings of the game box itself
mov cx,3                    ; 3 of top "T" (count)
mov dx,13*2                 ; offset delta (+13 characters to right)
mov al,194                  ; top "T"
mov di, ( 2*80 + 15)*2      ; offset of [2+13, 2] character ([x,y])
call FillCharOnly
mov al,193                  ; bottom "T"
mov di, (22*80 + 15)*2      ; offset of [2+13, 22] character ([x,y])
call FillCharOnly           ; CX+DX was preserved by the call, still same
mov al,195                  ; left "T"
mov di, ( 7*80 +  2)*2      ; offset of [2, 2+5] character ([x,y])
mov dx,(5*80)*2             ; offset delta (+5 lines below)
call FillCharOnly           ; CX is still 3, DX was updated
mov al,180                  ; right "T"
mov di, ( 7*80 + 54)*2      ; offset of [2+52, 2+5] character ([x,y])
call FillCharOnly           ; CX+DX was preserved by the call, still same
mov al,197                  ; crossings inside "+", will reuse the +5 lines delta
mov di, ( 7*80 + 15)*2      ; offset of [2+13, 2+5] character ([x,y])
call FillCharOnly           ; CX+DX was preserved by the call, still same
mov di, ( 7*80 + 28)*2      ; offset of [2+26, 2+5] character ([x,y])
call FillCharOnly           ; CX+DX was preserved by the call, still same
mov di, ( 7*80 + 41)*2      ; offset of [2+39, 2+5] character ([x,y])
call FillCharOnly           ; CX+DX was preserved by the call, still same
; wait for some key hit, like enter
mov ah,1
int 21h
; exit to DOS correctly
mov ax,4C00h
int 21h
; helper subroutines
; al = character to fill with, di = starting offset, cx = count, dx = next offset delta
; Will write al to memory es:[di], advancing di by delta in dx, modifies di
FillCharOnly:
push    cx                  ; preserve count
FillCharOnly_loop:
mov     es:[di],al          ; store the character
add     di,dx               ; advance di pointer
dec     cx
jnz     FillCharOnly_loop   ; repeat count-many times
pop     cx                  ; restore count
ret
; al = character to fill with, di = starting offset, cx = count
; Will write al to memory es:[di], advancing di by 2, modifies di and dx
; works as "horizontal line" filler in VGA text mode
FillCharOnly_horizontal:
mov     dx,2
jmp     FillCharOnly        ; continue with general subroutine
; al = character to fill with, di = starting offset, cx = count
; Will write al to memory es:[di], advancing di by 160, modifies di and dx
; works as "vertical line" filler in VGA text mode
FillCharOnly_vertical:
mov     dx,160              ; next line in 80x25 mode is +160 bytes away
jmp     FillCharOnly        ; continue with general subroutine

扩展的VGA ASCII代码来自这里:https://en.wikipedia.org/wiki/Code_page_437(注意什么是255 ...我们在高中时最喜欢的目录名称,当我们确实想让它"秘密"时)。

希望代码的注释足够容易理解,它会给你一些想法,如何节省自己在汇编中的一些打字,而不是在计算没有按照你预期的方式进行时用它来换取一些令人头疼的事情...... ;)

此外,emu8086具有内置调试器,它绝对是无价且必不可少的工具。每次编写新代码的一小部分时,跳入调试器,然后缓慢而细致地单步执行每条指令,检查所有报告的机器状态更改(寄存器值、标志、修改的内存),并将其与预期/假定的行为进行比较。任何差异都应该被推理和理解,导致代码的修复(做你想做的事),或者调整你的假设(正确理解计算机中真正发生的事情)。

这也意味着,你应该投入大量时间写你的来源,以确保书面来源清楚地陈述你的原始意图,并且易于阅读+理解。例如,使用长而有意义的标签和变量名称。注释代码的每一段,它的目的是什么。

不要试图节省你在源代码编写上的时间,因为这通常会在源代码读取+调试+修复上花费你更多的时间。来源应该读起来很好,并且应该对人类来说是可以理解的。仅仅通过汇编程序是不够的,这意味着它对机器来说是可读的,但机器不会帮助你修复它。


我确实使用涡轮汇编程序 4.1 测试了代码,使用了命令行(名为GAMEBOX.ASM的 ASM 文件(在 dosbox 中):

tasm.exe /m5 /w2 /l GAMEBOX
tlink GAMEBOX.OBJ

在源开头定义 EXE 目标:

.model small
.stack 1000h
.data
.code
start:
mov ax, @data               ; ds = data segment
...

要在 turbo 调试器 (TD.EXE) 中进行调试,您应该在选项 -> 显示选项">显示交换"中切换为">始终",否则直接视频内存覆盖可能在用户屏幕上不可见 (Alt+F5) (它们将覆盖调试器的屏幕,并且该屏幕将立即刷新它)。

相关内容

  • 没有找到相关文章

最新更新