# 123elf runs the real Lotus 1-2-3 binary on Linux by relinking it

> 123elf is a C port that does not reimplement Lotus 1-2-3 but relinks the original Release 3 UNIX object code for Linux, using binutils compiled with the coff-i386 target and a custom object rewriting step. The build requires your own copy of Lotus 1-2-3 for UNIX, and the resulting binary keeps the original spreadsheet's behaviour, including its live ASCII charts and context sensitive help.

**taviso/123elf** — A native port of Lotus 1-2-3 to Linux.

- Repository: https://github.com/taviso/123elf
- Stars: 1,299 · Forks: 57
- Language: C
- License: not declared
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/taviso-123elf

## It is a port, not a reimplementation

123elf is described as a native port of Lotus 1-2-3 Release 3 to Linux, and the distinction matters more than any other fact about the project.

The README points to an article documenting how this is possible, and the build instructions give away the mechanism. You need a copy of Lotus 1-2-3 for UNIX, you place the raw disk images in the build directory, you run extract.sh, and then you run make.

What the Makefile does with those files is the interesting part. The BFD targets are set to coff-i386 for both input and output, and the objcopy call converts the original object files.

```make
BFD_INP_TARGET = coff-i386
BFD_OUT_TARGET = coff-i386
OBJCOPY_FLAGS  = --wildcard --localize-symbols=localize.lst --globalize-symbols=globalize.lst --redefine-syms=redefine.lst $(shell cat symbols.lst)

123.o: orig/123.o $(OBJCOPY_FILES) | coffsyrup
	objcopy -I $(BFD_INP_TARGET) -O $(BFD_OUT_TARGET) $(OBJCOPY_FLAGS) $< $@
	coffsyrup $@ $(@:.o=.tmp.o) $$(cat undefine.lst)
	mv $(@:.o=.tmp.o) $@
```

So the pipeline reads the original 32-bit COFF object files that shipped with Lotus 1-2-3 for UNIX, rewrites their symbols with objcopy, and then runs a custom program, coffsyrup, whose source is in the repository as coffsyrup.c, to fix up the result before it is linked into a Linux binary.

That is what makes the port possible, and it is why the project exists rather than a reimplementation. Lotus 1-2-3 Release 3 was written in assembly for the original platform, and the behaviour of the original, the chart rendering, the macros, the 8192-cell limits, the idiosyncrasies, is not something a reimplementation would reproduce. The port keeps the actual program and changes the environment around it.

The result is a native binary. The README says just run ./123 in the project directory after building, or 123 if you installed it, and the screenshots show an interactive full-screen terminal application with live-updating ASCII-art charts, which is the original program's graphics, not a reimplementation's approximation of it.

## The binutils dependency is the first hurdle

The build dependencies section starts with something that will stop most people.

First, you need a version of binutils that is compiled with coff-i386 target support. You can check like this:

```
$ objdump --info | grep coff-i386
```

And the note that follows is the important one. Most distributions do not enable this for some reason.

The COFF target support is what lets objcopy read and write the object format the original Lotus 1-2-3 for UNIX was built in, and the reason distributions drop it is that COFF is a format nobody's distribution ships binaries in any more. It survives in the binutils source because the target is still there for the sake of the formats that were once common, and turning it off is a build-time size decision that has been made in most Linux distributions.

The Makefile therefore includes a check step that runs before anything else, and it fails with a specific message.

```make
check:
	@objdump --info | grep -Eq '^coff-i386$$' || (echo "$$BFD_TARGET_ERROR"; false)
```

And the error text is written out in the Makefile as a variable.

```make
define BFD_TARGET_ERROR
Your version of binutils was compiled without coff-i386 target support.
You can try running ./binutils.sh to build a version that does support it.
endef
```

The project ships the solution, then. There is a binutils.sh script at the top of the repository that downloads and compiles a version of binutils known to work, which the README recommends for anyone whose distribution has not enabled the target. Building binutils yourself is a few minutes, and it is the price of admission.

The rest of the dependencies are ordinary. The C flags are -m32 with -freg-struct-return, so the whole thing is a 32-bit build, and the linker flags include -no-pie, which was the usual requirement for this kind of fixed-address binary at the time. The libraries are ncurses, tinfo and the maths library, and the compiler flags include -D_FILE_OFFSET_BITS=64 and -D_TIME_BITS=64, which are the fixes for the 32-bit time and file offset overflows, and -D_GNU_SOURCE.

## The symbol lists are the real specification

The Makefile and the repository listing show an approach to the port that is worth understanding, because it is unusual and it is where the difficulty of the project lives.

The port cannot simply relink the original object files. They were built for a different operating system, they reference system calls and library functions by different names, and some of the symbols collide with the ones in the Linux C library. So the project maintains a set of lists that drive the rewriting.

