Command-line options

-D , -U

명령 라인에서도 -D 옵션을 이용해 매크로를 정의할 수 있습니다. = 를 이용해 값을 따로 정의하지 않으면 1 로 설정됩니다. -U 옵션은 predefined 매크로나 이전에 -D 옵션으로 정의한 매크로를 undef 할 수 있습니다.

$ gcpp -D __foo -D __bar=200
#ifdef __foo
"__foo" is : __foo
"__bar" is : __bar
#endif
@

"__foo" is : 1              # __foo 값은 1 이 된다.
"__bar" is : 200

-include file
-imacros file

이 옵션을 사용하면 소스파일의 제일위에 #include "file" 을 넣는 것과 같게됩니다. 한 가지 차이점은 파일을 찾을 때 소스파일이 위치한 곳에서 찾지 않고 현재 전처리기가 실행 중인 디렉토리에서 찾습니다. -imacros 옵션은 매크로만 include 됩니다.

-I dir
-iquote dir
-isystem dir
-idirafter dir

#include 지시자에서 파일 이름은 #include <filename>#include "filename" 두 가지 형태로 작성할 수 있습니다. 첫 번째를 시스템 헤더라고 하고 기존에 gcc 에 설정되어 있는 시스템 디렉토리를 이용해 상대 경로로 간단히 표기할 수 있습니다. 두 번째 quoted 형태의 경우는 파일을 찾을 때 현재 소스파일과 같은 디렉토리에서 찾게 됩니다.

gcc 에 기본적으로 설정되어 있는 시스템 디렉토리는 다음 명령으로 조회해볼 수 있습니다.

$ gcc -E -xc -Wp,-v /dev/null |& grep '^\s*/'        # C
 /usr/lib/gcc/x86_64-linux-gnu/10/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include

$ gcc -E -xc++ -Wp,-v /dev/null |& grep '^\s*/'      # C++
 /usr/include/c++/10
 /usr/include/x86_64-linux-gnu/c++/10
 /usr/include/c++/10/backward
 /usr/lib/gcc/x86_64-linux-gnu/10/include
 /usr/local/include
 /usr/include/x86_64-linux-gnu
 /usr/include

--------------------------------------------------

$ ls /usr/include/stdio.h
/usr/include/stdio.h

$ ls /usr/include/x86_64-linux-gnu/sys/socket.h 
/usr/include/x86_64-linux-gnu/sys/socket.h

#include <stdio.h>            # 시스템 디렉토리 상대 경로로 간단히 표기할 수 있다.
#include <sys/socket.h>

헤더 파일을 찾는 순서

기본적으로 옵션은 같은 옵션을 여러 번 중복해서 사용할 수가 있고 left-to-right 순으로 찾게 됩니다. -iquote 옵션은 #include "filename" 형태에만 적용이 되고 -I, -isystem, -idirafter 옵션의 경우는 #include <filename>, #include "filename" 두 가지 형태에 모두 적용됩니다.

  1. 소스파일을 작성할 때 설정한 #include "filename" 파일을 제일 먼저 찾습니다.
    ( 현재 소스파일과 같은 디렉토리에서 찾게 됩니다. )

  2. 명령 라인에서 -iquote dirname 옵션으로 설정한 디렉토리에서 파일을 찾습니다.

  3. 명령 라인에서 -I dirname 옵션으로 설정한 디렉토리에서 찾습니다.

  4. 명령 라인에서 -isystem dirname 옵션으로 설정한 디렉토리에서 찾습니다.

  5. gcc 에 기본적으로 설정되어 있는 시스템 디렉토리에서 찾습니다.

  6. 마지막으로 명령 라인에서 -idirafter dirname 옵션으로 설정한 디렉토리에서 찾습니다.

위에서 순서를 보면 시스템 디렉토리 보다 -I 옵션으로 설정한 디렉토리를 먼저 찾게 되므로 -I 옵션을 이용하면 시스템 헤더 파일을 override 할 수도 있습니다. ( vendor-supplied 시스템 헤더 파일을 설정할 때는 -isystem 옵션을 사용해야 합니다 ).

시스템 헤더 파일은 다르게 처리됩니다.

