2

I was wondering if it was (or is) possible to build Win32 PE .exe programs on DOS 8086. (Of course it's not possible to run them, because that would need at least a 386 CPU.) The minimum I need for that is an assembler and a linker.

The assembler is easy. The following hello-world source code can be assembled by MASM 5.x (1987–, masm.exe), MASM 6.00B (1992, ml.exe) and TASM 3.0--4.1 (1991–, tasm.exe):

.386p  ; prog.asm
.model flat
.code
public _mainCRTStartup
extrn _GetStdHandle@4:BYTE  ; HANDLE __stdcall GetStdHandle(DWORD nStdHandle);  /* KERNEL32.DLL */
extrn _WriteFile@20:BYTE  ; BOOL __stdcall WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);  /* KERNEL32.DLL */
extrn _ExitProcess@4:BYTE  ; void __stdcall __noreturn ExitProcess(UINT uExitCode);  /* KERNEL32.DLL */
STD_OUTPUT_HANDLE equ -11
EXIT_SUCCESS equ 0
NULL equ 0
_mainCRTStartup:
        push STD_OUTPUT_HANDLE
        call near ptr _GetStdHandle@4  ; Returns stdout handle in EAX.
        push NULL
        push NULL
        db 6Ah, msg_end-msg  ; push msg_end-msg  ; https://stackoverflow.com/q/78313289/97248
        ;push msg_end-msg  ; This would cause `error A2006: Phase error between passes' in MASM 5.00.
        push offset msg
        push eax  ; stdout handle.
        call near ptr _WriteFile@20
        push EXIT_SUCCESS
        call near ptr _ExitProcess@4
msg     db 'Hello, World!', 13, 10
msg_end:
end _mainCRTStartup

Please note that even TASM 1.0 (1988) is able to generate code for 386 32-bit protected mode, but it doesn't support .model flat.

Linking is also easy on Win32 and on 32-bit DOS:

  1. link.exe in Microsoft Visual C++ (earliest: 1.0 for NT, 1993) or Visual Studio, with the corresponding kernel32.lib works, e.g.: link /nologo /SUBSYSTEM:CONSOLE /OPT:NOREF prog.obj kernel32.lib.

    This command with link.exe from Microsoft Visual C++ 1.0 for NT works on 32-bit DOS if the files dosxnt.exe and dosxnt.386 are also in the same directory as link.exe.

  2. wlink.exe in Watcom C/C++ (earliest: 9.5b, 1993) works, e.g wlink @win32con.lnk f prog.obj.

    This command works on either Win32 or 32-bit DOS, depending on which wlink.exe is run from Watcom C/C++ 9.5b.

    win32con.lnk contains:

    format windows nt
    runtime console
    option quiet
    import '_GetStdHandle@4' 'kernel32.dll'.GetStdHandle
    import '_WriteFile@20'   'kernel32.dll'.WriteFile
    import '_ExitProcess@4'  'kernel32.dll'.ExitProcess
    

However, these linkers don't run on DOS 8086, and I haven't been able to find any linker which does. Is there any linker producing a Win32 PE .exe program which runs on DOS 8086?

I'm using emu2 for testing, that's a DOS emulator which doesn't support the 386 instructions.

I've found WALK32 (see also the WALK32 document and the win32asm.txt document), which contains the W32Link.exe linker (with source code, see also the W32Link NASM source code), which runs on DOS 8086. However, that WALK32 requires substantial changes to the assembly source code above, specifically the WALK32 macros have to be used. I'm looking for a linker which accepts OMF or COFF object files with vanilla assembly like the prog.asm above.

FYI OPTLINK 5.1 (1994) by SLR systems wasn't able to create Win32 PE .exe files (proof).


Even if such a linker doesn't already exist, it would be possible. I imagine it like this:

  • Input: OMF .obj files created with .model flat. No .lib files. No resource files. At most 236 input .obj files are supported.

  • Output: Win32 PE .exe. Console by default, but the byte can be changed to GUI.

  • In the beginning, .obj filenames are specified on the command-line, but that has a 126-character limit on DOS, so in the second version .obj file names can be read from a file.

  • All undefined symbols containing a @ (e.g. _ExitProcess@4) are assumed to be defined in kernel32.dll, and the corresponding import code will be added to the .exe output.

  • In the future, support importing from other DLLs. Until then, programs can use _LoadLibraryA@4 and _GetProcAddress@8 to call functions in other DLLs.

  • Memory usage:

    • It only uses conventional memory. DOS conventional memory is 640 KiB maximum, but BIOS, DOS and resident programs also use some of it, so the available memory is between 500 KiB and 635 KiB.

    • Program size and memory image are at most 64 KiB. It uses the small memory model (16-bit code and data pointers), except for the symbol table, for which pointers are 32-bit (segment:offset).

    • The symbol table is organized in a single red-black tree. Each entry contains the symbol name, terminating NUL for the symbol name, the symbol address (4 bytes), flag byte, .obj file identifier byte. Only imported symbols and symbols which are defined in a different .obj file need to be stored. (Is this true for the FIXUPPs generated by the common assemblers?)

    • Each symbol table entry uses 12 bytes (5 bytes for the packed red-black tree pointers and color, 1 byte for the terminating NUL, 1 flag byte, 1 .obj identifier byte, 4 bytes for the symbol address) + the size of the symbol name. For example: _ExitProcess@4 uses 12 + 14 bytes. A custom memory allocator is used for the symbol table, which leaves no gap, so there is no malloc overhead beyond the formula above.

    • 4 KiB is used for the last LEDATA OMF record. (We need to keep it in memory to apply fixups in the subsequent FIXUPP record.)

    • 3 KiB is used for storing the input .obj file names.

    • 4 KiB is used for other I/O buffers.

    • 1 KiB is used for the stack.

    • 1 KiB is used for other global variables (zero-initialized at startup).

    • Assuming (pessimistically) that 500 KiB of conventional memory is free, we have 500 - 64 - 4 - 3 - 4 - 1 - 1 == 423 KiB free for the symbol table. Assuming (pessimistically) that symbol names are 18 bytes long on average, we have room for 423 * 1024 / (12 + 18) >= 14438 symbols. That's plenty.

  • The linker can be written either in 8086 assembly, or in C and the linked with dosmc. The advantage of using C is that the linker can be compiled to Win32 and other systems for debugging and native testing.

1
  • 1
    I remember doing some experiments with the new PE format ca.1993 for Win32s and WinNT3.1, trying to create programs running under DOS as well as WindowsNT from the same binary. Although, I did not use a Linker but created all tables 'manually'.
    – Raffzahn
    Commented Apr 12 at 23:08

1 Answer 1

1

It's certainly possible to write a linker for WIN32 PE that runs on 8086 MS-DOS. I'm not surprised that nobody seems to have done it, because when these technologies were emerging, Intel-based computers were getting faster quite quickly.

Since any work at the time writing PE programs would require a real machine to test them on, and that machine would be far faster than any 8086-only machine, there would not have been any reason to write an 8086-only linker.

1
  • 3
    If the linker was written in a high level language like C or Pascal, it could be compiled for 8086 as well. The resources available during development does not necessarily dictate what the final product looks like. Commented Apr 14 at 23:05

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .