Search code examples
assemblygraphicsx86-16tasmemu8086

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


I am currently working on a project for class. Unfortunately I had some personal issues at home this semester, so I have been trying to catch up including this class, so sorry if the question may sound dumb or code looks horrendous.

I have to make a game. I drew the border, game grid etc and I basically wanted to print out digits including values over 10 to the screen. I have it down how to convert to ascii codes.

I was going to have an array of values for the game. I wanted to test with 1 value, 0, first to print across the screen before I put a bunch of numbers. I am doing this on emu8086 btw I have five variables under my data segment, the two I'm referring to is

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

Take note they are both dw's and have the same value, 0

Now, I have a working for loop to print values in the right places on the screen. I'm in textmode, at 80x25 (ax 0003h to be exact)

This code below is in the loop. I have another variable in the data segment called counter which counts for how many boxes I'm filling on the grid, and then shifts to the second row on the grid once the counter reaches a certain amount and so on until i fill the final box.

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

When I do this, dx receives an unsigned value of 205 when assigning numbers value to it (this is before add 48)

But when i use

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

It works fine. dx gets a value of 0 , then adding 48 gives the ascii code 48, which prints 0's. The previous one is getting an ascii value -3 which is printing tiny 2's.

Any ideas what is happening?

Also, my other two issues are my other variable, digitTwo. When I switch it from db to dw, my game no longer runs in 80x25, it goes to 40x25. I have no idea why. Making it db again brings it back to 80x25. Same thing happens if I just try to declare a new variable.

Another issue is, when I compile this to a com file to run on dosbox with TASM, let's say the 0's print successfully, they're all indented a certain amount over to the right. Why?

I don't know if this is right, but I feel like maybe I'm running out of memory or I'm not cleaning up correctly?

The rest of my code are basically just for loops creating a border around the screen and then a grid for the game. Those draw fine.

I would try to do this using TASM and dosbox, but it seems dosbox crashes for most programs, even example programs I found online. I read it may have to do with I'm on a 64 bit machine. That's why I downloaded emu8086

Thank you for reading


