Skip to content

Commit

Permalink
Use CWSDSTUB.EXE to produce standalone executables
Browse files Browse the repository at this point in the history
CWSDSTUB.EXE includes a copy of CWSDPMI and loads the DJGPP COFF that is
appended to it. The DJGPP toolchain is still not needed; COFFs are
produced by a linker script following the spec:
http://www.delorie.com/djgpp/doc/coff/.

Since executables are standalone, DOSBox (the most popular DOS emulator)
can now be used to run the demo program.

I'm quite proud of the work I've done on the ELF loader, but it is no
longer needed.
  • Loading branch information
jayschwa committed Oct 22, 2020
1 parent ee3c0bc commit a6d47b2
Show file tree
Hide file tree
Showing 12 changed files with 185 additions and 559 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "deps/cwsdpmi"]
path = deps/cwsdpmi
url = https://github.com/jayschwa/cwsdpmi.git
53 changes: 25 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ Write and cross-compile [DOS](https://wikipedia.org/wiki/DOS) programs with the
[Zig programming language](https://ziglang.org). Programs run in 32-bit
[protected mode](https://wikipedia.org/wiki/Protected_mode) and require a
resident [DPMI host](https://wikipedia.org/wiki/DOS_Protected_Mode_Interface).
[CWSDPMI](https://sandmann.dotster.com/cwsdpmi) is required for environments
that do not have DPMI built-in.
[CWSDPMI](https://sandmann.dotster.com/cwsdpmi) is bundled with the executable
for environments that do not have DPMI available.

To comply with the [CWSDPMI license](https://sandmann.dotster.com/cwsdpmi/cwsdpmi.txt),
published programs must provide notice to users that they have the right to
receive the source code and/or binary updates for CWSDPMI. Distributors should
indicate a site for the source in their documentation.

This package is in a primordial state. It is a minimal demonstration of how to
create a simple DOS program with Zig. Only basic file/terminal input/output are
Expand All @@ -17,7 +22,7 @@ you wish to adapt it for your own needs.
Install:

- [Zig](https://ziglang.org/download) (master)
- [DOSBox-X](https://dosbox-x.com)
- [DOSBox](https://www.dosbox.com)

Run:

Expand All @@ -27,27 +32,25 @@ zig build run

## Design

There are four main components of this package:
There are five main components of this package:

- [DOS API](https://stanislavs.org/helppc/int_21.html) wrappers call the 16-bit
real mode interrupt 21 routines (via DPMI). A transfer buffer residing in DOS
memory is used if the program data resides in extended memory.
- [DPMI API](http://www.delorie.com/djgpp/doc/dpmi) wrappers are used to enter
protected mode and manage memory.
- An [ELF](https://wikipedia.org/wiki/Executable_and_Linkable_Format) loader
program is an [MZ executable](https://wikipedia.org/wiki/DOS_MZ_executable)
(accomplished with a linker script) that can load and run an ELF file. It is
currently a standalone file, but will eventually act as a stub that can be
prepended to an ELF file.
- The demo program is an ELF that exercises all of the above.
real mode interrupt 21 routines (via DPMI) and implement operating system
interfaces for the Zig standard library.
- [DPMI API](http://www.delorie.com/djgpp/doc/dpmi) wrappers manage extended
memory blocks and segments.
- A custom [linker script](https://sourceware.org/binutils/docs/ld/Scripts.html)
produces [DJGPP COFF](http://www.delorie.com/djgpp/doc/coff) executables.
- The [CWSDPMI](https://sandmann.dotster.com/cwsdpmi) stub loader enters
protected mode and runs the COFF executable attached to it.
- A small demo program exercises all of the above.

## Roadmap

- Proper error handling.
- Parse environment data (command, variables) and hook into standard library abstractions.
- Implement `mprotect` for stack guard and zero pages.
- Implement a `page_allocator` for the standard library.
- Turn the ELF loader into prependable program stub.
- Add graphical demo program.

## Questions and Answers
Expand All @@ -62,17 +65,11 @@ the oldest CPU that can be targeted is an Intel 80386, which supports 32-bit
protected mode. It's guaranteed to be there and has a lot of advantages, so we
might as well use it.

### Why not piggyback off an existing DOS toolchain like DJGPP?

This was attempted and had mixed results. DJGPP's COFF and EXE formats are
subtly different from the ones produced by modern toolchains. Intermediate
conversion scripts had to be used to get them to work with DJGPP's linker and
stub tools, and it was not fool-proof. Ultimately, it felt like more trouble
than it was worth. Not relying on a separate toolchain will make things easier
in the long run.

### Why ELF instead of PE/COFF (i.e. the Windows executable format)?
### Why not integrate with an existing DOS toolchain like DJGPP?

Neither ELF nor PE/COFF are understood by DOS or any existing DOS extenders.
With that in mind, ELF was selected because it seems simpler, is the more
ubiquitous format overall, and Zig's standard library has a good ELF parser!
This was attempted and had mixed results. DJGPP's object format (COFF) is
subtly different from the one produced by modern toolchains such as Zig. Crude
conversion scripts had to be used to get them to work together and it was not
robust. While it would be nice to leverage DJGPP's C library, it ultimately
felt like more trouble than it was worth. Not relying on a separate toolchain
will make things easier in the long run.
70 changes: 26 additions & 44 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,55 +8,37 @@ pub fn build(b: *Builder) !void {
.Debug => .ReleaseSafe, // TODO: Support debug builds.
else => |mode| mode,
};
const dos16 = try CrossTarget.parse(.{
.arch_os_abi = "i386-other-code16",
.cpu_features = "_i386",
});
const dos32 = try CrossTarget.parse(.{
const coff_exe = b.addExecutable("demo", "src/demo.zig");
coff_exe.disable_stack_probing = true;
coff_exe.addPackagePath("dos", "src/dos.zig");
coff_exe.setBuildMode(.ReleaseSafe);
coff_exe.setLinkerScriptPath("src/djcoff.ld");
coff_exe.setTarget(try CrossTarget.parse(.{
.arch_os_abi = "i386-other-none",
.cpu_features = "_i386",
});

// 16-bit ELF loader
const exec_elf_exe = setup(b.addExecutable("execelf", "src/exec_elf.zig"));
exec_elf_exe.setBuildMode(mode);
exec_elf_exe.setTarget(dos16);
exec_elf_exe.setLinkerScriptPath("src/mz.ld");
exec_elf_exe.installRaw("execelf.exe"); // DOS (MZ) executable

// 32-bit demo program
const demo_exe = setup(b.addExecutable("demo", "src/demo.zig"));
demo_exe.setBuildMode(mode);
demo_exe.setTarget(dos32);

// Conserve the amount of space required at runtime by loading the executable
// data at 4 KiB (right after the zero page), not 4 MiB.
demo_exe.image_base = 0x1000;
}));
coff_exe.single_threaded = true;
coff_exe.strip = true;
coff_exe.installRaw("demo.coff");

demo_exe.install();
var cat_cmd = std.ArrayList(u8).init(b.allocator);
// TODO: Host-neutral concatenation.
try cat_cmd.writer().print("cat deps/cwsdpmi/bin/CWSDSTUB.EXE {} > {}", .{
b.getInstallPath(.Bin, "demo.coff"),
b.getInstallPath(.Bin, "demo.exe"),
});
const stub_cmd = b.addSystemCommand(&[_][]const u8{
"sh", "-c", cat_cmd.items,
});
stub_cmd.step.dependOn(b.getInstallStep());
const stub = b.step("stub", "Prepend stub to COFF executable"); // TODO: Incorporate into install.
stub.dependOn(&stub_cmd.step);

const run = b.step("run", "Run the demo program in DOSBox-X");
var mount_arg = std.ArrayList(u8).init(b.allocator);
try mount_arg.writer().print("mount c {}", .{b.getInstallPath(.Bin, "")});
const run_args = [_][]const u8{
"dosbox-x",
"-fastlaunch",
"-c",
mount_arg.items,
"-c",
"c:",
"-c",
"execelf.exe",
};
const run_cmd = b.addSystemCommand(&run_args);
const run_cmd = b.addSystemCommand(&[_][]const u8{
"dosbox", b.getInstallPath(.Bin, "demo.exe"),
});
run_cmd.step.dependOn(b.getInstallStep());
run_cmd.step.dependOn(stub);
run.dependOn(&run_cmd.step);
}

fn setup(obj: *LibExeObjStep) *LibExeObjStep {
obj.addPackagePath("dos", "src/dos.zig");
obj.disable_stack_probing = true;
obj.single_threaded = true;
obj.strip = true;
return obj;
}
1 change: 1 addition & 0 deletions deps/cwsdpmi
Submodule cwsdpmi added at ed2b39
108 changes: 108 additions & 0 deletions src/djcoff.ld
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* Linker script to produce a DJGPP COFF executable. */
/* See: http://www.delorie.com/djgpp/doc/coff/ */

PROVIDE(_stack_size = 0x8000); /* TODO: Get stack size from toolchain? */

SECTIONS {
. = 0;
/* COFF file header */
.file_head : {
SHORT(0x14c) /* Magic number */
SHORT(3) /* Section count */
LONG(0) /* Timestamp */
LONG(0) /* Symbol table file offset */
LONG(0) /* Symbol count */
SHORT(SIZEOF(.opt_head))
SHORT(0x10f) /* Flags */
}
/* Optional (executable) header */
.opt_head : {
SHORT(0x10b) /* Magic number */
SHORT(0) /* Version */
LONG(SIZEOF(.text)) /* Text section size */
LONG(SIZEOF(.data)) /* Data section size */
LONG(SIZEOF(.bss)) /* BSS section size */
LONG(ABSOLUTE(_start)) /* Entry point */
LONG(ADDR(.text)) /* Text file offset */
LONG(ADDR(.data)) /* Data file offset */
}
/* Text section header */
.text_head : {
/* Section name */
BYTE(0x2e) /* . */
BYTE(0x74) /* t */
BYTE(0x65) /* e */
BYTE(0x78) /* x */
BYTE(0x74) /* t */
BYTE(0)
BYTE(0)
BYTE(0)

LONG(ADDR(.text)) /* Physical address */
LONG(ADDR(.text)) /* Virtual address */
LONG(SIZEOF(.text)) /* Section size */
LONG(ADDR(.text)) /* File offset to section */
LONG(0) /* File offset to relocations */
LONG(0) /* File offset to line numbers */
SHORT(0) /* Relocation count */
SHORT(0) /* Line number count */
LONG(0x20) /* Flags */
}
/* Data section header */
.data_head : {
/* Section name */
BYTE(0x2e) /* . */
BYTE(0x64) /* d */
BYTE(0x61) /* a */
BYTE(0x74) /* t */
BYTE(0x61) /* a */
BYTE(0)
BYTE(0)
BYTE(0)

LONG(ADDR(.data)) /* Physical address */
LONG(ADDR(.data)) /* Virtual address */
LONG(SIZEOF(.data)) /* Section size */
LONG(ADDR(.data)) /* File offset to section */
LONG(0) /* File offset to relocations */
LONG(0) /* File offset to line numbers */
SHORT(0) /* Relocation count */
SHORT(0) /* Line number count */
LONG(0x40) /* Flags */
}
/* BSS section header */
.bss_head : {
/* Section name */
BYTE(0x2e) /* . */
BYTE(0x62) /* b */
BYTE(0x73) /* s */
BYTE(0x73) /* s */
BYTE(0)
BYTE(0)
BYTE(0)
BYTE(0)

LONG(ADDR(.bss)) /* Physical address */
LONG(ADDR(.bss)) /* Virtual address */
LONG(SIZEOF(.bss)) /* Section size */
LONG(0) /* File offset to section */
LONG(0) /* File offset to relocations */
LONG(0) /* File offset to line numbers */
SHORT(0) /* Relocation count */
SHORT(0) /* Line number count */
LONG(0x80) /* Flags */
}
.text : {
*(.text*)
}
.data : {
*(.rodata*)
*(.data*)
}
.bss : {
*(.bss*)
. += _stack_size;
. = ALIGN(16);
_stack_ptr = ABSOLUTE(.);
}
}
1 change: 0 additions & 1 deletion src/dos/dpmi.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pub usingnamespace @import("dpmi/init.zig");
pub usingnamespace @import("dpmi/interrupts.zig");
pub usingnamespace @import("dpmi/mem.zig");
pub usingnamespace @import("dpmi/paging.zig");
Expand Down
71 changes: 0 additions & 71 deletions src/dos/dpmi/init.zig

This file was deleted.

Loading

0 comments on commit a6d47b2

Please sign in to comment.