C++ CMake 링크 에러 LNK2019 | 원인과 해결
들어가며: “undefined reference to `…’” / LNK2019가 나올 때
GCC/Clang의 undefined reference to '함수명'이나 MSVC의 LNK2019: unresolved external symbol은 링크 단계에서 “선언은 있는데 정의를 찾을 수 없다”는 뜻입니다. 이 글에서는 컴파일은 통과했는데 링크에서 실패하는 대표적인 경우를 모아, CMake 기준으로 어디를 고치면 되는지 정리합니다.
다루는 원인은 크게 다섯 가지입니다. 정의가 아예 없거나 빌드에 포함되지 않은 다른 번역 단위(.cpp 하나가 컴파일된 결과)에만 있는 경우, target_link_libraries에 라이브러리가 빠진 경우, find_package가 라이브러리를 찾지 못하는 경우, C로 컴파일된 라이브러리를 C++에서 부르면서 extern "C"가 없는 경우, 그리고 링크 순서나 중복 정의 문제입니다. 마지막에는 에러 메시지에서 원인을 좁혀 가는 순서와 재발을 막는 CMake 구성 방법을 다룹니다.
관련 글: CMake 입문, 컴파일 과정, CMake 고급.
컴파일 vs 링크: 왜 “컴파일은 되는데” 링크에서만 실패할까?
C++ 빌드는 컴파일과 링크 두 단계로 나뉩니다.
flowchart LR
subgraph Compile[컴파일 단계]
A[.cpp 소스] --> B[컴파일러]
B --> C[.o / .obj 오브젝트]
end
subgraph Link[링크 단계]
C --> D[링커]
E[.a / .so / .lib] --> D
D --> F[실행 파일 / DLL]
end
컴파일러는 .cpp 파일을 하나씩 독립적으로 처리합니다. 호출하는 함수의 선언만 보이면 “이 함수는 어딘가에 있다”고 믿고 호출 코드를 만들어 둘 뿐, 정의가 어디 있는지는 확인하지 않습니다. 링커는 그다음에 모든 오브젝트 파일과 라이브러리를 합치면서 비워 둔 참조를 실제 정의와 연결합니다. 이때 정의를 찾지 못하면 undefined reference나 LNK2019가 납니다.
그래서 헤더만 include하고 라이브러리를 링크하지 않으면 컴파일은 문제없이 끝나고 링크에서만 실패합니다.
라이브러리 추가·C 연동·서브디렉터리에서 막히는 상황
새 라이브러리 추가 후 undefined reference
"Boost 라이브러리를 추가했는데 undefined reference to `boost::...' 가 나와요."
"컴파일은 되는데 링크에서만 실패해요."
헤더만 include하고 target_link_libraries에 Boost 타겟을 연결하지 않았거나, 필요한 컴파일 라이브러리 컴포넌트를 빠뜨린 경우입니다. find_package(Boost REQUIRED COMPONENTS ...)로 필요한 컴포넌트를 찾은 뒤 target_link_libraries(my_target PRIVATE Boost::headers Boost::filesystem)처럼 명시해야 합니다. Boost.Asio 자체는 헤더 전용이지만, 오래된 Boost 버전에서는 Boost.System을 함께 링크해야 했습니다.
”multiple definition of `foo’” 에러
"헤더에 함수를 정의했더니 multiple definition 에러가 나요."
"여러 .cpp에서 같은 함수가 중복 정의됐대요."
헤더에 inline도 템플릿도 아닌 일반 함수를 정의하고 여러 .cpp에서 include하면, 번역 단위마다 정의가 하나씩 생겨 링크 시 중복 정의 에러가 납니다. ODR(One Definition Rule) 위반입니다. 정의를 .cpp 하나로 옮기거나, inline을 붙이거나, static 또는 익명 네임스페이스로 번역 단위 안에 가둬서 해결합니다.
패키지를 설치했는데 CMake가 못 찾음
"vcpkg로 설치했는데 CMake가 libfoo를 못 찾아요."
"Could not find a package configuration file provided by 'Foo' 에러가 나요."
라이브러리는 설치됐지만 CMake가 그 경로를 모르는 경우입니다. 대개 CMAKE_TOOLCHAIN_FILE이나 CMAKE_PREFIX_PATH를 지정하지 않아서 find_package가 vcpkg나 Conan의 설치 경로를 뒤지지 않습니다. cmake -DCMAKE_TOOLCHAIN_FILE=[vcpkg]/scripts/buildsystems/vcpkg.cmake ..처럼 툴체인 파일을 넘기면 됩니다.
C 라이브러리를 C++에서 호출할 때
"libcurl을 C++에서 쓰는데 undefined reference to `curl_easy_init' 가 나와요."
"C로 빌드된 .a 파일을 링크했는데 심볼을 못 찾아요."
C 라이브러리는 이름 맹글링 없이 curl_easy_init이라는 이름 그대로 심볼을 내보냅니다. 그런데 선언이 extern "C" 없이 C++ 코드로 컴파일되면, 컴파일러는 _Z14curl_easy_initv 같은 맹글된 이름을 찾습니다. 헤더의 선언을 extern "C" { ... }로 감싸면 해결됩니다. libcurl 같은 주요 라이브러리의 공식 헤더는 이미 이 처리가 되어 있으므로, 이 에러는 직접 작성한 C 코드나 오래된 라이브러리에서 주로 만납니다.
서브디렉터리 라이브러리 링크 누락
"add_subdirectory로 mylib를 추가했는데 app에서 undefined reference to `mylib::init()' 가 나와요."
"mylib.cpp는 분명히 있는데요."
add_library(mylib STATIC mylib.cpp)로 라이브러리 타겟은 만들었지만, 실행 파일 쪽에 target_link_libraries(app PRIVATE mylib)를 쓰지 않은 경우입니다. 라이브러리를 빌드하는 것과 실행 파일에 링크하는 것은 별개라서, 이 한 줄을 추가해야 합니다.
시나리오별 해결 방향 요약
| 시나리오 | 특징 | 권장 접근 |
|---|---|---|
| 외부 라이브러리 undefined | find_package 후 링크 누락 | target_link_libraries에 타겟 추가 |
| multiple definition | 헤더에 정의, 여러 .cpp에서 include | inline/static/.cpp로 이동 |
| library not found | 패키지 설치했는데 못 찾음 | CMAKE_TOOLCHAIN_FILE, CMAKE_PREFIX_PATH |
| C 라이브러리 unresolved | C++에서 C 함수 호출 | extern “C” 선언 |
| 서브디렉터리 라이브러리 | add_library만 하고 링크 안 함 | target_link_libraries 추가 |
링크 에러 디버깅 흐름
flowchart TB
subgraph Detect[링크 에러 발생]
A[undefined reference / LNK2019 / multiple definition] --> B{에러 유형}
end
subgraph Undef[undefined reference]
B -->|정의 못 찾음| C[심볼명 확인]
C --> D{자체 코드?}
D -->|예| E[구현 .cpp가 add_executable/add_library에 포함?]
D -->|아니오| F[target_link_libraries에 라이브러리 추가]
E -->|아니오| G[target_sources 또는 target_link_libraries 수정]
end
subgraph MultiDef[multiple definition]
B -->|중복 정의| H[헤더에 일반 함수 정의?]
H -->|예| I[inline/static/.cpp로 이동]
end
subgraph NotFound[library not found]
B -->|라이브러리 못 찾음| J[CMAKE_TOOLCHAIN_FILE 확인]
J --> K[find_package 경로 확인]
end
에러 메시지 읽기
Unix (GCC/Clang)
undefined reference to '함수명' 또는 undefined reference to '네임스페이스::클래스::메서드(인자 타입)' 형태로, 정의를 찾지 못한 심볼이 그대로 나옵니다. 이 이름을 복사해서 선언이 어디 있고 구현은 어디에 있어야 하는지 찾아가면 됩니다. 에러 앞줄의 in function 'main' 부분은 그 심볼을 참조한 쪽을 알려 주므로, 어느 타겟에서 링크가 빠졌는지 판단하는 데 씁니다.
MSVC
LNK2019: unresolved external symbol "함수 시그니처" referenced in function ... 형태입니다. 데코레이션된 이름과 함께 풀어 쓴 시그니처가 나오므로, 선언과 정의의 시그니처가 정확히 일치하는지(const 여부, 참조, 네임스페이스, 호출 규약 등) 비교합니다. 선언은 void f(int&)인데 정의는 void f(int)로 되어 있으면 서로 다른 함수로 취급되어 이 에러가 납니다.
정의 누락·다른 파일에만 구현
상황
헤더에 함수나 클래스 선언은 있는데, 구현한 .cpp가 없거나 있어도 빌드에 포함되지 않은 경우입니다. 선언만 있는 함수를 호출하면 컴파일은 통과하고, 링크에서 정의를 찾지 못해 실패합니다.
해결
구현을 .cpp에 추가하고, 그 .cpp가 add_executable이나 add_library의 소스 목록에 들어가 있는지 확인합니다. 나중에 추가한다면 target_sources(my_target PRIVATE my_impl.cpp)를 씁니다. 소스 목록에 들어가야 오브젝트 파일이 만들어지고 링크에 포함됩니다.
구현이 서브디렉터리에서 add_library로 만든 라이브러리 안에 있다면, 실행 파일 쪽에서 target_link_libraries(실행파일 PRIVATE 그_라이브러리)로 링크해야 합니다. 실무에서 이 에러를 가장 자주 일으키는 원인은 새 .cpp 파일을 만들고 CMakeLists.txt에 추가하지 않은 경우입니다.
라이브러리 링크 누락 (CMake)
상황
Boost, OpenSSL, pthread 같은 외부 라이브러리의 헤더만 include하고, 실제 라이브러리 파일(.a, .so, .lib)은 링크하지 않은 경우입니다. 예를 들어 pthread 함수를 쓰면서 -pthread를 넘기지 않은 경우가 여기에 해당합니다.
해결 (CMake)
find_package로 찾은 타겟을 target_link_libraries에 넣습니다.
find_package(Threads REQUIRED)다음에target_link_libraries(my_target PRIVATE Threads::Threads)find_package(Boost REQUIRED COMPONENTS filesystem)다음에target_link_libraries(my_target PRIVATE Boost::filesystem)
라이브러리 파일 경로를 직접 지정할 수도 있지만(target_link_libraries(my_target PRIVATE /path/to/libfoo.a)), 가능하면 find_package가 제공하는 IMPORTED 타겟을 쓰는 편이 유지보수에 유리합니다. IMPORTED 타겟은 include 경로와 추가 의존성까지 함께 전달해 주기 때문입니다.
실행 파일이 링크하는 라이브러리 A가 또 다른 라이브러리 B에 의존한다면, A 타겟에 target_link_libraries(A PRIVATE B)로 의존성을 적어 둡니다. 그러면 실행 파일은 A만 링크해도 B가 함께 링크됩니다.
라이브러리 not found
상황
CMake 설정 단계에서 find_package(Foo REQUIRED)가 Could not find a package configuration file provided by "Foo"로 실패하거나, 링크 단계에서 cannot find -lfoo가 나는 경우입니다.
해결
vcpkg를 쓴다면 CMake 설정 시 툴체인 파일을 반드시 넘깁니다.
# vcpkg로 패키지 설치
vcpkg install fmt:x64-linux
# CMake 설정 시 툴체인 필수
cmake -B build -DCMAKE_TOOLCHAIN_FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake
# CMakeLists.txt
find_package(fmt REQUIRED)
target_link_libraries(my_app PRIVATE fmt::fmt)
find_package만 하고 target_link_libraries를 빼면, 헤더는 보이는데 링크 단계에서 undefined reference가 납니다.
직접 설치한 라이브러리라면 CMAKE_PREFIX_PATH로 설치 위치를 알려 줍니다.
cmake -B build -DCMAKE_PREFIX_PATH=/usr/local
패키지 설정 파일이 없는 라이브러리는 find_library와 find_path로 직접 찾습니다.
set(FOO_ROOT "/opt/foo")
find_library(FOO_LIBRARY foo HINTS ${FOO_ROOT}/lib)
find_path(FOO_INCLUDE_DIR foo.h HINTS ${FOO_ROOT}/include)
target_link_libraries(my_app PRIVATE ${FOO_LIBRARY})
target_include_directories(my_app PRIVATE ${FOO_INCLUDE_DIR})
C/C++ 혼합·extern “C”
상황
C로 작성하고 컴파일한 라이브러리의 함수를 C++에서 부르는 경우입니다. C++은 이름을 맹글링하므로, extern "C" 없이 선언된 함수는 C 라이브러리가 내보낸 이름과 다른 이름으로 찾게 됩니다. 라이브러리에는 foo라는 C 심볼이 분명히 있는데도 undefined reference to 'foo(int)'가 나는 것이 전형적인 증상입니다. 에러 메시지에 인자 타입이 붙어 있다면 C++ 이름으로 찾고 있다는 신호입니다.
해결
C++에서 쓰는 헤더에서 C 라이브러리 함수 선언을 extern "C"로 감쌉니다.
extern "C" {
void foo(int x);
}
같은 헤더를 C와 C++ 양쪽에서 include해야 한다면 __cplusplus 매크로로 감쌉니다. C 컴파일러는 extern "C" 문법을 모르기 때문입니다.
#ifdef __cplusplus
extern "C" {
#endif
void foo(int x);
#ifdef __cplusplus
}
#endif
CMake 쪽은 다른 라이브러리와 똑같이 find_library 등으로 찾아 target_link_libraries에 넣으면 되고, 헤더의 extern "C"만 확실히 해 두면 됩니다.
링크 순서·중복 정의
링크 순서
GNU ld는 정적 라이브러리(.a)를 명령줄에 나온 순서대로 한 번만 훑으면서, 그 시점까지 해결되지 않은 심볼을 채울 수 있는 오브젝트만 꺼내 씁니다. 그래서 libfoo.a가 libbar.a의 함수를 쓴다면 명령줄에서 foo가 bar보다 앞에 와야 합니다. 순서가 반대면 bar를 훑을 때는 아직 필요한 심볼이 없어서 아무것도 꺼내지 않고, 나중에 foo가 bar의 심볼을 요구해도 다시 돌아가지 않습니다. MSVC 링커와 macOS ld는 이런 순서 제약이 사실상 없어서, Linux에서만 실패하는 빌드가 생기기도 합니다.
CMake에서는 순서를 손으로 맞추기보다 타겟 사이의 의존 관계를 적어 두는 편이 낫습니다. foo가 bar에 의존하면 target_link_libraries(foo PRIVATE bar)를 써 두고, 실행 파일은 foo만 링크합니다. 그러면 CMake가 링크 명령줄에서 bar를 foo 뒤에 배치합니다.
중복 정의 (multiple definition)
같은 심볼이 여러 오브젝트 파일이나 라이브러리에 정의되어 있으면 multiple definition 에러가 납니다. 정의를 .cpp 하나에만 두고 나머지는 선언만 남기거나, inline을 붙이거나, static이나 익명 네임스페이스로 번역 단위 안에 가둬서 정리합니다.
헤더에 일반 함수 정의를 두면 include하는 모든 .cpp에 정의가 복사됩니다. 그래서 inline 함수나 템플릿이 아니라면 헤더에는 정의를 두지 않습니다.
구현 누락부터 링크 순서까지 재현 예제
undefined reference — 구현 파일 누락
에러 메시지:
/usr/bin/ld: main.cpp.o: in function `main':
main.cpp:(.text+0x15): undefined reference to `utils::compute(int)'
collect2: error: ld returned 1 exit status
잘못된 CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(Demo LANGUAGES CXX)
add_executable(app main.cpp)
# utils.cpp를 빼먹음!
main.cpp:
#include "utils.h"
int main() {
utils::compute(42);
return 0;
}
utils.h:
#pragma once
namespace utils {
int compute(int x);
}
utils.cpp (존재하지만 빌드에 미포함):
#include "utils.h"
namespace utils {
int compute(int x) { return x * 2; }
}
올바른 CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(Demo LANGUAGES CXX)
add_executable(app main.cpp utils.cpp)
# 또는 target_sources(app PRIVATE utils.cpp)
undefined reference — 라이브러리 링크 누락
에러 메시지:
undefined reference to `boost::filesystem::detail::status(boost::filesystem::path const&, boost::system::error_code*)'
잘못된 CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(FsDemo LANGUAGES CXX)
find_package(Boost REQUIRED)
# target_link_libraries 누락!
add_executable(app main.cpp)
target_include_directories(app PRIVATE ${Boost_INCLUDE_DIRS})
올바른 CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(FsDemo LANGUAGES CXX)
find_package(Boost REQUIRED COMPONENTS filesystem)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE Boost::filesystem)
Boost::filesystem 같은 IMPORTED 타겟을 링크하면 include 경로와 의존 컴포넌트가 함께 전달되므로, target_include_directories를 따로 쓸 필요가 없습니다.
multiple definition — 헤더에 정의
에러 메시지:
multiple definition of `helper::add(int, int)'
/usr/bin/ld: a.cpp.o: in function `helper::add(int, int)'
/usr/bin/ld: b.cpp.o: in function `helper::add(int, int)'
잘못된 helper.h:
#pragma once
namespace helper {
int add(int a, int b) { // ❌ 헤더에 정의 → include하는 모든 .cpp에 복사
return a + b;
}
}
해결 1, inline 사용:
#pragma once
namespace helper {
inline int add(int a, int b) { // ✅ inline: ODR 허용
return a + b;
}
}
해결 2, 선언과 정의 분리:
// helper.h
#pragma once
namespace helper {
int add(int a, int b); // 선언만
}
// helper.cpp
#include "helper.h"
namespace helper {
int add(int a, int b) { return a + b; } // 정의는 한 곳에만
}
add_library(helper STATIC helper.cpp)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE helper)
#pragma once나 include guard는 한 번역 단위 안에서 헤더가 두 번 들어가는 것만 막아 줍니다. 서로 다른 .cpp가 각각 include하는 것은 막지 못하므로 이 에러와는 관계가 없습니다.
library not found — vcpkg 툴체인 미지정
에러 메시지:
CMake Error at CMakeLists.txt:10 (find_package):
Could not find a package configuration file provided by "fmt" with any of
the following names:
fmtConfig.cmake
fmt-config.cmake
vcpkg로 fmt를 설치했지만 CMake가 vcpkg 경로를 모르는 상태입니다.
vcpkg install fmt:x64-linux
cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
cmake --build build
# CMakeLists.txt
find_package(fmt REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)
툴체인 파일은 첫 configure 때 캐시에 기록됩니다. 이미 만들어진 build 디렉터리에 나중에 옵션을 추가하면 반영되지 않을 수 있으니, 그럴 때는 build 디렉터리를 지우고 다시 configure합니다.
C 라이브러리 — extern “C” 누락
에러 메시지:
undefined reference to `my_c_func(int)'
C로 컴파일된 libmyc.a에는 my_c_func라는 심볼이 있는데, C++ 쪽 헤더에 extern "C"가 없어서 맹글된 이름으로 찾고 있는 상황입니다. 에러 메시지에 (int) 같은 인자 타입이 붙어 있는 것이 단서입니다.
/* my_c.h — 잘못된 헤더 */
int my_c_func(int x);
/* my_c.h — 올바른 헤더 (C와 C++ 양쪽에서 사용 가능) */
#ifdef __cplusplus
extern "C" {
#endif
int my_c_func(int x);
#ifdef __cplusplus
}
#endif
SQLite, libcurl, zlib 같은 널리 쓰이는 C 라이브러리의 헤더는 이미 이 패턴을 포함하고 있습니다. 이런 라이브러리에서 undefined reference to 'sqlite3_open'처럼 인자 타입 없는 이름이 나온다면 extern "C" 문제가 아니라 라이브러리를 링크하지 않은 것입니다.
find_package(PkgConfig REQUIRED)
pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE PkgConfig::SQLITE3)
OpenSSL — 여러 컴포넌트 링크
에러 메시지:
undefined reference to `SSL_CTX_new'
undefined reference to `EVP_sha256'
OpenSSL은 TLS를 담당하는 libssl과 암호 알고리즘을 담당하는 libcrypto 두 라이브러리로 나뉩니다. SSL_* 함수는 libssl에, EVP_* 같은 함수는 libcrypto에 있으므로 둘 다 필요합니다.
find_package(OpenSSL REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE OpenSSL::SSL OpenSSL::Crypto)
오래된 코드에서 SSL_library_init이 undefined로 나온다면, OpenSSL 1.1.0부터 이 함수가 매크로로 바뀌었기 때문일 수 있습니다. 1.0 시절 헤더로 컴파일한 코드를 1.1 이상 라이브러리에 링크하면 이렇게 됩니다. 헤더와 라이브러리 버전을 맞춰야 합니다.
nlohmann/json — 헤더 전용인데 링크 에러
nlohmann/json은 헤더 전용이라 링크할 라이브러리 파일이 없습니다. target_link_libraries(app PRIVATE nlohmann_json::nlohmann_json)는 include 경로를 전달하는 INTERFACE 타겟을 연결할 뿐입니다. 그런데도 이 라이브러리를 쓰는 코드에서 undefined reference가 난다면 대개 다른 라이브러리 문제입니다. 예를 들어 JSON으로 요청을 만드는 REST 클라이언트 코드가 libcurl을 쓰는데 그쪽을 링크하지 않은 경우입니다. 실제로 해결되지 않은 심볼이 어느 라이브러리 소속인지 에러 메시지와 nm으로 확인해 보면 됩니다.
링크 순서 문제 — 의존되는 쪽이 뒤에 와야 함
GNU ld에서 정적 라이브러리 순서가 뒤집히면 이런 에러가 납니다.
libfoo.a(foo.o): in function `foo()':
foo.cpp:(.text+0x5): undefined reference to `bar()'
잘못된 순서(foo가 bar의 심볼을 쓰는데 bar가 앞에 있음):
target_link_libraries(app PRIVATE bar foo)
올바른 순서(의존되는 쪽이 뒤):
target_link_libraries(app PRIVATE foo bar)
# 더 나은 방법: foo에 bar 의존성을 직접 기록
target_link_libraries(foo PRIVATE bar)
target_link_libraries(app PRIVATE foo) # foo를 링크하면 bar도 foo 뒤에 자동 포함
두 번째 방법처럼 타겟 간 의존 관계를 적어 두면 CMake가 순서를 알아서 맞춰 주므로, 라이브러리가 늘어나도 순서를 손으로 관리할 필요가 없습니다. 두 라이브러리가 서로를 참조하는 순환 의존이라면 $<LINK_GROUP:RESCAN,foo,bar>(CMake 3.24 이상)로 --start-group/--end-group을 쓸 수 있지만, 가능하면 순환 자체를 없애는 편이 낫습니다.
심볼 추출부터 상세 링커 출력까지 추적 단계
1단계: 에러 메시지에서 심볼명 추출
GCC/Clang에서는 undefined reference to 'X'의 X가 누락된 심볼입니다. MSVC의 LNK2019에서는 따옴표 안의 시그니처를 확인합니다.
2단계: 심볼 소유처 파악
심볼이 자체 코드에 속한다면 grep -r "함수명" src/나 IDE 검색으로 선언과 정의의 위치를 찾습니다. 외부 라이브러리 심볼이라면 해당 라이브러리 문서에서 CMake 타겟 이름과 링크 방법을 확인합니다.
3단계: 자체 코드인 경우
# 구현 .cpp가 타겟에 포함되는지 확인
grep -r "my_impl.cpp" CMakeLists.txt
grep -r "target_sources\|add_executable\|add_library" CMakeLists.txt
해당 .cpp가 add_executable, add_library, target_sources 중 한 곳에 있는지 확인합니다. 라이브러리로 분리했다면 실행 파일에 target_link_libraries(실행파일 PRIVATE 그_라이브러리)가 있는지도 봅니다.
4단계: 외부 라이브러리인 경우
# 라이브러리 내 심볼 확인 (Unix)
nm -C /path/to/libfoo.a | grep "함수명"
nm -D -C /path/to/libfoo.so | grep "함수명"
# MSVC
dumpbin /SYMBOLS libfoo.lib | findstr "함수명"
nm 출력에서 T는 그 라이브러리에 정의된 심볼, U는 다른 곳에서 가져와야 하는 심볼입니다. 라이브러리에 T로 정의되어 있는데도 에러가 난다면 링크 순서나 target_link_libraries 누락을 의심합니다. 아예 없거나 이름이 다르게 나온다면 라이브러리 버전이 다르거나, C/C++ 혼합에서 extern "C"가 빠진 경우입니다. -C 없이 맹글된 이름 그대로 비교하면 차이가 더 분명하게 보입니다.
5단계: CMake 링크 그래프 확인
cmake --graphviz=graph.dot ..
dot -Tpng graph.dot -o graph.png
실행 파일에서 라이브러리로 이어지는 의존 관계가 의도한 대로인지 그림으로 확인할 수 있습니다.
6단계: 상세 링커 출력
# 실제 링크 명령줄 확인
cmake --build build --verbose
# GNU ld가 어떤 파일을 읽는지 추적
cmake -B build -DCMAKE_EXE_LINKER_FLAGS="-Wl,--trace" ..
cmake --build build 2>&1 | less
--verbose로 실제 링커 명령줄을 보면 라이브러리가 빠졌는지, 순서가 어떤지 바로 확인할 수 있습니다.
pthread·vtable·__gxx_personality_v0: 자주 보는 링크 에러
”undefined reference to `pthread_create’”
pthread를 쓰는데 스레드 라이브러리를 링크하지 않은 경우입니다. glibc 2.34부터는 pthread가 libc에 합쳐져서 이 에러가 줄었지만, 이식성을 위해 CMake의 Threads 패키지를 쓰는 편이 안전합니다.
find_package(Threads REQUIRED)
target_link_libraries(my_app PRIVATE Threads::Threads)
”undefined reference to `dlopen’”
동적 로딩 함수(dlopen)를 쓰는데 -ldl을 링크하지 않은 경우입니다. 이것도 glibc 2.34부터 libc에 합쳐졌지만, 오래된 시스템을 지원해야 한다면 명시해 둡니다.
target_link_libraries(my_app PRIVATE ${CMAKE_DL_LIBS})
Windows에서 “unresolved external symbol main” 또는 “WinMain”
콘솔 서브시스템(/SUBSYSTEM:CONSOLE)으로 링크하는데 main이 없거나, GUI 서브시스템(/SUBSYSTEM:WINDOWS)으로 링크하는데 WinMain이 없는 경우입니다. 진입점 함수가 들어 있는 소스가 타겟에 포함됐는지 확인하고, GUI 앱이라면 WIN32 옵션을 줍니다.
add_executable(console_app main.cpp) # 콘솔
add_executable(gui_app WIN32 winmain.cpp) # GUI
”undefined reference to vtable for MyClass”
GCC와 Clang은 클래스의 vtable을 “인라인이 아닌 첫 번째 가상 함수”(key function)의 정의가 있는 번역 단위에 생성합니다. 그래서 그 가상 함수를 선언만 하고 정의하지 않았거나, 정의가 든 .cpp가 빌드에 빠지면 vtable 자체가 만들어지지 않아 이 에러가 납니다. 순수 가상 함수가 아닌 모든 가상 함수(가상 소멸자 포함)에 정의를 제공하고, 해당 .cpp를 타겟에 포함합니다. Qt 프로젝트에서는 Q_OBJECT 클래스의 moc 파일이 생성되지 않아도 같은 에러가 나므로 CMAKE_AUTOMOC 설정을 확인합니다.
”cannot find -lfoo” (library not found)
링커가 libfoo.a나 libfoo.so를 검색 경로에서 찾지 못한 경우입니다.
target_link_directories(my_app PRIVATE /path/to/lib)
target_link_libraries(my_app PRIVATE foo)
# 또는 전체 경로
target_link_libraries(my_app PRIVATE /path/to/lib/libfoo.a)
가능하면 find_package나 find_library로 경로를 찾아 타겟에 연결하는 방식이 설치 위치가 다른 환경에서도 잘 동작합니다.
”multiple definition” — 헤더의 전역 변수
헤더에 int counter;나 int counter = 0;처럼 전역 변수를 정의하고 여러 .cpp에서 include하면 multiple definition of 'counter'가 납니다. 함수와 마찬가지로 번역 단위마다 정의가 하나씩 생기기 때문입니다. C 코드에서 초기화하지 않은 int counter;는 예전 GCC에서는 common 심볼로 합쳐져 에러가 안 났지만, GCC 10부터 -fno-common이 기본이 되면서 에러가 나기 시작했습니다.
static int counter;로 바꾸면 링크 에러는 사라지지만, 번역 단위마다 별개의 counter가 생겨서 공유되지 않습니다. 이것이 의도가 아니라면 C++17의 inline int counter = 0;을 쓰거나, 헤더에는 extern int counter; 선언만 두고 .cpp 하나에서 정의합니다.
”undefined reference to `std::__throw_bad_alloc()’” 같은 libstdc++ 심볼
C++ 표준 라이브러리 심볼을 찾지 못하는 경우입니다. g++ 대신 gcc로 링크해서 libstdc++가 자동으로 붙지 않았거나, -nostdlib를 쓴 경우에 주로 납니다. CMake 프로젝트라면 project(... LANGUAGES CXX)로 C++ 링커가 쓰이도록 하는 것이 먼저이고, -nostdlib를 의도적으로 쓴다면 필요한 라이브러리를 직접 링크합니다.
# -nostdlib 사용 시 수동으로 링크
target_link_libraries(my_app PRIVATE stdc++)
”undefined reference to `__atomic_load_8’”
64비트 원자 연산을 하드웨어가 한 명령으로 지원하지 않는 일부 32비트 타겟(예: 일부 ARM, 32비트 RISC-V)에서, 컴파일러가 원자 연산을 libatomic 함수 호출로 바꿨는데 libatomic을 링크하지 않은 경우입니다.
find_library(ATOMIC_LIBRARY atomic)
if(ATOMIC_LIBRARY)
target_link_libraries(my_app PRIVATE ${ATOMIC_LIBRARY})
endif()
”undefined reference to `__gxx_personality_v0’”
C++ 예외 처리 런타임 심볼입니다. C++ 오브젝트를 C 링커(gcc)로 링크했거나 C++ 런타임이 링크되지 않은 경우에 납니다. CMake에 C++ 프로젝트임을 알려 C++ 링커가 쓰이도록 합니다. project(MyProj LANGUAGES CXX) 또는 enable_language(CXX)를 사용합니다.
INTERFACE 타겟·FetchContent·툴체인 고정으로 재발 막기
IMPORTED 라이브러리로 의존성 캡슐화
# FindMyLib.cmake 또는 Config 파일
add_library(MyLib::MyLib SHARED IMPORTED)
set_target_properties(MyLib::MyLib PROPERTIES
IMPORTED_LOCATION "${MYLIB_ROOT}/lib/libmylib.so"
INTERFACE_INCLUDE_DIRECTORIES "${MYLIB_ROOT}/include"
)
이렇게 IMPORTED 타겟을 만들어 두면 사용하는 쪽에서는 target_link_libraries(app PRIVATE MyLib::MyLib) 한 줄로 include 경로와 링크가 함께 전달됩니다. 이름에 ::가 들어간 타겟은 존재하지 않으면 CMake가 configure 단계에서 에러를 내므로, 오타가 링크 에러로 늦게 드러나는 것도 막아 줍니다.
FetchContent로 버전 고정
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.1.1
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(my_app PRIVATE fmt::fmt)
외부 의존성 버전을 소스와 함께 고정하면 어느 환경에서 빌드해도 같은 버전이 쓰입니다.
CI에서 vcpkg/Conan 툴체인 일관 적용
# GitHub Actions 예시
- name: Configure CMake
run: |
cmake -B build \
-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake \
-DCMAKE_BUILD_TYPE=Release
로컬과 CI가 같은 툴체인 파일을 쓰게 하면 “로컬에선 되는데 CI에서 안 돼요” 유형의 문제를 줄일 수 있습니다. CMakePresets.json에 툴체인 경로를 넣어 두면 두 환경이 같은 설정을 공유하기 더 쉽습니다.
의존성 전파 (PUBLIC/PRIVATE/INTERFACE)
add_library(mylib STATIC mylib.cpp)
target_include_directories(mylib PUBLIC include/) # 링크하는 쪽에 전파
target_link_libraries(mylib PRIVATE Boost::filesystem) # mylib 구현에서만 사용
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib) # mylib의 PUBLIC include 자동 적용
PUBLIC은 이 타겟과 이 타겟을 링크하는 쪽 모두에 적용되고, PRIVATE은 이 타겟에만 적용되며, INTERFACE는 링크하는 쪽에만 적용됩니다. 헤더 전용 라이브러리는 자신은 빌드할 것이 없으므로 INTERFACE로 선언합니다. 참고로 mylib가 정적 라이브러리라면 PRIVATE으로 적은 Boost::filesystem도 최종 링크에는 포함됩니다. 정적 라이브러리는 자체로 링크되지 않기 때문에 CMake가 의존성을 실행 파일 링크 단계까지 넘겨주기 때문입니다.
CMakeLists.txt 갱신 누락 같은 실무 실수
새 .cpp를 추가하고 CMakeLists.txt를 갱신하지 않으면 undefined reference to 새로_만든_함수 형태의 에러가 납니다. 일부 IDE는 파일을 추가할 때 CMakeLists.txt도 고쳐 주지만, 에디터에서 파일만 만들면 빠지기 쉽습니다. file(GLOB ...)로 소스를 자동 수집하는 방법도 있지만, 파일을 추가한 뒤 CMake를 다시 실행하지 않으면 새 파일이 인식되지 않아 같은 문제가 생깁니다.
find_package(X REQUIRED)만 쓰고 target_link_libraries를 빼먹으면, Boost나 OpenSSL 같은 외부 라이브러리 함수에서 undefined reference가 납니다. 두 줄은 항상 짝으로 씁니다.
vcpkg나 Conan을 쓰는 프로젝트에서 CMAKE_TOOLCHAIN_FILE을 넘기지 않으면 Could not find a package configuration file provided by "fmt"가 납니다. 새로 합류한 사람이 가장 먼저 부딪히는 문제이므로 README나 프리셋에 configure 명령을 적어 둡니다.
add_subdirectory(lib)로 라이브러리를 추가만 하고 target_link_libraries(app PRIVATE mylib)를 빼먹으면 undefined reference to mylib::함수 형태의 에러가 납니다.
빠른 참조: 에러 메시지 → 해결 키워드
| 에러 메시지 패턴 | 첫 번째 확인 사항 |
|---|---|
undefined reference to 내_네임스페이스:: | 해당 .cpp가 add_executable/add_library에 포함? |
undefined reference to boost:: | target_link_libraries에 Boost::* 추가 |
undefined reference to pthread_ | Threads::Threads 링크 |
undefined reference to curl_ | libcurl 링크 (CURL::libcurl) |
undefined reference to SSL_ | OpenSSL::SSL, OpenSSL::Crypto 링크 |
undefined reference to foo(int) (C 함수인데 인자 타입이 붙음) | extern “C” 누락 |
multiple definition of | 헤더에 일반 함수·변수 정의? → .cpp로 이동 또는 inline |
cannot find -lfoo | target_link_directories, find_library, CMAKE_TOOLCHAIN_FILE |
Could not find ... Foo | CMAKE_TOOLCHAIN_FILE, CMAKE_PREFIX_PATH |
undefined reference to vtable | 가상 함수 정의 누락, 해당 .cpp 포함 여부 |
자주 묻는 질문 (FAQ)
Q. C 라이브러리 함수를 C++에서 호출했더니 undefined reference가 나는 이유는 무엇인가요?
C++ 컴파일러는 오버로딩을 지원하기 위해 함수 이름에 인자 타입 정보를 붙이는 네임 맹글링을 하므로, C로 컴파일된 라이브러리의 심볼 이름과 C++ 쪽에서 찾는 이름이 달라집니다. 그래서 라이브러리를 제대로 링크해도 링커가 심볼을 찾지 못합니다. C 헤더의 선언을 extern "C" 블록으로 감싸고, 헤더를 C와 C++에서 함께 쓴다면 #ifdef __cplusplus 가드 안에 extern "C"를 두면 해결됩니다.
Q. undefined reference to vtable for MyClass는 왜 나나요?
클래스에 가상 함수를 선언만 하고 정의하지 않았거나, 그 정의가 들어 있는 .cpp가 타겟 빌드에 포함되지 않았을 때 납니다. 순수 가상 함수가 아닌 모든 가상 함수에 정의를 제공하고, 해당 .cpp를 add_executable/add_library 소스 목록에 넣었는지 확인하면 해결됩니다.
같이 보면 좋은 글
- C++ Segmentation fault | core dump
- C++ CMake 고급 | 멀티 타겟·외부 라이브러리 관리 (대규모 프로젝트 빌드)
- CMake 입문 | 수십 개 파일 컴파일할 때 필요한 빌드 자동화 (CMakeLists.txt 기초)
- C++로 작은 DB 엔진 만들기
- C++ Asio 데드락 디버깅 | 비동기 콜백 실전 [#49-3]
- 직접 만든 쿼리 엔진 최적화
- C++ 게임 엔진 기초
다음 글: [에러 해결·트러블슈팅 #49-3] Asio 비동기 콜백에서 발생하는 은밀한 데드락(Deadlock) 실전 디버깅
이전 글: C++ Segmentation fault 디버깅: core dump·GDB·LLDB·ASan·Valgrind로 원인 찾기