Hacker News new | past | comments | ask | show | jobs | submit login
C Portability Lessons from Weird Machines (begriffs.com)
270 points by begriffs on Nov 16, 2018 | hide | past | favorite | 117 comments



A lot of the "weird machines" for C are microcontrollers and the like; the 8051 is mentioned, but another big "C-hostile" MCU family is the Microchip PIC, which still has its own C compiler. DSPs are another category where unusual word sizes are common (24 bits for char/short/int is often encountered.)

It’s amazing that, by carefully writing portable ANSI C code and sticking to standard library functions, you can create a program that will compile and work without modification on almost any of these weird systems.

The big question, which I ask whenever someone harps on about portability, is does it even make sense? Is your average PC/smartphone application realistically ever going to be ported to a 6502 or a Unisys mainframe? Keep in mind that I/O in particular is going to differ significantly, and something like standard input might not even exist (think of a fixed-function device like an oven controller with only a few buttons and LEDs for I/O.) I don't think it's particularly amazing, because the "core" of C essentially represents all the functionality of a stored-program digital computer; so if you completely ignore things like I/O it's not hard to see how the same piece of code can express the same concepts on any of the latter type of computer.

It should also be noted that these "weird" environments are often not "strictly conforming" either, because it's either impossible or doesn't really make sense to. Besides the "omissions" they will also have "extensions" that help the programmer to more fully utilise the platform's capabilities.


>The big question, which I ask whenever someone harps on about portability, is does it even make sense?

For entire apps it doesn't always make sense but being able to port code fragments and small libraries is definitely nice. You can write a (non-optimized) portable memset or memcpy very easily (although one should be careful with how they're used if the arch has "weird" CHAR_BIT). Ditto for printf, a simple compression library, a function to strip whitespace and non printable characters from a string, code to handle the FAT filesystem etc... That's stuff that's useful potentially everywhere, from a 32core Xeon to an 8bit controller.

Look at kernels like Linux that are ported to a wide range of devices, there's still a significant amount of shared code.

These days it's also fairly common to have code that runs on both on amd64 on the desktop and ARM32/AARCH64 on smartphones and tablets. It's not as large a gap as going from some DSPs to general purpose CPUs but there are still enough pitfalls that being able to write portable C without too much difficulty is a good thing.


For scientific software, the answer is clearly YES. I wrote a popular bioinformatics package -- FASTA -- originally on a VAX running Unix (in 1983) and a 8086/CPM system, next on an Intel 2086 and Mac/68000 later PPC, moving it on to Sun/Sparc, MIPS, HP (whatever it was), and DEC/Alpha. The original version was not threaded, so porting from one environment to the next was mostly about declaring character types. The threaded version was a bit harder, but pthreads has served me well for more than 20 years. The programs also run fine on a Raspberry Pi. And there are Fortran based scientific packages that are much older.


Not just tablets but many SBC's (RPi, BBB) are also ARM.


Agreed. I think there's a lot of overhead stemming from the use of C and C++, and all the idiosyncrasies that stem from their specs needing to be vague enough to cover all that exotic things, for software that's realistically never actually going to run on them.

It's not that the ability to write code that is so portable isn't useful. For something like a library of algorithms (e.g. compression - think zlib), there's actual value to be derived from having a single ultra-portable implementation that can run everywhere. But does something like Evolution or LibreOffice really need to never assume that CHAR_BIT is not necessary 8, or that int might be less than 32, or that int64_t might not even be defined? I would say that of all the C and C++ code that's running on modern devices today, the vast majority could safely assume flat memory addressing, 8-bit chars, 32-bit ints, two's complement, IEEE floating point etc - and nobody would even notice. In fact, a lot of it likely already does assume some or all of that, just not explicitly.

It would be nice to have an ISO-standard superset of C that catalogs such assumptions. Basically, a "non-DSP, non-mainframe" version of C, that's portable across all modern non-exotic platforms, and provides definitions for as many things that are UB or implementation-defined in standard C as possible.


> the vast majority could safely assume flat memory addressing, 8-bit chars, 32-bit ints, two's complement, IEEE floating point

IME I've rarely, if ever, needed to make any of those assumptions, though early in my career I unfortunately did. Unless you're writing a kernel or a compiler there's rarely a good reason to assume a flat memory model. You would assume a flat memory model if you're manipulating objects in a way that grossly subverts the typing system; I say grossly because assuming a flat memory model usually isn't necessary even for most illegal type punning hacks.

Assuming 8-bit chars is useful, yes, but really only so you don't need to insert masks everywhere when doing bitwise operations on chars, such as when marshaling integer types (preferably without type punning). AFAIU an 8-bit ASCII string won't be packed 2 characters to a char on implementations using a 16-bit char, so pointer arithmetic and string manipulation will always look the same assuming the same string encoding. As with integers more generally, most of the time all that matters is that an integer type has at least N bits, whether or not you'll use them.

