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.
Background
Lotus 1-2-3 Demo
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!
Lotus Toolkit Guide
Progress
Scenelist NFO
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!
LPL Compiler
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!
Lotus 1-2-3 For UNIX
Lotus 1-2-3 for UNIX
Teledisk Logo
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)
123 UNIX Manual
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
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!
Hacking
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!
GNU objcopy
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? 🐧
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
Porting Problems
System Calls
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():
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.
Relocations
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
Incompatible Functions
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:
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!
Termios
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.
Licensing
License Failed
Incredibly, after a bunch of hacking it actually runs without crashing!
Lotus 1-2-3 Box
…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! 🏴☠️
License Check
Result
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.
I’m specifically talking about the classic R3, there were releases until 2002.↩︎
Linux did have lcall7 and lcall27 compatability support at one point, but alas no more.↩︎