This scratches the surface of why I hope C slowly fades away as the default low-level language. C sounds simple when you look through K&R C. C lets you feel like you understand the stack, ALU and memory. A pointer is just an integer and I can manipulate it like an integer.
But the reality is filled with a staggering number of weird special cases that exist because memory doesn't work like a simple flat address space; or the compiler needs to optimise field layouts, loops, functions, allocations, register assignments and local variables; or your CPU doesn't use the lower 4 bits or upper 24 bits when addressing.
C has no way to shield common language constructs from these problems. So everything in the language is a little bit compromised. Program in C for long enough and you'll hit a lot of special special cases – usually in the worst way: runtime misbehavior. Type punning corruption, pointers with equal bit representations that are not equal, values that change mysteriously between lines of code, field offsets that are different at debug and release time.
When using fundamental language constructs, we shouldn't need to worry about these ugly special cases – but C is built around these ideas. The need to specify memory layouts, direct memory access and other low level access should be gated by barriers that ensure the language's representation and the machine representation don't tread on each other's toes.
Rust has a long way to go but is so much more robust at runtime. I think there's room for other languages to occupy a similar space but they're need to focus on no-std-lib no-runtime operation (not always the sexiest target).
Rust might have gone too far the other way. Yes it strives to be a modern language, with a lot of functional programming features and "OOP" done right (aka no inheritance, simply method polymorphism). To me Rust is more a C++ replacement than a C replacement.
A replacement for C should try to be as simple as possible while fixing C weak typing mess with strong typing for instance, including when it comes to pointers (for instance like in Ada where you can't just take the address of anything you want, you need to declare it as aliased to begin with). In fact Ada tries hard to make pointers redundant which is a great thing. This and range types and bound check arrays( Ada has a lot lot of great ideas, and "C style" Ada is a treat to use).
So a C replacement should keep the procedural nature of C, with closures perhaps, but not go further in terms of paradigm. C aficionados often claim they love C because it's "simple"(it isn't) That's what they mean, they love C because you can't really do OO with it.
On the other hand, Rust macros are exemplary of what a good macro system should be.
> I think there's room for other languages to occupy a similar space but they're need to focus on no-std-lib no-runtime operation (not always the sexiest target).
You're describing the Zig language!
It aims to be as fast as C, and unlike most languages that say this is a goal, it means it. There's no "almost as fast as C if you ignore the garbage-collector and the array-bounds-checking", it's actually as fast as C.
Its author values the minimalism of C, but wants to create a far better language for doing that sort of work. [0]
They're doing surprisingly well. They even managed to make a faster-than-C (faster even than hand-tuned assembly) SHA-2 [1]
C sort of has this mindset built around it that you're writing portable assembly, except for messy bits like stack frame layout or register allocation. But that doesn't really hold true anymore, and arguably hasn't for decades.
The first obvious issue is that C is specified by an abstract machine that doesn't really correspond to actual hardware. There's no concept of segmented memory, or multiple address spaces in C. Traps don't quite work the way you'd want in C [1]; there's no way to catch a trap that occurs in a specific region of code. Worse, the abstract machine leans on a definition of undefined behavior in such a way that it's painful for programmers but not useful enough to make automatic security permissible. (Unsigned overflow, which is going to be a more common cause of overflows that lead to security vulnerabilities, is well-defined).
However, the more real problem with C is that it is outright missing hardware features that you'd like access to. Things like rotate instructions or wide multiply are easy enough to pattern match, so their lack isn't so devastating. But flag bits--particularly add with carry or checked overflow operations--aren't figured into at all. SIMD vectors have no conception. Returning multiple values via registers. Traps [1]. Coroutines. Multiple address spaces (especially important for GPUs).
[1] Admittedly, the OS is also to blame here: POSIX signals are a really atrocious way to handle machine-generated faults.
> A pointer is just an integer and I can manipulate it like an integer.
Except it's not. Not unless you cast it to uintptr_t. Doy. :) Understanding the underlying asm can give you context for what a pointer is, but you can't assume that a pointer is an integer, any more than you can assume 'int' is a machine word in length.
There are reasons why C seems so abstruse and abstract. It has to exist on a huge variety of architectures and environments, from supercomputers to tiny embedded microcontrollers. Fun fact: The Lisp Machine had a C compiler! In Lisp Machine C, pointers were fat: they consisted of a machine pointer to an allocated chunk of memory and an offset within that chunk. And of course it was invalid to have a pointer outside an allocated object; the machine would throw an exception if you even tried. Hence why C pointers are only well-defined over valid references to objects and arrays, one past the end of an array, and NULL. And lest you say, "well no modern cpu works like that anymore", there are programs out there written for the Burroughs architecture that are still running. Are you going to cut off the maintainers of those programs because the architecture they're coding for doesn't resemble an x86?
This is part of why I disdain the call for modernity in programming. "Modern" means "limited". "Modern" means "I have no intention of testing this on anything but my laptop, so I will make assumptions that suit my use case and fuck you if yours differs".
C is a pain in the ass, but it becomes more approachable if you take into account its history. And it still fits in your head more easily than Rust.
The weird special cases were largely(1) introduced recently by optimizer writers hijacking the standard in order to soften the semantics so that previously illegal optimizations would now be legal, by simply declaring the vast majority of existing C code as "undefined" and up for grabs.
Which is why the Linux kernel, among others, has to set special flags in order to get back to a somewhat sane variant.
(1) Except for those special architectures where the hardware is that weird, but in that case you typically know about it.
But as you know, CPU ISAs are designed for C programs and compilers are optimized for C programs. So everything, even new languages like Rust, have to buy into C's model to some extent.
Truly getting out from under C's shadow is going to be very difficult. Maybe better languages are a first step on that path but they are only a small step.
Some of your complaints could be said about assembly,too, but are you wanting to get rid of that, too? Some of your other complaints are about how processors and other hardware work, not C. In many ways, you are asking for C to be like other languages but then it wouldn't be C and all the advantages it does have.
If you get rid of C, you get rid of most of the software your operating system runs on and that begs the question you need to ask yourself: if C is so bad, why is everyone using it?
I feel like C++11 and Rust have shown a way forward and the C standard should focus on a design that takes into account ownership and protected arrays in a simple way, hopefully as backwards compatible as possible.
Maybe the solution is for the major compilers to default to strong static analysis.
Even though there are safe languages, the enormous integration and compatibility of C means that it isn't going anywhere soon, so I think even imperfect solutions can offer a lot if they are low hanging fruit.
I've been writing C on and off since 1990, across embedded systems, game consoles, massive SGI supercomputers, and modern kernels.
C is not fading away because of it's portability and low overhead. It has some nasty behaviors, as the original post shows, but you hit those once in a blue moon. The language is not unsound because of it, it just has some warts like any other language.
I don't particularly enjoy writing C, as it's too low level for my liking and I have to write too much code to do basic things (slist_* makes me shudder), but the reason that I have used it is due to its tremendous portability and direct compilation to machine code. That still matters in some places.
In places where memory is precious, or realtime performance precludes things like memory managers, C will continue to thrive, beside languages like Rust which are great for different goals. There's room for both.
C, in my opinion, has stood the test of time on its simplicity, portability, and the ubiquity of the C standard library, tiny as it may be.
On top of that C just got here thanks to widespread of UNIX and POSIX, and we had so much better options.
Yes they were languages with helmet, seat belts and airbag bolted on, but just like old technology cars on a motorway crash, the CVE database shows what happens when things go wrong.
These architectural bizarrities exist and have to be dealt with by someone. Of course it is better if that someone isn’t you—if some rust maintainer ports the language to the obscure hardware where the CPU ignores the lower four bits when addressing and presents nice, neat, and performance abstractions for you to use.
But who is going to pay for that effort? If the hardware manufacturer cared enough to to invest a lot in developer tooling it probably wouldn’t have picked such a hostile interface to begin with.
Is there a site or book out there that talks about these common pitfalls? I work in C89 so K&R C does seem pretty simple to me. I clearly haven't worked with it long enough to spot these things, or I have code in the wild that unknowingly is victim to these issues and I've yet to stumble on it.
> memory doesn't work like a simple flat address space
Can you expand on that? I thought that modern (non-segmented) memory does work like a simple flat address space, at least from the perspective of userspace programs. Isn't all the weirdness only a result of compiler optimisations?
That's compiled with '-std=c11 -O1' as in the article. The result is the same of pcmp is moved into a separate file so that when compiling it the compiler has no knowledge of the origins of the two pointers.
I don't like this at all. It bugs me that I can get different results comparing two pointers depending on where I happen to do the comparison.
Yes, I agree. It makes far more sense to make guarantees in a language, that two values are compared using a metric that is invariant. Especially in a language like C, where we expect it to be a very thin abstraction over the machine.
Yup, what is described in the article looks like a bugged optimization to me were the runtime behavior differs from the static model used for the optimization. I am sure the actual runtime semantics is in fact to compare addresses but the optimization claims to know (falsely) enough about the pointers to determine they must be not equal.
I don't understand the people in this comment section defending this. The cited spec gives no evidence that this should happen (I fail to see the undefined behavior here). It specifies when pointers are equal, and both pointers ended up pointing to the same object, so they should be equal. Just because the compiler doesn't know, shouldn't mean it can replace an equality check with false.
The spec didn't say "also, they are not equal in case the conpiler thinks that they proooobably aren't pointing to the same object".
I do agree that one should avoid this type of pointer arithmetic, but that doesn't excuse the confusing behavior of this primitive operstion.
One should at least argue for the behavior to be consistent.
I like the behavior of the compiler here. There is no guarantee that a and b are next to each other in memory. That's why the comparison fails, the alternative makes is runtime/compiler/optimization level dependent which would be a total mess.
As usual with those C bashing articles you won't run into trouble if you don't try very hard to write contrived code.
I mean, the moment you see:
int *q = &b + 1;
on your screen alarm bells should go off. Doing pointer arithmetic on something that is not an array is asking for trouble. If the standard should be amended in any way it should be undefined behavior right away you do pointer arithmetic on non-array objects.
the comparison at the start is nonsense - there is no specification for the ordering or location of stack variables. by taking the address of these variables, you could see that they actually are the same value, and so intuitively you’d think they might be the same, but a different compiler might put them in different locations. or they may be elided entirely through optimisation. it’s far safer to fail the equality test in this case - this is what the model specifies.
this is not even the first example of this counter-initiative behaviour. imagine two floating point values with exactly the same bit-representation. it is possible, without any trickery for them to fail an equality check - i.e, they are both NaN.
this is what IEE754 demands of a compliant floating point implementation. and indeed, it’s a sane choice when you understand why it was made.
similarly, it’s perfectly reasonable for this example to fail.
> cc pointereq.c
> ./a.out
0x7ffee83163b8 0x7ffee83163b8 1
> cc -O pointereq.c
> ./a.out
0x7ffeeeb8b3b8 0x7ffeeeb8b3c0 0
So without optimization, the pointers are the same and compare as equal. With optimization, the pointers compare as not equal. At first that seemed horrible, until I saw the pointers actually are not the same. Since I don't recall any guarantees about stack layout, that seems perfectly fine.
> cat pointereq.c
#include <stdio.h>
int main(void) {
int a, b;
int *p = &a;
int *q = &b + 1;
printf("%p %p %d\n", (void *)p, (void *)q, p == q);
return 0;
}
There's nothing surprising in the first example. Comparing the addresses of stack variables is undefined behaviour.
The second one is more interesting:
extern int _start[];
extern int _end[];
void foo(void) {
for (int *i = _start; i != _end; ++i) { /* ... */ }
}
GCC optimized "i != _end" into "true". The kernel guys fixed this by turning "_start" and "_end" into "extern int*". I always thought [] was just syntactic sugar over a regular pointer, but seems like I was wrong.
Actually, the headline article tells us that it optimized it to true.
That affected some of my code from years ago, too; and another fix is to keep _start and instead of having another array called _end have an external size_t giving the number of elements of start and loop while i!=_start+count .
In fairness to the compiler people there shouldn't really be anything surprising in the second example, either. There are x86 memory models (compact and large) where that loop never terminates in the general case, because the two arrays are not in the same data segment and no amount of incrementing a (far but not huge) pointer will get from one to the other. So it's not the case that this is a new pitfall. It was a pitfall decades ago.
People just thought that it didn't exist with modern tiny (a.k.a. flat) memory models, because the old explanation wasn't applicable. It does, but with a different explanation.
> I always thought [] was just syntactic sugar over a regular pointer, but seems like I was wrong.
It depends on the context.
When used in function arguments, yes, it's the same.
When used during the declaration of a variable, it's entirely different.
The confusion in the example is the choice of names "start" and "end". If we rename them "some_array" and "some_totally_different_array" the undefined behavior is clear.
> I always thought [] was just syntactic sugar over a regular pointer, but seems like I was wrong.
For another example showing their differences, if a.c has:
int n[] = { 1 };
And b.c has:
extern int *n;
printf("%d\n", *n);
Then running b.c will segfault, whereas if it said extern int n[], it would work as expected (hint: extern int n and printf("%d\n", n) would also work). Arrays degrade to pointers at the drop of a hat, but they're distinct from them.
Two pointers compare equal if and only if both are null pointers, both are pointers to the same object (including a pointer to an object and a subobject at its beginning) or function, both are pointers to one past the last element of the same array object, or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.
Can someone explain to me the rationale behind this?
Why not just "two pointers compare equal if they point to the same address"?
The C standard was (and is) written so that it allowed for machines that didn't have simple pointers; a highly relevant example for the time was x86 machines in real or protected mode using segment registers. Even when such environments have accessible linear address spaces under the hood, comparing the absolute address of two pointers requires some extra work (you have to reassemble the absolute address from the segment + offset pair). In the grand tradition of allowing the most efficient implementation possible, the C standard says that implementations don't have to do this work; they can simply compare pointer values directly, provided that they insure that pointers to the same object work (eg use the same segment + offset pair, which they normally naturally will).
The standard was also written to allow for C interpreters, where you may not have an underlying linear memory model at all and all pointers are represented internally in some complex way (for example 'object ID + offset inside object'). Here you don't have any particular idea of 'an address' as a distinct thing and so you can't naturally compare two pointers to different objects in any meaningful way.
Because (a) pointers do not point to addresses, and (b) now you have to define the concept of addresses being equal in the language standard and haven't actually reached the goal. (-:
> If we step back from the standard and ask our self does it make sense to compare two pointers which are derived from two completely unrelated objects? The answer is probably always no.
The one big counterexample I can think of is the difference between memcpy and memmove. The latter is supposed to be able to do arithmetic on memory regions, to see if they overlap. Is this article saying that the standard C implementation of memmove is relying on unspecified behavior?
Memmove is frequently cited as an example of a standard library function that can't be implemented using only standard C. It's incorrect, though: the way to do memmove using standard C is to use malloc to allocate a temporary buffer.
But the whole question is not terribly relevant. When memmove is provided as part of the C implementation it can rely on non-portable platform behaviour just fine. There's no rule that you have to implement libc using only standard C facilities.
Another example of arbitrary pointer comparison I'm familiar with is for resolving lock ordering problems. If I have a big set of locks that I need to take to complete a particular operation, a simple way to prevent deadlocking is to acquire those locks in the order of their pointers.
> ...or one is a pointer to one past the end of one array object and the other is a pointer to the start of a different array object that happens to immediately follow the first array object in the address space.
I was not aware of this special case. What's the rationale? Is there even a way in standard C to guarantee that two array objects are laid out in memory like that, with no padding?
He's adding "1" to the pointer that points to the variable
"b" which in this case is allocated immediately below "a" in the stack. Thus adding one will make the two pointers equal.
[+] [-] gilgoomesh|7 years ago|reply
But the reality is filled with a staggering number of weird special cases that exist because memory doesn't work like a simple flat address space; or the compiler needs to optimise field layouts, loops, functions, allocations, register assignments and local variables; or your CPU doesn't use the lower 4 bits or upper 24 bits when addressing.
C has no way to shield common language constructs from these problems. So everything in the language is a little bit compromised. Program in C for long enough and you'll hit a lot of special special cases – usually in the worst way: runtime misbehavior. Type punning corruption, pointers with equal bit representations that are not equal, values that change mysteriously between lines of code, field offsets that are different at debug and release time.
When using fundamental language constructs, we shouldn't need to worry about these ugly special cases – but C is built around these ideas. The need to specify memory layouts, direct memory access and other low level access should be gated by barriers that ensure the language's representation and the machine representation don't tread on each other's toes.
Rust has a long way to go but is so much more robust at runtime. I think there's room for other languages to occupy a similar space but they're need to focus on no-std-lib no-runtime operation (not always the sexiest target).
[+] [-] nostalgeek|7 years ago|reply
Rust might have gone too far the other way. Yes it strives to be a modern language, with a lot of functional programming features and "OOP" done right (aka no inheritance, simply method polymorphism). To me Rust is more a C++ replacement than a C replacement.
A replacement for C should try to be as simple as possible while fixing C weak typing mess with strong typing for instance, including when it comes to pointers (for instance like in Ada where you can't just take the address of anything you want, you need to declare it as aliased to begin with). In fact Ada tries hard to make pointers redundant which is a great thing. This and range types and bound check arrays( Ada has a lot lot of great ideas, and "C style" Ada is a treat to use).
So a C replacement should keep the procedural nature of C, with closures perhaps, but not go further in terms of paradigm. C aficionados often claim they love C because it's "simple"(it isn't) That's what they mean, they love C because you can't really do OO with it.
On the other hand, Rust macros are exemplary of what a good macro system should be.
[+] [-] MaxBarraclough|7 years ago|reply
You're describing the Zig language!
It aims to be as fast as C, and unlike most languages that say this is a goal, it means it. There's no "almost as fast as C if you ignore the garbage-collector and the array-bounds-checking", it's actually as fast as C.
Its author values the minimalism of C, but wants to create a far better language for doing that sort of work. [0]
They're doing surprisingly well. They even managed to make a faster-than-C (faster even than hand-tuned assembly) SHA-2 [1]
[0] https://andrewkelley.me/post/intro-to-zig.html , https://github.com/ziglang/zig/wiki/Why-Zig-When-There-is-Al...
[1] https://ziglang.org/download/0.2.0/release-notes.html , it's also mentioned somewhere in this talk https://youtu.be/Z4oYSByyRak
[+] [-] jcranmer|7 years ago|reply
The first obvious issue is that C is specified by an abstract machine that doesn't really correspond to actual hardware. There's no concept of segmented memory, or multiple address spaces in C. Traps don't quite work the way you'd want in C [1]; there's no way to catch a trap that occurs in a specific region of code. Worse, the abstract machine leans on a definition of undefined behavior in such a way that it's painful for programmers but not useful enough to make automatic security permissible. (Unsigned overflow, which is going to be a more common cause of overflows that lead to security vulnerabilities, is well-defined).
However, the more real problem with C is that it is outright missing hardware features that you'd like access to. Things like rotate instructions or wide multiply are easy enough to pattern match, so their lack isn't so devastating. But flag bits--particularly add with carry or checked overflow operations--aren't figured into at all. SIMD vectors have no conception. Returning multiple values via registers. Traps [1]. Coroutines. Multiple address spaces (especially important for GPUs).
[1] Admittedly, the OS is also to blame here: POSIX signals are a really atrocious way to handle machine-generated faults.
[+] [-] bitwize|7 years ago|reply
Except it's not. Not unless you cast it to uintptr_t. Doy. :) Understanding the underlying asm can give you context for what a pointer is, but you can't assume that a pointer is an integer, any more than you can assume 'int' is a machine word in length.
There are reasons why C seems so abstruse and abstract. It has to exist on a huge variety of architectures and environments, from supercomputers to tiny embedded microcontrollers. Fun fact: The Lisp Machine had a C compiler! In Lisp Machine C, pointers were fat: they consisted of a machine pointer to an allocated chunk of memory and an offset within that chunk. And of course it was invalid to have a pointer outside an allocated object; the machine would throw an exception if you even tried. Hence why C pointers are only well-defined over valid references to objects and arrays, one past the end of an array, and NULL. And lest you say, "well no modern cpu works like that anymore", there are programs out there written for the Burroughs architecture that are still running. Are you going to cut off the maintainers of those programs because the architecture they're coding for doesn't resemble an x86?
This is part of why I disdain the call for modernity in programming. "Modern" means "limited". "Modern" means "I have no intention of testing this on anything but my laptop, so I will make assumptions that suit my use case and fuck you if yours differs".
C is a pain in the ass, but it becomes more approachable if you take into account its history. And it still fits in your head more easily than Rust.
[+] [-] mpweiher|7 years ago|reply
wants to. There fixed that for you ;-)
The weird special cases were largely(1) introduced recently by optimizer writers hijacking the standard in order to soften the semantics so that previously illegal optimizations would now be legal, by simply declaring the vast majority of existing C code as "undefined" and up for grabs.
Which is why the Linux kernel, among others, has to set special flags in order to get back to a somewhat sane variant.
(1) Except for those special architectures where the hardware is that weird, but in that case you typically know about it.
[+] [-] ChrisSD|7 years ago|reply
Truly getting out from under C's shadow is going to be very difficult. Maybe better languages are a first step on that path but they are only a small step.
[+] [-] sureaboutthis|7 years ago|reply
If you get rid of C, you get rid of most of the software your operating system runs on and that begs the question you need to ask yourself: if C is so bad, why is everyone using it?
[+] [-] CyberDildonics|7 years ago|reply
Maybe the solution is for the major compilers to default to strong static analysis.
Even though there are safe languages, the enormous integration and compatibility of C means that it isn't going anywhere soon, so I think even imperfect solutions can offer a lot if they are low hanging fruit.
[+] [-] oppositelock|7 years ago|reply
C is not fading away because of it's portability and low overhead. It has some nasty behaviors, as the original post shows, but you hit those once in a blue moon. The language is not unsound because of it, it just has some warts like any other language.
I don't particularly enjoy writing C, as it's too low level for my liking and I have to write too much code to do basic things (slist_* makes me shudder), but the reason that I have used it is due to its tremendous portability and direct compilation to machine code. That still matters in some places.
In places where memory is precious, or realtime performance precludes things like memory managers, C will continue to thrive, beside languages like Rust which are great for different goals. There's room for both.
C, in my opinion, has stood the test of time on its simplicity, portability, and the ubiquity of the C standard library, tiny as it may be.
[+] [-] pjmlp|7 years ago|reply
Yes they were languages with helmet, seat belts and airbag bolted on, but just like old technology cars on a motorway crash, the CVE database shows what happens when things go wrong.
[+] [-] bradleyjg|7 years ago|reply
But who is going to pay for that effort? If the hardware manufacturer cared enough to to invest a lot in developer tooling it probably wouldn’t have picked such a hostile interface to begin with.
[+] [-] andrewmcwatters|7 years ago|reply
[+] [-] tomp|7 years ago|reply
Can you expand on that? I thought that modern (non-segmented) memory does work like a simple flat address space, at least from the perspective of userspace programs. Isn't all the weirdness only a result of compiler optimisations?
[+] [-] Koshkin|7 years ago|reply
[+] [-] tzs|7 years ago|reply
I don't like this at all. It bugs me that I can get different results comparing two pointers depending on where I happen to do the comparison.
[+] [-] JoeAltmaier|7 years ago|reply
[+] [-] 0x417572656c|7 years ago|reply
[+] [-] quelltext|7 years ago|reply
I don't understand the people in this comment section defending this. The cited spec gives no evidence that this should happen (I fail to see the undefined behavior here). It specifies when pointers are equal, and both pointers ended up pointing to the same object, so they should be equal. Just because the compiler doesn't know, shouldn't mean it can replace an equality check with false.
The spec didn't say "also, they are not equal in case the conpiler thinks that they proooobably aren't pointing to the same object".
I do agree that one should avoid this type of pointer arithmetic, but that doesn't excuse the confusing behavior of this primitive operstion.
One should at least argue for the behavior to be consistent.
[+] [-] ash_gti|7 years ago|reply
When I ran this locally I get:
With -O0 I get 1/1 instead of 0/0 so this appears to be a compiler optimization.[+] [-] bluecalm|7 years ago|reply
As usual with those C bashing articles you won't run into trouble if you don't try very hard to write contrived code. I mean, the moment you see:
on your screen alarm bells should go off. Doing pointer arithmetic on something that is not an array is asking for trouble. If the standard should be amended in any way it should be undefined behavior right away you do pointer arithmetic on non-array objects.[+] [-] foxhill|7 years ago|reply
this is not even the first example of this counter-initiative behaviour. imagine two floating point values with exactly the same bit-representation. it is possible, without any trickery for them to fail an equality check - i.e, they are both NaN.
this is what IEE754 demands of a compliant floating point implementation. and indeed, it’s a sane choice when you understand why it was made.
similarly, it’s perfectly reasonable for this example to fail.
[+] [-] mpweiher|7 years ago|reply
[+] [-] tzahola|7 years ago|reply
The second one is more interesting:
GCC optimized "i != _end" into "true". The kernel guys fixed this by turning "_start" and "_end" into "extern int*". I always thought [] was just syntactic sugar over a regular pointer, but seems like I was wrong.[+] [-] JdeBP|7 years ago|reply
That affected some of my code from years ago, too; and another fix is to keep _start and instead of having another array called _end have an external size_t giving the number of elements of start and loop while i!=_start+count .
In fairness to the compiler people there shouldn't really be anything surprising in the second example, either. There are x86 memory models (compact and large) where that loop never terminates in the general case, because the two arrays are not in the same data segment and no amount of incrementing a (far but not huge) pointer will get from one to the other. So it's not the case that this is a new pitfall. It was a pitfall decades ago.
People just thought that it didn't exist with modern tiny (a.k.a. flat) memory models, because the old explanation wasn't applicable. It does, but with a different explanation.
[+] [-] jcelerier|7 years ago|reply
It depends on the context. When used in function arguments, yes, it's the same. When used during the declaration of a variable, it's entirely different.
[+] [-] mwkaufma|7 years ago|reply
[+] [-] kbp|7 years ago|reply
For another example showing their differences, if a.c has:
And b.c has: Then running b.c will segfault, whereas if it said extern int n[], it would work as expected (hint: extern int n and printf("%d\n", n) would also work). Arrays degrade to pointers at the drop of a hat, but they're distinct from them.[+] [-] jjnoakes|7 years ago|reply
Can you elaborate? I don't think this is true.
[+] [-] nuriaion|7 years ago|reply
[+] [-] jerrre|7 years ago|reply
Whether something useful is behind that address is another question
[+] [-] crehn|7 years ago|reply
Can someone explain to me the rationale behind this? Why not just "two pointers compare equal if they point to the same address"?
[+] [-] thatcks|7 years ago|reply
The standard was also written to allow for C interpreters, where you may not have an underlying linear memory model at all and all pointers are represented internally in some complex way (for example 'object ID + offset inside object'). Here you don't have any particular idea of 'an address' as a distinct thing and so you can't naturally compare two pointers to different objects in any meaningful way.
[+] [-] JdeBP|7 years ago|reply
[+] [-] isaachier|7 years ago|reply
float my_bad_function(void) { int x = 5; float* y = (float) &x; return y; }
[+] [-] Kenji|7 years ago|reply
[deleted]
[+] [-] joosteto|7 years ago|reply
[+] [-] psyklic|7 years ago|reply
Version: 8.1.1 Options: -std=c11 -O1 Target: x86_64-redhat-linux
[+] [-] oconnor663|7 years ago|reply
The one big counterexample I can think of is the difference between memcpy and memmove. The latter is supposed to be able to do arithmetic on memory regions, to see if they overlap. Is this article saying that the standard C implementation of memmove is relying on unspecified behavior?
[+] [-] slrz|7 years ago|reply
But the whole question is not terribly relevant. When memmove is provided as part of the C implementation it can rely on non-portable platform behaviour just fine. There's no rule that you have to implement libc using only standard C facilities.
[+] [-] coldnose|7 years ago|reply
E.g. if I have three locks I need to take,
then I'll take them in order a, c, b;[+] [-] Sharlin|7 years ago|reply
I was not aware of this special case. What's the rationale? Is there even a way in standard C to guarantee that two array objects are laid out in memory like that, with no padding?
[+] [-] bsder|7 years ago|reply
0x7fff5dbd89fc 0x7fff5dbd89fc 1
Which kind of shoots the whole article in the foot ...
[+] [-] zubyak|7 years ago|reply
[+] [-] mabynogy|7 years ago|reply
[+] [-] andyjohnson0|7 years ago|reply
[+] [-] JdeBP|7 years ago|reply
Now try with /O2 . (-:
[+] [-] _RPM|7 years ago|reply
[+] [-] armitron|7 years ago|reply
[+] [-] Kenji|7 years ago|reply
[deleted]