Once upon a time, I wrote a reasonably-strict-C89 C language program that “relocates” a function in memory and then runs it. That relocated function can relocate itself again, and run that copy, and so on and so forth.
I wrote it (as I recall) on a Sun 3/260 workstation in about 1989 or 1990. I know I wrote it on a Motorola 68K Sun, because the group I was in had 2 workstations, a 68K and a nearly new SPARC based system, can’t recall which model.
I had the inspiration at work, tried it, the program just crashed,
but as I was driving home, I realized that I had to call functions in
the relocated function via function pointers.
Next day at work, I could change my code, and wonder of wonders, it worked!
I think my very first version actually copied itself on to the stack,
using the now scorned
alloca() C library function,
but I soon modified it to use
malloc(), and hence execute from the heap.
I’ve been able to get it to run on all kinds of CPU architectures, M68020, SPARC, Mips R2000, R3000, R4000, x86, x86_64. Big-endian, little-endian, RISC and CISC. All of these CPUs run the code unmodified. DEC Alpha and M68040 took a little extra work, putting an embedded assembly instruction in the code to flush the instruction cache. The Alpha CPU as a DEC “UDB”, a fairly early iteration of that CPU architecture. I recall it having memory barrier instructions, and I believe that’s what I used. Alas, I’ve lost the Alpha and M68040 versions long ago.
The idea of “portability” to different architectures has gotten lost in these latter days. “Portability” used to mean a program could work with big- or little-endian CPUs, did not violate alignment restrictions, and could be compiled with variant C compilers. Sun had it’s own C compiler for Solaris, which did not descend from BSD’s “PCC” compiler or AT&T’s C compiler. Really portable programs could even be compiled and run under non-Unix, non-POSIX operating systems.
Now, something’s portable if it compiles for 32- or 64-bit x86 CPUs on Linux and Windows. If you’re lucky, it works on an ARM-based RaspberryPi.
I don’t really know if the changed connotation of “portability” is good or bad. It took some discipline to write a portable program, maybe that discipline is best used elsewhere.
People have learned along the way.
“Portable” C programs were typically filled with conditional compilation constructs,
maybe redefining a particular type,
or renaming functions.
Sometimes a “portable” program was actually 2 different programs,
where there was one
#ifdef construct with one program in the true-branch,
and an entirely different program in the false-branch.
Nowadays, conditional compilation is considered to be bad form.
You can see this reflected in programming language design. C and C++ had a pre-processor that could make code OS- CPU- or compiler-specific. Java, a language from the early/mid 1990s, does not have conditional compilation, and it’s compiled to bytecode, which turned out to get used as a semi-portable intermediate representation of machine code. But most architectures and OSes fell by the wayside, so a machine-independent, intermediate representation isn’t so attractive.
Go from 2010 compiles to machine code, has file-level conditional compilation, rather than redefining lexical tokens and doing expression-level conditional compilation. The entire Go compiler toolchain explicitly does cross-compilation. All the OS- and CPU-level portability concerns get moved into the runtime and the extensive standard library.
What next for portability? Probably it will disappear. We appear to be making ARM CPUs a de facto standard, and Linux the de facto standard operating system.