NULL != 0

Environmets:

  • compiler: x86_64-linux-gnu 7.4.0, x86_64-linux-gnu 4.8.5
  • assembly: AT&T
  • code base: glibc 2.23.90

All content is written based on GNU C.

GNU C에서 NULL과 0은 같지 않다는 것은 알고 있을 것입니다.

NULL != 0

그래서 NULL이 뭔가요? 간단한 sample code로 쉽게 확인해 봅시다.

Check with sample code

-> sample source code: null.c

#include <stddef.h>

int main(void) {
	char *value = NULL;

	return 0;
}

-> preprocess only

$ gcc -E null.c

-> result

# 1 "null.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "null.c"
# 1 "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h" 1 3 4
.....

# 3 "null.c"
int main(void) {
 char *value = 
# 4 "null.c" 3 4
	((void *)0) /* 0 == ((void *)0) */
...
}

pre-process의 결과에서 볼 수 있듯이, NULL 은 ((void *)0) 로 replace 됩니다.

NULL == ((void *)0)

stddef.h 대신 stdio.h를 include 하면 어떨까요?

-> include stdio.h instead of stddef.h

#include <stdio.h>

int main(void) {
	char *value = NULL;

	return 0;
}

-> preprocess only

$ gcc -E null.c

-> result

# 4 "null.c"
int main(void) {
 char *value = 
# 5 "null.c" 3 4
	((void *)0)
...
}

결과는 동일합니다.
왜냐하면 NULL은 stdio.h에 정의되어 있지 않고, 단지 stddef.h를 include 하고 있기 때문입니다.
만약 NULL이 stdio.h에 정의되어 있었다면, stddef.h에 의해 re-defined 되었을 겁니다.

-> stddef.h is also included by stdio.h

# 1 "null.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "null.c"

# 1 "/usr/include/stdio.h" 1 3 4
...
# 1 "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h" 1 3 4
...

-> content of stdio.h

#define __need_size_t
#define __need_NULL
#include <stddef.h>
...

gcc의 stddef.h를 살펴보면, NULL은 다음과 같이 정의되어 있습니다.

-> NULL definition in stddef.h

/* A null pointer constant.  */

#if defined (_STDDEF_H) || defined (__need_NULL)
#undef NULL		/* in case <stdio.h> has defined it. */
#ifdef __GNUG__
#define NULL __null
#else   /* G++ */
#ifndef __cplusplus
#define NULL ((void *)0)
#else   /* C++ */
#define NULL 0
#endif  /* C++ */
#endif  /* G++ */
#endif	/* NULL not defined and <stddef.h> or need NULL.  */
#undef	__need_NULL

GNU C++ compiler1에서의 NULL은 __null로 정의되어 있으며, C++2에서는 0으로 정의되어 있습니다.

-> check with GNU C++

$ g++ -E null.c

-> result with GNU C++<

# 4 "null.c"
int main(void) {
 char *value = 
# 5 "null.c" 3 4
	__null
...

그렇다면,
0 대신 NULL을 사용하거나, NULL 대신 0을 사용하면 문제가 발생할까요?
0과 NULL의 가장 중요한 차이점은 0은 int 이며 NULL은 void * 라는 점입니다.
다시 말해, 64-bit machine에서는 0과 NULL의 사이즈가 다를 겁니다.

-> sizeof NULL in 64-bit

#include <stdio.h>

int main(void) {
	printf("sizeof(0): %lu, sizeof(NULL): %lu\n", sizeof(0), sizeof(NULL));

	return 0;
}

-> size of result

sizeof(0): 4, sizeof(NULL): 8

그러므로, NULL을 기대하는 곳에 0을 넣으면 사이즈가 다르기 때문에 문제가 발생할 수 있습니다.
이러한 문제는 Variable-length Argument(e.g. va_arg (val, *)….)를 사용하는 곳에서 쉽게 드러납니다.

-> problem examples when put 0 not NULL

#include <stdio.h>
#include <stdarg.h>

int expect(const char *fmt, ...) {
	va_list ap;
	char *val;

	va_start(ap, fmt);

	while (val = va_arg(ap, char *))
		printf("value: %s\n", val);

	va_end(ap);

	return 0;
}

int main(void) {
  /* because of x86-64bit calling convention,
        passed many arguments for reproduction. */
	expect("aaa", "bbb", "ccc", "ddd", "fff", "ggg", "hhh", "iii", 0);

	return 0;
}

expect() 함수는 va argument로 char* pointer를 기대하고 있습니다.
그래서 8-byte를 stack에서 pop 할 것이고, 만약 NULL대신 0을 넣었다면, 4-byte stack overflow가 발생할 것입니다.

-> compile with gcc-4

$ gcc-4.8 -o null null.c

-> result

value: bbb
value: ccc
value: ddd
value: fff
value: ggg
value: hhh
value: iii
Segmentation fault (core dumped)

-> change to

//expect("aaa", "bbb", "ccc", "ddd", "fff", "ggg", "hhh", "iii", 0);
expect("aaa", "bbb", "ccc", "ddd", "fff", "ggg", "hhh", "iii", NULL);

-> result

value: bbb
value: ccc
value: ddd
value: fff
value: ggg
value: hhh
value: iii

참고로, gcc-7 이상에서는 위 코드로 문제가 재현 안될 것입니다.
gcc-7의 코드를 살펴보면, argument를 넘길 때, push 대신 pushq를 사용하도록 compile이 됩니다.
gcc가 점점 똑똑해지네요.

...
7d9:	6a 00                	pushq  $0x0
7db:	48 8d 05 f5 00 00 00 	lea    0xf5(%rip),%rax        # 8d7 
...

execl 류의 함수들의 man page를 살펴보면, 0 대신 NULL을 사용하라고 강조합니다.


man execl
The const char *arg and subsequent ellipses in the execl(), execlp(), and execle() functions can be thought of as arg0, arg1, ..., argn.

Together they describe a list of one or more pointers to null-terminated strings that represent the argument list available to the executed program.

The first argument, by convention, should point to the filename associated with the file being executed.
The list of arguments must be terminated by a `null pointer`, and, since these are variadic functions, this pointer must be `cast (char *) NULL`.

glibc의 execl의 코드를 살펴보면 이유를 알 수 있습니다.
execl 함수는 argc의 갯수를 알기 위해 NULL을 사용합니다.

-> glibc/posix/execl.c

/* Execute PATH with all arguments after PATH until
   a NULL pointer and environment from `environ'.  */
int
execl (const char *path, const char *arg, ...)
{
  ptrdiff_t argc;
  va_list ap;
  va_start (ap, arg);
  for (argc = 1; va_arg (ap, const char *); argc++)
    {
      if (argc == INT_MAX)
	{
	  va_end (ap);
	  errno = E2BIG;
	  return -1;
	}
    }
  va_end (ap);
....

결론적으로, NULL != 0 이라는 것을 항상 인지하고 서로 혼용해서 사용하는 것은 피해야 할 것 같네요.

jooojub.

  1. GNU C++ = (GNUG __= GNUC__&&__cplusplus) 

  2. C++ = (__cplusplus) 

gcc attribute: format, format_arg

The format attribute specifies that a function takes printf, scanf, strftime or strfmon style arguments which should be type-checked against a format string. Continue reading

gcc options: -Wformat

Published on November 28, 2020

gcc builtin: choose_expr

Published on September 15, 2019