C++/문법 및 메소드(STL)

[C++][Build] 컴파일 단계 - 심볼, 저장 방식 지정자, 그리고 이름 맹글링

pledge24 2025. 4. 14. 17:55

심볼 테이블 예제. 출처: https://iq.opengenus.org/symbol-table-in-compiler/

개요

컴파일러는 컴파일 단계에서 소스 코드를 목적 코드(예: .obj, .o 파일)로 변환하여, 이후 단계인 링커 단계에서 링커가 사용할 수 있도록 한다. 이 목적 코드에는 크게 두 가지 핵심 요소가 포함되는데, 첫 번째 요소로는 실행 가능한 기계어 수준의 “명령어”, 두 번째 요소로는 링커가 코드 조각들을 연결할 때 참조하는 “심볼 테이블”이다. 이 중 심볼 테이블은 외부 또는 내부 심볼을 식별하고 연결하는 데 필요한 정보를 담고 있으며, 이를 이해하려면 먼저 심볼(symbol)이라는 개념을 정확히 파악할 필요가 있다.

 

따라서, 이 글에서는 심볼이 무엇인지, 그리고 어떤 종류의 심볼이 존재하는지 살펴본다. 이어서, 컴파일러가 심볼을 심볼 테이블에 추가할 때 사용하는 저장 방식 지정자(storage class specifier)가 어떤 역할을 하는지 설명하고, 마지막으로 이름 맹글링(name mangling)이 심볼 생성 과정에서 왜 필요한지를 간단히 정리한다.

 


심볼(Symbol)이란?

  • C++에서의 심볼링커가 코드 내의 식별자(변수, 함수, 클래스 등)를 식별하고 참조하는 데 사용하는 이름을 의미한다.
  • 심볼의 유형은 크게 두 가지로, 값을 저장하는 변수인 "데이터 객체(Data Objects)"와 실행 가능한 코드 블록인 "함수 객체(Function Objects)"가 있다.
    • 이외에도 타입(클래스, 구조체 등의 정의), namespace, 템플릿 등의 심볼 유형이 있다.
  • 심볼은 컴파일러에 의해 생성되며, 컴파일러는 소스 코드를 해석하여 각 심볼의 가시성(visibility) 정보를 생성하고, 이를 목적 파일(.o)의 심볼 테이블에 저장한다.
    • 즉, 컴파일러는 소스 코드의 식별자를 링커가 참조할 수 있는 형태로 가공하여 심볼을 생성한다.
  • 이후, 목적 파일(.o)의 심볼 테이블에 저장된 심볼들은 링커의 링킹 작업에 의해 위치가 확정된다.

 


저장 방식 지정자(Storage Class Specifier)

  • C++에서 저장 방식 지정자심볼들의 저장되는 위치와 방식을 지정하는 키워드를 의미한다.
  • 지정 방식 지정자를 통해 심볼들의 저장 기간(Strorage duration)링크 방식(Linkage)을 결정하며, 현재 C++에서 허용하는 저장 방식 지정자 종류는 아래와 같이 총 4가지가 존재한다. (auto는 C++11, register은 C++17에 삭제됨)
    • static
    • thread_local
    • extern
    • mutable
  • 위 4가지 지정 방식 지정자 중, mutable 키워드는 다른 지정 방식 지정자들과 달리 저장 기간과 링크 방식에 영향을 주지 않고, 단지 const 객체 내에서 특정 멤버 번수의 상수성(constness)을 제거하기 위한 용도로 사용된다.

 


저장 기간(Storage duration)

  • 저장 방식 지정자로 결정되는 요소 중 하나인 저장 기간은 심볼 유형중 데이터 객체의 수명(lifetime)을 결정한다.
  • 저장 기간은 크게 4가지로 구분되며, 프로그램의 모든 데이터 객체들은 이 4가지 중 하나의 저장 기간을 반드시 가지게 된다. 저장 기간의 4가지 유형은 아래와 같다.
    • 자동(automatic) 저장 기간
    • 정적(static) 저장 기간
    • 쓰레드(thread) 저장 기간
    • 동적(dynamic) 저장 기간
  • 지정된 저장 기간은 링커가 데이터 객체, 즉, 변수의 데이터를 어느 메모리에 배치할 것인지 결정할 때 사용한다.

