This article was submitted to QB Express a while ago, but the upcoming issue of QB Express was never published and hence neither was this article. Since then, QuickBasic has lost a lot of ground to QB64, other BASIC dialects, and other languages, and so the content of this article will be less relevant to many readers.
Countless beginners have proclaimed "I'm going to write an operating system in QBasic". Countless more experienced programmers have replied "That's not possible".
Of course, no beginner could write an operating system - in any language. However, I claim that it is probably feasible to write an operating system in QuickBasic.
I'm not talking about a shell, GUI, DOS extender, or using some special BASIC dialect. I'm also not out of my mind.
Specifically, I claim that it is possible to write a kernel (and by extension, an operating system) using the QuickBasic compiler, and of course a fair amount of assembly language. And not just any kernel - one that runs in protected mode and facilitates proper multitasking (not just multi-interpreting as a few of the QB GUIs are capable of). To my knowledge this has never been done.
This article explains what a kernel is and then goes on to explore the possibilities that QuickBasic has in this area.
A kernel is a piece of software whose purpose is to manage a computer's resources on behalf of other software.
The i286 processor brought to us the magic of Protected Mode, a special processor mode that allows you to run software in a kind of "secure environment" - if the software attempts to do anything out of the ordinary, the processor will stop executing it and notify the kernel, which could decide what to do about this.
Its successor, the i386 processor, was the first 32-bit processor in this line. This meant (among other things) that it could address more memory more easily. Practically all desktop processors available today are backwards-compatible with the 386.
What are the advantages of protected mode? Protected mode means that a single program doing something stupid or harmful won't bring the whole system down. It means that programs can be prevented from interfering with the memory of other programs. It means that all this is available without compromising speed!
In case you're not sure what pre-emptive multitasking is, it involves the operating system switching between programs without the help of the programs themselves. Essentially if a program runs for too long, the OS yanks the processor away from it and lets another program run. (Compare this with co-operative multitasking, where a program has to willingly give up the processor in order to let other programs run.)
A kernel that performs this sort of multitasking should be able to:
On top of this, a good deal of setting-up must be performed before the system is ready to go. (For instance, switching to protected mode is quite a long process, and a lot of hardware needs to be configured.)
As you can see, a kernel is not a "program" as you're used to, but more a set of routines that deal with specific events.
What about QuickBasic, then? OK. Firstly, BC (the Basic Compiler that ships with QuickBasic) produces .OBJ files. They're files that contain the compiled (machine-code) version of your QB code. However they don't contain all the BASIC routines (like PRINT, CLS, OPEN, etc.) - those are located in the libraries that ship with QB. When you link your OBJ file with these libraries (the QB IDE does this for you), they are combined to create an executable file.
But hang on - what if you... say, edited those QB libraries? Could you make different things happen? Could you change the font of PRINT? The answer is yes, but it's not exactly an easy job. And beyond the scope of this article. But could you take the code in your OBJ file and add some support routines made in assembly language, and potentially create... a kernel? The answer is a qualified "yes".
It's important to realise that QuickBasic calls a lot of procedures internally (as well as the explicit calls like CLS or PRINT). For instance, at the start of a SUB or FUNCTION, B$ENRA is usually called to allocate stack space, and at the end B$EXSA is called to release it again. B$MUI4 is called if you want to multiply two LONG-integers. Concatenating two strings is also done with a procedure. There are loads of QB procedures and I don't know what most of them do. But a bit of experimentation should help figure out the most important ones.
So, in order to be able to do certain things you will need to provide the procedures that QuickBasic depends on. Something like multiplying two LONGs can probably be lifted straight out of QB's runtime library, but something like concatenating two strings involves memory management and will be very difficult to support. Long story short, most "features" of the BASIC language will require a lot of work if you want to use them in your code.
You would need to reverse-engineer parts of the QuickBasic library, which means disassembling the code and figuring out what you can keep and what needs to be rewritten. Even if you had the QuickBasic library's source code, the procedures in it were written in assembly language.
Thankfully things like IFs and FORs that only use INTEGER arithmetic usually don't involve calling any procedures.
Beyond that, there would be more assembly language required to prepare the processor to run your QB code, and give the QB code access to hardware information and more control over the machine.
So (you're probably wondering), realistically what proportion of the code would be assembly language and what proportion would be BASIC? If you made an effort to keep assembly to a minimum, and stick to things like integers, IFs, loops, SUBs, FUNCTIONs, static arrays and TYPEs (and avoid strings), my educated guess would be about 70% BASIC code and 30% assembly language, not counting routines copied from the runtime library. This of course depends very much on what your kernel is actually capable of doing.
To help you imagine what it would be like, here is my "artist's impression" of what the timer interrupt handler and thread switching routines might look like:
SUB timerInterrupt DIM NewThreadID AS INTEGER systemTicks = systemTicks + 1 'used for calculating time of day etc. curThreadTicksRemaining = curThreadTicksRemaining - 1 IF curThreadTicksRemaining = 0 THEN 'timeslice expired, run something else checkSleepQueue 'See if any process's sleep call has finished NewThreadID = selectNewThread switchToThread NewThreadID END IF END SUB SUB switchToThread(theadID AS INTEGER) curThreadID = threadID curThreadTicksRemaining = threads(curThreadID).timesliceTicks 'the following routine would be written in ASM 'saves the registers on the stack before switching the KERNEL stack 'then restores the new set of registers from the new stack asmStackSwitch(threads(curThreadID).stackPtr) END SUB
Properly explaining the motivation behind that code would be an article in itself, so it won't happen here. Hopefully it gives you an idea of what the code would be like.
The most important thing about BC-generated code is that it is 16-bit code. (This simply means the processor must be in 16-bit mode to run the code.) It is possible for the whole kernel code to be 16-bit (this was the only option in the 286) however this effectively prevents you from running any 32-bit programs.
A preferable option is to use a bit of 32-bit assembly language to switch to 16-bit mode and back in order to run the BC-generated code. This would in fact allow a QB kernel to run 32-bit software. That sounds so sweet, I think I'll say it again: an OS written in QB can support 32-bit programs! (Note that I never said this was easy.)
There is another problem. QuickBasic is designed to run under DOS. Things like opening files, allocating memory, and graphics rely on operating system support. A standalone kernel does not have this luxury - essentially you need to either go without, or create your own version of such features from scratch. (There are certain exceptions but they're not too important to this discussion.)
If you want your OS to allow programs to read files from the hard disk, you will need to write a device driver that talks with the disk, as well as code that understands the filesystem on the disk. If you want to allow programs to receive input from the keyboard, you need to write a keyboard driver.
It's time for the truth. There's no technical advantage to any of this. It will not be particularly fast. It will not be at all easy. You won't be able to use many of QB's features.
So is there any reason to doing this at all? Well here are some:
If you're thinking about writing a kernel, I wouldn't recommend using QB. Use C and assembly like a sane person. Don't try writing a kernel in QB unless that is exactly what you want to do.