Requires :

  • compiler: gcc 2.8 later

만약 compile option 없이 char를 사용한다면, signed 일까요 unsigned?
결과는 architecture에 따라, 그리고 compiler version에 따라 다를 것입니다.

많은 사람들은 대부분 charsigned char라고 생각하며 코드를 작성합니다.
그러나 이건 architecture 또는 compiler option에 따라 다르기 때문에 위험한 코드입니다.
특히 architecture independent code를 작성해야 한다면 절대로 피해야 하는 코딩 습관입니다.

gcc compiler에는 charsigned 또는 unsigned로 다룰지에 대한 gcc option1이 있다.

-fsigned-char, -funsigned-char, -fno-signed-char”, -fno-unsigned-char

많은 option이 있지만 결국은 두 가지 의미로 사용됩니다.

char -> signed char: -fsigned-char == -fno-unsigned-char
char -> unsigned char: -funsigned-char == -fno-signed-char

아주 심플한 gcc option이며, gcc 문서에서도 자세히 설명되어 있습니다.


gcc-7.4.0/C-Dialect-Options
-funsigned-char
Let the type char be unsigned, like unsigned char.
Each kind of machine has a default for what char should be. It is either like unsigned char by default or like signed char by default. Ideally, a portable program should always use signed char or unsigned char when it depends on the signedness of an object. But many programs have been written to use plain char and expect it to be signed, or expect it to be unsigned, depending on the machines they were written for. This option, and its inverse, let you make such a program work with the opposite default.
The type char is always a distinct type from each of signed char or unsigned char, even though its behavior is always just like one of those two.

ref. https://gcc.gnu.org/onlinedocs/gcc-7.4.0/gcc/C-Dialect-Options.html#C-Dialect-Options

이 option이 처음 등장했던 gcc version을 자세히 살펴보진 않았지만, gcc 2.8에는 이미 해당 option이 포함되어 있습니다.

-> git checkout gcc-2_8_0-release

$ cat ./gcc/toplev.c

