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년 가까이 정의 없는 선언이 남아있었다는 점이 재밌었다. 호출하지 않으면 빌드에 영향이 없다 보니 아무도 눈치채지 못한 채 살아남은 것이다. 규모가 크든 작든, 코드베이스에는 이런 사각지대가 생길 수 있다는 걸 새삼 느꼈다.