10
\$\begingroup\$

I am trying to learn the 8086 assembly language. So as exercise, I decided to write a hexdump program, that outputs the contents of files in a format similar to Linux hexdump.

I am developing under Concurrent CP/M 86 inside an emulator.

My version of hexdump reads a file name from command line and print the contents on the console. I don't have any experience with the assembly language, so I'm trying to find ways to improve. I still don't know how to structure code and name labels and variables.

ccpmint equ 224
p_termcpm equ 0
c_read equ 1
c_write equ 2
c_writestr equ 9
f_open equ 15
f_close equ 16
f_read equ 20
f_dmaoff equ 26

cseg

start:

    ; copy filename to fcb
    mov cx, 0
    mov di, offset fcb
    mov si, 05ch
fcbloop:
    cmp cx, 16
    jz  fcbend
    mov dl, [si]
    mov [di], dl
    inc si
    inc di
    inc cx
    jmp fcbloop
fcbend:

    ; set current dma to dma in dseg
    mov cl, f_dmaoff
    mov dx, offset dma
    call ccpm

    ; open the file
    mov cl, f_open
    mov dx, offset fcb
    call ccpm
    cmp al, 0
    jnz error

    ; read the file
    mov readcnt, 0
read_loop:
    mov cl, f_read
    mov dx, offset fcb
    call ccpm
    cmp al, 0
    jnz read_end

    mov ax, readcnt
    inc ax
    mov readcnt, ax
    mov cl, c_writestr
    mov dx, offset crlf
    call ccpm
    call print_dma
    mov cl, c_read
    call ccpm
    jmp read_loop
read_end:
    cmp al, 01h
    jnz error

    ; close the file
    mov cl, f_close
    mov dx, offset fcb
    call ccpm
    cmp al, 0
    jnz error

terminate:
    ; terminate the program
    mov cl, p_termcpm
    mov dx, 0
    call ccpm

error:
    ; error handling
    mov tmp, ax
    mov cl, c_writestr
    mov dx, offset errmsg
    int ccpmint
    mov al, last_cl
    call print_byte
    mov cl, c_writestr
    mov dx, offset erral
    int ccpmint
    mov ax, tmp
    call print_byte
    mov cl, c_writestr
    mov dx, offset errah
    int ccpmint
    mov ax, tmp
    mov al, ah
    call print_byte
    mov cl, c_writestr
    mov dx, offset crlf
    int ccpmint
    jmp terminate

print_dma:
    ; fill lines of 16 bytes and prints each line
    mov cx, 0
    mov si, offset dma
    mov di, offset buf
pdmaloop:
    mov ax, [si]
    mov [di], ax
    inc cx
    inc si
    inc di
    mov ax, 0fh
    and ax, cx
    jnz pdmaloop
    call print_line
    cmp cx, 128
    jz pdmaend
    mov di, offset buf
    jmp pdma_loop
pdmaend:
    ret


print_line:
    ; prints a single dma line: address, bytes, chars
    ; address = readcnt*128 + cx
    push cx
    mov dx, cx
    mov ax, readcnt
    mov cl, 7
    shl ax, cl
    add ax, dx
    call print_word
    mov cl, c_write
    mov dl, 32
    call ccpm

    ; print bytes
    mov di, offset buf
    mov cx, 0
plbloop:
    mov tmp, cx
    mov al, [di]
    call print_byte
    mov cl, c_write
    mov dl, 32
    call ccpm
    mov cx, tmp
    inc cx
    inc di
    cmp cx, 16
    jnz plbloop

    ; print chars
    mov di, offset buf
    mov cx, 0
plcloop:
    mov tmp, cx
    mov al, [di]
    cmp al, 32
    jb plcpdot
    cmp al, 126
    jbe plcpchr
plcpdot:
    mov al, '.'
plcpchr:
    mov cl, c_write
    mov dl, al
    call ccpm
    mov cx, tmp
    inc di
    inc cx
    cmp cx, 16
    jb plcloop

    ; write crlf and return
    mov cl, c_writestr
    mov dx, offset crlf
    call ccpm
    pop cx
    ret

print_word:
    ; prints a word: the highest byte, then the lowest
    push ax
    mov al, ah
    call print_byte
    pop ax
    call print_byte
    ret

print_byte:
    ; prints the two characters of a byte in al
    push ax
    and al, 0f0h
    mov cl, 4
    shr al, cl
    call print_nibble
    pop ax
    and al, 0fh
    call print_nibble
    ret

print_nibble:
    ; prints a single hexadecimal digit
    cmp al, 9
    jg pnib_af
    add al, '0'
    jmp pnib_w
pnib_af:
    add al, 'A'-10
pnib_w:
    mov cl, c_write
    mov dl, al
    int ccpmint
    ret

ccpm:
    mov last_cl, cl
    int ccpmint
    ret

dseg
org 100h

errmsg  db  'error: function=$'
erral   db  ', al=$'
errah   db  ', ah=$'
crlf    db  10,13,'$'
fcb rs  33
dma rs  180
buf rs  16
tmp rw  1
readcnt rw  1
last_cl db  0
end