There are five of them, all listed in the Makefile as OBJCOPY_FILES, and each has a distinct job. localize.lst and globalize.lst control the visibility of symbols, which is how a symbol that was local in the original build becomes visible or the reverse. redefine.lst renames symbols, which is how a call to an original operating system routine is redirected to a different name on Linux. undefine.lst removes definitions, and symbols.lst, which is passed to objcopy directly, is the master list of the symbols that need attention. The file named symbols.lst is read into the flags with a shell substitution, $(shell cat symbols.lst).

The build then runs coffsyrup, a program written for this project, on the output. The name is a pun on COFF syrup, and the source is coffsyrup.c in the repository, with a matching header coff.h. The step takes the objcopy output and the contents of undefine.lst and produces the final object, so it is the piece that finishes whatever the mechanical rewriting did not cover.

The source tree is organised around the original program's structure, with the headers named after the areas of the spreadsheet, lotdefs.h, loterrs.h, lotfuncs.h and lottypes.h, plus kfun.h for the function table, and a set of list files. There are also support directories for the two things the original program needed and Linux does not provide in the same form, atfuncs/ for the at-funcs, the Lotus compatible function names, and ttydraw/ for the terminal drawing.

The ttydraw/ directory has its own Makefile invocation in the top level, and it is the reason the graphics work. The original Lotus 1-2-3 for UNIX used a specific terminal interface, and ttydraw provides a replacement, and the README's screenshot of interactive, live-updating ASCII-art charts is that layer doing its job.

The Makefile also builds the keymaps. KEYMAPS lists xterm and rxvt-unicode by default, and the pattern adds the 256 colour variants of each, and then screen.xterm-256color, vt100 and the current $TERM. The keymap/ directory holds them, and the README's bug list notes that the keyboard map seems to be incomplete with a strikethrough and a note that XTerm compatible terminals should be working, please test others.

## What works and what does not, according to the project

The Bugs section of the README is unusually honest, and it is the most useful part of the document for anyone deciding whether to try this.

The keyboard map seems to be incomplete is struck through, with the note that XTerm compatible terminals should be working, please test others. Graphs don't work yet is struck through with Partially working!, and a link to issue 5. Printing doesn't work yet is struck through, with Print to file works, print to lpr is being worked on, and a link to issue 50.

So the charts partially work and the printing partially works, and the README is honest about both. What remains open is listed plainly. STEP mode does not display the current step, marked Nearly working! with a link to issue 101. And there is limited i18n support, We're working on it, with a link to issue 73.

The closing line of the section is a good description of the project's posture. File an issue if you notice something, there are probably lots of minor issues that can be fixed!

The FAQ has two entries and both are the kind of thing that only shows up in real use. How do I quit 123? If the status indicator in the top right says READY, try /Quit Yes, and if it does not say READY, it might say ERROR, HELP, POINT or MENU or something else, in which case you try hitting Esc until it goes back to READY. And the second is the error invalid compressed data, code out of range, which comes from a very old gzip with broken lzw/pack support, and the fix is to run the gzip.sh script to build a more recent gzip and then rerun the extract script. There is a gzip.sh in the repository root, next to the binutils.sh and extract.sh.

The getting started note in the Running section tells you the interface details. You use the / key to open the 123 menu, and functions use @ instead of =, and the common functions like @SUM, @AVG, @INDEX and even @HLOOKUP all work as you would expect. The context sensitive online help is there, you press F1 at most times to see some hints, and the screenshots list it as a feature.

The man page is in share/man/man1/123.1 and describes the command line options, and the full manual is linked from the original Lotus 1-2-3 Release 3.1 reference on archive.org.

## Where 123elf is the wrong choice

The first wrong case is anyone who wants a spreadsheet to use. This is not a modern spreadsheet and does not pretend to be. It is the 1990s binary, with the 8192-cell limits, the macro language, the Lotus-style function names, and the character-mode interface. If the requirement is a functional spreadsheet, a modern tool is faster, more capable and does not need a patched binutils.

The second wrong case is a user who cannot obtain Lotus 1-2-3 for UNIX. The project is explicit that you need a copy of it, linked from an archive.org page, and the build will not proceed without it. The extract.sh step, and the gzip compatibility issue in the FAQ, are both consequences of that dependency. This is not a criticism, since the project is not trying to distribute the original, but it does mean the port is only reachable for people who can legitimately obtain a copy of the 1980s UNIX release.

The third wrong case is a team that needs a maintained product. The latest tagged release is v1.0.0rc4, a release candidate, from 2023-03-28, with the previous ones in 2022, and the last push to master was on 2026-06-27. The work is clearly alive, but the release line has not reached 1.0 and the bug list still has entries marked nearly working. A team depending on this in production would be depending on an upstream that has never declared itself stable.

The fourth wrong case is a team that cannot build binutils. The check step fails fast with a good message, and the project offers a script to work around it, but building a patched binutils on a locked-down or non-x86 machine is a real obstacle, and the whole port is 32-bit x86 with the COFF toolchain required.

## How it differs from reimplementing Lotus 1-2-3

The comparison worth making is with a clean-room reimplementation, since that is the obvious alternative, and the difference in approach is fundamental.

A reimplementation writes a new program that behaves like Lotus 1-2-3. You get the formulas, the cell references, the 1-2-3 function names, the / menu structure, and you choose how to implement each. The advantage is a clean codebase that compiles anywhere and is maintainable. The disadvantage is fidelity, because Lotus 1-2-3 had a great many behaviours that are not in any manual, the way the screen is redrawn, what a particular key combination does in a particular cell state, how an error is worded, and the fact that the original had bugs that users relied on.

123elf takes the opposite approach and keeps the original program. The behaviour is the original behaviour, exactly, including the parts nobody documented, because the code is the code. And the whole difficulty of the project moves from reimplementing the spreadsheet to building an environment the original object code can run in.

That is a completely different engineering problem, and the repository shows what it involves. Reading and rewriting COFF object files. Managing symbol visibility, renaming and redefinition. Providing the terminal drawing layer so the original's graphics code has something to talk to. Providing the @-prefixed function entry points. Translating the original operating system interfaces onto Linux ones. And generating keymaps for every terminal anyone might use.

The second difference is the size of the work for the user. A reimplementation gives you a program you can install. This gives you a build that needs a specific binutils target, a disk image, an extraction step, and possibly a rebuilt gzip. That is a much bigger commitment for a user, and it is the right trade for the project and probably the wrong one for most people.

The third difference is the failure modes. A reimplementation fails in ways you can debug in source. This fails in ways that involve the original code, which you cannot read, so the debugging tools are the ones that ship with the port, the size report at the end of the build, the issue tracker, and the FAQ. The project's own bug list, with graphs partially working and printing to lpr still in progress, is a fair picture of what porting a closed binary actually involves.

## Conclusion

123elf fits a retrocomputing enthusiast who wants the authentic Lotus 1-2-3 Release 3 on Linux rather than a reimplementation, and it fits a systems programmer who wants to read a worked example of binary rewriting and cross-object-format linking. It does not fit a user who wants a modern spreadsheet, and it does not fit anyone unwilling to source a copy of Lotus 1-2-3 for UNIX and to build a patched binutils. Before attempting the build, check that your binutils supports coff-i386, since the README notes most distributions do not enable it, and note that the tagged releases are release candidates, with v1.0.0rc4 from 2023-03-28 and the last push on 2026-06-27.

## FAQ

### How does 123elf port Lotus 1-2-3 to Linux?

It relinks the original Release 3 UNIX object code rather than reimplementing the program. The build extracts the files from a copy of Lotus 1-2-3 for UNIX, converts the COFF objects with objcopy using localize, globalize, redefine, undefine and symbols lists, then runs the project's own coffsyrup program over the result before linking it into a Linux binary.

### What do I need to build 123elf?

A binutils compiled with coff-i386 target support, checked with objdump --info, though most distributions do not enable it so the repository ships binutils.sh to build a working one. You also need your own copy of Lotus 1-2-3 for UNIX, whose raw disk images go in the build directory before running extract.sh. The listed packages are build-essential, gcc-multilib and lib32ncurses-dev, with Fedora equivalents.

### How do I install and run 123elf?

Run make install to install into /usr/local, or make install prefix=/my/prefix for a custom prefix, and make uninstall to remove it. To run it, just run ./123 in the project directory after building, or 123 if you installed it. A man page in share/man/man1/123.1 describes the command line options.

### What does not work in 123elf yet?

The README's bug list is explicit. The keyboard map seems to be incomplete but XTerm compatible terminals should work, graphs are partially working, and printing works to file while printing to lpr is still being worked on. STEP mode does not display the current step, which is marked nearly working, and there is limited i18n support.

### What is the current release of 123elf?

The tagged releases are release candidates, with v1.0.0rc4 from 2023-03-28, v1.0.0rc3 from 2022-09-05 and v1.0.0rc2 from 2022-07-10. The last push to main was on 2026-06-27, and the licence is listed as unknown, so the terms in the repository itself need checking.

## Sources

- [Issues](https://github.com/taviso/123elf/issues)
- [README](https://github.com/taviso/123elf/blob/main/README.md)
- [Releases](https://github.com/taviso/123elf/releases)
- [taviso/123elf on GitHub](https://github.com/taviso/123elf)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/taviso-123elf
