여느 대형 프로그램과 마찬가지로 리눅스 커널에서는 코딩 편의성과 코드 가독성을 위해 매크로를 많이 활용한다. 기발해 보이는 작은 매크로(주의: 웃음 포인트)들을 옮겨 보자면,

SYSCALL_DEFINEn()

이름 그대로 시스템 호출 핸들러를 정의할 때 쓰는 매크로다. n은 매개변수 개수다.

linux/kernel/bpf/syscall.c:

SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int size)
{
        union bpf_attr attr = {};
        int err;

        if (sysctl_unprivilieged_bpf_disabled && !capable(CAP_SYS_ADMIN))
                return -EPERM;

        err = check_uarg_tail_zero(uattr, sizeof(attr)
        if (err)
                return err;
        size = min_t(u32, size, sizeof(attr));

        ...

        return err;
}

매크로 정의는 이렇다.

linux/include/linux/syscalls.h:

/*
 * __MAP - apply a macro to syscall arguments
 * __MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
 *    m(t1, a1), m(t2, a2), ..., m(tn, an)
 * The first argument must be equal to the amount of type/name
 * pairs given.  Note that this list of pairs (i.e. the arguments
 * of __MAP starting at the third one) is in the same format as
 * for SYSCALL_DEFINE<n>/COMPAT_SYSCALL_DEFINE<n>
 */
#define __MAP0(m,...)
#define __MAP1(m,t,a) m(t,a)
#define __MAP2(m,t,a,...) m(t,a), __MAP1(m,__VA_ARGS__)
#define __MAP3(m,t,a,...) m(t,a), __MAP2(m,__VA_ARGS__)
#define __MAP4(m,t,a,...) m(t,a), __MAP3(m,__VA_ARGS__)
#define __MAP5(m,t,a,...) m(t,a), __MAP4(m,__VA_ARGS__)
#define __MAP6(m,t,a,...) m(t,a), __MAP5(m,__VA_ARGS__)
#define __MAP(n,...) __MAP##n(__VA_ARGS__)

#define __SC_DECL(t, a) t a
#define __TYPE_AS(t, v) __same_type((__force t)0, v)
#define __TYPE_IS_L(t)  (__TYPE_AS(t, 0L))
#define __TYPE_IS_UL(t) (__TYPE_AS(t, 0UL))
#define __TYPE_IS_LL(t) (__TYPE_AS(t, 0LL) || __TYPE_AS(t, 0ULL))
#define __SC_LONG(t, a) __typeof(__builtin_choose_expr(__TYPE_IS_LL(t), 0LL, 0L)) a
#define __SC_CAST(t, a) (__force t) a
#define __SC_ARGS(t, a) a
#define __SC_TEST(t, a) (void)BUILD_BUG_ON_ZERO(!__TYPE_IS_LL(t) && sizeof(t) > sizeof(long))

...

#define SYSCALL_METADATA(sname, nb, ...)        /* 생략 */

...

#define SYSCALL_DEFINE0(sname)                                  \
        SYSCALL_METADATA(_##sname, 0);                          \
        asmlinkage long sys_##sname(void)

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)

#define SYSCALL_DEFINE_MAXARGS  6

#define SYSCALL_DEFINEx(x, sname, ...)                          \
        SYSCALL_METADATA(sname, x, __VA_ARGS__)                 \
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...)                                 \
        asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))       \
                __attribute__((alias(__stringify(SyS##name))));         \
        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));  \
        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));      \
        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))       \
        {                                                               \
                long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));  \
                __MAP(x,__SC_TEST,__VA_ARGS__);                         \
                __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));       \
                return ret;                                             \
        }                                                               \
        static inline SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

다채로운 정찬이다. 게다가 맵이라니…

  • __VA_ARGS__: 가변 인자 매크로
  • __force (__attribute__((force))): man sparse(1). 정적 분석기에게 주는 힌트.
  • __same_type: 내장 함수 __builtin_types_compatible_p. 호환되는 타입인가?
  • __builtin_choose_expr(): 내장 함수 __builtin_choose_expr. 삼항 연산자 ?:와 비슷.
  • __attribute__((alias())): alias 속성. 별명 붙이기.
  • __stringify(): linux/include/linux/stringify.h

    /* Indirect stringification.  Doing two levels allows the parameter to be a
     * macro iteself.  For example, compile with -DFOO=bar, __stringify(FOO)
     * converts to "bar".
     */
      
    #define __stringify_1(x...)     #x
    #define __stringify(x...)       __stringify_1(x)
    