If you're depending on fixed-width types then usually there's a leak in your abstraction somewhere or you're doing bitwise manipulations where it's probably wise to use explicit masking, if only to make the code more clear, if not provide the ability to parameterize the value range. Typical exceptions would be cryptographic code, hashes, etc, particularly code that needs to perform rotations. But thankfully C has provided fixed-width types for nearly 20 years now, and almost all (if not all) compilers, even niche compilers, support these.

Assuming two's complement is only useful when you're depending on overflow characteristics. But signed overflow is undefined, anyhow (and for still-good reasons), and unsigned integer types are already guaranteed to be using two's complement representation (which requires emulation on some architectures, similar to how compilers transparently synthesize 64-bit long long integers on 32-bit platforms).

I've never done heavy floating point arithmetic so can't speak to the usefulness of assuming IEEE floating point for, e.g., managing error accumulation. But IME general purpose software likes to assume IEEE floating point for its indirect benefit, like how JavaScript provides the ability to perform accurate 32-bit scalar arithmetic by dint of providing a single 64-bit IEEE floating point integer type, or how VMs (including JavaScript JIT VMs) abuse floating point to implement tagged types; but arguably the reliance on 64-bit IEEE floating point has been on balance detrimental by, e.g., making the adoption of 64-bit scalar arithmetic in languages like JavaScript more difficult.

Also, it's noteworthy that WebAssembly makes use of some of the flexibility (or, rather, stricture) of the C standard. Appreciating the subtleties of C semantics helps make your code portable not only to archaic environments, but to bleeding-edge and future environments.


ISO C does not guarantee that int32_t etc are present. It says that they must be present if the platform can offer them, but if you rely on them being present, your code is no longer portable from a standard C perspective. If you want to be strictly portable, you need to use int32_least_t, int32_fast_t etc.

And yes, you can do that. The question is, why have all this complexity when in practice int32_t is there pretty much all the time - as you've said yourself - and when lots of existing code relies on it?

Same thing on other counts. Yes, you can work without all these assumptions. But why waste time on making the effort, when it simply doesn't matter?


Assuming twos complement goes beyond overflow characteristics. I did not use C on the Univac 1100, so it may have finessed this somehow, but I did get bitten a few times in other languages by the fact that positive and negative zero do not compare equal on a ones complement machine.


> two's complement

For C2x and C++2x it looks likely that they will drop support for sign-magnitude and ones' complement representations of signed integers, essentially mandating two's complement.

https://gustedt.wordpress.com/2018/11/12/c2x/

https://herbsutter.com/2018/11/13/trip-report-fall-iso-c-sta...

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p090...


>But does something like Evolution or LibreOffice really need to never assume that CHAR_BIT is not necessary 8, or that int might be less than 32, or that int64_t might not even be defined?

No and why should they? I assume all these things routinely when I write code that I don't expect to run on "weird" systems. And I don't have to worry about int being less than 32 since I when I need this guarantee use "int32_t" like a civilized human being.

The overhead is only here if you care to really write entirely portable C code but in practice very few people bother to do that outside of comp.lang.c.

>It would be nice to have an ISO-standard superset of C that catalogs such assumptions. Basically, a "non-DSP, non-mainframe" version of C, that's portable across all modern non-exotic platforms, and provides definitions for as many things that are UB or implementation-defined in standard C as possible.

POSIX C then? It assumes that chars are 8bits, that data and function pointers are castable and a bunch of other assumptions that are not possible in strict C. It also offers a richer API than C's stdlib. When I write C for a "non-DSP, non-mainframe" environment (that is, 99.9% of the time) I almost always target POSIX C, not strict ANSI.


Sorry, that was meant to be "ever assume", not "never assume".

And yes, POSIX C is a very good example (and I completely forgot about the function/data pointer thing... that one is probably one of the most common assumptions people rely in, and I bet most of them don't even know that it's not standard ISO C!). It's a great starting point, but POSIX being as old as it is, it doesn't include some things - e.g. it doesn't guarantee any particular size for int above and beyond ISO. At the same time, the richer POSIX API is not actually available on some platforms - notably, Win32 - so taken as a whole, it's not really a pragmatic portable superset, if you want to target all mainstream platforms.

What I was thinking about is more along the lines of, look at existing platforms that "matter", find the set of common assumptions that ISO C doesn't guarantee, but which all the platforms nevertheless provide in practice, and give an official blessing to that set.


