Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can the compiler cast `(void *) 0` in `execl(prog, arg, (void*) 0)` to a null pointer of the appropriate type?

From The Linux Programming Interface

execl(prog, arg, (char *) 0);
execl(prog, arg, (char *) NULL);

Casting NULL in the manner of the last call above is generally required, even on implementations where NULL is defined as (void *) 0.

This is because, although the C standards require that null pointers of different types should test true for comparisons on equality, they don’t require that pointers of different types have the same internal representation (although on most implementations they do).
And, in a variadic function, the compiler can’t cast (void *) 0 to a null pointer of the appropriate type.

The C standards make one exception to the rule that pointers of different types need not have the same representation: pointers of the types char * and void * are required to have the same internal representation. This means that passing (void *) 0 instead of (char *) 0 would not be a problem in the example case of execl(), but, in the general case, a cast is needed.

  1. "Casting NULL in the manner of the last call above is generally required"

    Does the C standard requires the null pointer be represented the same as (char*) 0?

  2. "in a variadic function such as execl(), the compiler can’t cast (void *) 0 to a null pointer of the appropriate type."

    Is (void *) 0 not a null pointer of a type?

    If yes, why can't the compiler cast (void *) 0 in execl(prog, arg, (void*) 0) to "a null pointer of the appropriate type"?

  3. "pointers of the types char * and void * are required to have the same internal representation. This means that passing (void *) 0 instead of (char *) 0 would not be a problem in the example case of execl()".

    Can the compiler cast (void *) 0 in execl(prog, arg, (void*) 0) to "a null pointer of the appropriate type" now?

    Why does it contradict to the quote in my point 2?

  4. If I replace (void *) 0 in execl(prog, arg, (void*) 0) with cast of 0 to any type's pointer, such as (int *) 0, can the compiler cast (int *) 0 in execl(prog, arg, (int*) 0) to "a null pointer of the appropriate type"? Thanks.

  5. For a non-variadic function call, such as in sigaction(SIGINT, &sa, (int*) 0), can the compiler cast (int *) 0 to "a null pointer of the appropriate type"?

Thanks.

like image 739
Tim Avatar asked Sep 06 '18 01:09

Tim


2 Answers

Firstly, the compiler does not "cast" in any circumstance. A cast is a syntax construct in the source code which requests a conversion.

I will assume that when you talk about "the compiler casting" you mean to talk about implicit conversion which is the process whereby a value of one type may be converted to a value of another type, without a cast operator.

The Standard specifies precisely the contexts in which implicit conversion may be applied; there must always be a target type. For example in the code int x = Y; the expression Y can be some type that is not an int, but which has implicit conversion to int defined.

No implicit conversion is applied to function arguments that correspond to the ... part of a prototype, other than the default argument promotions. For pointer values, the default argument promotions leave them unchanged.

A common thread of your question seems to be that the compiler should somehow pretend that execl behaves as if there were a prototype in place for the last argument. But in fact there is not, and the compiler doesn't have any magic behaviour for specific functions. What you pass is what you get.


  1. The standard specifies that the value of the expression (char *)0 is a null pointer. It says nothing about the representation of null pointers, and there may be multiple different representations that are all null pointers.

  2. The execl function specification says that the argument list should be terminated by (char *)0 which is a value of type char *. A value of type void * is not a value of type char * and there are no implicit conversions in this context as discussed above.

  3. There is still no implicit conversion; the text you quote is saying that you can use the wrong type argument in this one specific situation (no prototype parameter; and char * expected but void * provided, or vice versa).

  4. That would be undefined behaviour , the text you quoted in point 3 does not apply to int *.

  5. The sigaction function has a prototype; the parameter in question is struct sigaction *oldact. When you try to initialize a prototype parameter (or any variable) with a value of different type, implicit conversion to the type of the parameter is attempted. There is implicit conversion from any null pointer value to a null pointer value of a different type. This rule is in C11 6.3.2.3/4 . So that code is OK.

like image 147
M.M Avatar answered Oct 06 '22 09:10

M.M


Since C99, the specification of va_arg reads in part

If [the type passed to va_arg as an argument] is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

  • one type is a signed integer type, the other type is the corresponding unsigned integer type, and the value is representable in both types;
  • one type is pointer to void and the other is a pointer to a character type.

The second bullet point means that, for any variadic function that uses va_arg to access its arguments, a call of the form

variadic_function("a", "b", "c", (void *)0);

will be valid whenever

variadic_function("a", "b", "c", (char *)0);

would have been.

There is, unfortunately, a catch: I can't find any requirement for variadic standard library functions1 to [behave as-if they] access their arguments by making a series of calls to va_arg. You're probably thinking, well, how else are they going to do it? In practice it's va_arg or hand-written assembly language, and maybe the committee didn't want to require the hand-written assembly language to be perfectly equivalent, but I wouldn't worry about it.

So the book you are quoting is technically incorrect. However, I would still write

execl(prog, arg, (char *) NULL);

if I was going to use NULL in the first place (I generally prefer to use 0 as the null pointer constant), because you shouldn't write code that relies on NULL expanding to ((void *)0), and

execl(prog, arg, 0);

is unquestionably incorrect. For example, execl will not receive a null pointer from that 0 on any ABI where int is 32 bits, char * is 64 bits, and int quantities are not sign- or zero-extended to 64 bits when passed as part of a variable argument list.


1execl is not part of the C standard, but it is part of the POSIX standard, and any system providing execl in the first place is probably compliant with at least a subset of POSIX. All of clause 7.1.4 of the C standard can be assumed to apply to functions specified by POSIX as well.

like image 27
zwol Avatar answered Oct 06 '22 10:10

zwol