$Id: a07cf90837a3c4373b82d6724b97593810766af7 $
It’s an exciting time in the Lotus 1-2-3 enthusiast community – that was a joke, there is no enthusiast community, it’s just me! 😆
It really is an exciting time though – that part isn’t a joke!
There have been some major developments in the last few weeks, and I guess that’s pretty unusual for 30 year old abandonware.
I’ll cut to the chase; through a combination of unlikely discoveries, crazy hacks and the 90s BBS warez scene I’ve been able to port Lotus 1-2-3 natively to Linux – an operating system that literally didn’t exist when 1-2-3 was released!1
If you want to hear how a proprietary application could be ported to new operating systems 30 years after release, read on.
I really like Lotus 1-2-3, I even maintain a driver to make sure it works well on modern systems. I had to reverse engineer the driver api to make that happen, but it works beautifully.
Getting that driver working was quite an adventure, but there is still one piece of the puzzle missing: add-ins. 1-2-3 was designed to be extensible with plugins (or “add-ins”) – in theory you could add support for modern spreadsheet functions or integrate with Google Finance or something!
The problem is add-ins had to be written in a special language called LPL, and unfortunately the compiler and SDK have been lost. This is not really surprising, this was a niche product and Lotus didn’t just give the SDK away – they charged $395 for it. I can’t imagine many people have a copy just lying around.
There were lots of commercial plugins available in the heyday, some were really impressive. I emailed a few developers who worked on some to see if they had any old backups, the answer was always the same – 1-2-3 was the biggest name in software, nobody thought it was going anywhere, so why would they keep backups?
There were also two third-party books written about LPL, I managed to track them down from old libraries. I like the cover on this one, very serious business. It’s so frustating, I can see screenshots of the debugger, compiler output and sample add-ins - but just can’t do anything with it without the SDK!
Fast forward a year or two, and I found someone who used to be a sysop in the ’90s BBS scene. Check out his website, he still has a catalogue of NFOs and a telnet BBS online if you want to see some rad ANSI art!
He had kept tape backups from old BBS systems, and was able to recover a warez copy of the SDK – unbelievable! It actually worked, I was able to build a few sample plugins!
You can download the ADK right here, and here is a sample LPL program.
This would already have been enough to keep me entertained for a while, but it doesn’t stop there. It turns out that the BBS also had a warez copy of Lotus 1-2-3 for UNIX. This was widely thought to be lost – I’m told it couldn’t compete with a more popular UNIX office suite called SCO Professional, so there were not many copies sold.
I didn’t really have any use for it, but I’m definitely curious enough to poke around on the installation media to see what’s in it!
The warez release was a bunch of TD0 files, that’s a format I’ve never seen before – apparently it’s an old compressed disk image format from the 80s. I found this page which recommends using samdisk to convert it to a raw disk image.
$ ls
123UNIX1.TD0 123UNIX2.TD0 123UNIX3.TD0 123UNIX4.TD0 123UNIX5.TD0 LEGAL.NFO WHATITIS
$ file *.TD0
123UNIX1.TD0: floppy image data (TeleDisk)
123UNIX2.TD0: floppy image data (TeleDisk)
123UNIX3.TD0: floppy image data (TeleDisk)
123UNIX4.TD0: floppy image data (TeleDisk)
123UNIX5.TD0: floppy image data (TeleDisk)
That seemed promising, samdisk
builds and seems to work! I’ve uploaded the full images onto the Internet Archive in case anyone else is curious enough to take a look.
$ samdisk info 123UNIX1.TD0
[123UNIX1.TD0]
Type: TD0
Size: 80 Cyls, 2 Heads
created : 1991-06-22 20:24:04
Lotus 1-2-3 for UNIX System V
$ for i in *.TD0; do samdisk copy ${i} ${i/.TD0/.RAW}; done
Wrote 80 cyls, 2 heads, 18 sectors, 512 bytes/sector = 1474560 bytes
Wrote 80 cyls, 2 heads, 18 sectors, 512 bytes/sector = 1474560 bytes
Wrote 80 cyls, 2 heads, 18 sectors, 512 bytes/sector = 1474560 bytes
Wrote 80 cyls, 2 heads, 18 sectors, 512 bytes/sector = 1474560 bytes
Wrote 80 cyls, 2 heads, 18 sectors, 512 bytes/sector = 1474560 bytes
$ file *.RAW
123UNIX1.RAW: tar archive
123UNIX2.RAW: ASCII cpio archive (pre-SVR4 or odc)
123UNIX3.RAW: ASCII cpio archive (pre-SVR4 or odc)
123UNIX4.RAW: ASCII cpio archive (pre-SVR4 or odc)
123UNIX5.RAW: ASCII cpio archive (pre-SVR4 or odc)
I know that UNIX software was usually distributed as raw archives, and you were expected to insert the diskette and run something like tar -C / -xvf /dev/fd0
, so these files look right. I think this is smart, why waste precious bytes on a filesystem?
$ tar xf 123UNIX1.RAW
$ for i in 123UNIX{2..5}.RAW; do cpio -id < $i; done
1555 blocks
2606 blocks
2510 blocks
2481 blocks
$ tree --prune -Dh
.
├── [ 512 May 20 13:53] lotus
│ ├── [ 512 May 20 13:53] 123.v10
│ │ ├── [ 512 May 20 13:53] cbd
│ │ │ ├── [2.4K May 20 13:53] dfbcp437.bun.z
│ │ │ ├── [2.4K May 20 13:53] dfbcp850.bun.z
│ │ │ ├── [3.0K May 20 13:53] l13cUSA3.cbd
│ │ │ ├── [3.0K May 20 13:53] l13cUSF3.cbd
│ │ │ ├── [3.0K May 20 13:53] l13cUSL3.cbd
│ │ │ ├── [1.6K May 20 13:53] l_ascii.bun.z
│ │ │ ├── [1.8K May 20 13:53] l_latin1.bun.z
│ │ │ └── [ 874 May 20 13:53] l_lrf.bun.z
│ │ ├── [ 512 May 20 13:53] fonts
│ │ │ ├── [4.6K Sep 20 1990] aa012laa.lrf.z
│ │ │ ├── [4.3K Sep 20 1990] aa012lfa.lrf.z
│ │ │ ├── [5.4K Sep 20 1990] aa012lha.lrf.z
│ │ │ ├── [5.9K Sep 20 1990] aa017laa.lrf.z
│ │ │ ├── [7.8K Sep 20 1990] aa017lia.lrf.z
│ │ │ ├── [7.8K Sep 20 1990] aa024lfa.lrf.z
│ │ │ ├── [ 11K Sep 20 1990] aa024lha.lrf.z
│ │ │ ├── [8.8K Sep 20 1990] aa025laa.lrf.z
│ │ │ ├── [ 11K Sep 20 1990] aa030laa.lrf.z
│ │ │ ├── [6.6K Sep 20 1990] aa030lga.lrf.z
│ │ │ ├── [ 12K Sep 20 1990] aa033laa.lrf.z
│ │ │ ├── [ 18K Sep 20 1990] aa033lia.lrf.z
│ │ │ ├── [ 22K Sep 20 1990] aa050laa.lrf.z
│ │ │ ├── [ 29K Sep 20 1990] aa060laa.lrf.z
│ │ │ ├── [ 15K Sep 20 1990] aa060lga.lrf.z
│ │ │ ├── [ 53K Sep 20 1990] aa100laa.lrf.z
│ │ │ ├── [5.4K Sep 20 1990] ac016lfa.lrf.z
│ │ │ ├── [7.1K Sep 20 1990] ac016lha.lrf.z
│ │ │ ├── [6.1K Sep 20 1990] ac017laa.lrf.z
│ │ │ ├── [8.1K Sep 20 1990] ac022laa.lrf.z
│ │ │ ├── [ 11K Sep 20 1990] ac022lia.lrf.z
│ │ │ ├── [ 13K Sep 20 1990] ac033laa.lrf.z
│ │ │ ├── [ 16K Sep 20 1990] ac040laa.lrf.z
│ │ │ ├── [9.4K Sep 20 1990] ac040lga.lrf.z
│ │ │ ├── [ 34K Sep 20 1990] ac066laa.lrf.z
│ │ │ ├── [4.7K Sep 20 1990] ai012laa.lrf.z
│ │ │ ├── [4.3K Sep 20 1990] ai012lfa.lrf.z
│ │ │ ├── [5.5K Sep 20 1990] ai012lha.lrf.z
│ │ │ ├── [6.1K Sep 20 1990] ai017laa.lrf.z
│ │ │ ├── [8.0K Sep 20 1990] ai017lia.lrf.z
│ │ │ ├── [7.9K Sep 20 1990] ai024lfa.lrf.z
│ │ │ ├── [ 11K Sep 20 1990] ai024lha.lrf.z
│ │ │ ├── [9.0K Sep 20 1990] ai025laa.lrf.z
│ │ │ ├── [ 11K Sep 20 1990] ai030laa.lrf.z
│ │ │ ├── [6.5K Sep 20 1990] ai030lga.lrf.z
│ │ │ ├── [ 12K Sep 20 1990] ai033laa.lrf.z
│ │ │ ├── [ 19K Sep 20 1990] ai033lia.lrf.z
│ │ │ ├── [ 23K Sep 20 1990] ai050laa.lrf.z
│ │ │ ├── [ 29K Sep 20 1990] ai060laa.lrf.z
│ │ │ ├── [ 15K Sep 20 1990] ai060lga.lrf.z
│ │ │ ├── [ 55K Sep 20 1990] ai100laa.lrf.z
│ │ │ ├── [5.5K Sep 20 1990] aj016lfa.lrf.z
│ │ │ ├── [7.1K Sep 20 1990] aj016lha.lrf.z
│ │ │ ├── [6.2K Sep 20 1990] aj017laa.lrf.z
│ │ │ ├── [8.1K Sep 20 1990] aj022laa.lrf.z
│ │ │ ├── [ 11K Sep 20 1990] aj022lia.lrf.z
│ │ │ ├── [ 13K Sep 20 1990] aj033laa.lrf.z
│ │ │ ├── [ 17K Sep 20 1990] aj040laa.lrf.z
│ │ │ ├── [9.4K Sep 20 1990] aj040lga.lrf.z
│ │ │ ├── [ 35K Sep 20 1990] aj066laa.lrf.z
│ │ │ ├── [3.6K Sep 20 1990] bc008lba.lrf.z
│ │ │ ├── [4.0K Sep 20 1990] bc010lba.lrf.z
│ │ │ ├── [5.3K Sep 20 1990] bc012lea.lrf.z
│ │ │ ├── [5.5K Sep 20 1990] bc014lda.lrf.z
│ │ │ ├── [5.8K Sep 20 1990] bc016lba.lrf.z
│ │ │ ├── [5.8K Sep 20 1990] bc016lca.lrf.z
│ │ │ ├── [5.7K Sep 20 1990] bc016lda.lrf.z
│ │ │ ├── [6.1K Sep 20 1990] bc018lba.lrf.z
│ │ │ ├── [6.8K Sep 20 1990] bc021laa.lrf.z
│ │ │ ├── [ 12K Sep 20 1990] bc031lca.lrf.z
│ │ │ ├── [ 12K Sep 20 1990] bc031lda.lrf.z
│ │ │ ├── [ 14K Sep 20 1990] bc042laa.lrf.z
│ │ │ ├── [1.7K Sep 20 1990] bo008lda.lrf.z
│ │ │ ├── [2.6K Sep 20 1990] bo014lca.lrf.z
│ │ │ ├── [2.6K Sep 20 1990] bo014lda.lrf.z
│ │ │ ├── [3.1K Sep 20 1990] bo016lba.lrf.z
│ │ │ └── [3.5K Sep 20 1990] bo019laa.lrf.z
│ │ ├── [ 512 May 20 13:53] hlp
│ │ │ └── [ 512 May 20 13:53] USA-English
│ │ │ ├── [407K May 20 13:53] 123_sysV.hlp.z
│ │ │ ├── [7.3K May 20 13:53] keyedit.hlp.z
│ │ │ └── [8.2K May 20 13:53] setup123.hlp.z
│ │ ├── [ 512 May 20 13:53] keymaps
│ │ │ ├── [ 512 May 20 13:53] a
│ │ │ │ └── [3.7K May 20 13:53] AT386.z
│ │ │ ├── [ 512 May 20 13:53] p
│ │ │ │ └── [2.7K May 20 13:53] PC-NFS.z
│ │ │ ├── [ 512 May 20 13:53] s
│ │ │ │ ├── [2.6K May 20 13:53] sco386-101.z
│ │ │ │ ├── [3.7K May 20 13:53] sun.z
│ │ │ │ ├── [3.7K May 20 13:53] sysV386-101.z
│ │ │ │ └── [5.7K May 20 13:53] sysV386-xterm
│ │ │ ├── [ 512 May 20 13:53] v
│ │ │ │ ├── [4.7K May 20 13:53] vt100.z
│ │ │ │ └── [ 10K May 20 13:53] vtxxx.z
│ │ │ └── [ 512 May 20 13:53] w
│ │ │ ├── [3.2K May 20 13:53] wyse-pce.z
│ │ │ ├── [ 14K May 20 13:53] wyse50-lts123
│ │ │ └── [5.9K May 20 13:53] wyse50.z
│ │ ├── [ 512 May 20 13:53] pbd
│ │ │ ├── [ 604 Sep 20 1990] egas25ca.vbd.z
│ │ │ ├── [ 604 Sep 20 1990] egas25ci.vbd.z
│ │ │ ├── [ 605 Sep 20 1990] egas25cs.vbd.z
│ │ │ ├── [ 602 Sep 20 1990] egas25la.vbd.z
│ │ │ ├── [ 602 Sep 20 1990] egas25li.vbd.z
│ │ │ ├── [ 604 Sep 20 1990] egas25ls.vbd.z
│ │ │ ├── [ 574 Sep 20 1990] egas25ma.vbd.z
│ │ │ ├── [ 574 Sep 20 1990] egas25mi.vbd.z
│ │ │ ├── [ 576 Sep 20 1990] egas25ms.vbd.z
│ │ │ ├── [ 592 Sep 20 1990] egas25pa.vbd.z
│ │ │ ├── [ 592 Sep 20 1990] egas25pi.vbd.z
│ │ │ ├── [ 593 Sep 20 1990] egas25ps.vbd.z
│ │ │ ├── [ 604 Sep 20 1990] egas43ca.vbd.z
│ │ │ ├── [ 604 Sep 20 1990] egas43ci.vbd.z
│ │ │ ├── [ 605 Sep 20 1990] egas43cs.vbd.z
│ │ │ ├── [ 574 Sep 20 1990] egas43ma.vbd.z
│ │ │ ├── [ 574 Sep 20 1990] egas43mi.vbd.z
│ │ │ ├── [ 575 Sep 20 1990] egas43ms.vbd.z
│ │ │ ├── [ 588 Sep 20 1990] egas43pa.vbd.z
│ │ │ ├── [ 588 Sep 20 1990] egas43pi.vbd.z
│ │ │ ├── [ 589 Sep 20 1990] egas43ps.vbd.z
│ │ │ ├── [2.4K Sep 20 1990] epdtfx.pbd.z
│ │ │ ├── [2.4K Sep 20 1990] epdtmx.pbd.z
│ │ │ ├── [3.7K Sep 20 1990] eplql15p.pbd.z
│ │ │ ├── [3.3K Sep 20 1990] eplql25m.pbd.z
│ │ │ ├── [4.2K Sep 20 1990] eplql25p.pbd.z
│ │ │ ├── [3.4K Sep 20 1990] eplql2mc.pbd.z
│ │ │ ├── [4.3K Sep 20 1990] eplql2pc.pbd.z
│ │ │ ├── [2.9K Sep 20 1990] eplql80m.pbd.z
│ │ │ ├── [3.6K Sep 20 1990] eplql80p.pbd.z
│ │ │ ├── [1020 Sep 20 1990] gc_bs.pbd
│ │ │ ├── [ 754 Sep 20 1990] gc_nobs.pbd
│ │ │ ├── [3.9K Sep 20 1990] hplj1flo.pbd
│ │ │ ├── [2.6K Sep 20 1990] hplj1jlo.pbd.z
│ │ │ ├── [2.6K Sep 20 1990] hplj1nol.pbd
│ │ │ ├── [5.2K Sep 20 1990] hplj1zlo.pbd.z
│ │ │ ├── [4.3K Sep 20 1990] hplj2fhi.pbd.z
│ │ │ ├── [3.4K Sep 20 1990] hplj2flo.pbd.z
│ │ │ ├── [4.5K Sep 20 1990] hplj2jhi.pbd.z
│ │ │ ├── [3.6K Sep 20 1990] hplj2jlo.pbd.z
│ │ │ ├── [3.9K Sep 20 1990] hplj2noh.pbd.z
│ │ │ ├── [3.0K Sep 20 1990] hpljnolo.pbd.z
│ │ │ ├── [4.3K Sep 20 1990] hpljpfhi.pbd.z
│ │ │ ├── [3.4K Sep 20 1990] hpljpflo.pbd.z
│ │ │ ├── [3.7K Sep 20 1990] hpljpjhi.pbd.z
│ │ │ ├── [2.9K Sep 20 1990] hpljpjlo.pbd.z
│ │ │ ├── [3.1K Sep 20 1990] hpljpnoh.pbd.z
│ │ │ ├── [2.3K Sep 20 1990] hpljpnol.pbd.z
│ │ │ ├── [6.3K Sep 20 1990] hpljzhi.pbd.z
│ │ │ ├── [5.4K Sep 20 1990] hpljzlo.pbd.z
│ │ │ ├── [2.9K Sep 20 1990] hppjpapr.pbd.z
│ │ │ ├── [2.9K Sep 20 1990] hppjtran.pbd.z
│ │ │ ├── [ 976 Sep 20 1990] hrcf25ma.vbd.z
│ │ │ ├── [ 976 Sep 20 1990] hrcf25mi.vbd.z
│ │ │ ├── [ 977 Sep 20 1990] hrcf25ms.vbd.z
│ │ │ ├── [1006 Sep 20 1990] hrcs43ma.vbd
│ │ │ ├── [1006 Sep 20 1990] hrcs43mi.vbd
│ │ │ ├── [1006 Sep 20 1990] hrcs43ms.vbd
│ │ │ ├── [7.6K Sep 20 1990] ps00alw0.pbd.z
│ │ │ ├── [ 12K Sep 20 1990] ps00alwp.pbd.z
│ │ │ ├── [ 604 Sep 20 1990] vgas25ca.vbd.z
│ │ │ ├── [ 604 Sep 20 1990] vgas25ci.vbd.z
│ │ │ ├── [ 606 Sep 20 1990] vgas25cs.vbd.z
│ │ │ ├── [ 574 Sep 20 1990] vgas25ma.vbd.z
│ │ │ ├── [ 573 Sep 20 1990] vgas25mi.vbd.z
│ │ │ ├── [ 576 Sep 20 1990] vgas25ms.vbd.z
│ │ │ ├── [ 601 Sep 20 1990] vgas34ca.vbd.z
│ │ │ ├── [ 601 Sep 20 1990] vgas34ci.vbd.z
│ │ │ ├── [ 602 Sep 20 1990] vgas34cs.vbd.z
│ │ │ ├── [ 575 Sep 20 1990] vgas34ma.vbd.z
│ │ │ ├── [ 575 Sep 20 1990] vgas34mi.vbd.z
│ │ │ ├── [ 575 Sep 20 1990] vgas43ms.vbd.z
│ │ │ ├── [ 602 Sep 20 1990] vgas60ca.vbd.z
│ │ │ ├── [ 602 Sep 20 1990] vgas60ci.vbd.z
│ │ │ ├── [ 603 Sep 20 1990] vgas60cs.vbd.z
│ │ │ ├── [ 575 Sep 20 1990] vgas60ma.vbd.z
│ │ │ └── [ 575 Sep 20 1990] vgas60mi.vbd.z
│ │ ├── [ 512 May 20 13:53] ri
│ │ │ └── [ 512 May 20 13:53] USA-English
│ │ │ ├── [1001 May 20 13:53] 123.ri.z
│ │ │ ├── [ 602 May 20 13:53] dr123txt.ri
│ │ │ ├── [1.9K May 20 13:53] inst_dl.ri
│ │ │ ├── [5.3K May 20 13:53] keyedit.ri.z
│ │ │ ├── [2.0K May 20 13:53] l123smp3.ri
│ │ │ ├── [ 49K May 20 13:53] l123txt3.ri.z
│ │ │ ├── [ 844 May 20 13:53] license.ri.z
│ │ │ ├── [6.9K May 20 13:53] printer.ri.z
│ │ │ ├── [2.6K May 20 13:53] prsetup.ri.z
│ │ │ └── [ 23K May 20 13:53] setup123.ri.z
│ │ ├── [ 512 May 20 13:53] smpfiles
│ │ │ ├── [1.3K May 20 13:53] ACCTG.WK3.z
│ │ │ ├── [1.5K May 20 13:53] CONSOL.WK3.z
│ │ │ ├── [1.7K May 20 13:53] DATA.WK3.z
│ │ │ ├── [3.2K May 20 13:53] DBT13S.WK3.z
│ │ │ ├── [3.2K May 20 13:53] DBT14S.WK3.z
│ │ │ ├── [1.3K May 20 13:53] INC10S.WK3.z
│ │ │ ├── [3.0K May 20 13:53] INC11S.WK3.z
│ │ │ ├── [4.1K May 20 13:53] INC12S.WK3.z
│ │ │ ├── [3.7K May 20 13:53] INC16S.WK3.z
│ │ │ ├── [ 619 May 20 13:53] INC2S.WK3
│ │ │ ├── [ 965 May 20 13:53] INC4S.WK3
│ │ │ ├── [ 712 May 20 13:53] INC5S.WK3.z
│ │ │ ├── [1.8K May 20 13:53] INC6S.WK3
│ │ │ ├── [1.2K May 20 13:53] INC7S.WK3.z
│ │ │ ├── [1.4K May 20 13:53] INC8S.WK3.z
│ │ │ ├── [1.7K May 20 13:53] INC9S.WK3.z
│ │ │ ├── [3.8K May 20 13:53] MAC17S.WK3.z
│ │ │ ├── [2.0K May 20 13:53] MFG.WK3
│ │ │ ├── [1.8K May 20 13:53] SALES.WK3
│ │ │ ├── [4.7K May 20 13:53] SAMPMACS.WK3.z
│ │ │ ├── [1.5K May 20 13:53] SHOES.WK3.z
│ │ │ ├── [2.6K May 20 13:53] SMPSCHED.WK3.z
│ │ │ ├── [1.3K May 20 13:53] SUM1988S.WK3.z
│ │ │ ├── [1.5K May 20 13:53] SUMMARY.WK3
│ │ │ ├── [2.2K May 20 13:53] TABLES.WK3.z
│ │ │ ├── [6.4K May 20 13:53] consale.wk1.z
│ │ │ ├── [6.3K May 20 13:53] consale.wk3.z
│ │ │ ├── [6.8K May 20 13:53] consaler.wk1.z
│ │ │ ├── [7.2K May 20 13:53] income.wk3.z
│ │ │ ├── [8.7K May 20 13:53] income2.wk3.z
│ │ │ ├── [8.7K May 20 13:53] income3.wk3.z
│ │ │ ├── [8.7K May 20 13:53] income4.wk3.z
│ │ │ ├── [7.2K May 20 13:53] incomer.wk3.z
│ │ │ ├── [ 512 May 20 13:53] intro
│ │ │ │ ├── [ 874 Sep 20 1990] chicago.wk3.z
│ │ │ │ ├── [1.8K Sep 20 1990] debugmac.wk3
│ │ │ │ ├── [ 886 Sep 20 1990] houston.wk3.z
│ │ │ │ ├── [ 799 Sep 20 1990] less1ex1.wk3
│ │ │ │ ├── [ 565 Sep 20 1990] less1ex2.wk3.z
│ │ │ │ ├── [ 581 Sep 20 1990] less2ex1.wk3.z
│ │ │ │ ├── [ 877 Sep 20 1990] less2ex2.wk3
│ │ │ │ ├── [4.1K Sep 20 1990] less3ex2.wk3.z
│ │ │ │ ├── [ 706 Sep 20 1990] less4ex1.wk3.z
│ │ │ │ ├── [ 834 Sep 20 1990] less4ex2.wk3.z
│ │ │ │ ├── [ 798 Sep 20 1990] less5ex1.wk3.z
│ │ │ │ ├── [ 811 Sep 20 1990] less5ex2.wk3.z
│ │ │ │ ├── [ 931 Sep 20 1990] less5ex3.wk3.z
│ │ │ │ ├── [ 690 Sep 20 1990] less6ex1.wk3.z
│ │ │ │ ├── [ 781 Sep 20 1990] less6ex2.wk3.z
│ │ │ │ ├── [ 839 Sep 20 1990] less7ex1.wk3.z
│ │ │ │ ├── [ 856 Sep 20 1990] less7ex2.wk3.z
│ │ │ │ ├── [ 896 Sep 20 1990] less8ex2.wk3.z
│ │ │ │ ├── [1.7K Sep 20 1990] less9ex1.wk3
│ │ │ │ ├── [1.8K Sep 20 1990] macros.wk3
│ │ │ │ ├── [1.9K Sep 20 1990] menuexam.wk3
│ │ │ │ ├── [ 774 Sep 20 1990] menumac.wk3
│ │ │ │ ├── [ 647 Sep 20 1990] mis.wk3.z
│ │ │ │ ├── [ 939 Sep 20 1990] mlibrary.wk3
│ │ │ │ ├── [ 890 Sep 20 1990] nyc.wk3.z
│ │ │ │ ├── [1.8K Sep 20 1990] parts.wk3.z
│ │ │ │ ├── [ 698 Sep 20 1990] planning.wk3.z
│ │ │ │ ├── [2.0K Sep 20 1990] q2graph.wk3
│ │ │ │ ├── [1.3K Sep 20 1990] q2graph2.wk3.z
│ │ │ │ ├── [ 690 Sep 20 1990] q2inc.wk3
│ │ │ │ ├── [ 763 Sep 20 1990] q2inc2.wk3.z
│ │ │ │ ├── [ 770 Sep 20 1990] q2mac.wk3.z
│ │ │ │ ├── [1.2K Sep 20 1990] q2macro.wk3.z
│ │ │ │ ├── [1.3K Sep 20 1990] q2price.wk3.z
│ │ │ │ ├── [ 674 Sep 20 1990] research.wk3.z
│ │ │ │ ├── [1.9K Sep 20 1990] salary.wk3
│ │ │ │ ├── [ 878 Sep 20 1990] sanfran.wk3.z
│ │ │ │ └── [2.0K Sep 20 1990] supplier.wk3.z
│ │ │ ├── [ 12K May 20 13:53] menu2.wk3.z
│ │ │ ├── [ 12K May 20 13:53] menu3.wk3.z
│ │ │ ├── [ 11K May 20 13:53] menu4.wk3.z
│ │ │ └── [ 512 May 20 13:53] nf
│ │ │ ├── [1.4K Sep 20 1990] acctg.wk3.z
│ │ │ ├── [5.3K Sep 20 1990] actual.wk3.z
│ │ │ ├── [ 871 Sep 20 1990] asset.wk3.z
│ │ │ ├── [1023 Sep 20 1990] bud91.wk3
│ │ │ ├── [5.0K Sep 20 1990] budget.wk3.z
│ │ │ ├── [1.5K Sep 20 1990] calcex.wk3.z
│ │ │ ├── [2.9K Sep 20 1990] consol.wk3.z
│ │ │ ├── [3.2K Sep 20 1990] data.wk3.z
│ │ │ ├── [4.9K Sep 20 1990] dataex.wk3.z
│ │ │ ├── [4.8K Sep 20 1990] datatab.wk3.z
│ │ │ ├── [ 948 Sep 20 1990] dattim.wk3.z
│ │ │ ├── [ 677 Sep 20 1990] dept_emp.wk3.z
│ │ │ ├── [ 690 Sep 20 1990] deptexp.wk3.z
│ │ │ ├── [ 852 Sep 20 1990] deptname.wk3
│ │ │ ├── [4.4K Sep 20 1990] dfillex.wk3.z
│ │ │ ├── [6.9K Sep 20 1990] graphex.wk3.z
│ │ │ ├── [ 588 Sep 20 1990] hotel90.wk3.z
│ │ │ ├── [2.0K Sep 20 1990] hotel_q1.wk1.z
│ │ │ ├── [2.0K Sep 20 1990] hotel_q2.wk1.z
│ │ │ ├── [2.0K Sep 20 1990] hotel_q3.wk1.z
│ │ │ ├── [2.0K Sep 20 1990] hotel_q4.wk1.z
│ │ │ ├── [2.4K Sep 20 1990] joinex.wk3.z
│ │ │ ├── [2.5K Sep 20 1990] macex.wk3.z
│ │ │ ├── [3.4K Sep 20 1990] macros.wk3.z
│ │ │ ├── [1.4K Sep 20 1990] mfg.wk3.z
│ │ │ ├── [1.8K Sep 20 1990] mis.wk1.z
│ │ │ ├── [1.8K Sep 20 1990] planning.wk1.z
│ │ │ ├── [8.4K Sep 20 1990] print.wk3.z
│ │ │ ├── [8.4K Sep 20 1990] print_bw.wk3.z
│ │ │ ├── [1.3K Sep 20 1990] printex.wk3.z
│ │ │ ├── [1.3K Sep 20 1990] q2price.wk3.z
│ │ │ ├── [1.8K Sep 20 1990] research.wk1.z
│ │ │ ├── [3.1K Sep 20 1990] rev-1.wk3.z
│ │ │ ├── [3.1K Sep 20 1990] rev-2.wk3.z
│ │ │ ├── [1.9K Sep 20 1990] salary.wk3
│ │ │ ├── [1.3K Sep 20 1990] sales.wk3.z
│ │ │ ├── [8.4K Sep 20 1990] shoes.wk3.z
│ │ │ ├── [8.4K Sep 20 1990] shoes_bw.wk3.z
│ │ │ ├── [ 842 Sep 20 1990] summary.wk3.z
│ │ │ ├── [3.3K Sep 20 1990] tables.wk3.z
│ │ │ ├── [3.5K Sep 20 1990] temp.wk3.z
│ │ │ ├── [ 985 Sep 20 1990] util.wk3.z
│ │ │ └── [4.2K Sep 20 1990] world.wk3.z
│ │ └── [ 512 May 20 13:53] sysV386
│ │ ├── [ 512 May 20 13:53] bin
│ │ │ ├── [ 924 May 20 13:53] 123.sh.z
│ │ │ ├── [1.1M May 20 13:53] 123_exe.z
│ │ │ ├── [8.6K May 20 13:53] inst_dl.z
│ │ │ ├── [ 96K May 20 13:53] keyedit.z
│ │ │ ├── [ 14K May 20 13:53] l13pbanr.z
│ │ │ ├── [1.4K May 20 13:53] l13pupif.z
│ │ │ ├── [9.5K May 20 13:53] prsetup123.z
│ │ │ └── [ 95K May 20 13:53] setup123_exe.z
│ │ ├── [ 512 May 20 13:53] dvr
│ │ │ └── [ 97K May 20 13:53] l13pepdt.dvr.z
│ │ ├── [ 512 May 20 13:53] lib
│ │ │ ├── [1.1M May 20 13:53] 123.o.z_1
│ │ │ ├── [717K May 20 13:53] 123.o.z_2
│ │ │ ├── [ 913 May 20 13:53] dl_init.o.z
│ │ │ ├── [ 76K May 20 13:53] ld.z
│ │ │ ├── [ 14K May 20 13:53] mkdlobj.z
│ │ │ ├── [ 649 May 20 13:53] stub.o
│ │ │ ├── [ 328 May 20 13:53] tail.o
│ │ │ └── [1.3K May 20 13:53] wyse50-lts123
│ │ └── [ 56 May 20 13:53] lotus.bcf
│ └── [ 512 May 20 13:53] man
│ ├── [ 512 May 20 13:53] cat.LOCAL
│ │ ├── [3.0K May 20 13:53] 123.1.z
│ │ ├── [3.1K May 20 13:53] 123.LOCAL.z
│ │ ├── [ 738 May 20 13:53] inst_dl.1.z
│ │ ├── [1.2K May 20 13:53] inst_dl.LOCAL
│ │ ├── [ 706 May 20 13:53] keyedit.1.z
│ │ ├── [1.1K May 20 13:53] keyedit.LOCAL
│ │ ├── [1.5K May 20 13:53] setup123.1.z
│ │ └── [2.7K May 20 13:53] setup123.LOCAL
│ └── [ 512 May 20 13:53] man1
│ ├── [2.6K May 20 13:53] 123.1.z
│ ├── [ 931 May 20 13:53] inst_dl.1
│ ├── [ 946 May 20 13:53] keyedit.1
│ └── [2.0K May 20 13:53] setup123.1
├── [ 512 May 20 13:53] tmp
│ ├── [ 512 May 20 13:53] _lbl
│ │ └── [ 512 May 20 13:53] prd=123
│ │ └── [ 512 May 20 13:53] typ=n386
│ │ └── [ 512 May 20 13:53] rel=1.0
│ │ └── [ 0 Sep 20 1990] vol=01
│ ├── [ 178 Sep 20 1990] init.123
│ └── [ 512 May 20 13:53] perms
│ └── [ 985 Sep 20 1990] 123
└── [ 512 May 20 13:53] usr
└── [ 512 May 20 13:53] tmp
└── [ 512 May 20 13:53] lotus_install
└── [ 512 May 20 13:53] 123
├── [ 19K Sep 20 1990] Install
├── [ 860 Sep 20 1990] banner
├── [ 20K Sep 20 1990] chkdskno
├── [ 25K Sep 20 1990] combine_exe
├── [ 208 Sep 20 1990] copyright.d
├── [ 167 Sep 20 1990] filelist.1
├── [4.8K Sep 20 1990] filelist.2
├── [ 351 Sep 20 1990] filelist.3
├── [ 294 Sep 20 1990] filelist.4
├── [ 257 Sep 20 1990] filelist.5
├── [ 12K Sep 20 1990] messages
└── [ 25K Sep 20 1990] mk_banner
35 directories, 338 files
While poking around, this one directory caught my eye, what exactly is this?
$ ls -l
total 2.0M
-rw-r--r-- 1 taviso taviso 1.2M May 20 13:53 123.o.z_1
-rw-r--r-- 1 taviso taviso 717K May 20 13:53 123.o.z_2
-rw------- 1 taviso taviso 913 May 20 13:53 dl_init.o.z
-rwx------ 1 taviso taviso 77K May 20 13:53 ld.z*
-rwx------ 1 taviso taviso 14K May 20 13:53 mkdlobj.z*
-rw------- 1 taviso taviso 649 May 20 13:53 stub.o
-rw------- 1 taviso taviso 328 May 20 13:53 tail.o
-rw-r--r-- 1 taviso taviso 1.4K May 20 13:53 wyse50-lts123
That 123.o
file is huge, even compressed it had to be split across two disks. Let’s take a closer look…
$ cat 123.o.z_? | gzip -d > 123.o
$ file 123.o
123.o: Intel 80386 COFF object file, not stripped, 5 sections, symbol offset=0x1efbdc, 19755 symbols, optional header size 28
Yikes - it’s an original unstripped object file from 1-2-3. There are nearly 20,000 symbols including private symbols and debug information.
Why would Lotus ship this? It’s so big it must have required them to phyiscally ship an extra disk to every customer? Could it have been a mistake, accidentally left on the final release image?
I had so many questions, but I’m not old enough to have any experience with SysV, so I asked the greybeards on alt.folklore.computers if they had seen this before and why this might have happened.
The answer was that this is probably deliberate - dlopen()
was not widely available on UNIX in the early 90s, so there was no easy way to load native plugins or extensions. To solve this, vendors would ship a bunch of partially linked object files with a script to relink them with your extensions – Clever!
I can’t tell you how useful this discovery was – the debug information answered so many questions I had about Lotus 1-2-3 internals! This was a direct source port from DOS, so it mostly worked the same way but now I had debugging data. For example, I really wanted to hook into the rasterizer in my driver so that I could improve the appearance of graphs in the terminal… but it was just too complex to understand without documentation.
I now know that the rasterizer dynamically generates little bytecode programs that are interpreted by the graphics engine. Now that I know what the opcodes are I can disassemble and change them in my driver to improve the output!
Okay… but there is one more big question, I know that objcopy
can convert COFF object files to ELF, the format used by Linux. It seems like a long shot, but is it possible I could link this into a native Linux program? 🐧
$ objcopy -I coff-i386 -O elf32-i386 123.o 123elf.o
$ file 123elf.o
123elf.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
Hilariously, the first version of Linux hadn’t even been released when this object file was compiled – but I think this is possible! If you want to hear about the technical challenges, read on!
$ objdump -p 123.o | grep Date
Time/Date Sat Sep 8 06:23:50 1990
The first problem is that Linux and UNIX do not use a compatible system call interface. UNIX uses the lcall7 interface, so we need to find those calls and fix them up. Here is how this object file calls open()
:
$ objdump -M intel --disassemble=open 123elf.o
123elf.o: file format elf32-i386
Disassembly of section .text:
000e20d4 <open>:
e20d4: b8 05 00 00 00 mov eax,0x5
e20d9: 9a 00 00 00 00 07 00 call 0x7:0x0
e20e0: 0f 82 c6 01 00 00 jb e22ac <_cerror>
e20e6: c3 ret
e20e7: 90 nop
That call
instruction is what’s known as a callgate, which isn’t supported on Linux2, it will just crash. I want to remove this and route all calls through glibc instead. My first thought was just to mark these symbols as undefined, and then let the linker fix that up by importing a replacement symbol from glibc.
Nothing is ever easy, it turns out that won’t work! If we try, objcopy
will simply refuse:
$ objcopy -I coff-i386 -O elf32-i386 --strip-symbol open 123.o 123elf.o
objcopy: not stripping symbol `open' because it is named in a relocation
What is objcopy
trying to tell us here?
This is a relocatable object file, which simply means it can be loaded at any address and still work. That’s possible because it contains all the necessary information – the relocations – to adjust it.
Relocations are really simple, the compiler just records the name of the symbol and the references to it. Now the linker can just walk through and patch each reference to point to the new location – easy.
So objcopy
is saying that you can’t remove this symbol, because the linker won’t know what to patch in when it moves it. Fair enough – but, just because objcopy won’t do it doesn’t mean it’s impossible! We could just fix the relocations too, right?
I don’t know of any tool that can do that, but COFF is not a complicated format – I’ll write one!
Introducing coffsyrup, a tiny little tool that will remove those pesky COFF symbols even if objcopy refuses!
$ coffsyrup 123.o 123new.o open
MATCH open
RELOC rel open @0x180fa ~0xc9ede
RELOC rel open @0x4c9a1 ~0x95637
RELOC rel open @0x4d348 ~0x94c90
RELOC rel open @0x4ec13 ~0x933c5
Now that we can reroute functions, we have to worry about incompatible functions.
Lots of standard UNIX functions are source but not binary compatible, this is because nobody promises that structures are the same size or layout across UNIX versions. The obvious example is struct stat
.
For example, this code is likely to work on any UNIX-like system you can compile it on:
struct stat sb;
if (stat("/etc/passwd", &sb) == 0) {
"Size: %u\n", sb.st_size);
printf( }
However, the resulting object file is not likely to work on any other system. That’s because the size of struct stat
and the offset of st_size
will be different – it will probably just corrupt your stack and crash!
Luckily there are not really that many functions like this in UNIX. In fact, the number is small enough that I can probably write wrappers to translate them. The important ones are stat()
, times()
, uname()
, fcntl()
, ioctl()
and so on.
All I have to do is rename those symbols with objcopy
, then mark them undefined with coffsyrup
. Now I can write a little wrapper that translates a Linux struct stat
to a UNIX struct stat
and it should work!
Well…I said “little” wrappers – but there are some big incompatabilities in places. One big nightmare was termios. Go ahead and take a look at the termios(3)
man page, pretty complex right? Well, everything here works differently in subtle, incompatible, and difficult to debug ways on every UNIX system.
Incredibly, after a bunch of hacking it actually runs without crashing!
…and refuses to work without a license, damn! Well, I am a legitimate licensed 1-2-3 owner with a boxed copy of 1-2-3, and this is 32 year old abandonware. I think Mitch Kapor will forgive me for bypassing this check.
I can see from breaking on exit()
that there is an internal symbol called lic_init()
responsible for checking for a valid license. I looked at the code in IDA, and figured out the logic.
It is simply looking for a file called LICENSE.000
, which contains an expiry date, username and systemname. If that all matches what the system reports, the check passes! 🏴☠️
That’s it, Lotus 1-2-3 has been ported to a new operating system. There are a few kinks that need to be ironed out, and I need to port over my terminal driver, but it is 100% usable. At the moment, the DOS version running under emulation looks better - but this can be fixed!
I hope you enjoyed reading about this, in the highly unlikely event you actually want to try it yourself – all of my code is on github.