Showing posts with label venix. Show all posts
Showing posts with label venix. Show all posts

20200716

Traditional Unix Toolchains

Traditional Unix Toolchains

Older Unix systems tend to be fairly uniform in how they handle the so-called 'toolchain' for creating binaries. This blog will give a quick overview of the toolchain pipeline for Unix systems that follow the V7 tradition (which evolved along with Unix, a topic for a separate blog maybe).

Unix is a pipeline based system, either physically or logically. One program takes input, process the data and produces output. The input and output have some interface they obey, usually text-based. The Unix toolchain is no different.

Overview

Here's a simplified view of what's going on. We'll add more detail later.
In this view, the C compiler takes .c code and turns it into assembler. How it does that, and how it optimizes, etc is for another blog post. Once the assembler is created, it's passed to as(1) which translates the assembler into .o files. The .o files contain the binary representation fo the assembler, plus a lot of metadata about it: what addresses correspond to what symbols, how to relocate the raw assembler when connected together, various debugging information (sometimes) and what section each bit of data resides in. You cannot directly execute a .o file. ld takes all the .o files and produces an executable (the default name of which is a.out). a.out files are executable. They happen to be in the same format as the .o files, except they have a different magic number which tells the kernel how to load them into memory and initialize the CPU's registers for that program.

Program Layout

In traditional unix, there were only three sections to a program. There were no shared libraries or other fancy things done by the linker (such as linker sets). The world view was much simpler. There were three sections, each one had a size. There was the 'text' section. This was the executable code. There was the 'data' section, which contained initialized data. And there was the 'bss' section which also contained data that was initialized to 0. 
The heap resides above the bss and is managed by the unix sbrk(2) system call. The so-called "break" is set to the end of the bss segment (often referred to by the symbol ebss). Malloc(3) is built on top of sbrk(2) and will manage returning bits to the OS when it can.

For the PDP-11 and other segmented architectures, there can be complications. There can be separate I&D space (instruction and data) so that each one resides in it's own address space. This helps PDP-11 programs break the 64k limit. In addition, there can be overlays. Overlays are 8k segments that are mapped into the address space as needed to increase the text size of the program. The linker handles much of this, but the programmer must specify the overlap groups. Each group can be no more than 8k in size, and the main program can be no more than 56k in size (and the overlay manager uses 8k of the data segment as well). Programs in unix tend to not use overlays, but the kernel makes heavy use of them.

Compiler (cc, f77, etc)

Compilers create assembler output. Compilers, like the C compiler, may invoke other programs to do this. The C compiler runs the source through cpp to create an intermediate file (.i files) that it runs through the first pass of the C compiler. There are a number of other passes of the compiler that take the initial output and optimize it in various ways (usually by parsing and rewriting assembler). The final output of the compilers, at least in this era, is always textual assembler.

Assembler (as)

The assembler is the only thing in the system that creates .o files. Well, not strictly true since ld can also take .o files and produce a .o file from it, but true enough. The assemble takes the textual assembler and creates a .o file from that. The .o file includes information about how to relocate it (ld uses this info), what symbols go where, what bits are in the text section, what is in the data, etc.

Archive Files (ar)

It's fairly expensive to open a lot of files on Unix, especially if they are small. It's also inconvenient to carry around a number of different files to implement something. So unix ld also supports reading files in from an archive. An archive adds some headers to describe the file and then places a copy of the file into the archive. Unix has had a number of different archive formats (I could do a mini-blog entry on all of the ones through 4BSD), but conceptually they are all the same. The ar(1) program is used to create and manage .a files for this purpose.

One motivation for the library is to save space. If it's efficient to create a lot of tiny .o files, then the loader can only bring in what's needed saving space. When the address space is only 64k (or 64k+64k for separate I&D machines), every little bit helps. The archiver removes the overhead of having to do directory lookups on hundreds of files by creating a container for those objects that ld doesn't have to process by name.

When processing through a library, ld looks at each .o that's in the archive for symbols and includes it if it finds any that are needed for the current image so far. It just does one pass through an archive, though. This means if you have foo.o and bar.o in the archive and foo.o depends on bar.o somehow, foo.o needs to come first so that ld can find bar.o later. If they are in the other order, then ld may pass over bar.o entirely and then when it is processing foo.o, it will not go back and look for it.

lorder(1) and tsort(1) are used to try to optimize the order of the .o's in the library to make it possible to just do one pass through the library (though when you have circular dependencies, you're back to this same issue). lorder uses nm(1) to read all the .o's on the command line and produce dependencies. tsort takes these dependencies and sorts them into a list in the proper dependency order where possible. When cycles exist, it produces an order that minimizes passes required to resolve them all.

That sounds quite inconvenient, and it is. libc.a, especially in newer versions, has a number of circular dependencies that a single pass fails to resolve. One can work around this issue by specifying libc multiple times (which is unsatisfying, even if it doesn't produce a binary with two copies of everything), or do something else. The something else involves adding a table of contents to the library. A program called ranlib will read through an archive creating an index of defined symbols that points to the offset in the file that he .o with that symbol is present at. It does this by creating a first member of the archive named __.SYMDEF and placing its table in there. The format of the table is something that ranlib and ld agree on. ld uses this table to include files that are needed and to be able to seek backwards easily. In effect, this is the same as ld doing two passes over an archive (one to build this index, and one to process it), but in effect precomputes the first pass to keep ld simpler.

Loader (ld)

The linker can operate in two different modes. The first mode most people are not familiar with. In this mode, it will take .o files and partially link them together to produce a new .o file. libc uses this mode of operation to create all the assembler glue to call system calls and optimize out some of the local labels that the assembler produces and expects ld to optimize away. Since these files are consulted so often, the build process of libc does it at build time so that every invocation of ld later can be faster.

The second mode is the mode people are more familiar with. In this mode, ld combines a number of .o and .a files to create an executable (a.out by default, so that's what people call executables in general). a.out binaries lacked shared libraries, so ld produced the final output. This was both good and bad. It wasted space with all those copies of libc, but it also produced self-contained binaries that didn't need any external libraries to work. The PDP-11 didn't really have good demand paging hardware, like the later VAX machines, so was a poor fit to shared libraries. Shared libraries generally rely on mmap(2) working and larger address spaces to map the libraries in at. mmap(2) requires a page-grained MMU, which didn't usefully exist on the PDP-11 (it had 8k segments, which was far to large a percent of the whole address space to be useful). One good thing about binaries being self contained means that if the kernel can run the system calls that are in it, the binary will work. This has allowed PWB systems to be able to execute both v6 and v7 binaries, despite the two having different system call interfaces.

The Kernel

Speaking of the kernel, the kernel is the last step in the toolchain, or can be thought of as such. The kernel reads in the headers from the a.out files, and sets up the address space for the process when a new a.out binary is exec'd. It uses the layout I showed above, or some variant of it, to set things up, to populate memory and makes whatever arrangements with the MMU to protect the pages from other processes (if possible, some systems like an 8086 don't have MMUs but do have segments so can fake all this except the memory protection benefits). For separate I&D space binaries, it also sets up the segment registers for that to work.

The stack is also initialized. The detail of exactly where it goes varies somewhat. It's usually located with the data segment since it holds data almost exclusively. Stacks in this era were usually quite small. This tended to drive programs that had shallow call graphs and that made use of more global variables than a more modern style would suggest. All these things conserved stack space, though it's generally agreed today that it required more effort to read and understand because the context is spread out over more parts of the program than more modern coding practices tend to produce.

Conclusion

Without the complications of shared libraries, or link time optimizations, the tools of this area tended to be rather simple. They had simple interfaces between them. There were good boundaries between the different components. This limited the number of programs with knowledge of the formats for the different layers. Due to this limited spread of knowledge, switching out different parts for other parts often could be done without changing components that didn't directly know about the object format. This also produced simpler programs that used different engineering tricks to get the most performance out of the limited hardware of the day. The PDP-11s were approximately 0.1-0.5MIPS machines in this time frame with super slow I/O paths. This is about 100,000 times slower than most computers people interact with today. One advantage of the thoughtful engineering trade offs is that all the pieces are relatively easy to understand.

The modern ecosystems that we have today are more complex. ELF came along in the 90s and obsoleted the text, data, bss world view. shared libraries made huge programs, like X11, feasible. Today, clang bypasses the separate assembler stage and generates .o files directly. The llvm linker, lld, can optimize binaries between modules to produce better code. All these new features added complication to a simple model. While I morn for the loss of simplicity, I've become too used to the rich features they provide to want to go back. Understanding the roots of this complexity, though, helps to understand some of the weird quirks that persist, even to this day.

And speaking of weird quirks, I'd like to end with 'bss'. It's a 1950s IBM assembler mnemonic for 'block started by symbol' and was used to create storage that was associated with a symbol, but had no initial value. Today, 'bss' is no longer that, exactly. Its origin has been lost, for most people, in the sands of time and now it just means 'zeroed storage area'. So this very Unix centric term actually predates Unix by 10 or 15 years for a machine that Unix wouldn't run on until it was 10 or 15 years old... Here's a snapshot from the IBM assembler manual, available from the UA-SAP wikipedia page
showing the original source...

[[ This blog edited to include snapshot of the BSS manual entry ]]

20170604

Reading in my Rainbow Floppy Collection

Recently, I acquired a kryoflux board to help read in the Venix floppies... I finally have it up and running.

I've managed to read in most of my collection of ~300 Rainbow floppies. 95% of them were actual Rainbow disks. The other 5% were either completely blank, or some variety of IBM format (including 3.5" 720k on a 5.25" floppy!) with a couple of 'robin' formatted ones just to keep you on your toes. I didn't wind up seeing any of the very rate dual-sided RX-50's (they exist, but are super rare because this was a home-brew job not an official thing).

While it is possible just to do
dtc -ffoo.img -i4
It takes a long time since it's reading the back-side that has no data on it. Using
dtc -ffoo.img -g0 -i4
is all that's needed to go fast. Some caveats should be observed.

Nearly every track has extra data in at least one of the sectors. This threw me for a loop, but is harmless (as stated in the manual).

I had some issues with old, ill-maintained floppy drives. Bought a refurbished one and all my troubles just went away. I had several old drives and based on the good floppy drive, I could find known good floppy diskettes and was able to refurbish one of my old units. I had the best luck with the TEAC FD55-GFR drives, btw. Not sure it's worth while to refurb the rest of the units, though, since I have something that's working...

Keep an eye on the number of sectors reported. It's usually 10. If it isn't, stop right away. It isn't a Rainbow / RX-50 formatted disk. If it's 8 or 9 sectors, it's likely an 320k or 360k IBM floppy. Removing -g0 and adding -k2 is useful for these diskettes (saves time more than anything. If it's 15 sectors, it's likely a 1.2MB floppy. Had 2 or 3 of those mixed in with these diskettes despite never having a machine that produced them.

As these disks age, you may need to try multiple times. I have some disks, though (maybe 1%) that I can't read back certain tracks on even with that. The default is good, but trying more helps with a similar number (so -t10 -tc5). I don't know if there's a way to retry individual sectors, or if dtc is internally buffering good sectors from previous retires or not. Previous programs I've had on the Rainbow for extraction had great luck retrying sectors multiple times when the full track read didn't work...

Some disks are copy protected. Not sure how best to preserve these disks. The -i4 format won't work with, for example, Lotus 1-2-3 system disks because tracks 78 and 79 have sectors numbered up to 12 and are missing sectors (they do this with a special format that I believe omits sectors 9 and 10 and instead has sectors 11 and 12). There are other copy protected disks that have deleted sectors since the Rainbow could easily read those, but they were hard to create from standard interfaces. Don't know if -i2 images would help, or if I have to go all the way down to -i0 to ensure proper imaging. The copy protected disks, however, were all broken in the '80s, so preserving them isn't so interesting to me. There's also a couple of disks that have data on track 82, but it's marked as track 79 and it's unclear what this data is. I know there are several old formats that preserve this info (TELDISK and DISKIMAGE being two that I've run into most).

cpmtools is able to extract data from CP/M formatted disks (use dec_pro format). It understands all about the interleaving you need to do on tracks 2+ and can work on raw kyroflux images.
As for MS-DOS disks, I wrote a small utility to convert the physical layout to a logical one, and to (optionally) replace sector 0 with a standard boot record with the BPB in it (Rainbow floppies have Z80 boot code which has a different starting byte (f3) and doesn't have the tables expected. Doing this, both mtools and FreeBSD's msdosfs can copy files easily enough. But so far it's not reversible, so you can't (yet) use it to create floppy images. I'll be testing two tricks (just prepending the magic sector and appending sector 0 at the end) to see if that might help. I can post a pointer if there's interest.

Venix disks are a mixed bag as far as data extraction, but are otherwise boring: 80 tracks, 1 side, 10 sectors per track. Just had brief access to the distribution diskettes, mostly prior to having kryoflux (and the reason for buying it). Ironically, I couldn't get the kryoflux working due to the drive issues I discussed above before I had to return the distribution diskettes. But I was able to copy them natively on my Rainbow...

Finally, there's still 2 or 3 disks that I've been able to read in that I have no clue what the format is. They are kinda sorta CP/M, but also kinda-sorta MS-DOS from looking at the first two tracks, but neither quite works to decode them. Physically, they RX-50. They don't look like ODS-2 nor v7 unix file systems, but that's harder to know for sure. They are unbootable (tracks 0 and 1 are 0xe5, the erasure pattern for RX-50s).

My only real complaint is that it would be nice if dtc would spit out a summary at the end, including tracks unreadable. I have to keep an eye on the logs otherwise. Not a huge deal, to be sure.

20170511

v7 compilation for simple .c's in /usr/src/cmd.

After about 4 and a half hours of build time, I have results to share. There's 110 .c files in /usr/src/cmd. 85 of these compile and link (it's unclear if they run). 25 have a compile or link error:

  • accton, sa (lack of acct in libc)
  • ar, nm, prof, ranlib, size, strip (mismatch between NMAGIC/OMAGIC and ARMAGIC_X)
  • arcv (obsolete conversion program written in old style)
  • clri, dcheck, dump, dumpdir, icheck, mkfs, ncheck, quot, restor (inode differences)
  • dmesg (message buffer issues?)
  • getty (freaks out compiler)
  • graph (missing symbols for curses and libm stuff)
  • osh (old shell? Written in funky style)
  • ps, pstat (proc definition differences)
  • tc (silly compiler issue, trivial to fix, but I have not Tek4015 terminals)
