IMPLEMENTATION MODULE Linker ; (* Linker and .COM writer for TP3-compiled programs. The compiler has already produced the whole image - Runtime is copied to offset 0 of the code buffer and the program follows it, so every address in the image is already absolute (see Compiler.Inittur). There is therefore no relocation to do: "linking" here means 1. pad the image so the data area exists, and 2. write it out as a DOS .COM file. Why padding is needed at all: a .COM file is loaded at CS:0100 with the whole 64 KiB segment available, but the program's globals live at Compiler.DataBase = rtSz + 1000H, which for a small program is far past the last code byte. Those globals are read and written by absolute address, so the bytes have to be present in the file. The gap between code and data is zero-filled, which is also what makes it safe: a .COM's stack lives at the TOP of the segment (DOS gives a .COM SS=SP=CS:FFFE), so code and data both have to stay well below it. A 4 KiB gap keeps them apart for programs up to 4 KiB of code, which is the documented limit. Faithfulness note: the original did this in the compiler's overlay loader (TPSRC7 opendest) and wrote a .COM from a different buffer layout with a larger header served by the overlay segment. Our header is our own - see Runtime.EmitInitMem - and the file layout is the same idea, smaller. *) FROM SYSTEM IMPORT BYTE, ADR ; FROM Posix IMPORT open, write, close, unlink ; FROM Compiler IMPORT ImageBytes, ImageByteAt, DataBase, DataBytes ; CONST O_WRONLY = 1 ; O_CREAT = 64 ; O_TRUNC = 512 ; VAR padByte : BYTE ; (* the filler for the code/data gap *) PROCEDURE DropCh (v : CHAR) ; BEGIN END DropCh ; PROCEDURE DropB (v : BOOLEAN) ; BEGIN END DropB ; PROCEDURE DropC (v : CARDINAL) ; BEGIN END DropC ; PROCEDURE ZCopy (VAR dst : ARRAY OF CHAR ; src : ARRAY OF CHAR) ; (* Copy into a NUL-terminated buffer. The C bindings take a plain ADDRESS, so the string must be terminated in OUR buffer - passing the caller's unbounded array straight through would pass a pointer to whatever followed it. The VAR is load-bearing, and was found by running: without it the formal is a COPY, the writes land in the copy, and the caller writes to whatever garbage was in its buffer. The symptom was a .COM file named "END" - adjacent string data - created with the right size and the right contents in the wrong file. tests/OpenArrTest.mod measures the three variants. *) VAR i : CARDINAL ; BEGIN i := 0 ; WHILE (i <= HIGH (dst) - 1) AND (i <= HIGH (src)) AND (src [i] # 0C) DO dst [i] := src [i] ; INC (i) END ; dst [i] := 0C END ZCopy ; PROCEDURE LinkSize () : CARDINAL ; (* Size of the linked file in bytes: the image, padded out to cover the whole data area. This is what a .COM must be, and it is larger than Compiler.ImageBytes whenever the program declares no globals, because the data base is a fixed 4 KiB above the code. *) BEGIN IF DataBase () + DataBytes () > ImageBytes () THEN RETURN DataBase () + DataBytes () END ; RETURN ImageBytes () END LinkSize ; PROCEDURE MakePad () ; BEGIN (* A local array is not required to be zeroed, and relying on it would be a silent-corruption bug: an uninitialised pad byte would appear in the middle of a program's data area. ISO will not let an array be assigned the ZType, so the filler is a single explicit BYTE. *) padByte := 0 END MakePad ; PROCEDURE WriteCom (path : ARRAY OF CHAR) : BOOLEAN ; (* Write the linked image to `path` as a DOS .COM. Returns TRUE on success; on failure the file is removed, so a half-written .COM is never left behind to be mistaken for a good one. *) VAR fd : INTEGER ; n : LONGINT ; i, total, chunk : CARDINAL ; buf : ARRAY [0..255] OF BYTE ; z : ARRAY [0..511] OF CHAR ; BEGIN MakePad ; ZCopy (z, path) ; total := LinkSize () ; fd := open (ADR (z), O_WRONLY + O_CREAT + O_TRUNC, 420) ; IF fd < 0 THEN RETURN FALSE END ; i := 0 ; WHILE i < total DO chunk := 0 ; WHILE (chunk < 256) AND (i + chunk < total) DO IF i + chunk < ImageBytes () THEN buf [chunk] := ImageByteAt (i + chunk) ELSE buf [chunk] := padByte (* the gap before the data *) END ; INC (chunk) END ; n := write (fd, ADR (buf), chunk) ; IF n # VAL (LONGINT, chunk) THEN DropC (VAL (CARDINAL, close (fd))) ; DropB (unlink (ADR (z)) = 0) ; RETURN FALSE END ; i := i + chunk END ; IF close (fd) # 0 THEN DropB (unlink (ADR (z)) = 0) ; RETURN FALSE END ; RETURN TRUE END WriteCom ; END Linker.