개요

: CMake는 플랫폼 중립적 언어로 빌드 프로세스는 CMakeLists.txt 파일에 기술한다. 이는 C/C++, 자바, 일부 스크립트 언어를 지원하며 Unix 시스템에서 실행될 때 기본 동작은 Make 기반 프레임워크(makefile과 여러개의 프레임워크 파일)를 생성한다.

기초 문법

: 다음의 쉬운 예제를 보며 기본 문법을 소개하겠다.

project (basic-syntax C)

cmake_minimum_required (VERSION 2.6)

set (name Jay)
set (addr Seoul)
message ("${name}, please give me home in ${addr}")

set_property (SOURCE add.c PROPERTY Author Jay)
set_property (SOURCE mult.c PROPERTY Author Ho)
get_property (author_name SOURCE add.c PROPERTY Author)
message ("The author of add.c is ${author_name}")

위에서와 같이,

  • 모든 명령은 space로 구분된 인자를 전달받는다: command (arg1 arg2 ...)
  • 두 개의 단어를 하나의 인수로 사용하고 싶다면, 따옴표(')를 사용해 하나의 인수로 지정한다.
  • project 명령은 빌드 시스템을 식별할 수 있는 고유한 이름을 정의한다. 이는 eclipse와 같이 프로젝트 이름이 필요한 네이티브 빌드 도구에서 사용된다. 또한 C/C++, Java처럼 사용할 프로그래밍 언어를 지정한다.
  • cmake_minimum_required 명령은 CMake 2.6 버전 이후부터 지원되는 명령을 사용하겠다는 의미이다.
  • set 명령은 변수와 그 값을 정의한다.
  • set_property 명령을 통해 속성 값을 설정한다. 이를 통해 파일에 값을 저장할 수 있고 빌드 시스템은 이 파일 이름을 기준으로 속성값을 관리하고 다른 명령에서 자유롭게 접근할 수 있다.
add_exectuable (calculator add sub mult calc)
  • 컴파일 명령을 생성하고 종속성 그래프에 파일 이름을 추가한다.
  • 여기서 확장자가 사용되지 않았는데, CMake는 확장자를 자동으로 붙여준다. 따라서 최종 실행 프로그램명은 calculator.exe가 된다.
add_library (math STATIC add sub mult)
add_executable (calculator calc)
target_link_libraries (calculator math)
  • add_library 명령에서 add.c, sub.c, mult.c를 컴파일해 정적 라이브러리를 생성한다.
  • add_executable, target_link_libraries 명령을 통해 calc.c파일을 컴파일한 후 math 라이브러리와 링크하여 calculator 프로그램을 생성한다.

기타

  • include_directories 명령을 통해 컴파일러에 헤더 파일 경로를 추가할 수 있다. 이는 gcc의 -I 옵션과 같다.
  • link_directories 명령을 통해 라이브러리 경로를 추가할 수 있다. 이는 gcc의 -L 명령과 같다.

컴파일 플래그 설정

: 빌드 specification에 어떤 타입의 결과물을 생성할지 지정하고, 사용할 컴파일 플래그를 설정한다.

  • set (CMAKE_BUILD_TYPE Debug) 명령을 통해 소스 레벨 디버깅 정보를 포함한 debug 빌드를 생성할 수 있다.
  • set_property (DIRECTORY PROPERTY COMPILE_DEFINITIONS TEST=1) 명령을 통해 현재 디렉토리에 있는 모든 C 파일을 컴파일 시 TEST 심볼을 정의하도록 한다.
  • set_property (SOURCE add.c PROPERTY COMPILE_DEFINITIONS QUICKADD=1) 명령을 통해 add.c를 컴파일 할 때 QUICKADD 심볼이 추가되게 한다.

외부 명령과 타겟 추가

add_custom_target 명령은 새로운 상위 레벨 타겟을 정의하고 실행될 순서를 지정한다. 이는 결과물을 생성하지 않고 파일의 갱신상태에 의존하지 않는다.

project (custom_target)
cmake_minimum_required (VERSION 2.6)

add_custom_target (print-city ALL
    COMMAND echo "Seoul is nice city")

add_custom_target (print-time
    COMMAND echo "SIt is now 8:45")

add_custom_target (print-day
    COMMAND echo "Today is Sunday")

add_dependencies (print-city print-time print-day)
  • add_custom_target (print-city ALL ... 에서 ALL 키워드는 개발자가 타겟을 명시하지 않을 때 print-citydefault build로 실행되게 정의한다.
  • add_dependencies 명령을 통해 print-city가 print-time, print-day에 종속된다는 것을 정의한다.

흐름제어

if (${my_var})
    message ("..")
else ()
endif()

if (NOT my_var) # 변수에 ${}를 사용하지 않아도 됨

if (${my_age} EQUAL 40) #다른 변수나 상수와 비교 가능

if (EXIST file.txt) # 파일의 존재여부 확인

if (file.txt IS_NEWER_THAN file2.txt) # 두 파일 간 생성순서 비교

macro (my_macro ARG1 ARG2 ARG3) # 매크로 정의
    message("${ARG1} ...")

my_macro(1 2 3) # 정의한 매크로 사용

일반적으로 사용되는 캐시 변수

: 새로운 캐시 변수를 정의할 수 있고, 아래의 값들을 기본값으로 초기화할 수도 있으며, 캐시 변수는 일반 변수와 같이 사용될 수 있다. 그리고 이 값을 set명령으로 변경할 수 있다.

  • CMAKE_AR, CMAKE_C_COMPILER, CMAKE_LINKER: 라이브러리를 묶어주는 아카이브 도구, C 컴파일러, 오브젝트 링커의 절대 경로이다.
  • CMKAE_MAKE_PROGRAM: /usr/bin/gmake와 같은 네이티브 빌드 도구의 절대 경로이다. 기본 설정 버전을 다른 버전으로 변경할 수 있다.
  • CMAKE_BUILD_TYPE: 생성하고 싶은 빌드 트리의 타입을 지정할 수 있다.
    • Debug: 생성되는 오브젝트 파일과 실행파일이 디버깅 정보를 포함하게 한다.
    • Release: 최종 실행 파일이 최적화되고 디버깅 정보를 포함하지 않게 된다.
    • RelWithDebInfo: 실행 파일이 최적화되지만, 디버깅 정보를 포함한다.
    • MinSizeRel: 실행 파일이 최소한의 메모리를 사용하게 한다.
  • CMAKE_C_FLAGS_*: 앞의 네 가지 빌드 타입에 따라 C 컴파일 옵션이 지정된다.
  • CMAKE_EXE_LINKER_FLAGS_*: 앞의 C 컴파일 옵션과 유사하게 각 빌드 타입에 따른 링커 옵션을 나타낸다.

'빌드&테스트도구 > GCC&Make&CMake' 카테고리의 다른 글

make - make에 대한 개요  (0) 2020.10.06
GCC - (4) 링커(ld)에 관하여  (0) 2020.09.13
GCC - (3) gcc의 각종 옵션  (0) 2020.09.09
GCC - (2) gcc의 세부 과정  (0) 2020.09.07
GCC - (1) C 컴파일 과정 개요  (0) 2020.09.02

make란?

: 이전 게시물에서 gcc를 통한 컴파일 명령어들을 살펴 보았다. gcc의 문제는 프로젝트의 크기가 커질 때, gcc를 통한 컴파일이 상당히 번거로운 작업이 될 것이다. 따라서 이를 위해 파일간의 종속 관계를 기술해 컴파일 명령을 순차적으로 수행하는 것이 make이다.

간단한 Makefile

다음과 같은 파일 구조가 있다고 가정하자.

jay@jay-VirtualBox:~/tutorial/make$ tree .
.
├── plus.c
├── calculator.h
├── main.c
└── minus.c

이를 위한 간단한 Makefile은 다음과 같을 것이다.

all: calculator

calculator : minus.o plus.o main.o
    gcc -W -Wall -o calculator minus.o plus.o main.o

minus.o : minus.c
    gcc -W -Wall -c -o minus.o minus.c

plus.o : plus.c
    gcc -W -Wall -c -o plus.o plus.c

main.o : main.c
    gcc -W -Wall -c -o main.o main.c

clean : 
    rm -rf *.o diary

여기서 make 명령어를 사용하면, Makefile 내에서 제일 처음 오는 타겟 을 찾는다.
all 타겟의 종속 항목인 minus.o가 만들어지지 않았으므로 minus.o를 생성하는 룰을 찾아 해당 명령을 수행하며, 이는 반복적으로 수행되어진다.

매크로를 사용한 Makefile

: 매크로를 정의하면 이는 Makefile 내에 정의된 문자열로 치환되어 사용되어 진다. 이는 일관성있고 이식성 높은 Makefile을 만드는 데 도움을 준다. 다음은 매크로 작성의 규칙이다.

  1. #은 주석문을 의미한다.
  2. 정의한 매크로를 참조할 때에는 다음의 방법이 있다.
    • NAME = string
    • ${NAME}
    • $(NAME)
    • 여기서 중괄호는 셸의 환경변수 치환에도 사용되므로, 소괄호를 사용하는 것을 권장한다.
  3. 정의되지 않은 매크로는 null 문자열(공백)로 치환된다.
  4. 중복 정의된 경우 최후에 정의된 값으로 치환된다.
  5. 매크로 정의 시, 이전에 정의한 값을 참조해 정의할 수 있다.
    • NAME2 = my valuable $(NAME)
  6. 대입에는 다음의 방법들이 있다.
    • NAME1 = string: 재귀적 확장 매크로, 대입문에 post-defined variable이 사용될 경우 이를 계속적으로 스캔하며 대입한다.
    • NAME2 := string: 단순 확장 매크로, 대입문에 pre-defined variable만이 대입된다.
    • NAME3 += string: 기존의 매크로에 공백을 두고 덧 붙인다.
    • NAME4 ?= string: NAME4가 기존에 정의되어있지 않은 경우 이 명령을 통해 정의하고, 그렇지 않다면 무시한다.

NOTE

대입문에 따옴표(")를 넣으면 따옴표(") 또한 문자열로 인식하여 대입한다.

 

내부적으로 정의되어 있는 매크로

: 내부적으로 정의되어 있는 매크로를 확인하기 위해서는 make -p 명령을 사용하면 된다. 다음은 그 일부를 나타낸다.

# default
CC = cc
# environment
_ = /usr/bin/make
# default
CHECKOUT,v = +$(if $(wildcard $@),,$(CO) $(COFLAGS) $< $@)
# environment
MANAGERPID = 1220
# environment
CLUTTER_IM_MODULE = ibus
# environment
LESSOPEN = | /usr/bin/lesspipe %s
# environment
LC_NAME = ko_KR.UTF-8
# default
CPP = $(CC) -E
# default
LINK.cc = $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
# default
MAKE_HOST := x86_64-pc-linux-gnu
# environment
PATH = /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
# default
LD = ld

...
...

# Implicit Rules

%: %.o
#  recipe to execute (built-in):
    $(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@

%.c:

%: %.c
#  recipe to execute (built-in):
    $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@

%.ln: %.c
#  recipe to execute (built-in):
    $(LINT.c) -C$* $<

%.o: %.c
#  recipe to execute (built-in):
    $(COMPILE.c) $(OUTPUT_OPTION) $<

%.cc:

%: %.cc
#  recipe to execute (built-in):
    $(LINK.cc) $^ $(LOADLIBES) $(LDLIBS) -o $@

%.o: %.cc
#  recipe to execute (built-in):
    $(COMPILE.cc) $(OUTPUT_OPTION) $<

여기서 주요하게 살펴볼 것들은 CC, CXX, CFLAGS, CXXFLAGS, LD 등이 있다.

또한 다음의 자동 매크로 리스트를 살펴보자.

매크로 설명
$? 현재 타겟보다 최근에 변경된 종속 항목 리스트
(확장자 규칙에서 사용 불가)
$< 현재 타겟보다 최근에 변경된 종속 항목 리스트
(확장자 규칙에서만 사용 가능)
$* 현재 타겟보다 최근에 변경된 종속 항목의 이름(확장자 제외)
(확장자 규칙에서만 사용 가능)
$^ 현재 타겟의 종속 항목 리스트
(확장자 규칙에서 사용 불가)
$@ 현재 타겟의 이름
$% 현재 타겟이 라이브러리 모듈일 때, .o 파일에 대응되는 이름

위의 자동 매크로를 이용하면 아래와 같이 편리하게 기술할 수 있다.

target : dep1.c dep2.c
    gcc -o target dep1.c dep2.c
CC = gcc
CFLAGS = -W -Wall
TARGET = calculator

all : $(TARGET)

$(TARGET) : dep1.c dep2.c
    $(CC) $(CFLAGS) -o $@ $^

확장자 규칙의 사용

: *.c 파일을 오브젝트로 컴파일(gcc의 -c 옵션)하면 그에 상응하면 *.o파일이 생성된다. 이를 이용한 것이 make -p 명령어의 아래에서 볼 수 있는

%.o: %.c
#  recipe to execute (built-in):
    $(COMPILE.c) $(OUTPUT_OPTION) $<

등의 pre-defined 명령문이다. 따라서 gcc는 확장자 규칙에 맞게 생성된 파일들(*.cc, *.java, *.c)의 파일을 보고 암묵적으로 적절한 컴파일러를 이용해 컴파일한다. 이를 이용하면 위의 Makefile은 더욱 간단해 질 수 있다.

OBJECTS = minus.o plus.o main.o

all : calculator
calculator : $(OBJECTS)
    $(CC) -o $@ $^
clean :
    rm -rf *.o calculator

위의 기술문에서 calculator를 생성하기 위한 종속항목들(minus.o, plus.o, main.o)을 생성하기 위해 확장자가 .o인 내부 확장자 규칙을 이용해 현재 디렉토리에서 minus.o, plus.o, main.o를 생성할 파일을 찾는다. 이러한 파일들은 확장자를 제외하고 동일한 이름을 가지고 있어야 한다.

더 나아가서 추가적으로 내부 확장자 규칙을 재 정의할 수 있다. 내부 확장자 규칙에서 디버깅 메시지를 활성화하고 싶은 경우, 다음과 같이 재정의 할 수 있다.

OBJECTS = minus.o plus.o main.o

.SUFFIXES : .o .c
%.o : %.c          # .o와 대응되는 .c를 발견 시 아래 명령어를 수행한다.
    $(CC) -DDEBUG -c -o $@ $<

all : calculator
calculator : $(OBJECTS)
    $(CC) -o $@ $^
clean :
    rm -rf *.o calculator

여기서 확장자 규칙을 재 정의하기위해 .SUFFIXES라는 특수 타겟을 사용했다. .SUFFIXES종속 항목은 확장자 규칙을 검사하는 데 사용되는 확장자들의 리스트이다. 따라서 확장자 규칙을 사용하려는 확장자는 먼저 .SUFFIXES 타겟의 종속 항목으로 설정되어야 한다.

여기서 %는 일치하는 확장자를 제외한 파일명을 의미한다.

프로젝트의 파일이 수 없이 많을 경우 이를 모두 기술하는 것은 거의 불가능에 가깝다. 따라서 다음과 같이 편리하게 만들 수 있다.

SRCS = $(shell ls *.c)
SRCS = $(wildcard *.c)
OBJECTS = $(SRCS:.c=.o)
  • $(shell 실행할 셸 명령): 셸 명령을 수행하고 결과를 리턴한다.
  • $(wildcard *.c): *.c와 일치하는 파일들을 공백으로 구분하여 대입
  • $(SRCS:.c=.o): 대입 참조기법을 사용해 .c가 .o로 바뀐다.

NOTE

clean:
    cd ~/Desktop/tutorial
    rm -rf *.o
    @echo "with @ option print only result, not with command itself"

위와 같은 명령을 수행하면 원하는 결과를 얻지 못할 수 있다.
각각의 명령은 새로운 셸을 띄워

/bin/sh -c cd ~/Desktop/tutoril
/bin/sh -c rm -rf *.o

를 수행하므로 이에 유의해야한다.

.

NOTE

echo $?
위 명령을 수행하면 직전에 수행한 명령의 리턴값을 나타낸다. 0일 경우 ERROR_NONE을 나타낸다.

재귀적 make의 사용

all : calculator.o plus.o minus.o main.o
    $(CC) -o calculator $^

calculator.o:
    cd calculator && make

...

: 재귀적으로 Makefile을 선언해 사용할 경우, 부모 Makefile에 기술한 매크로를 자식 Makefile에 전달하고 싶을 때가 있다. 이 경우 다음과 같이

TARGET := calculator
export CC = gcc

export 구문을 사용한다. 만약 모든 매크로에 대해 전달하고 싶을 경우 export 구문을 단독으로 사용하면 된다.

링킹이란?

  • 링킹은 컴파일과 링킹에서 마지막 과정으로 조각난 오브젝트 파일들을 하나의 바이너리 이미지로 합치는 과정이다.
  • ELF 포맷으로 되어있는 각 오브젝트를 섹션 종류별로 하나의 오브젝트로 합치는 과정이다.
  • $ ld -o main main.o sub.o
  • ld는 링킹 과정에서 각 오브젝트를 같은 종류의 섹션(text, data, bss 등)별로 묶어 하나의 ELF 바이너리로 만든다 : 결합
  • 결합과정 후에 합쳐진 각 섹션을 실제 코드에 맞게 조정한다. 이 때, 바이너리에 각 심볼이 가질 실제 주소를 구하고 이를 참조하는 부분에 주소를 설정한다. : 재배치

ELF 바이너리 포맷

NOTE

링킹과정에서 주요하게 알아두어야 할 부분은 ELF 바이너리 포맷과 링커 스크립트이다. 따라서 링킹 과정에 대해 자세히 알아보기 전 간략하게 ELF 바이너리 포맷에 대해 알아두고 넘어가자.

ELF 바이너리 포맷의 구조

: ELF 바이너리 포맷의 구조는 크게 ELF header, Program header table, Section & Segment area, Section header table 로 구성되어 있다. 여기서 Program header table에는 재배치 불가능한 ELF 오브젝트(실행 바이너리), Section header table에는 재배치 가능한 ELF 오브젝트(*.o 바이너리)가 온다.

  • readelf -h {executable binary}: ELF header를 조회한다.

  • readelf -l {executable binary}: 실행 바이너리 파일의 프로그램 헤더 테이블, Segment Section을 조회한다.

    • 프로그램 헤더의 내용에는 다음을 포함한다.

      LOAD 0x000000 0x... 0x... 0x... 0x... RWE 0x...

      여기서 맨 첫번째 값(타입)에 올 수 있는 값들의 종류는 다음과 같다.

      타입명 의미
      LOAD 메모리에 로드해야 되는 세그먼트
      NULL 사용되지 않는 프로그램 헤더
      DYNAMIC 동적 링크를 해야 되는 자료를 포함하는 세그먼트
      INTERP 인터프리터 이름을 포함하는 세그먼트
      PHDR 프로그램 헤더를 포함하는 세그먼트
      NOTE 노트 정보를 포함하는 세그먼트
      SHLIB 예약된 프로그램 헤더
    • Segment Section에는 위의 프로그램 헤더의 각 세그먼트가 어떤 섹션들로 이루어져 있는지를 나타낸다.

ld를 이용한 링킹

  1. Application Linking

    • 재배치 가능한 오브젝트( file.o )의 결합과정

    • 재배치 과정을 통해 재배치 불가능한 오브젝트( 실행 바이너리 main )를 만들어 내는 과정

      /usr/libexec/gcc/i386-redhat-linux/4.1.2/collect2
      -o test  
      /usr/lib/gcc/crt1.o  
      ...  
      main.o func.o
      -lgcc —as-needed -lgcc\_s —no-as-needed -lc -lgcc —as-needed -lgcc\_s —no-as-needed  
      /usr/lib/gcc/crtend.o

      위와 같은 명령을 위해선 main.o , func.o 가 존재해야하는데, 이는 $ gcc -c *.c 명령을 통해 얻을 수 있다. 그리고 위 명령어는 아래와 동일한 값을 가지기도 한다.

      ld -dynamic-linker /lib/ld-linux.so.2 
      -o main /usr/lib/crt1.o /usr/lib/crtn.o main.o func.o -lc

      -dynamic-linker lib/ld-linux.so.2 : 동적 링커로 ld-linux.so.2 를 사용한다. 이는 .interp 섹션에 해당 동적 링커의 경로가 들어가 main 프로그램 실행 시 해당 링커를 메모리 주소 공간이 같이 로드해 먼저 수행한다. 이후, 동적 링커는 프로그램 실행에 필요한 공유 라이브러리들을 확인하여 메모리에 로드 및 주소공간을 맵핑한다.

      NOTE
      -dynamic-linker lib/ld-linux.so.2 옵션을 주지 않으면 링커는 -lc 옵션을 동적링커로 인식하여 오류를 발생시킴에 주의하자.

      아키텍처에서 지원하지 않는 연산이 사용되어 gcc의 내부 함수로 바뀐 경우, 해당 연산에 대한 라이브러리(libgcc.a, libgcc_s.so)가 필요한 경우가 있다. 이 때, 이러한 라이브러리가 추가되어야 한다.

      ld -L/usr/lib/gcc/i386-redhat-linux/4.1.2
      -dynamic-linker /lib/ld-linux.so.2 -o test /usr/lib/crt1.o  
      main.o func.o -lc -lgcc -lgcc_s
  2. -l 옵션으로 라이브러리 지정

    : 링킹시 특정 라이브러리를 포함하려면 -l{lib_name} 을 사용한다. 만약 포함할 라이브러리가 libJay.so 라면 -lJay 만을 기입하면 충분하다.

    : -l 옵션은 반드시 소스보다 뒤에 와야한다. 이는, ld가 C 소스 파일에서 호출한 함수들을 라이브러리에서 찾을 때 명령라인 입력을 기준으로 한 번만 스캔해서 찾아 링킹하기 때문이다. 만약 이를 위반할 경우 ld가 해당 라이브러리의 심볼들을 resolve 하지 못하는 문제가 발생한다.

  3. -L 옵션으로 라이브러리 검색 디렉토리 지정

    : ld는 기본적으로 /usr/i386-redhat-linux/lib, /usr/local/lib, /lib, /usr/lib 디렉토리 순서로 라이브러리를 찾는다. 이 경로에 없는 라이브러리의 경우 위 옵션을 이용해 경로를 지정해 주어야 한다.

  4. -static 옵션으로 정적 링킹

    : 공유 라이브러리와 동적 라이브러리가 동시에 있는 경우 위 옵션을 사용하면 정적 라이브러리와 링킹한다. 정적 링킹 시 포함되는 모든 라이브러리의 정적 라이브러리가 있어야 한다.

     ld -static -L/usr/lib/gcc/i386-redhat-linux/4.1.2 -o test main.o func.o 
     -\( -lgcc -lgcc_eh -lc -\)

    : -\( -lgcc -lgcc_eh -lc -\) 옵션은 앞의 라이브러리에 대해 참조하는 모든 심볼을 완전히 찾을 때까지 라이브러리들을 계속 스캔하면서 찾는 옵션이다. 이는 ld가 기본적으로 정적 라이브러리에 있는 오브젝트를 링킹하고자 할 때 명령 라인에서 주어지는 순서대로 한 번만 스캔하면서 심볼의 참조를 찾고, 해당 심볼을 정의하는 정적 라이브러리 내의 해당 오브젝트만 링킹하기 때문이다.

  5. -shared 옵션으로 공유 라이브러리와 링킹

    : 정적 라이브러리와 공유 라이브러리가 동시에 존재할 경우 공유 라이브러리와 링킹한다. 이 옵션은 default 값으로 -static 을 지정하지 않으면 자동으로 지정되는 옵션이라 보면 된다.

  6. -r 옵션으로 부분 링킹

    : 재배치 가능한 오브젝트들을 링킹해 하나의 큰 재배치 가능한 오브젝트로 만드는 옵션이다. 이는 나중에 다시 링킹될 수 있다.

     ld -r -o Operator.o plus.o minus.o mult.o div.o
    
     Prev           |  Next
     plus.o minus.o |  Operator.o
     mult.o div.o   |

gcc를 이용한 컴파일 옵션

다음의 gcc 컴파일 명령어를 통해 사용되는 옵션에 대해 간략히 알아보자.

gcc -v -I/usr/local/include -DDEBUG -Wall -W -O2 -L/usr/local/lib -o like like.c -lm
  • v: gcc C컴파일러의 옵션으로 컴파일 과정을 출력한다. (verbose)
  • I/usr/local/include -DDEBUG: 전처리기(cc1 -E)의 옵션
  • Wall -W -O2: cc1의 옵션
  • L/usr/local/lib -o like like.c -lm: collect2의 옵션. 이는 ld를 호출 시 전달한다.

man gcc 명령을 통해 기능별 옵션 리스트를 볼 수 있다.

gcc 옵션 규칙

  1. 각각의 옵션에는 그에 상응하는 gcc 내부 변수가 존재한다.

    • gcc/options.c, gcc/options.h에는 gcc 옵션에 상응하는 변수들을 볼 수 있다.

      extern int warn_unused_variable;
      extern int flag_branch_target_load_optimize;
      ...
      
    • 예를 들어, int warn_unused_variable 변수는 Wunused_variable 옵션을 주면 값이 설정되는 변수이다. 아무런 옵션도 주지 않으면 기본 값이 0으로 설정된다.

  2. [f|W|m][옵션]은 설정이고, -[f|W|m]no-[옵션]은 해제이다.

    • Wno-unused_function을 이용해 0으로 해제한다.
  3. 동일 종류의 옵션은 최종 옵션만 옵션 변수에 설정된다.

    • gcc ... -W-unused_function -Wno-unused_function 은 마지막 설정인 -Wno-unused_function이 적용된다.
  4. 그룹 옵션이 존재한다.

    • fdump-tree-all 은 -fdump-tree-original, -fdump-tree-optimize ... 의 옵션을 동시에 준 것과 동일하다.
    • O1, -Wall이 그러하다.
  5. f는 플래그, -W는 경고, -m은 아키텍처 종속, 기타 나머지들이다.

gcc 필수 옵션 및 중요 옵션

  1. gcc C 컴파일러 드라이버 옵션
    • -E: 전처리 과정의 결과를 화면에 보인다.
    • -S: 어셈블리 파일(*.s)만 생성하고 컴파일을 멈춘다.
    • -c: 오브젝트 파일(*.o)만 생성하고 컴파일을 멈춘다.
    • -v: 컴파일 과정을 화면에 자세히 출력한다.
    • -save-temps: 컴파일 과정의 중간파일인 전처리 파일(*.i), 어셈블리 파일(*.s), 오브젝트 파일(*.o)를 지우지 않고 디렉토리에 저장한다.
  2. cc1 -E 또는 cpp0 전처리기 옵션
    • -I: 전처리 과정 중 헤더파일을 탐색하는 기본 디렉토리를 추가한다. 기본적으로 #include <file> 형식은 시스템 헤더 디렉토리로 설정된 /usr/local/include, /usr/lib/gcc/i386-redhat-linux/4.1.2/inclue, /usr/include 순으로 찾고, #include "file"형식은 현재 디렉토리에서 먼저 찾고 위 순서로 찾는다. 따라서 -I옵션으로 디렉토리를 지정하면 시스템 헤더 디렉토리보다 우선해서 찾는다.
    • -include {file}: 헤더파일을 소스 내에 추가할 때 사용한다. 이는 #include "file"한 것과 동일한 결과를 나타낸다.
    • -D{macro_name}: 매크로를 외부에서 define할 때 사용한다. 이는 디버깅에서 많이 쓰인다.
    • -D{macro_name}={value}: 이는 #define macro value한 것과 동일한 결과를 나타낸다.
    • -U{macro_name}: 이는 -D와 반대의 결과를 나타내고, #undef macro와 동일한 결과를 나타낸다.
    • -M, -MM: (Makefile작성 시 유용한 )종속 항목(컴파일되기 위해 필요한 항목) 리스트를 출력한다. -M은 make를 위한 소스 파일의 모든 종속 항목을 출력하고, -MM은 기본 include 디렉토리에 있는 헤더 파일은 빼고 종속 항목을 출력한다.
    • -nostdinc: 디폴트 include 디렉토리(/usr/include 등)에서 헤더 파일을 탐색하지 않고, -I 옵션으로 추가한 디렉토리에서만 헤더 파일을 탐색한다.
    • -C: 전처리 과정에서 주석을 제거하지 않는다.
    • -Wp,{options}: 전처리기의 옵션으로 전달하고자 할 때 사용한다. 이는 전처리기(cc1 -E)와 C 컴파일러 드라이버(gcc)의 옵션이 서로 동일하다면 해당 옵션은 C 컴파일러 드라이버의 옵션으로 처리되므로 gcc의 해석을 거치지 않고 전처리기로 옵션을 전달하고자 할 때 사용된다.
  3. cc1 C 컴파일러 옵션
    • -ansi: ANSI C 표준에 부합하는 소스를 작성하려 할 때 사용된다.
    • std={C std}: C 표준 버전을 지정하고자 할 때 사용된다. 여기에는 c89, c99 등이 있다.
    • -traditional: Traditional C 문법으로 문법을 검사한다.
    • -W, -Wall: -Wall은 모든 모호한 문법에 대한 경고 메시지를 출력한다. -W은 합법적이지만 모호한 코딩에 대해 부가적인 정보를 제공한다. 따라서 두 옵션을 모두 사용하는 것이 좋다. gcc -W -Wall -o main main.c
    • -w: 모든 경고 메시지를 제거한다. 이는 -Wall, -W{options}보다 우선시된다.
    • -O0: 어떤 최적화도 수행하지 않는다. 아무런 최적화 옵션을 주지 않으면 -O0를 준 것과 동일하다.
    • -O1, -O2, -O3: 해당 옵션 적용시 세부적인 사항은 다음의 링크를 통해 확인할 수 있다.
    • -Os: 사이즈 최적화를 수행한다. 이는 임베디드 시스템 같이 메모리 공간이 협소한 곳에서 자주 사용된다.
    • -g: 디버거에 제공하는 디버깅 정보(변수 타입, 전역 심볼명, 주소, 소스 라인정보 등)를 바이너리에 삽입한다. 이는 readelf -a file 명령으로 읽을 수 있으며, .debug_*로 시작되는 섹션들이 이에 해당한다. 이 또한 -O옵션처럼 레벨이 있는데 -g0는 아무런 디버깅 정보를 삽입하지 않고, -g-g2와 동일하다.
  4. as의 옵션
    • -Wa,{as options}: 여기서 {as options}에 들어가는 옵션들은 다음과 같다.
      -al: 어셈블 된 인스트럭션을 보여준다.
      -as: 정의된 심볼을 보여준다.
      -I{path}: include 디렉토리를 지정한다.
      -W, --no-warn: 경고 메시지를 출력하지 않는다.
      -march={architectur}: 해당 어셈블리로 번역한다.
  5. collect2, ld의 옵션
    • -L{library_path}: 라이브러리를 찾을 디렉토리를 지정한다. 기본적으로는 /usr/i386-redhat-linux/lib, /usr/local/lib, /lib, /usr/lib 순으로 찾는다.
    • -l{name_library}: 같이 링크할 라이브러리를 지정한다. 이 옵션은 main()함수가 있는 소스 파일의 뒤에 와야한다. 만약 그렇지 않다면 undefined 참조 오류가 발생할 수 있다. 이는 링커가 아카이브를 찾는 데 있어 항상 main()함수 뒤에서 탐색하기 때문이다.
    • -shared: 공유 라이브러리와 정적 라이브러리가 같이 있을 경우 공유 라이브러리를 우선해서 링크한다. 이는 디폴트 옵션이기도 하다.
    • -static: 정적 라이브러리를 우선하여 링크한다. 이 경우, 파일의 사이즈가 커짐에 유의하자.
    • -nostdlib: 링크 시 표준 C 라이브러리를 사용하지 않는다.
    • nostartfiles: crt1.o, crti.o, ctrbegin.o, crtend.o, crtn.o와 같은 crt 오브젝트 파일들을 링크하지 않는다. 이는 OS, bootloader 같은 프로그램을 컴파일 시 사용된다.
    • -Wl{liker_options}, -Xliner f{liker_option}:
      gcc -o main main.c -Wl,-M,-s: collect2를 거쳐 ld에 -M -s 옵션이 전달된다.
      gcc -o main main.c -Xlinker -M -Xlinker -s: 이 또한 위와 같다.
    • 다음은 사용 가능한 유용한 링커 옵션들이다.
      -s: 실행 파일에서 심볼 테이블 제거
      -x: 출력 파일에서 로컬 심볼 제거
      -e{name}: 시작 심볼을 name 심볼로 사용한다. 기본적으로 시작 심볼은 _start 심볼이다.
      -M: 링커 스크립트와 메모리 맵을 자세히 출력한다.

gcc를 이용한 최적화 컴파일

  1. machine dependent 옵션을 이용한 최적화
    : -march={cpu_type} -mutne={cpu-type} -mcpu={cpu-type}:
    • -march 옵션은 cpu-type으로 지정하는 프로세서의 instructio set으로 코드를 생성한다. 이는 아래에서 설명할 -mtune을 내부적으로 포함하므로 -mtune을 굳이 주지 않아도 무방하다.
    • -mtune 옵션은 인스트럭션을 스케쥴링하거나 정렬할 때 해당 CPU에 최적화되게 스케쥴링 및 정렬한다. 이 때에는 CPU 파이프라인 단계, 각 인스트럭션을 수행하는 데 걸리는 사이클, 버스, 캐시 크기 등을 고려한다.
    • -mcpu 옵션은 -mtune옵션과 거의 동일하다.
    • cpu-type으로 generic을 사용할 수 있는데, 이때 -march에서는 사용할 수 없다. 이는 CPU 종류마다 인스트럭션 셋이 다르기 때문이다.
  2. 프로파일 결과를 이용한 최적화
    : 컴파일한 프로그램을 한 번 더 수행해 프로그램의 동작 특성을 파악하여, 이 정보를 바탕으로 다시 컴파일하며 최적화하는 방법이다.
    1. gcc -O2 -o main main.c -fprofile-generate를 통해 main 바이너리에 프로파일 정보를 출력하는 코드가 추가하여 컴파일한다.
    2. $ ./main을 통해 프로그램을 수행한다.
    3. gcc -O2 -o main main.c -fprofile-use를 통해 프로그램을 수행하며 수집한 정보를 바탕으로 좀 더 최적화하여 바이너리를 생성한다.
  3. 레지스터 활용을 통한 최적화
    • -fforce-mem: 메모리에 있는 값을 레지스터에 로드해 연산을 수행한다. (GCC 4.2 below)
    • -fforce-addr: 메모리 주소 값을 레지스터에 로드 후 연산을 수행한다.
  4. gcc 컴파일 속도 최적화
    • -pipe: 컴파일 과정에서 임시파일을 생성해 전달하지 않고 파이프로 전달해 컴파일 속도를 더 빠르게 한다.

기타

  1. 환경변수를 사용한 gcc 설정
    • COMPILER_PATH: gcc는 COMPILER_PATH에 등록된 전처리기(cc1 -E), 컴파일러(cc1), 어셈블러(as), 링커(collect2, ld)를 찾는다. 따라서 export COMPILER_PATH={directory}를 통해 원하는 전처리기, 컴파일러, 어셈블러, 링커를 선택케 할 수 있다.
    • C_INCLUDE_PATH, CPLUS_INCLUDE_PATH, OBJC_INCLUDE_PATH: 소스 파일을 전처리 할 때 헤더파일을 찾을 디렉토리를 지정하는 환경변수이다.
    • LIBRARY_PATH: 라이브러리를 찾을 디렉토리를 지정하는 환경 변수이다.
  2. specs 파일을 이용한 gcc 설정

전처리 과정 with cc1 -E

전처리과정은 cc1 -E에 의해 수행되는데, 이는 크게 두 부분으로 나눌 수 있다.

  1. 헤더 파일의 삽입
    • 시스템 헤더파일 디렉토리로 등록된,
      /usr/local/include
      /usr/lib/gcc-lib/i386-redhatlinux/3.2.2/include
      /usr/include
      순으로 헤더파일을 찾는다.
    • 기본 탐색 include 디렉토리를 추가하기 위해서는 gcc 옵션으로 I{include_directory}를 붙이면 해당 디렉토리를 먼저 탐색할 수 있게 한다.
  2. 매크로 치환 및 적용
    • #define된 부분은 심볼테이블에 저장되고, 심볼 테이블에 들어있는 문자열과 같은 문자열을 만나면 해당 내용으로 치환된다.
    • #ifedf와 같은 매크로를 처리하기 위해서는 cpp0 -D{define_option}을 지정해 주면 된다. 이는 #define {define_option}을 해 준것과 같다.
    • 시스템 관련된 predefined macro(__linux__ 등)는 이전 전처리기(cpp0)에서는 사용자가 명시적으로 predefine 해야했으나 cc1 -E에서는 내부적으로 알아서 predefine 된다.

어셈블 과정 with cc1

어셈블 과정은 cc1에 의해 수행되는데, 이는 크게 세 부분으로 나뉠 수 있다. front-end, middle-end, back-end 가 그것이다.

  1. front-end
    • 언어 종속적인 부분을 처리한다. 소스코드가 올바르게 작성되었는지 분석하고 GIMPLE 트리를 생성한다.
    • 어휘 분석, 구문 분석, 의미 분석, 중간 표현 생성을 수행한다.
  2. middle-end
    • GIMPLE 트리를 이용해 아키텍처 비종속적인 최적화를 수행후 RTL(Register Transfer Language: 고급언어와 어셈블리 언어의 중간 형태)을 생성한다.
    • GIMPLE 트리를 받아 SSA(Static Single Assignment) 형태로 변환한 후 아키텍쳐 비종속적인 최적화를 수행한다. 그 후 back-end에서 사용할 RTL을 생성한다.
      • 아키텍처 비종속적인 최적화의 리스트
        • return value optimizations
        • full redundancy elimination
        • conditional constant propagation
        • dead code elimination
        • copy propagation
        • value range propagation
        • branch prediction and profiling
        • dead store elimination
        • etc.
      • RTL 표현을 보기 위해서는 gcc -o main main.c -da와 같이 da 옵션을 주면 된다.
  3. back-end
    • 아키텍처 비종속적인 최적화와 아키텍처 종속적인 최적화가 함께 수행된다. 그 후, 목적 코드(어셈블리 코드)를 생성한다.
      • 아키텍처 종속적인 최적화의 리스트
        • 각종 고전 최적화
        • common sub-expression elimination
        • GCSE/PRE
        • constant propagation
        • copy propagation
        • cross-jumping, block merging
        • basic block re-ordering
        • IF-conversion
        • 각종 loop 최적화
        • loop un-rollling
        • 연산 강도 경감
        • loop 내의 불변 코드 옮김(Invariant code motion)
        • Instruction scheduling
        • register allocation
        • satifying operand constraints
    • 여기서는 Instruction scheduling, Register allocation이 일어난다.

기계어 코드 생성 with as

NULL

링킹 with collect2

정적 라이브러리와 동적 라이브러리란?

정적 라이브러리

파일 이름이 lib{name_of_library}.a의 형식을 가진다.
/usr/lib/ 디렉토리에 위치한다.
ar -x /usr/lib/libc.a를 수행하면 해당 라이브러리를 풀어 어떤 파일들로 구성되어 있는지 확인할 수 있다.
추가로, libc.a는 ioputs.o 등과같은 목적파일들로 구성되어 있다고 할 때, 이러한 목적 파일에 어떠한 함수가 들어있는지 확인하려면 nm ioputs.o 명령을 수행하면 된다.
이러한 오브젝트들을 gcc 옵션 중 -static 옵션을 주면 main.o와 링크되어 삽입된다.
collect2는 내부적으로 링커인 ld를 호출해 링킹과정을 수행하므로 실제로는 ld -o main main.o scanf.o printf.o가 수행된다.
정적 라이브러리를 이용한 컴파일은 실행 파일에 같은 오브젝트가 중복되어 링크되므로 메모리 공간을 낭비한다는 것이다.

공유 라이브러리

공유 라이브러리를 이용해 오브젝트와 링킹할 때에는 해당 함수를 직접 호출하지 않는다. 대신, PLT(Procedure Linkage Table) 섹션의 특정 영역을 호출하게 링크한다. 여기서, PLT 섹션의 코드에는 GOT(Global Offset Table)(정확히는 GOT.PLT)섹션의 해당 함수 엔트리에 있는 값을 in-direct로 점프하게 되어있다.
이는 프로그램 실행 중, system call을 발생시키고 이는 ELF 헤더를 읽어 동적 링커를 사용한다는 정보를 얻어 수행에 필요한 섹션들과 동적 링커(/lib/ld-linux.so.2)를 프로세스 주소 공간에 로드한다. 그 후, 제어권을 동적 링커에게 넘긴다.
공유 라이브러리는 lib{name_of_library}.so.{major_version}.{minor_version}.{patch_version}으로 구성되어 있다.
표준 C 공유 라이브러리와 동적 링커는 ldd {name_of_library}로 확인할 수 있다.

다음은 collect2의 수행 모습에 대한 간략한 코드와 이에 대한 설명이다.

.../collect2 --eh-frame-hdr -m elf_i386 
--hash-style=gnu -dynamic-linker /lib/ld-linux.so.2 -o like
/usr/lib/gcc/crt1.o
/usr/lib/gcc/crt2.o
-L/usr/lib/gcc
like.o
-lgcc --ass-needed -lgcc_s --no-as-needed -lc
/usr/lib/gcc/crtend.o
/usr/lib/gcc/crtn.o
  • m elf_i386: 바이너리 포맷과 CPU 타입 지정
  • dynamic-linker /lib/ld-linux.so.2: 동적 링커로 ld-linux.so.2를 지정
  • -o like: 링크 과정이 끝나고 생성되는 실행 파일명은 like
  • L/usr/lib/gcc: 라이브러리 파일을 찾을 디렉토리를 지정
  • lgcc: 링크할 라이브러리를 지정

-l 옵션 뒤에오는 것이 라이브러리 명이다. libc.so(libc.a)의 경우 -lc가 된다.
만약 정적 라이브러리와 동적 라이브러리가 동시에 존재한다면 동적 라이브러리 가 우선시 된다.

crt1.o, crtend.o 등의 파일은 초기화 루틴 & 종료 루틴을 위해 링크된다.

들어가기 전

GCC에 대해 알아보기 전, C 소스의 컴파일 과정에 대해 간략히 짚어보고 들어가자.

C의 컴파일 과정

NOTE

gcc와 GCC는 전혀 다른 의미이다.
gcc는 GNU C Compiler의 약어이고,
GCC는 GNU Compiler Collection의 약어이다.

/usr/bin/gcc는 엄밀한 의미에서는 C 컴파일러가 아니다.
/usr/bin/gcc는 내부적으로 전처리기인 cpp0 또는 cc1 -E명령을 통해 전처리 과정을 수행하고
실제적인 C 컴파일러인 cc1을 통해 컴파일한 후
어셈블러인 as를 통해 목적 코드를 만들고,
링커를 호출하는 collect2를 호출해 collect2가 실제적인 링커 ld를 호출함으로써 라이브러리와 링크해 실행 파일로 만들어낸다.

따라서 진짜 C 컴파일러는 /usr/libexec/gcc/i386-redhatlinux/4.1.2/cc1이라 할 수 있다.

다음은 /usr/libexec/gcc/i386-redhatlinux/4.1.2/ 내부에 있는 실행파일의 역할에 대한 설명이다.

  • cc1: C 컴파일러, -E 옵션을 붙이면 전처리기로 동작
  • cc1plus: C++컴파일러, -E 옵션을 붙이면C++ 전처리기로 동작
  • cpp0: 전처리기(old ver, 현재는 컴파일러에 전처리 과정이 통합되어 있다)
  • cc1obj: Objective-C 컴파일러, -E 옵션을 붙이면 전처리기로 동작
  • collect2: 링커(내부적으로 링커인 ld를 호출해 링크)
  • ld: 링커(실제로는 존재하지 않지만 설명을 위해 써 놓음)

다음은 /usr/bin/ 디렉토리에 존재하는 컴파일러 드라이버들이다.

  • cc: C 컴파일러 드라이버, gcc와 동일
  • gcc: C 컴파일러 드라이버
  • c++: C++ 컴파일러 드라이버, g++과 동일
  • g++: C++ 컴파일러 드라이버

위 컴파일러 드라이버는 전처리기, 컴파일러, 어셈블러, 링커를 각각 호출해 주는 역할만을 담당한다. 또한 자신이 지원하는 언어의 소스가 아닐지라도 해당 소스에 맞는 전처리기와 컴파일러를 호출해 주기도 한다. 따라서 gcc가 C++ 소스를 컴파일 할 수도 있는 것이다. 하지만 C++ 소스를 컴파일하기 위해 기본적으로 요구되는 라이브러리를 알지 못하므로 lstdc++ 옵션을 컴파일 명령시 추가해 주지 않으면 오류가 발생할 것이다.

다음은 간략한 컴파일 과정 전개도이다.

main.c --'cc1 -E'--> main.c 전처리 결과 --'cc1'--> main.s 어셈블리 파일 --'as'--> like.o 오브젝트 파일 --'ld|collect2'--> main.exe

이러한 과정들을 직접 눈으로 확인하고 싶다면 다음의 명령어를 사용하라.

gcc -v -save-temps -o main main.c

여기서 -v 옵션은 verbose의 약어로 컴파일 과정을 화면으로 출력하는 옵션이고,
-save-temps 옵션은 컴파일 과정 중 발생되는 중간 파일을 지우지 않고 저장하는 옵션이다.

+ Recent posts