Solution

  • First the main reason for most of the "weird" behaviour happening.

    You were assembling+running it as COM file, which is one of the two DOS executable types.

    The COM file is raw binary (65280 bytes long at most (65536-256)), which is loaded into some segment of free memory from offset 100h (the first 256 bytes contain things like command line (arguments), and other DOS environment values, can't recall details, IIRC that zone is called PSP). After the DOS will load the binary, it will jump to that offset 100h.

    Your code did start with data, so they were executed by CPU as instructions, causing some other damage/discrepancies (like the 40x25 text mode, etc).

    When EXE type of binary would be used, it would work fine, as the EXE contains further meta data about entry point (end start in your full source specifies the entry point for EXE, but not for COM) and can load data into separate data segment. And also can contain several code/data segments, so the total size of EXE can be much larger (if you have big code base which doesn't fit into single segment).


    Now about syntax problems, to make your source compilable with TASM, and some hints about code itself.


    segment .data - not recognized by TASM, you have to use either just .data - shortcut to define data segment, or full-fledged segment definition, including ends, etc. (check TASM documentation).

    segment .code - same story, just .code is correct in TASM

    But neither .data or .code is needed if you are targetting COM binary, then only the org 100h is important (and start with code immediately, even if it's just jmp start followed by data). And vice versa, with EXE target the org 100h shouldn't be there, let the linker specify memory map of EXE executable, it has some reasonable defaults. So pick one type, and stick to it. In TASM you can pre-select assembler behaviour by using .model small for EXE, or .model tiny for COM (there are also bigger models, but you will not need those). In emu8086 there's some help/docs file, where the directives for COM/EXE are explained, I don't remember and don't even want to know, as emu8086 is IMO quite atrocious (but hey, as long as it works for you...).

    BTW, those .data/.code fixes will very likely work in emu8086 too, as it basically ignores almost everything it does not fully understand, not enforcing any particular syntax.


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

    Wrong comment, AL is lower byte, AH is higher byte, so 0003h is AH=0, AL=3.


    Clearing screen: wow, that's quite some abuse of "Scroll active page up" service, impressive for figuring it out. But there's simpler way to clear screen just by overwriting video memory:

    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)
    

    About the manual drawing of board... let's start with syntax problems.

    mov ptr es: [bx], dx
    

    The standalone ptr directive means nothing in this context (not even sure if there is any context when it would in MASM/TASM mean something). If you want say that memory should be written, the square brackets are enough, i.e mov es:[bx],dx. If you want to specify the size of data to write, then you have to specify it mov byte ptr es:[bx],dx, asking assembler to produce instruction which will store byte at address es:bx from dx - which will be reported as error in TASM, because dx is of word size, and such instruction doesn't exist. (working alternatives would be mov word ptr es:[bx],dx or mov byte ptr es:[bx],dl).

    Usually when the value to be stored is taken from register, the size specifier is not written in source, as it's sort of "obvious" from the size of register used. I.e. dx to write = word to write.

    But then you draw the grid with instructions like:

    mov ptr es: [bx], 240
    

    Where the assembler can't deduct the size of 240, so the size modifier is needed and strongly recommended (some assemblers will silently guess size by the value, but such source is hard to read+debug, because you don't know which value did want programmer to store). So fix that with: mov byte ptr es: [bx], 240 (as you want to write only the ASCII part, not attribute -> byte size only).

    And finally the code style, I will dissect the first loop only:

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

    I'm almost happy with this one, although the comment is not completely "intent-like". I would rather describe it as ; offset of [1, 0] character ([x,y]) ... and I would probably do mov bx, ( 0*80 + 1)*2 so I can use identical lines in code later (I even put there extra space ahead 0 and 1 to allow for two-digit coordinates later), but changing only coordinates, see below.

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

    Now here it becomes plain fugly... the "bottom horizontal" is somewhat saving it, but still very cryptic, consider this alternative:

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

    This way you make the assembler to do the calculation stuff for you during assembling. The resulting machine code is of course identical, value 3842 in both cases, but in this way you can read the source and understand what that offset should represent.

    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
    

    This is quite fine. It would be possible to use only single offset register bx and the second write to be +24 lines away like:

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

    Which would allow you to delete all that di stuff as redundant, but I would in the end do the whole thing differently, as I will post below, this is just an idea how often in assembly you can use fewer registers, when something is just offset-away from something you already have.

       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
    

    Again "what is 158" AND jl. jl is short for "jump less", and that's signed-arithmetic. But offsets are unsigned. In text mode you will not hit a problem with it, because even bottom of screen is only about 4k offset, but in pixel 320x200 mode the bottom pixels are at offsets around 64000, and that's already negative value when you treat it as 16b signed, so the jl would not loop in such case. jb is the more correct one, for comparing memory offsets.

       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
    

    Then later when you fix corners, you have code like this:

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

    That's a bit overcomplicating things, x86 does have also MOV r/m8,imm8 variant, so you can do directly:

    mov byte ptr es:[3998], 188
    

    Check this answer for all legal variants of addressing operand in 16b real mode of x86 x86 16-bit addressing modes. (In 32/64 bit protected mode there are lot more variants available, if you see things like mov al,[esi+edx*4], that's legal x86 instruction in 32b mode, but not in 16b)

    And finally that's lot of code which repeats a lot, how about using some subroutines to make it shorter? My try (whoa, still about ~100 lines, but compare with yours):

        ... 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
    

    The extended VGA ASCII codes from here: https://en.wikipedia.org/wiki/Code_page_437 (notice what is 255 ... our favourite directory name when at high school, when we did want to make it "secret").

    Hopefully that code is commented enough to be easy to understand, and it will give you some ideas how to save yourself some typing in assembly, and instead trade it off for some heave head scratching when the calculations don't go the way you expected... ;)

    Also the emu8086 has built-in debugger, it's absolutely invaluable and essential tool. Every time you write some small part of your new code, jump into debugger, and single-step over each instruction, slowly and meticulously, checking all reported machine state changes (register values, flags, modified memory), and compare that with expected/assumed behaviour. Any discrepancy should be reasoned about and understood, resulting either into fix of code (to do what you did want), or adjusting your assumptions (to correctly understand what truly happens in the computer).

    That also means, that you should invest heavily into time spend writing your source, to make sure that the written source is clearly stating your original intent, and is easy to read+understand. For example use long and meaningful label and variable names. Comment every section of code, what is its purpose.

    Don't try to save your time on source writing, because that will usually cost you much more time upon source reading+debugging+fixing. The source should read well and should be understandable for human. Just passing through assembler is not enough, that mean it's readable for machine, but machine will not help you to fix it.


    I did test the code with turbo assembler 4.1, command line used (ASM file named GAMEBOX.ASM (in dosbox):

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

    With EXE target defined at the source beginning:

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

    For debugging in turbo debugger (TD.EXE), you should switch in Options -> Display options "Display swapping" to "Always", otherwise the direct video memory overwrites may be not visible on user screen (Alt+F5) (they will overwrite debugger's screen, and that one will refresh it immediately after).