Tuesday, July 24, 2012

Writing PDP11 assembly code from Linux (and running it on bare -simulated- metal!)

Let's pretend for a moment you are as crazy as myself about computing and, specially, about classic computers like the PDP-11. Let's say your crazyness gets to the point you start to consider seriously writing your own operating system for the PDP-11. Or, at least, doing the first steps to write something similat to an operating system. Sounds scary, doesn't it? No way! It sounds fun!

Still reading? Fine, because we are going to do those very first steps towards this goal. And the very first step to build an operating system is to have the hability to write, debug and run standalone software. That is, to run programs in a PDP-11 without any operating system loaded.

To do that, we can use two different approaches:
  • We can use a running PDP11 system with an existen operating system to write and assemble the software, moving it to our "empty" system.
  • We can use a cross-assembler and cross-compiler to write the software under another operating system, and feed it to our PDP11.
At this point, it is important to remember we are talking about a simulated PDP-11. At least in my case, I don't have access to a real machine. Then, it makes sense to use the host operating environment to write and compile the software we will run in the simulator. The plan is to be able to build files loadable into the SIMH simulator using the "load" console command. Then we can use simh to run the software or to single step it. No operating system needed for that.

Load file format

Simh can load into memory files representing a paper tape image. Yup, you have read it well. Punched paper tape. Now we are talking about classic computing! 

We can find the format of those images reading the pdp11_sys.c source file of the simh distribution. The code is in a function called sim_load. The comments block of that function describes the file format, which is not very complicated. The file is composed by byte blocks, each one of them preceded by a header and followed by a checksum. The last block is en empty one (just header and checksum). The structure of the header is as follows:

Offset Length Datatype Content
0 1 char Fixed value: 1
1 1 char Fixed value: 0
2 2 word Size of the data block, in little endian format
4 2 word Load address for that block, in little endian format

The checksum is computed adding every byte value of the block including the header and taking the 2's complement of the low order byte of the result. In other words, the negative value of the low order byte taken as an unsigned character.

The last (empty) block also contains a "load address", but in this case the content of that field will be loaded into the PC register of the machine after the file has been loaded. So it has to contain the entry point for the loaded program. 

We will refer to this format as "load format".

Generating a load format file

From RT-11

The easiest way to generate a load file is to use a running RT-11 system. The RT-11 linker has the option of generating directly load files instead of native executables. We have just to add the "/LDA" switch to the LINK command and we'll get a file with a LDA extension instead of the usual SAV one. The linker will set up the file to load at the octal address 01000 by default, just over the interrupt vector area, so we will be mostly fine with the default. We can change it using the /BOTTOM switch if we really need to load our code in any other place.

So, we can edit our source code in our RT-11 system, assemble/compile it using the native MACRO-11 compiler or whatever HL language we want, and LINK the resulting object into a LDA file. Now we have to transfer that file to our host environment. We have several options to do that. As examples:
  • We can use KERMIT to move the file to our host system. For some reason, I've not been able to do this. When I launch KERMIT in SERVER mode in the RT-SYSTEM it ignores my download requests. 
  • We could use a TCP/IP stack. I haven't done that, so I can't really help about that option.
  • We can use the paper tape emulation in SIMH. To do so, we have to SYSGEN our RT-11 adding the PC device, and then we can simply COPY from or to PC: to transfer the files.
This procedure works, but you have to use the RT-11 environment and specifically the KED editor, which I've found difficult to use with my terminal emulators. You can edit in your host environment and use the paper tape device to upload your sources to RT-11, but its quite cumbersome (and you must be sure you are using the DOS convention for the line terminators...), so I wanted to find an alternative.

Cross-compiling and cross-assembling

The obvious solution is to use a cross-assembler and a cross-compiler to generate the PDP-11 code directly in your host environment. In my case, that host environment is a laptop running Ubuntu Linux. After asking in the simh mailing list, I found myself with several opti (ons:
  • The simh distribution contains a port of the "native" assembler, MACRO-11, to unix. It compiles without problems and generates PDP-11 object files. It also uses the DEC source format (being a port of the native assembler), so it seams to be the best approach to the problem. Unfortunately, an OBJ file is not loadable to SIMH, neither is enough to do medium complex things. I wanted to be able to write modular code, and to be able to link together assembly and C code. Without a linker, I couldn't do that. So I had to discard macro11. Bob Armstrong sent me an utility to extract the contents of the OBJ file to build files in EPROM HEX format; it could be extended to generate LDA files, but nevertheless the linker would still be missing.
  • The GNU toolchain. That means the gas assembler, part of the binutils package, and the very known gcc C compiler, as well as ld and the rest of utilities. After a wrong start that is the option I have choosen. Right now I have been able to write, compile and execute a "Hello world" program writing directly to the emulated PDP-11 console. So I guess I'm in the right path... Let's elaborate a little bit about how to achieve this.

Building the cross-assembler and cross-compiler

First, the bill of materials. I used these GNU packages:

That is obviously not the last gcc version, but I was not able to build the cross-compiler using the last release, so I downgraded back to 3.4. Anyway, we don't need the bleeding edge features of the last version, so we will be fine with 3.4.6. If you are going to build the cross-compiler, download just the "core" archive. Unless you want c++, fortran, ada and the rest of languages the core is all you need.

The recommended procedure to build the tools is as follows:
  • Unpackage the binutils tarball
  • Create a new directory for your binutils build, and cd to it
  • Execute the follwing command (from the new, empty directory):
<binutils_dir>/configure --target=pdp11-aout

Substitute <binutils_dir> for the directory where the tarball was expanded. This command will configure the build for a /usr/localprefix installation. You can change it if you want, using the corresponding configure options.

  • After the completion of configure, just type make to build the software and make install to move the executables to the /usr/local/bin directory (you will probably need to run that command as root using sudo or similar). You will end with a series of executables named pdp11-aout-as, pdp11-aout-ld and so on. Those executables are your cross-building tools.
  • After building binutils, you can build the cross-compiler. The procedure is basically the same, substituting binutils by gcc. It's also recommended to use a separate directory to do the build.

Using the cross-building tools

If you try to compile and link a "helloworld" program you will find the linker does not work. It will complain about a syntax error in its default linking script... 

I really had no idea about that "linker script". Well, we learn everyday. With some inspiration from this nice site, I wrote my own linking script, adapted to generate loadable PDP11 code. The script itself is this:

phys = 00001000;
  .text phys : AT(phys) {
    code = .;
    . = ALIGN(0100);
  .data : AT(phys + (data - code))
    data = .;
    . = ALIGN(0100);
  .bss : AT(phys + (bss - code))
    bss = .;
    . = ALIGN(0100);
  end = .;
ldaout.cmd (END)                    

We are basically telling the linker we want an output format known as "a.out", and that we will be defining the three classical "C" sections (text, data and bss). The executable entry point is start, so we will have to create a global empty point in our code with that name. We are telling the linker to configure the executable to load at the address 01000 octal (0x200), and to align each section at a 0100 (octal) byte boundary.

Obviously, we are missing a detail. SIMH can't load a.out executables. The ld linker supports a binary output format, which could be easily converted to LDA just adding the header and computing the checksum, but unfortunately ld relocates the code to be loaded at the 0 memory position, and that is not useful for a PDP-11 system. So I had to write a program to convert from a.out to lda format... If you are interested you can get the sources from the git repository. It should build without any problems in any unix-like system. To use the utility to generate a LDA file from an unstructured binary type:
bin2load -f input_file -o output_file -b load_address (in octal)

To generate an LDA file from an a.out executable:
bin2load -a -f input_file -o output_file

Don't specify a load address when you convert an a.out file; that information is in the executable itself... And please take into account the utility can be improved a lot. For instance, there is no boundary checking so you could generate something which would load over the PDP-11 64K barrier. Just treat it as a toy :)

Let's play a little bit

So we have all the tools we need and it is time to give them a try. I have written a pair of very simple assembly sources. The first one is a subroutine to write a single character on the console device. It will loop until the console is ready to receive a byte, will write it and will wait again to ensure it has already been sent. Notice this source is in BSD syntax (it uses the dollar sign to specify immediate arguments).

        .TITLE putconch: send a byte to the system console
        .IDENT "V01.00"

        .GLOBAL _putconch

        XCSR    = 0177564
        XBUF    = 0177566
        TXRDY   = 0x0080
        NRETRY  = 5000


        mov     r1,-(sp)
        mov     r2,-(sp)
        mov     $NRETRY, r1
        mov     XCSR,r2
        bit     r2, $TXRDY
        bne     20$
        dec     r1
        bne     10$
        mov     $2,r0
        jmp     999$

20$:    movb    r0,XBUF
        mov     $NRETRY, r1
30$:    mov     XCSR,r2
        bit     r2, $TXRDY
        bne     40$
        dec     r1
        bne     30$
        mov     $2, r0
        jmp     999$

40$:    mov     $0, r0
        mov (sp)+, r2
        mov (sp)+, r1
        rts     pc

        .end _putconch                        

The second one uses this routine to write a string to the console. This one has the "start" symbol and hence is the entry point for the program:

     .TITLE Say hello on console
        .IDENT "V00.00"

        .GLOBAL start
        .GLOBAL _putconch

        STACK = 0x1000

        mov     $STACK, sp
        mov     $hellom, r1
        mov     $helloc, r2
10$:    movb    (r1), r0
        jsr     pc, _putconch
        dec     r2
        beq     99$
        inc     r1
        jmp     10$

99$:    nop

hellom: .ascii  "Hello world!"
        helloc = . - hellom


To assemble the routines we will use our cross-assembler:

pdp11-aout-as putconch.s -o putconch.o
pdp11-aout-as hellopdp.s -o hellopdp.o

After that we can link the a.out executable and generate the LDA file:

pdp11-aout-ld -T ldaout.cmd hellopdp.o putconch.o -o hellopdp.out
bin2load -a -f hellopdp.out -o hellopdp.lda

And now it is the real moment:

sim> load hellopdp.lda
sim> e pc
PC:     001000
sim> e -m 001000:01100
1000:   MOV #10000,SP
1004:   MOV #1200,R1
1010:   MOV #14,R2
1014:   MOVB (R1),R0
1016:   JSR PC,1040
1022:   DEC R2
1024:   BEQ 1034
1026:   INC R1
1030:   JMP 1014
1034:   NOP
1036:   HALT
1040:   MOV R1,-(SP)
1042:   MOV R2,-(SP)
1044:   MOV #11610,R1
1050:   MOV 177564,R2
1054:   BIT R2,#200
1060:   BNE 1076
1062:   DEC R1
1064:   BNE 1050
1066:   MOV #2,R0
1072:   JMP 1140
1076:   MOVB R0,177566
sim> g
Hello world!
HALT instruction, PC: 001040 (MOV R1,-(SP))

It worked! Not bad for such a patch work!

In the next post we will try to add some C code to the mix...