Overview

Although GCC claims to support PDP-11 targets there are some bugs that must be worked around both in GCC and the GNU assembler.

GNU binutils Bugs

Problem Description

One of the addressing modes supported by the PDP-11 is ‘index deferred’, represented by @X(Rn). This operand indicates that Rn contains a pointer which should be dereferenced and the result added to X to generate a new pointer to the final location. For example, consider the following four values, one stored in a register and the other three in memory. Then @2(R1) is the value 222.

R1: 1000
1000: 2000
2000: 111
2002: 222

Similarly, @0(R1) is the value 111. In most PDP-11 assemblers, including DEC’s MACRO-11 assembler, the string @(Rn) is an alias to @0(Rn). But when the GNU assembler encounters @(Rn) it assembles it as though it were (Rn), a single level of indirection instead of two levels!

If we’re only writing assembly then we can work around this bug by always using the form @0(Rn). But what if we’re writing C and using GCC to compile it? Consider the following C code example, taken directly from some stack-based debugger code written for the PDP-11.

uint16_t ** csp = (uint16_t **) 070000;
*csp = (uint16_t *) 060000;
**csp = 0;

When GCC transpiles this to assembly it generates code of the form @(Rn) when assigning a value to **csp thus causing the value 0 to overwrite the value 060000 at *csp if GNU as is used to assemble the code.

Solution Description

The following patch, tested on GNU binutils 2.28, fixes the bug. Since it overloads the operand->code variable to pass unrelated state information to parse_reg() I haven’t submitted it for inclusion in GNU binutils. Once I’m done bug-hunting in my toolchain I will clean up all the fixes and submit them in one bundle.

--- tc-pdp11.c  2017-06-24 22:33:00.260210000 -0700
+++ tc-pdp11.c.fixed    2017-06-24 22:32:12.455205000 -0700
@@ -431,6 +431,9 @@
 {
   LITTLENUM_TYPE literal_float[2];

+  /* Store the value (if any) passed by parse_op_noreg() before parse_reg() overwrites it. */
+  int deferred = operand->code;
+
   str = skip_whitespace (str);

   switch (*str)
@@ -451,6 +454,15 @@
      operand->code |= 020;
      str++;
    }
+      /*
+       * This catches the case where @(Rn) is interpreted as (Rn) rather than @0(Rn)
+       */
+      else if (deferred)
+        {
+          operand->additional = 1;
+          operand->word = 0;
+          operand->code |= 060;
+        }
       else
    {
      operand->code |= 010;
@@ -581,6 +593,12 @@

   if (*str == '@' || *str == '*')
     {
+      /*
+       * operand->code is overwritten by parse_reg() inside parse_op_no_deferred()
+       * We use it to temporarily catch the alias @(Rn) -> @0(Rn) since
+       *   parse_op_no_deferred() starts at str+1 and thus misses the '@'.
+       */
+      operand->code |= 010;
       str = parse_op_no_deferred (str + 1, operand);
       if (operand->error)
return str;