Discussion:
Why is this assembly program not quitting on ESC?
(too old to reply)
Johann 'Myrkraverk' Oskarsson
2019-10-26 22:43:42 UTC
Permalink
Dear c.o.m.programmer and c.l.a.x86,

[Note: crossposted to c.o.m.p and c.l.a.x]

I am working my way through this 16bit assembly tutorial,

https://github.com/adamsmasher/sokobanDOS/tree/master/lesson2

except that I'm using the OpenWatcom assembler, wasm.

As far as I know, I've translated the nasm syntax to wasm correctly;
which is incidentally pretty similar to masm. And from reading the
code, I'm fairly certait *it should work* but doesn't. When I run it in
DOSBox-X, it doesn't quit when I press escape.

What am I missing here? I apologize for the lengthy code, it's the
complete tutorial lesson 2.

I'm compiling it like so,

wasm dostut2.asm
wlink sys dos com file dostut2.obj

and then running it in DOSBox-X. I'm fairly certain the problem is with
my code, and not in DOSBox-X.

Thank you.


;; file dostut2.asm
.286 ; we don't use bits 16 in wasm
.model tiny

.code
org 100h ; dos loads here
main proc
call installkb
call initvideo
gameloop:
call waitframe
cmp byte ptr [quit], 1
jne gameloop

call restorevideo
call restorekb

;; exit
mov ah, 4ch
mov al, 00h
int 21h
main endp

quit: db 0

include video.asm
include kb.asm
end
;; end of file: dostut2.asm

;;-----------------------------------------------------------------
;; file: video.asm
waitframe proc
push dx
;; port 0x3da contains vga status
mov dx, 03dah
waitretrace:
in al, dx ; read from port 0x03da into al
;; bit 3 will be on if we're in retrace
test al, 08h ; are we in retrace?
jnz waitretrace

endrefresh:
in al, dx
test al, 0x08 ; are we in refresh?
jz endrefresh

pop dx
ret
waitframe endp

initvideo proc
;; set mode to 0x13
mov ax, 13h
int 10h
ret
initvideo endp

restorevideo proc
;; return to text mode 0x03
mov ax, 03h
int 10h
ret
restorevideo endp
;; end of file: video.asm

;;-----------------------------------------------------------------
;; file: kb.asm
oldkbhandler: dw 0
oldkbseg: dw 0

installkb proc
push es
push bx
push dx
;; backup old kb interrupt
mov ax, 3509h
int 21h
mov [oldkbhandler], bx
mov [oldkbseg], es
;; install new kb interrupt
mov ah, 23h
mov dx, kbhandler
int 21h
pop dx
pop bx
pop es
ret
installkb endp

restorekb proc
push dx
push ds
mov ax, 2509h
mov dx, [oldkbhandler]
mov ds, [oldkbseg]
int 21h
pop ds
pop dx
ret
restorekb endp

kbhandler proc
push ax
in al, 60h ; get key event
cmp al, 01h ; esc pressed?
jne done ; if this is .done then wasm silently
mov byte ptr [quit], al ; doesn't create the object file.
done: mov al, 20h ; ack
out 20h, al ; send ack
pop ax
iret
kbhandler endp
;; end of file: kb.asm
--
Johann | email: invalid -> com | www.myrkraverk.com/blog/
I'm not from the Internet, I just work there. | twitter: @myrkraverk
Ross Ridge
2019-10-29 02:16:15 UTC
Permalink
Post by Johann 'Myrkraverk' Oskarsson
Dear c.o.m.programmer and c.l.a.x86,
[Note: crossposted to c.o.m.p and c.l.a.x]
I just noticed the follow up I made to your post was sent to a moderated
newsgroup. As I never intended that, and I have no idea whether it will
be accepted or not, I'm reposting it to just the unmoderated group.
Post by Johann 'Myrkraverk' Oskarsson
;; install new kb interrupt
mov ah, 23h
mov dx, kbhandler
int 21h
The MS-DOS service for setting interrupts is 25h instead of 23h.
Post by Johann 'Myrkraverk' Oskarsson
kbhandler proc
push ax
in al, 60h ; get key event
cmp al, 01h ; esc pressed?
jne done ; if this is .done then wasm silently
mov byte ptr [quit], al ; doesn't create the object file.
DS could be anything at this point. Very likely its going to be the
DS value you want because it's your own code that's being interrupted.
In theory though it could be code in some other interrupt handler being
interrupted.

