개요

: 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

GTEST 설치하기

: GTEST는 google에서 개발한 C++ testing framework이다. 이를 이용해 손쉽게 TC를 만들어 개발한 코드 또는 API의 유효성을 검증할 수 있다.

다음은 이를 설치하는 방법이다.

sudo apt-get install libgtest-dev
cd /usr/src/gtest
sudo cmake CMakeLists.txt
sudo make

cd ./lib
sudo cp *.a /usr/lib

간단한 예제를 통해 살펴보기

아래와 같이 calculator API가 있다고 가정하자.

// calculator.h
class calculator
{
private:
    /* data */
public:
    int plus(int var1, int var2);
    int minus(int var1, int var2);
    int divide(int var1, int var2);
    int multiply(int var1, int var2);
};
// calculator.cpp
#include <iostream>
#include "calculator.h"

int calculator::plus(int var1, int var2)
{
    std::cout << var1 + var2 << std::endl;

    return var1 + var2;
}

int calculator::minus(int var1, int var2)
{
    std::cout << var1 - var2 << std::endl;

    return var1 - var2;
}

int calculator::divide(int var1, int var2)
{
    std::cout << var1 / var2 << std::endl;

    return var1 / var2;
}

int calculator::multiply(int var1, int var2)
{
    std::cout << var1 * var2 << std::endl;

    return var1 * var2;
}

위의 calculator.h, calculator.cpp 에서 제공하는 plus, minus, divide, multiply API의 동작(간단한 테스트를 위해 divide by zero 등과 같은 것들은 고려하지 않았습니다)을 검증하기 위해 다음과 같이 TC를 작성할 수 있다.

// main.cpp
#include "calculator.h"
#include "gtest/gtest.h"

namespace {

class CalculatorTest : public ::testing::Test
{
    public:
    calculator gCalculator;

    protected:
    CalculatorTest() {}

    virtual ~CalculatorTest() {}
    virtual void SetUp()
    {
        std::cout << "Setup" << std::endl;
    }

    virtual void TearDown()
    {
        std::cout << "TearDown" << std::endl;
    }
};

TEST_F(CalculatorTest, plus_p)
{
    EXPECT_EQ(2, gCalculator.plus(1, 1));
}

} // namespace

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

이렇게 작성한 파일들을 다음의 명령으로 컴파일 및 실행하면

