You can just use ret at the end, saving 3 bytes. Also, the initial value of bp is 09xx on every version of MS-DOS since 4.0, so you can also start off with an xchg ax,bp to save another byte.
xchg ax,bp
mov dx,107
int 21
ret
db "hello, world" 0d 0a '$'
By the way, I noticed just now that @userbinator occurs in the credits section of an earlier repository of mine too: https://github.com/susam/tucl#credits :)
Personally, I don't think it's necessary. After all, it wasn't me who came up with the idea and it's probably one that has been rediscovered multiple times in the past.
Crazy that once upon a time just sequences of machine code were written to files, without any headers, or checksums, or basically any modern metadata. Just plain machine code, to be directly fed into the CPU.
Cue lamentations of today's complexity mixed with feelings of life being great because of it.
If you're programming a microcontroller, that time is now.
(OK, so it'll often be the output of a C compiler, but there's usually some work to be done in asm to get the system in a state with a stack, clocks, RAM etc to run a C program!)
Yes but also multitasking is a very notable UX improvement, and virtual memory, its part that gives processes separate address spaces, makes sure that one buggy program won't bring the entire OS down with it. With OSes providing applications with more services (stuff DOS didn't have like a network stack, GUI, abstracted access to all kinds of complex hardware) you start wanting dynamic libraries, which are impossible to implement sanely without some kind of metadata in the executable.
Fun fact. The COM file size was limited to 64K. The program size was not. Most COM memory models set CS=DS=SS=ES. However, the Zortech C compiler, would have CS set differently.
CS pointed to the start of the code segment, DS to the start of the data segment. As long as the code segment plus the initialized data of the data segment was less than 64K, you could have a considerably larger runtime footprint (up to 128K) and still be a COM file with all near pointers.
There was a technique that used a thing called overlays[0]. You could write a program that is much larger than the available address space (64k on MS/PCDOS) but you split it up into functional areas or modules that are loaded on demand into a reserved area of your 64k address space.
I'm not sure when it started, but the .com and .exe extensions were ignored, and the loader looked at the first two bytes to see if it was "MZ" and was an exe file.
I like that every windows exe/dll still have the MZ on the front. Also a working DOS program there in the first hundred bytes which is pretty much a program to say 'hey run this in windows'. As the real windows header is 100 bytes in.
On the PC you can enter pretty much any byte by holding Alt and typing the (decimal) value on the number pad. But here's a "Hello world" in plain ASCII, that works on two different CPU architectures (x86 and 8080):
My first version was shorter than 80 characters and "symmetrical" (same number of bytes before and after the string), but it didn't work on the 80486 and under DOSBox. The '486 needed an extra jump to clear the pipeline, and DOSBox didn't support the CP/M "CALL 5" interface.
I don't have the code handy, but I once was hanging out with a buddy in an OS/2 lab at uni and used copy con to write a reboot COM program. I meant it to be a soft reboot, but it ended up being a simulated hard boot.
And you frequently do do assembly level coding, either because you're cheap and need to eliminate every byte, or because you're trying to slap an 'accidental backdoor' on there to steal all your users coins...
Well, DLTs are supposed to abstract away the distributed-ness. The execution layer usually provides computational primitives, basic memory and cryptographic functions. Not much distributed computing there.
its more similar to distributed microcontrollers by a matter of fact that the nodes don't allow much processing and there are many nodes that replicate the function. it is less similar to simply microcontrollers or simply distributed computing.
I first used toggle switches to input things into a PDP-11/?? I was trying to debug back in 1982 at Rose-Hulman. I next used them to toggle things into an IMSAI earlier this year... the PDP-11 had a better feel, but in both cases it really sucks.
MS-DOS v1 didn't have an assembler in debug.exe (or was it .com?) so the only way to author machine code on a 5150 with no extra dev tools was to code it on paper, translate it by hand, and enter it in hex.
The fun thing to do with it is send it your name and have it disassemble it.
(I use it to check the object code generated by the compiler. Instead of pretty-printing the instruction stream, it disassembles the binary object code. This helps to find mistakes in either the code generation or the disassembler, as they must match.)
Anyhow, feel free to incorporate it into your obj file dumper, exe file dumper, debugger, or any tool that can benefit by examining code.
If I recall correctly, the COM (stands for something like "COre Memory image") file format is limited to 64k, and was intended to be loaded into the same memory location.
Ah, debug. I used debug extensively for machine code programming on DOS. I couldn't afford MASM and certainly didn't have access to free assemblers. My first hello world program was almost exactly this.
However, I always ended my programs with INT 20 instead of RET, or INT 21 with AH = 0. RET worked both for standalone programs or in debug, but there was one exception: if you ran your code repeatedly in debug with G 100. It would work the first time, but IIRC the stack wasn't reinitialized to return to the correct place on subsequent G commands. I spent a lot of time typing, running and patching in a single session and INT 20 always worked correctly.
AH=4C is new in MSDOS 2.0, AH=0 dates back to CP/M (the low DOS calls are compatible with CP/M, hence the use of FCBs for example) and always sets the exit status to 0.
Good old days of writing small utilities directly using Debug and modifying the disk sectors etc etc!!!
The Ralf Brown's Interrupt List was the bible for me... It went on until late 2000s is when I moved out of ASM to C.
Now fiddling with Golang and Rust, but anytime would prefer C or ASM or C with ASM.
Please don't jump on me as I've been primarily in the System's programming and in Information Security for my entire life. No other lang supports me like C / ASM.
For anyone unfamiliar with DOS programming who might be trying to understand the code, you will find it helpful to know that INT 21h makes a call to the DOS API, and the value in AH indicates which API function to call, in this case the "display string" function. See https://en.wikipedia.org/wiki/DOS_API
I had an old full height RLL hard drive if I moved the computer the drive would loose sector zero and I would have to low level format it using a command in debug.
Many years ago I used debug.exe create a bootable floppy that did nothing but display my name on the screen when booted from. I peed a little when it finally worked.
Similar - many years ago, I used DEBUG.EXE to assemble the samples from "The Little Black Book of Computer Viruses" I found in the local library and started a long journey into computer security. I also used it to modify the COMMAND.COM on my all of my high school's DOS media lab and library computers to display rude alternatives to "Please insert floppy disk" and "Insufficient disk space" - which in those days were frequently displayed. (hey, I was a teenager).
...and B000 on the MDA (mode 7), which was to allow both an MDA and CGA to coexist in the same system. Eventually the VGA took B800 even for its text modes, and PCs started booting in mode 3 by default. I'm not sure how the UEFI stuff works, but even the latest GPUs still have a VGA-compatible controller in them:
Oh yeah I have a vague memory of B000. I was more interested in mode 13 (256 colors 360x200). I think it was starting from A000. I'm proud that I found that address by trial and error. I was able to put pixels on screen using `int 10h` but it wasn't fast enough to render my animations. I read in a translated (into Persian) tech book that there's something called screen memory but no mention of the address itself so I started from 0 and checked all blocks. I knew I didn't need to check every single byte. I was going like 0000, 0100, 0200, ... until I saw a few pixels on screen! Of course after a few crashes.
Impressive. Is there a reason to jump over the string instead of just having the string after the program? Seems like one could save two bytes doing that.
I wrote this about 20 years ago during my university days. I happened to stumble upon it today in my archives and thought of sharing it on GitHub. I was still learning microprocessors back then. While browsing the C:\Windows directory, I fortuitously happened to discover the DEBUG.EXE program. Turned out it was available on any standard installation of MS-DOS as well as Windows 98. That chance encounter helped me to dive into the world of assembly language programming much before the coursework introduced me to more popular assemblers.
Since I was still learning the x86 CPUs, the intention here was not to save bytes but instead to have something working. I believe I picked up the style of having the string at the top and jumping over it from other similar code I had come across in those days.
You are right of course. Here is a complete example that moves the string to the bottom and saves two bytes:
C:\>DEBUG
-A
1165:0100 MOV AH, 9
1165:0102 MOV DX, 10B
1165:0105 INT 21
1165:0107 MOV AH, 0
1165:0109 INT 21
1165:010B DB 'hello, world', D, A, '$'
1165:011A
-G
hello, world
Program terminated normally
-N HELLO.COM
-R CX
CX 0000
:1A
-W
Writing 0001A bytes
-Q
C:\>HELLO
hello, world
C:\>
I have now updated the GitHub repository with this updated source code and binary. Thank you for the nice comment!
Curious. These other programmers probably learned the habit from some even older code. Jumping over data isn't a very obvious way of organising code, so probably it served some purpose many years ago.
A friend of mine used to program the PDP-8 a lot. The PDP-8 didn't have a stack; to call a subroutine, the JMS FOO instruction would store the return address at (the effective address computed from) FOO, then continue execution at the following word. So to return, the called subroutine would do an indirect jump using the address stored in the memory word before the beginning of its code.
I was surprised when he told me how people normally passed arguments. They would put them after the JMS instruction in the caller. So, the callee would indirect from the "return address" stored in FOO to retrieve its arguments, incrementing the return address after each one. So the code to call FOO with arguments 1 and 2 would be (in Intel syntax)
Maybe someone had this habit in their assembly and adapted it to the 8086's calling convention by inserting MOVs and a JMP instruction?
You can also imagine that a compiler might be somewhat simpler if it generated code in the order in which it encountered the expressions. Gas lets you switch back and forth between .text and .data, so that if your compiler does this, you still don't have to insert JMP instructions. However, a few years ago when I wrote Ur-Scheme, I did do this for lambda expressions: the code for the lambda expression is generated inline in the middle of its containing code, which then has to contain a jump past it.
One case where it has a use is on partition (or floppy disk) boot sectors. First instruction is a jump to skip the disk metadata (actually a BIOS parameter block), which is in a fixed place. See https://en.wikipedia.org/wiki/Volume_boot_record
I remember seeing that in old Asm books too. My best guess is that it avoids having too many forward references, which would take up precious memory in the systems of the time and perhaps reach the limit of the assembler sooner.
This and the hard coding address comments are the key - forward looking references were expensive (and early enough, impossible) whereas you already knew where your data points were as they came before.
Maybe also better for linking the files. At least this was the reason I did it on Amiga when using absolute addresses. I only had to remember the start of the address area even when recompiling.
I've seen it done fairly often in mainframe assembler - embed an 'eyecatcher' string near the top of your program and jump to the real entry point. Used as an aid for debugging when reading dumps I believe. Not generally program data though, ususally metadata like program name, author, date assembled etc.
This is an ASM program with a very standard structure (including the standard printing API: http://spike.scu.edu.au/~barry/interrupts.html#ah09) using a very standard tool (DEBUG.exe, common at the time for quick debugging); I'm confused why this is impressive.
Most if not all ANCIENT assembers did a 1-pass translation of input source and thus would not recognize tokens that was referenced but not yet declared.
Hence you declare your variables at the start of the program and jump past them.
Any "real" assembler has to support forward references, otherwise you couldn't even jump past the initial data section, except by using an absolute address!
Minimizing the number of forward references may make the assembly run faster or use less memory, but usually it's just a convention to make the source code more neatly organized.
That mnemonic would be "JMP" on x86. At address 5 in the PSP is the CP/M-style syscall entry point, it is not advised to not use it because it relies on the A20 line being short-circuited to zero, which cannot be relied on in later MS-DOS versions.
I don't see why this would be faster than using RET directly and having the INT 20 opcode at address 0 do the work.
An 8 year old, using the ring bound manual that came with their computer, can figure out the basics - enough to write a program like this, and examine it. I’m not being dismissive here, but speaking literally from experience. Personally, it’s my opinion that computers were a lot simpler back then.
Nowadays we’re very, very far from the hardware - hardware that has grown very complicated in comparison.
The debugger and the 8086 instruction set is well documented, much better than the "modern" software that i have to work with at dayjob. Its not magic.
It's not the assembly language that I feel is like dark magic, but rather having a prompt where you essentially have almost unfettered access to the system, and have basically kernel privileges as long as you understand the hardware well enough (because MS-DOS memory protection is a bit lol).
What I'd really like to see is a way to boot FreeDOS on UEFI. Of course this would require a BIOS emulator somewhere, either inside a native UEFI version of FreeDOS (not very probable to happen, I think), or as an external UEFI executable that can then boot a BIOS system (parts of this do exist, I think).
Developing for "legacy" BIOS is easier at this point because there is less legacy to implement (Windows binaries, Win64 ABI, FAT file system). EFI is a huge pain osdev-wise and thats why nobody wants to do it.
It's interesting how ASM nowadays appears as a dark magic, since only a small fraction of the programmers are (rightfully) very far from that level.
Curiously, I found learning Rust way more challenging than learning 16-bit assembly (it was way simpler back then; no complex instructions, less baggage, simpler processors... and less expectations :)).
I've learned x86 16-bit assembly originally, and I find that most of that knowledge is still applicable when looking at assembly listings while debugging C/C++ today (which is the most likely area where one might have to deal with it in production these days; few people get to write asm from scratch).
For x86, at least, I wouldn't even say that it was less complex. Segmented memory alone is a huge complication, and then on top of that there was all the legacy CISC stuff like the BCD helpers or ENTER/LEAVE; x64 is comparatively streamlined.
Yeah, segmented memory was such a headache. The large memory model that used "far" pointers broke some things that were taken for granted in C. Comparing pointers especially was an easy place to introduce difficult to find bugs. A given memory location could have multiple valid pointers, so you couldn't do an int compare. The 32-bit int 0x00000011 is smaller than 0x00010000 but 0000:0011 pointed to the byte after 0001:0000.
I painstakingly entered so many bytes into DEBUG from the back pages of PC Magazine. It was an amazing experience when they added the ability to pay $4.95 or something cheap (for the time) to access "ZiffNet" as a walled-off section of CompuServe and download the utilities from there.
On other hand I would argue that something like javascript in modern browser is much more arcane and probably opaque.
At least with DEBUG and assembler I could reasonably reason and do things by hand. With many modern languages I have no idea where to even start checking what they do and how.