VOGONS

Common searches


Reply 21 of 39, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

EDIT:
I replaced part of the code initially posted here to fix a mistake.

How does this look?

DEFINT A-Z

TYPE RegTypeX
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
ds AS INTEGER
es AS INTEGER
END TYPE

DECLARE SUB INTERRUPTX (intnum AS INTEGER, inreg AS RegTypeX, outreg AS RegTypeX)

DIM CommandTail AS STRING
DIM FCB1 AS STRING
DIM FCB2 AS STRING
DIM Parameters AS STRING
DIM Program AS STRING
DIM Registers AS RegTypeX

CLS
CommandTail = CHR$(0)
FCB1 = STRING$(37, 0)
FCB2 = STRING$(37, 0)

Parameters = STRING$(2, 0) '00h = Environment.
Parameters = Parameters + MKI$(VARSEG(CommandTail)) + MKI$(SADD(CommandTail))'02h = Command tail.
Parameters = Parameters + MKI$(VARSEG(FCB1)) + MKI$(SADD(FCB1)) '06h = FCB1.
Parameters = Parameters + MKI$(VARSEG(FCB2)) + MKI$(SADD(FCB2)) '0Ah = FCB2.
Parameters = Parameters + STRING$(8, 0) '0Eh + 12h = SS:SP + CS:IP upon return.


Program = "qb.exe" + CHR$(0)

Registers.ax = &H4B00
Registers.ds = VARSEG(Program)
Registers.dx = SADD(Program)
Registers.es = VARSEG(Parameters)
Registers.bx = SADD(Parameters)
INTERRUPTX &H21, Registers, Registers
PRINT "Return code: "; HEX$(Registers.ax)

It works when compiled but still throws an out of memory error from the IDE.

Last edited by Peter Swinkels on 2023-07-09, 08:52. Edited 1 time in total.

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 22 of 39, by pan069

User metadata
Rank Oldbie
Rank
Oldbie
Peter Swinkels wrote on 2023-07-08, 13:07:

It works when compiled but still throws an out of memory error from the IDE.

Which might make sense sense because if you're running it from within the IDE, the IDE itself has allocated a lot of memory (heap), so trying to execute another program in that context that also needs a lot of memory, it won't fit. When you compile into an EXE and then run it, the compiled EXE uses a lot less memory, so it's more likely to execute the external program.

Keep in mind that QB runs in real mode and can only use memory in the 640Kb address space, unless you specially program it to use EMS/XMS.

Reply 23 of 39, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

I would believe the insufficient memory error if the SHELL statement would fail to launch programs too. Which it doesn't.

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 24 of 39, by Jo22

User metadata
Rank l33t++
Rank
l33t++

Hi. Just stumbled over this issue of QB Cult Magazine.
It covers memory in QB45 and XBX.

http://qbcm.hybd.net/issues/1-6/

Not sure if it's helpful in this case. Haven't fully read it yet, since I'm on a mobile device.

Anyway, good luck. 🙂🤞

"Time, it seems, doesn't flow. For some it's fast, for some it's slow.
In what to one race is no time at all, another race can rise and fall..." - The Minstrel

//My video channel//

Reply 25 of 39, by doshea

User metadata
Rank Member
Rank
Member
Peter Swinkels wrote on 2023-07-08, 13:07:
[…]
Show full quote
Parameters = STRING$(2, 0)                                        '00h = Environment.
Parameters = MKI$(VARSEG(CommandTail)) + MKI$(SADD(CommandTail)) '02h = Command tail.
Parameters = MKI$(VARSEG(FCB1)) + MKI$(SADD(FCB1)) '06h = FCB1.
Parameters = MKI$(VARSEG(FCB2)) + MKI$(SADD(FCB2)) '0Ah = FCB2.
Parameters = STRING$(8, 0) '0Eh + 12h = SS:SP + CS:IP upon return.

After the first line of code I quoted above, shouldn't the subsequent ones be doing "Parameters = Parameters + ..."?

Also, due to little endianness, the offset comes before the segment, so swap the two MKI$ expressions in each statement to store the offset part first.

Peter Swinkels wrote on 2023-07-08, 21:32:

I would believe the insufficient memory error if the SHELL statement would fail to launch programs too. Which it doesn't.

What do you think of SETMEM? When I made the changes I proposed above plus a call to SETMEM, I was able to run COMMAND.COM, it got its environment properly, and I was even able to pass it a command-line parameter with a little change, all while running from within the IDE, so I think you're almost there!

