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.