Crusader: No Remorse — Phar Lap DOS Extender Analysis
This file covers the Phar Lap 286 DOS extender code portion of CRUSADER.EXE — the outer MZ/P2 stub that bootstraps the NE game image.
Architecture Notes
Correction: The Game Ships As A Bound NE Executable
Important: The installed copy does not contain a separate .EXP file. CRUSADER.EXE is a bound executable with an outer DOS MZ stub and an internal NE executable image. The Phar Lap loader/runtime code and the game's real segment layout are both described inside this same file.
The flow is:
entry → checks DOS version, CPU type
init_dos_extender → sets up protected mode (VCPI/DPMI)
load_exp_file → opens the game's .EXP file
load_executable_image → parses P2/MZ headers, creates segments, applies relocations
task_switch_to_child → transfers control to the actual game code
For the installed retail copy, this means the currently loaded Ghidra program is only one interpretation of CRUSADER.EXE. The next import should target the NE layer of the same file, not a missing external .EXP.
Segment 1339: Fast Memory Operations
FUN_1339_02a8 contains an unrolled loop (Duff's device pattern with 57 iterations) — a hand-optimized fast memory fill/add routine, typical in DOS game graphics engines.
EMS Memory (Segment 1677)
The game uses EMS (Expanded Memory Specification) via INT 67h for additional memory beyond the 1MB real-mode limit. Functions in segment 1677 manage EMS page frames and handle allocation/deallocation.
Named Functions
Entry & Startup
| Address |
Name |
Description |
10da:7c40 |
entry |
Program entry point — checks CPU, parses command line, launches game |
10da:1816 |
main_init_and_run |
Main initialization — loads child EXP, sets up subsystems, runs game |
1760:1432 |
parse_cmdline_and_run |
Parses command-line args and invokes main_init_and_run |
1760:42fa |
init_dos_extender |
Initializes Phar Lap 286 DOS extender (CPU check, VCPI/DPMI setup) |
Executable Loading
| Address |
Name |
Description |
1760:2cdf |
load_exp_file |
Loads .EXP executable — opens file, reads headers, allocates memory |
1760:1dfc |
load_executable_image |
Parses P2/MZ headers, loads segments, creates LDT entries |
1760:24a6 |
apply_relocations |
Applies segment relocations to loaded executable |
1760:5eca |
exec_child_process |
Executes child process with command-line arguments |
1760:5fee |
exec_program_with_args |
Builds command line, locates and executes a program |
10da:1f7e |
load_and_run_child |
Wrapper: loads child EXP and initializes it |
System Services
| Address |
Name |
Description |
10da:2330 |
dos_exit |
Calls INT 21h AH=4Ch (terminate program) |
1760:42aa |
detect_cpu_type |
Detects CPU: 0=8086, 2=286, 3=386+ |
1339:04a6 |
dpmi_set_interrupt_vector |
INT 31h — DPMI set interrupt vector |
1339:06ca |
switch_to_real_mode |
Switches CPU from protected to real mode |
1339:06f2 |
switch_to_protected_mode |
Switches CPU from real to protected mode |
1339:0076 |
setup_interrupt_handlers |
Configures interrupt vectors via INT 21h |
1339:0a38 |
dos_int21h_wrapper |
Simple INT 21h call wrapper |
1339:0a82 |
dos_int21h_with_regs |
INT 21h call with register parameters |
10da:2360 |
get_flags_register |
Returns CPU FLAGS register |
10da:2363 |
set_flags_register |
Sets CPU FLAGS register |
Memory Management
| Address |
Name |
Description |
1677:0d12 |
cleanup_ems_memory |
Frees EMS (INT 67h) memory handles |
10da:14fc |
init_stack_fill_cc |
Fills stack with 0xCC (INT 3) for debugging/guard |
10da:1706 |
get_segment_base_addr |
Computes linear base address from segment descriptor |
Task Management
| Address |
Name |
Description |
10da:19ca |
task_switch_to_child |
Context switch to child process |
10da:1946 |
task_switch_from_child |
Context switch back from child process |
10da:1af4 |
call_termination_handler |
Calls registered termination callback |
I/O & Output
| Address |
Name |
Description |
10da:00d6 |
flush_output_buffer |
Flushes buffered output via function pointer |
10da:0132 |
putchar_buffered |
Writes character to buffer, flushes on newline |
10da:0808 |
memcopy_to_buffer |
Copies N bytes from source to destination buffer |
10da:178c |
print_error_message |
Formats and prints load error (references "not loaded: %s") |
10da:09e4 |
print_fatal_error |
Prints "Fatal Error" prefix + message |
10da:192a |
print_internal_error |
Prints "Internal Error" message |
Interrupt Management
| Address |
Name |
Description |
10da:1ec0 |
restore_interrupt_vectors |
Restores INT 2Fh and INT 67h vectors |
10da:2249 |
restore_int_2f_67 |
Restores INT 15h vector if saved |
1760:3d86 |
init_system_check |
Validates system (CPU, DOS version, VCPI/DPMI, memory) |
Utility
| Address |
Name |
Description |
10da:15ea |
check_ds_segment |
Returns true if DS == 0x10 (checks data segment selector) |
1760:3c9e |
nop_stub |
Always returns 0 (unused hook) |
Key String References
| Address |
String |
Context |
13fc:0016 |
$Id: comhighc.c 1.1 91/08/06... |
Phar Lap C runtime source ID |
13fc:0048 |
$Id: comutils.c 1.1 91/08/06... |
Phar Lap utility functions source ID |
13fc:0078 |
Serial Number |
DOS extender serial validation |
13fc:14ca |
Internal Error |
Error class prefix |
13fc:14da |
Fatal Error |
Fatal error class prefix |
13fc:156a-1628 |
File error messages |
Not found, bad format, no memory, etc. |
1760:665c |
Copyright (C) 1986-93 Phar Lap Software, Inc. |
DOS extender copyright |
1760:73da |
-LDTSIZE 4096 -EXTHIGH D0_0000h -NI 18 -ISTKSIZE 3 |
Default extender config |
1760:76fc-7c5a |
Numbered error messages |
System requirement errors (1000-2170) |