박종훈 기술블로그
close menu

JDK-8379873 기여기: extern "C"와 name mangling으로 이해하는 미사용 선언 제거

JDK-8379873에 기여해보기로 했다. HotSpot JVM의 Windows 플랫폼 코드에서 정의 없이 남아있던 extern "C" 함수 선언을 제거하는 작업이다. 코드 변경 자체는 4줄 삭제에 불과하지만, 이 과정에서 extern "C"의 역할, C++ name mangling, 그리고 C/C++ 빌드에서 선언과 정의가 분리되는 구조를 정리해 보았다.

이슈의 제목은 다음과 같다.

Remove undefined debugging declarations in os_windows.cpp

이슈 내용

이슈 본문은 다음과 같다.

In os_windows.cpp there is a small group of function declarations, described as being “for PostMortemDump”. But these functions don’t seem to be defined anywhere.

void safepoints();
void find(int x);
void events();

os_windows.cpp에 PostMortemDump 용도로 설명된 함수 선언이 있지만, 이 함수들의 정의(본체)가 코드베이스 어디에도 존재하지 않는다는 내용이다.

배경 지식

os_windows.cpp란

os_windows.cpp는 JVM(HotSpot)의 Windows 플랫폼 추상화 계층이다.

JVM은 여러 OS에서 동작해야 하므로, OS별로 다른 기능을 os:: 네임스페이스 아래 통일된 인터페이스로 구현한다. 이 파일은 Windows 전용 구현체이며, Linux용은 os_linux.cpp, macOS용은 os_bsd.cpp가 대응한다.

System.currentTimeMillis(), Thread.start(), Runtime.loadLibrary() 같은 Java API를 호출하면 결국 네이티브 레이어를 거쳐 이 파일의 함수들이 실행된다.

문제의 코드

os_windows.cpp에는 다음과 같은 선언이 있었다.

// Used for PostMortemDump
extern "C" void safepoints();
extern "C" void find(int x);
extern "C" void events();

조사해 보면:

  • 이 세 함수의 정의(본체)가 코드베이스 어디에도 없다
  • 호출하는 코드도 없다
  • “PostMortemDump” 문자열도 이 주석 외에는 존재하지 않는다
  • 2007년 초기 JDK 레포지토리 로드 시점부터 존재하던 코드다

오픈소스 전환 이전의 Windows 디버깅/포스트모템 덤프 기능의 잔재로 추정된다.

extern "C"와 name mangling

여기서 extern "C"가 사용되고 있는데, 이것은 C++ 컴파일러에게 해당 함수를 C 방식으로 처리하라고 지시하는 선언이다.

핵심은 네임 맹글링(name mangling) 차이에 있다.

C++은 함수 오버로딩을 지원하기 때문에, 컴파일러가 같은 이름의 함수를 구별하기 위해 함수 이름을 변형한다.

// C++ 컴파일 후 심볼 이름 (예시)
void foo(int x)      _Z3fooi
void foo(float x)    _Z3foof
void foo()           _Z3foov

반면 C 컴파일러는 함수 이름을 그대로 유지한다.

// C 컴파일 후 심볼 이름
void foo(int x)    foo  (또는 _foo)

C로 컴파일된 라이브러리를 C++에서 링크할 때, C++은 mangled된 이름으로 함수를 찾지만 라이브러리엔 원본 이름만 있어서 링크 오류가 발생한다. extern "C"를 사용하면 mangling을 하지 않으므로 이 문제가 해결된다.

결론적으로, extern "C"로 선언되어 있다는 것은 이 함수들이 외부 어딘가에 C 링킹 방식으로 구현되어 있고, 링크 시점에 연결될 것이라는 약속이다. 그런데 코드베이스 어디에도 이 함수들의 구현체가 존재하지 않는다. 약속만 있고 실체가 없는 셈이다. 따라서 이 선언들은 안전하게 삭제할 수 있다.

C/C++에서 본체 없는 선언이 컴파일되는 이유

C/C++에서는 선언(declaration)과 정의(definition)가 분리되어 있다.

extern "C" void safepoints();  // 선언만 - "이런 함수가 어딘가에 있다"는 약속
  • 컴파일 단계: 선언만 보고 넘어간다. 본체가 없어도 에러가 아니다.
  • 링크 단계: 실제로 그 함수를 호출하는 코드가 있을 때만 링커가 본체를 찾는다. 못 찾으면 undefined reference 에러가 발생한다.

이 경우 호출하는 코드가 없으니 링커가 본체를 찾을 필요 자체가 없어서 빌드가 정상적으로 된다.

C/C++ 빌드 과정: 컴파일에서 링크까지

이를 이해하려면 C/C++의 빌드 과정을 알아야 한다.

소스코드 (.cpp)
    ↓  전처리 (Preprocessor)
전처리된 코드
    ↓  컴파일 (Compiler)
어셈블리 코드 (.s)
    ↓  어셈블 (Assembler)
오브젝트 파일 (.o / .obj)   ← 여기서 심볼 테이블 존재
    ↓  링크 (Linker)
실행 파일 (.exe / ELF)

.o 파일은 기계어 코드이지만, 아직 미완성이다. 함수 호출 주소가 빈칸으로 남아있고, 심볼 테이블이라는 함수 이름 목록을 가지고 있다. 링커가 여러 .o 파일을 합치면서 이 빈칸을 채워 최종 실행 파일을 만든다.

이때 링커는 심볼 이름으로 함수를 찾기 때문에, C++의 mangled된 이름과 C의 원본 이름이 다르면 링크 오류가 나는 것이다. extern "C"는 이 심볼 이름 불일치 문제를 해결하기 위한 것이다.

JDK-8379873의 작업 내용

해야 할 일은 명확하다. 정의도 없고, 호출하는 곳도 없는 아래 4줄(주석 + 선언 3개)을 제거하면 된다.

// Used for PostMortemDump
extern "C" void safepoints();
extern "C" void find(int x);
extern "C" void events();

마무리

코드 변경사항 자체는 4줄 삭제에 불과하지만, 이 코드가 왜 있었는지, extern "C"가 무엇인지, 본체 없는 선언이 왜 컴파일되는지를 이해하는 과정에서 C/C++의 빌드 과정과 링킹 메커니즘에 대해 공부할 수 있었다.

이번 기여를 통해 정리한 내용을 요약하면 다음과 같다.

  • extern "C"는 C++ name mangling을 비활성화하여 C 방식의 심볼 이름을 유지하게 한다
  • C/C++에서 선언은 “이런 함수가 존재한다”는 약속이고, 호출하지 않으면 정의가 없어도 빌드된다
  • 링커는 실제로 호출되는 함수만 심볼 테이블에서 찾으므로, 미사용 선언은 링크 오류를 일으키지 않는다

한편, OpenJDK처럼 수천 명이 참여하는 대규모 프로젝트에서도 2007년부터 약 20년 가까이 정의 없는 선언이 남아있었다는 점이 재밌었다. 호출하지 않으면 빌드에 영향이 없다 보니 아무도 눈치채지 못한 채 살아남은 것이다. 규모가 크든 작든, 코드베이스에는 이런 사각지대가 생길 수 있다는 걸 새삼 느꼈다.

categories: 개발

tags: JDK , OpenJDK , HotSpot , C++ , extern C , name mangling , 링커 , 오픈소스 기여