Hacker News new | past | comments | ask | show | jobs | submit login
Programming with DOS Debugger (2003) (susam.net)
107 points by susam 10 months ago | hide | past | favorite | 47 comments



The fact that this debugger program is always available with MS-DOS or Windows 98 system means that these systems are ready for some rudimentary assembly language programming without requiring any additional tools.

PC magazines from the 80s through the 90s took advantage of this, publishing listings of "source code" that could be typed into DEBUG to create tiny but useful utilities, mostly under 1K in size. It was an era when "power users" or even moderately advanced users would often know some actual programming too and many grew into full-time developers; a stark contrast from these days when many "real" developers barely understand anything about their environment (and are incentivised not to.)


> "source code" that could be typed into DEBUG

Oh, wow - I remember doing something similar on my Commodore 64. The C64 came with a BASIC interpreter, but no built-in assembler. There was a magazine called Compute's Gazette that included, in the back of every issue, a simple assembler of sorts written in BASIC. So if you wanted to run any of the programs included in the magazine, you'd first type in the code for the assembler and use that to "bootstrap" your assembler programs. They were long (pages and pages) of listings of hexadecimal, with a checksum at the end of each line to make sure you didn't make a tiny mistake toward the beginning that rendered the whole thing useless.

It was actually pretty amazing how sophisticated some of these programs would end up being, considering the entire hex dump could realistically be printed on four or five A4 standard pages.

Man, I spent a lot of time typing those things in - I still remember off the top of my head, almost 40 years later, that the opcode for LDA was A9 and STA was 8D, because I typed those in a _lot_. Back then, I thought that this was more or less what a career in computer programming would be like - honestly, with the JIRA tickets and Sprint retrospectives and standups and planning sessions that it's turned out to be, I'm a little disappointed.


Things turned out to be a lot less fun than I thought they would be, just for what you've mentioned.


> and are incentivised not to

Could you elaborate on that?


You are incentivized to user higher abstractions, sometimes for good reasons (portability, productivity, etc...).

But the higher you go, the more magic there is. On a DOS machine when x86 processors actually ended with "86", or on home computers like C64s and Amigas, everything was simple. No concurrency, no security, no networking, you had a processor and it ran instructions. You go from a character displayed on screen and with a little research, have a good idea about everything that happened down to the transistor. Now, it is impossible, most of the hardware components of a modern computer are made of little computers, each one orders of magnitude more complex than these early systems, many of them run encrypted software.

It means that now, software development is more about trying to get an idea of what the people who made the layer under yours were thinking about. It is becoming harder to go with first principles, too complex, it is therefore discouraged, as it is more likely to hurt productivity.


> on home computers like C64s [...], everything was simple

Simple? I take you've never had to implement a division on the 6502. Or write complex code due to the limitations of 8 bits (which couldn't even directly address the pixels on the X axis) and/or 3 registers.


I didn't mean that using or programming the machine was simple, far from it. But the machine itself was simple.

If some pixel doesn't light up right on your C64, you can follow the path from your code to the electron beam in the monitor. That entire path could fit in a single person head. It is a puzzle but you have all the pieces in front of you.

Now, it is not a puzzle anymore, you just call some drawText() function and then some magic happens and there is text on the screen. If the magic doesn't happen, monitoring the output of your GPU won't help you, there is too much in between, some of it deliberately obfuscated. So you try random stuff that don't make much sense except that because of your experience, you know they work. Or more likely, someone with experience already did it, posted it on StackOverflow, and you found it using Google.


Not much time for in-depth learning when you have sprint commitments to make. Just have Copilot generate the boilerplate and tests and fix them as you go.


Gotta keep that velocity up!


Or copy-paste from stackunderflow.


Example: Everyone knows how to use a Dictionary. It's not worth anyone's time to know how it works under the hood, because only a fool would actually implement one from scratch in production code. Niche embedded cases possibly excepted.


> It's not worth anyone's time to know how it works under the hood

Knowing (at a high level) how your programming language implements dictionaries has relevance to questions like time complexity of various operations, potential security vulnerabilities (a hash table might succumb to hash collision denial of service, especially if the implementation isn’t hardened against that possibility; a tree-based implementation probably won’t have that vulnerability), likely impact of different bugs (e.g. a buggy hash method can cause much more problems on a hash table than on a tree, while for a buggy comparison method it is the other way around), concurrency, etc

> because only a fool would actually implement one from scratch in production code

I’ve implemented a dictionary before in C. Not for work (only wrote C code for work one single time ever, and it was only a page worth of code that was called from Java, no complex data structures needed), just for my own learning/amusement. That said, C is probably the one context in which people still commonly “roll their own” basic data structures, even in production code, just because C’s standard library is so weak in that regard (and C’s lack of generics/templates doesn’t help either)


I would argue this is because we've raised the bar for what counts as a computational primitive, and not a qualitative difference.

A modern developer can ignore the details of a dictionary in the exact same way a programmer of the past could ignore the details of the mov instruction. (And be more productive for it.)


The problem is when developers having virtually no background in actual computer science (e.g. algorithm complexity) start implementing their own algorithms because what thy want is not directly available as a primitive. I have the impression that this tendency to use more and more complex software primitives, while at the same time introducing further levels of indirection in the actually run machine code, is one of the reasons that many applications are now as slow (or slower) than they were 10 years ago despite the hardware being much faster. Just compare the performance of an Electron-based application (which seems the go-to solution of most developers nowadays for desktop app implementation) with a native one...


I think you're embedding a value judgment in this (slow execution = bad) that is probably not shared by the people using the primitives you think are inappropriate.

In other words, when people don't care for speed they use Electron. What part of that indicates that people have no background in algorithm complexity? We have traded off run-time speed against other things for ages -- even in computing science.


> I think you're embedding a value judgment in this (slow execution = bad) that is probably not shared by the people using the primitives

Can be... because I'm also a user, and it's frustrating to have the PC bloated by dozens of Electron-based apps using huge amounts of RAM, draining the battery more than necessary, and for part of them, actually slow to use. Perhaps some users don't care because they are either not sensitive to small delays or resource usage, or because they have never seen anything else.

I see also an environmental question arising here. More CPU cycles = more energy used. Makes no difference for applications used by small groups of people, but for software used in millions of copies, I'm wondering how many MWh we are wasting (I wouldn't care much —except for battery life— if energy was only from renewable sources... but it's not the case)


I'm guessing, just guessing, that most of the people who don't think slow execution is bad are probably not that interested in what the machine actually has to do to execute their code, and hence, are not actually well educated in making those tradeoffs.


> only a fool would actually implement one

No, only a fool would let his boss know that he did.


You need permission to write software and distribute for modern platforms. This is very disincentivizing .. not so when you've got a compiler/assembler onboard everywhere and can just ship without permission from MegaCorp...


DEBUG was also able to read/write disk blocks and it could load / save files from memory based on register settings. I had written a batch file and a file of DEBUG commands ( redirected into DEBUG ) that would load the blocks from 1.44M diskettes at about 64K at a time, save each just-under-64k segment with a separate filename, and then zip up the whole collection. I also had written the counterpart scripts to decompress and block-write the data back out to a 1.44M floppy. This allowed us to archive the .zip collections as disk images for some critical boot disks and such.

I had also written a utility in C published in Windows/DOS Developer's Journal (April 1995) before some email clients could UUDECODE or Base64-decode attachments. This utility wrote a short loader as a text file followed by the G command to execute it. Following the G command were lines of UUENCODED data. The assembly language loader code would begin reading lines from the standard input device and would UUDECODE them as it added them into DEBUG's current working buffer starting at 100h. At the tail end of the script, the N command was used to name the output file, the CX register was updated via RCX to set the output length, and the W command was used to save the file. This allowed me to send some encoded binary files embedded in the body of the email to folks that didn't have sophisticated email clients. They could just pipe this portion of the email into DEBUG and it would ultimately write the decoded binary file.


Your solution it's similar to sharutils.


I once made a tiny pre-emptive kernel using the DOS debugger. It was like the day after graduating from CS undergrad, and I needed a cleanse from the many years-long C on Unix mission (which happens to be continuing, decades later).


I once made a lot of money by guiding a colleague over the phone how to write a program with DEBUG.com to do a software 'upgrade' consisting of 128 bytes to change in the older version .. this was pre-Internet-everywhere, and the time cost of waiting for a tape (yes we delivered DOS software on tape back in the day) to arrive across country was too high. An hour of careful hex narration later, we got it done and days of downtime were saved. Oh, what a bonus that was (went straight into a new 486, lol).

Kind of miss DEBUG.com now that I think about it.


A few things to point out:

1) The Debugger and the program being debugged -- are effectively running in the same execution environment; the same memory space (both programs are different ranges of the same memory and both can access each other's memory without restrictions, but only one can be running at one time (single stepping swaps control (for an instant) and then swaps it back again, as does running under the debugger until a breakpoint is hit -- as there are absolutely NO threads (concurrent code execution paths) in this environment!)

If the debugged program crashes for any reason, for example, enters an infinite loop and/or doesn't yield control back to the debugger via executing a RET instruction -- then the entire PC (if DOS is running directly on hardware (as opposed to under Windows and/or a VM) will crash, AKA "lock-up"! Powering the entire machine on and off is now necessary to reboot it! (Dave Letterman, many years ago, in response to the then-feared upcoming Y2K disaster: "Just Reboot!")

2) While this mode of programming is hard, laborious, counter-intuitive, slow, and error-prone(!) -- it should also be lauded, praised -- because equal-and-oppositely, whoever is programming at this level has effectively gotten rid of 99.99% of the "tech stack" dependencies -- the code written by other programmers in compilers, operating systems, programing languages, programming environments, libraries, frameworks, tech stacks, other software components, etc., etc.

