Requires :

  • compiler: gcc 2.8.0 later

Prior knowledge:

이전 post에서 다뤘듯이 gcc option의 -Wformat를 이용하면 printf, scanf와 같이 arugment를 다루는 glibc 함수 사용 시 format 관련 실수를 compile-time에 확인할 수 있어 많은 이점이 있다고 하였습니다.

그렇다면 printf, scanf와 같이 glibc 함수가 아닌 다른 함수에서는 -Wformat의 도움을 받을 수 없을까요?
다음과 같은 코드를 작성했다고 가정해 보겠습니다.

-> sample source code: format

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

int report(const char *fmt, ...) {
	va_list arg;

	va_start(arg, fmt);
	vfprintf(stderr, fmt, arg);
	va_end(arg);

	return 0;
}

int main(void) {
	report("%s\n", "report!", "excess");

	return 0;
}

-> Compile with -Wformat

$ gcc -o format format.c -Wformat
...

report의 format과 argument의 개수가 맞지 않음에도 아무런 warning 없이 컴파일 성공하였습니다.
그렇다면 제가 작성한 report() 함수의 -Wformat 지원을 위해서는 어떻게 해야 할까요?
gcc 문서를 잘 찾아보면, format이라는 attribute를 찾을 수 있습니다.


format (archetype, string-index, first-to-check)
The format attribute specifies that a function takes printf, scanf, strftime or strfmon style arguments which should be type-checked against a format string.
...
ref. https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html

설명이 좀 길어서 다 생략했습니다…
결론은 자신이 원하는 함수에도 -Wformat을 사용할 수 있다는 설명입니다.

그럼 위 예제 코드에서 attribute foramt을 추가해 compile-time에 -Wformat 검출 가능한 코드로 변경해 보겠습니다.

-> sample source code: format.c

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

__attribute__((format(printf, 1, 2)))
int report(const char *fmt, ...) {
	va_list arg;

	va_start(arg, fmt);
	vfprintf(stderr, fmt, arg);
	va_end(arg);

	return 0;
}

int main(void) {
	report("%s\n", "report!", "excess");

	return 0;
}

-> Compile with -Wformat

$ gcc -o format format.c -Wformat

format.c: In function ‘main’:
format.c:16:9: warning: too many arguments for format [-Wformat-extra-args]
   16 |  report("%s\n", "report!", "excess");
      |         ^~~~~~

제가 작성한 report 함수에 대해서도 -Wformat option에 의해 warning으로 검출되었습니다.

사용법은 간단합니다.

__attribute__((format(archetype, string-index, first-to-check)))
  • archetype에는 printf, scanf, strftime을 사용할 수 있습니다. target에 따라 glibc의 gnu_를 붙일 수도 있고, MinGW의 ms_를 붙일수도 있다고 합니다.
  • string-index에는 format argument의 위치를 지정하면 됩니다. 중요한 점은 index가 0이 아닌 1부터 시작한다는 것입니다. 예제의 int report(const char *fmt, ...) 함수에서는 fmt가 첫 번째 argument이기 때문에 1을 지정했습니다.
  • first-to-check에는 argument에 위치를 지정하면 됩니다.

참고로
vprintf와 같이 argument(like …)가 없는 함수에는 first-to-check를 0으로 지정하면 됩니다.

-> sample source code: format_vprintf.c

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

__attribute__((format(printf, 2, 0)))
int _va_report(int n, const char *fmt, va_list ap) {
    return vfprintf(stdout, fmt, ap);
}

int report(int n, ...) {
	va_list arg;

	va_start(arg, n);
  _va_report(n, "%s\0\n", arg);
	va_end(arg);

	return 0;
}

int main(void) {
  report(2, "1");
	return 0;
}

-> Compile with -Wformat

$ gcc -o format_vprintf format_vprintf.c -Wformat

format_vprintf.c: In function ‘report’:
format_vprintf.c:13:22: warning: embedded ‘\0in format [-Wformat-contains-nul]
   13 |     _va_report(n, "%s\0\n", arg);
      |                      ^~

이 경우에는 format에 대해서만 check 합니다.
즉 아래와 같은 코드는 검출이 안됩니다.

-> -Wformat-extra-args is not detected

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

__attribute__((format(printf, 2, 0)))
int _va_report(int n, const char *fmt, va_list ap) {
    return vfprintf(stdout, fmt, ap);
}

int report(int n, ...) {
	va_list arg;

	va_start(arg, n);
  _va_report(n, "%s\n", arg);
	va_end(arg);

	return 0;
}

int main(void) {
  report(3, "1", "2", "3");
	return 0;
}

-> Compile with -Wformat-extra-args, but not detected

$ gcc -o format_vprintf format_vprintf.c -Wformat -Wformat-extra-args
...

strtime도 마찬가지입니다. first-to-check를 0으로 설정해서 사용합니다.

-> sample source code: strftime.c

#include <stdio.h>
#include <time.h>

__attribute__((format(strftime, 1, 0)))
void get_time(const char *fmt, struct tm * tptr) {
    char buf[64];

    strftime(buf, sizeof(buf), fmt, tptr);
    puts(buf);
}

int main(void) {
    time_t tmp;
    struct tm *tptr;

    tmp = time(NULL);
    tptr = localtime(&tmp);

    get_time("%A, %b %d.\nTime: %r..%i?", tptr);

    return 0;
}

-> Compile with -Wformat

$ gcc -o strftime strftime.c -Wformat

strftime.c: In function ‘main’:
strftime.c:19:38: warning: unknown conversion type character ‘i’ in format [-Wformat=]
   19 |     get_time("%A, %b %d.\nTime: %r..%i?", tptr);
      |                                      ^

함수 선언에 attribute를 설정해도 됩니다.

int report(const char *fmt, ...) __attribute__((format(printf, 1, 2)));

