C:\RETRO\DOS\TP\TOOLCH~1>type turbop~5.htm
Turbo Pascal Toolchain, Part 5: From 6.0 to 7.0 - Compiler, Linker, and Language Growth
Parts 1-4 covered workflow, artifacts, overlays, and BGI integration. This last part goes inside the compiler/language boundary: memory assumptions, type layout, calling conventions, and assembler integration from TP6-era practice to TP7/BP7 scope.
Structure map
- Version framing — TP6 vs TP7/BP7 scope, continuity and deltas
- Execution model — real-mode assumptions, segmentation, near/far
- Data type layout — size table, alignment, layout probe harness
- Memory layout consequences — ShortString, sets, records, arrays
- Procedures vs functions — semantics and ABI implications
- Calling conventions — stack layout, parameter order, return strategy
- Compiler directives — policy, safety controls, project-wide usage
- Assembler integration — inline blocks, external OBJ, boundary contracts
- TP6→TP7 migration — pipeline evolution, artifact implications, language growth
- Protected mode and OOP — BP7 context, object layout, VMT considerations
- Migration checklist — risk controls, test loops, regression traps, common pitfalls
Version framing: what changed and what stayed stable
The TP6 to TP7 shift was less a language revolution and more an expansion of operational surface:
- larger project/tooling workflows became easier
- artifact and mixed-language integration became more central
- language core stayed recognizably Turbo Pascal
So the technical model below is largely continuous across this generation, with feature breadth increasing in later packaging. Borland Pascal 7 (BP7) extended this further with protected-mode compilation, built-in debugging support, and richer IDE integration, while TP7 remained primarily a real-mode product.
Version-specific nuances — TP7.0 (1990) stabilized the TP6 object model and
improved unit compilation speed. TP7.1 addressed bugs and refinements; some
teams held at 7.0 for compatibility with shared codebases. BP7 (1992) bundled
Turbo Debugger, expanded the RTL, and introduced DPMI target support. Exact
behavior of directives like {$G+} (80286 instructions), {$A+} (record
alignment), and codegen choices can vary between these builds; when precise
version behavior matters, treat claims here as a starting point and validate
against your toolchain.
Execution model assumptions (the non-negotiables)
Real-mode DOS assumptions drive everything:
- Segmented memory model — 64 KB segments, selector:offset addressing, 20-bit physical address space. DS usually points at the program’s data; SS at the stack; CS at code. Overlays swap code segments in and out of a single overlay area.
- 16-bit register-centric calling paths — AX, BX, CX, DX, SI, DI, BP, SP; segment registers CS, DS, SS, ES.
- Near vs far distinctions — near calls use same segment (16-bit offset), far calls require segment:offset (32-bit); overlay units demand far entry points.
- Conventional memory pressure — first 640 KB shared by DOS, TSRs, drivers, and your program; overlays and heap compete for the same pool.
The linker’s choice of memory model (Tiny, Small, Medium, Large, Huge) constrains code and data segment layout. TP6 and TP7 both default to Small model in typical configurations: one code segment, one data segment, with near pointers. Tiny folds code and data into one segment (for .COM output); Medium allows multiple code segments (far code, near data); Large/Huge allow multiple data segments with far pointers—changing pointer size from 2 to 4 bytes. Switching to Large model changes pointer sizes and call conventions; map-file analysis becomes essential when hunting link errors or unexpected runtime behavior.
Artifact implications — Small model yields a single .EXE with code and data in separate segments. Overlays add .OVR files; each overlay is its own code segment loaded on demand. The linker produces a .MAP file listing segment addresses and public symbols; use it to verify overlay boundaries and diagnose “fixup overflow” or segment-order issues. Segment order in the map (CODE, DATA, overlay segments) affects load addresses; changing unit compile order can shift symbols and break code that assumes fixed offsets. A typical map lists segments in load order with start/stop addresses; overlays appear as named segments with their size—verify overlay sizes match expectations before debugging load failures.
Data layout and ABI — Record fields, set bit layouts, and string formats
are stable within a compiler version but can differ across TP6, TP7, and BP7.
When sharing binary structures (e.g., files or shared memory) between programs
built with different toolchains, define a canonical layout and validate with
layout probes. Ignoring these constraints while reading Pascal source leads to
wrong performance and ABI conclusions. A layout-probe program that prints
SizeOf for all shared types, run under each toolchain, gives a quick
compatibility report before committing to a cross-toolchain design.
Data type layout: practical table
Common TP-era sizes in real-mode profiles:
Byte: 1 byteShortInt: 1 byteWord: 2 bytesInteger: 2 bytesLongInt: 4 bytesChar: 1 byteBoolean: 1 bytePointer: 4 bytes (segment:offset in real mode)String[N]:N+1bytes (length byte + payload)
Floating-point and extended numeric types (Real, Single, Double,
Extended, Comp) exist with version/profile-specific behavior and FPU/emulation
settings, so treat exact codegen cost as configuration dependent. With {$N+},
the compiler uses native FPU instructions; with {$N-}, software emulation
(via runtime library) is typical. Real is typically 6-byte BCD in older
profiles and can map to Single or a software type in others—verify in your
build. Extended is 10 bytes (80-bit); Comp is 8-byte integer format often
used for currency. Set types use one bit per element; set of 0..7 is 1 byte,
set of 0..15 is 2 bytes, up to 32 bytes for set of 0..255.
Alignment and packing — Turbo Pascal generally packs record fields without
inserting padding; fields align to their natural size (1, 2, 4 bytes). The
{$A+/-} (Align Records) directive, where available, can change this—{$A+} may
align record fields to word boundaries for faster access on some processors.
Packed records (packed record) minimize size at potential performance cost.
For structures crossing the Pascal–C–assembly boundary, explicit layout
verification is mandatory; C struct alignment rules often differ.
Quick layout probe harness
If binary layout matters, measure your exact compiler profile:
program LayoutProbe;
type
TRec = record
B: Byte;
W: Word;
L: LongInt;
end;
TPackedRec = packed record
B: Byte;
W: Word;
end;
begin
Writeln('SizeOf(Integer)=', SizeOf(Integer));
Writeln('SizeOf(Pointer)=', SizeOf(Pointer));
Writeln('SizeOf(String[20])=', SizeOf(String[20]));
Writeln('SizeOf(TRec)=', SizeOf(TRec));
Writeln('SizeOf(TPackedRec)=', SizeOf(TPackedRec));
Writeln('SizeOf(Single)=', SizeOf(Single), ' SizeOf(Real)=', SizeOf(Real));
end.Expected outcome: concrete numbers for your environment. Never assume layout from memory when ABI compatibility is at stake.
Memory layout consequences developers felt daily
ShortString behavior
String in classic Turbo Pascal is a short string (length-prefixed), not a
null-terminated C string. Consequences:
- O(1) length read via byte 0
- max 255 characters;
String[80]is 81 bytes - direct interop with C APIs needs conversion: either build a null-terminated
copy or pass
Str[1]and ensure the C side respects the length byte
A simple conversion helper for C library calls (TP7’s Strings unit has
StrPCopy; this illustrates the manual pattern):
procedure PascalToCString(const S: String; var Buf; MaxLen: Byte);
var
I: Byte;
P: ^Char;
begin
P := @Buf;
I := 0;
while (I < S[0]) and (I < MaxLen - 1) do begin
P^ := S[I + 1];
Inc(P); Inc(I);
end;
P^ := #0;
end;Set and record layout
Set/record memory footprint is compact but sensitive to declared ranges and
packing decisions. A set of 0..255 consumes up to 32 bytes (one bit per
element); smaller ranges use fewer bytes (e.g., set of 0..15 is 2 bytes).
Record alignment follows the directive and packing mode. Bit ordering within
set bytes is implementation-defined; when exchanging set values with C or
assembly, document and test the mapping. If binary compatibility matters,
verify layout with SizeOf tests in a dedicated compatibility harness. TP6
and TP7 generally match on these layouts, but mixed toolchains (e.g., C object
modules) may introduce padding differences.
Arrays
Arrays are contiguous. High-throughput code benefits from locality, but segment boundaries and index range checks (if enabled) influence speed and safety. Multi-dimensional arrays are stored in row-major order. Static arrays and open-array parameters have different calling semantics: open arrays pass a hidden length (typically as the last parameter or in a known slot), which affects the ABI at procedure boundaries. Example:
procedure Process(const Arr: array of Integer); { Arr: ptr + hidden High(Len) }String parameters pass by reference (address of the length byte); value
parameters of record type may be copied onto the stack or via a hidden pointer,
depending on size—records larger than a few bytes often use a hidden var
parameter to avoid stack bloat. When interfacing with assembly, document how
each parameter type is passed.
Procedures vs functions: not just syntax
Difference in language semantics:
procedure: action with no return valuefunction: returns value and can appear in expressions
Difference in engineering use:
- procedures often model side-effecting operations
- functions often model value computation or query paths
In low-level interop, function return strategy and calling convention details
matter for ABI compatibility with external objects. Scalars (Byte, Word, Integer,
LongInt, pointers) typically return in registers: Byte in AL, Word/Integer in
AX, LongInt in DX:AX (high word in DX, low in AX), pointers in DX:AX (segment
in DX, offset in AX). Larger types (records, arrays) may use a hidden var
parameter or a caller-allocated temporary; the threshold and mechanism vary by
version and type size—commonly, records exceeding 4 bytes use a hidden
first parameter for the return buffer.
When calling or implementing assembly routines that mimic Pascal functions,
match the return mechanism or corruption is likely. A function declared
external in Pascal must place its return value where the Pascal caller
expects it; an inline asm block that computes a LongInt return should
leave the result in DX:AX before the block ends. For Word returns, ensure
the high byte of AX is clean if the caller extends the value.
Calling conventions and ABI boundaries
Turbo Pascal default calling convention differs from C conventions commonly used by external modules. Pascal uses left-to-right parameter push order; C typically uses right-to-left (cdecl). Pascal procedures usually clean the stack (ret N); C callers often clean (cdecl). Name mangling can differ: Pascal may export symbols with no decoration or with a leading underscore; C compilers vary. At integration boundaries, define explicitly:
- Parameter order — left-to-right (Pascal) vs right-to-left (C)
- Stack cleanup responsibility — callee (Pascal-style) vs caller (cdecl)
- Near vs far procedure model — must match declaration and link unit
- Value return mechanism — register vs stack for large returns
If any of these is ambiguous, “link succeeds but runtime breaks” is predictable.
Stack frame layout — The compiler sets up BP as a frame pointer; parameters
are accessed via positive offsets from BP. For a near call, the return address
occupies 2 bytes (IP only); for a far call, 4 bytes (CS:IP). Parameter offsets
shift accordingly. Typical Pascal caller view: parameters pushed left-to-right,
then call. Callee sees highest parameter at lowest address. Example frame for
Proc(A: Word; B: LongInt) (near call):
{ Stack grows down. After PUSH BP; MOV BP, SP: BP+2 = ret addr, BP+4 = first param }
{ BP+4 = A (Word), BP+6 = B low, BP+8 = B high. Callee uses RET 6. }
{ For far call: BP+4 = CS, BP+6 = IP; first param at BP+8. }Near procedures use CALL near ptr and RET; far procedures use CALL far ptr
and RETF. The callee must not change BP, SP, or segment registers except as
permitted by the convention. For external C routines, use cdecl or equivalent
where the object was built with C; otherwise stack imbalance or wrong parameter
binding occurs. Inline assembly that calls external code must replicate the
expected convention:
function CStrLen(P: PChar): Word; cdecl; external 'CLIB';
// or, if linking C OBJ directly:
{$L mystr.obj}
function CStrLen(P: PChar): Word; cdecl; external;In mixed-language projects, write one tiny ABI verification test per external
routine family before integrating into real logic—e.g., call with known inputs,
assert expected output. Example harness: a small program that calls
MulAcc(100, 200, 50), expects a known result, and exits with code 0 on success;
run it immediately after linking a new assembly module to catch offset or
cleanup mismatches before they surface in production.
Compiler directives as architecture controls
Directives are not cosmetic. They change behavior and generated code.
Examples frequently used in serious projects:
{$R+/-}: range checking — array bounds, subrange{$Q+/-}: overflow checking — integer arithmetic{$S+/-}: stack checking — overflow sentinel{$I+/-}: I/O checking — handle errors vs continue{$G+/-}: 80286+ instructions (in BP7/profile-dependent builds){$N+/-}and related: FPU vs software float
Exact availability/effects vary by version/profile, so freeze directive policy per build profile and avoid per-file drift. A project-wide policy file or leading include can enforce consistency:
{ GLOBAL.INC - lock directives for release build }
{$R+} { Range check in debug only if you prefer; some teams use {$R-} for ship }
{$Q-} { Overflow off for speed in release }
{$S+} { Stack overflow detection recommended }
{$I+} { I/O errors as exceptions or Check(IOResult) }
{$F+} { FAR calls if using overlays }TP5/TP6/TP7 anchor points:
{$F+/-}(Force FAR Calls) is a local directive with default{$F-}.- In
{$F-}state, compiler chooses FAR for interface-declared unit routines and NEAR otherwise. - Overlay-heavy programs are advised to use
{$F+}broadly to satisfy overlay FAR-call requirements.
For {$DEFINE} and conditional compilation, centralize symbols (e.g.,
DEBUG, USE_OVERLAYS) so builds stay reproducible. Avoid scattering
version-specific {$IFDEF} blocks without documentation. Use {$IFOPT R+} to
check directive state rather than relying on a separate define when debugging
build configuration.
Directive gotchas — {$R+} adds runtime cost; many shipped builds use
{$R-}. {$I+} makes I/O failures raise runtime errors; {$I-} requires
explicit IOResult checks. Switching these mid-project causes subtle
bugs. Directive scope matters: a unit’s directives do not always affect the main program unless inherited via include. Document the chosen directive set in a README or build script so new contributors do not override them.
Assembler integration paths
Turbo Pascal projects typically used two integration patterns:
- Inline assembler blocks inside Pascal source —
asm ... end - External object modules linked with
{$L filename.OBJ}declarations
Inline path is great for small hot routines where Pascal symbol visibility helps;
you can reference parameters and locals by name. External path is better for
larger modules and reuse across projects. Both require strict stack discipline
and adherence to the chosen calling convention. Inline blocks cannot use
RET or RETF to exit the routine—control must flow to the block end so the
compiler can emit the standard epilogue. For conditional exit, use goto to a
label after the block or restructure the logic.
Inline assembler
Minimal inline shape:
function BiosTicks: LongInt;
begin
asm
mov ah, $00
int $1A
mov word ptr [BiosTicks], dx
mov word ptr [BiosTicks+2], cx
end;
end;This style keeps the function contract in Pascal while performing low-level work in assembly. It is ideal for small hardware-touching routines. The compiler generates prologue/epilogue; your inline block must preserve BP, SP, and segment registers as required. Do not assume register contents on entry except parameters passed in. DS and SS are typically valid for data/stack access; ES may be used for string operations or be undefined—save and restore if you modify it.
External OBJ integration
{$L FASTMATH.OBJ}
function MulAcc(A, B, C: Word): Word; external;The OBJ must export a public symbol matching the Pascal identifier. Calling
convention (parameter order, stack cleanup, near/far) must match. If the OBJ
was built with TASM or MASM, ensure the PROC declaration uses the right
model (e.g., NEAR/FAR) and that parameter offsets line up with Pascal’s
push order. Example TASM side for function MulAcc(A, B, C: Word): Word:
|
|
Pascal passes A, B, C left-to-right (A at lowest offset); callee cleans with
RET 6. Mismatch in offset or cleanup causes wrong results or stack crash.
Note: BP+4 assumes a 2-byte return address for near calls; far calls use 4 bytes,
so offsets shift—for the same routine declared far, A would be at BP+8.
Always verify against the generated Pascal code or map file. A quick sanity
check: compile a trivial Pascal wrapper that calls the external routine with
known values, run it, and assert the result before integrating into production.
Boundary contract checklist
Before relying on an external routine:
- symbol resolves at link (no “undefined external” or mangling mismatch)
- stack discipline preserved (balanced push/pop, correct
retform) - deterministic output under vector tests
If the third condition fails, ABI mismatch is the first suspect. Add a minimal harness that calls the routine with known inputs and asserts the result before integrating into production code. Record the test in the project so future linker or compiler upgrades can re-validate. Mixed Pascal–C–assembly projects benefit from a single “ABI smoke” program that exercises every external boundary with canned inputs.
TP6→TP7 migration: pipeline evolution and artifact implications
Compiler and linker pipeline
From TP6 to TP7, the pipeline stayed conceptually the same: compile units to OBJ, link OBJ with RTL and any external modules to EXE. The flow is: source (.PAS) → compiler (TPC.EXE / TPCX.EXE) → object (.OBJ) → linker (TLINK.EXE) → executable (.EXE) and optional map (.MAP). Overlay units add an extra overlay manager and .OVR files produced during linking. Command-line builds typically use TPC with options for target model and overlays; the IDE invokes the same tools under the hood. Saving OBJ files from each compile allows incremental linking and faster iteration, but migration should start from a full clean rebuild.
Behavioral shifts — TP7’s compiler produced more consistent symbol naming and improved handling of large unit graphs. The linker remained TLINK; its /m, /s, and overlay options work similarly across TP6 and TP7, but segment ordering and fixup resolution can produce different map layouts. When comparing before/after migration, expect segment addresses to change even when logic is identical.
What changed was robustness and integration:
- Larger projects — TP7 handled more units and larger dependency graphs without tripping over internal limits. Map file output and symbol resolution improved.
- Object file compatibility — TP6 OBJs generally link with TP7, but the reverse is not guaranteed; TP7 may emit slightly different record layouts or name mangling in edge cases. Recompile from source when migrating, do not mix TP6 and TP7 object files.
- RTL and units — Standard units (Crt, Dos, Graph, etc.) evolved; some routines gained parameters or changed behavior. Re-test code that relies on unit internals. Graph unit BGI handling, Dos unit path parsing, and Crt screen buffer assumptions are frequent sources of minor incompatibility.
- OBJ linkage — TP7’s TLink (or TLINK) remained compatible with TASM/MASM
object format. Mixed Pascal–assembly projects typically compile Pascal to OBJ,
assemble .ASM to OBJ, then link together. Ensure segment naming and model
(SMALL, MEDIUM, etc.) match across all modules. Use
PUBLICandEXTRNin assembly to mirror Pascal’sexternaldeclarations; symbol names must match exactly. A “Fixup overflow” or “Segment alignment” error often indicates model or segment-name mismatch between modules.
Language and OOP growth
TP6 introduced objects; TP7 refined them. Object layout (VMT, instance size) generally remained compatible, but virtual method tables and constructor/destructor semantics can vary. BP7 added further extensions. For migration:
- Recompile all object-based units under TP7.
- Run targeted tests on inheritance chains and virtual overrides.
- Avoid depending on undocumented VMT layout.
Objects store the VMT pointer at a fixed offset (often the first field); virtual methods are dispatched through it. When writing assembly that allocates or manipulates object instances, preserve that layout:
type
TBase = object
X: Integer;
procedure DoSomething; virtual;
end;
PBase = ^TBase;Instance size and VMT offset are compiler-dependent; use SizeOf(TBase) and
avoid hardcoding. Constructor calls initialize the VMT pointer; manual
allocation (e.g., New or heap blocks) requires proper init. Descendant objects
add their fields after the parent’s; single inheritance keeps layout predictable.
Multiple inheritance was not part of classic Turbo Pascal objects, so no VMT
merging concerns apply. When passing object instances to assembly, pass the
pointer (^TBase) and treat the first word/dword as the VMT pointer.
Constructor and destructor order — Turbo Pascal objects use Constructor Init and Destructor Done (or custom names). Call order matters: base
constructors before derived, destructors in reverse. Failing to call the
destructor on heap-allocated objects leaks memory. TP7 tightened some edge cases
around constructor chaining; if migration reveals odd behavior in object init,
compare TP6 and TP7 object code for the constructor to spot differences.
Protected mode and BP7 context
Borland Pascal 7 added protected-mode compilation, producing DPMI-compatible executables that can access extended memory. Key implications:
- Segment model — 32-bit selectors instead of 16-bit segments; pointer representation and segment arithmetic differ. Code that assumes real-mode segment:offset layout may fail. Far pointers in protected mode are selector:offset but the selector is a DPMI descriptor, not a physical segment. Near pointers remain 32-bit offsets within a segment; the segment limit is 4 GB in 32-bit mode, changing allocation and pointer-arithmetic assumptions.
- RTL differences — Protected-mode RTL uses DPMI calls for memory and interrupts; DOS file I/O and system calls go through the DPMI host. Heap allocation, overlay loading, and BGI driver loading all route differently than in real mode.
- Assembly interop — Inline and external assembly must use 32-bit-safe
patterns; some real-mode tricks (segment manipulation, direct ports) require
different handling. Real-mode
intinstructions work via DPMI emulation but with different semantics for protected-mode interrupts.
OOP in protected mode — Object and VMT layout are compatible with real-mode BP7, but instance allocation and constructor behavior may differ when the RTL uses DPMI memory services. Virtual method dispatch itself is unchanged; problems typically arise from code that reads segment values or assumes physical addresses. If your project stays in real mode, TP7 is sufficient. Moving to BP7 protected mode is a larger migration: treat it as a separate phase with dedicated tests. Real-mode TP7 binaries remain the norm for DOS-targeted applications; BP7 protected-mode targets DPMI-aware environments (e.g., Windows 3.x, OS/2, or standalone DPMI hosts like 386MAX).
Practical migration checklist (technical, not nostalgic)
|
|
Each step is auditable: (1) gives a baseline; (2) isolates toolchain change; (3) surfaces size and symbol shifts; (4) catches OBJ/ABI drift; (5) exercises integration points; (6) prevents future drift from stray directive changes. Run the checklist in order; skipping (1) or (2) makes later steps harder to interpret when regressions appear.
Risk controls
- Baseline capture — Checksum the TP6 EXE and map before migration; record build date and compiler version. Store baseline outputs in a known location; diff tools and checksum utilities should be available for comparison.
- Incremental migration — Migrate one unit or subsystem at a time where possible; isolate changes to reduce debugging scope. Migrate leaf units (those with no dependencies on other project units) first; then work inward toward the main program.
- Fallback — Keep TP6 build environment available until TP7 build is validated. If TP7 regression appears, you can bisect by reverting units. Preserve TP6 compiler, linker, and RTL paths; document them so the fallback is reproducible.
Test loops
- Smoke — Program starts, minimal user path completes. Include at least one path that loads overlays and one that uses BGI if the project employs them; silent failure on init is common.
- Regression traps — Known inputs that produced known outputs under TP6; re-run and compare under TP7. Capture checksums or golden files for critical outputs (reports, exports, screenshots). Automate where possible: a batch script that runs the program with canned input and diffs output against baseline catches many regressions.
- Boundary tests — Overlay load/unload, BGI init/close, TSR hooks, assembly entry points. Exercise code paths that touch segmented memory or far pointers. Add a dedicated test that calls every external assembly routine with edge values (0, -1, max) to uncover ABI mismatches.
Expected outcome
- Same behavior with clarified build policy, or
- Explicit, measurable deltas you can explain and document.
Not acceptable: “it feels mostly fine” without verification. Aim for a migration report that states: baseline version, target version, checksum deltas (or “identical”), test results (pass/fail counts), and any known behavioral differences with root cause. Future maintainers will thank you.
Common migration pitfalls
- Mixed OBJ versions — Linking TP6 units with TP7-built units can produce subtle ABI mismatches. Clean rebuild from source.
- Directive inheritance — Unit A’s directives can affect units that use it;
a stray
{$R-}in a deeply included file can disable range checks project-wide. - Overlay entry points — Overlays require far calls; if
{$F-}is set where overlay code is invoked, near calls hit the wrong segment and crash. - BGI driver paths — TP7 may look for .BGI files in different locations; verify InitGraph and driver loading.
- FPU detection —
{$N+}assumes FPU present; on 8086/8088, use{$N-}or runtime detection to avoid invalid opcode traps. - Map file drift — After migration, diff the new map against the baseline. Segment order and symbol addresses may shift; large or unexpected changes warrant investigation. If overlay segment names or orders change, overlay load addresses will differ—ensure overlay manager configuration matches the new map.
Where this series goes next
You asked for practical depth, so this series now has dedicated companion labs:
- Turbo Pascal Overlay Tutorial: Build, Package, and Debug an OVR Application
- Turbo Pascal BGI Tutorial: Dynamic Drivers, Linked Drivers, and Diagnostic Harnesses
Full series index
- Part 1: Anatomy and Workflow
- Part 2: Objects, Units, and Binary Investigation
- Part 3: Overlays, Memory Models, and Link Strategy
- Part 4: Graphics Drivers, BGI, and Rendering Integration
- Part 5: From 6.0 to 7.0 - Compiler, Linker, and Language Growth
If you want the next layer, I recommend one additional article focused only on calling-convention lab work with map-file-backed stack tracing across Pascal and assembly boundaries.