Oh sure, there's still DOS in the background... but the DOS code/API fits into like what, like 32KB? (That's Kilobytes with a 'K', not Megabytes or Gigabytes... several orders of magnitude smaller...)

And a pure assembly low-level programmer -- does not even need to depend/rely on DOS or BIOS calls... they can effectively get rid of those too by writing their own hardware drivers... OS developers that write in Assembly typically do this or something like this...

Anyway, an excellent article!


DOS makes a nice runtime for low level and embedded applications. Some implementations are 64 bits, such as this one: https://github.com/dosemu2/fdpp

I wish there were an ARM-compatible version of DOS, if possible stateless. It would often be more suitable for an ARM board than a full-fledged Linux, given its almost non-existent attack surface, low resource consumption, and simplicity. Heck, I'd even like to see DOS microservices on stateless nano-VMs.


> whoever is programming at this level has effectively gotten rid of 99.99% of the "tech stack" dependencies

Not necessarily: I once worked with a team whose reference manual was the S/360 "Principles of Operation" — but they weren't exactly working on the metal; everyone's model of the 360 architecture was virtual, all running on top of a (1960's!) supervisor multiplexing the actual box.

https://en.wikipedia.org/wiki/Conversational_Monitor_System

http://bitsavers.trailing-edge.com/pdf/ibm/360/princOps/A22-...


The IBM S/360 family of computers -- are well worth studying for any serious student of computer history:

https://en.wikipedia.org/wiki/IBM_System/360

>"The slowest System/360 model announced in 1964, the Model 30, could perform up to 34,500 instructions per second, with memory from 8 to 64 KB.[3] High-performance models came later. The 1967 IBM System/360 Model 91 could execute up to 16.6 million instructions per second.[4] The larger 360 models could have up to 8 MB of main memory,[5] though that much memory was unusual; a large installation might have as little as 256 KB of main storage, but 512 KB, 768 KB or 1024 KB was more common. Up to 8 megabytes of slower (8 microsecond) Large Capacity Storage (LCS) was also available for some models."

But -- Even if someone had the full 8 Megabytes (8MB, not 8GB or 8TB!) back in the 1960's (which would have been the exception, rather than the norm!) -- that still would not be enough to shoehorn a modern-day Linux kernel into it, much less also gcc, glibc, libraries, Node.js, npm, Python, and whatever other programs, libraries or software components are being used for someone's tech stack...

Whether a virtual machine was running in the background -- or not...

In the 1980's and 1990's, while corporate Mainframes may have had hardware support for virtualization -- consumer x86 PC's most certainly did not.

Thus, if someone was running MS-DOS directly on an x86 PC of that era without Windows, they were effectively running on "bare metal" -- because MS-DOS didn't trap and proxy and reroute x86 IN / OUT instructions (or DMA for that matter) -- assembler programmers of this era using an assembler under DOS had full access to ALL of the underlying hardware in their PC's...

Still, you make an excellent point that modern VM's, as programs, have historic ancestors that existed in the mainframe computer world as far back as the 1960's -- most notably as CMS on the IBM S/360 mainframe family.


I'm sure I'm not the only one who's learned to write 6502 assembly on a machine code monitor [1].

[1] https://www.c64-wiki.com/wiki/Machine_Code_Monitor


You could easily create a tiny .COM program to reboot the PC, using DEBUG.

You had to start DEBUG, then, at its prompt, use, in sequence, the A (Assemble) command to assemble a JMP instruction, the N (Name) command to name the .COM file to write the assembled machine code to, the R (Register) command to set the CX register, indicating the number of assembled machine code bytes to write to the file (with the BX register already initialised to 0 by DEBUG on startup, because both BX and CX were used to indicate the number of bytes to write) and then the W (Write) command to write that many bytes of machine code to that file.

Then you would exit DEBUG.

Now, at any time after that, you could run that .COM file to reboot your PC.

So if it was called R.COM, you could just type R and hit Enter to reboot. (In DOS, you did not have to type the executable file name's extension, whether it was .COM or .EXE or .BAT.)

So if you were playing a game in the office during working hours, it could be faster to type R and hit Enter, than even to press Ctrl-Alt-Del or to press the reboot button of your PC. Useful if your boss suddenly came into the room ...

IIRC the JMP instruction was: JMP F000:FFF0. (1)

That would make the PC jump to that address in memory (in the BIOS ROM) and start executing the code there.

And that was the start of the BIOS machine code to reboot the PC.

(1) When I googled just now to confirm the address, since I was not sure of the exact one, interestingly, the first hit was an article about the same topic on ... Susam's site :)

But his BIOS address as shown is different from mine.

See the section "Reboot Program" at:

https://susam.net/rebooting-with-jmp-instruction.html


>So if you were playing a game in the office during working hours, it could be faster to type R and hit Enter,

Of course, you would have to exit the game first, but many DOS games could be exited fast, by just pressing Esc rapidly one or more times, or Alt-X, or other similar methods.


My second paragraph above only outlines how to do it. It assumes that you know the syntax of the DEBUG commands.

Susam's article linked above shows the exact procedure, with precise syntax.


The .COM program was only 5 bytes long, so it loaded instantly too.


Debug is a passable quick-and-dirty assembler. It's no fun to refactor code after you've resolved all the addresses and hard-coded them into the "source" but when you're in a pinch and don't have access to real tools it works well.


I "solved" that by using two assembling passes. The first had dummy jump/call addresses in order to determine the code offsets of labels. Then I would resolve the relative offsets, replace the target labels with offset deltas and reassemble.

I used that trick to speed up my QBasic programs with graphics, string and list handling, etc. It was one of the first rewarding times in my programming "career" and it actually made me feel proud of myself.


Yeah-- I did the same thing. It just sucks when you've forgotten s prefix or a push/pop early in the code and you have to retarget all the subsequent addresses. It really makes you appreciate a real assembler.


If you write basic blocks only with a certain alignment there's much less hand patching imposed for small changes


I remember reading about people doing that on the Apple II with the Woz mini assembler. I always had an assembler on the 6502 so I never did it myself. Makes sense though. Never thought to do it on x86 but it's a good idea.


After a while, you get really good at hex arithmetic and memorising x86 instruction lengths.


Lovely article, that brought back memories! A wee pedantic bit here, to exit the program, it was the school of thought, to use the following instruction - set the exit code to zero and issue terminate.

mov ah, 4ch mov al, 00h ; Or combine both mov into mov ax, 4c00h int 21h


I was thinking about DEBUG a bit ago.

There's a story, it may even be true, that Chuck Moore bootstrapped ColorForth using just DEBUG and a floppy.

Now, to be fair, you can develop in DEBUG with DEBUG, and an editor (EDIT.COM perhaps). Create "prog.txt" and C:\> DEBUG < prog.txt. Boom, "assembler".

But I was considering what it might be like to have JUST DEBUG. You have DEBUG, you can load a file, you can write a file, but that's it. (So, no DEBUG < prog.txt).

And how now, you're in a real "image editing" mode. You just have a blob (or blobs) of RAM that you can load and save to disk, and DEBUG with which to manipulate it. Saving images.

How would one approach that?

I was thinking, fundamentally, that I'd create "lots of subroutines", and have them all starting in a 16 byte "page" boundary, i.e. XXX0. If nothing else, this gives you a bit of buffer to expand a routine in place (maybe you'd use a 32 byte block size), without necessarily impacting the entire program.

It would also make routine address stand out 1000, 1020, 1040...10A0, 10C0, etc. Cognitively (perhaps) a teensy bit better to manage than just random locations.

You could write mostly PIC (position independent) code. And you can see, perhaps, how easily a Forth style can be adopted. Ideally, higher level code ends up being mostly JSR 10A0; JSR 20C0; JSR ... That's essentially what "direct threaded code" is.

You could, to a point, almost make it straightforward to relocate an address. If you REALLY need to move 10A0 to 3440 (vs simply putting a "JMP 3440" at 10A0), you can hunt down whatever the JSR opcode is, followed by your address. Odds are, thats a routine to monkey patch.

It's an interesting thought experiment.


I still have a copy of "Assembly language step-by-step" which started with DEBUG then moved to nasm in later chapters.


Funny that this would come up today; I'm working through that very book now. I'm still in the debug part and using 'a' though; I haven't moved onto the assembler yet.


I have yet a book of Peter Norton of 80286 assembly that uses DEBUG a lot to do the first steps programming assembly.


This is where I first learned to program. the manual for MS-DOS 3.1 I think it was came with instructions for the debugger and assembler, and how you could create programs.


My 80s era BBC Micro came with a 6502 assembler as part of BASIC built into ROM.

It was a little quirky in that it was a two-pass assembler only if you put it inside a FOR loop and turned off unknown-identifier errors on the first pass. I was told you could write rudimentary assembler macros with judicious use of PROC but I could never get it working.


Actually, using DEBUG for creating anything besides one or two simple DOS/BIOS calls was a PITA due to the lack of symbolic labels. After that, the MASM/TASM boilerplate started to feel bearable.


I liked the Hello World program = 17 bytes. 13B is just the text to print.




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: