Search code examples
assemblynasmx86-16bootloadervga

What is causing text to be displayed in a random place?


I am trying to write some code which displays some text at a given position on the screen.

When doing some research, I found this page that shows the formula position = (y_position * characters_per_line) + x_position;.

Here is the snipped of code that calculates and sets the position:

set_cursor_pos:
  push ax
  push bx

  mov al, [ypos]
  mov bl, 80
  mul bl
  add ax, [xpos]
  mov bl, 2
  mul bl
  mov di, ax

  pop bx
  pop ax

  ret

This works until ypos = 3 and xpos = 15 After this, it seems to wrap around to the beginning. Hear are some examples:

y=2, x = 30:

enter image description here

y=0, x = 60:

enter image description here

y=3, x=15:

enter image description here

y=4, x=0:

enter image description here




As you can see, my algorithm works until y=3, x-15. After that, it wraps around.

Is this because there is not enough memory? Do I need to enable the A20 line? Is it another problem? If so, please can you explain what and why.

Finally, here is all. of my code:

org 0x7c00


mov ax, 0xB800
mov es, ax
xor di, di

cli

mov ah, 0Fh
mov si, msg
call set_cursor_pos
call print

hlt

print:
  cli
  lodsb
  stosw
  cmp al, 0
  jne print
  ret


set_cursor_pos:
  push ax
  push bx

  mov al, [ypos]
  mov bl, 80
  mul bl
  add ax, [xpos]
  mov bl, 2
  mul bl
  mov di, ax

  pop bx
  pop ax

  ret

msg db 'Hello, World', 0

xpos db 0
ypos db 4

times 510-($-$$) db 0
dw 0xaa55

Solution

  • Look at your operand-sizes. xpos is only 1 byte, but you're reading 2 bytes with add ax, [xpos]. Also, mul bl does ax = al * bl, so you're throwing away the high half of the mul-by-80 result.

    i.e. set_cursor_pos returns with

    di = (( (80*ypos) & 0xff) + (xpos + (ypos<<8)) ) & 0xFF) * 2
    

    From your previous questions, you're targeting a 386-compatible, so you could write it with

    movzx  di, byte [ypos]
    imul   di, di, 80
    movzx  ax, byte [xpos]
    add    di, ax
    
    add    di, di       ; di *= 2.  equivalent to shl di, 1  but more efficient.
    

    (80 = 16 * 5, so you can also avoid imul and use one lea di, [edi + edi*4] / shl di, 4. Or whatever 8086-compatible trick for multiply by a number with so few set bits.)

    There is zero point in using mul to multiply by 2, unless you were going to use mul bx and use the full 32-bit result in dx:ax. But even then, for 2 you should just use add di,di / setc al because the carry-out can only be 1 bit.

    If xpos and ypos were 16-bit, you could use them as memory operands:

    imul   di, [ypos], 80
    add    di, [xpos]
    add    di, di       ; di *= 2
    

    Or of course you could keep them in registers in the first place.