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.

gcc options: -Wformat

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. Continue reading

gcc builtin: choose_expr

Published on September 15, 2019

gcc builtin: alloca

Published on August 04, 2019