4 ms·
>Even C crashes safely on null pointer dereference. Uh, that's a rather optimistic way to look at this. In C NULL pointer dereference is UB so you shouldn't re
by simias 9y ago
>Even C crashes safely on null pointer dereference.
Uh, that's a rather optimistic way to look at this. In C NULL pointer dereference is UB so you shouldn't rely on this behaviour. For one thing the compiler is allowed to do weird things if it can statically assert that a NULL reference takes place, for instance by dropping any code that can be proved to lead to a NULL dereference. Not so safe or friendly.
Most of the time for userland apps dereferencing a NULL pointer for reading or writing will cause a segfault but that might not be the case for bare metal/MMU-less applications. Furthermore even when it does crash it simply terminates the process without any unwinding which might leave non-volatile data in a non-consistent state.
The real problem with pointers is that they're effectively an algebraic data type, they can point to something or be NULL. But the type system can't enforce that, there's no concept of non-nullable pointers in C.
Take the prototype of nanosleep(2) for instance:
int nanosleep(const struct timespec *req, struct timespec *rem);
Turns out that the 2nd parameter is NULLable if you don't care for the reminder. The first one isn't however, so passing NULL there is a big no-no.
In rust the equivalent signature would be something like:
fn nanosleep(req: TimeSpec, rem: Option<&mut TimeSpec>) -> Result<(), i32>;
Which is a lot more explicit (mind you, that's probably not what the API of nanosleep would look like in rust since output parameters are not very common, you'd probably return rem in the Result).
The problem is the same on the library side. When you're passed a pointer, should you assume it's nullable? Should you check for it? If you want to be safe you do, and you end up with a bunch of redundant checks.
It's also easy to end up with a NULL-ish pointer that's not NULL if you happen to offset a NULL pointer by mistake (like a NULL array or NULL struct pointer). That can be pretty tricky to track down since the crash doesn't occur when the offset is calculated but rather when the bogus quasi-NULL pointer is dereferenced:
#include <stdio.h>
struct s {
int a;
int b;
};
void bar(int *i) {
if (i == NULL) {
printf("NULL\n");
} else {
printf("%d\n", *i); /* i is invalid but not NULL, this (probably) crashes */
}
}
void foo(struct s *ps) {
int *b = &ps->b; /* ps assumed not-NULL here */
bar(b);
}
int main(void) {
foo(NULL); /* This is where the actual error takes place,
foo's argument is not NULLable */
return 0;
}
Also consider what happens if we're using a NULL pointer to a very large buffer. Dereferencing the beginning of the buffer would probably crash but higher addresses might end up in mapped pages and you start accessing and possibly overwriting random parts of the runtime.
- jstimpfle 9y agoThere are so many ways things can break if used incorrectly. I don't disagree with most of the things you listed. But what's the conclusion? > The real problem with pointers is that they're effectively an algebraic data type No, they are not. Join my religion and preach: pointers are just data, pointers are just data. And be free. C is about machine representation, and it's typed just enough to map to hardware (specify ABI of functions). Don't be deluded to think you can represent all (or even many) invariants in types. Some other languages try to do that (and it always ends up in a big mess). If you want more "safety" - which (for the things you still need to implement) just means crash in a more friendly way, not less - then use a managed language. But they come with their own pros and cons, of course. To address the issue with nanosleep, this is not what we were talking about. nanosleep does expect a null pointer in the second argument. (And I think in this rare case an assert(req) would be justified. Or maybe just split it into 2 functions).
- simias 9y agoBut they are not just data because the language handles NULL differently from other values. The compiler is allowed to treat NULL pointers differently from other pointer values. NULL is special in C, it's not just a pointer to address 0. >C is about machine representation, and it's typed just enough to map to hardware (specify ABI of functions). I don't understand that at all. How do enums map to hardware? How do structs? Clearly the line is arbitrary. Would adding generics mean that C maps less to hardware? What would that even mean? And if C is all about talking directly to the hardware, how come the language has no first class support for SIMD, for preload, for the various CPU flags? And to get back to our point, how does NULL map to hardware? A CPU has no issue addressing at 0, it's nothing special. An assembler doesn't treat address 0 differently from any other, unlike C explicitly does. Join my religion and preach: C is not a macro assembler, C is not a macro assembler. >Don't be deluded to think you can represent all (or even many) invariants in types. Some other languages try to do that (and it always ends up in a big mess). [citation needed] >To address the issue with nanosleep, this is not what we were talking about. nanosleep does expect a null pointer in the second argument. (And I think in this rare case an assert(req) would be justified. Or maybe just split it into 2 functions). My point is that in this case the rem pointer is an algebraic data type. Passing NULL here doesn't mean "please store rem at (void*)0" it means "I don't want rem". Except this is not expressed in C's type system so you don't know that unless you read nanosleep's implementation or its documentation. the rem pointer, unlike the req pointer, is nullable, therefore it's effectively an option type but as far as C is concerned they're the same type. If you assert() it then you can catch the problem early on at a runtime cost (and only when the situation eventually arises). If you make this part of the type system the compiler can validate that statically at compile time.