具有相同值的两个变量正在为寄存器分配不同的值? ASM(还有一些其他问题)

Two variables with the same value are assigning different values to register? ASM (also some other issues)

我目前正在为 class 做一个项目。不幸的是,这学期我在家里遇到了一些个人问题,所以我一直在努力赶上进度,包括这个 class,如果这个问题听起来很愚蠢或代码看起来很可怕,我们深表歉意。

我必须做一个游戏。我画了边框、游戏网格等,我基本上想在屏幕上打印出包括值超过 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。

知道发生了什么吗?

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

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

我不知道这是否正确,但我觉得我可能 运行 内存不足或者我没有正确清理?

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

我会尝试使用 TASM 和 dosbox 来执行此操作,但对于大多数程序来说,dosbox 似乎都会崩溃,甚至是我在网上找到的示例程序。我读到它可能与我在 64 位机器上有关。这就是我下载 emu8086

的原因

感谢阅读

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

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

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

您的代码确实以数据开头,因此它们由 CPU 作为指令执行,导致其他一些 damage/discrepancies(如 40x25 文本模式等)。

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


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


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

segment .code - 同样的故事,只是 .code 在 TASM

中是正确的

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

顺便说一句,那些 .data/.code 修复也很可能在 emu8086 中起作用,因为它基本上忽略了几乎所有它不完全理解的东西,不强制执行任何特定语法。


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

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


清屏:哇,这是对 "Scroll active page up" 服务的一些滥用,弄清楚它令人印象深刻。但是有更简单的方法可以通过覆盖视频内存来清除屏幕:

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)

关于board的手工绘制...先从语法问题说起吧

mov ptr es: [bx], dx

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

通常当要存储的值取自寄存器时,大小说明符不会写在源代码中,因为它与所用寄存器的大小类似"obvious"。 IE。 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)

虽然评论不完全"intent-like",但我对这个几乎满意。我宁愿将其描述为 ; offset of [1, 0] character ([x,y]) ... 我可能会做 mov bx, ( 0*80 + 1)*2 这样我以后可以在代码中使用相同的行(我什至在 0 和 1 前面放了额外的 space 以允许对于稍后的两位数坐标),但仅更改坐标,请参见下文。

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

现在这里变得很乱..."bottom horizontal" 有点保存它,但仍然很神秘,考虑这个替代方案:

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 多余的东西,但我最终会以不同的方式做整个事情,正如我将在下面 post,这只是一个想法如何通常在汇编中你可以使用更少的寄存器,当某些东西与你已经拥有的东西相距甚远时。

   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

再次 "what is 158" 和 jljl 是 "jump less" 的缩写,这是有符号算术。但是偏移量是无符号的。在文本模式下你不会遇到问题,因为即使屏幕底部也只有大约 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 variant,所以你可以直接做:

mov byte ptr es:[3998], 188

检查此答案以了解在 x86 的 16b 实模式下寻址操作数的所有合法变体 x86 16-bit addressing modes。 (在 32/64 位保护模式下,有更多可用的变体,如果您看到类似 mov al,[esi+edx*4] 的内容,那是 32b 模式下合法的 x86 指令,但在 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 是什么......我们在高中时最喜欢的目录名称,当时我们确实想要 "secret")。

希望代码有足够的注释以便于理解,它会给你一些想法如何在汇编中节省一些输入,而不是在计算不成功时用它来换取一些头疼的问题正如您所期望的那样...;)

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

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

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


我确实使用 turbo 汇编程序 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)(它们将覆盖调试器的屏幕,之后会立即刷新)。