In practice - like you say - for non-DSP, non-mainframe this doesn't matter. For applications development, differences between the standard libraries and even compilers are much more difficult than the few differences in architectures. Endiness, strict alignment, char signedness and overflow are about the only things that matter anymore even if you want to support nearly every modern architecture used for popular OSs, and they are relatively easy to handle. Writing C that compiles and works on eg both Unix and Windows is far more about libraries and compiler quirks.


>Basically, a "non-DSP, non-mainframe" version of C

I feel like if you start going down that road, towards “commonly portable”, you naturally end up with one of the not-c variants (zig, nim, etc). ...and ifc, if you start writing a new language, you might as well toss in features from the last thirty years of language design.

C is C largely because its so stupidly (and impressively) portable; if you aren’t drawing that benefit, its likely a case of the wrong tool for the job.


>C is C largely because its so stupidly (and impressively) portable; if you aren’t drawing that benefit, its likely a case of the wrong tool for the job.

I'm working in medium sized embedded systems (not embedded Linux, but also not the smallest embedded systems). I've only rarerly worked on 8 or even 16bit Microcontrollers. Most of what I work on is 8bit chars, 32bit, Little Endian normal Microcontrollers.

I really don't need the portability, the targets are all the same, however they only have C/C++ compilers. And we can't use GC. Rust would be a very nice choice at least for the application code. I'm not sure how to write low level drivers in Rust, but with FFI I would just push that to the boundaries, and make at least 90% of the applications Rust. But there's no LLVM backend for most embedded processors I work with, and even if there is, the maturity might not be there, and no company would switch to that considering that the next target might not have a backend.

We have to use C, because it's the only thing that we are offered. But for desktop applications I see no real reason to start something in C nowadays.


Some microcontrollers do enjoy Basic and Pascal compilers as alternatives to C.

https://www.mikroe.com/compilers

But as you say, the company needs to give the option to developers.


The point is that we already have millions of lines of code written in this less-portable C dialect, and probably thousands more are written every year. So no, it's not the same as a new language (for which all that code would need to be rewritten).


I think in a lot of cases it’s the only tool for the job. I have a hunch that a significant majority of the machine instructions executed in a given second globally came from C source code, and a large portion of that is running on something “weird”. It’s somewhat funny, “commonly portable” C is portable across the most uncommon C execution environments.


psst, your personal website is returning a 502


Thanks for the heads up!


> It would be nice to have an ISO-standard superset of C that catalogs such assumptions

But you can already make those assumptions, and safely ensure that they are satisfied by using compile-time asserts (i.e. static_assert), which are very powerful in C++.

Need to know that int is 32 bit? Not a problem. Need to know that long is strictly larger than int? Not a problem. And if you do break someone's build with these static_assert's, that's fine -- it wasn't intended to be a supported platform anyway.


For one thing, static_assert is C++, not C. There's still plenty of C around, and plenty more is being written.

The other problem with this approach is that it effectively defines lots of distinct subtly different pseudo-platforms, each a set of assumptions. And your average developer might not know which of those assumptions actually are "portable enough", and which are not. The point of defining such a profile would be for experts to provide a blessed set of assumptions that is, in fact, portable across all general-purpose platforms that are in active use.

For example, the fact that CHAR_BIT is 8, or that int32_t is defined, or that ints are two's complement, are all reasonable assumptions. But two's complement overflow for signed arithmetic is not, even today - even though in practice it works just fine on some platforms (more specifically, on some implementations).


IIRC static_assert is in C as well now (as _Static_assert).


Oops! Completely missed that we were talking about C.

I'll continue under the assumption that C is capable of equally powerful compile-time asserts.

> it effectively defines lots of distinct subtly different pseudo-platforms, each a set of assumptions

Indeed, but using this approach, all that's needed to define a new 'ordinary platform C' is a clearly defined set of requirements. No need for a whole new standard in the usual sense.

> experts to provide a blessed set of assumptions

Sure, and that's compatible with this approach. It could take the form of a header file.


I'm perfectly fine with the idea of code-as-specification like that. And you're right, a lot of these assumptions could probably be so encoded (some I'm not sure about - e.g. how would you static_assert for two's complement, without triggering UB?). But I think this sort of thing would need some official blessing to be widely adopted, regardless of whether it's a written spec or a header.


> how would you static_assert for two's complement, without triggering UB?

Good question, I'm not certain that's possible. A quick google turned up surprisingly little.

The ghastly hack alternative would be to detect which compiler/target we're dealing with and compare against a whitelist.

> I think this sort of thing would need some official blessing to be widely adopted, regardless of whether it's a written spec or a header.

It would certainly help to have it properly branded and given real credibility, yes. I'm reminded of MISRA C, which has a good deal of tooling and documentation.


>Need to know that int is 32 bit? Not a problem.

I have 15 years of programming on 8but micros where that assumption/declaration is crazy. :)

Never use int. Always uint32_t. There! Problem of portability solved.


> I have 15 years of programming on 8but micros where that assumption/declaration is crazy. :)

Right, but that's the point here - we're discussing how to describe a stricter variant of C, which make exactly the kinds of guarantees that preclude exotic architecture targets. Ultra-portability isn't a goal for many codebases.


Then... make uint32_t the standard and this is never a problem ever again?


uint32_t is in the standard since C99. But it is conditionally present - it's only there if the platform can provide it. In practice, all platforms that most software cares about do so. But it's not actually codified everywhere, and that codification is what I was talking about.


uint32_t is already available on the platforms where it makes sense to support the type. It is a strength of the C language that it also supports exotic platforms.

By 'standard' do you just mean more widespread use? Some APIs already do something like this, such as Windows with its 'DWORD'.


That reminds me of when I got a bug report that my C library crashed on SPARC. It was a library related to audio. I said, that's ok let's try to fix it, but it seems this requires changing our memory layout which is a big thing to ask since the memory layout is based on the data format we are processing, and I can't reproduce the error because it doesn't crash on any machine I have access to. Are you really using this library on SPARC? And it turns out no, he was just adamant about portability and testing lots of libraries on various architectures. I said, do you have a use case for doing audio on SPARC, is _anyone_ doing audio on SPARC? And he wouldn't let it go even though he couldn't cite a single reason I should care about this platform so I had to just ignore it. He eventually suggested a fix but it was a huge change to the code that wasn't backwards compatible, so I had to reject. I'm sad my library won't work on SPARC.. but.. Portability is a virtuous goal but one has to be pragmatic.


However, if your program crashed on SPARC then it probably contains undefined behaviour. And if that's the case, then at any time a new GCC release might make it fail.


Indeed. And I'll fix it as soon as that happens ;) But I can't really fix something I can't reproduce. At least, I draw the line there if it requires massive changes. For example, how am I to know that my fixes won't introduce further undetectable problems? That last thing you want as a maintainer is to rely on one single user who reported a problem to help you out with testing and maintaining the fix forever..


I still have to deal with SPARC at work, and the only major differences between it and x86, with respect to C, are:

* It's big-endian

* unaligned reads will fail (if you have to read packed structures, you need to declare the structure as packed and the compiler will generate code to do unaligned reads)

Given you mention "memory layout" it seems you are expecting possibly a little-endian system, and maybe unaligned reads.


Yes, it was to do with unaligned reads. Blows my mind that that crashes the program on some architecture, but until I'm personally using such an architecture or have an easy test rig for it, I have a hard time caring. (I did try on qemu.. doesn't crash.)


What you write about I/O is interesting to me, because I am generally of the belief that any decent library in C should have a way to replace I/O with an alternate mechanism via a couple of function pointers.

Even if you stick to recent platforms, depending totally on FILE* can be a mistake. For example Windows has all its idiosyncrasies with sharing modes and whatnot, not all of them captured by fopen. Or you may wish to implement streaming or some alternate data source not on the filesystem. The author of a library can't predict these things, so should allow the caller to bring their own.


One issue is that FILE can't be extended in standard C, although there non portable extensions (like fopencookie).


DSPs are pretty good discriminators for supposed-portable code. On SHARC for example everything is 32-bits wide (except long double, which is 64). Chars are 32 bits. Bytes are 32 bits. That is, int is 32 bits and sizeof(int) == 1. I imagine that porting Java, where byte is defined as an octet, to this architecture must be a chore.


Yeah, people won't port java to DSPs. There's no point in doing that


Doens't Java runs pretty much everywhere, including smartcards?



The very limited flavor of Java running on smartcards is pretty far from generic Java.


More practical portability questions are whether the code assumes heap and stack are boundless and flagrantly allocates large objects.


> The big question, which I ask whenever someone harps on about portability, is does it even make sense?

Just one example, because I'm writing one right now. Protocol encoding/decoding. I just write it once, can share headers with struct definitions, and can compile it for both ends, be it x86_64 on one end and PIC on the other.


One of the things that really struck me about this article was that many of these weird architectures make a lot more sense if you're programming them in assembly language (or lisp in the case of the lisp machine). It's kind of amazing how much architectural variation it's possible to abstract away with C, but maybe it's not always the best idea to do so. If I'm writing assembler, a 36-bit machine word isn't a hard concept to grasp. But if I'm writing C, the idea that my int might have 36 bits sometimes, on some platforms, and I have to code around that possibility, is a major drag. I think this article really exposes the limits of the notion of C as a portable assembler.


> The big question, which I ask whenever someone harps on about portability, is does it even make sense?

A related question when talking about portability to odd or historic machines, exactly how is this supposed to be my problem and not the problem of the guy that decided to go that route?


A C hostile chip is a major opportunity for a compiler engineer :-)


The 6502 is just such a chip.


> The big question, which I ask whenever someone harps on about portability, is does it even make sense?

Yes. I probably won't port my program to a PDP-11 or a Motorola 68000. But by relying on undefined behavior or implementation-defined behavior, I am making it hard to port my program, say, from Windows/x86-64 to Debian/ARM (e.g. Raspian).


Compiling C++ to asm.js with Emscripten is another weird architecture you might actually use these days.

Unaligned accesses don't trap with a SIGBUS or anything, they just round the pointer value down to an aligned address and read whatever that is.

Reading from and writing to NULL will generally succeed (just as on SGI).

Function pointers are just indices into tables of functions, one table per "function type" (number of arguments, whether the arguments are int or float, whether it returns an argument or not). Thus, two different function pointers may have the same bit pattern.


Re functions pointers, how does Emscripten gurantee the POSIX behavior that you can roundtrip function pointers to void*?


So long as you cast it back to the correct type, there isn't any issue. Casting back to a different type is undefined behaviour anyway.


But this means that two distinct function pointers might compare equal when converted to void*, unless the type is somehow encoded.


This is the first I'm hearing that arbitrary function pointers converted to void* should be distinct on POSIX - do you have a reference?


Pointers to different objects should not compare equal in C (or at least in C++, can't remember if this is also a rule in C). Then again, functions are not objects.


Does Emscripten aim to be POSIX compliant?


I hope so if it want to have any chance to support most software out there.


> Thus, two different function pointers may have the same bit pattern.

Is this likely/guaranteed to happen by the wasm standard itself, or, could runtimes/emcc fill in stub table entries to avoid this?


One thing that they didn't list there that probably deserves a mention is SHARC with its 32-bit word architecture, which they decided to manifest directly in C type sizes:

   CHAR_BIT == 32
   sizeof(char) == 1
   sizeof(int) == 1
   sizeof(short) == 1
   sizeof(float) == 1
   sizeof(double) == 2
I suppose the alternative would be to use an addressing scheme encoding bit offset in the pointer, like some of the other machines in this story. But that's also much more expensive, and this is a DSP architecture, so they went with something more straightforward. Curiously, this set-up is still fully accommodated by ISO C standard.


16- or 32-bit chars are pretty common on DSPs from any vendor. I’ve found that it’s usually only an issue with some cross-platform serialization libraries that make invalid assumptions about how an array of octets looks in memory.


> Curiously, this set-up is still fully accommodated by ISO C standard.

This allegedly means that the standard doesn't require fgetc() (which returns a char converted to an int on success, or EOF on failure) to have sensible behavior on such platforms [1].

[1] https://stackoverflow.com/a/3861506


sizeof(double) is 1 if you're using Analog's compiler. You have to use --double-size-64 to get the bigger one.


Seeing this article reminds me that there's a good litmus test in C (at least older versions) in determining whether a feature is undefined behavior or merely unspecified or implementation-defined behavior. Undefined behavior means that there is some processor that will throw an exception if you do it; if there isn't such, then the behavior is implementation-defined. So signed overflow is undefined because some processors have trap-on-signed-overflow, and unsigned overflow is not because that feature is not present. The part about undefined signed overflow being useful for optimizations only came decades later.

That said, I'm at a loss to explain why i = i++; is undefined and not merely unspecified.


> and unsigned overflow is not because that feature is not present.

you can compile in a way that unsigned overflow traps with gcc / clang's -fsanitize=undefined.

It has solved countless bugs of mine


Because, subexpression evaluation order is indeterminate in C. We really need -fprecedence-left-to-right and -fprecedence-right-to-left to specify evaluation strategies.


It is an exception to the apparent pattern in which behaviors that trap on one of the early C targets were later declared undefined.

Given the standard, it's obvious why i=i++ is undefined: it modifies i twice between sequence points. The question is why it was "undefined" (in which the implementation is allowed to make demons fly out your nose) and not something like, say, the "unpredictable" that occurs in many architecture specs (in which the program behaves as if i had been set to something in particular, but with no requirement on which value it is: unchanged is ok, incremented is ok, 12345 is ok if i is wide enough to hold it, but silently failing to generate any code for statements that occur after the i=i++ isn't).


Subexpression evaluation order has nothing to do with traps and everything to do with optimisation. It is not like your CPU requires mandatory exclusive lock to access a particular memory location.


I think we're talking past each other. I'm responding to the comment about the heuristic where for the most part trap (somewhere) means UB.

Maybe it's UB only because implementation-specified is too onerous and it didn't seem to be worth defining a bounded "unpredictable result" behavior.


I remember that back in the early 90s, the DEC Alpha was pretty "weird", as it was one of the first common LP64 unix machines. I fixed so many issues due to sizeof(int) != sizeof(char *) when building open-source packages for DEC OSF/1 in the early 90s..

Later, the alpha was FreeBSD's first 64-bit platform, and when working on the port to alpha, we hit a lot of the same issues in the FreeBSD kernel. As alpha was also FreeBSD's first RISC machine with strict alignment constraints, we also hit a lot of unaligned access issues in the kernel.

Ah, those were the days. I now find myself grumbling about having to check to ensure my code is portable to 32-bit platforms.


Alignment is one of those interesting things, because x86 doesn't care so much about it... And everyone starts out assuming everything is x86.

Meanwhile, ARM does care about alignment, and its now the most popular architecture for anything that's "not a PC".

My first experience with this was writing some smartphone code that died with a SIGBUS when trying to make a function call, where the reason was totally non-obvious from simply looking at the code.


If you are accessing RAM, modern ARM chips don't care about alignment.

A couple decades ago, sure, ARM was different. Had it stayed that way, ARM would not be so popular today.


You think a chip will be a financial success based on whether it supports unaligned reads?

Almost zero software out there actually needs unaligned reads.


Yes.

Most of the troubles related to -fstrict-aliasing involve unaligned reads. All sorts of file formats, TIFF for example, are most easily handled with unaligned reads.


Most of the troubles related to -fstrict-aliasing involve -fstrict-aliasing.


If the processor supports unaligned reads, sure, you could say that, though it still technically violates the C standard. Otherwise, no.

A typical issue would have code like this:

foo = * (bar * )baz; // baz is a char pointer into a binary blob, and bar is a type that needs alignment

If the data were all properly aligned, then most likely there would be no desire to write such code. The correct types would be used.

Aside from gcc abusively optimizing, the above works fine on x86, PowerPC, and modern ARM. It does not work on older ARM.

That sort of code is everywhere.


One machine I came across early in my career was the BTI-8000, designed in the mid to late 70s. Only a few dozen were sold. It was a multiprocessor mainframe, and the user memory space was 512KB. So what does an architecture do with all those extra bits after using up the first 19 as a linear space mapping all 512KB? Why encode other address modes. On that machine, it was possible to have a pointer to a particular bitfield in memory, something like [16:0] = 32b word of memory, [21:17] = bit offset, [26:22] = bit width, [31:27] addressing mode. Other modes provided the ability to point to a register, or a bitfield within a register. There were many other encodings, such as register plus immediate offset, base+offset, etc.

If the instruction was something like "ADD @R1, @R2, 5", it would fetch the word, register, or bitfield pointed at by R2, add immediate 5, then save it to the word, register, or bitfield pointed to by R1.

The machine didn't have shift/rotate instructions, but it could be effected by saving to a bitfield then reading from a bitfield offset by n bits.

They had a working (but not polished) C compiler but that project got shut down when they realized the system was not going to take off.

http://btihistory.org/bti8000.html#isa


Article is wrong in a few ways. One is about the R3000 where it says that integer overflow traps. there are actually two separate addition instructions, they operate the same way, except for one difference. One will trap on signed overflow and one will not. They both produce the same result. No c compiler I know of uses the trapping version of the instructions.


Does -ftrapv generate add instructions?


"Accessing any address [on 6502] higher than the zero page (0x0 to 0xFF) causes a performance penalty."

I never thought of it that way, but that's true. However, he didn't mention the biggest issue with C on the 6502, i.e., the extremely constrained 256-byte hardware stack. To do anything practical requires some sort of software-maintained stack to have stack frames of any decent size or quantity (in parallel, or replacing the use of the hardware stack completely). "Downright hostile to C compilers," indeed.


It'd be nice if compilers were smarter about using the stack (looking at you, cc65) -- you could have "big" stack frames and little stack frames, pass variables in registers, push values as 8-bit instead of upcast to 16-bit, convert locals to static, etc.

I'm going to take a look at yet another alternative 6502 language called C02 now: https://github.com/RevCurtisP/C02


The big problem with passing in registers is you only have three, and only one of them can do most of the work. Maybe zero page could help there for a fast call convention, but either way you're probably going to have to spill to memory, which fortunately is not a big deal on the 65xx.


"[3B2] Fairly normal architecture, except it is big endianian, unlike most computers nowadays. The char datatype is unsigned by default."

That sounds like any SGI, or Sun, and although they're mostly gone there's still the Power series from IBM (runs AIX), and the only reason to use the expression ".. unlike most computers nowadays" is by counting the sheer number of Intel, AMD and ARM chips in use. Of course those numbers are overwhelming - ARM alone sells billions - but it's not like big endian is some obscure old concept in a dusty corner. (The irony is that ARM can be used in both BE and LE modes, by setup). Anyway, at work I have to write all the code so that it runs on BE as well as LE architectures. BE is alive and well.


> That sounds like any SGI, or Sun, and although they're mostly gone there's still the Power series from IBM (runs AIX)

According to wikipedia, there's also IBM's z/Architecture and the AVR32 µc. PPC looks to be switchable like ARM.

> The irony is that ARM can be used in both BE and LE modes, by setup

I've always wondered how common it is for ARM CPUs to run in BE mode. Does anyone have info?

> BE is alive and well.

If only because network protocols are generally BE.


Big-endian arm is pretty rare -- it's basically a niche requirement for markets like embedded network hardware where there is a lot of pre-existing code that assumes big-endian because it used to be run on m68k or mips. Most network protocols are big-endian on the wire, so on a BE host if you forget a htons() or ntohs() call it all still works; auditing a legacy codebase for that kind of bug in order to port it to a LE host is painful. But any general-purpose-ish Arm system (intended for running Linux or Android or Windows) will be LE.

Fun fact: the optimized string library for the original ARM1176 raspberry pi had a bonkers implementation of memcmp() which used the SETEND insn to temporarily switch to bigendian, because on that core setend is only 1 cycle and it saved an insn or two later. (On newer cores setend is a lot more expensive and the trick doesn't work.)


Yes, PPC is also switchable, as is MIPS (my R4000 SGI boxes are big endian, while my R4000 Jazz box (Olivetti M700-10) is little endian).

https://en.wikipedia.org/wiki/Endianness#Bi-endianness lists ARM >= v3, PowerPC, Alpha, SPARC V9, MIPS, PA-RISC, SuperH SH-4 and IA-64 as bi-endian (can be configured as wither). MIPS is still used a lot in routers. I don't know if the MIPS-compatible PIC32 is also bi-endian - there's maybe no reason it should be.

About ARM, I don't think I have run into any BE-mode ARM systems in a while, but I do recall using big-endian gcc versions for something in the past. And apparently some folks are running Cubieboards in big endian mode (if that's common or not for Cubieboards/Cubietrucks I don't know).

Yeah, network protocols will keep BE alive if nothing else - and yet, the Power 7 systems I'm coding for at the moment are big endian and IBM won't change that in the reasonably long term.


>> BE is alive and well.

>If only because network protocols are generally BE.

I think a big part of this is that, until x86 conquered all, big-endian was far more common than little-endian. So when all those "foundational" protocols were originally designed, it was little-endian that would have been the bizarre choice.


Yeah, BE is mostly about POWER these days (even though Linux distros are adding ppc64le now).

But both POWER and ARM have unsigned chars by default :)


Sorry to be that guy, but a weird machine is a defined computer science term. I got the wrong impression from the title, as I'm guessing, did others. https://en.wikipedia.org/wiki/Weird_machine


The title makes no sense if you interpret it that way, also, it seems to be a very niche term (most of the top google results are not computer related at all). I think 99% of people who saw the headline understood it the correct way.


> The title makes no sense if you interpret it that way

The lesson of weird machines is that weird machines can lurk anywhere; that's why they're 'weird'. Something to do with CPP or bit endianness, perhaps, or exploiting undefined behavior is what I thought clicking on it.


Ditto for the endianness. Or one of those embedded boards with really weird 24-bit stuff or something similar.


When you get into DSP 24bit isn't too weird. But then you also expect that going in.


I also interpreted it the 'wrong' way.


Weird? 68000 is hardly weird -- the original Sun machines were 68Ks. The MIPS CPUs were designed to run C from the get go.

And the reference in the manual to the Unisys machine not having byte pointers: the PDP-6 (the original 36-bit machine as far as I know) had byte pointers that allowed you to specify bytes in the width 1 to 63 bits wide. It was common to have six-bit characters (you could pack six to a word) as well as 7-bit ASCII characters (you could pack 5 to a word).


"Weird" with respect to the current homogeneity, at least outside of microcontrollers and DSPs.


If unaligned access traps are weird, then so is ARM: https://medium.com/@iLevex/the-curious-case-of-unaligned-acc...


Also x86 can trap on unaligned access, if "Alignment Check" (eflags bit 18) is 1 and CR0 "Alignment Mask" (coincidentally also bit 18) is 1.

Then misaligned access will trap on x86. Can be useful for emulating architectures that don't support misaligned access.


I learned C on a 68000 system (Amiga.) Later I worked on Sparc, MIPS, and POWER boxes. The 90's were fun. Sadly, in today's world, if it's not Intel or ARM, it may as well be "weird."


The PDP-6 and PDP-10 were word addressed, you could then extract any bitfield from a word with a single instruction, there wasn't a true "pointer" to that bitfield.