저장 기간의 4가지 유형

 

자동 (automatic) 저장 기간

  • 자동 저장 기간에 해당하는 데이터 객체들은 스코프(’{ }’) 안에 정의된 객체들로, 객체가 선언된 스코프를 벗어나는 순간 자동으로 소멸된다.
  • 저장 방식에 영향을 주는 지정자(static, thread_local, extern)를 붙이지 않은 모든 지역 객체들이 해당 자동 저장 기간을 가지게 된다.
    • 아래 예시에 있는 counter, ex, a는 전부 자동 저장 기간에 해당한다.
class Example {
    mutable int counter;  // mutable 멤버 변수 <- 클래스의 저장 기간을 따라감
};

void function() {
    Example ex;           // 지역 객체 <- 자동 저장 기간    
    int a;                // 지역 변수 <- 자동 저장 기간                   
}

 

 

정적(static) 저장 기간

  • 정적 저장 기간에 해당하는 데이터 객체들은 함수 밖에 정의된 것들(ex. namespace 내에 정의된 변수, 전역 변수)이나 정적 지역 변수들로, 프로그램이 시작 시 할당되어 프로그램 종료 시 소멸되는 객체들이다.
  • static 또는 extern 지정자를 붙인 데이터 객체들이 해당 정적 저장 기간을 가지게 된다.
    • 아래 코드에 있는 a, b, x는 전부 정적 저장 기간을 가진다.
    • 즉, static 지정자를 붙이지 않아도 extern 변수나 namespace 내에 정의된 변수처럼 정적 저장 기간을 가지는 데이터 객체가 존재한다.
// a.h
int a;              // 전역 변수 <- 정적 저장 기간
namespace ss {
    int b;            // 네임스페이스 변수 <- 정적 저장 기간
}

// b.h
extern int a;       // extern 변수 <- 정적 저장 기간
int func() {
    static int x;     // 함수 내 static 변수 <- 정적 저장 기간
}

 

 

쓰레드(thread) 저장 기간

  • 쓰레드 저장 기간에 해당하는 데이터 객체들은 thread_local 지정자를 붙인 변수들로, 쓰레드 시작 시 할당되어 쓰레드가 종료 시 소멸되는 객체들이다.
  • 각 쓰레드마다 해당 개체의 복사본이 생성되며, 쓰레드 전용 메모리 공간에 할당된다.
    • 아래 코드에서 변수 i가 쓰레드 저장 기간을 가진다.
#include <iostream>
#include <thread>

thread_local int i = 0;

void g() {std::cout << i; }

void threadFunc(int init){
    i = init;
    g();
}

int main(){
    std::thread t1(threadFunc, 1);
    std::thread t2(threadFunc, 2);
    std::thread t3(threadFunc, 3);

    t1.join();
    t2.join();
    t3.join();

    std::cout << i;
}

// 실행 결과(매 실행마다 다르게 출력됨)
// 3120

 

 

동적(dynamic) 저장 기간

  • 동적 저장 기간에 해당하는 데이터 객체들은 new, malloc 등으로 동적 할당된 객체들로, 명시적으로 delete, free 등으로 메모리를 해제하기 전까지 존재하는 객체들이다.
  • 힙 메모리에 할당되며, 아래 코드에서 변수 p가 동적 저장 기간을 가진다.
int* p = new int(10);

 

 


링크 방식(Linkage)

  • 저장 방식 지정자로 결정되는 또 다른 요소인 링크 단위는, 같은 심볼이 여러 번 등장했을 때 심볼들이 같은 걸 의미하는지, 외부에서 접근 가능한지를 판단하는 규칙이다.
  • 앞선 저장 방식이 데이터 객체들에게만 해당하는 내용이었다면, 링크 방식은 C++의 모든 데이터 객체, 함수, 클래스, 템플릿, namespace 등 모든 유형의 심볼에 적용되는 규칙이다.
  • 컴파일러는 링크 방식에 따라 심볼(이름)이 어느 범위까지 사용될 수 있는지 지정하며(가시성(visibility) 지정), 이후 링커가 지정된 정보를 기반으로 심볼들을 연결한다. 링크 방식은 아래와 같이 총 3가지 유형이 존재한다.
    • 링크 방식 없음(No linkage)
    • 내부 링크 방식(Internal linkage)
    • 외부 링크 방식(External linkage)

링크 방식의 3가지 유형

 

링크 방식 없음(No linkage)

  • 링크 방식 없음(No linkage)에 해당하는 심볼들은 블록 스코프( ‘{ }’ ) 안에 정의되어 있는 심볼들로, 같은 스코프 안에서만 존재하고 참조할 수 있다.
    • 예외적으로 extern 키워드가 붙은 심볼은 블록 스코프와 상관없이 링크 방식 없음에 해당하지 않는다.
  • 링크 방식 없음에 해당하는 심볼들은 링커가 링킹 작업을 할 대상에 포함되지 않는다. (링킹을 하지 않는다는 의미)
  • 아래 코드에 존재하는 심볼 x, LocalStruct, a는 전부 링크 방식 없음에 해당한다.
int main() {
    int x = 10;              // 지역 변수 <- 링크 방식 없음
    struct LocalStruct {     // 구조체 <- 링크 방식 없음
        int a;               // 구조체 내부 변수 <- 링크 방식 없음
    };  

    return x;
}

 

 

내부 링크 방식(Internal linkage)

  • 내부 링크 방식(Internal linkage)에 해당하는 심볼들은 static 함수, static 변수, 템플릿 함수, 템플릿 변수, 상수 전역 변수 등으로, 내부 링크 방식 심볼들은 같은 TU안에서만 참조할 수 있다. (extern 접근 불가)
    • 그 외에도 익명 namespace에 정의된 함수나 변수들 모두 내부 링크 방식이 적용된다.
    • 이러한 방식으로 인해 같은 심볼이 다른 파일에 여러 번 등장해도 심볼 사이의 충돌이 발생하지 않는다.
  • 아래 코드에 존재하는 심볼 counter, value, a, helper는 전부 내부 링크 방식에 해당한다.
// a.cpp
static int counter = 0;     // static 정적 변수 <- 내부 링크 방식
const int value = 42;       // const 정적 변수 <- 내부 링크 방식
namespace {
    int a;                  // 익명 namespace 변수 <- 내부 링크 방식
}
static void helper() {}     // static 함수 <- 내부 링크 방식

 

 

외부 링크 방식(External Linkage)

  • 외부 링크 방식(External Linkage)에 해당하는 심볼들은 위 두 가지 방식(링크 방식 없음, 내부 링크 방식)에 해당하지 않은 나머지 심볼들로, 대표적으로 함수, extern 변수, 이름이 있는 namespace 등이 있다.
  • 외부 링크 방식 심볼들은 다른 TU에서도 참조가 가능하다. 즉, 여러 소스 파일에서 공유하는 심볼이다.
    • 참고로 외부 링크 방식 심볼들은 “언어 링크 방식”을 정의할 수 있어서 아래 코드처럼 다른 언어(C와 C++)와 함수를 공유하는 것이 가능하다.
extern "C" int func();    // C 및 C++에서 사용할 수 있는 함수.
extern "C++" int func2(); // C++에서만 사용할 수 있는 함수.
// int func2 // C++에서 정의한 모든 함수들은 extern "C++"이 숨어 있다고 보면 된다
  • 아래 코드에 존재하는 심볼 count, a, helper는 전부 내부 링크 방식에 해당한다.
// a.cpp
int count = 42;               // 전역 변수 <- 외부 링크 방식