char *lang_options[] =
{
	...
  "-fsigned-char",
  "-funsigned-char",
  "-fno-signed-char",
  "-fno-unsigned-char",
  ...

gcc release note에서는 찾아볼 수 없으나, cpp에서는 gcc 3.1에 추가된 걸로 보입니다.

-> git checkout gcc-3_1-release

$ cat ./gcc/cppinit.c

#define COMMAND_LINE_OPTIONS                                      \
....
  DEF_OPT("fsigned-char",             0,      OPT_fsigned_char)
...
  DEF_OPT("funsigned-char",           0,      OPT_funsigned_char)

option이 의도대로 잘 동작하는지 코드로 살펴봅시다.

Check with code

-> sample source code: char.c

#include <stdio.h>

int main(void) {
	char a = (1 << 8) - 1;

	printf("%d\n", a);

	return 0;
}

-> gcc version 7.4.0 –target=x86_64-linux-gnu

$ gcc -o char char.c 
$ ./char
-1

x86_64에서는 charsigned char로 취급합니다. -funsigned-char option을 추가해 봅시다.

-> gcc version 7.4.0 –target=x86_64-linux-gnu

$ gcc -funsigned-char -o char char.c 
$ ./char
255

-funisgned-char option이 추가되어 charunsigned char로 취급합니다.

각 architecture 별로 char를 어떻게 다루는지 살펴보았습니다.

-> x86_64: default signed char

...
 652:	c6 45 ff ff          	movb   $0xff,-0x1(%rbp)
 /* 
  - movsbl (Move a Sign-extended Byte): signed char
  - movzbl (Move a Zero-extended Byte): unsigned char
 */
 656:	0f be 45 ff          	movsbl -0x1(%rbp),%eax
...

-> aarch64: default unsigned char

...
 72c:	12800000 	mov	w0, #0xffffffff            	// #-1
 730:	39007fa0 	strb	w0, [x29, #31]
 /*
  - ldrsb (Load Register Signed Byte): signed char
  - ldrb (Load Register Byte): unsigned char
 */
 734:	39407fa1 	ldrb	w1, [x29, #31]
...

-> mips64: default signed char

...
 10000b20:	2402ffff 	li	v0,-1
 10000b24:	a3c20000 	sb	v0,0(s8)
 /*
  - lb (Load Byte): signed char
  - lbu (Load Byte unsigned): unsigned char
 */
 10000b28:	83c20000 	lb	v0,0(s8)
...

-> ppc: default unsigned char

 /*
  - lis (Load Immediate Shifted): signed char
  - li (Load Immediate): unsigned char
 */
 1000046c:	38 00 ff ff 	li      r0,-1
 10000470:	98 1f 00 0a 	stb     r0,10(r31)

stack overflow에서 charsigned인지 unsigned인지를 쉽게 확인할 수 있는 sample code를 찾았습니다.

#include <stdio.h>

int main(void) {
  printf("%d\n", '\x80');

	return 0;
}

-> result

$ gcc -o simple simple.c 
$ ./simple
-128

$ gcc -funsigned-char -o simple simple.c 
$ ./simple
128

거대한 project에서는 global 하게 compile option이 추가되기 때문에, 각 process 별로 compile option을 통일하는 게 일반적입니다.

즉, compile option에 의존해서 코드를 작성하는 것 보다는 signed 인지 unsigned 인지가 중요한 코드에서는 char가 아닌 명시적으로 signed char 또는 unsigned char를 이용해서 코드를 작성하는 게 좋아 보입니다.

jooojub.
  1. gcc 문서에는 gcc commands라고 명명하고 있습니다. 저는 두 용어 모두 혼용해서 사용하겠습니다 :) 

Requires :

  • compiler: gcc 3.3 later

nonnull attribute를 사용하면, function argument에 NULL을 넘기면 안되는 함수에 NULL을 사용하는 경우를 compile-time에 검출할 수 있습니다.

그러나 NULL이 implicitly(묵시적) 으로 지정된 경우에만 감지되고, 그렇지 않은 상황을 검출할 수 없는 한계가 있습니다.
이러한 한계는 이후에 다시 자세히 설명하겠습니다.

이 attribute는 -Wnonnull-Werror=nonnull compile options과 함께 사용되어야 의미가 있습니다.
-Wno-nonnull를 사용하면 애써 nonnull attribute를 사용한 의미가 없게 됩니다.

gcc-3.3 release note에서 nonnull이 처음 소개되었고, kernel이나 glibc와 같이 많은 오픈소스에서 사용되고 있습니다.


GCC 3.3 Changes
C/ObjC/C++
...
A new function attribute, nonnull, has been added which allows pointer arguments to functions to be specified as requiring a non-null value. The compiler currently uses this information to issue a warning when it detects a null value passed in such an argument slot.

ref. https://gcc.gnu.org/gcc-3.3/changes.html

자세한 내용은 gcc 문서에 친절하게 나와 있습니다.


gcc 7.3/Common-Function-Attributes/nonnull
nonnull (arg-index, …)

The nonnull attribute specifies that some function parameters should be non-null pointers. For instance, the declaration:
extern void *
    my_memcpy (void *dest, const void *src, size_t len)
    __attribute__((nonnull (1, 2)));
causes the compiler to check that, in calls to my_memcpy, arguments dest and src are non-null. If the compiler determines that a null pointer is passed in an argument slot marked as non-null, and the -Wnonnull option is enabled, a warning is issued. The compiler may also choose to make optimizations based on the knowledge that certain function arguments will never be null.

If no argument index list is given to the nonnull attribute, all pointer arguments are marked as non-null. To illustrate, the following declaration is equivalent to the previous example:
extern void *
    my_memcpy (void *dest, const void *src, size_t len)
    __attribute__((nonnull));
ref. https://gcc.gnu.org/gcc-3.3/changes.html

단순한 attribute이기 때문에 간단한 sample code로 이해할 수 있습니다.
중요한 것은 argument index list가 0-based가 아니라 1-based 라는 것입니다.

Check with code

-> sample source code: nonnull.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void __attribute__((nonnull(1, 2)))
  my_test_function(char *dest, const char *src, int len) {
	strncpy(dest, src, len);
}

int main(void) {
	my_test_function(NULL, NULL, 10);

	return 0;
}

my_test_function()에서 destsrc는 NULL이 되면 안 된다고 명시했습니다.

$ gcc -Wnonnull nonnull.c

nonnull.c: In function ‘main’:
nonnull.c:11:2: warning: null argument where non-null required
										(argument 1) [-Wnonnull]
  my_test_function(NULL, NULL, 10);
  ^~~~~~~~~~~~~~~~
nonnull.c:11:2: warning: null argument where non-null required
										(argument 2) [-Wnonnull]

compiler가 친절하게 argument 1, 2에는 NULL을 사용하면 안 된다고 warning 메시지를 줬습니다.
Compile warning은 가끔 실수로 지나칠 수 있으니, compile이 실패 발생하도록 error로 바꾸는 게 좋겠네요.

$ gcc -Werror=nonnull nonnull.c

nonnull.c: In function ‘main’:
nonnull.c:11:2: error: null argument where non-null required
										(argument 1) [-Werror=nonnull]
  my_test_function(NULL, NULL, 10);
  ^~~~~~~~~~~~~~~~
nonnull.c:11:2: error: null argument where non-null required
										(argument 2) [-Werror=nonnull]
cc1: some warnings being treated as errors

중요한 건, nonnull attribute을 사용했다고 해서, my_test_function() 함수 안에서 NULL check를 하지 않아도 된다는 뜻은 아닙니다.
이 attribute는 오직 compile-time에서 예측 가능한 상황에서만 동작합니다.
compile-time 검출의 한계이며 어찌 보면 당연한 한계 입니다.

-> Can not detect for the following situations:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void __attribute__((nonnull(1, 2)))
		my_test_function(char *dest, const char *src, int len) {
	strncpy(dest, src, len);
}

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

	my_test_function(a, "test", 5);

	return 0;
}
$ gcc -Werror=nonnull nonnull.c
/* build success! */

위 상황은 compiler 입장에서 충분히 예측 가능한 NULL인데도 불구하고 검출을 못 하네요..

만약 nonnull attribute을 사용할 때, argument index list를 주지 않으면, 모든 argument에 대해서 nonnull check를 하게 됩니다.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void __attribute__((nonnull))
		my_test_function(char *dest, const char *src, int len) {
	strncpy(dest, src, len);
}

int main(void) {
	my_test_function(NULL, NULL, 5);

	return 0;
}
$ gcc -Werror=nonnull nonnull.c

nonnull.c: In function ‘main’:
nonnull.c:11:2: error: null argument where non-null required
										(argument 1) [-Werror=nonnull]
  my_test_function(NULL, NULL, 10);
  ^~~~~~~~~~~~~~~~
nonnull.c:11:2: error: null argument where non-null required
										(argument 2) [-Werror=nonnull]
cc1: some warnings being treated as errors

비록 검출의 한계가 있지만, user의 실수를 compile-time에 검출할 가능성이 있다는 것은 큰 이점으로 보입니다.
Compiler attribute는 Runtime overhead도 없기 때문에, 이러한 attribute을 많이 활용할 계획입니다.

jooojub.

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) 