So 10 classes of issues for the affected files. Not too terrible. But there's other issues that will need looking into. 'ld' compiles fine, but it works on pdp11 a.out files, for example. cc compiles, but doesn't have the venix specific switches. I've written a shell script to do this, not a Makefile, and the commands used aren't quite right. Venix appears to support different amounts of stack under the data segment (but without docs, it's hard to know what flags I need), but none of my binaries have those. There's NMAGIC and OMAGIC binaries that have different meanings, and so on. All this makes the 'file foo /bin/foo' differ. Plus I haven't done the more complicated commands, nor have I poked at the compiler apart from cc, nor the kernel really at all, nor the libraries, nor the .y files. And factor.s and prime.s are pdp-11 assembler, so I'd have to pull those in from somewhere.

But given that almost all the normal commands compiled just fine, and many appear to work gives me hope that I might be able to reconstruct the sources used to build at last parts of the system. For some reason, that's appealing to me, but I can't really articulate a reason why, or come up with why that might be useful. Though it seems like a fun background project to celebrate the 40th anniversary of Version 7 in a couple of  years. Though running it on a 35 year old computer at that point may limit its appeal...

Rebuilding v7 sources on Venix, early results

I just started rebuilding the v7 sources on my Venix Rainbow on a lark. I'll post a full writeup, but here's a two early things I've learned.

First, it takes 1 minute to compile cat.c into cat. Yes, one full minute. On my normal server, it takes .1s of elapsed time. This is a 600x speedup since then, likely more since clang isn't known for being gentle on system resources.

Second, some compiler error messages are classic:
"getty.c", line 12: compiler error: insane structure member list
I have always thought getty to be rather opaque to use. The code is somewhat simple, but that simplicity doesn't lead to easy understanding in this case.

Most things not dealing with the filesystem are building w/o a problem. This is a pleasant surprise. Though the speed at which they are building isn't going to set any records. About 5% have issues that prevent compilation or linkage out of the box, though the problems look easy to fix. I started the build at 9:30. It's now 2 hours later and we're to building 'grep'. We're on pace for a rebuild of just the trivial foo.c -> foo programs taking 5-6 hours. This isn't for the more complicated programs like make or tar that have their own subdirectory.


20170507

Rainbow Venix with BSW Enhancements

I've taken some time to look at the Venix disks. I've managed to read them all after retrying the read errors enough. I've created new disks and installed it on my system. I'll go through all that in a forthcoming post since there's lots of fiddly bits.

This post I'll expose what BSW is. It's the Boston Software Works. It was a company that was run by Larry Campbell from 1985 until 1995. They had a number of different products over the years. Through about 1988 they offered their enhanced version of Venix for $800 a copy (not their choice on the price).  It was normal Venix, only better. Larry posted the following to mod.newprod on Usenet back in December1986 describing what it was. I've edited the formatting a bit, and trimmed the headers:
The Boston Software Works (BSW) is now distributing an improved version of VENIX/Rainbow.  VENIX/Rainbow is a complete UNIX(tm) system for the DEC Rainbow 100, developed by VenturCom, Inc. of Cambridge, Mass. It is a V7-based system, with extensions such as plot(3) drivers for the Rainbow color graphics board and DEC LA50 printer, semaphores, and shared memory.
Standard VENIX/Rainbow requires and supports the DEC RD51 winchester disk (10MB) only.
The Boston Software Works enhancements to VENIX/Rainbow include:
  1. Support for any disk physically connectible to the Rainbow. Our development system runs a 70MB Micropolis disk.
  2. Virtual consoles, as found in VENIX on IBM-compatible machines, and also in XENIX and Microport System V/AT.
  3. Support for the NEC V20 chip, which replaces the Intel 8088 for a 10-15% performance gain.
  4. Support for simultaneous use of a monochrome display (for terminal/console use) and a color graphics display (for graphics).
  5. Ports (on an as-is basis) of several public domain programs, such as the Usenet news software, rn, smail, and Jove, an excellent EMACS subset.
  6. Support for the CHS dual-winchester controller.
The price (single copy) for VENIX/Rainbow is $795 and includes a complete manual set.
I've installed both the original Venix/Rainbow and this enhanced version. The enhanced version is the way to go. It knows how to cope with the larger disk I installed in my Rainbow, and the installation process was much less painful (though there were some up-front gotchas that I wound up having to re-run WUTIL to get through).
These are all cool things. The only issue that I've seen is that the .o's aren't included so it's impossible to rebuild the kernel with these neat, new features without significant hassle. The other problem is that while I can get stable file transfer on MS-DOS with LCTERM at 9600 baud, I can't with Venix. 4800 is error prone even. Messing with the serial chip seems to also trigger the dreaded Interrupts Off message, so I've not messed with it too much. Still don't know if that's my Rainbow going bad, or other issues (have some spare parts arriving soonish). The maximum throughput is only 240-300 CPS anyway with LCTERM, so maybe I'll just run at 2400 or 3600 baud.
Not listed above: it kinda seems like it is reading my clikclok. I did have to fix the 'date' command to grok Y2k though (well, since I don't have the sources, I used the Unix v7 sources from TUHS). If this keeps up, I'll put together a distribution of useful things from V7 :).
I'll blog more when I've run through another install and worked out how to boot in MESS (it has a bug when booting directly off the hard drive, but maybe the BOOT program will fix that).

[[ Edited May 8, 2017 to supply link to original mod.newprod article on google groups ]]