g++ -o main main.cpp calculator.cpp -lgtest -lpthread
./main
./main 
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from CalculatorTest
[ RUN      ] CalculatorTest.plus_p
Setup
2
TearDown
[       OK ] CalculatorTest.plus_p (0 ms)
[----------] 1 test from CalculatorTest (0 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (0 ms total)
[  PASSED  ] 1 test.

이렇게 TC의 동작 결과를 확인할 수 있다.

More about GTEST

위의 예제에서 EXPECT_EQ()을 통해 TC의 동작을 확인하였다. EXPECT_EQ처럼 사용할 수 있는 구문은 다음과 같다.

ASSERT_@@EXPECT_@@의 차이는 ASSERT_@@의 경우 TC에서 Fail이 발생하였을 경우 해당 위치 아래의 TC들은 더이상 실행하지 않고 종료하는 반면, EXPECT_@@의 경우 TC에서 Fail이 발생하여도 아래의 TC들을 계속해서 실행한다.

이러한 내용들을 아래에서는 ASSERT_@@Fatal assertion, EXPECT_@@Nonfatal assertion으로 부른다.

Basic Assertion

: 참과 거짓을 테스트하는 구문이다.

Fatal assertion Nonfatal assertion Verifies
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition is true
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition is false
     

Binary Comparison

: 비교문을 통해 테스트하는 방법입니다.

Fatal assertion Nonfatal assertion Verifies
ASSERT_EQ(val1, val2); EXPECT_EQ(val1, val2); val1 == val2
ASSERT_NE(val1, val2); EXPECT_NE(val1, val2); val1 != val2
ASSERT_LT(val1, val2); EXPECT_LT(val1, val2); val1 < val2
ASSERT_LE(val1, val2); EXPECT_LE(val1, val2); val1 <= val2
ASSERT_GT(val1, val2); EXPECT_GT(val1, val2); val1 > val2
ASSERT_GE(val1, val2); EXPECT_GE(val1, val2); val1 >= val2
     

String Comparison

: String의 내용에 대해서 비교하여 테스트하는 방법입니다.

Fatal assertion Nonfatal assertion Verifies
ASSERT_STREQ(str1,str2); EXPECT_STREQ(str1,str2); 두 개의 C String이 동일한 값을 가지고 있을 때
ASSERT_STRNE(str1,str2); EXPECT_STRNE(str1,str2); 두 개의 C String이 서로 다른 값을 가지고 있을 때
ASSERT_STRCASEEQ(str1,str2); EXPECT_STRCASEEQ(str1,str2); 두 개의 C String이 대/소문자를 고려하지 않고 동일한 값을 가지고 있을 때
ASSERT_STRCASENE(str1,str2); EXPECT_STRCASENE(str1,str2); 두 개의 C String이 대/소문자를 고려하지 않고 다른 값을 가지고 있을 때
     

TEST_F와 TEST의 차이

: 위에서 사용하지는 않았지만 TEST_F외에도 TEST 구문이 존재한다.

TEST_F 의 경우에는 SetUp(), TearDown()을 통해서 TC수행 이전에 필요한 변수, 값들을 설정하고 TC가 끝난 이후에 할당한 변수, 값들을 해제하는 작업을 손쉽게 행할 수 있다. 만약 Instance 생성, Network 연결 등의 작업을 TC 수행 전에 반복적으로 해야하는 경우에는 SetUp()내에 이러한 작업을 해 두면 손쉽게 TC를 진행할 수 있다.

간단하게 말해서

  1. SetUp() 수행
  2. TEST_F(Fixture, add) 수행
  3. TearDown() 수행
  4. SetUp() 수행
  5. TEST_F(Fixture, diff) 수행
  6. TearDown() 수행

위의 순서대로 수행된다고 보면 된다.

TEST의 경우에는 위의 SetUP(), TearDown()이 필요없는 간단한 TC의 경우에 사용하면 좋을 듯 하다.

설치 오류

: 설치 후 gtest를 사용하기위해 #include "gtest/gtest.h" 를 하였을 때, 아래와 같은 오류를 본 적이 있는가?

Googletest/gtets: fatal error: gtest.h: No such file or directory

그렇다면 이 페이지(jayy-h.tistory.com/21)에서 알려주고 있는 설치 가이드를 따르기 바란다. 나 또한 위와 같은 오류를 겪었고, 오랜 시간 발품을 팔아 확인한 솔루션이다.

 

Reference 
googletest 공식문서

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 구문을 단독으로 사용하면 된다.

valgrind를 이용한 메모리 디버깅

: valgrind는 할당되지 않은 포인터 변수에 값을 넣는 것, malloc과 free를 수행하며 dangling pointer가 발생되거나 이중 free가 발생되는 문제와 같이 memory leak, invalid memory reference 등의 메모리 관련 문제점을 찾아주는 도구이다.

: 다음은 이러한 valgrind를 어떻게 사용하는지를 나타내는 예제이다.

#include <stdlib.h>
#include <iostream>

using namespace std;

int main()
{
    int *pInt = NULL;
    int cnt = 100;
    pInt = (int *)malloc(sizeof(int) * cnt);

    for (int i = 0; i < cnt; i++) {
        pInt[i] = i;
    }

    cout << pInt[cnt] << endl;

    for (int i = 0; i < cnt; i++) {
        free(&pInt[i]);
    }

    return 0;
}
Pre-requisite

valgrind를 사용하기 위해서는 해당 툴이 설치되어있어야 한다.
설치는 다음의 명령어를 통해 할 수 있다.

$ sudo apt  install valgrind

이제 다음의 명령어를 통해 컴파일한다.

jay@jay-VirtualBox:~/tutorial/valgrind$ g++ -o memory -g memory.cpp
jay@jay-VirtualBox:~/tutorial/valgrind$ ls
memory  memory.cpp

그 후, memory leak과 관련된 오류가 발생하였는지 다음의 명령어를 통해 알 수 있다. 이는 valgrind가 해당 프로그램을 수행시켜보며 메모리 관련된 문제가 발생하였는지를 판단하는 것이므로 test-case를 통해 memory leak을 판단하고자 할 때, test-case가 작성한 코드의 모든 범위와 조건들을 충족하는지에 따라 검출되는 버그가 다르다는 것에 유의하길 바란다.

jay@jay-VirtualBox:~/tutorial/valgrind$ valgrind --tool=memcheck --leak-check=yes --leak-resolution=high -v ./memory 
==39434== Memcheck, a memory error detector
==39434== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==39434== Using Valgrind-3.15.0-608cb11914-20190413 and LibVEX; rerun with -h for copyright info
==39434== Command: ./memory
==39434== 
--39434-- Valgrind options:
--39434--    --tool=memcheck
--39434--    --leak-check=yes
--39434--    --leak-resolution=high
--39434--    -v

...

==39604== 99 errors in context 2 of 2:
==39604== Invalid free() / delete / delete[] / realloc()
==39604==    at 0x483CA3F: free (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==39604==    by 0x1092AA: main (memory.cpp:19)
==39604==  Address 0x4dafc84 is 4 bytes inside a block of size 400 free'd
==39604==    at 0x483CA3F: free (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==39604==    by 0x1092AA: main (memory.cpp:19)
==39604==  Block was alloc'd at
==39604==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==39604==    by 0x109214: main (memory.cpp:10)

lcov, genhtml을 이용한 coverage 깔끔하게 보기

NOTE
이전의 gocv를 이용한 커버리지 정보 확인과 밀접한 관계가 있는 글 입니다.
이 글을 풍부하게 이해하기 위해서는 gcov 관련 게시글을 보고 오시길 바랍니다.
Pre-requsite

gcov의 결과를 웹 페이지를 통해 깔끔하게 보여주기 위해 lcov를 사용한다.
이를 위해 다음의 명령어를 이용해 lcov 설치를 진행하자.

sudo apt install lcov

우선 다음과 같은 프로그램이 있다고 가정하자.

// main.cpp

#include <iostream>
using namespace std;

void func(int dividend, int divisor)
{
    cout << "Incoming variable: " << dividend << " " << divisor << endl;

    int quotient = 0;
    int remainder = 0;
    try {
        if (divisor == 0)
            throw exception();
        quotient = dividend / divisor;
        remainder = dividend % divisor;
    } catch (...) {
        cerr << "Exception occured!" << endl;
        quotient = 0;
        remainder = dividend;
    }

    switch(remainder) {
    case 0:
        cout << "Dividend is multiplier of divisior" << endl;
        break;
    case 100:
        cout << "Cannot reach here under this circumstances!" << endl;
        break;
    default:
        cout << "Nothing" << endl; 
        break;
    }
}

int main()
{
    for (int i = 0; i < 100; ++i) {
        for (int j = 0; j < 100; ++j) {
            func(i, j);
        }
    }

    return 0;
}

다음의 명령어를 이용해 컴파일을 수행한다.

g++ -o main main.cpp -fprofile-arcs -ftest-coverage -g

여기서 -fprofile-arcs -ftest-coverage 옵션은 소스의 각 베이직 블록에 프로파일링 코드를 삽입하라는 옵션이다.

이 명령을 수행하면 다음과 같은 파일들이 생성된다.

jay@jay-VirtualBox:~/tutorial/gcov$ g++ -o main main.cpp -fprofile-arcs -ftest-coverage -g
jay@jay-VirtualBox:~/tutorial/gcov$ ls
main  main.cpp  main.gcno

./main을 통해 프로그램을 한 번 수행시킨다. 이는 다음의 main.gcda 파일을 생성시킨다.

jay@jay-VirtualBox:~/tutorial/gcov$ ./main
jay@jay-VirtualBox:~/tutorial/gcov$ ls
main  main.cpp  main.gcda  main.gcno

gcov main.cpp를 수행한다.

jay@jay-VirtualBox:~/tutorial/gcov$ gcov main.cpp 
File 'main.cpp'
Lines executed:89.29% of 28
Creating 'main.cpp.gcov'

File '/usr/include/c++/9/iostream'
No executable lines
Removing 'iostream.gcov'

File '/usr/include/c++/9/bits/exception.h'
Lines executed:100.00% of 1
Creating 'exception.h.gcov'

jay@jay-VirtualBox:~/tutorial/gcov$ ls
exception.h.gcov  main  main.cpp  main.cpp.gcov  main.gcda  main.gcno

lcov --rc lcov_branch_coverage=1 --capture --directory ${gcov_data_file_directory} --output-file {output_file_name} 명령을 통해 gcov 데이터 파일을 이용해 genhtml에 쓰일 파일을 생성한다.

jay@jay-VirtualBox:~/tutorial/gcov$ lcov --rc lcov_branch_coverage=1 --capture --directory ./ --output-file generated.info
Capturing coverage data from ./
Found gcov version: 9.3.0
Using intermediate gcov format
Scanning ./ for .gcda files ...
Found 1 data files in ./
Processing main.gcda
Finished .info-file creation
jay@jay-VirtualBox:~/tutorial/gcov$ ls
exception.h.gcov  main      main.cpp.gcov  main.gcno
generated.info    main.cpp  main.gcda

여기서 --rc lcov_branch_coverage=1 옵션은 분기 커버리지를 보기 위한 옵션으로 genhtml에서도 쓰인다.

마지막으로 genhtml ${info_file} --branch-coverage --output-directory ${output_directory} 명령으로 coverage 정보를 담는 html 파일을 만든다.

jay@jay-VirtualBox:~/tutorial/gcov$ genhtml generated.info --branch-coverage --output-directory ./
Reading data file generated.info
Found 2 entries.
Found common filename prefix "/usr/include/c++/9"
Writing .css and .png files.
Generating output.
Processing file /home/jay/tutorial/gcov/main.cpp
Processing file bits/exception.h
Writing directory view page.
Overall coverage rate:
  lines......: 89.7% (26 of 29 lines)
  functions..: 100.0% (3 of 3 functions)
  branches...: 76.9% (10 of 13 branches)
jay@jay-VirtualBox:~/tutorial/gcov$ ls
amber.png         generated.info     index-sort-f.html  main.gcda
bits              glass.png          index-sort-l.html  main.gcno
emerald.png       home               main               ruby.png
exception.h.gcov  index.html         main.cpp           snow.png
gcov.css          index-sort-b.html  main.cpp.gcov      updown.png

이렇게 되면 다음과 같이 coverage 정보를 시각적으로 아름답게 표현해주는 html 파일이 생성되었다.

 

genhtml을 통해 생성된 파일

gcov를 통한 커버리지 정보 알아보기

: gcov는 gcc에 포함된 유틸리티로 코드 커버리지를 파악하는 데 쓰인다. 이는 어떤 부분이 얼마만큼 수행되는지, 어느 부분이 한 번도 수행되지 않는지를 파악할 수 있고, 이런 정보를 이용해 불필요한 코드 제거에 사용되어질 수 도 있다. 앞서 설명한 gprof와 비교하자면, gprof는 함수 단위의 프로파일링 정보만을 제공하는 반면에 gcov는 베이직 블록 단위의 프로파일링을 수행해 함수 내 어떤 루프가 많이 수행되었는지, if-else 블록에서 어느 블록이 많이 수행되었는지를 알 수 있다.

// main.c
#include <stdio.h>

void func1()
{
    printf("Hello World \n");
}

void func2(int delay)
{
    printf("Delay: %d", delay);

    while (delay--);
}

int main()
{
    for (int i = 0; i < 10; ++i) {
        func1();
    }

    for (int i = 0; i < 100; ++i) {
        func2(i);
    }

    return 0;
}

위와 같은 파일이 있다고 할 때, gcov를 이용한 라인 커버리지를 확인하는 방법은 다음과 같다.

  1. gcc -o main main.c -fprofile-arcs -ftest-coverage -g 명령으로 컴파일을 수행한다.
    • -fprofile-arcs -ftest-coverage 옵션은 소스의 각 베이직 블록에 프로파일링 코드를 삽입하라는 옵션이다.
  2. ./main을 통해 프로그램을 한 번 수행시킨다.
    • 이는 main.gcda, main.gcno 파일을 생성시킨다. 이 두 파일에 소스파일의 베이직 블록에 대한 프로파일링 정보가 포함된다.
  3. gcov main.c 명령을 수행하여 main.c.gcov파일을 생성시킨다.
  4. cat main.c.gocv 명령을 통해 프로파일링 정보를 확인한다.
  1: 1: #include <stdio.h>
  -: 2: 
  -: 3: void func1()
 10: 4: {
 10: 5:     printf("Hello World \n");
 10: 6: }
  -: 7: 
  -: 8: void func2(int delay)
100: 8: {
...
  • 각 라인의 맨 앞에 있는 숫자는 해당 라인이 몇 번 호출되었는지를 나타낸다.

readelf 명령으로 ELF파일의 각종 정보 보기

: readelf 명령은 EFL 파일의 각종 정보를 보기 위한 명령이다. 사용 가능한 옵션은 아래와 같다.

옵션 설명
-a 모든 정보를 출력
-h ELF 헤더 정보를 출력
-l 프로그램 헤더를 출력
-S 섹션 헤더를 출력
-e 모든 헤더를 출력
-s 심볼 테이블을 출력
-n note 섹션의 정보를 출력
-r 재배치 섹션의 정보를 출력
-d 동적 섹션의 정보를 출력

: 더 많은 정보를 원한다면, readelf --help 명령을 통해 알아보자.

'빌드&테스트도구 > 기타' 카테고리의 다른 글

nm - 심볼 테이블 확인  (0) 2020.09.22
gprof - 프로파일 정보 확인  (0) 2020.09.21
Mangling - 심볼명 확인  (0) 2020.09.20

nm으로 ELF 포맷의 심볼 테이블 보기

: 바이너리의 심볼을 볼 때 사용하는 명령어이다.
: nm {binary_file_name} 명령어로 사용한다. 그 결과는 아래와 같이 출력된다.

// Address    symbol_type   symbol_name
08044323 T func1 
...

: 두 번째 필드의 종류는 다음과 같다.

심볼 타입 명 설명
A 주소가 절대적이어서 변경되지 않는 심볼
B 심볼이 BSS 섹션에 존재(un-initialized global varible 등)
C 공통 심볼(un-initialized data)
D 심볼이 data 섹션에 위치(initialized global variable 등)
G 작은 공간의 전역 객체, data 섹션에 위치(initialized data 등)
I 심볼이 다른 심볼의 indirect
N 디버그 심볼
R Read-only 섹션에 존재
S 작은 객체를 위한 BSS 섹션의 심볼
T text 섹션에 있는 심볼(code)
U 정의되지 않은 심볼
V 확정적이지 않은 심볼, 링크 시 확정적인 심볼을 만나면 변경
W 확정적이지 않은 심볼, 링크 시 확정적인 심볼을 만나지 못하면 0으로 설정
- stabs 심볼
? 알수없는 심볼

: nm에서 사용할 수 있는 유용한 옵션은 다음과 같다.

옵션 설명
-a 디버깅 심볼까지 포함해 모든 심볼 출력
-D 동적 심볼만 출력(공유 라이브러리의 심볼 등)
-g 외부 심볼만 출력
-v 주소로 출력 정렬
-s 정적 라이브러리의 심볼 출력 시, 어떤 오브젝트 파일에 존재하는 지 출력
-u 정의되지 않은 심볼만 출력

'빌드&테스트도구 > 기타' 카테고리의 다른 글

readelf - ELF파일 정보 보기  (0) 2020.09.23
gprof - 프로파일 정보 확인  (0) 2020.09.21
Mangling - 심볼명 확인  (0) 2020.09.20

+ Recent posts