----------------
title : 씹어먹는 C++ - <20 - 2. 코드 부터 실행 파일 까지 - 컴파일 (Compile)>
cat_title : 20 - 2.코드 부터 실행 파일 까지 - 컴파일 (Compile)
path : /C++
publish_date : 2020-10-20
next_page : 321
----------------

이번 강좌에서는

* 유일 정의 규칙 (One Definition Rule)
* 정의와 선언의 차이
* 목적 코드 생성

에 대해서 다루어 보겠습니다.

![](/img/ChewingCpplogo.png)

먼저 [앞선 강의](/319) 에서 7 번째 단계가 끝난 다면 각 코드 별로 해석 유닛 (TU) 를 생성한다고 하였습니다. 물론 이 TU 가 제대로 생성되기 위해서는 우리가 흔히 생각하는 C++ 상에서의 문법 오류가 없어야 겠지요. 예를 들어서 변수들과 함수들의 타입이 맞아야 하고, 또 적절한 연산자를 호출해야 합니다.

이러한 자질 구레한 것들은 빼고도 우리가 잘 인지하지 못하는 TU 에 적용되는 중요한 규칙 하나가 있습니다. 바로 **각 TU 에 존재하는 모든 변수, 함수, 클래스, enum, 템플릿 등등의 정의(Definition) 은 유일**해야 하고 **inline 이 아닌 모든 함수의 변수들의 정의는 전체 프로그램에서 유일해야 한다** 라는 **유일 정의 규칙 (One Definition Rule - 줄여서 ODR)** 입니다.

그렇다면 C++ 에서 이야기 하는 **정의** 란 과연 무엇일까요?

### 정의 (Definition) 와 선언 (Declaration)

우리는 종종 정의와 선언을 혼동해서 사용하곤 합니다. 하지만 C++ 에서 이 둘은 엄연히 다른 개념입니다. 먼저 **선언 (Declaration) 이란 TU 에 새로운 이름을 도입하거나, 기존에 선언된 이름을 재선언 하는 것**입니다. \sidenote{C++ 표준 6.2.1: A declaration may introduce one or more names into a translation unit or redeclare names introduced by previous declarations.}

예를 들어서

```cpp-formatted
int f();
```

의 경우 `f` 라는 함수를 **선언** 하였습니다.

그리고 정의는 선언을 포함하는 개념으로, **선언된 개체를 완전히 정의함을 뜻합**니다. 따라서 모든 정의는 선언입니다. 예를 들어서

```cpp-formatted
int a;
```

의 경우 `a` 라는 `int` 변수를 **정의** 한 것입니다.

뿐만 아니라 아래 몇 가지 경우를 제외하면, 모든 선언도 정의 입니다. 선언이지만 정의가 아닌 경우를 살펴보자면

```cpp-formatted
int f();
```

위에서 보았듯이 `f` 라는 함수를 선언하였지만, 정의한 것은 아닙니다. `f` 를 정의하기 위해서는 반드시 함수의 몸체를 제공해야 합니다. 예를 들어서

```cpp-formatted
int f() { return 0; }
```

의 경우 `f` 를 정의한 것입니다. 클래스의 경우도 비슷합니다.

```cpp-formatted
class A;  // A 를 선언
```

의 경우 클래스 `A` 를 선언하였지만 정의하지는 않았습니다. 반면에

```cpp-formatted
class A {};
```

의 경우 `A` 를 선언한 것입니다.

일반적인 변수의 경우 선언과 정의는 동일합니다. 예를 들어서

```cpp-formatted
int a;
```

는 `a` 라는 변수를 정의한 것입니다. 하지만, `extern` 지정자가 들어간 선언의 경우 명시적으로 초기화 되지 않는다면 선언입니다.

```cpp-formatted
extern const int a;      // a 를 선언하였지만 정의하지 않음
extern const int b = 1;  // b 를 정의함
```

위 경우 `a` 는 선언이지만 정의가 아닙니다. 반면에 `b` 의 경우 1 로 초기화 되었으므로 정의 입니다.

클래스 정의 내부에 `inline` 이 아닌 `static` 멤버의 경우 정의 입니다.

```cpp-formatted
struct S {
  int n;                // S::n 정의
  static int i;         // S::i 를 선언하지만 정의는 아님
  inline static int x;  // S::x 를 정의
};                      // S 를 정의
int S::i;               // S::i 를 정의
```

