3 ms·
Because for many conforming (not necessarily strictly conforming) programs, should and does are different things and many programs are written to be conforming
by jkrejcha 2y ago
Because for many conforming (not necessarily strictly conforming) programs, should and does are different things and many programs are written to be conforming rather than strictly conforming (hence why most non-trivial sane projects (including Clang in some cases, amusingly enough) builds themselves with flags that disable many of these style of checks).
The argument goes like this
Assuming that p lives at 0x0 and q lives at 0x1 (this is the only interesting case anyway, if this isn't the case the program clearly prints nothing)...
p[0] = {0}
q[0] = {0}
Line 1: ip = undefined, iq = undefined
Line 2: ip = 0x0 + 1 = 0x1 (assigned), iq = undefined
Line 3: ip = 0x1, iq = 0x1 (assigned)
Line 4: ip = 0x1, iq = 0x1 (ip == iq, so take branch, goto line 5)
Line 5: ip = 0x1, iq = 0x1
- p[1] = 0x10.
- Because p([0]) is at 0x0, p[1] must be at 0x1
- (address of p[1]) = 0x1 and (address of q[0]) = 0x1 and 0x1 == 0x1
- Therefore because these pointers have the same address, these two objects obviously must alias each other at this point
Line 6: print(q[0]); -- should print 10 because p[1] = 10 which means q[0] = 10 which because they're at the same location
The argument against this is that a pointer really is a address and some hidden data that tracks what allocation it was, therefore you can't treat p[1] as equaling q[0]. I find myself tending to agree with the original argument (this is C after all) rather than this counterargument, but I understand it nonetheless.
This touches a pain point on the evolution of C in general. C is a lot of the times the domain of stuff of it being a "portable assembler"[1] which many programmers who are writing C code for things like operating system kernels and other systems level code expect and like, but because a lot of these make non-strictly conforming programs, many popular compilers are writing optimizations in their evolution that attempt to target strictly conforming programs only at the expense of... well a lot of the reason people use (and have to use) C.
[1]: Many might take issue with the characterization of C, but it's how it was characterized even in things like the 1st ed of "The C Programming Language".
- RossBencina 2y ago> which many programmers who are writing C code for things like operating system kernels and other systems level code expect and like Strong agree. Isn't this the whole point of C? I don't have any insight into why the standards committee could see it any other way. It seems like the whole situation got hijacked by optimization writers who had somehow lost touch with their users. But maybe there is a more coherent explanation of how this happend.
- nine_k 2y agoDo you suppose that most C programmers enjoy the fact that random aliasing and free out-of-bounds array access can lead to silent data corruption, as shown in the example?
- IgorPartola 2y agoThis example isn’t showing silent data corruption due to out of bounds array access. It is showing that LLVM and others do not correctly apply optimizations to the original code. The program is correct and does what it’s supposed to in the original example.
- nine_k 2y agoYou mean, accessing an array member past the declared array bounds and overwriting another variable that happens to be allocated right next to it is what the programmer should have consciously planned to do here? IMHO, a planned aliasing would look like a union. The declaration we see does not even guarantee that q follows p in the address space, or maybe even that p and q are adjacent. (I know that int[0] sort of means "no bounds", and I think it should be an error for a stack-allocated variable which ought to have definite bounds.)
- RossBencina 2y agoI'm not sure what your point is here. With respect to "the example". The code in the linked article does not perform out of bounds array access. `ip` points to one-past-end, which is legal, and it is never dereferenced (in the original, correct, user written code). All issues described in the article relate to faulty compiler transformations. On the other hand, if you are arguing that the language should protect the programmer from "silent data corruption" by limiting their freedom to implement correct low-level memory access or pointer manipulation (correct, but not easily provable by the compiler infrastructure) then I disagree. C is not a safe language and it should not protect the programmer. It is not acceptable to me that the compiler writers are overly conservative about which correct programs are expressible in C, and which can only be written in assembler. In C I am willing to take the risk of "silent data corruption" in exchange for the freedom to write the programs that I want to write. If for some project the risk/benefit equation weighs in a different direction I will choose a different language.
- nine_k 2y agoAh, I see, it's trivial out-of-bounds array access, and the compiler's inability to notice that. The cause of innumerable data corruption bugs, crashes, and exploits. I wish C did not have implicit pointer aliasing; it already has explicit aliasing via `union`. (And a different handling of nulls. And no UD. And a pony. Sadly, I don't have a time machine to travel back to 1969.)
- jkrejcha 2y agoI mean yes in this case the problem is you're doing a dereference out-of-bounds, which makes a program not strictly conforming. The problem tends to be is that the real world is much more (essentially) complex and the domain of C is that which needs to be able to deal with that essential complexity, which includes things like type punning and int-to-ptr casts and the like which makes programs not strictly conforming. As it stands, a lot of these rules make it impossible to implement a lot of... C libraries... in ISO C, let alone things like operating system kernels and the like. > And no UD I assume this means undefined behavior? But in any case, undefined behavior is okay to have in a language, it's good for when there are no portable semantics for a particular operation or failure mode[1]. It's generally essential (although some undefined behaviors would definitely be better served being implementation defined or unspecified). It is also okay for programmers to eschew this portability (so if they want to narrow their focus from "anything that can run strictly conforming C" to a superset that targets say a specific memory model or set of processors). [1]: For example, at an extreme example, what happens if the CPU starts burning up? Obviously the standard can't describe such semantics for how a program should behave in the face of this.
- nine_k 2y agoI agree, type punning exists, this is why I mentioned `union`, invented to make some type punning easier. An explicit mechanism like that is essential. Doing it implicitly is, to my mind, too dangerous to be beneficial. Same with undefined behavior. I'm fine with implementation-dependent behavior (like what size is `int`, etc). But modern compilers see UD and feel free to do anything, even removing the code in question. Such things should not be silent, or even a warning. Instead it should be something like `unsafe` sections in Rust: "I know what I'm doing, the usual safety assumptions are irrelevant here, do what I literally say". Doing weird things is sometimes required e.g. to handle memory-mapped hardware registers, etc. But such areas should be clearly delineated, for they are usually few. They also tend to not be portable. A CPU burning up, hardware malfunctioning, etc is usually a situation where all bets are off. An unanticipated pointer aliasing in a trivial program running on perfectly working hardware is a different case.
- jkrejcha 2y agoNote: the fact that this was actually done through q[0] rather than p[1] isn't really material for the argument, but this was an error I made in the original argumentation (that I can't go back and edit now) so just wanting to make that correction. If anything, it makes the argument stronger, as the program as written is valid syntactically and semantically rather than relying on undefined behavior.