Requires :

  • compiler: gcc 3.3.1 later

systemd 코드에서, ‘attribute’ keyword 들을 많이 볼 수 있습니다. 그중에서 security coding에 많은 도움이 될 수 있는 ‘cleanup’ keyword에 대해서 살펴보겠습니다.

‘cleanup’ keyword에 대해서 gcc 문서에서는 다음과 같이 설명합니다.


cleanup
The cleanup attribute runs a function when the variable goes out of scope. This attribute can only be applied to auto function scope variables; it may not be applied to parameters or variables with static storage duration. The function must take one parameter, a pointer to a type compatible with the variable. The return value of the function (if any) is ignored.

If -fexceptions is enabled, then cleanup_function is run during the stack unwinding that happens during the processing of the exception. Note that the cleanup attribute does not allow the exception to be caught, only to perform an action. It is undefined what happens if cleanup_function does not return normally.

ref. https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html

다음 설명이 중요한 요점 같네요.

The cleanup attribute runs a function when the variable goes out of scope

즉, 잘 사용한다면, pair를 맞춰야 하는 코드 {malloc/free, open/close, …} 관리가 편해, leak이 발생하는 상황을 막을 수 있어 보입니다.

Check with code

-> sample source code: simple

#include <stdio.h>

void auto_function(int *arg) {
	printf("%s: called by __clean_up__: %d\n", __func__, *arg);

	return;
}

