TWIN OS reverse engineered

After many hours and lots of puzzles and assumptions I finally released the reverse engineered sources for the operating system of the Signetics TWIN microprocessor development system. And not just one, but two for good measure: SDOS 2.0 as well as TOS. As an indication what a massive amount of effort this took: the two OSes together take up more than 28,000 lines of assembler code!

The source code is not a simple disassembly of the binary code. Sources are annotated with ample comments to explain the inner workings. All subroutines, variables and constants have been given meaningful names, so that reading the code gives an understanding how the software works. See the example below.

Black and white photo of a Signetics TWIN setup.

This effort was necessary understanding how the software and hardware operate hand in hand. For example, although a TWIN contains two Signetics 2650 microprocessors only one of them can be active at any time. During debugging there is a constant hand-over between the primary and secondary processors, signaled by interrupts and halting of either microprocessor. Some of these interrupts are generated by the debug hardware. The user manuals and reference manuals give only a superficial description of the working of the TWIN. The source code is required to fully understand the interplay between OS and hardware.

SDOS and TOS are closely related, and reverse engineering them side-by-side reduced the overall effort as the two systems sometimes employed different software solutions to obtain the same effect. For example the File Control Block and the Job Control Block are entirely identical. It can also be seen that TOS is most likely derived from SDOS 2.0, not the other way around, as TOS corrects some of the bugs in SDOS 2.0 and refines its code.

The next step will be to do the same for the other versions of the TWIN operating systems: SDOS 4.2, EXOS, UDOS and the various versions of Tektronix’ branding TEKDOS. By comparing the sources it will be possible to work out the family tree of all OSes.

As an example of my work, have a look at just 48 of those 28,000 lines. Below is the subroutine that stores a byte in a ring buffer. The raw disassembly is given first, followed by the annotated version. If you are interested in my preferred way of working have a look at this description.

02EF				L02EF:
02EF : 3F 0D 85		"?  "		bsta,un	L0D85
02F2 : 0C 90 D4		"   "		loda,r0	*X10D4
02F5 : 16		" "		retc,lt
02F6 : 44 3F		"D?"		andi,r0	H'3F'
02F8 : CC 90 D4		"   "		stra,r0	*X10D4
02FB : 0E F0 D8		"   "		loda,r0	*X10D8,r2
02FE : C3		" "		strz	r3
02FF : 01		" "		lodz	r1
0300 : CF B0 D4		"   "		stra,r0	*X10D4,r3,+
0303 : EF 90 D4		"   "		coma,r3	*X10D4
0306 : 98 02		"  "		bcfr,eq	L030A
0308 : 07 00		"  "		lodi,r3	H'00'
030A				L030A:
030A : 03		" "		lodz	r3
030B : CE F0 D8		"   "		stra,r0	*X10D8,r2
030E : 0E F0 D6		"   "		loda,r0	*X10D6,r2
0311 : E3		" "		comz	r3
0312 : 98 08		"  "		bcfr,eq	L031C
0314 : 0C 90 D4		"   "		loda,r0	*X10D4
0317 : 64 80		"d "		iori,r0	H'80'
0319 : CC 90 D4		"   "		stra,r0	*X10D4
031C				L031C:
031C : 20		" "		eorz	r0
031D : 17		" "		retc,un

Note that it already takes an effort to determine the boundaries of a subroutine within a code listing.

Now compare this to the annotated version:

* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*
* Subroutine PutR1DevRingbufR2 - push R1 into ringbuffer for device R2
*
* Ringbuffers start with a head, and are followed by one or more spaces in
* which data can be stored. Ringbuffers work like a stack with a fixed maximum
* size. Data is written at one end, and read from the other end. Both ends
* wrap-around, thus forming a ring.
* The first byte contains the max length, at most h'3F', often much shorter.
* Bits 0x80 and 0x40 are indicators:
*  bit 0x80 = buffer full
*  bit 0x40 = buffer empty
*
* Preserves R1,R2
* In:
*  R1	byte to store
*  R2	device number of the ringbuffer
* Out:
*  CC	LT=ringbuffer full, EQ=stored successfully
* Uses:
*  AdrB	set to the address of the ringbuffer
*  DevGetIdx	index of the current read pointer (table)
*  DevPutIdx	index of the current write pointer (table)
*
PutR1DevRingbufR2:
	; set up the ring buffer address
	bsta,un	SetAdrBToDevR2Ringbuf
	loda,r0	*AdrB		get head of ringbuffer
	retc,lt			return if full
	andi,r0	H'3F'
	stra,r0	*AdrB		clear the 'empty' bit
	loda,r0	*_DevPutIdx,r2	get current write pointer
	strz	r3
	lodz	r1
	stra,r0	*AdrB,r3,+	store byte in the ringbuffer
	coma,r3	*AdrB		pointer equals the buffer length?
	bcfr,eq	PDR1
	lodi,r3	0		wrap-around
PDR1	lodz	r3
	stra,r0	*_DevPutIdx,r2	save the write pointer
	loda,r0	*_DevGetIdx,r2
	comz	r3		write pointer has reached the read pointer?
	bcfr,eq	PDR2
	loda,r0	*AdrB		if so, mark the buffer as full
	iori,r0	ringbFull
	stra,r0	*AdrB
PDR2	eorz	r0
	retc,un