In an interrupt handler the only segment register you can depend on is
CS, and since this a tiny model program it happens to contain same value
as your program's DS. You could reload DS from CS, but it's better to
just use a segment override:

mov byte ptr cs:[quit], al

If your interrupt handle happens to have a lot of data variable acceses,
you could also undo the implicit segment assumptions created by the
".model tiny" directive:

kbhandler proc
ASSUME DS:NOTHING, ES:NOTHING, SS:NOTHING
push ax
in al, 60h ; get key event
cmp al, 01h ; esc pressed?
jne done ; if this is .done then wasm silently
mov byte ptr [quit], al ; doesn't create the object file.

This will cause the assembler to generate a CS segment override to access
"quit" because it doesn't know of any other segment register that can
reach it.

Finally I don't know if this is a problem with the Watcom tools or not,
but you should specify "main" as your entry point with the END directive:

end main

Without it the .COM file that Microsoft linker generates will have 256
bytes of zeros at the start. This is because the "org 100h" directive
actually creates 256 bytes of zeros and the Microsoft linker uses the
start address to know what should be cut off from the start of the
generated file.
--
l/ // Ross Ridge -- The Great HTMU
[oo][oo] ***@csclub.uwaterloo.ca
-()-/()/ http://www.csclub.uwaterloo.ca/~rridge/
db //
Johann 'Myrkraverk' Oskarsson
2019-10-29 08:01:48 UTC
Permalink
Post by Ross Ridge
Post by Johann 'Myrkraverk' Oskarsson
Dear c.o.m.programmer and c.l.a.x86,
[Note: crossposted to c.o.m.p and c.l.a.x]
I just noticed the follow up I made to your post was sent to a moderated
newsgroup. As I never intended that, and I have no idea whether it will
be accepted or not, I'm reposting it to just the unmoderated group.
Thank you.
Post by Ross Ridge
Post by Johann 'Myrkraverk' Oskarsson
;; install new kb interrupt
mov ah, 23h
mov dx, kbhandler
int 21h
The MS-DOS service for setting interrupts is 25h instead of 23h.
Yes. I've fixed this by now.
Post by Ross Ridge
Post by Johann 'Myrkraverk' Oskarsson
kbhandler proc
push ax
in al, 60h ; get key event
cmp al, 01h ; esc pressed?
jne done ; if this is .done then wasm silently
mov byte ptr [quit], al ; doesn't create the object file.
DS could be anything at this point. Very likely its going to be the
DS value you want because it's your own code that's being interrupted.
In theory though it could be code in some other interrupt handler being
interrupted.
In an interrupt handler the only segment register you can depend on is
CS, and since this a tiny model program it happens to contain same value
as your program's DS. You could reload DS from CS, but it's better to
mov byte ptr cs:[quit], al
Is this different from writing byte ptr [cs:quit], al? I've made this
change now.
Post by Ross Ridge
If your interrupt handle happens to have a lot of data variable acceses,
you could also undo the implicit segment assumptions created by the
kbhandler proc
ASSUME DS:NOTHING, ES:NOTHING, SS:NOTHING
push ax
in al, 60h ; get key event
cmp al, 01h ; esc pressed?
jne done ; if this is .done then wasm silently
mov byte ptr [quit], al ; doesn't create the object file.
This will cause the assembler to generate a CS segment override to access
"quit" because it doesn't know of any other segment register that can
reach it.
Finally I don't know if this is a problem with the Watcom tools or not,
end main
This is simpler than having "public main" and telling the linker main is
the starting address.
Post by Ross Ridge
Without it the .COM file that Microsoft linker generates will have 256
bytes of zeros at the start. This is because the "org 100h" directive
actually creates 256 bytes of zeros and the Microsoft linker uses the
start address to know what should be cut off from the start of the
generated file.
Yes, you can see my post (reposted just now) where I was struggling with
exactly this. Figuring out how to work around this problem was a little
bit painful.
--
Johann | email: invalid -> com | www.myrkraverk.com/blog/
I'm not from the Internet, I just work there. | twitter: @myrkraverk
Ross Ridge
2019-10-29 17:57:46 UTC
Permalink
Post by Ross Ridge
Without it the .COM file that Microsoft linker generates will have 256
bytes of zeros at the start. This is because the "org 100h" directive
actually creates 256 bytes of zeros and the Microsoft linker uses the
start address to know what should be cut off from the start of the
generated file.
Yes, you can see my post (reposted just now) where I was struggling with
exactly this. Figuring out how to work around this problem was a little
bit painful.
Yah, the ORG directive and start address work together both account for
the fact the first byte of the contents of .COM file are loaded into
memory at offset 0100h. It would be much simpler if .COM files loaded
at offset 0, but the first 256 bytes of the segment are reserved for
the program segment prefix (PSP).

