DOS stubs
by X-Calibre (of Diamond)



Essay Level: Intermediate
Date: 4-6-'99
Tools used:
  • ASM compiler & linker
  • Hexeditor

As you may (or may not) know, there is a piece of DOS code still in every Win32 executable file. This piece of code is referred to as the 'stub' and ensures that the Win32 program won't cause a crash when run on a DOS system. It just prints the familiar 'This program can not be run in DOS' message and exits.

'So what do we care?' you might ask... Well, Microsoft's linker provides the option to link your own stub instead of the standard one. And, you must have guessed it already by now: We can do it better than Microsoft!

So, how do we do this then?

Well, actually it's very simple: The first part of the Win32 executable is literally a DOS file. There's just one small requirement: at offset 3Ch (60) there is a DWORD specifying the start of the PE block relative to the start of the file (the offset).

So basically you can just put any DOS EXE program in there, as long as you make sure that there is room for the DWORD at offset 3Ch in the file. Usually this is no problem, since the EXE header itself is usually quite big, and a lot of the space is not being used. Microsoft's own stub has an empty header mostly, and the code starts right after the DWORD, at offset 40h.

That's all fine and nice and whatever, but what can we do with this info? Well, you could link in an entire DOS program for people not using Windows (Look at REGEDIT.EXE in Windows 9x for an example. How about including a Fire or Plasma effect when your program is run in DOS?).
You could create your own 'This program can not be run in DOS mode' message. But, most importantly: you can create smaller EXE files! One of the nicer applications of this stub, which I'm going to explain a bit here.

What is the smallest size for the stub, theoretically speaking?

Well, considering the fact that at offset 60, there MUST be an offset pointing to the PE header, the minimum size will be 60 bytes.
The actual stub file has to be 64 bytes, because of restrictions of Microsoft's linker. But be sure not to use the last 4 bytes, since the linker will put in the offset there.

Well, so in 60 bytes, you can't really do much. But just printing a small warning for DOS users and then exit is just about possible. Microsoft made their version a little large: 120 bytes. So we can try to do just about the same in 60 bytes.

We're going to use a little trick here, to get the program as small as 60 bytes. At offset 20h, there is room for a relocation table for the code. But since we won't be needing them, we're going to put our code in there. This is perfectly possible, because you can specify how many relocation table items your program will be using. We just put in a 0 word at offset 6 in the header, and the table is ours.

For all you non-DOS coders out there, this is what the program looks like:
(You might want to skip this piece, learn DOS ASM and make it yourself. The knowledge will come in handy someday.)


.Model Tiny
 
.code
start:
    push cs      ; Point the data segment to the code segment, since
    pop  ds      ; we're putting the data after the code to save space.
 
    mov  dx, offset message ; Load pointer to the string for the call.
    mov  ah, 9              ; 9 is the print argument for int 21h.
    int  21h                ; The DOS interrupt.
 
    mov  ah, 4Ch            ; 4C is the exit argument for int 21h
    int  21h
 
; Put our string here
message db      'Windows prg!',0Dh,0Ah,'$'
 
; A little explanation may be required:
;
; 0Dh is the 'Carriage return' ASCII code.
; 0Ah is the 'Line feed' ASCII code.
; '$' is the string-terminator in DOS (like 0 is in Windows and other C based
; OSes)
end start

The message can be 15 bytes at most, including the string terminator, since the program itself starts at offset 32 in the file, and is 12 bytes long. (32+12+15 = offset 59 bytes, so the next byte will be used for the PE offset DWORD).

This version yields an undefined error code on exit. The error code is specified in al when you call the exit DOS function. The errorcode actually depends on the output in al of the int 21h call that prints the string. This is ofcourse undefined (actually it is 24h in Windows 98).

Microsoft's stub has a defined errorcode of 1. If you want to make your stub 100% the same, then you must replace the 'mov ah, 4Ch' with 'mov ax, 4C01h'. Mind you, that this code is 1 byte longer, so your message can then be only 14 bytes long in total.

Since I'm never going to use the errorcode, I decided to save the byte and use a larger string.

And that's that. Now you may run into trouble with the linker. I couldn't find a linker that kept the EXE header to its minimum (which is 32 bytes). I used TLINK, which made a 512 byte header. So I just edited the file manually, and got it to its minimum size. A document explaining the EXE header format is enclosed, and so is the STUB.EXE I made, and a small Win32 application using it (with relocated PE header at 40h). Download these now.

Well, after we have created our DOS stub, all we have to do is link it in. With Microsoft's linker it goes like this:

LINK code.obj /SUBSYSTEM:WINDOWS /STUB:STUB.EXE

And that's all you need! You can ignore the warning the linker gives about the incomplete header. We know that the program runs. The linker just doesn't consider EXE headers with no relocation table (which could actually be considered a bug, since our EXE header specifies that the table has length 0, and therefore the code can start at offset 20h. The DOS EXE loader does interpret it correctly, so in fact, the linker could be considered incompatible).

The only problem with Microsoft's linker is that it doesn't seem to want to link the PE block right after the DOS stub. Maybe other linkers do, but I haven't found one that does yet. Microsoft's linker just dumps some garbage, and then puts its PE block at offset 78h. Maybe that is because their stub is 78h bytes long and they don't consider shorter stubs? The offset at which the PE block is linked depends on the initial SP value specified at offset 10h, actually (why is that?). It can also link at offset 80h or 88h. You could move the PE block to offset 40h, and pad with 0's after the PE block, using a hex-editor. This way it will compress even better, maybe. And you could perhaps edit the PE block and move the code forward a bit too (there's a great util in this. Shall we make it?).

Well, anyway... Have fun, and get crazy with your custom DOS stubs!

And remember:

DOS Knowledge is power!

X-Calibre


This essay is copyright 2000 by X-Calibre (of Diamond).