이 attribute는 gcc 2.8.0 release 때부터 이미 포함되어 있었던 오래된 attribute입니다.

-> gcc 2.8.0 release: c-common.c

static void
init_attributes ()
{
  ...
  add_attribute (A_FORMAT, "format", 3, 3, 1);
  add_attribute (A_FORMAT_ARG, "format_arg", 1, 1, 1);
  ...
}

void
decl_attributes (node, attributes, prefix_attributes)
     tree node, attributes, prefix_attributes;
{
  ...
  case A_FORMAT:
  {
    ...
    if (TREE_CODE (format_type) == IDENTIFIER_NODE
		&& (!strcmp (IDENTIFIER_POINTER (format_type), "printf")
		    || !strcmp (IDENTIFIER_POINTER (format_type),
				"__printf__")))
	      is_scan = 0;
	    else if (TREE_CODE (format_type) == IDENTIFIER_NODE
		     && (!strcmp (IDENTIFIER_POINTER (format_type), "scanf")
			 || !strcmp (IDENTIFIER_POINTER (format_type),
				     "__scanf__")))
	      is_scan = 1;
	    else if (TREE_CODE (format_type) == IDENTIFIER_NODE)
	      {
		error ("`%s' is an unrecognized format function type",
		       IDENTIFIER_POINTER (format_type));
		continue;
	      }
	    else
	      {
		error ("unrecognized format specifier");
		continue;
	      }
  ...
}

이 당시에는 archetype이 printfscanf 만 지원됐었습니다.

strftimegcc-2.9에 추가되었습니다.

-> release/gcc-2.95: cat This-change-is-from-an-idea-suggested-by-Arthur-Davi.patch

+	* c-common.c (decl_attributes, record_function_format,
+	check_format_info, init_function_format_info):
+	Add support for strftime format checking.
...
+		char *p = IDENTIFIER_POINTER (format_type_id);
+		
+		if (!strcmp (p, "printf") || !strcmp (p, "__printf__"))
+		  format_type = printf_format_type;
+		else if (!strcmp (p, "scanf") || !strcmp (p, "__scanf__"))
+		  format_type = scanf_format_type;
+		else if (!strcmp (p, "strftime")
+			 || !strcmp (p, "__strftime__"))
+		  format_type = strftime_format_type;
+		else
+		  {
+		    error ("`%s' is an unrecognized format function type", p);
+		    continue;
+		  }

format_arg

gcc 코드와 document를 살펴보면 format_arg라는 attribute도 볼 수 있습니다.
이 attribute도 format attribute와 같은 역할을 합니다.
다른 점은 format만 있는 함수에서 사용합니다. 예를 들어 다음과 같이 format string에 prefix를 추가하는 함수를 만들 수 있습니다.
이 경우에는 format_arg를 사용하면 됩니다.

-> sample source code: format_arg.c

#include <stdio.h>

__attribute__((format_arg(3)))
char *debug_format(char *buf, size_t len, char *fmt) {
    snprintf(buf, len, "[debug] %s", fmt);

    return buf;
}

int main(void) {
    char buf[32];

    printf(debug_format(buf, sizeof(buf), "%s\n"), "arg1", "excess");

    return 0;
}

-> Compile with -Wformat

$ gcc -o format_arg format_arg.c -Wformat

format_arg.c: In function ‘main’:
format_arg.c:13:43: warning: too many arguments for format [-Wformat-extra-args]
   13 |     printf(debug_format(buf, sizeof(buf), "%s\n"), "arg1", "excess");
      |                                           ^~~~~~

개인적으로 -Wformat은 자칫 run-time에 발생할 수 있는 문제를 compile-time에 미리 알 수 있는 강력한 option이라고 생각합니다.
그래서 전 코드 작성할 때 가능하면 -Werror=format을 추가해 compile error를 발생시키도록 하고 있습니다.
하지만 사실 코드를 작성하다 보면, 제가 작성한 코드에 대해서 -Wformat 관련 attribute 추가하는 것을 잊어버릴 때가 많이 있습니다.

이럴 때를 위해 gcc는 -Wmissing-format-attribute이라는 option도 제공해 줍니다.
-Wmissing-format-attribute에 대해서도 추가 post를 작성할 계획입니다.

결론은 -Wformat때와 동일합니다. compile-time에 예측 가능한 문제들을 미리 해결할 수 있도록 format attribute를 많이 활용하길 권장 드립니다.

jooojub.

Requires :

  • compiler: gcc 2.8 later
  • glibc 2.2 later

printf 와 같이 format을 사용하는 glibc 함수들을 사용할 때, 잘못된 format을 사용할 경우 compile warning이 발생하는 것을 종종 보았을 것입니다.

-> If format is used incorrectly, a compile warning occurs.

$ cat ./format_warning.c
#include <stdio.h>

int main(void) {
	printf("%d, %d\n", (int)1, (unsigned long)2);

	return 0;
}
$ gcc -o format_warning format_warning.c
format_warning.c: In function ‘main’:
format_warning.c:4:15: warning: format ‘%d’ expects argument \
  of type ‘int’, but argument 3 has type ‘long unsigned int’ [-Wformat=]
  printf("%d, %d\n", (int)1, (unsigned long)2);
              ~^             ~~~~~~~~~~~~~~~~

compiler라 unsigned long을 왜 %d로 출력하려고 하냐고 친절하게 warning으로 알려줍니다.
argument type 뿐만 아니라 argument 갯수가 잘못된 상황도 warning으로 보여줍니다.

-> Even if the number of arguments is incorrectly set.

$ cat ./format_warning2.c
#include <stdio.h>

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

	return 0;
}
$ gcc -o format_warning2 format_warning2.c
format_warning2.c: In function ‘main’:
format_warning2.c:4:15: warning: format ‘%d’ expects a matching ‘int’ argument [-Wformat=]
    printf("%d, %d\n", 1);

-Wformat에 대해서 우선 gcc 문서를 살펴 보겠습니다.


gcc-7.5.0/Warning-Options
-Wformat
-Wformat=n
Check calls to printf and scanf, etc.,
to make sure that the arguments supplied have types appropriate to the format string specified, and that the conversions specified in the format string make sense.
This includes standard functions, and others specified by format attributes (see Function Attributes),
in the printf, scanf, strftime and strfmon (an X/Open extension, not in the C standard) families (or other target-specific families).
Which functions are checked without format attributes having been specified depends on the standard version selected, and such checks of functions without the attribute specified are disabled by -ffreestanding or -fno-builtin.

The formats are checked against the format features supported by GNU libc version 2.2.
These include all ISO C90 and C99 features, as well as features from the Single Unix Specification and some BSD and GNU extensions.
Other library implementations may not support all these features; GCC does not support warning about features that go beyond a particular library’s limitations.
However, if -Wpedantic is used with -Wformat, warnings are given about format features not in the selected standard version (but not for strfmon formats, since those are not in any version of the C standard).

ref. https://gcc.gnu.org/onlinedocs/gcc-7.5.0/gcc/Warning-Options.html#Warning-Options

glibc 2.2 이후의 몇몇 standard function에 대해서 지원한다고 나와 있습니다.
그럼 glibc의 어떤 함수들이 -Wformat을 지원할까요?
gcc 코드를 살펴보면 지원되는 함수 종류들을 볼 수 있습니다.

-> git checkout releases/gcc-3.0