int main(int argc, char **argv) {
	__attribute__ ((__cleanup__(auto_function))) int val = 5;

	return 0;
}

-> result

auto_function: called by __clean_up__: 5

-> assembly: x86_64 AT&T

00000000004005c1 <main>:
  ...
  4005ec:	48 8d 45 e4             lea    -0x1c(%rbp),%rax
  4005f0:	48 89 c7                mov    %rax,%rdi
  4005f3:	e8 9e ff ff ff          callq  400596 <auto_function>
  ...

auto_function(& val)이 자동으로 호출되는 것을 볼 수 있습니다.
이곳에 free() 또는 close()를 추가하게 되면 신경 쓰지 않아도 자동으로 호출되게 할 수 있습니다.

-> sample source code: fclose

...
void fclosep(FILE **f) {
	fclose(f);
}

int main(int argc, char **argv) {
	__attribute__ ((__cleanup__(fclosep))) FILE *f = fopen(name, "r");
    ...
    /* We don't need to call fclose(f) manually */

	return 0;
}

__cleanup__ attribute에 의해 호출되는 함수의 시점이 중요합니다.
문서에는 다음과 같이 명시되어 있습니다.
The cleanup attribute runs a function when the variable goes out of scope
확인해봅시다.

-> sample source code: scope

#include <stdio.h>
#include <stdlib.h>

void freep(void *p) {
	free(*(void **) p);
	printf("value freed\n");
}

int main(int argc, char **argv) {
	{
		__attribute__ ((__cleanup__(freep))) void *p = malloc(10);
	}

	printf("before return\n");

	return 0;
}

-> result

value freed
before return

-> assembly: x86_64 AT&T

  ...
  40066a:	31 c0                	xor    %eax,%eax
	{
		__attribute__ ((__cleanup__(freep))) void *p = malloc(10);
  40066c:	bf 0a 00 00 00       	mov    $0xa,%edi
  400671:	e8 9a fe ff ff       	callq  400510 <malloc@plt>
  400676:	48 89 45 f0          	mov    %rax,-0x10(%rbp)
  40067a:	48 8d 45 f0          	lea    -0x10(%rbp),%rax
  40067e:	48 89 c7             	mov    %rax,%rdi
     # The function is called by the life cycle of the variable
  400681:	e8 a0 ff ff ff       	callq  400626 <freep>
	}

	printf("before return\n");
  400686:	bf 40 07 40 00       	mov    $0x400740,%edi
  40068b:	e8 50 fe ff ff       	callq  4004e0 <puts@plt>

	return 0;
  400690:	b8 00 00 00 00       	mov    $0x0,%eax
}

즉, 다음과 같은 실수를 하지 않도록 조심해야 합니다.

-> sample source code: be careful with scope

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void freep(void *p) {
	free(*(void **) p);
}

void *new_buffer(int size) {
	__attribute__ ((__cleanup__(freep))) int *p = malloc(size);

	return p;
}

int main(int argc, char **argv) {
	char *value = NULL;

	value = (char *)new_buffer(5);
	strncpy(value, "test", 5);

	printf("value: %s\n", value);

	return 0;
}
Segmentation fault (core dumped)
jooojub.