리눅스 같은 운영체제나 gcc 의 runtime libraries 에서 사용되는 헤더 파일의 경우는 strictly conforming C 하게 작성할 수가 없다고 합니다. 따라서 gcc 는 시스템 헤더 를 특별히 취급해서 헤더 파일을 프로세싱 할때 발생하는 모든 warning 을 suppress 한다고 합니다.

-isystem-idirafter 옵션으로 설정한 디렉토리는 시스템 디렉토리로 간주되므로 시스템 디렉토리와 동일하게 처리됩니다. 다음을 보면 리눅스 소스에서 linux/jiffies.h 헤더 파일을 매크로 확장하려고 한 것인데 -I 옵션을 사용하면 warning 이 발생하지만 -isystem 옵션을 이용하면 발생하지 않는 것을 볼 수 있습니다.

$ cd linux-source-x.xx.x

$ cpp -dD -I include -I include/uapi -I arch/x86/include -I arch/x86/include/uapi \
          -I arch/x86/include/generated -I arch/x86/include/generated/uapi \
          <<< $'#include <linux/kconfig.h>\n#include <linux/jiffies.h>' > /dev/null
. . .
arch/x86/include/asm/processor.h:188: warning: "cache_line_size" redefined
  188 | #define cache_line_size() (boot_cpu_data.x86_cache_alignment)
      | 
. . .
--------------------------------------------------------------------------------

# -isystem 옵션을 이용해 시스템 디렉토리로 설정하면 warning 이 발생하지 않는다.
$ cpp -dD -isystem include -isystem include/uapi \
          -isystem arch/x86/include -isystem arch/x86/include/uapi \
          -isystem arch/x86/include/generated -isystem arch/x86/include/generated/uapi \
          <<< $'#include <linux/kconfig.h>\n#include <linux/jiffies.h>' > /dev/null
$

시스템 헤더로 설정하는 방법에는 #pragma GCC system_header 를 이용하는 방법도 있습니다. 이 방법은 디렉토리와 상관없이 적용이 되고 매크로가 파일 중간에 사용되면 이후 내용에 대해서만 적용됩니다. ( 이것은 헤더 파일에서만 사용할 수 있습니다.)

-nostdinc
-nostdinc++

이 옵션을 사용하면 헤더 파일을 찾을 때 gcc 에 기본적으로 설정되어 있는 standard system directories 를 찾지 않습니다. -I, -iquote, -isystem, -idirafter 옵션으로 설정한 디렉토리만 찾게 됩니다.

-Wsystem-headers

시스템 헤더 에서도 warnings 을 출력하게 합니다.

Quiz

리눅스 커널 소스에서 linux/pci.h 헤더를 include 했을때 사용할 수 있는 함수를 보려면 어떻게 할까요?

$ cd linux-source-x.xx.x
$ cp /boot/config-`uname -r` ./.config
$ make init/main.i     # 처음 한번만 실행

$ header-function <<\@ -- -D__KERNEL__ \
    -I./arch/x86/include -I./arch/x86/include/generated -I./include \
    -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi \
    -I./include/uapi -I./include/generated/uapi | less

#include <linux/kconfig.h>
#include <linux/pci.h>
@
. . .
---------------------------------------------------------------

# Diagnostics 메뉴에서 작성한 header-* 함수에 다음을 추가.

_header-kernel () {
        local v dash="--"
        for v do [ "$v" = "--" ] && { dash=""; break ;}; done
        ${FUNCNAME[1]%?} "$@" $dash \
        -D __KERNEL__ -D CC_USING_FENTRY \
        -include ./include/linux/kconfig.h \
        -I./arch/x86/include -I./arch/x86/include/generated -I./include \
        -I./arch/x86/include/uapi -I./arch/x86/include/generated/uapi \
        -I./include/uapi -I./include/generated/uapi
}
header-functionk () { _header-kernel "$@" ;}
header-macrok () { _header-kernel "$@" | sed -E '/^#define CONFIG_|^#include <generated\/autoconf\.h>/d' ;}
header-typedefk () { _header-kernel "$@" ;}
header-structk () { _header-kernel "$@" ;}
header-enumk () { _header-kernel "$@" ;}
header-dumpk () { _header-kernel "$@" | sed -E '/^#define CONFIG_/d' ;}

$ header-functionk linux/pci.h
. . .
$ header-structk net/ip.h -s 'sk_buff\b'
. . .
$ header-functionk ext4.h -- -I fs/ext4
. . .
$ header-functionk '#include "fs/ext4/ext4.h"'
. . .
$ header-functionk linux/syscalls.h -- -D CC_USING_FENTRY
. . .
$ header-functionk <<\@ -s IS_ERR
#if defined(CONFIG_FUNCTION_TRACER)
#define CC_USING_FENTRY
#endif
#include "init/main.c"
@
. . .

파일 단위로 전처리 완료된 결과를 보려면 다음과 같이 하면 됩니다.

$ cd linux-source-x.xx.x
$ cp /boot/config-`uname -r` ./.config

# net/ipv4/tcp.c 파일의 전처리 결과를 보려면
$ make net/ipv4/tcp.i
  . . .
  CPP     net/ipv4/tcp.i      <---- 전처리 완료된 파일

# arch/x86/kernel/vmlinux.lds.S 파일의 전처리 결과를 보려면
$ make arch/x86/kernel/vmlinux.lds
  . . .
  LDS     arch/x86/kernel/vmlinux.lds      <---- 전처리 완료된 파일

다음은 *.c 파일의 전처리 결과를 볼 수 있는 스크립트입니다. make foo/bar.i 한것과 동일한 결과인데 헤더부분을 제외하고 foo/bar.c 파일 내용만 출력합니다. 결과를 clang-format 해서 출력하기 때문에 확장 결과에 따라 시간이 오래 걸릴 수 있습니다. ( 분 단위로 걸릴 수도 있음 )

$ cd linux-source-x.xx.x          # 사용 방법
$ header-make block/ioctl.c       # ioctl.c 파일만 출력됩니다. 헤더에 포함된 inline 함수
. . .                             # 내용을 보고 싶으면 block/ioctl.i 파일을 보면됩니다.

# "-d" (dump) 옵션은 clang-format 하지 않고 출력하기 때문에 빠르게 결과를 볼 수 있습니다.
$ header-make -d block/ioctl.c
function header-make ()
(
    set -o errexit

    dump=false

    if [ $# -eq 0 ]; then
        echo "Usage: $FUNCNAME filename.c" >&2
        exit 1
    elif [ $# -eq 1 ]; then
        target_c=$1
    else
        [ "$1" = "-d" ] && target_c=$2 || target_c=$1
        dump=true
    fi

    if ! target_p=`readlink -e "$target_c"`; then
        echo "Error: $target_c file does not exist" >&2
        exit 1
    fi

    tmpdir=`mktemp -d -p /dev/shm`
    cp -f "$target_p" "$tmpdir"
    target_o=$tmpdir/${target_c##*/}
    trap '[ -e "$target_o" ] && mv -f "$target_o" "$target_p"; rm -rf "$tmpdir"' EXIT

    sed -E -i 's/^\s*#\s*include .+/\n\a\n&\n\a\n/' "$target_c"
    target_i=${target_c%.*}.i

    make "$target_i" >&2
    if [[ ! -e $target_i ]]; then
        echo ">>> Failed: make $target_i" >&2
        exit 1
    fi
    sed '/\a/,/\a/d' "$target_i" |
    sed -zE 's/^.*\n# [0-9]+ "<command-line>" [0-9]+\n//' |
    sed -E '/^(#define |#pragma |# [0-9]+ )/d' > "$tmpdir/expanded"

    if $dump; then 
        sed -zE 's/^\n+/\n/' "$tmpdir/expanded" | vi --not-a-term -c 'set ft=c' -
        exit
    fi

    awk -v dir="$tmpdir" 'BEGIN { num = 0; file = dir "/0000" } 
        /^}$/ { print > file; close(file); num += 1; 
                file = dir sprintf("/%04d", num); next }
        { print > file }' "$tmpdir/expanded"

    printf "%s\n" "$tmpdir"/[0-9][0-9][0-9][0-9] |
        xargs -P `nproc` -i sh -c 'clang-format -i -style="{IndentWidth: 4}" "{}"'

    mv -f "$target_o" "$target_p"
    cat "$tmpdir"/[0-9][0-9][0-9][0-9] | vi --not-a-term -c 'set ft=c' -
)