그 외에도 선언이지만 정의가 아닌 경우가 몇 가지 있습니다. 자세한 내용은 [여기](https://en.cppreference.com/w/cpp/language/definition) 와 [여기](https://eel.is/c++draft/basic.def#1) 를 참조해주시기 바랍니다.

### 유일 정의 규칙 (One Definition Rule)

그렇다면 앞서 언급했던 유일 정의 규칙 (ODR) 을 다시 살펴보도록 하겠습니다.

> 각 TU 에 존재하는 모든 변수, 함수, 클래스, `enum`, 템플릿 등등의 정의(Definition) 은 유일 해야 하고 `inline` 이 아닌 모든 함수의 변수들의 정의는 전체 프로그램에서 유일해야 한다

이 말은 즉슨 다음과 같은 두 사실을 내포하고 있습니다.

먼저 첫 번째 문장 부터 살펴봅시다. **각 TU 에 존재하는 모든 변수, 함수, 클래스 등등의 정의는 유일해야 한다**는 점입니다. 이 말을 다시 보자면, TU 안에 같은 선언은 여러개 있어도 괜찮다는 의미 입니다. 실제로

```cpp-formatted
int f();  // f 의 선언
int f();  // f 의 선언
int f();  // f 의 선언

int main() {}
```

와 같은 코드는 아무런 문제 없이 컴파일 됩니다. 왜냐하면 `int f()` 는 `f` 의 선언이지 정의가 아니기 때문이죠. 아주 올바른 C++ 코드 입니다.

그렇다면 아래와 같은 코드는 어떨까요?

```cpp-formatted
int f() {  // f 의 정의
  return 0;
}

int f();  // f 의 선언
int f();  // f 의 선언

int main() {}
```

마찬가지로 ODR 규칙에 위배되지 않고 잘 컴파일 됨을 알 수 있습니다. 왜냐하면 `f` 의 정의는 유일하기 때문이죠. 문제는 `f` 의 정의가 여러개일 경우 입니다.

```cpp-formatted
int f() {  // f 의 정의
  return 0;
}

int f() {  // f 의 정의 <-- ODR 위반
  return 0;
}

int f();  // f 의 선언
다
```

위와 같은 경우 컴파일 하였다면 아래와 같은 오류가 발생하게 됩니다.

```compiler-warning
test.cc:5:5: error: redefinition of ‘int f()’
    5 | int f() {
      |     ^
test.cc:1:5: note: ‘int f()’ previously defined here
    1 | int f() {
      |     ^
```

위와 같이 `f` 가 여러번 정의되었다고 컴파일러가 이야기 합니다.

그렇다면 두 번째 문장을 다시 살펴봅시다. **inline 이 아닌 모든 함수의 변수들의 정의는 전체 프로그램에서 유일** 해야 한다. 이 말은 즉슨, `inline` 으로 정의되지 않는 모든 함수들과 변수들의 경우 프로그램을 구성하는 모든 TU 에서 정의가 단 하나 있어야 합니다. 반면에 `inline` 인 변수나 함수의 경우 이를 사용하고자 하는 TU 안에 반드시 정의되어 있어야 합니다.

예를 들어서 첫 번째 TU 안에

```cpp-formatted
// TU 1
int f();  // 선언
```

가 있고 두 번째 TU 안에도

```cpp-formatted
// TU 2
int f() {  // 정의
  return 1;
}
```

가 있다고 해봅시다. 이는 ODR 위반이 아닙니다. 왜냐하면 나중에 TU 1 과 TU 2 가 합쳐졌을 때 정의는 딱 하나가 있게 되기 때문이죠. 반면에

```cpp-formatted
// TU 1
int f() {  // 정의
  return 1;
}
```

가 있고 두 번째 TU 안에도

```cpp-formatted
// TU 2
int f() {  // 정의
  return 1;
}
```

와 같이 정의가 있다면 TU 1 과 TU 2 가 합쳐졌을 때, ODR 을 위반하게 됩니다.

따라서 이와 같은 이유로 보통 다른 파일들에서 사용하는 함수를 정의하려면, 헤더파일에 함수의 선언을 써놓고, 단 한개의 소스 파일에 함수의 정의를 쓰게 됩니다. 예를 들어서

```cpp-formatted
// a.h
int SomeFunction();
```

과 같이 헤더파일에 함수를 선언을 하고

```cpp-formatted
// a.cc
int SomeFunction() {  // 정의
  return 0;
}
```

와 같이 `SomeFunction` 을 유일하게 정의해놓는다면

```cpp-formatted
// b.h
#include "a.h"
int main() { SomeFunction(); }
```

을 하더라도 문제가 되지 않습니다. 왜냐하면 `b.h` 의 TU 는

```cpp-formatted
// b.cc
int SomeFunction();  // 선언
int main() { SomeFunction(); }
```

와 같이 될 것이기 때문이죠. 반면에 아래와 같이

```cpp-formatted
// a.h
int SomeFunction() { return 0; }
```

헤더파일에 함수의 정의를 적어놓고 다른 파일에서 이 헤더파일을 `include` 한다면

```cpp-formatted
// b.cc
#include "a.h"
int main() { SomeFunction(); }
```

결국 두 개의 서로 다른 TU 에 `SomeFunction` 의 정의가 들어가게 되서 ODR 규칙을 위반하게 됩니다.

### inline 키워드의 의미

앞서 ODR 에서 `inline` 인 변수나 함수의 경우 이를 사용하고자 하는 TU 안에 반드시 정의되어 있어야 한다고 하였습니다. 원래 `inline` 키워드가 처음 도입되었을 때 의미는 컴파일러에게 **이 함수를 호출하는 문장을 그냥 이 함수의 내용으로 치환시켜도 된다** 라는 의미였습니다. (단 한 번도 이 함수를 **반드시 인라인 화 시켜야 한다** 라는 의미 였던 적은 없습니다) \sidenote{사용자가 함수를 인라인으로 정의하는 이유는 함수를 호출하는 작업이 공짜인 것이 아니기 때문입니다. 그 대신 함수를 호출하는 모든 부분이 인라인화 된 함수의 코드로 치환되기 때문에 실행 파일의 크기가 조금 더 커질 수 있는 문제가 있습니다.}

하지만 현재의 C++ 컴파일러는 굉장히 똑똑해졌기 때문에 우리가 굳이 `inline` 이라고 명시 하지 않아도 만일 인라인 하는게 성능 면에서 낫다고 생각하는 경우 그냥 함수를 인라인 해버립니다. 반대로 `inline` 인 함수여도 컴파일러가 생각했을 때 인라인 하지 않는 것이 오히려 효율이 낫다고 판단한다면 인라인화 하지 않습니다. 따라서 `inline` 키워드는 그냥 다음과 같은 의미를 나타낸다고 보시는 것이 낫습니다.

> 이 함수는 여러개의 TU 에 정의되어 있어도 괜찮음!

쉽게 말해 `inline` 인 함수의 경우 전체 프로그램에서 여러 군데에 정의가 되어 있어도 상관이 없습니다만 해당 함수를 사용하는 TU 안에서는 인라인 함수의 정의가 반드시 들어 있어야만 합니다. 이렇게 `inline` 키워드의 의미가 변질되었기 때문에 C++ 17 에서는 일반적인 변수 자체도 **여러 정의를 허용한다** 라는 의미에서 `inline` 으로 사용할 수 있습니다.

이와 반대로 `inline` 이 아닌 함수의 경우 사용하기 위해서 반드시 TU 에 해당 함수의 정의가 있을 필요가 없지만, 대신 전체 TU 에 정의가 반드시 단 한개 있어야만 합니다.

예를 들어서 `inline` 함수는 헤더파일에 정의해도 상관 없습니다.

```cpp-formatted
// a.h
inline int SomeFunction() { return 0; }
```

```cpp-formatted
// b.cc
#include "a.h"
int main() { SomeFunction(); }
```

예를 들어 위 같이 `a.h` 에 `SomeFunction` 을 정의하고 다른 파일들에서 `a.h` 를 `include` 하더라도 문제될 것이 없습니다. 반면에 일반적인 함수의 정의 처럼 아래와 같이 헤더에만 선언을 해놓고 구현을 다른데에서 한다면


```cpp-formatted
// a.h
inline int SomeFunction();
```

```cpp-formatted
// a.cc
inline int SomeFunction() { return 0; }
```

```cpp-formatted
// b.cc
#include "a.h"
int main() { SomeFunction(); }
```

이 경우 `b.cc` 의 TU 에 `SomeFunction` 의 정의가 없기 때문에

```compiler-warning
In file included from main.cc:1:
a.h:4:13: warning: inline function ‘int a()’ used but never defined
    4 | inline int a();
      |             ^
```

와 같은 오류가 발생하게 됩니다.

한 가지 재미있는 점은 클래스 내부에 정의되어 있는 함수들은 자동으로 `inline` 이란 점입니다. 즉

```cpp-formatted
class A {
 public:
  int Func() { return 0; }
};
```

위와 같이 클래스 `A` 에 정의된 멤버 함수 `Func` 은 굳이 `inline` 으로 명시하지 않아도 컴파일러가 알아서 `inline` 으로 취급합니다. 따라서 위 클래스 파일을 여러 파일들에 `include` 해도 문제될 것이 없죠. 반면에

```cpp-formatted
class A {
 public:
  int Func();
};

int A::Func() { return 0; }
```

위와 같이 클래스 안에 함수의 정의가 없고 선언만 있을 경우 `inline` 으로 분류되지 않습니다. 따라서 함수의 정의 부분인

```cpp-formatted
int A::Func() { return 0; }
```

이 부분의 경우 `inline` 이 아닌 함수로 취급되어서 모든 TU 전체에 정의가 딱 하나 있어야겠죠. 따라서 멤버 함수를 클래스 밖에 따로 정의할 때에는 대부분의 경우 `cc` 파일에 정의하게 됩니다.

### Code Generation

```lec-warning
아래 부분은 리눅스 상에서 적용되는 내용입니다. 이전 강좌에서 이야기 하였듯이 윈도우즈의 경우 PE 파일을 생성하며 해당 파일 포맷은 리눅스에서 주로 사용되는 ELF 파일 포맷과는 조금 차이가 있습니다. (그래도 큰 틀에서는 비슷합니다.) 따라서 아래 제공하는 예제들과 파일들을 분석하는데 사용하는 툴들 (`objdump`, `readelf`) 는 모두 리눅스 상에서만 사용할 수 있습니다. 

물론 윈도우에서도 WSL 를 통해서 우분투 리눅스와 같은 유명한 리눅스 배포판들을 간단히 사용할 수 있기 때문에 아래 예제들을 실행해보는데 큰 문제가 없을 것이라 생각합니다.
```

앞서 TU 에서 적용되는 중요한 규칙인 ODR 에 대해서 살펴보았습니다. 각각의 TU 에서 문법이 맞는지 확인하고 ODR 규칙들을 적용하고 나면 컴파일러는 각각의 TU 별로 실제 어셈블리 코드를 생성 (Code Generation)하게 됩니다. 한 가지 중요한 점은 컴파일러가 어셈블리 코드를 생성할 때에는 모든 TU 들을 독립적으로 생성하게 됩니다.

따라서 TU 1 과 TU 2 가 있을 때 TU 1 의 어셈블리는 딱 TU 1 만을 보고 결정되지 다른 TU 들은 보지 않는다는 것입니다.

그렇다면 한 가지 문제가 있습니다. 앞서 ODR 규칙에 따르면 `inline` 이 아닌 함수의 정의는 전체 TU 들에 대해 유일하다고 하였습니다. 예를 들어서

```cpp-formatted
// TU 1
int SomeFunction();  // 선언

int main() { SomeFunction(); }
```

와 TU 2 에 아래와 같이 있다고 해봅시다.

```cpp-formatted
// TU 2
int SomeFunction() { return 0; }
```

만일 TU 1 에서 TU 2 의 정의된 함수 `SomeFunction` 사용한다고 해봅시다. 그렇다면 TU 1 의 코드 생성 단계에서는 함수를 호출 할 때 해당 함수가 어디 있는지 알아야 하는데 해당 함수는 TU 2 에 정의되어 있기 때문에 도무지 알 수 없기 때문이지요.

그렇다면 TU 1 을 컴파일 한 목적 코드에는 도대체 뭐가 들어 있을까요?

```info-term
$ file b.o
b.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
```

리눅스 상에서 file 프로그램을 사용하면, 해당 파일의 대략적인 정보를 알 수 있습니다. file 프로그램에 따르면 우리가 생성한 목적 파일은 사실 일반적인 ELF 파일 입니다. 다만 나와 있듯이 LSB (리틀 엔디안) 형식의 relocatable 파일 이죠. 이 **재배치 가능 하다 (Relocatable)** 라는 의미는 이 ELF 파일을 특정 위치에 배치할 수 있다는 의미 입니다.

사실 곰곰히 생각해보면 그럴 수 밖에 없는 것이 실행 파일을 만들 때에서 비로소 우리가 정의하였던 함수들의 위치가 정해지게 됩니다. 따라서 링킹 단계에서 이 생성된 목적파일들을 **재배치** 시켜서 정확한 위치에 자리잡게 하기 위함이지요.

그렇다면 해당 목적파일을 조금 뜯어보도록 합시다.

```info-term
$ objdump -S b.o

b.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:	f3 0f 1e fa          	endbr64 
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	e8 00 00 00 00       	callq  d <main+0xd>
   d:	b8 00 00 00 00       	mov    $0x0,%eax
  12:	5d                   	pop    %rbp
  13:	c3                   	retq   
```

위는 TU 1 의 목적 코드를 `objdump` 라는 프로그램을 사용해서 그 어셈블리를 출력해본 것입니다. `SomeFunction()` 을 호출하는 부분이 바로

```info-term
   8:	e8 00 00 00 00       	callq  d <main+0xd>
```

이 부분인데, 원래는 `e8` 뒤에 현재 위치로 부터 **얼마만큼 떨어져 있는 곳에 있는 함수를 실행할지 그 오프셋 값이 들어가 있어야 합니다**. 하지만 지금은 위와 같이 그냥 0 으로 채워져있죠. 왜냐하면 컴파일 단계에서는 SomeFunction 이 도대체 어디에 배치될 지 알 수 없기 때문에 링킹 과정이 이루어지기 전 까지 `e8` 뒤에 어떤 오프셋 값을 넣어야 하는지는 알 수 없습니다. 그래서 위 처럼 그냥 0 으로 채워놓게 됩니다.

만약에 링킹 과정에서 `SomeFunction` 을 찾을 수 없다면 해당 부분을 채울 수 없겠죠. 따라서 아래와 같이 종종 보이는 *함수를 찾을 수 없다* 라는 오류가 컴파일러 단에서 발생하는 것이 아니라 링크 단에서 발생하는 이유도 그것 입니다.

```compiler-warning
/usr/bin/ld: /tmp/ccvTlhA3.o: in function `main':
b.cc:(.text+0x9): undefined reference to `SomeFunction()'
collect2: error: ld returned 1 exit status
```

위와 같이 TU 2 를 포함하지 않고 TU 1 만 가지고 실행파일을 생성하고 한다면 링커 (위 경우 `ld`) 에서 오류가 발생한 것을 볼 수 있습니다.

물론 링커 입장에서 어셈블러가 생성한 명령이 정말로 `e8 00 00 00 00` 일 수 도 있기 때문에 목적 파일에 링커에게 알려주기 위해서 **이 부분을 이런 식으로 고쳐라** 라는 정보를 남겨놓게 됩니다. 이 정보를 보기 위해서는 `readelf` 프로그램을 사용하면 됩니다.

```info-term
$ readelf -r b.o

Relocation section '.rela.text' at offset 0x230 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000009  000b00000004 R_X86_64_PLT32    0000000000000000 _Z12SomeFunctionv - 4

Relocation section '.rela.eh_frame' at offset 0x248 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0
```

`readelf` 프로그램은 리눅스에서 `ELF` 파일 정보를 보기 좋게 출력해주는 프로그램입니다. `-r` 옵션을 주게 되면 해당 파일의 **재배치 테이블(Relocation Table)** 정보를 출력하게 됩니다. 이 재배치 테이블 중 `.rela.text` 에선 해당 목적 파일에서 링킹 시에 수정해야할 곳의 위치와 어떠한 식으로 수정해야 할 지에 대해서 설명하고 있습니다.

위 경우 오프셋 9 의 위치에 (정확히 e8 바로 뒤에 00 00 00 00 부분을 의미 합니다) `_Z12SomeFunctionv` 심볼의 정보를 `R_X86_64_PLT32` 방식으로 덮어 씌우라고 링커에게 알려주는 것입니다.

이 부분에 대해서 두 가지 궁금증이 있는데

1. 도대체 왜 우리 `SomeFunction` 이 `_Z12SomeFunctionv` 와 같은 괴상한 이름으로 바뀌었는지
2. `R_X86_64_PLT32` 은 도대체 뭔지

에 대해서는 다음 강의에서 다루어보도록 하겠습니다.

##@ chewing-cpp-end