인자 타입 검사를 하는 __SC_TEST() 매크로 정의에는 BUILD_BUG_ON_ZERO()라는 게 있다. 음… 제로의 영역에서 버그 만들기?

BUILD_BUG_ON*()

컴파일 타임 assert()다.

linux/include/linux/build_bug.h:

/*
 * Force a compilation error if condition is true, but also produce a
 * result (of value 0 and type size_t), so the expression can be used
 * e.g. in a structure initializer (or where-ever else comma expressions
 * aren't permitted).
 */
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:(-!!(e)); }))

...
#define BUILD_BUG_ON_MSG(cond, msg) compiletime_assert(!(cond), msg)

/**
 * BUILD_BUG_ON - break compile if a condition is true.
 * @condition: the condition which the compiler should know is false.
 *
 * ...
 */
#ifndef __OPTIMIZE__
#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))
#else
#define BUILD_BUG_ON(condition) \
        BUILD_BUG_ON_MSG(condition, "BUILD_BUG_ON failed: " #condition)
#endif

멋지다. 근데 오류 메시지를 찍자면 결국 compiletime_assert()를 써야 된다.

linux/include/linux/compiler.h:

#ifndef __compiletime_error
# define __compiletime_error(message)
/*
 * Sparse complains of variable sized arrays due to the temporary variable in
 * __compiletime_assert. Unfortunately we can't just expand it out to make
 * sparse see a constant array size without breaking compiletime_assert on old
 * versions of GCC (e.g. 4.2.4), so hide the array from sparse altogether.
 */
# ifndef __CHECKER__
#  define __compiletime_error_fallback(condition) \
        do { ((void)sizeof(char[1 - 2 * condition])); } while (0)
# endif
#endif
#ifndef __compiletime_error_fallback
# define __compiletime_error_fallback(condition) do { } while (0)
#endif

#ifdef __OPTIMIZE__
# define __compiletime_assert(condition, msg, prefix, suffix)           \
        do {                                                            \
                bool __cond = !(condition);                             \
                extern void prefix ## suffix(void) __compiletime_error(msg); \
                if (__cond)                                             \
                        prefix ## suffix();                             \
                __compiletime_error_fallback(__cond);                   \
        } while (0)
#else
# define __compiletime_assert(condition, msg, prefix, suffix) do { } while (0)
#endif

#define _compiletime_assert(condition, msg, prefix, suffix) \
        __compiletime_assert(condition, msg, prefix, suffix)

/**
 * compiletime_assert - break build and emit msg if condition is false
 * @condition: a compile-time constant condition to check
 * @msg:       a message to emit if condition is false
 *
 * ...
 */
#define compiletime_assert(condition, msg) \
        _compiletime_assert(condition, msg, __compiletime_assert_, __LINE__)

linux/include/linux/compiler-gcc.h:

#ifndef __CHECKER__
# define __compiletime_warning(message) __attribute__((warning(message)))
# define __compiletime_error(message) __attribute__((error(message)))
#endif /* __CHECKER__ */

#ifdef이 난무하는데, 결국은 error 속성 사용 방식이 우선이라는 얘기다.

  • __attribute__((error())): error 속성. 호출하면 컴파일 오류.

BUILD_BUG_ON()에는 컴파일 타임에 상수로 평가되는 식을 써야 된다. 근데 커널 구성 옵션 등에 따라 상수 식이 될 수도 있고 아닐 수도 있는 식이 있을 수 있다.

linux/include/linux/bug.h:

#define MAYBE_BUILD_BUG_ON(cond)                        \
        do {                                            \
                if (__builtin_constant_p((cond)))       \
                        BUILD_BUG_ON(cond);             \
                else                                    \
                        BUG_ON(cond);                   \
        } while (0)

IS_ENABLED()

#define이 있으면 #ifdef도 있다. #ifdef을 많이 쓰는 경우가 커널 구성 옵션 확인할 때다.

linux/include/net/net_namespace.h:

struct net {
        ...
#if defined(CONFIG_IP_SCTP) || defined(CONFIG_IP_SCTP_MODULE)
        struct netns_sctp       sctp;
#endif
#if defined(CONFIG_IP_DCCP) || defined(CONFIG_IP_DCCP_MODULE)
        struct netns_dccp       dccp;
#endif
        ...
} __randomize_layout;

모듈 사용 여부에 의존적인 코드마다 #ifdef ... #endif 쓰는 거, 지겨운 데다 실수하기도 쉽다. 다른 방법 없냐고 하면, 있다.

linux/net/ipv4/tcp_input.c:

static void tcp_openreq_init(...)
{
        struct inet_request_sock *ireq = inet_rsk(req);

        ...

        ireq->ir_mark = inet_request_mark(sk, skb);
#if IS_ENABLED(CONFIG_SMC)
        ireq->smc_ok = rx_opt->smc_ok;
#endif
}

일단 짧아졌다. 그리고 이런 것도 된다.

linux/net/ipv4/tcp_input.c:

int tcp_conn_request(...)
{
        ...

        if (want_cookie && !tmp_opt.saw_tstamp)
                tcp_clear_options(&tmp_opt);

        if (IS_ENABLED(CONFIG_SMC) && want_cookie)
                tmp_opt.smc_ok = 0;

        ...
}

커널 구성 항목 심볼(CONFIG_SMC, CONFIG_SMC_MODULE)은 정의돼 있지 않거나 1로 정의돼 있다. 각 경우에 0과 1로 평가되는 매크로가 필요하다. 그리고 내부적으로 || 처리도 필요하다.

linux/include/linux/kconfig.h:

#define __ARG_PLACEHOLDER_1 0,
#define __take_second_arg(__ignored, val, ...) val

#define __and(x, y)                     ___and(x, y)
#define ___and(x, y)                    ____and(__ARG_PLACEHOLDER_##x, y)
#define ____and(arg1_or_junk, y)        __take_second_arg(arg1_or_junk y, 0)

#define __or(x, y)                      ___or(x, y)
#define ___or(x, y)                     ____or(__ARG_PLACEHOLDER_##x, y)
#define ____or(arg1_or_junk, y)         __take_second_arg(arg1_or_junk 1, y)

#define __is_defined(x)                 ___is_defined(x)
#define ___is_defined(val)              ____is_defined(__ARG_PLACEHOLDER_##val)
#define ____is_defined(arg1_or_junk)    __take_second_arg(arg1_or_junk 1, 0)

#define IS_BUILTIN(option) __is_defined(option)

#define IS_MODULE(option) __is_defined(option##_MODULE)

#define IS_REACHABLE(option) __or(IS_BUILTIN(option), \
                                __and(IS_MODULE(option), __is_defined(MODULE)))