$ cat ./gcc/c-format.c
...
void
init_function_format_info ()
{
  if (flag_hosted)
    {
      /* Functions from ISO/IEC 9899:1990.  */
      record_function_format (get_identifier ("printf"), NULL_TREE,
			      printf_format_type, 1, 2);
      record_function_format (get_identifier ("__builtin_printf"), NULL_TREE,
			      printf_format_type, 1, 2);
      record_function_format (get_identifier ("fprintf"), NULL_TREE,
			      printf_format_type, 2, 3);
      record_function_format (get_identifier ("__builtin_fprintf"), NULL_TREE,
			      printf_format_type, 2, 3);
      record_function_format (get_identifier ("sprintf"), NULL_TREE,
			      printf_format_type, 2, 3);
      record_function_format (get_identifier ("scanf"), NULL_TREE,
			      scanf_format_type, 1, 2);
      record_function_format (get_identifier ("fscanf"), NULL_TREE,
			      scanf_format_type, 2, 3);
      record_function_format (get_identifier ("sscanf"), NULL_TREE,
			      scanf_format_type, 2, 3);
      record_function_format (get_identifier ("vprintf"), NULL_TREE,
			      printf_format_type, 1, 0);
      record_function_format (get_identifier ("vfprintf"), NULL_TREE,
			      printf_format_type, 2, 0);
      record_function_format (get_identifier ("vsprintf"), NULL_TREE,
			      printf_format_type, 2, 0);
      record_function_format (get_identifier ("strftime"), NULL_TREE,
			      strftime_format_type, 3, 0);
    }

  if (flag_hosted && flag_isoc99)
    {
      /* ISO C99 adds the snprintf and vscanf family functions.  */
      record_function_format (get_identifier ("snprintf"), NULL_TREE,
			      printf_format_type, 3, 4);
      record_function_format (get_identifier ("vsnprintf"), NULL_TREE,
			      printf_format_type, 3, 0);
      record_function_format (get_identifier ("vscanf"), NULL_TREE,
			      scanf_format_type, 1, 0);
      record_function_format (get_identifier ("vfscanf"), NULL_TREE,
			      scanf_format_type, 2, 0);
      record_function_format (get_identifier ("vsscanf"), NULL_TREE,
			      scanf_format_type, 2, 0);
    }
  ...

gcc 코드는 너무 복잡해서 자세히는 이해 못 했지만, 크게 printf, scanf, strftime로 분류하는 것으로 보입니다.

그렇다면 glibc standard 함수가 아닌 va_arg()를 이용해 정의한 자신만의 함수도 -Wformat의 검출 대상으로 추가하는 방법은 없을까요?
위 문서에서도 살짝 언급했듯 __attribute__((format))를 사용하면 됩니다.
__attribute__((format))에 대해서는 다른 post에서 자세히 설명하겠습니다.
(사실 __attribute__((format))를 소개하고 싶어서 -Wformat를 먼저 설명하고 있는 것입니다…)

-Wformat 종류의 option은 많은 종류가 있습니다. 이것들에 대해서 간략하게 확인해봅시다.
gcc-7.5.0 기준으로 알아보겠습니다.

-Wformat-contains-nul

format에 NUL string(‘\0’)이 포함되어 있는지를 검사합니다.

-> sample source code: format_contains_nul.c

#include <stdio.h>

int main(void) {
    char buf[16];
    sprintf(buf, "%s\0", "test");

    return 0;
}

-> compile

$ gcc -o format_contains_nul format_contains_nul.c -Wformat-contains-nul

-> output

format_contains_nul.c: In function ‘main’:
format_contains_nul.c:5:21: warning: embedded ‘\0in format [-Wformat-contains-nul]
    5 |     sprintf(buf, "%s\0", "test");
      |                     ^~

-Wformat-extra-args

format의 specifiers 보다 argument 개수가 많을 경우를 검사합니다.

-> sample source code: format_extra_args.c

#include <stdio.h>

int main(void) {
    printf("excess arguments: %s", __LINE__, "what");

    return 0;
}

-> compile

$ gcc -o format_extra_args format_extra_args.c -Wformat-extra-args

-> output

format_extra_args.c: In function ‘main’:
...
format_extra_args.c:4:12: warning: too many arguments for format [-Wformat-extra-args]
    4 |     printf("excess arguments: %s", __LINE__, "what");

-Wformat-overflow

sprintf나 vsprintf와 같이 format에 따라 destination buffer에 쓰는 경우, buffer size 보다 큰 값이 들어와 overflow가 발생하는지를 검사합니다.

-> sample source code: format_overflow.c

#include <stdio.h>

int main(void) {
    char buf[10];
    sprintf(buf, "overflow %s", "ho!");

    return 0;
}

-> compile

$ gcc -o format_overflow format_overflow.c -Wformat-overflow

-> output

format_overflow.c: In function ‘main’:
format_overflow.c:5:28: warning: ‘%s’ directive writing 3 bytes into a region of size 1 [-Wformat-overflow=]
    5 |     sprintf(buf, "overflow %s", "ho!");
      |                            ^~   ~~~~~
format_overflow.c:5:5: note: ‘sprintf’ output 13 bytes into a destination of size 10
    5 |     sprintf(buf, "overflow %s", "ho!");
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

만약 compile time에 예측하기 힘든 상황에서는 어떻게 될까요?

-> sample source code: format_overflow.c

#include <stdio.h>

int main(int argc, const char *argv[]) {
    char buf[10];
    sprintf(buf, "overflow %s", argv[0]);

    return 0;
}

-> compile

$ gcc -o format_overflow format_overflow.c -Wformat-overflow

-> output

...

사실 argv 값에 상관없이 format에 의해 이미 buffer overflow가 발생하는 코드지만, -Wformat-overflow가 overflow를 감지하지 못하고 있습니다.

이럴 때는 Wformat-overflow의 level을 지정하면 됩니다.

-> compile with -Wformat-overflow=2

$  gcc -o format_overflow format_overflow.c -Wformat-overflow=2

-> output

format_overflow.c: In function ‘main’:
format_overflow.c:5:30: warning: ‘sprintf’ may write a terminating nul past the end of the destination [-Wformat-overflow=]
    5 |     sprintf(buf, "overflow %s", argv[0]);
      |                              ^
format_overflow.c:5:5: note: ‘sprintf’ output 10 or more bytes (assuming 11) into a destination of size 10
    5 |     sprintf(buf, "overflow %s", argv[0]);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

-Wformat-zero-length

format이 zero length일 경우를 검사합니다.

-> sample source code: format_zero_length.c

#include <stdio.h>

int main(void) {
    printf("","zero-length");
    return 0;
}

-> compile

$ gcc -o format_zero_length format_zero_length.c -Wformat-zero-length

-> output

format_zero_length.c: In function ‘main’:
format_zero_length.c:4:12: warning: zero-length gnu_printf format string [-Wformat-zero-length]
    4 |     printf("","zero-length");
      |            ^~

사실상 이런 코드는 작성될 리가 없겠죠? 하지만 이런 코드는 의외로 자주 발생됩니다.

    const char fmt[] = "";
    printf(fmt,"zero-length");

-Wformat-nonliteral

format이 string literal 인지 검사합니다.

-> sample source code: format_nonliteral.c

#include <stdio.h>

int main(void) {
    char fmt[] = "%s";
    printf(fmt,"test");

    return 0;
}

-> compile

$ gcc -o format_nonliteral format_nonliteral.c -Wformat-nonliteral

-> output

format_nonliteral.c: In function ‘main’:
format_nonliteral.c:5:12: warning: format not a string literal, argument types not checked [-Wformat-nonliteral]
    5 |     printf(fmt,"test");
      |            ^~~

fmt를 const char로 지정할 경우 이 warning은 사라지게 됩니다.

- char fmt[] = "%s";
+ const char fmt[] = "%s";

하지만 경우에 따라 fmt를 dynamic 하게 변경해야 하는 코드를 작성해야 하는 경우도 있습니다.

#include <stdio.h>

char* get_format(int value) {
    if (value == 1)
        return "%d";
    else
        return "%s";
}

int main(void) {
    printf(get_format(1),"test");

    return 0;
}

그래서 경우에 따라 이 option을 무시하는 코드를 추가하기도 합니다.

#pragma GCC diagnostic ignored "-Wformat-nonliteral"
...
#pragma GCC diagnostic warning "-Wformat-nonliteral"

-Wformat-security

format이 string iternal이 아니면서, argument가 없을 경우를 감지합니다.

-> sample source code: format_security.c

#include <stdio.h>

int main(void) {
    char *fmt = "%s";
    printf(fmt);

    return 0;
}

-> compile

$ gcc -o format_security format_security.c -Wformat-security

-> output

format_security.c: In function ‘main’:
format_security.c:5:5: warning: format not a string literal and no format arguments [-Wformat-security]
    5 |     printf(fmt);
      |     ^~~~~~

이것을 감지하는 이유는 무엇일까요? option 이름이 security인 이유가 있습니다.
non-iternal string일 경우 format을 %n으로 변경 가능하며, stack overflow 등을 통해서 arugment를 조작 가능하기 때문에 Format String Bug Exploration 가능한 코드가 됩니다.
즉 security hole이 있는 코드가 됩니다.
Format String Bug Exploration에 대해서는 기회가 된다면 post로 다뤄볼 계획입니다.

-Wformat-signedness

이름 그대로 format이 signed인데 argument를 unsigned로 사용했을 경우, 또는 그 반대의 경우를 감지합니다.

-> sample source code: format_signedness

#include <stdio.h>

int main(void) {
    unsigned int a = 10;
    signed int b = -10;

    printf("%d %u", a, b);

    return 0;
}

-> compile

$ gcc -o format_signedness format_signedness.c -Wformat-signedness

-> output

format_signedness.c: In function ‘main’:
format_signedness.c:7:14: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘unsigned int’ [-Wformat=]
    7 |     printf("%d %u", a, b);
      |             ~^      ~
      |              |      |
      |              int    unsigned int
      |             %d
format_signedness.c:7:17: warning: format ‘%u’ expects argument of type ‘unsigned int’, but argument 3 has type ‘int’ [-Wformat=]
    7 |     printf("%d %u", a, b);
      |                ~^      ~
      |                 |      |
      |                 |      int
      |                 unsigned int
      |                %u

-Wformat-truncation

snprintf와 같이 length를 지정하는 function에 의해 결과가 잘린다는 것을 감지하고 경고해 줍니다.

-> sample source code: format_truncation

#include <stdio.h>

int main(void) {
    char buf[10];
    snprintf(buf, sizeof(buf), "%s", "string truncation");

    return 0;
}

-> compile

$ gcc -o format_truncation format_truncation.c -Wformat-truncation

-> output

format_truncation.c: In function ‘main’:
format_truncation.c:5:33: warning: ‘%s’ directive output truncated writing 17 bytes into a region of size 10 [-Wformat-truncation=]
    5 |     snprintf(buf, sizeof(buf), "%s", "string truncation");
      |                                 ^~   ~~~~~~~~~~~~~~~~~~~
format_truncation.c:5:5: note: ‘snprintf’ output 18 bytes into a destination of size 10
    5 |     snprintf(buf, sizeof(buf), "%s", "string truncation");
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

위와 같은 문제들은 사소해 보일 수도 있지만, 상황에 따라 runtime에 stack overflow가 발생할 수도 있으며, security hole이 될 수도 있습니다.
프로젝트가 커질수록 이러한 문제들을 Compile time에 인지하고 미리 수정 가능하다는 것이 큰 이점으로 작용할 수 있습니다.
따라서, 최대한 compile option에 -Wformat 혹은 -Werror=format을 추가하는 것을 권장 드립니다.

jooojub.

Requires:

  • compiler: gcc 3.1 later

이번은 gcc builtin 중 __builtin_choose_expr에 대해서 살펴봅시다.
이 built-in function은 C++에는 없고 C에만 존재합니다.

마치 C의 3-way operator(? : operator)처럼 동작합니다.
그러나 run-time이 아니라 compile-time에 동작한다는 것을 명심해야 합니다.

이 built-in은 gcc-3.1에 ‘Aldy Hernandez’ patch를 통해 추가되었습니다.

gcc 문서의 설명을 확인해보겠습니다.


Built-in Function: type __builtin_choose_expr (const_exp, exp1, exp2)
You can use the built-in function __builtin_choose_expr to evaluate code depending on the value of a constant expression. This built-in function returns exp1 if const_exp, which is an integer constant expression, is nonzero. Otherwise it returns exp2.

This built-in function is analogous to the ‘? :’ operator in C, except that the expression returned has its type unaltered by promotion rules. Also, the built-in function does not evaluate the expression that is not chosen. For example, if const_exp evaluates to true, exp2 is not evaluated even if it has side effects...

ref. https://gcc.gnu.org/onlinedocs/gcc-7.4.0/gcc/Other-Builtins.html#Other-Builtins

gcc code를 살펴보면 이 builtin function이 어떻게 동작하는지에 대해 쉽게 이해하실 수 있을 겁니다.
gcc 7.4의 코드를 살펴보겠습니다.

-> git checkout gcc-7_4_0-release

$ cat ./gcc/c/c-parser.c

	...
	case RID_CHOOSE_EXPR:
	  {
	    ...
	    e1_p = &(*cexpr_list)[0];
	    e2_p = &(*cexpr_list)[1];
	    e3_p = &(*cexpr_list)[2];

	    c = e1_p->value;
	    mark_exp_read (e2_p->value);
	    mark_exp_read (e3_p->value);
	    if (TREE_CODE (c) != INTEGER_CST
		|| !INTEGRAL_TYPE_P (TREE_TYPE (c)))
	      error_at (loc,
			"first argument to %<__builtin_choose_expr%> not"
			" a constant");
	    constant_expression_warning (c);
	    expr = integer_zerop (c) ? *e3_p : *e2_p;
	    set_c_expr_source_range (&expr, loc, close_paren_loc);
		...

코드들 자세히 보시면 3-way ooperator로 구현이 되었다는 것을 보실 수 있습니다.

expr = integer_zerop (c) ? *e3_p : *e2_p;
-> 
	const_exp ? exp1 : exp2

Don’t confuse gcc builtin as a function called at compile time

첫 번째 argument는 const_exp 이어야 합니다.
다시 말해, variable이 compile time에 정해진다고 해도 argument로 사용할 수 없다는 뜻입니다.

-> can we use the variables for the first argument?

#include <stdio.h>

int main(void) {
	const int cond = 1;
	char *boolean = __builtin_choose_expr(cond, "true", "false");

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

	return 0;
}

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

$ gcc -o cond_expr cond_expr.c

cond_expr.c: In function ‘main’:
cond_expr.c:5:18: error: first argument to ‘__builtin_choose_expr’ not a constant
  char *boolean = __builtin_choose_expr(cond, "true", "false");

compile error가 발생하는걸 볼 수 있습니다.
즉, 다음과 같이 const expr만 사용할 수 있습니다.

-> use const expr for the first argument

#define BOOL_TO_STR(__x) \
	__builtin_choose_expr(((__x)), "true", "false")

int main(void) {
	printf("%s\n", BOOL_TO_STR(0));
	printf("%s\n", BOOL_TO_STR(1));

	return 0;
}

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

$ gcc -o bool_to_str bool_to_str.c
$ ./bool_to_str

false
true

-> assembly: x86_64 AT&T

int main(void) {
  400526:       55                      push   %rbp
  400527:       48 89 e5                mov    %rsp,%rbp
        printf("%s\n", BOOL_TO_STR(0));
  40052a:       bf d4 05 40 00          mov    $0x4005d4,%edi
  40052f:       e8 cc fe ff ff          callq  400400 <puts@plt>
        printf("%s\n", BOOL_TO_STR(1));
  400534:       bf da 05 40 00          mov    $0x4005da,%edi
  400539:       e8 c2 fe ff ff          callq  400400 <puts@plt>

        return 0;
  40053e:       b8 00 00 00 00          mov    $0x0,%eax
}
  400543:       5d                      pop    %rbp
  400544:       c3                      retq   
  400545:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  40054c:       00 00 00 
  40054f:       90                      nop
$ strings -t x bool_to_str
...
    5d4 false
    5da true

__builtin_choose_expr가 compile-time에 각각 exp1 또는 exp2로 변환 된 것을 볼 수 있습니다.

const exp만 사용할 수 있기 때문에, 불필요한 builtin function 아닌가 생각할 수도 있습니다.

그러나, 의외로 많은 활용을 할 수 있습니다.

예를 들어, 특정 bit가 설정되어 있는지 compile time에 확인하는 IS_MASKED macro를 다음과 같이 작성할 수 있습니다.

-> e.g. masked

#define IS_MASKED(__value) \
	__builtin_choose_expr(((__value) & 0x1), 1, 0)

int main(void) {
	printf("masked: %d\n", IS_MASKED(0xff));
	printf("masked: %d\n", IS_MASKED(0x02));
	printf("masked: %d\n", IS_MASKED(0x03));
}

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

$ gcc -o masked masked.c
$ ./masked

masked: 1
masked: 0
masked: 1

sizeof와 같이 활용할 수도 있습니다.
Kernel에서 eBPF code를 살펴보면, 아래와 같이 sizeof와 같이 활용한 예가 있습니다.

-> e.g. sizeof in BPF

$ cat ./include/trace/bpf_probe.h

/* cast any integer, pointer, or small struct to u64 */
#define UINTTYPE(size) \
	__typeof__(__builtin_choose_expr(size == 1,  (u8)1, \
		   __builtin_choose_expr(size == 2, (u16)2, \
		   __builtin_choose_expr(size == 4, (u32)3, \
		   __builtin_choose_expr(size == 8, (u64)4, \
					 (void)5)))))
#define __CAST_TO_U64(x) ({ \
	typeof(x) __src = (x); \
	UINTTYPE(sizeof(x)) __dst; \
	memcpy(&__dst, &__src, sizeof(__dst)); \
	(u64)__dst; })
}

이 builtin function 3-way operator와는 약간 다른 차이점이 있습니다.

3-way operator는 더욱 사이즈가 큰 type으로 type cast 되어 return 되지만, builtin functionexp1 또는 exp2 각자의 type이 return 됩니다.

-> check return type

#include <stdio.h>

#define RET_TYPE(__x) \
	__builtin_choose_expr((__x), ret_int(), ret_char())

int ret_int(void) {
	return 1;
}

char ret_char(void) {
	return 'a';
}

int main(void) {
	/* return char */
	printf("sizeof: %lu\n", sizeof(RET_TYPE(0)));
	/* return int */
	printf("sizeof: %lu\n", sizeof(RET_TYPE(1)));

	return 0;
}

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

$ gcc -o return_type return_type.c

./return_type
sizeof: 1
sizeof: 4

__builtin_types_compatible_p와 같이 사용하면 더욱 멋진 활용이 가능합니다만, 다음 post를 위해 아껴두겠습니다. :)

__builtin_choose_expr은 const expr를 return 하는 다른 build-in function과 조합해서 많이 사용합니다.
예를 들어 __builtin_types_compatible_p와 조합하면 C에서 불가능해만 보였던 type-arguments을 위한 function overloading 구현도 가능합니다.

-> functon overloading

#include <stdio.h>

#define debug(var)																					\
	printf(																										\
		__builtin_choose_expr( 																	\
			__builtin_types_compatible_p(typeof(var), int) 				\
			,"%d\n", __builtin_choose_expr(												\
				__builtin_types_compatible_p(typeof(var), char []) 	\
				,"%s\n", "0x%x\n")), var);

int main(void) {
	debug(1234);
	debug("jooojub");

	return 0;
}

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

$ gcc -o function_overloading function_overloading.c 
$ ./function_overloading

1234
jooojub

__builtin_types_compatible_p에 대해서는 다른 post에서 다루겠습니다.

또한 standard C에서도 macro를 이용해 number of arguments를 구현할 수 있습니다. 기회가 된다면 이 또한 다른 post에서 다루겠습니다.

__builtin_choose_expr는 쉽게 단순해서 이해하기 쉽지만, 유용하게 사용한다면 불필요한 코드를 줄일 수 있으므로, 많이 활용하길 바랍니다.

jooojub.

Requires:

  • compiler: gcc 2.8 later

gcc builtin 중의 하나인 __builtin_alloca에 대해서 살펴보겠습니다.

우선 명심해야 할 것은 많은 책이나 posts에서 alloca built-in을 security 관점에서 사용하지 말 것을 권고하고 있습니다. 이번 post를 통해서 왜 그런지 이유를 명확하게 이해했으면 좋겠습니다.

open-source code를 많이 살펴보셨다면, 종종 __builtin_alloca를 잧아 볼 수 있을 겁니다.
예를 들어 glibc의 strdupa macro를 다음과 같이 정의되어 있습니다.

-> strdupa macro (glibc/string/string.h)

# define strdupa(s)							   			\
  (__extension__							     		\
    ({									      			\
      const char *__old = (s);							\
      size_t __len = strlen (__old) + 1;				\
      char *__new = (char *) __builtin_alloca (__len);	\
      (char *) memcpy (__new, __old, __len);			\
    }))

