Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
A 23-byte “hello, world” program assembled with DEBUG.EXE in MS-DOS (github.com/susam)
196 points by susam on Oct 30, 2022 | hide | past | favorite | 111 comments


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 '$'
22 bytes, of which 15 is the message.


Wow from feedback to commit in 30 minutes. https://github.com/susam/hello/commit/36fa08e7cafb7c5268b651...


seems like a reasonable pace on a Sunday


Our PR dept wanted a more chill out version. Shaving a byte to the sound of ukuleles:

    db '4LOHA- W0rld' 0d 0a '$'
    xchg ax, bp
    xchg cx, dx
    int 21
    ret


Updated the repository to use your RET suggestion. Thanks!


With no attribution nevertheless :) (/s)

I love how, without the 'hello, world' message itself, 25% of your entire HELLO.ASM codebase is from a random HN comment

Edit: I saw the new commit, HN is lovely


Thanks for pointing out the missing attribution. Added a credits section to the README here: https://github.com/susam/hello#credits

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.


I genuinely think you’re misusing the smiley face or sarcasm tag if you think it’s possible for that first line to include both.


As seen in 2022: Hello world example with 350 contributors.

Beat the enterprise Fizbuzz by 25 contribs.


I was going to suggest the RET but I didn't remember the BP being set to save the additional byte. Nice, albeit sacrificing some compatibility.

For anybody interested some DOS default register values documented: http://www.fysnet.net/yourhelp.htm


It's an old demoscene trick, so perhaps a bit obscure but somewhat common in the sizecoding community.


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.


Yes, although this was for COM files only, which were limited to (64k - 0x100) bytes. EXEs didn't have this limitation, but they indeed had a header.


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.


Actually, .com files can be larger. I don't know how exactly it was handled, but I distincly remember seeing versions of edit.com >64k


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.

[0]: https://en.wikipedia.org/wiki/Overlay_(programming)


They were .exe internally.


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.


One of my favorite old bbs taglines was "Real programmers use copy con file.exe"

Mighta been file.com. Mighta been real men.


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):

x)y'!B:)),DM!G@))p,T]l%)),@@@Hello, world!$XP5]A%=!P[PZ^V!wV!wX5.<(GV(gX534,!(GWr!

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.


I still create one liner batch files using copy con <batchfilename.bat>


alt+f6 <enter> to save


IIRC it's just F6, which is simply an alias for Ctrl+Z (ASCII code 26). That was the end-of-file marker on CP/M.


I stand corrected, you're right. You only need to press F6 <enter>.


Programming smart contracts is like writing to a bunch of distributed microcontrollers

The limitations are similar and you can do assembly level coding


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.


And before that, they were toggled directly into the machine using physical switches!


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.


You are right. It was DEBUG.COM. Indeed it did not have an assembler. It had a disassembler though. An archived copy from MS-DOS v1.25 can be found here: https://github.com/microsoft/MS-DOS/blob/master/v1.25/bin/DE...

  C:\>DEBUG.COM
  -A
   ^ Error
  -N HELLO.COM
  -L
  -U 100 107
  0340:0100 B409          MOV     AH,09
  0340:0102 BA0801        MOV     DX,0108
  0340:0105 CD21          INT     21
  0340:0107 C3            RET
  -
There is another copy of the debugger for MS-DOS v2.0 here: https://github.com/microsoft/MS-DOS/blob/master/v2.0/bin/DEB... This one does does have an assembler.

  C:\>DEBUG.COM
  -A
  0482:0100 MOV AH, 2
  0482:0102 MOV DL, 41
  0482:0104 INT 21
  0482:0106 RET
  0482:0107
  -N A.COM
  -R CX
  CX 0000
  :7
  -W
  Writing 0007 bytes
  -Q

  C:\>A
  A
  C:\>


Here's a one file disassembler for x86 and its successors:

https://github.com/dlang/dmd/blob/master/compiler/src/dmd/ba...

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.

It dates back to CP/M days.


Before that, on vax, COM files were text files that contained commands to execute.


You could load the 12 characters directly into the screen memory and make useful use of the meta data.


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.


Eventually, Borland came out with the Turbo Debugger and Turbo Assembler, "only" $100, which was a steal compared to Microsoft tools.


AH = 0? I only recall AH = 4C?

That would exit the program with AL being the exit value. I'm not aware of AH = 0?

Sadly, I used to write these kinds of programs with a hex editor, so "B4 4C CD 21" is etched into my brain even though "CD 20" was shorter.


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.


4C is correct. My INT 21 function codes memory is a bit fuzzy.


int 20h for .com, and ah=4ch; int 21h; for .exe


This was like a time travel!!

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.


> 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.

I have that printed and bound somewhere in my parent's house! great memories.


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.


Was that "g =c800:5" or "g =c800:0" ?

It's probably been over 3 decades since I had to use that command, but I still remember it for some reason...


This brings back memories!

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.

Why did MS stop shipping it???


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).


DEBUG.COM begat DEBUG.EXE which did not survive the move to x64 on Windows.


So nostalgic. All the work is done by 21h interrupt though. 10h is faster. And the fastest is to put characters in 0xB800 memory address.

How do I remember that number without looking it up?


...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:

https://01.org/sites/default/files/documentation/intel-gfx-p...


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.


mode 13h was 320x200 resolution with 256 colors. A000h was the top left pixel.


Meh. I still remember my ICQ #.


For these kinds of things, there's a wiki nowadays: http://www.sizecoding.org/wiki/Main_Page

It's a very welcoming scene. Highly recommended, though I'll warn you that sizecoding can be an addictive hobby.


Brings back memories of 35 or so years ago compiling C with Turbo-C 1.5 using the Tiny model to create an .EXE and then running the DOS command:

EXE2BIN program.exe program.com

Apps were super tiny doing this. Nice to see someone taking it to the extreme with Assembly.


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.

Maybe someone here knows what was that purpose?


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)

        JMS FOO
        DW 1
        DW 2
        [code to run after returning]
Mark Smotherman explains this in more detail, using PDP-8-compatible syntax, in https://people.computing.clemson.edu/~mark/subroutines/pdp8.....

It seems sort of intuitive to do things this way; if you're writing atan2(3, 4), it's nice to be able to translate this to

        jms    atan2
               3
               4
rather than, say, mov $3, %rdi; mov $4, %rsi; call atan2.

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.


I wrote a compiler for a small machine that did this so that it could output the string content straight away without having to buffer it in memory.


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


you can hard-code values if you know their address in memory. If the data section comes first, then it doesn't move around if your code size changes.


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.


DEBUG.EXE is present in all versions of MS-DOS since the very first.



It was a .COM file back then but still the same program.


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.


debug.exe is single pass and doesn't do labels so by having the string first you know its address later.


You can compute the address of the string yourself like a two-pass assembler would, though, so that shouldn't be limiting.


The COM executable gets loaded by DOS at address 100h, so the first bytes have to be executable code, if memory serves me well?


I think OP is saying what if you wrote it as:

        mov ah, 9
        mov dx, offset helloworld
        int 21
        mov ah, 0
        int 21
    .helloworld:
        db 'hello, world', d, A, '$'


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.


You are right and sister comment to this one is wrong. Thusly:

MOV AH, 9

MOV DX, str

INT 21

MOV AH, 0

INT 21

Str:

DB 'hello, world', d, A, '$'


This program can be simplified further:

  MOV AH, 9
  MOV DX, str
  INT 21
  RET
  str:
  DB 'hello, world', d, A, '$'
Why can

  MOV AH, 0
  INT 21
be replaced by RET? Here is the answer: https://stackoverflow.com/a/60805758

UPDATE: Under https://news.ycombinator.com/item?id=33398592 userbinator posted an additional possible optimization.


Not a size optimisation, but a performance optimisation...

  INT 21
  RET
can be replaced with

  JP 5


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.


Jump parity? But according to http://www.fysnet.net/yourhelp.htm the parity flag is only set in DOS 7.1+


Getting close to the Kolmogorov complexity of hello world, I guess. But a bit disappointing that it is still longer than the length of the string.

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


When did DEBUG become an .EXE file instead of a .COM file itself?


Iirc MS-DOS 5 and older was DEBUG.COM.

Many shipped apps in MS-DOS 6 had became .EXE:s.

One limit of a .COM is that it is one segment size (64 kilobytes) max file size.


23 bytes is a 'long' program, my shortest is 17 bytes:

mov eax, $c08e0040

Beat that!


Why Debug.exe instead of Masm or Tasm?


DEBUG.EXE is part of the base system.

Essentially, from MS DOs 2.0 onwards, with DEBUG.EXE you had an “adequate” development environment. No LSP, though :)


Debug ships with DOS itself, so it's available on every system.

Masm and other assemblers have to be obtained and installed separately.


ahhh a bit of time travel to the mid-1980's ....love it


DEBUG.EXE is some necronomicon-tier dark magic.


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.


They were pretty simple, but to be fair INT 21h did a lot of heavy lifting on DOS, since it would handle the screen, files, etc.


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).

If that isn't dark magic I don't know what is.


This is why i really like the FreeDOS project, enabling future generations of hackers to experience this level of access to the computer.


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.


What do you mean? It's a simple straightforward debug tool, or a monitor as it used to be called in 8-bit systems.


PC magazines used to publish source code listings for little utilities in the form of DEBUG scripts.


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.

For those who want to re-live the moment, look at this PDF starting on internal page 873 (file page 894): http://vtda.org/books/Computing/OperatingSystems/DOSPowerToo...


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.




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

Search: