19 ms·
C was not created as an abstract machine
- saagarjha 4y agoOk, so this article is at least contributing to the good side of the discourse about undefined behavior, because it isn't actually delusional about reality. (I mean this in the sense of people being deluded/having the wrong notions about actual facts, by the way, rather than actual craziness. The people who are deluded about undefined behavior tend to say things like "C is definitely a portable assembler" or "compilers are trying to trick me" which are definitely false. A non-deluded person can be upset about the current state but understanding how C currently works is important if you want to discuss it.) Anyways, to the actual content of the article, I agree with it but I think the frustration it accepts as reasonable is actually misguided. Here is my understanding of how things ended up for C (disclaimer: I was not born when most of this stuff happened.) In the beginning, you had a C compiler for your computer, and it was basically just an assembler. This is what the "make C great again" people think the language really is under the hood, by the way. However, very quickly people realized that they want their C code to run elsewhere, and every computer does things differently, so they needed to have some sort of standard of what was approximately the lowest common denominator for most machines and that become the C standard for what it is legal to do. The guarantee created at that point was that if you conform to the standard, every implementation of C has to run your program as the standard specifies. This was palatable to people because they had a bunch of machines with weird byte orderings or whatever, and it was obvious what would happen if the dumb compilers of the time translated their platform-specific code to a new architecture. Later, the weird architectures started becoming rare. At the same time, though, a new architecture started growing: a virtual architecture, one where the compiler would actually "port" your code to the exactly same processor you were compiling for before, but the code would run faster. It would do this by starting to take latitude through intermediate transformations which it assumed it could do because your program should have been portable to the abstract machine. Now, this completely weirded people out, because "I'm compiling to a new virtual architecture called 'x86-64 -O3' that is the same as 'x86-64 -O0' but faster and more restrictive" sounds really stupid. It's the same architecture, and they're not even real processors! But if you really think about it compilers are really just taking advantage of the fact that your code is portable, because it works in the space of the C abstract machine, to do a "port" called "run a bunch of optimization passes". People understand when a port ends up causing a trap on another processor because of course it does that on the new platform. But getting people to understand that your unaligned accesses on the "-O0" machine are no longer valid on the "-O3" machine, because, again, the instructions that come out look awfully similar and straightforward most of the time, except for the weird times where a change "surprises" you because the transition between the two crossed through an invalid space. Kind of like a path that seems to have a "weird jump" because it normally crosses through 3D space and at some point someone found a shortcut through the fourth dimension. Anyways, the performance virtual architecture is all well and good, but what I think will be interesting moving forward is the security virtual architecture, where overflows and out-of-bounds accesses and type confusions are focused on more. Right now as a side effect of performance optimization they end up causing headaches for people, but Valgrind/sanitizers are an interesting look into what compiling to "x86-64 for security testing" architecture looks like. The logical next step is even more exciting, because we're actually starting to deploy real architectures with security-focused features that will require ports that are every bit as concrete as any other physical architecture difference, which I think will "legitimize" this mindset to the people who I called deluded at the start of this now very rambly comment. Page protections means that "const" is not something you can ignore. Pointer signing can mean that your "but they're the same bits underneath!" type confusions are no longer valid. ARM's Morello now means you can't play fast-and-loose with your pointers anymore; they're 128+ bits and you can't just decide you want forge one out of an integer anymore without caring. Ports to these architectures absolutely rely on the existence of a C abstract machine, which has served pretty well considering that its existence is really just what a piece of paper says is legal or not, rather than something really planned beforehand.
- pjmlp 4y agoTo see how cross platform C looked like in those early days, nothing like reaching out to books like "A book on C" from 1984 (Robert Edward Berry and B. A. E. Meekings), which has an implementation of RatC [0]. https://www.amazon.de/-/en/Robert-Berry/dp/0333368215/ref=sr_1_1 https://www.amazon.de/-/en/Robert-Berry/dp/0333368215/ref=sr... Then there were Small-C and BDS C, as yet two other major subsets in those early days. [0] - Similar in ideas to Ratfor but applied to a K&R C subset
- kragen 4y agoworth noting that bds c, which generates 8080 code, is now free software
- disruptiveink 4y agoI like this description. It's a useful mental model. But in summary, doesn't that just move the target from "The compiler is stupid, it shouldn't be doing this, it clearly should know what I mean and I didn't mean that!" to "This is a bad minimum common denominator, if any architecture really needs this guarantee or for things to behave like this, then they should pay a performance penalty. We shouldn't all have to pay the portability price for this one thing that isn't a issue anywhere." And to be honest, most of the UB hate I see is about the latter, not the former, no?
- kllrnohj 4y agoMost of the UB hate is that bugs that always existed only recently became exposed. "This code worked fine for years why is the compiler breaking it!" is always the rant, but it's misplaced. It should instead be "why didn't I get a sanitizer/linters/debug-whatever error first?" The proliferation of optimization passes outpaced decent debuggability, and that's really the problem. Rants against UB are nearly always irrelevant or even just outright wrong. And worse still, those crusaders are harmful. You can see this in Rust as a perfect example. Signed integer overflow is defined two's compliment, much rejoicing from the "UB always bad!" crowd. Except wait a minute, in a debug build of Rust it's defined to be a panic. Why? Because signed integer overflow is 99.999% of the time a bug, and defining how it overflows doesn't actually help anyone. So instead you're left with the worst of both worlds - you both can't rely on how signed ints behave in Rust as a programmer because they have 2 extremely incompatible defined behaviors, and the optimizer/runtime then can't take advantage of them being undefined behavior in practice in release builds to optimize better.
- lucidguppy 4y ago"This is simultaneously true and false." Why am I not surprised about this when talking about C.
- deleted 4y ago[deleted]
- pjmlp 4y ago> It's also not the case that C only succeeded in environments that were designed for it. In fact C succeeded in at least one OS environment that was relatively hostile to it and that wanted to be used with an entirely different language. Well, I had to look into what was this. And as expected it refers to C uptake against Pascal on the Mac OS. Except what replaced Object Pascal was C++, not C, even though C minded folks like to think otherwise. MacApp was ported from Object Pascal into C++, and Metrowerks also added PowerPlant to the party. MPW ultimately happened, because a few folks pushed for it. https://en.wikipedia.org/wiki/Macintosh_Programmer%27s_Workshop https://en.wikipedia.org/wiki/Macintosh_Programmer%27s_Works... https://en.wikipedia.org/wiki/MacApp https://en.wikipedia.org/wiki/MacApp https://en.wikipedia.org/wiki/PowerPlant https://en.wikipedia.org/wiki/PowerPlant After Object Pascal, the new kid on the block for Apple was C++, even if Macintosh Toolbox exposed API entry points as C like. Newton OS (Dylan lost to C++), Taligent and Copland were also mainly C++ based. https://en.wikipedia.org/wiki/Newton_OS https://en.wikipedia.org/wiki/Newton_OS https://en.wikipedia.org/wiki/Taligent https://en.wikipedia.org/wiki/Taligent https://en.wikipedia.org/wiki/Copland_(operating_system) https://en.wikipedia.org/wiki/Copland_(operating_system)
- jb1991 4y agoImagine a world where Apple did not latch onto C++ but rather... lisp.
- jb1991 4y agoWise man once said: "C is just memory, with syntax sugar."
- 1ris 4y agofor a definiton of "just memory" that includes the strict aliasing rule. Certainly not a buffer of bytes, what most people would assume when they hear "just memory".
- jb1991 4y agoThe strict aliasing rule that you point out is not part of the “just memory” part, but rather the “syntax sugar” part. Now you can debate if it’s the right kind of sugar, however.
- 1ris 4y agoNo, The strict aliasing part of the semantic of the memory. “syntax sugar” is, per definition, part of the syntax.
- gpderetta 4y agoIndeed. What the strict aliasing rule implies is that only well typed C programs have meaning. Which means that C is indeed not "just memory". If C was just memory, the only operations allowed would be on and through memory addresses, and values wouldn't be first class.
- jstimpfle 4y ago"Memory" might not even be a concept in the C standard, rather it is what C programmers think about in practice. The culture around the language is that the language should get out of the way as much as possible while still providing a good amount of convenience, portability, and performance. The standard is a necessity, but is not the center of attention while working.
- 4y ago
- bluetomcat 4y agoEarly C largely inherited the data model and semantics of BCPL, which was built around "machine words" and not byte-addressable memory. The introduction of the C "char" allowed them to take advantage of PDP-11's byte addressability for special occasions, but "int" was still the default choice for storing everything from sizes, to file descriptors, to pointers. In that sense, it was not exclusively built for a byte-addressable machine with registers of a varying size.
- pjmlp 4y agoAlso worthy of note that BCPL was originally designed as means to Bootstrap CPL, not to go around writing full systems with it, hence why it was so basic.
- kragen 4y agothere are an awful lot of chars in the v6 unix code, i don't think it's really 'for special occasions' also pointer arithmetic on a byte-addressed machine is different from int arithmetic, so you have to know if something is an int or an int pointer if you want to increment it from the horse's mouth in https://www.bell-labs.com/usr/dmr/www/chist.html https://www.bell-labs.com/usr/dmr/www/chist.html, dmr's hopl ii paper, with the advantage of 20 years of hindsight > The machines on which we first used BCPL and then B were word-addressed, and these languages' single data type, the `cell,' comfortably equated with the hardware machine word. The advent of the PDP-11 exposed several inadequacies of B's semantic model. First, its character-handling mechanisms, inherited with few changes from BCPL, were clumsy: using library procedures to spread packed strings into individual cells and then repack, or to access and replace individual characters, began to feel awkward, even silly, on a byte-oriented machine. > Second, although the original PDP-11 did not provide for floating-point arithmetic, the manufacturer promised that it would soon be available. Floating-point operations had been added to BCPL in our Multics and GCOS compilers by defining special operators, but the mechanism was possible only because on the relevant machines, a single word was large enough to contain a floating-point number; this was not true on the 16-bit PDP-11. > Finally, the B and BCPL model implied overhead in dealing with pointers: the language rules, by defining a pointer as an index in an array of words, forced pointers to be represented as word indices. Each pointer reference generated a run-time scale conversion from the pointer to the byte address expected by the hardware. > For all these reasons, it seemed that a typing scheme was necessary to cope with characters and byte addressing, and to prepare for the coming floating-point hardware. Other issues, particularly type safety and interface checking, did not seem as important then as they became later.
- planede 4y agoSome actionable takeaways about the abstract machine and undefined behavior of the standard. * Just because something is undefined in the standard now, it does not mean that it has to be undefined forever. It might be an oversight, or priorities might shift. If you disagree with a particular undefined behavior and about ways how it can manifest you can raise it with the committee and/or compiler vendor. * The behavior of a program is just as defined by the implementation now than before a standard existed. If you write code that contains operations where the standard does not define behavior, the implementation might (but they don't have to). Consult your implementation for the compilers you care about. They have flags to enable certain extensions that define the behavior of some operations, at the expense of disabling some optimizations.
- yencabulator 4y agoThe standard doesn't even need to change. The compilers can just stop interpreting the rules around UB with malicious compliance for the sake of 0.1% optimization in some other scenario.
- opportune 4y agoI don’t think it makes sense to say C begat the modern OS/CPU or vice versa. These things tended to develop in tandem over time, which is kinda the author’s point. There are of course many such examples regarding OS’s and chip features, languages and frameworks/ecosystems, etc. Also, not to be overly Neoplatonic, but is it not the case that C is basically what a smart person would come up with as a way to write portable-but-not-very-abstracted imperative code on a Von Neumann machine?
- pjmlp 4y agoYes, in fact one only has to look into what was happening during the late 50's in systems programming, starting with JOVIAL in 1958, or outside Bell Labs, to see it was indeed so.
- nine_k 4y agoBut C is late 1960s / early 1970s, created after PL/I, Algol-68, and even Pascal. Sadly, C went forth not only with hard-to-parse syntax, but also with stuff like undefined behavior, null-terminated strings, unchecked pointer arithmetic for array access, etc. It was designed as a language for a confident kernel hacker working close to hardware, but ended up as a general-purpose programming language for the entire OS, and it's not the best fit for that role.
- lucidguppy 4y agoI try to always understand the current state of things in the context of how they were before the current state. C and Unix came on to the scene - and they were much much better than what came before. Users were meant to use the shell commands + awk and get a whole lot done with that. C programs were meant to be small. How small? Well - how much code would you be able to write using ed? I think Unix intended for programmers to develop languages for users - use lex and yacc to come up with something to hand off to users. The Unix operating system assumed a corporate org structure that just does not exist. Genius programmers and highly educated users. https://youtu.be/tc4ROCJYbm0 https://youtu.be/tc4ROCJYbm0 <--- this is what they were expecting If we went back in time to tell them how real corporations would be set up, I think C would have had safe defaults that could be disabled when needed and syntax closer to Go than C. In fact if we told them how things would be in 40 years, they would have settled on the erlang VM for stuff outside of systems and graphics programming.
- zokier 4y agoThere was this idea of "Friendly C" which in part would have brought C semantics closer to the concrete computer semantics. But it fizzled out before it really even got started properly. Regehrs "post-mortem" post summarizes the problem: https://blog.regehr.org/archives/1287 https://blog.regehr.org/archives/1287 While there is a "group of C users who are unhappy with aggressively optimizing C compilers", they are not unhappy enough to put in the effort to define and implement alternative semantics.
- gpderetta 4y agoturns out DWIM is not a spec :)
- nullc 4y agoIt also turns out that the optimizations are valuable. It's easy to think its a bad deal when you are just complaining and don't actually have to face losing the optimizations.
- imtringued 4y agoIf there was an optimization flag for sanity then I doubt people would complain.
- zokier 4y agoThe way I see friendly c could have been more likely to be a perf win than loss; it's more replacement for -O0 (etc) and allow at least some optimizations on otherwise hopeless codebases.
- crabbone 4y agoUNIX as a whole, and C in particular weren't any kind of well-designed projects. They are awful inside and out. Their staying power isn't due to the goodness of the design or their remarkable performance. They are with us because of the network effect. Same as IPv4 + NAT, Base64-encoded email attachments etc. Plan 9 would be a similar attempt to this "Friendly C", or D language: these are attempts by people who were misguided to believe that the original technology was mostly good, and needed only a nudge in the right direction to fix a few problems here and there to make it perfect... but it turned out that the technology didn't succeed on technological merits, and improving the technology only so slightly isn't going to bring any new audience to the clone. C has way too many problems, far beyond undefined behavior. None of that is important as long as the most popular operating system on the planet is written in C, and C89 at that...
- GianFabien 4y agoC was created to write Unix on PDP-11 hardware. Prior to that operating systems were typically written in assembler. C was seen as a high level assembler and only later ported to other hardware. Realistically, C should have been restricted for writing operating systems and device drivers. It is far too low-level for application programming. But since it was often the only portable high level language, software vendors adopted it for purposes that it was never designed to handle. That was partly the reason that C++ and Objective C emerged.
- danuker 4y agoAlso Java and Python.
- pjc50 4y agoAlso Java and Python... what? This sentence no verb.
- hdjjhhvvhga 4y agoRelatively quickly we got Pascal, and the Borland dialect got widely adopted in the academia. Unfortunately, because of teething problems (various dialects, TP only available for DOS/Windows etc.), eventually it lost the battle with C. It's a pity as we would have to deal with much less bugs over those years.
- 4y ago
- neilv 4y ago> (There have also been actual C interpreters, some of which had strict adherence to the abstract semantics, cf (available online in the Usenix summer 1988 proceedings).) I used one of these a little, Saber-C, circa 1990, on a Sun SPARCstation. It was glorious, and several steps beyond the Turbo C IDE on MS-DOS that I'd been using at home as a teen. One of the big wins of Saber-C, before Purify and the later fancy open source memory checkers, was that it could quickly find memory problems. One of my mentors spent some evenings doing a Saber-C-powered memory-bug-search&destroy blitz through an open source Unix X11 game. (I think our own code had less need of help like that, so less Saber-C low-hanging-fruit fun.) https://archive.org/details/1988-proceedings-summer-san-francisco/page/161/mode/1up https://archive.org/details/1988-proceedings-summer-san-fran...
- Quequau 4y agoWithout trying to suggest the author is wrong (I'm confident he isn't) "C Minus-Minus" is interesting and does aim to be a "portable assembly language" which in turn is pretty much a target for an abstract machine which most modern compilers can deal with. https://en.wikipedia.org/wiki/C-- https://en.wikipedia.org/wiki/C--
- Karellen 4y ago> The determined refusal of the specification to tie this abstract machine in concrete CPUs is the source of a significant amount of frustration in people who would like, for example, for there to be some semantics attached to what happens when you dereference an invalid pointer. They note that actual CPUs running C code all have defined semantics, so why can't C? Because while all CPUs running C code have defined semantics for any given construct, not all CPUs have the same defined semantics. Making C adhere to one CPU architecture for the sake of convenience would make implementations for other CPU architectures decidedly inconvenient. Rather than dereferencing an invalid pointer doing "whatever this CPU does", suddenly all C implementations now have to do whatever, for example, a VAX did. It would mean that C code running on simple CPUs without memory protection would have to look out for invalid pointer derefs (how?) and simulate access violations if one were mandated by the spec. Or, if the spec said that the address space should be treated as a flat memory area all of which is accessible by any pointer, modern implementations would have to simulate that.
- kragen 4y agoit would solve a lot of problems if leniency introduced into the ansi spec to permit c implementations on the burroughs large systems line was only used in cases like that, instead of compiler vendors abusing it to eke out 0.2% performance gains on amd64 hardware at the cost of introducing priority 1 security bugs into operating system kernels
- torstenvl 4y ago> Rather than dereferencing an invalid pointer doing "whatever this CPU does", suddenly all C implementations now have to do whatever, for example, a VAX did. Dereferencing an invalid pointer doesn't do "whatever this CPU does," it does completely unpredictable things. I think that's part of the problem (to the extent it's a problem)
- pavlov 4y agoIt’s up to the platform in the end. I recall IBM’s AIX used to put a zeroed-out block at address zero, so this code was guaranteed to always return 0: *(int *)NULL The reason was that it allowed their optimizing compiler to speculatively dereference a pointer before executing a surrounding if condition.
- puffoflogic 4y agoThe history of the c specification is irrelevant to the question of the abstract machine. Other languages developed this way are rust, scheme, and... yep, pretty much every single other language spec in existence.
- rtpg 4y agoNot at all a C partisan, but this blog is pretty amazing, with a bunch of interesting asides. A sort of hyperlinked version of somebody's entire worldview, and this is the entrypoint. Good job on the output
- lordleft 4y agoThis reminds me of how prestige dialects or formalizations of natural languages (e.g. Panini with Sanskrit) are perceived as the origin of a language, when in fact they are derivatives or refinements of vernacular.
- HarHarVeryFunny 4y agoI don't think the C standard's inclusion of implementation / undefined behavior is due to what OP calls a "documentation standard", documenting a variety og behavior already codified into various implementations... Rather, it seems that the C standard tried (& succeeded) from the beginning in making it easy to implement a compiler that generated good code, even to the extent of including register allocation hints to the compiler. This also appears to be why the short/int/long types are defined the way they are - not as types with specific ranges, but rather only that long >= int >= short. This allowed a given implementation to map int to whatever was most appropriate to the target CPU, be it 8-, 16- or 32-bit. In this vein, wanting to be fast on all targets, I don't think C wanted to assume the presence of an MMU or any hardware required to efficiently detect NULL pointers, so instead just left this as an instance of implementation defined behavior. C isn't alone in this regard - AFAIK many languages only define the behavior of correct programs and leave the behavior of buggy ones as implementation defined.
- ChuckMcM 4y agoAnother one of my favorites of his: https://utcc.utoronto.ca/~cks/space/blog/programming/CTriumph https://utcc.utoronto.ca/~cks/space/blog/programming/CTriump... (the C Juggernaut) I like it in part because I lived through some of that time period (getting code running on early Macs) I can relate to people who just like programming in C. :-)
- puffoflogic 4y agoYou know what is commonly specified with abstract machine semantics now? Assembly language. See e.g. the ARMv8 Architecture Reference Manuals, where code for an abstract machine is listed right there in every single instruction listing, and a great deal of appendix space is devoted to providing a library of helper routines for this machine, and also specifying things like the virtual memory system using the AM. C programmers who think abstract machine semantics are bad are completely out of touch with reality. I put them in the same bucket as people who think all CPUs are 32-bit x86. They in turn are like the devs who maintain old COBOL systems, except less useful because backward compat and huge emulation advances have made it unnecessary to actually run such 32-bit x86 configurations, unlike the COBOL case.
- WalterBright 4y agoCuriously, C semantics influenced CPU instruction set architecture to support C better.
- favorited 4y agoIt's not unique in that regard; ARMv8.3 added an instruction to handle floating point conversion errors in a way that is more efficient for JavaScript. https://twitter.com/gparker/status/1047246359261106176 https://twitter.com/gparker/status/1047246359261106176