0

I'm trying to create a line-drawing algorithm in assembly (more specifically Bresenham's line algorithm). After trying an implementation of this algorithm, the algorithm fails to work properly even though I almost exactly replicated the plotLineLow() function from this Wikipedia page.

It should be drawing a line between 2 points, but when I test it, it draws points in random places in the window. I really don't know what could be going wrong because debugging in assembly is difficult.

I'm using NASM to convert the program to binary, and I run the binary in QEMU.

[bits 16]                                               ; 16-bit mode
[org 0x7c00]                                            ; memory origin

section .text                                           ; code segmant
    global _start                                       ; tells the kernal where to begin the program
_start:                                                 ; where to start the program

; main
call cls                                                ; clears the screen
update:                                                 ; main loop
    mov cx, 0x0101                                      ; line pos 1
    mov bx, 0x0115                                      ; line pos 2
    call line                                           ; draws the line

    jmp update                                          ; jumps to the start of the loop


; functions
cls:                                                    ; function to clear the screen
    mov ah, 0x00                                        ; set video mode
    mov al, 0x03                                        ; text mode (80x25 16 colours)
    int 0x10                                            ; BIOS interrupt
    ret                                                 ; returns to where it was called


point:                                                  ; function to draw a dot at a certain point (dx)
    mov bx, 0x00ff                                      ; clears the bx register and sets color
    mov cx, 0x0001                                      ; clears the cx register and sets print times
    mov ah, 0x02                                        ; set cursor position
    int 0x10                                            ; BIOS interrupt

    mov ah, 0x09                                        ; write character
    mov al, ' '                                         ; character to write
    int 0x10                                            ; BIOS interrupt
    ret                                                 ; returns to where it was called


line:                                                   ; function to draw a line at two points (bx, cx)
    push cx                                             ; saves cx for later
    push bx                                             ; saves bx for later

    sub bh, ch                                          ; gets the value of dx
    mov [dx_L], bh                                      ; puts it into dx
    sub bl, cl                                          ; gets the value of dy
    mov [dy_L], bl                                      ; puts it into dy

    mov byte [yi_L], 1                                  ; puts 1 into yi (positive slope)

    cmp byte [dy_L], 0                                  ; checks if the slope is negative
    jl .negative_y                                      ; jumps to the corresponding sub-label
    jmp .after_negative_y                               ; if not, jump to after the if

    .negative_y:                                        ; if statement destination
        mov byte [yi_L], -1                             ; sets yi to -1 (negative slope)
        neg byte [dy_L]                                 ; makes dy negative as well
    .after_negative_y:                                  ; else statement destination

    mov ah, [dy_L]                                      ; moves dy_L into a temporary register
    add ah, ah                                          ; multiplies it by 2
    sub ah, [dx_L]                                      ; subtracts dx from that
    mov [D_L], ah                                       ; moves the value into D

    pop bx                                              ; pops bx to take a value off
    mov [y_L], bh                                       ; moves the variable into the output
    
    pop cx                                              ; pops the stack back into cx
    mov ah, bh                                          ; moves x0 into ah
    mov al, ch                                          ; moves x1 into al

    .loop_x:                                            ; loop to go through every x iteration
        mov dh, ah                                      ; moves the iteration count into dh
        mov dl, [y_L]                                   ; moves the y value into dl to be plotted
        call point                                      ; calls the point function

        cmp byte [D_L], 0                               ; compares d to 0
        jg .greater_y                                   ; if greater, jumps to the if statement
        jmp .else_greater_y                             ; if less, jumps to the else statement

        mov bh, [dy_L]                                  ; moves dy into a temporary register
        .greater_y:                                     ; if label
            mov bl, [yi_L]                              ; moves yi into a temporary register
            add [y_L], bl                               ; increments y by the slope
            sub bh, [dx_L]                              ; dy and dx
            add bh, bh                                  ; multiplies bh by 2
            add [D_L], bh                               ; adds bh to D
            jmp .after_greater_y                        ; jumps to after the if statement
        .else_greater_y:                                ; else label
            add bh, bh                                  ; multiplies bh by 2
            add [D_L], bh                               ; adds bh to D
        .after_greater_y:                               ; after teh if statement

        inc ah                                          ; increments the loop variable
        cmp ah, al                                      ; checks to see if the loop should end
        je .end_loop_x                                  ; if it ended jump to the end of teh loop
        jmp .loop_x                                     ; if not, jump back to the start of the loop

    .end_loop_x:                                        ; place to send the program when the loop ends

    ret                                                 ; returns to where it was called


section .data                                           ; data segmant
dx_L: db 0                                              ; used for drawing lines
dy_L: db 0                                              ; ^
yi_L: db 0                                              ; ^
xi_L: db 0                                              ; ^
D_L: db 0                                               ; ^
y_L: db 0                                               ; ^
x_L: db 0                                               ; ^


section .text                                           ; code segmant
; boot the OS
times 510-($-$$) db 0                                   ; fills up bootloader space with empty bytess
db 0x55, 0xaa                                           ; defines the bootloader bytes
Gpopcorn
  • 13
  • 1
  • 5
  • 1
    I cannot check your code right now, but one way I use to avoid the mess of debugging assembly is: first, right the whole thing in C until it works; then, transform the C to assembly-like C (no `if-else`, `for`, etc.; only use goto; one operation at a line); If the assembly-like C runs fine, then finally translate line-by-line from this pseudo-assembly to real assembly. I think it is faster to get working code this way than trying to write straight in assembly from start, if the routine is not trivial. – xiver77 Mar 08 '22 at 07:14
  • 1
    Also, it would be nice to explain how to run your code on QEMU (or maybe DOSBox?), so that people can test your code. – xiver77 Mar 08 '22 at 07:16

1 Answers1

2
  1. I see no video mode

    just 80x25 text VGA mode (mode = 3) you set at start with cls so how can you render points? You should set the video mode you want assuming VGA or VESA/VBE see the link above.

  2. why to heck use VGA BIOS for point rendering?

    that will be slooooooooow and I have no idea what it does when no gfx mode is present. You can render points by direct access to VRAM (at segment A000h) Ideal use 8/16/24/32bit video modes as they have pixels aligned to BYTEs ... my favorite is 320x200x256c (mode = 19) as it fits into 64K segment so no paging is needed and pixels are Bytes.

    In case you are using characters instead of pixels then you still can use access to VRAM in the same way just use segment B800h and chars are 16 bit (color and ASCII).

  3. integer DDA is faster then Bresenham on x86 CPUs since 80386

    I do not code in NASM for around 2 decades and closest thing to line I found in my archive is this:

     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     line:   pusha       ;ax=x0,bx=x1,dl=y0,dh=y1,cl=col
         push    ax      ;expecting ES = A000h
         mov si,bx
         sub si,ax
         sub ah,ah
         mov al,dl
         mov bx,ax
         mov al,dh
         sub ax,bx
         mov di,ax
         mov ax,320
         sub dh,dh
         mul dx
         pop bx
         add ax,bx
         mov bp,ax
         mov ax,1
         mov bx,320
         cmp si,32768
         jb  .r0
         neg si
         neg ax
     .r0:    cmp di,32768
         jb  .r1
         neg di
         neg bx
     .r1:    cmp si,di
         ja  .r2
         xchg    ax,bx
         xchg    si,di
     .r2:    mov [.ct],si
     .l0:    mov [es:bp],cl
         add bp,ax
         sub dx,di
         jnc .r3
         add dx,si
         add bp,bx
     .r3:    dec word [.ct]
         jnz .l0
         popa
         ret
     .ct:    dw  0
     ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
    

    So you have something to cross check (took me a while to find it in my archives as I coded whole 3D polygonal engines with textures at that time so I do not have much 2D code in NASM...)

    The example expects 320x200x256c VGA video mode

  4. If you were writing a DOS .com file, things would be a bit simpler: segment registers would all be set the same, with your code/data at offset 100 from them. And you could end with ret.

    [BITS 16]
    [ORG 100h]
    
    [SEGMENT .text]
        ret
    

As @bitRAKE and @PeterCoders pointed out in case You run this in BOOT SECTOR the org is ok. However in such case there is no OS present so if you were going to do more with the stack or any other block of memory outside your 512 bytes, you'd want to point the stack to somewhere known. (It does start out valid, though, because interrupts are enabled.)

More importantly, you need to initialize DS to match your ORG setting, so ds:org reaches a linear address of 7C00. With org 0x7c00, that means you want DS=0. Otherwise instructions like mov [dx_L], bh would be using memory at some unknown location.

    [BITS 16]
    [ORG 7C00h]
    
    [SEGMENT .text]
        mov ax,0000h
        mov ds,ax          ; DS=0 to match ORG
        mov ss,ax          ; if you set SS:SP at all, do it back-to-back
        mov sp,7C00h       ; so an interrupt can't fire half way through.
        ; here do your stuff
    l0: 
      hlt      ; save power
      jmp l0  
  1. Hope you are using VC or NC configured as IDE for NASM and not compiling/linking manually

    This one is usable in MS-DOS so if you are running BOT SECTOR you out of luck. Still You can create a *.com executable debug and once its working in dos change to BOOT SECTOR...

    see Is there a way to link object files for DOS from Linux? on how to setup MS-DOS Volkov commander to automatically compile and link your asm source code just by hitting enter on it ... You can also run it just by adding line to the vc.ext line ... but I prefer not to so you can inspect error log first

  2. Convenient debugging

    You can try to use MS-DOS (DOSBox) with ancient Borland Turbo C/C++ or Pascal and use their inline asm { .... } code which can be traced and stepped directly in the IDE. However it uses TASM (different syntax to NASM) and have some restrictions ...

    Sadly I never saw any decent IDE for asm on x86 platform. The best IDE for asm I worked with was Herkules on ZX Spectrum ... was possible to done things even modern C++ IDEs doesnt have.

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • 1
    He's executing directly from the boot sector - no OS. So, machine state can be configured by QEMU for graphics to be active. Nice example, btw. – bitRAKE Mar 08 '22 at 08:32
  • @bitRAKE that must be anightmare to deal with ... thx for the info. I think you should add this info to the Question too as its not obvious for those of us not using QEMU ... however bootsector is 512Byte anf the offset is ~31KByte so unless I am missing something its still wrong ... Also (s)he uses `cls` at start which switch to text mode so even if the vide omode was set before its gone before `line` is called. Also if no OS then I think stack should be configured too otherwise it points to god knows where – Spektre Mar 08 '22 at 08:41
  • 1
    Are you talking about the `org 0x7c00`? The legacy BIOS MBR ABI guarantees your code gets loaded at that linear address, with either CS:IP=0000:7c00 or 0x07c0:0000. (And not many other guarantees in terms of reg / mem state, apparently, and even the existence of both options is due to some BIOS vendors misinterpreting the original standard I think, if it ever existed on paper.) See [Boot loader doesn't jump to kernel code](https://stackoverflow.com/q/32701854) for Michael Petch's bootloader dev tips if you're curious about this legacy stuff that's mostly replaced by UEFI these days. – Peter Cordes Mar 08 '22 at 10:20
  • 1
    Another thing that's guaranteed AFAIK at the start of a legacy BIOS MBR is that SS:SP points somewhere usable for small amounts of storage. You don't know where, so you should set it if you want to use any other memory at a fixed address (e.g. to load more sectors), but it's fully usable for making BIOS calls and external handling interrupts, because they're enabled. And it's fine to leave CS:IP alone. But yes, you *do* need to set ES and/or DS (to match your ORG) if you use them at all, though, like this code does for static data such as `[dx_L]`. – Peter Cordes Mar 08 '22 at 10:59
  • Your example starting with `mov ax,0000h ; the same as cs` has 2 bugs: 1st, that's not necessarily the same as CS. You don't care what CS:IP addresses your boot sector, you just want DS to match your `org`, i.e. `org 0x7c00` means that NASM will assume the first byte of the file is at that offset, so to reach the right linear address you need `0000:7C00`. The other bug is ending with `ret`. There's nothing to return to, no return address on the stack, and no OS to "exit" to. The normal thing is to put `hlt` inside an infinite loop. – Peter Cordes Mar 08 '22 at 11:04
  • @PeterCordes hmm youre right ... can I move the cs though ax? `mov ax,cs; mov ss,ax` ? cant remember if such instructon is possible I just remember vaguely I can not load segment register directly – Spektre Mar 08 '22 at 11:07
  • Actually also a 3rd bug: `mov sp, ax` is not the next instruction after `mov` to SS. `mov` to SS only temporarily delays interrupts until after the end of the next instruction. (Early 8086 steppings had a bug where they didn't delay interrupts at all, apparently. But that's easily fixable: use `mov sp, 0x7c00` to set the top-of-stack to right below your code. If you can't remember how push/pop work in x86 asm, maybe don't try to give bootloader tips. – Peter Cordes Mar 08 '22 at 11:08
  • @Spektre: Yes, you could `mov ax, cs`, but you don't want to. You don't know whether CS is 0 or 07C0. That was my point earlier, you don't care how CS:IP reaches your code, you only care about setting DS to match your ORG, because that's what governs how NASM fills in the [disp16] for absolute addressing modes like [symbol]. So you want `xor ax,ax` / `mov ss, ax` / `mov sp, 0x7c00`. – Peter Cordes Mar 08 '22 at 11:09
  • @PeterCordes so Stack goes downwards ... and the space before 0x7C00 is usable? does not contain any stuff ? – Spektre Mar 08 '22 at 11:09
  • IIRC yes, I think I've seen that recommendation as a place to put your stack in SO answers by people like Michael Petch, Fuz and/or others who apparently still play around with legacy 16-bit MBRs sometimes and know what they're doing. I only know about them from reading SO answers. The bios data area is apparently somewhere else. – Peter Cordes Mar 08 '22 at 11:12
  • IMO there's no point to moving SS:SP for this, though; like I said it's valid and has enough space to invoke `int 0xwhatever`. What you need to do is set DS (to zero to match your ORG), which your example doesn't. – Peter Cordes Mar 08 '22 at 11:14
  • @PeterCordes why not the same as `cs` ? I do not think `ds` is relative to `cs` ... feel free to correct it – Spektre Mar 08 '22 at 11:14
  • @PeterCordes or it is 32bit linear addressing like in protected mode? Never coded that ... but I think its not the case as it used `[BITS 16]` ... and assuming real mode is set after reset – Spektre Mar 08 '22 at 11:29
  • When your MBR is loaded at linear address 0x7C00 (which is always guaranteed), and you want `[7C45]` or whatever to address the byte at `dx_L: db 0`, you need DS=0, regardless of whether the instruction using that absolute address is running from CS:IP = `07C00:0012` or from `0000:7C12`. The valid options for addressing a specific byte of (linear) physical address space are independent of how some other logical address is constructed. You need to set DS to match ORG, not how the BIOS set CS:IP before jumping to your code. – Peter Cordes Mar 08 '22 at 11:30
  • @PeterCordes aaaaahhh that is what I was missing I forgot that segment register and index register overlaps... however I always thought that `[org 0x7c00]` in NASM refers to address inside local code segment and not to direct linear address... but I always coded just up to 64K stuff – Spektre Mar 08 '22 at 11:33
  • 1
    `org 0x7c00` *is* an offset. You're telling NASM that you want something like `mov ax, [_start]` to get assembled to machine code using `[disp16=7C00]`, not `[disp16 = 0]`. That addressing mode implicitly uses DS, so if you also set DS=0 then logical addresses using it will have offset = linear. The other common option for a bootloader is to set DS=07C0 and use `org 0`. The point of `org` is to tell NASM what value to use any time the absolute offset of a symbol is needed, whether for a far jump or for a load/store. Relative branches are relative so they still just work. – Peter Cordes Mar 08 '22 at 11:37