assemblyx86x86-16bootloaderreal-mode

Register corruption when iterating in a nested loop


It's my first time writing x86-16 assembly. I'm writing a bootloader game, but I'm struggling with iterating through level data.

I'm attempting to iterate through a 2d array - each byte of this array is split into 2-bit chunks. Therefore, I have a 3-layered loop. The video output that I'm getting shows the characters in positions that they aren't meant to be able to reach.

I tested my calculation of the position and I believe it to be sound, which leads me to believe the registers are being corrupted by another operation I'm performing.

Here is the algorithm that I am using:

    ; Draw map
    ;   Loop setup will have 
    ;   ax - Y pos counter
    ;   bx - X pos counter
    ;   cx - Bit pair counter
    ;   di - Offset (map_ptr + bp)
    pusha
    mov si, [map_ptr]           ; Storing map data

    xor di, di                  ; Clearing offset to 0
    xor ax, ax                  ; Starting loop
    .loop_y:
    xor bx, bx                  ; Reset X for each Y

    .loop_x:
    xor cx, cx                  ; Reset bit pair counter for each X

    .loop_bits:
    ; Move cursor
    xor dx, dx
    movzx dx, bl
    imul dx, dx, e_MAP_TILE_PER_BYTE
    add dx, cx
    mov dh, al
    call s_move_cursor

    ; Get data
    push cx
    push si

    add si, di
    movzx dx, byte [si]         ; Move full byte into dl (dl = byte)
    imul cx, cx, 2              ; Calculate shift amount
    shl dx, cl                  ; Shift tile data into dh
    and dh, 00000011b           ; Mask only the last tile, now dh = tile type

    pop si
    pop cx

    ; Print character
    push ax
    push bx

    cmp dh, e_BLOCK_WALL
    je .draw_wall
    cmp dh, e_BLOCK_LAVA
    je .draw_lava
    cmp dh, e_BLOCK_END
    je .draw_end
    jmp .draw_none

    .draw_wall:
    mov al, e_WALL_CHR
    mov bl, e_WALL_COL
    jmp .draw_start
    .draw_lava:
    mov al, e_LAVA_CHR
    mov bl, e_LAVA_COL
    jmp .draw_start
    .draw_end:
    mov al, e_END_CHR
    mov bl, e_END_COL
    jmp .draw_start

    .draw_start:
    call s_print
    .draw_none:
    pop bx
    pop ax

    .loop_bits_end:
    inc cx
    cmp cx, e_MAP_TILE_PER_BYTE
    jne .loop_bits
    
    .loop_x_end:
    inc di                      ; Increment offset by 1 for next access
    inc bx
    cmp bx, e_MAP_MAX_LENGTH
    jne .loop_x

    .loop_y_end:
    inc ax
    cmp ax, e_MAP_MAX_HEIGHT
    jne .loop_y
    ; fall through naturally if ended

    popa

There is some other information that is useful

; Preprocessor constants
e_MAP_MAX_LENGTH equ 2          ; Map size (Split into half-nibbles for 8 cells)
e_MAP_MAX_HEIGHT equ 7 
e_MAP_TILE_PER_BYTE equ 4
e_MAP_BSIZE      equ e_MAP_MAX_LENGTH * e_MAP_MAX_HEIGHT * e_MAP_TILE_PER_BYTE

; Maps
map_i   db e_MAP_BSIZE dup (0x55)  ; Cells are 1byte=4cells
map_ii  db e_MAP_BSIZE dup (0x55)  ; Cells are 1byte=4cells
map_iii db e_MAP_BSIZE dup (0x55)  ; Cells are 1byte=4cells
map_iv  db e_MAP_BSIZE dup (0x55)  ; Cells are 1byte=4cells


; Data and variables
map_ptr dw map_i

I believe my push/pop structure would make these subroutines safe, but just so everything is clear here they are:

; SUBROUTINE
; dx - positional information
s_move_cursor:
    push ax
    push bx

    mov ah, 02h                 ; Call interrupt to move cursor
    mov bh, 0                   ; Page number
    int 10h

    pop ax
    pop bx
    ret

; SUBROUTINE
; al - character
; bl - style
s_print:
    push ax
    push bx
    push cx

    mov bh, 0                   ; Page number
    mov cx, 1                   ; Repeat count
    
    mov ah, 09h                 ; Call interrupt to print
    int 10h

    pop cx
    pop bx
    pop ax
    ret

I expected this function to print a full wall of #'s. Instead all printed characters are #'s, however they are in incorrect positions.

Stepping through each iteration in this with gdb resulted in me seeing the registers take up the incorrect values (almost as if they are iterating too early?). I can't see how an early iteration could happen in my code though, instead I suspect an instruction or subroutine is corrupting the register with data.


Solution

  • Although you say that you think that your subroutines are fine, I believe it is there that the trouble starts.

    Whatever it is that you push to the stack, it must come off in the reverse order. That is not the case in the s_move_cursor subroutine:

    push ax
    push bx
    mov ah, 02h                 ; Call interrupt to move cursor
    mov bh, 0                   ; Page number
    int 10h
    pop ax
    pop bx
    ret
    

    The pop ax must come after the pop bx.