#define IS_ENABLED(option) __or(IS_BUILTIN(option), IS_MODULE(option))

평소에 뭘 먹으면 이런 생각을 할 수 있게 되는 걸까.

이 방식의 또 다른 장점은 비활성화 코드도 일단 컴파일은 하고서 제거한다는 점이다. 즉 이 경우 저 경우 따로 컴파일 하지 않아도 컴파일 오류 정도는 잡을 수 있다.

복잡한 매크로들을 봤으니 이제 진짜로 작고 귀여운 걸 봐야겠다.

가변 인자

가변 인자 매크로를 이용해 printk() 비슷한 뭔가를 만들 때는 꼭 콜론이 문제가 된다.

$ cpp << EOF
> #define dprintf(fmt, ...) printf("DBG: " fmt, __VA_ARGS__)
> 
> dprintf("val=%d\n", val);
> dprintf("fmt-only\n");
> EOF
# 1 "<stdin>"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "<stdin>"


printf("DBG: " "val=%d\n", val);
printf("DBG: " "fmt-only\n", );

게다가 컴파일러에 따라선 dprintf("fmt-only\n", );라고 해야 매크로가 확장되기도 한다. 복잡하다. 특별한 이유 없으면 가변 인자 인라인 함수가 답이다.

리눅스 커널에서는 gcc 확장(##__VA_ARGS__)을 쓴다.

linux/include/linux/printk.h:

#ifndef pr_fmt
#define pr_fmt(fmt) fmt
#endif

#define pr_emerg(fmt, ...) \
        printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)

나중에는 __VA_OPT__라는 걸 쓰게 될지도 모르겠다.

한 번만

좀 더 아래를 보면 재밌어 보이는 매크로가 있다.

linux/include/linux/printk.h:

#define printk_once(fmt, ...)                                   \
({                                                              \
        static bool __print_once __read_mostly;                 \
        bool __ret_print_once = !__print_once;                  \
                                                                \
        if (!__print_once) {                                    \
                __print_once = true;                            \
                printk(fmt, ##__VA_ARGS__);                     \
        }                                                       \
        unlikely(__ret_print_once);                             \
})

말하자면 pthread_once() 같은 건데, 어쩌다 여러 번 찍는다고 큰일 나는 건 아니니까 설렁설렁 구현해 놨다. 제대로 하자면 락을 쓰면 된다. 그런데 실행 여부를 나타내는 플래그(위의 __print_once) 검사에 드는 CPU 클럭이 아까울 수도 있다.

linux/include/linux/once.h:

#define DO_ONCE(func, ...)                                                   \
        ({                                                                   \
                bool ___ret = false;                                         \
                static bool ___done = false;                                 \
                static DEFINE_STATIC_KEY_TRUE(___once_key);                  \
                if (static_branch_unlikely(&___once_key)) {                  \
                        unsigned long ___flags;                              \
                        ___ret = __do_once_start(&___done, &___flags);       \
                        if (unlikely(___ret)) {                              \
                                func(__VA_ARGS__);                           \
                                __do_once_done(&___done, &___once_key,       \
                                               &___flags);                   \
                        }                                                    \
                }                                                            \
                ___ret;                                                      \
        })

#define get_random_once(buf, nbytes)                                         \
        DO_ONCE(get_random_bytes, (buf), (nbytes))

기본 구조는 비슷하다. 그리고 쪼잔한 클럭 절약 방법은 정적 키 사용이다.

이름 바꾸기

__RENAME()이라는 간단한 매크로가 하나 있다.

linux/include/linux/string.h:

#ifndef __HAVE_ARCH_STRNLEN
extern __kernel_size_t strnlen(const char *,__kernel_size_t);
#endif
...

#define __FORTIFY_INLINE extern __always_inline __attribute__((gnu_inline))
#define __RENAME(x) __asm__(#x)

void fortify_panic(const char *name) __noreturn __cold;
...

#if !defined(__NO_FORTIFY) && defined(__OPTIMIZE__) && defined(CONFIG_FORTIFY_SOURCE)
...

extern __kernel_size_t __real_strnlen(const char *, __kernel_size_t) __RENAME(strnlen);
__FORTIFY_INLINE __kernel_size_t strnlen(const char *p, __kernel_size_t maxlen)
{
        size_t p_size = __builtin_object_size(p, 0);
        __kernel_size_t ret = __real_strnlen(p, maxlen < p_size ? maxlen : p_size);
        if (p_size <= ret && maxlen != ret)
                fortify_panic(__func__);
        return ret;
}

...

#endif

매크로를 확장해 보자.

extern __kernel_size_t __real_strnlen(const char *, __kernel_size_t) __asm__("strnlen");

함수 선언 뒤에 __asm__이 붙고 그 안에는 함수 이름만 덜렁 있다. 그리고 커널 소스에는 __real_strnlen()이라는 함수가 없고 대신 strnlen() 구현이 (또!) 있다.

linux/lib/string.c:

#ifndef __HAVE_ARCH_STRNLEN
/**
 * ...
 */
size_t strnlen(const char *s, size_t count)
{
        const char *sc;

        for (sc = s; count-- && *sc != '\0'; ++sc)
                /* nothing */;
        return sc - s;
}
EXPORT_SYMBOL(strnlen);
#endif

특이한 문법이 대부분 그렇듯 컴파일러 확장이다. 어셈블러 코드에서 쓸 변수/함수 이름을 지정하는 방법이다. 즉 컴파일 중에는 __real_strnlen이라는 이름을 쓰다가 어셈블러 코드를 생성할 때는 strnlen를 쓰는 식이다.

$ cat onetwo.c
int one asm("two") = 2;

int two(void) asm("one");

int two(void)
{
        return 1;
}
$ gcc -S -O2 onetwo.c -o -
        .file   "onetwo.c"
        .text
        .p2align 4,,15
        .globl  one
        .type   one, @function
one:
.LFB0:
        .cfi_startproc
        movl    $1, %eax
        ret
        .cfi_endproc
.LFE0:
        .size   one, .-one
        .globl  two
        .data
        .align 4
        .type   two, @object
        .size   two, 4
two:
        .long   2
        .ident  "GCC: (Ubuntu 7.2.0-8ubuntu3.2) 7.2.0"
        .section        .note.GNU-stack,"",@progbits

비슷한 문법을 써서 레지스터를 특정 변수 전용으로 고정할 수도 있다. 아키텍처 의존성이 있긴 하지만 유용할 때가 가끔은 있을 것 같다.