.COM files are simple binary files without any metadata whatsoever.
MS-DOS allocates a segment in memory for them and then simply loads the
contents of the file without modification into that segment starting
at offset 0100h. This means that the first byte of the .COM is loaded
into memory at offset 0100h. It then sets the segment registers to all
point to that segment and jumps to XXXX:0100 where XXXX is the segment
it created for the program.

By using "org 0100h" at the start of your program you tell the linker
the first byte of your program will be loaded at offset 0100h. It can
then adjust the absolute references in your code so they reflect the
offsets in memory where your program will be loaded. However since
segments can really only start at offset 0, it does this by simply
inserting 256 (0100h) zero bytes at the start of the segment, moving
what you want the start of your program to offset 0100h.

However this would mean that the actual start of your program, as stored
on disk, would be those 256 zero bytes. When MS-DOS loads your .COM it
would put the 256 zero bytes at offset 0100h and the start of your code
and intended first byte of your program would be loaded at offset 0200h.
The absolute references in your code will still assume that your code
was loaded at offset 0100h.

Your code would end up looking like this to the CPU:

100: 00 00 add BYTE PTR [bx+si],al
102: 00 00 add BYTE PTR [bx+si],al
...
1fc: 00 00 add BYTE PTR [bx+si],al
1fe: 00 00 add BYTE PTR [bx+si],al
200: e8 3a 00 call 0x23d
203: e8 27 00 call 0x22d
206: e8 14 00 call 0x21d
209: 80 3e 1c 01 01 cmp BYTE PTR ds:0x11c,0x1
20e: 75 f6 jne 0x206
...

Note that since the call and jump instructions use relative addressing,
as you can see by the encoding, they all reflect where the code would
be actually loaded into memory. It's the reference to "quit" in the
CMP instruction that's wrong, and refers to a location in all those
zero bytes. But this isn't actually a problem, since the code in the
interrupt handler will also use address ds:0x11c, and this byte happens
to be also initialized to zero.

The real problem shows up when setting the interrupt vector:

240: b8 09 35 mov ax,0x3509
243: cd 21 int 0x21
245: 89 1e 39 01 mov WORD PTR ds:0x139,bx
249: 8c 06 3b 01 mov WORD PTR ds:0x13b,es
24d: b4 25 mov ah,0x25
24f: ba 6a 01 mov dx,0x16a
252: cd 21 int 0x21

It sets the address of the keyboard interrupt handler to somewhere in
the middle of those zeroes rather than your intended handler.

To fix the problem you just need to tell the linker where you actually
want the entry point of your program to be and it'll cut off everything
before it. Your intended entry point becomes the first byte of the file
on disk which becomes the first byte MS-DOS loads to offset 0100h.

Now you may be wondering why all this is necessary. After all .COM
files always load at at offset 0100h and so assembler and linker could
just assume this when you're building a .COM file. The reason, at least
with the Microsoft tools, is because simple plain binary files can be
used for other things.

For example, MS-DOS supports a variant of .COM files called .SYS files,
installable drivers loaded with DEVICE= in CONFIG.SYS, and these .SYS
files are loaded at offset 0. Drivers don't have their own PSP, so
there's no need to make space for it like with .COM files. (Note that
.SYS files can also be MZ format executes, in which case they're loaded
normally as indicated by the MZ format metadata.) A bootsector can
use an load offset of 0, though some authors prefer to use an offset
0x7c00 depending on how they set DS. When creating custom firmware,
like for an option boot ROM, you probably also want to use an offset of 0.
--
l/ // Ross Ridge -- The Great HTMU
[oo][oo] ***@csclub.uwaterloo.ca
-()-/()/ http://www.csclub.uwaterloo.ca/~rridge/
db //
Johann 'Myrkraverk' Oskarsson
2019-10-31 10:14:08 UTC
Permalink
Post by Ross Ridge
Post by Ross Ridge
Without it the .COM file that Microsoft linker generates will have 256
bytes of zeros at the start. This is because the "org 100h" directive
actually creates 256 bytes of zeros and the Microsoft linker uses the
start address to know what should be cut off from the start of the
generated file.
Yes, you can see my post (reposted just now) where I was struggling with
exactly this. Figuring out how to work around this problem was a little
bit painful.
Yah, the ORG directive and start address work together both account for
the fact the first byte of the contents of .COM file are loaded into
memory at offset 0100h. It would be much simpler if .COM files loaded
at offset 0, but the first 256 bytes of the segment are reserved for
the program segment prefix (PSP).
.COM files are simple binary files without any metadata whatsoever.
MS-DOS allocates a segment in memory for them and then simply loads the
contents of the file without modification into that segment starting
at offset 0100h. This means that the first byte of the .COM is loaded
into memory at offset 0100h. It then sets the segment registers to all
point to that segment and jumps to XXXX:0100 where XXXX is the segment
it created for the program.
By using "org 0100h" at the start of your program you tell the linker
the first byte of your program will be loaded at offset 0100h. It can
then adjust the absolute references in your code so they reflect the
offsets in memory where your program will be loaded. However since
segments can really only start at offset 0, it does this by simply
inserting 256 (0100h) zero bytes at the start of the segment, moving
what you want the start of your program to offset 0100h.
However this would mean that the actual start of your program, as stored
on disk, would be those 256 zero bytes. When MS-DOS loads your .COM it
would put the 256 zero bytes at offset 0100h and the start of your code
and intended first byte of your program would be loaded at offset 0200h.
The absolute references in your code will still assume that your code
was loaded at offset 0100h.
100: 00 00 add BYTE PTR [bx+si],al
102: 00 00 add BYTE PTR [bx+si],al
...
1fc: 00 00 add BYTE PTR [bx+si],al
1fe: 00 00 add BYTE PTR [bx+si],al
200: e8 3a 00 call 0x23d
203: e8 27 00 call 0x22d
206: e8 14 00 call 0x21d
209: 80 3e 1c 01 01 cmp BYTE PTR ds:0x11c,0x1
20e: 75 f6 jne 0x206
...
Note that since the call and jump instructions use relative addressing,
as you can see by the encoding, they all reflect where the code would
be actually loaded into memory. It's the reference to "quit" in the
CMP instruction that's wrong, and refers to a location in all those
zero bytes. But this isn't actually a problem, since the code in the
interrupt handler will also use address ds:0x11c, and this byte happens
to be also initialized to zero.
240: b8 09 35 mov ax,0x3509
243: cd 21 int 0x21
245: 89 1e 39 01 mov WORD PTR ds:0x139,bx
249: 8c 06 3b 01 mov WORD PTR ds:0x13b,es
24d: b4 25 mov ah,0x25
24f: ba 6a 01 mov dx,0x16a
252: cd 21 int 0x21
It sets the address of the keyboard interrupt handler to somewhere in
the middle of those zeroes rather than your intended handler.
To fix the problem you just need to tell the linker where you actually
want the entry point of your program to be and it'll cut off everything
before it. Your intended entry point becomes the first byte of the file
on disk which becomes the first byte MS-DOS loads to offset 0100h.
Now you may be wondering why all this is necessary. After all .COM
files always load at at offset 0100h and so assembler and linker could
just assume this when you're building a .COM file. The reason, at least
with the Microsoft tools, is because simple plain binary files can be
used for other things.
For example, MS-DOS supports a variant of .COM files called .SYS files,
installable drivers loaded with DEVICE= in CONFIG.SYS, and these .SYS
files are loaded at offset 0. Drivers don't have their own PSP, so
there's no need to make space for it like with .COM files. (Note that
.SYS files can also be MZ format executes, in which case they're loaded
normally as indicated by the MZ format metadata.) A bootsector can
use an load offset of 0, though some authors prefer to use an offset
0x7c00 depending on how they set DS. When creating custom firmware,
like for an option boot ROM, you probably also want to use an offset of 0.
Think you for the detailed explanation. I'll try to write up a summary
(even if only for myself) about how all this works.
--
Johann | email: invalid -> com | www.myrkraverk.com/blog/
I'm not from the Internet, I just work there. | twitter: @myrkraverk
Ross Ridge
2019-10-29 18:24:38 UTC
Permalink
Post by Johann 'Myrkraverk' Oskarsson
quit: db 0
One other thing I would note is that this is invalid MASM syntax. Presumbly
this works with Watcom assembler, but with MASM you should write:

quit db 0

That is, leave out the colon (:). I'd recommend making this change even
if you don't care whether you code assembles with MASM, as it apparently
Post by Johann 'Myrkraverk' Oskarsson
mov byte ptr [quit], al ; doesn't create the object file.
MASM rejects your code because when a symbol is defined with a colon
like this the symbol is given type NEAR, which conflicts with the DB
directive which defines symbols of type BYTE. I'm guessing the Watcom
is resolving this conflict by defining "quit" as a NEAR symbol. If it
was defined as a BYTE symbol you could leave the "byte ptr" part out:

mov cs:[quit], al

(I've also added the CS override necessary because this code is in your
interrupt handler. And, yes, you could write [cs:quit] instead.)
Post by Johann 'Myrkraverk' Oskarsson
oldkbhandler: dw 0
oldkbseg: dw 0
...
Post by Johann 'Myrkraverk' Oskarsson
mov [oldkbhandler], bx
mov [oldkbseg], es
is because the NEAR type the same same size as the WORD type.

None of this affects the correctness of your code, but leaving out the
colons in the future should save you some typing.
--
l/ // Ross Ridge -- The Great HTMU
[oo][oo] ***@csclub.uwaterloo.ca
-()-/()/ http://www.csclub.uwaterloo.ca/~rridge/
db //
Johann 'Myrkraverk' Oskarsson
2019-10-31 10:17:54 UTC
Permalink
Post by Ross Ridge
Post by Johann 'Myrkraverk' Oskarsson
quit: db 0
One other thing I would note is that this is invalid MASM syntax. Presumbly
quit db 0
That is, leave out the colon (:). I'd recommend making this change even
if you don't care whether you code assembles with MASM, as it apparently
Post by Johann 'Myrkraverk' Oskarsson
mov byte ptr [quit], al ; doesn't create the object file.
MASM rejects your code because when a symbol is defined with a colon
like this the symbol is given type NEAR, which conflicts with the DB
directive which defines symbols of type BYTE. I'm guessing the Watcom
is resolving this conflict by defining "quit" as a NEAR symbol. If it
mov cs:[quit], al
(I've also added the CS override necessary because this code is in your
interrupt handler. And, yes, you could write [cs:quit] instead.)
Post by Johann 'Myrkraverk' Oskarsson
oldkbhandler: dw 0
oldkbseg: dw 0
...
Post by Johann 'Myrkraverk' Oskarsson
mov [oldkbhandler], bx
mov [oldkbseg], es
is because the NEAR type the same same size as the WORD type.
None of this affects the correctness of your code, but leaving out the
colons in the future should save you some typing.
Thanks again for the detailed explanation.
--
Johann | email: invalid -> com | www.myrkraverk.com/blog/
I'm not from the Internet, I just work there. | twitter: @myrkraverk
Loading...