$Id: a07cf90837a3c4373b82d6724b97593810766af7 $
I tend to use a lot of retro software, partly because I’m a big nerd but also because I just prefer to work in a terminal. There were a ton of major commercial software products for MS-DOS that were highly polished, and can still be used effectively in an xterm today.
I do like using Lotus 1-2-3, I even have a boxed copy of the last DOS version released, version 4!
The truth is I’m cheating, it does work, but it only supports a few standard text mode resolutions. If your terminal is not exactly 80 columns wide, it just makes a big ugly mess on your screen.
There’s a workaround, just type stty cols 80
, and it will be confined to a portion of your terminal, looking a bit sad. There is no way to display more columns, and maximizing your terminal will do nothing.
I wondered if other people still uses 123, and if they had found a solution. I enjoy using it, but being limited to such a small work area is a real nuisance.
I spent an hour reading vintage computing forums and old USENET posts.
Unfortunately, the answer is that nobody has a solution to my resolution problem, but I did find something interesting…
I found this old manual for a 1993 PC Workstation called the Epson Endeavor, which claimed to be distributed with a utilities disk that enabled a 100x31 text mode in 1-2-3.
How could such a thing be possible?
I was able to find the drivers on an old ftp site. Sadly they didn’t work, presumably they needed a real Cirrus Logic VGA card, but the fact that these drivers exist at all made me curious. It seems plausible that 123 can internally generate an arbitrary sized canvas, and it simply asks the display driver what resolution it wants?
I know that dosemu can simulate arbitrary text mode resolutions, and some DOS software is able to use it. Is it possible that I could write a dosemu display driver, and 123 would just work in a fullscreen xterm?
The display driver API is unfortunately quite complicated, I think it might take a while to figure out with just a disassembler and a debugger. Further complicating matters, 123 uses a technique called overlays (like software bank switching) which confuses the debugger I’m using as it loses breakpoints whenever a overlay switch occurs!
I figured that Lotus must have sent Cirrus Logic some documentation to write the driver, perhaps that still exists somewhere and would save me some time.
I spent some time looking, but came up empty.
I learned a lot about the Lotus add-in ecosystem, and how they worked. I figured I would download all the drivers and add-ins I could find, I’ll start reversing the simplest one, and move on until I know enough to write my own.
If you want to read about what commercial addins were available, this article is a fun read.
Believe it or not, the old Lotus ftp site is still online. I mirrored everything they had to my workstation and sifted through it.
Then I got a lucky break…
Lotus 1-2-3 was ported to a bunch of systems, including OpenVMS, Xenix, and even System/390. In 1991, Lotus released a version for SunOS4 on SPARC.
A few years later, a Lotus engineer uploaded a patch for a file management bug in the XALERT component that affected the 1.2 update. Luckily, they forgot to remove the STABS data, leaving incredibly rich debugging data.
typedef short int lmbcs_type;
typedef short int platform;
typedef short int memory_type;
struct envblk_ { /* size 68 id 9 */
short int envsize; /* bitsize 16, bitpos 0 */
short int pfid; /* bitsize 16, bitpos 16 */
short int csid; /* bitsize 16, bitpos 32 */
short int pad; /* bitsize 16, bitpos 48 */
short int maps; /* bitsize 16, bitpos 64 */
short int type; /* bitsize 16, bitpos 80 */
mptr nulh; /* bitsize 32, bitpos 96 */
long unsigned int maxsize; /* bitsize 32, bitpos 128 */
unsigned char *applid; /* bitsize 32, bitpos 160 */
lhdl gabinfo; /* bitsize 32, bitpos 192 */
char *regfile; /* bitsize 32, bitpos 224 */
mptr (*get) (/* unknown */); /* bitsize 32, bitpos 256 */
short int (*free) (/* unknown */); /* bitsize 32, bitpos 288 */
lhdl (*map) (/* unknown */); /* bitsize 32, bitpos 320 */
void (*unmap) (/* unknown */); /* bitsize 32, bitpos 352 */
lhdl (*alloc) (/* unknown */); /* bitsize 32, bitpos 384 */
short int (*dealloc) (/* unknown */); /* bitsize 32, bitpos 416 */
short unsigned int (*load) (/* unknown */); /* bitsize 32, bitpos 448 */
short unsigned int (*unload) (/* unknown */); /* bitsize 32, bitpos 480 */
short unsigned int (*syscall) (/* unknown */); /* bitsize 32, bitpos 512 */
};
typedef struct envblk_ /* id 9 */ envblk;
typedef long int LpiSTATUS;
...
This was the only file (trust me, I checked them all) that had any debugging data left on the whole ftp!
Now, this file was for a different architecture, operating system, compiler, release and was a long obsolete format. But, the clues it contained about the driver API were invaluable, and saved me so much time.
$ file alert.so1
alert.so1: SPARC demand paged shared library not stripped
The file was so old that to read the data I had to find an older version of binutils that still supported a.out-sunos-big
. Luckily that worked and it was able to reconstruct a ton of useful data.
Here is the full recovered data, if you’re interested.
I got another stroke of luck, I found a third party printer driver on an old SUNET archive for the Siemens Highprint 7400. Remarkably, it had some ancient Codeview debugging data left in it.
Here is a picture of a lady printing some rorschach tests with her new Highprint 7400, for some reason.
The debugging data was so old, I struggled to find a tool that could parse it. The data had a version code NB02
, which means they probably used link.exe
version 5, released around 1988. I tried TDUMP, CVDUMP and even SYMDEB, and none recognized it. I did eventually find an old enough version of Codeview that could read it, but it had no way to save output to a file except “printing” your backlog, and the backlog wasn’t big enough to hold all the data!
I had to script a DOS emulator to scrape the data out a page at a time! I can’t tell you how much time I wasted getting this working, but it was worth it, it gave some valuable hints about how the DEVPRIM (Device Primitive) API worked.
Publics for: _DVTBL!*
Symbols for: L13PSI74!*
3CD5:016E struct psd
3CD5:006E struct GdvEntryTbl1_ pcf
3CD5:01DC unsigned int print_head_height_dots
3CD5:000A char grph_init_string[]
3CD5:0158 char gr_eol_str[]
3CD5:016C unsigned int col_res_num_pos
3CD5:0020 char start_grph_str[]
3CD5:0050 char last_grph_lf_str[]
3CB1:001C unsigned char far * graph_data_ptr
3CD5:0064 char gr_eog_str[]
3CD5:0006 unsigned long graph_data_size
3CD5:0000 unsigned int scan_width
3CD5:001E unsigned int stripNum
3AC8:0006 unsigned int PROC hmu_len()
[BP+0008] unsigned char far * lmbcs_str_buf
[BP+0006] unsigned int lbuf_len
[BP-0006] unsigned int return_hmu
[BP-000A] unsigned char far * bundle_ptr
[BP-0004] unsigned char far * orig_ptr
3AC8:0148 void PROC finish_down()
[BP+0006] unsigned int distance
3CB1:000C char max_feed[]
[BP-0006] int i
[BP-0004] unsigned char far * p
3AC8:01A4 int PROC graphics_init_printer()
Here is the full recovered data.
With this data, and a weekend in IDA I was able to figure out a large amount of the API. It was painful getting a toolchain that worked, but I was eventually able to produce something that Lotus could load.
I think I must be the first person to write a Lotus 1-2-3 driver in thirty years!
My driver didn’t actually do anything, but when I responded to device information requests claiming to support 256 columns, I saw in my log that it sent me an LMBCS string to decode like this:
ENTER WriteLmbcsStringWithAttributes, caller 15f:6c7d
lotdemu.c:291: TRACE byteslen 238
lotdemu.c:293: TRACE attrs 000
lmbcs.c:104: TRACE translate_lmbcs
lmbcs.c:106: TRACE maxdst 256
lmbcs.c:107: TRACE maxsrc 238
000000: 20 20 20 20 41 20 20 20 20 20 20 20 20 42 20 20 A B
000010: 20 20 20 20 20 20 43 20 20 20 20 20 20 20 20 44 C D
000020: 20 20 20 20 20 20 20 20 45 20 20 20 20 20 20 20 E
000030: 20 46 20 20 20 20 20 20 20 20 47 20 20 20 20 20 F G
000040: 20 20 20 48 20 20 20 20 20 20 20 20 49 20 20 20 H I
000050: 20 20 20 20 20 4a 20 20 20 20 20 20 20 20 4b 20 J K
000060: 20 20 20 20 20 20 20 4c 20 20 20 20 20 20 20 20 L
000070: 4d 20 20 20 20 20 20 20 20 4e 20 20 20 20 20 20 M N
000080: 20 20 4f 20 20 20 20 20 20 20 20 50 20 20 20 20 O P
000090: 20 20 20 20 51 20 20 20 20 20 20 20 20 52 20 20 Q R
0000a0: 20 20 20 20 20 20 53 20 20 20 20 20 20 20 20 54 S T
0000b0: 20 20 20 20 20 20 20 20 55 20 20 20 20 20 20 20 U
0000c0: 20 56 20 20 20 20 20 20 20 20 57 20 20 20 20 20 V W
0000d0: 20 20 20 58 20 20 20 20 20 20 20 20 59 20 20 20 X Y
0000e0: 20 20 20 20 20 5a 20 20 20 20 20 20 20 20 Z
translated characters is 238
lotdemu.c:209: TRACE WriteStringToFramebuffer
It’s asking me to write 26 column headers to the framebuffer!
This validated the idea, Lotus 1-2-3 does internally support arbitrary resolutions!
A ton of hacking later, and I do now have a usable driver for dosemu that supports arbitrary resolutions, just look at all those columns!
I know enough about the API that I think I can probably add libcaca support so that text mode graphs work, but that’s a project for another weekend!
My work-in-progress source code is available on github.
I’ve put all of my notes online so that in the event that anybody else wants to make 123 DOS work somewhere new, they won’t have to repeat all my work!
I got ascii art graphs working in a terminal too! Here is a 123 plotting a sine wave in an xterm.