Incidentally I think after you run SETMEM with a negative value to free up some memory, you should run it with a positive value again later, otherwise the IDE seems to eventually start complaining about memory and can't even save, but you can run e.g. ? setmem(65535) in the immediate window to fix it.

Jo22 wrote on 2023-07-08, 22:28:

This looks like it's worth a read! From a quick skim, it looks like strings can be moved around, so probably instead of building up the parameter block via appending, it is necessary to make one big buffer and then modify it using the MID$ statement which I think is what they suggested in that article. Maybe you need to also consider the order of operations to prevent things being moved after you've taken their address?

Reply 26 of 39, by pan069

User metadata
Rank Oldbie
Rank
Oldbie
Peter Swinkels wrote on 2023-07-08, 21:32:

I would believe the insufficient memory error if the SHELL statement would fail to launch programs too. Which it doesn't.

Highly unlikely. There is a big difference between trying to execute an external program through an interrupt and using the IDE to SHELL into DOS.

When the IDE SHELLs into DOS, the IDE understands the context and it will be able to release/free all the memory it doesn't need. It does this because SHELLing into DOS is a specific IDE feature with the intent to run other programs while in the SHELL.

When you execute another program through an interrupt, the IDE (or QB for that matter) has no idea of the context of this. It will not therefore not free up memory but leaves everything allocated as it did when it was started.

Reply 27 of 39, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

@doshea: you're right, that's a pretty stupid mistake on my part. I'll fix those lines and see how that works out. I will look into the other replies soon. And no, SETMEM doesn't work for me at all.
@pan069: Alright, so SHELL frees up memory then, does anyone have any idea how it does that?

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 28 of 39, by doshea

User metadata
Rank Member
Rank
Member
Peter Swinkels wrote on 2023-07-09, 08:47:

@doshea: you're right, that's a pretty stupid mistake on my part. I'll fix those lines and see how that works out. I will look into the other replies soon. And no, SETMEM doesn't work for me at all.
@pan069: Alright, so SHELL frees up memory then, does anyone have any idea how it does that?

I think SHELL frees up memory in much the same way that SETMEM does. Maybe you should play with the parameter you pass to it, it controls how much memory is freed up. The online help for that function seems to be somewhat useful.

Reply 29 of 39, by pan069

User metadata
Rank Oldbie
Rank
Oldbie
Peter Swinkels wrote on 2023-07-09, 08:47:

@pan069: Alright, so SHELL frees up memory then, does anyone have any idea how it does that?

I guess that theoretically you can do something like this but I haven't tried this:

x% = SETMEM(0)
SETMEM(-x%)

rem INT to execute program here.

SETMEM(x%)

Or something along those lines but I don't think it might be that easy... 😀

Reply 30 of 39, by pantercat

User metadata
Rank Newbie
Rank
Newbie

a program that wants to load other program must first reduce the memory that DOS has assigned to it and keep only what it needs. this qb program and its stack sure fits in 1 KB, so bx = 1024/16 and call int 21/4a to modify allocated memory block. somethin' like this...

mov bx,1024/16	; new requested block size in paragraphs (1KB/16)
mov ah,4ah ; modify allocated memory block (SETBLOCK)
int 21h

push cs
pop es ; ES:BX -> PARAM_BLOCK

;; Parameter block
lea bx, PARAM_BLOCK
mov Word Ptr [bx],0
mov Word Ptr [bx+2],80h ; PSP
mov Word Ptr [bx+4],cs
mov Word Ptr [bx+6],5ch ; FCB 0
mov Word Ptr [bx+8],cs
mov Word Ptr [bx+0ah],6ch ; FCB 1
mov Word Ptr [bx+0ch],cs
;;

lea dx, FILENAME ; DS:DX -> FILENAME (ASCIIZ)
mov ax,4b00h ; EXEC/Load and Execute Program
int 21h

push cs
pop ds

mov ax,4c00h ; Terminate
int 21h

FILENAME db "WHATEVER.COM",0
PARAM_BLOCK db 22 DUP (0)

Reply 31 of 39, by pan069

User metadata
Rank Oldbie
Rank
Oldbie
Peter Swinkels wrote on 2023-07-05, 14:10:

I am trying to use int 21h, 4bh to launch programs from a Quick Basic program. It only works from inside a compiled program for me. When executed from the run-time environment the function always claims there isn't enough memory (ax = 8h). I know for a fact that in most cases this can't be true, because when I use the SHELL statement instead it usually works fine.

