Essential C [pdf]
cslibrary.stanford.edu
cslibrary.stanford.edu
For instance in the whole network stack, some "agents of the planned obsolescence" did not use constant expressions in switch/case statements or initializers. Also tell those who are using that toxic c11 "Generic" keyword to use explicit code instead. I think, the main issue for C now is the standard body trying to obsolete stable in time C code and forcing upon devs/integrators the usage of the only few compilers which are able to follow fast enough their tantrums (latest gcc/latest clang). Results: all attempts to provide "working" alternative C compilers is beyond reasonable. This is becoming so accute, it is more likely to be a worldwide scam than anything else.
Yep, open source software is not enough anymore, it needs to be lean, simple but able to do reasonably "the job", and ofc very stable in time hence C89 only with benign bits of c99/c11, or "standard assembly".
This is because of the particulars of the C language. For instance, the way it handles arrays and pointer arithmetic make it difficult to detect every instance of out-of-bounds access.
[0] -fwrapv, see https://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html
Using out of bounds references in arrays and pointer arithmetic are unsafe but they can be defined. The definition of "array accesses using indexes that are outside the range of 0 - the declared array size will access memory in the data segment. The compiler will treat that access as if the addressed memory had the type specified for the array." The behavior is defined, it is unsafe, but it is defined. Not like "the compiler may or may not choose to optimize out loads and stores, or re-order them." which especially on embedded systems creates bugs where the things like "read the status register THEN read the data register (which clears the status register when read)" those get re-ordered and suddenly your loop never exits because your status bit is never set. That kind of UB needs to die in a fire IMHO.
If you do pointer arithmetic to derive a pointer value pointing 2 or more elements beyond the final element of an array, that's undefined behaviour, even if you never dereference that pointer.
C lacks the intrinsics you'd actually want for this (explicit load/ store of various machine sizes) but it does provide the "volatile" storage qualifier which is what you should be using to do what you apparently wanted in C today. It makes no sense to demand everybody else writes some sort of "not-volatile" qualifier in front of every variable to tell the compiler that actually this is just a variable and it's OK to optimise.
Why are the embedded systems programmers not using `volatile`, which exists for this exact reason?
All it does is forbid reordering or removing accesses to a particular memory location.
Historically, many compilers implemented it as a hard memory barrier, but that isn't how the standard defines it.
The compiler is free to reorder accesses to multiple volatile variables if they happen prior to the same sequence point. So (roughly) expressions involving two volatile variables do not have those accesses sequenced.
But you are right that the usage described earlier is OK, as long as both variables are marked volatile, and the accesses straddle a sequence point.
The more common mistake with volatiles is to use the for multithreading primitives.
I believe performance (compiler optimisation) is the reason the language isn't defined that way. Permitting the compiler to assume that the runtime error will never arise, opens the door to all sorts of optimisations. (At least, that's the idea.)
C permits the 'union trick' to (roughly speaking) access the bit-pattern of a value as another type, which is to say an escape-hatch is offered in the language.
Similarly the strict aliasing rule is surprising to people who are new to C, but the C standard committee seem to be committed to keeping it, presumably for performance reasons.
> Not like "the compiler may or may not choose to optimize out loads and stores, or re-order them." which especially on embedded systems creates bugs
Right, but it's defined that way to enable compiler optimizations, not to spite the programmer. As others have mentioned, C has features like volatile specifically to address this kind of thing. If the C standard required memory fences to be inserted everywhere, performance would be ruined.
> That kind of UB needs to die in a fire IMHO.
C cannot easily be made into a safe language, and I think the committee is doing the right thing in declining to try to make C into something it isn't. On the plus side, there are plenty of other languages around, many of them with compelling advantages over C. Ada, Rust, and Zig, for instance.
Then again, just use C++ alongside std::vector, std::array and std::string with FORTIFY turned on (or equivalent) and be done with it.
But the current trade-off (performance always wins) means that kernels and other embedded-style programs cannot rely on the compiler doing the "reasonable" thing for UB because it's explicitly allowed to do whatever it wants (which is generally, try for better performance).
For kernel-style work, slightly lower performance but predictable/defined behavior for some of what is currently UB, makes life much simpler.
I mean you can't even write an allocator in standard C.
I might be missing something, but I don't believe this is true? The type aliasing rules have an explicit carve-out for `char *` aliases of other types, which is intended to solve this exact issue.
Similarly, C99 and C11 both allow aliasing through a union of pointers without violating the strict aliasing rule.
malloc itself is specified to return a void pointer, but the effective type established during assignment circumvents what would otherwise be UB via aliasing[2].
Edit: Specifically, the reasoning is:
1. Allocated objects have no declared type;
2. 6.5.6: Objects with no declared type have the type of their accessing lvalue
3. 6.5.7: Access of a stored value is valid for lvalue expressions that are type compatible with the effective type.
So there's no UB here. `malloc` itself doesn't need to know about the effective type produced by the lvalue, and C (at least C99 onwards) respects that type for strict aliasing purposes.
> The lifetime of an allocated object extends from the allocation until the deallocation. Each such allocation shall yield a pointer to an object disjoint from any other object.
If the implementation of malloc is visible, then it can be inlined etc., and then you have to deal with the effects of this obviously not being true.
#include <stdlib.h> /* for size_t */
static char data_storage[10000000];
static char *nextp = data_storage;
void *malloc(size_t size)
{
void *p = nextp;
nextp += size;
return p;
}
void free(void *p){}More context around your quote:
> The lifetime of an allocated object extends from the allocation until the deallocation. Each such allocation shall yield a pointer to an object disjoint from any other object.
https://port70.net/%7Ensz/c/c11/n1570.html#7.22.3
In context, I take that to mean that all allocated objects must be disjoint from one another throughout their lifetime. It wouldn't make sense otherwise.
Yes, plus if you call it twice it returns two pointers to the same object, because there's no way in C to create another "object". Of course, as long as the callers don't know this it's fine, but it would be a problem if eg you compile your custom malloc with ASAN/Valgrind and don't tell it that it's a malloc.
I think C++ partially addresses this with "placement new" but not sure how far.
How are you defining an object? This draft version of the C11 spec defines 'object' as a "region of data storage in the execution environment, the contents of which can represent values". https://port70.net/%7Ensz/c/c11/n1570.html#3.15
malloc is defined to return a pointer to a new "base object". But your code doesn't do that; you can see by reading it that it returns a pointer to `data_storage`. That means the UB conditions for using that pointer don't match the spec.
You could say it's supposed to magically work if the function is named `malloc`, but my understanding of all C implementations is they don't do that.
That is why I tend to always write ISO C instead of C.
And I'm not sure I understand why you're using the inability to describe certain actions as the reason why you use ISO C.
(A syscall is an example of "something you can only do because the implementation isn't visible to the caller" - it can violate aliasing that way.)
Also, my argument isn't about type aliasing, it's about UB on out of bounds pointers. Could be some other aliasing issues though.
As you seem to care about this, have you made this argument on LKML yourself? What Linux kernel related work are you active on that drives your position?
Its nicely documented on LWN https://lwn.net/SubscriberLink/885941/01fdc39df2ecc25f/
It was also discussed here on HN https://news.ycombinator.com/item?id=30459634
Never heard it pronounced car!
I wish I knew the formal phonetic symbols of these, but I don't. But disambiguating these is just what they're for.
Car rhymes with are, or charge.
And care with pear, pair, hair, etc.
But I'm Australian, so I talk funny.
Likewise I say char as "car".
I have gone over half of it, and I will start recommending it left and right.
> /* Computes double of a number. Works by tripling the number, and then subtracting to get back to double. /
static int Twice(int num) {
int result = num * 3;
result = result - num;
return(result);**