// b.cpp
extern int count;             // extern 변수 <- 외부 링크 방식
void func();                  // 일반 함수 <- 외부 링크 방식
namespace MyNamespace {       // 이릉 있는 namespace <- 외부 링크 방식
    // 외부 링크 (기본값)
    void externalFunction();  // namespace 내부 함수 <- 외부 링크 방식
}

 

 

링크 방식 요약 표

종류 설명 키워드 예시
No Linkage 심볼끼리 전혀 연결 안 됨 자동 지역 변수, 함수 매개변수
Internal Linkage 같은 파일에서만 접근 가능 static static 전역 변수/함수
External Linkage 다른 파일에서도 접근 가능 기본값, extern 전역 변수, 함수

 


C++ 저장 방식 지정자 비교표

  • 아래에서 표현하는 namespace는 일반 전역 변수와 함수 등이 속한 “전역 namespace”를 포함한다.
지정자 적용 대상 저장 기간 링크 방식
static 블록 범위 변수(지역 변수), namespace 범위 변수/함수, 클래스 멤버 정적 저장 기간 블록 범위(’ { } ‘): 링크 방식 없음
namespace 범위: 내부 링크 방식
thread_local 블록 범위 변수(지역 변수), namespace 범위 변수, 클래스 멤버 쓰레드 저장 기간 변수 위치에 따름
extern namespace 범위 변수/함수 정적 저장 기간 외부 링크 방식
mutable 클래스 멤버 변수 객체에 따름 링크 방식 없음

 


이름 맹글링(Name Mangling)

  • C++에서는 컴파일 단계에서 심볼 이름을 그대로 사용하지 않고 특정 규칙에 따라 변형해서 사용하는데, 이 과정을 “이름을 엉망진창 만든다(Mangling)”고 해서 이름 맹글링이라 부른다.
  • C 언어는 컴파일 시 이름 맹글링을 사용하지 않기 때문에, C++에서 정의한 함수를 C 환경에서 그대로 사용할 수 없다. 이때 extern "C"로 언어 링크 방식을 명시하면 C 방식으로 컴파일되어 이름 맹글링 없이 연결할 수 있다.

 

이름 맹글링 예시

  • func() 함수의 경우 함수 이름 변환 자체가 이루어지지 않았지만, 반면에 func2() 함수는 이름을 엉망진창 만들어 버린다.
extern "C" int func(const char* s);    // C 및 C++에서 사용할 수 있는 함수.
int func2(const char* s);              // C++에서만 사용할 수 있는 함수.

int func(const char* s){} // => 컴파일러가 func라는 이름 그대로 사용
int func(const char* s){} // => 컴파일러가 _Z4funcPKc라는 이름 그대로 사용

 

 

C++에서 이름 맹글링을 하는 이유

  • C++에서 굳이 이름 맹글링을 하는 이유는, C와 달리 함수 오버로딩이나 namespace와 같은 기능을 지원하여 동일한 이름을 가진 여러 식별자가 존재할 수 있기 때문이다.
    • 함수 오버로딩에 의해 인자가 다른 동일한 이름의 함수를 여러 개 정의할 수 있으며, namespace에 의해 소속된 namespace를 달리하여 같은 이름과 인자를 가진 함수를 여러 개 정의할 수 있다.
  • 따라서 컴파일러는 함수 이름뿐 아니라 namespace 정보와 함수 시그니처(인자 타입 등)의 정보가 담기도록 이름을 맹글링 하여 각각의 심볼을 구분할 수 있는 고유한 이름으로 변환한다.
int func(const char* s) {}
int func(int i) {}
int func(char c) {}

namespace n { // namespace n
int func(const char* s) {}
int func(int i) {}
int func(char c) {}
}

 

이름 맹글링에 의한 변환 예시

000000000000001d T _Z4funcc       
000000000000000f T _Z4funci
0000000000000000 T _Z4funcPKc
000000000000004a T _ZN1n4funcEc
000000000000003c T _ZN1n4funcEi
000000000000002d T _ZN1n4funcEPKc

 


참고 자료

https://modoocode.com/321