I was just looking through some QB documents and noticed there is the SHELL command (sorry, I haven't used QB in like decades and I have forgotten most of what I knew back in the day about it). Initially I was under the impression that with SHELL you were referring to the SHELL IDE (Under the File menu), and although the behaviour is the same (from a QB point of view), why are you using the INTERRUPT to launch an external program instead of use using the SHELL statement?

The only difference I can imagine between the two is that the SHELL statement first starts a new copy of COMMAND.COM before running the external program while the INTERRUPT does not. But as you have seen, using the INTERRUPT requires a lot more memory management on the behalf of the developer. I think COMMAND.COM takes up about 40Kb or something, so unless you really really need that 40Kb...

SHELL "C:\DOS\MEM.EXE"

Reply 32 of 39, by doshea

User metadata
Rank Member
Rank
Member
pantercat wrote on 2023-07-09, 11:32:

a program that wants to load other program must first reduce the memory that DOS has assigned to it and keep only what it needs. this qb program and its stack sure fits in 1 KB, so bx = 1024/16 and call int 21/4a to modify allocated memory block. somethin' like this...

I think what you're suggesting is probably what the SETMEM function does, but SETMEM seems to do it safely, reducing the requested size if it knows that it cannot be satisfied. I think QB probably also packs its memory (moves things around so there is a large contiguous free region of memory) too. Simply calling the interrupt as you propose seems unsafe to me as you don't actually know for sure how much memory is free, and QB won't know about the change so may use some of the memory you just told DOS is free.

I think I saw that there was a function to ask QB to pack its memory. If someone was really intent on going behind QB's back and telling DOS to take away some of its memory in a way which QB won't know about, then I'd suggest at least packing the memory first if possible.

pan069 wrote on 2023-07-09, 22:02:

why are you using the INTERRUPT to launch an external program instead of use using the SHELL statement?

I think that another benefit is that the SHELL statement throws away the return status/exit code/ERRORLEVEL/whatever you want to call it, and that information can be useful.

Reply 33 of 39, by pan069

User metadata
Rank Oldbie
Rank
Oldbie
pan069 wrote on 2023-07-09, 22:02:

why are you using the INTERRUPT to launch an external program instead of use using the SHELL statement?

I think that another benefit is that the SHELL statement throws away the return status/exit code/ERRORLEVEL/whatever you want to call it, and that information can be useful.
[/quote]

Fair enough. Depending on the use case that can be useful.

Reply 34 of 39, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

I thought using the interrupt call might be better because the SHELL statement launches another command prompt to execute a command which seems like a waste of memory.

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 35 of 39, by pan069

User metadata
Rank Oldbie
Rank
Oldbie

@Peter Swinkels. Okay, I did a more more tinkering and this is what I found...

But before I go into the details, first I want to give a big shout out to both @Jo22 and especially @doshea for providing some really good links and reading material. Thanks guys!

So, right off the bat... Executing through INT &h4B is different from within the IDE than from a compiled EXE. I am not a 100% on the details but my assumption is that the IDE simply grabs all available memory and that the compiled EXE only allocates what the program needs. I guess the IDE itself needs more memory and there might be a difference in running the program in an interpreted mode vs running it in a compiled mode.

It basically comes down to the following. When you want to execute from the IDE, you first need to free up some heap memory before doing the INT &h4B and restore it after executing the program.

Now the code...

DEFINT A-Z

TYPE RegTypeX
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
ds AS INTEGER
es AS INTEGER
END TYPE

DECLARE SUB INTERRUPTX (intnum AS INTEGER, inregs AS RegTypeX, outregs AS RegTypeX)

DIM FCB1 AS STRING
DIM FCB2 AS STRING
DIM Params AS STRING
DIM Program AS STRING
DIM Regs AS RegTypeX

CLS

rem Uncomment the one you want. I have used TREE.COM to test passing a parameter.
'Program$ = "C:\DOS\TREE.COM"
'Params$ = "C:\"
Program$ = "C:\DOS\MEM.EXE"
Params$ = ""

rem Make sure the Program$ ends in a trailing zero.
Program$ = Program$ + CHR$(0)
rem The Params$ needs to start with a byte that represents the length of the string and be terminated with 13 (cr) and trailing zero.
Params$ = CHR$(LEN(Params$)) + Params$ + CHR$(13) + CHR$(0)
rem FCB's can be used to pass information to the program being executed, probably used for executing overlays. Ours are just empty.
FCB1$ = STRING$(37, 0)
FCB2$ = STRING$(37, 0)

rem Use 7 integers (WORDs) as our parameter "block" (https://stanislavs.org/helppc/int_21-4b.html)
rem block(0) can be set to zero unless you want to populate an environment.
DIM blocks(7) AS INTEGER
blocks(0) = 0
blocks(1) = SADD(Params$)
blocks(2) = VARSEG(Params$)
blocks(3) = SADD(FCB1$)
blocks(4) = VARSEG(FCB1$)
blocks(5) = SADD(FCB2$)
blocks(6) = VARSEG(FCB2$)

Regs.ax = &H4B00
Regs.ds = VARSEG(Program$)
Regs.dx = SADD(Program$)
Regs.es = VARSEG(blocks(0))
Regs.bx = VARPTR(blocks(0))
x& = SETMEM(-65536) ' Only needed within the IDE
INTERRUPTX &H21, Regs, Regs
x& = SETMEM(x&) ' Only needed within the IDE
PRINT "Return code: "; HEX$(Regs.ax AND &HFF)

When you execute MEM.EXE from within the IDE, you will see that MEM.EXE reports that there is 64Kb available memory. That is the 65536 bytes that were freed from the heap. In this example I have freed 64Kb from the heap before execution (lines 56 & 58) but the program you might want to execute might need more memory available. Again, when you compile to EXE you should comment out the SETMEM lines (lines 56 & 58) as they are not required in that context.

I hope this helps.

Last edited by pan069 on 2023-07-13, 22:09. Edited 5 times in total.

Reply 36 of 39, by Peter Swinkels

User metadata
Rank Oldbie
Rank
Oldbie

@pan069:

Thank you for your code, I will look into it later. 😀

Do not read if you don't like attention seeking self-advertisements!

Did you read it anyway? Well, you can find all sorts of stuff I made using various programming languages over here:
https://github.com/peterswinkels

Reply 37 of 39, by doshea

User metadata
Rank Member
Rank
Member
pan069 wrote on 2023-07-13, 10:09:

But before I go into the details, first I want to give a big shout out to both @Jo22 and especially @doshea for providing some really good links and reading material. Thanks guys!

Thanks for working on this stuff! I'd like to re-learn BASIC one day but don't have time, so I appreciate your enabling me to kind of tinker from a distance 😁

So, right off the bat... Executing through INT &h4B is different from within the IDE than from a compiled EXE. I am not a 100% on the details but my assumption is that the IDE simply grabs all available memory and that the compiled EXE only allocates what the program needs. I guess the IDE itself needs more memory and there might be a difference in running the program in an interpreted mode vs running it in a compiled mode.

It's weird, the online help for SETMEM says:

Note: A first call to SETMEM trying to increase the far heap has no
effect because BASIC allocates as much memory as possible to
the far heap when a program starts.

And I made a test program which frees as much memory as it can then indicates what the new heap size is, which makes it seem to me like the online help is true:

CLS
initial = SETMEM(0)
PRINT "Initial size of far heap:"; initial
minimal = SETMEM(-999999)
PRINT "Minimal size of far heap:"; minimal
restored = SETMEM(999999)
PRINT "Restored size of far heap:"; restored

When I run it from the IDE, I get:

Initial size of far heap: 227056
Minimal size of far heap: 5648
Restored size of far heap: 227056

When I run a compiled .EXE outside the IDE, I get:

Initial size of far heap: 422512
Minimal size of far heap: 32
Restored size of far heap: 422512

That shows the IDE is consuming about 200KB which isn't available for the far heap, and also consumes some of the far heap. Compared to how much memory I have before I run the .EXE file though, the difference is only about 80K - roughly the size of my EXE file plus BRUN45.EXE - so it seems like it is consuming all the available memory (as you'd expect for a DOS application), so it's weird that SETMEM doesn't need to be called from a compiled .EXE program!

Also note that passing -999999 and 999999 works and is probably the best way to maximise the memory available to the application being run, and then to QB afterward.

[…]
Show full quote
rem Use 7 integers (WORDs) as our  parameter "block" (https://stanislavs.org/helppc/int_21-4b.html)
rem block(0) can be set to zero unless you want to populate an environment.
DIM blocks(7) AS INTEGER

Nice, that's a very elegant way of dealing with the structure!

My only concern with your code is: could SETMEM move things around in memory after addresses have already been taken? I haven't had time to read that whole article that Jo22 posted, but I'd be inclined to do some test like:

A$ = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
B$ = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
DIM blocks(7) AS INTEGER
initial = VARPTR(blocks(0))
A$ = ""
B$ = ""
SETMEM(-999999)
if VARPTR(blocks(0)) <> initial then
print "it moved :("
end if
SETMEM(999999)

(not tested, and since I'm so rusty at BASIC the syntax might not even be right!) or maybe with some other things which might get resized, to see if the address could be changed by SETMEM. Or alternatively, maybe the code can be rearranged so that everything is allocated before the call to SETMEM but filled in (with addresses) after the call.

Again, when you compile to EXE you should comment out the SETMEM lines (lines 56 & 58) as they are not required in that context.

I imagine that it might not be necessary to comment them out since SETMEM doesn't error if it can't shrink the heap, in fact if I understand correctly if you want to know if it shrank the heap you have to do something like:

initial = SETMEM(0)
new = SETMEM(-65536)
if initial - new <> 65536 then
if new = initial then
print "SETMEM did nothing at all!"
else
print "SETMEM didn't free as much memory as I hoped for"
end if
end if

You can get an error if you do anything that requires memory allocation from QB's heap when there's no memory free though, so a subsequent SETMEM(999999) call is important.

Jo22 wrote on 2023-07-08, 22:28:

Hi. Just stumbled over this issue of QB Cult Magazine.
It covers memory in QB45 and XBX.

http://qbcm.hybd.net/issues/1-6/

Thanks (again?)! I still haven't read this, only skimmed it, but it looks like the relevant part is:

BASIC Techniques and Utilities, Chapter 2
Variables and Constant Data

By Ethan Winer <ethan@ethanwiner.com>

For anyone who is interested, the whole book is available for free at the author's site: http://ethanwiner.com/fullmoon.html He's also the author of PDQ and the other Crescent Software libraries I keep mentioning.

Reply 38 of 39, by pan069

User metadata
Rank Oldbie
Rank
Oldbie

@doshea Good write up and good tips! I'll do some more testing and digging around when I get the opportunity.

Re. SETMEM. The simple test I did was to execute MEM.EXE, from both the IDE and from compiled EXE. When executed in the IDE, I have to release memory otherwise it returns a 0x08 Out Of Memory error (i.e. the IDE allocated all memory for its own usage). Then when using SETMEM to free up 64Kb (-65536), when executed from the IDE, MEM.EXE reports 64Kb of free memory. I think MEM.EXE is smart enough not to include its own memory footprint.

When commenting out the SETMEM lines and executing MEM.EXE from a compiled version, MEM.EXE reports something like 482Kb of free memory. This was indicating to me that the compiled EXE doesn't just grab all memory (like the IDE does) but only allocates what it requires at startup and then further locates memory when it is required. However, it could very well be that more memory could have been freed up by using SETMEM... 🤔

I was scanning through that PDQ code base a bit and I could not see any meaningful SETMEM calls in there that were used to free up memory either, and you'd expect to find them in conjunction with the LDEXEC function of that library if they where required, not?

Reply 39 of 39, by doshea

User metadata
Rank Member
Rank
Member
pan069 wrote on 2023-07-16, 20:41:

I was scanning through that PDQ code base a bit and I could not see any meaningful SETMEM calls in there that were used to free up memory either, and you'd expect to find them in conjunction with the LDEXEC function of that library if they where required, not?

Good point!

Knowledge Base article Q31308, which claims to cover:

- The Standard and Professional Editions of Microsoft Visual Basic for MS-DOS, version 1.0 - Microsoft QuickBasic for MS-DO […]
Show full quote

- The Standard and Professional Editions of Microsoft Visual Basic
for MS-DOS, version 1.0
- Microsoft QuickBasic for MS-DOS, versions 4.0, 4.0b and 4.5
- Microsoft BASIC Compiler for MS-DOS, versions 6.0 and 6.0b
- Microsoft BASIC Professional Development System (PDS) for MS-DOS,
versions 7.0 and 7.1

says (emphasis mine):

2. The EXEMOD.EXE utility provided with the Microsoft Macro Assembler allows you to modify the header of an .EXE to shorten t […]
Show full quote

2. The EXEMOD.EXE utility provided with the Microsoft Macro Assembler
allows you to modify the header of an .EXE to shorten the maximum
upper load address of a program. By default, BASIC .EXE programs
assume that all of RAM is available.
If you make the load address
smaller, you must make sure that there is enough room for the
BASIC program's code and data. Microsoft does not encourage using
EXEMOD with compiled BASIC programs. The SETMEM function should be
used instead.

but you have some pretty clear evidence that it doesn't behave that way! Looks like one of those cases where the documentation is just plain wrong 🙁