__builtin_alloca는 dynamic variable를 할당할 때, heap 대신 stack에 할당되도록 설정할 수 있는 gcc built-in function입니다.
그래서 glibc의 strdupa 코드를 살펴보면 free()를 따로 호출하지 않는 것을 볼 수 있습니다.
malloc()처럼 heap에 할당되는 것이 아니기 때문에 life-time은 function block이 됩니다.

따라서, 여느 local variable과 동일하게 별도의 free() 없이도, caller function으로의 stack pointer 복원만으로도 free를 하는 효과를 발휘할 수 있습니다.

결과적으로 malloc보다 cpu-time과 memory 모두 이점이 있습니다.

__builtin_alloca은 run-time에 process에 의해 호출되는 함수가 아닌 compile-time에 gcc에 의해서 호출되는 함수입니다.
macro처럼 동작하지만, 엄연히 말해 macro는 아닙니다.
disassemble을 통해 gcc에 의해 __builtin_alloca가 다른 코드로 변환되는 것을 확인할 수 있습니다.

-> builtin_alloca was replaced to

void func(size_t n, const char* src) {
    ...
	char *val = (char *)__builtin_alloca(n);
 739:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
 73d:	48 8d 50 0f          	lea    0xf(%rax),%rdx
 741:	b8 10 00 00 00       	mov    $0x10,%eax
 746:	48 83 e8 01          	sub    $0x1,%rax
 74a:	48 01 d0             	add    %rdx,%rax
 74d:	b9 10 00 00 00       	mov    $0x10,%ecx
 752:	ba 00 00 00 00       	mov    $0x0,%edx
 757:	48 f7 f1             	div    %rcx
 75a:	48 6b c0 10          	imul   $0x10,%rax,%rax
 75e:	48 29 c4             	sub    %rax,%rsp
 761:	48 89 e0             	mov    %rsp,%rax
 764:	48 83 c0 0f          	add    $0xf,%rax
 768:	48 c1 e8 04          	shr    $0x4,%rax
 76c:	48 c1 e0 04          	shl    $0x4,%rax
 770:	48 89 45 f0          	mov    %rax,-0x10(%rbp)

	memcpy(val, src, n);
	...

callq __builtin_alloca와 같은 함수 호출 형태가 아니라는 것을 볼 수 있습니다.

이 builtin은 dynamic variable을 할당하고 local variable처럼 임시적으로 함수 안에서 사용하는 게 보장되는 곳에서 사용하고 있습니다.

gcc 문서를 살펴보겠습니다.


Built-in Function: void *__builtin_alloca (size_t size)
The __builtin_alloca function must be called at block scope. The function allocates an object size bytes large on the stack of the calling function. The object is aligned on the default stack alignment boundary for the target determined by the __BIGGEST_ALIGNMENT__ macro. The __builtin_alloca function returns a pointer to the first byte of the allocated object. The lifetime of the allocated object ends just before the calling function returns to its caller. This is so even when __builtin_alloca is called within a nested block.

ref. https://gcc.gnu.org/onlinedocs/gcc-7.4.0/gcc/Other-Builtins.html#Other-Builtins

gcc 코드를 자세히 살펴보진 않았지만, gcc 2.8에부터 존재했던 builtin입니다.

sample code를 통해 사용법을 쉽게 확인해 봅시다.

-> sample code: alloca.c

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

void func(const size_t n, const char* src) {
	char *val = (char *)__builtin_alloca(n);
	strncpy(val, src, n);
	val[n] = '\0';

	printf("val: %s\n", val);
}

int main(void) {
	func(3, "simple");

	return 0;
}

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

$ gcc -g -o alloca alloca.c
$ ./alloca
val: sim

-> assembly: x86_64 AT&T

void func(const size_t n, const char* src) {
 6fa:	55                   	push   %rbp
 6fb:	48 89 e5             	mov    %rsp,%rbp
 6fe:	48 83 ec 20          	sub    $0x20,%rsp
 702:	48 89 7d e8          	mov    %rdi,-0x18(%rbp)
 706:	48 89 75 e0          	mov    %rsi,-0x20(%rbp)
 70a:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax
 711:	00 00 
 713:	48 89 45 f8          	mov    %rax,-0x8(%rbp)
 717:	31 c0                	xor    %eax,%eax
	char *val = (char *)__builtin_alloca(n);
 719:	48 8b 45 e8          	mov    -0x18(%rbp),%rax
 71d:	48 8d 50 0f          	lea    0xf(%rax),%rdx
 721:	b8 10 00 00 00       	mov    $0x10,%eax
 726:	48 83 e8 01          	sub    $0x1,%rax
 72a:	48 01 d0             	add    %rdx,%rax
 72d:	b9 10 00 00 00       	mov    $0x10,%ecx
 732:	ba 00 00 00 00       	mov    $0x0,%edx
 737:	48 f7 f1             	div    %rcx
 73a:	48 6b c0 10          	imul   $0x10,%rax,%rax
 73e:	48 29 c4             	sub    %rax,%rsp
 741:	48 89 e0             	mov    %rsp,%rax
 744:	48 83 c0 0f          	add    $0xf,%rax
 748:	48 c1 e8 04          	shr    $0x4,%rax
 74c:	48 c1 e0 04          	shl    $0x4,%rax
 750:	48 89 45 f0          	mov    %rax,-0x10(%rbp)
	strncpy(val, src, n);
 ...
 7a6:	c9                   	leaveq 
 7a7:	c3                   	retq   

compiler에 의해 align routines 또한 추가되었지만 중요한 부분은 variable 할당을 위해 단지 stack size를 증가시켰다는 것이며, 함수가 return 할 때 free() 없이 단순히 retq 하였다는 것입니다.

gcc 문서에서는 scope에 대해 다음과 같이 설명되어 있습니다.

The lifetime of the allocated object ends just before the calling function returns to its caller

glibc 1.09에서는 __builtin_allocaalloca로 define 하여 긴 이름을 줄였습니다.

-> __builtin_alloca is defined as alloca in glibc/stdlib/alloca.h

#ifdef	__GNUC__
# define alloca(size)	__builtin_alloca (size)
#endif /* GCC.  */

그래서 만약 alloca.h를 include 한다면, 단순히 alloca라고 사용할 수 있습니다.

그러나 개인적인 의견으로는 gcc built-in은 run-time에 호출되는 함수가 아니라 compile-time에 gcc에 의해 호출되는 함수이기 때문에 이를 나타내는 __builtin이라는 prefix를 그대로 표현하게는 좋지 않을까 생각됩니다.
그러나 이것은 단순히 coding style 문제이고 만약 여러 사람이 참여하는 프로젝트라면 어떤 식으로 사용할지에 대해 약속하고 그것을 사용하면 됩니다.

C99에서 지원하는 VLA1와 같아 보이지만 VLA와는 lifetime이 다릅니다.

VLA의 litftime block scope이고 alloca는 function scope입니다.
즉, 다음과 같은 상황에서는 VLA을 사용할 수 없습니다.

-> It is impossible in VLA

#define COUNT 10

struct sample {
	unsigned char *p_x;
};

void func(void) {
	struct sample val[COUNT];
	int i;

	for (i = 0; i < COUNT; i++) {
		/* Use VLA */
		unsigned char x[i];
		/* unsigned char *x = (unsigned char *)__builtin_alloca(i); */
		memset(x, 0, i);

		val[i].p_x = x;
		/* The lifetime of x is terminated */
	}

	sumthing_to_do(val);

	return;
}

위에서 언급했던 거처럼, 많은 책과 post에서 stack에 변수를 동적 할당하는 VLA나 alloca는 security 측면에서 un-safe 하기 때문에 사용을 자제하라고 권고하고 있고, 저 또한 동의합니다.
stack에 변수를 동적 할당하는 코드는 stack overflow를 유발 할 수 있으며 이것이 security hole이 될 수 있습니다.
또한 length에 nagative number가 사용된다면 전혀 의도하지 않은 방향으로 코드가 흘러갈 수 있습니다.
그리고 Standard C에서 흔히 동적 할당에 사용되는 alloc/free 짝을 맞춰 코딩하는 방식과는 다르기 때문에 alloca를 모르는 사람들에게 혼돈을 줄 수 있습니다.
-> alloca는 standard가 아닙니다 - GNU extension 입니다…

위와 같은 이유에서, kernel 프로젝트에서는 VLA 코드 제거에 많은 노력을 하였으며 결과적으로 4.20에서 완벽하게 성공하였습니다.

ref: https://www.phoronix.com/scan.php?page=news_item&px=Linux-Kills-The-VLA

GNU document에서는 alloca의 이점에 대해서 다음과 같이 설명하였습니다:


Advantages-of-Alloca
* Using alloca wastes very little space and is very fast. (It is open-coded by the GNU C compiler.)

* Since alloca does not have separate pools for different sizes of blocks, space used for any size block can be reused for any other size. alloca does not cause memory fragmentation.

* Nonlocal exits done with longjmp (see Non-Local Exits) automatically free the space allocated with alloca when they exit through the function that called alloca. This is the most important reason to use alloca.

ref. https://www.gnu.org/software/libc/manual/html_node/Advantages-of-Alloca.html#Advantages-of-Alloca

또한, 단점은 다음과 같이 설명하였습니다.


Disadvantages-of-Alloca
* If you try to allocate more memory than the machine can provide, you don’t get a clean error message. Instead you get a fatal signal like the one you would get from an infinite recursion; probably a segmentation violation (see Program Error Signals).

* Some non-GNU systems fail to support alloca, so it is less portable. However, a slower emulation of alloca written in C is available for use on systems with this deficiency.

ref. https://www.gnu.org/software/libc/manual/html_node/Disadvantages-of-Alloca.html#Disadvantages-of-Alloca

gcc 4.7에는 __builtin_alloca_with_align가 추가되었으며 gcc 8.1에는 __builtin_alloca_with_align_and_max가 추가되었습니다.
alloca를 좀 더 safe하게 사용할 수 있게 max_size 또는 align을 설정할 수 있도록 추가되었습니다.

단순한 추가입니다. 문서에서 확인하세요.

https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html

Built-in Function: void *__builtin_alloca_with_align (size_t size, size_t alignment)
Built-in Function: void *__builtin_alloca_with_align_and_max (size_t size, size_t alignment, size_t max_size)

alloca를 더욱 safe 하게 사용할 수 있도록 gcc 7.0에서는 alloca의 max size를 compile-time에 확인할 수 있는 compile option이 추가되었습니다.
또한 코드에서 alloca가 사용되었는지도 확인할 수 있습니다.

이것들은 이후 다른 post에서 설명하겠습니다.

-Walloca-larger-than, -Walloca …

만약 alloca를 사용하게 된다면, size와 range check에 신경 써야 합니다.

참고로 alloca를 inline function에서 사용할 경우 의도하지 않은 동작이 될 수도 있습니다.
이유는 구글링해 보시면 쉽게 아실 수 있습니다 :)

jooojub.
  1. variable-length array (VLA), also called variable-sized, runtime-sized, is an array data structure whose length is determined at run time instead of at compile time 

gcc release notes link

link post

  • Check the gcc release note for each versions
    • link: https://gcc.gnu.org/releases.html
  • And also gcc online document for each versions
    • link: https://gcc.gnu.org/onlinedocs/