This is how it's compiled: enter image description here

And here is what it looks like when executed: enter image description here enter image description here

\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

I'm not familiar with Concurrent CP/M 86 but I do know my 8086.

...
dseg
org 100h
...

I found it remarkable to see the org 100h directive so far down in your program. I would have expected this almost at the first source line!

I still don't know how to structure code and name labels and variables.

Apart from my remark about ORG these are actually quite good.
You used descriptive names and there's a good ordering of the subroutines.


mov dx, 0

Each time you clear a register (any general purpose register!), you can do this more efficiently by XOR ing the register with itself.

xor dx, dx    ;Will set DX=0

   ; copy filename to fcb
    mov cx, 0
    mov di, offset fcb
    mov si, 05ch
fcbloop:
    cmp cx, 16
    jz  fcbend
    mov dl, [si]
    mov [di], dl
    inc si
    inc di
    inc cx
    jmp fcbloop
fcbend:

The part where you copy the filename can be much improved by using the movs string primitive instruction. You load the desired amount in CX and then repeat (rep) the operation. The direction flag (DF) is cleared beforehand to force the CPU updating the SI and DI registers moving up in memory.

; copy filename to fcb
mov cx, 16
mov di, offset fcb
mov si, 05ch
cld
rep movsb

mov ax, readcnt
inc ax
mov readcnt, ax

Incrementing the variable readcnt can be done in a single instruction:

inc readcnt

print_dma:
    ; fill lines of 16 bytes and prints each line
    mov cx, 0
    mov si, offset dma
    mov di, offset buf
pdmaloop:
    mov ax, [si]
    mov [di], ax
    inc cx
    inc si
    inc di
    mov ax, 0fh
    and ax, cx
    jnz pdmaloop
    call print_line
    cmp cx, 128
    jz pdmaend
    mov di, offset buf
    jmp pdma_loop
pdmaend:
    ret

This procedure must process bytes (you raise the addresses by 1), but you read and write words!
The test to see if 16 bytes were processed is too complicated. All that is needed is testing CX with 15.

print_dma:
    ; fill lines of 16 bytes and prints each line
    xor cx, cx
    mov si, offset dma
pdmaloop_:
    mov di, offset buf
pdmaloop:
    movsb
    inc cx
    test cx, 15
    jnz pdmaloop
    call print_line
    cmp cx, 128
    jne pdma_loop_
    ret

See how I reduced the number of instructions by introducing an extra label on top and no longer needing the label at the bottom?


print_word:
    ; prints a word: the highest byte, then the lowest
    push ax
    mov al, ah
    call print_byte
    pop ax
    call print_byte
    ret
print_byte:

A call directly above a ret can safely be replaced by a jmp.
But since the target immediately follows, you can just not write the jmp.

print_word:
    ; prints a word: the highest byte, then the lowest
    push ax
    mov al, ah
    call print_byte
    pop ax
print_byte:

    call print_nibble
    pop ax
    and al, 0fh
    call print_nibble
    ret
print_nibble:

Similarly this snippet becomes:

    call print_nibble
    pop ax
    and al, 0fh
print_nibble:

    cmp al, 9
    jg pnib_af
    add al, '0'
    jmp pnib_w
pnib_af:
    add al, 'A'-10
pnib_w:

A simpler way of converting follows. It uses less labels which is a good thing for readability. It uses less jumps which is a good thing for speed.

    cmp al, 10
    jb  pnib_
    add al, 7      ;7 is ('A' - 10) - '0'
pnib_:
    add al, '0'
\$\endgroup\$
5
  • 2
    \$\begingroup\$ That org 100h is within the data segment. All the stuff before it is in the code segment. \$\endgroup\$ Commented Mar 26, 2018 at 15:39
  • 2
    \$\begingroup\$ Thank you very much. It really helped me. The org 100h in dseg is because in Concurrent CP/M-86 the small memory model uses the first 256 bytes of the data segment for the base page (this is where I copy the file name given as argument from). When the org 100h is in cseg, it's called the 8080 memory model in the Concurrent CP/M documentation. \$\endgroup\$
    – andrei-n
    Commented Mar 26, 2018 at 18:13
  • \$\begingroup\$ Here is the improved version, in case someone would want to test my code, with some bugs corrected and behaviour improved (the user can press q to quit and there is no pause after last screen). \$\endgroup\$
    – andrei-n
    Commented Mar 31, 2018 at 11:47
  • \$\begingroup\$ @andrei Indeed a much improved version! Nice to see that you corrected a few errors that I had overlooked (like those addresses that were out of sync). Concerning the addition of the user response: Allowing the user to quit is a nice feature, but I think it would be best to not jump to the Terminate label but rather to the CloseFile code directly above it. \$\endgroup\$
    – Sep Roland
    Commented Apr 1, 2018 at 13:58
  • \$\begingroup\$ @andrei You could consider posting your improved version as a follow up question on this forum. There's more advice that can be offered... \$\endgroup\$
    – Sep Roland
    Commented Apr 1, 2018 at 14:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.