The PDP-6 and -10 hardware absolutely did have true bitfield pointers as a major feature of the instruction set. A single instruction ("ILDB") would do the equivalent of Rn=* cp++ (where Rn is a register, and cp is a char* ). Even better, the pointer itself specified how many bits were being addressed, and the ++ part of the operation would move forward that many bits. See "Byte instructions" at http://hakmem.org/pdp-10.html


I know how the byte instructions worked, they were copied to the Lisp Machine. You still don't get a single pointer to part of a machine word.


I don’t know why you would say that if you didn’t program a -6 or a -10. Byte pointers could e e incremental to move though memory just like word pointer, using the IBP instruction (increment byte pointer). If the byte spanned a word it would increment the word address.

Http://pdp10.nocrew.org/docs/instruction-set/Byte.html


Furthermore, MIPS was specifically designed to be friendly as a compiler target (e.g. 3-operand form by default)


I do bare metal C on modern ARM chips and nowadays it is near identical to anything you'd do on a desktop. We don't have to write portable code to compile most of the same libraries on real computers for proper unit testing.

It is hilarious to think about the possibility of having to make your code portable to a 9 bit big endian system too.


Agreed. I’m on ARM every day and it’s not nearly as foreign as people think.

Here is the issue though, portability is a joke even between chips of the same series let alone chips of different mfgs and code for PC.

In a PC I’m not stuffing every possible bit in a structure because I’m going to push this “object” over a 10kbps bus at some point or because on system still measures it’s ram with a K not a G.

And typically it’s moot. Even if I have an RTOS, the majority of what a micro is doing is loading data in and out of peripherals. When it’s CPU heavy it’s typically (for me anyhow) just crunching some data in order to load a result into a peripheral. Well - those peripherals are never the same between mfgs. So portability it’s really as important to me as understanding how the mfg intended the peripheral to be used and doing so efficiency.

I’ve been doing this for some time and while it’s a nice goal to write once use many, reality gets in the way.


I was thinking what a headache it must be to read a bitstream from one of those machines onto an 8 bit byte machine.


I have a 32bit system that has a LIN connection to an 8bit system (or few or them). On the 8bit side I can still handle a 32bit address or hash for example. It’sslower but works fine in effect to what the user would experience.

Before the great serialization options we have now (MessageBuffers, ProtoBuf, etc) there was a bit more ambiguity with what your data stream was. TLV (type-length-value) packing being pretty common... but I guess I’ve written plenty of domain specific ‘protocols’.

If you mean a constant bitstream of never ending data, if you can use an established format like I2C or SPI the hardware on both sides really takes care of most the gritty stuff giving you a nice interrupt on both sides. Doesn’t really matter one side has the “preference” to look at in in 32bit chunks and the other in 8bit. Besides, even on ARM the physical transfer is typically in 8bit units anyhow. SPI can send 16s or 32s, but can also stop at 8s usually. It’s the ASIC/drivers/devices that are more rigid in their streaming requirements (this device MUST accept 8bit transfers, etc).


I meant consuming a 9bit/byte bytestream on an 8bit/byte box.


Is it weird to support some of these today? we just dropped acorn26, although it was gone for a few years.

I think mips r3k has a better behaving add ("addu"), or is that only on some? if your compiler outputs these you don't have to worry about special behavior.

I'd say a bigger concern, for vax - the lack of IEEE754 is noticeable when people pick unsuitable float constants, or it traps by default. or the many GCC bugs now.

For mips r3k, the complete lack of atomics. And the load delays.


r3k didn't really need atomics. It wasn't SMP, and being a braindead simple 5-stage RISC, a syscall to atomic primitives was in the neighborhood of as cheap as atomic instructions are today. Cheaper if you were already in a KSEG.

And load delays are annoying when writing asm manually, but not for the C compiler.

And yeah, addu was in MIPS-I.


> lack of IEEE754

Course for 98% of use cases IEEE754 is broken.


I've owned or used about half the machines on that list. Good times. We had to push our bits uphill both ways back then.


The weird machine my Computer Architecture class had us learn was the PDP-8, which is almost comically constrained. 12-bit, with exactly one register, no hardware stack, no arithmetic beyond addition and negation.


I bet you used The Art of Digital Design:

http://bitsavers.informatik.uni-stuttgart.de/pdf/dec/pdp8/pd...

I started with this design for my relay computer, but evolved it to be even simpler: no accumulator.

http://relaysbc.sourceforge.net/arch.html



TOPS-20 on the DECSYSTEM-20 had 7-bit characters, but this was a 36-bit machine... so sizeof(int) needs to return a fraction or something...

(There was a C compiler on it, but it cheated and used 9-bit chars).


Ah ha! Great!! It reminds me my Grany stories in childhood. Huge memory disk, Floppies and room-sized mainframes.

Worth to read it.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: