소스 코드로 작성한 C 기반 공유 라이브러리에서 어떤 함수나 전역 변수가 외부로 export되고 있는지 확인하려면, 각각의 운영체제에 맞는 도구를 사용해야 합니다. Linux의 .so(ELF 형식)와 Windows의 .dll(PE 형식)은 바이너리 포맷이 다르므로, 이를 분석하는 도구도 달라집니다. 아래에서는 CLI 도구와 GUI 도구를 활용하여 각 플랫폼별로 라이브러리 내보낸 심볼 목록을 확인하는 방법을 설명합니다. 또한 빌드 시에 필요한 옵션이나 유의사항도 함께 다룹니다.
Linux – 공유 라이브러리(.so)의 심볼 확인
Linux에서 공유 라이브러리(.so)에 export된 심볼(외부에 공개된 함수나 전역변수)이 무엇인지 확인하는 대표적 CLI 도구는 nm과 objdump, readelf 등이 있습니다. 이들은 ELF 파일의 심볼 테이블을 읽어와 어떤 심볼들이 정의되어 있고(export) 또는 참조되고(import) 있는지 보여줍니다.
nm 도구 사용 (CLI)
nm은 오브젝트 파일이나 라이브러리의 심볼 목록을 출력해주는 GNU Binutils 도구입니다. 기본적으로 nm을 그냥 실행하면 정적(symbol table) 심볼까지 모두 보여주는데, 공유 라이브러리의 export 심볼만 보려면 동적 심볼 테이블만 표시하도록 -D 옵션을 주는 것이 좋습니다. nm -D는 공유 라이브러리의 동적 심볼(다이나믹 심볼 테이블에 등재된 항목)만 표시하며, 이것이 곧 외부에 노출된 API 심볼과 외부 의존 심볼을 나타냅니다. 사용 예시는 다음과 같습니다.
$ nm -D libexample.so
위 명령의 출력에는 각 심볼의 주소(address), 타입(code/data/undefined 등), 이름(name)이 나옵니다. 예를 들어 어떤 라이브러리가 exported_function이라는 글로벌 함수와 exported_variable이라는 글로벌 변수를 내보내고, internal_function이라는 static 함수와 internal_variable이라는 static 변수를 내부적으로만 가지고 있다고 가정해보겠습니다. 이 경우 nm -D libexample.so의 출력 예시는 대략 다음과 같습니다:
00000000 T exported_function
00000000 D exported_variable
U printf@GLIBC_2.2.5
- T exported_function – T는 전역(Text) 심볼로서, 라이브러리 내부에 정의된 글로벌 함수임을 나타냅니다. 즉 이 함수가 외부로 export 되어 있음을 의미합니다
- D exported_variable – D는 전역(Data) 심볼로서, 초기값이 있는 글로벌 변수가 export되었음을 나타냅니다
- U printf@GLIBC_2.2.5 – U는 정의되지 않은(Undefined) 외부 심볼로, 이 라이브러리가 사용하는 다른 라이브러리의 함수입니다 (여기서는 printf, GLIBC로부터 가져옴). 즉 import된 심볼을 뜻하며, export 심볼과 구분됩니다.
nm 출력의 심볼 타입 코드는 대소문자로 구분되며, export 여부와 섹션을 나타냅니다. 예를 들어 대문자 T, D, B 등은 전역 심볼(외부 공개)을 의미하고, 소문자 t, d, b 등은 로컬 심볼(내부)을 의미합니다. 주요 코드의 의미는 다음과 같습니다.
- T (Text): 전역 함수 코드 심볼 (export된 함수)
- t (text): 지역 함수 코드 심볼 (static 함수 등, 외부 비공개)
- D (Data): 전역 초기화 데이터 심볼 (export된 전역 변수)
- d (data): 지역 초기화 데이터 심볼 (파일 내부 static 변수 등)
- B (BSS): 전역 초기화되지 않은 심볼 (export된 전역 변수, 초기값 없음)
- b (bss): 지역 초기화되지 않은 심볼 (static 전역 변수, 초기값 없음)
- U (Undefined): 정의되어 있지 않은 심볼 (다른 객체/라이브러리에서 제공되는 함수나 변수)
nm -D로 보면 외부에 노출된 심볼들만 나타나기 때문에, 내부 static 함수나 변수들은 출력되지 않습니다 (동적 심볼 테이블에 없으므로). 만약 nm을 -D 없이 실행하면 정적 심볼 테이블까지 포함하여 모든 심볼이 출력되는데, 이 경우 static 심볼들도 보입니다. 예를 들어 nm libexample.so (옵션 없이)의 출력에 internal_function이 소문자 t로 나타난다면, 이는 해당 함수가 내부(static) 심볼이라 외부로 export되지 않았음을 알 수 있습니다. 반면 nm -D 출력에는 internal_function이 없으므로 export되지 않은 것입니다.
참고: C++로 작성된 라이브러리의 경우 nm 출력 심볼 이름이 mangled name(이름 변환) 형태일 수 있습니다. 이때 nm --demangle (또는 nm -C) 옵션이나 c++filt 도구를 사용하면 사람이 읽기 쉬운 원래의 함수 이름으로 풀어줍니다. C 코드 또는 extern "C"로 선언된 함수는 이름이 그대로 출력됩니다.
nm은 맥 OS 등 다른 Unix 계열에서도 사용 가능합니다. Mac의 .dylib에서는 nm -gU 등을 사용하지만, 여기서는 Linux ELF에 집중합니다.
objdump 도구 사용 (CLI)
objdump는 바이너리의 다양한 정보를 표시할 수 있는 만능 도구입니다. 심볼 목록을 보려면 objdump에 적절한 옵션을 주면 됩니다. 두 가지 방식이 있습니다.
- objdump -t libexample.so : 정적 심볼 테이블(symbol table)의 모든 심볼을 표시합니다 (nm 기본 출력과 유사). 여기엔 로컬 심볼까지 모두 나오지만, 보통 공유 라이브러리는 릴리즈 빌드시 불필요한 심볼을 strip하기 때문에 -t 출력이 없을 수도 있습니다.
- objdump -T libexample.so : 동적 심볼 테이블을 표시합니다. -T는 ELF의 .dynsym 섹션을 덤프하며, 이는 nm -D와 마찬가지로 export된 심볼과 import된 외부 참조만 보여줍니다
예를 들어 objdump -T libexample.so의 출력은 다음과 같은 컬럼을 가집니다.
$ objdump -T libexample.so
00000000 g DF .text 0000003d Base exported_function
00000000 g DO .data 00000004 Base exported_variable
...
- 앞의 주소와 함께 g DF .text처럼 표시되는데, g는 글로벌, DF는 FUNC (함수), .text는 코드 섹션에 정의됨을 뜻합니다. 즉 exported_function이 전역 함수로 export되었음을 보여줍니다.
- DO .data는 OBJECT (데이터 객체)를 의미하며 .data 섹션에 있는 전역 변수 심볼임을 나타냅니다. exported_variable이 전역 변수로 export되었다는 뜻입니다.
- Base는 심볼 버전(version)이 없는 기본 심볼임을 나타냅니다 (라이브러리에 버전 스크립트를 사용한 경우 해당 심볼의 버전 정보가 표시될 수 있습니다).
이처럼 objdump -T는 nm -D와 유사한 정보를 제공하지만, 심볼의 크기나 섹션 같은 추가 정보도 보여줍니다. 만약 자세한 헤더와 섹션 정보까지 보고 싶다면 objdump -x libexample.so (파일 헤더와 섹션 헤더 등 모두 출력)로 전체 내용을 볼 수 있습니다
readelf 도구 사용 (CLI)
readelf는 ELF 파일 구조를 해석해주는 도구입니다. 심볼 정보를 보려면 readelf -s 옵션을 사용합니다. 기본적으로 readelf -s libexample.so는 심볼 테이블을 모두 보여주는데, 공유 라이브러리의 경우 정적 심볼 테이블(.symtab)과 동적 심볼 테이블(.dynsym)이 모두 있을 수 있습니다. 동적 심볼만 보려면 readelf --dyn-syms -s libexample.so 또는 줄여서 readelf -Ds libexample.so 명령을 사용할 수 있습니다 출력 형식은 nm보다 상세하여, 각 심볼의 인덱스, 값, 크기, 타입(Func/Object), 바인딩(Global/Local), 섹션, 이름 등이 테이블 형태로 나옵니다. Global 바인딩의 FUNC/OBJECT가 곧 export된 함수/변수이고, UND로 표시된 것은 undefined (import) 심볼입니다.
예를 들어 readelf -Ws libexample.so의 출력 일부는 다음과 같을 수 있습니다 (열을 간략화하여 표시):
Num: Value Size Type Bind Vis Ndx Name
10: 0000111b 0x3d FUNC GLOBAL DEFAULT 13 exported_function
11: 00004010 0x4 OBJECT GLOBAL DEFAULT 25 exported_variable
12: 00000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5
여기서 Bind GLOBAL이고 Ndx (섹션 인덱스)가 UND가 아닌 항목들이 바로 이 라이브러리에서 제공하는 export 심볼입니다.
readelf의 출력은 기계적으로 정확하지만 다소 장황할 수 있으므로, 간단히는 nm이나 objdump 출력만으로도 충분합니다.
Linux에서의 빌드 옵션과 심볼 가시성
Linux의 공유 라이브러리는 별다른 지시 없이 컴파일하면 모든 전역 함수/변수가 기본적으로 export됩니다. 즉, static으로 선언하지 않은 함수는 특별히 숨기지 않는 한 동적 심볼 테이블에 나타나게 됩니다. 경우에 따라 내부 구현 함수까지 외부에 노출되는 것을 원하지 않을 수 있는데, 이런 불필요한 심볼 노출을 막기 위해 visibility를 제어할 수 있습니다. GCC에서는 -fvisibility=hidden 컴파일 옵션을 주면 기본적으로 모든 전역 심볼을 숨기고, 필요한 심볼만 선택적으로 export할 수 있습니다. 예를 들어 컴파일/링크 옵션에 -fvisibility=hidden을 추가하고, 외부에 공개해야 할 함수나 변수 선언에 __attribute__((visibility("default")))를 붙이면 해당 심볼만 외부로 공개되고 나머지는 숨겨집니다 .
이 기법을 사용하면 nm -D 출력에서 공개하지 않은 내부 함수들은 사라지고, 설계한 API만 export됩니다. (참고로, GCC 4.x 이후로는 라이브러리 제작 시 이 방법이 권장되어, 불필요한 심볼을 줄이고 DSO 로딩 시간을 개선하는 등 이점이 있습니다.) 예시:
// 헤더 파일 선언부
__attribute__((visibility("default"))) void public_func();
// 구현 파일
void public_func() { /* ... */ } // 공개함수: default visibility
static void internal_util() { /* ... */ } // 내부함수: hidden (static이거나 visibility hidden)
이렇게 빌드하면 nm -D에 public_func만 나타나고 internal_util은 나타나지 않게 됩니다. (동일 효과를 링커의 버전 스크립트로도 낼 수 있지만, 컴파일 속성을 사용하는 방법이 더 간편합니다.)
Windows – DLL(.dll)의 심볼 확인
Windows DLL에서는 어떤 함수들이 외부로 export되었는지 확인하는 방법으로 CLI 도구와 GUI 도구를 모두 사용할 수 있습니다. Windows에서는 기본적으로 DLL을 빌드할 때 개발자가 export 대상을 지정해주지 않으면 아무 심볼도 export되지 않는다는 점이 Linux와 다릅니다. 따라서 Windows DLL의 내보낸 심볼 목록을 확인하는 것은 곧, 빌드 시 설정한 __declspec(dllexport)나 .def 파일에 의해 지정된 API가 잘 포함되었는지를 확인하는 과정입니다.
대표적인 CLI 도구로는 Visual Studio에 포함된 dumpbin이 있고, GUI 도구로는 Dependency Walker 등이 널리 사용됩니다.
dumpbin 도구 사용 (CLI)
Microsoft의 Visual C++ 툴체인에는 dumpbin이라는 실행 파일이 포함되어 있는데, 이를 이용하면 DLL(또는 LIB)의 정보를 덤프할 수 있습니다. Developer Command Prompt (개발자 명령 프롬프트)에서 dumpbin /EXPORTS 파일명.dll 명령을 실행하면 해당 DLL의 내보낸 심볼 목록을 볼 수 있습니다. 사용 순서는 다음과 같습니다.
- Visual Studio에서 개발자 명령 프롬프트를 엽니다 (VS2019/2022의 경우 Tools > Command Line > Developer Command Prompt 메뉴 등으로 실행).
- DLL 파일이 있는 디렉토리로 이동하여, 명령어 dumpbin /exports MyLibrary.dll를 실행합니다.
- 출력 결과를 확인합니다.
예를 들어 MyLibrary.dll에 대해 dumpbin /exports MyLibrary.dll을 실행하면 다음과 같은 형식의 출력이 나타납니다:
Dump of file MyLibrary.dll
File Type: DLL
Section contains the following exports for MyLibrary.dll
00000000 characteristics
00000000 time date stamp
0.00 version
1 ordinal base
3 number of functions
3 number of names
ordinal hint RVA name
1 0 00001000 MyFunction
2 1 00001030 MyVariable
3 2 00002000 _DllMainCRTStartup@12
위 출력에서 중요한 부분은 마지막 표 형태입니다. 각 열의 의미는 다음과 같습니다.
- ordinal: 내보낸 심볼의 서수(Ordinal) 값입니다. DLL에서 심볼마다 고유한 번호가 부여되며, import 라이브러리가 이 번호로 참조할 수도 있습니다. Ordinal base가 1이면 첫 번째 심볼의 ordinal이 1부터 시작함을 뜻합니다.
- hint: 심볼 이름 검색을 빠르게 하기 위한 힌트 인덱스입니다. (보통 내부용이라 크게 신경쓰지 않아도 됩니다.)
- RVA: 해당 심볼의 Relative Virtual Address (상대 가상 주소)로, DLL 로드 시 메모리에서의 오프셋 주소를 나타냅니다. 즉, DLL 시작을 기준으로 함수 코드가 있는 주소 값입니다.
- name: 내보낸 심볼 이름입니다. 함수 이름 또는 변수 이름이 표시됩니다. C로 export된 함수는 그대로 이름이 나오지만, __stdcall 규약을 사용했다면 이름 뒤에 @<스택 바이트> 형식으로 표기되거나, C++ 함수라면 맹글링된 이름이 보일 수 있습니다. 예를 들어 _DllMainCRTStartup@12는 DLL 엔트리 포인트로, stdcall 규약에 따라 추가 장식이 된 이름입니다.
dumpbin /exports 결과에서 함수명/변수명 목록을 보고, 내가 의도한 함수들이 잘 export되었는지 확인할 수 있습니다. 만약 리스트에 없다면, 빌드 과정에서 내보내기 선언을 빼먹은 것입니다. C++ 함수가 목록에 있을 경우 맹글링된 이름 (예: ?FuncName@@YAXH@Z)으로 표시될 수 있는데, 이는 Visual C++의 장식 규칙에 따른 것입니다. 이런 경우 DLL을 배포할 때 C 인터페이스로 extern "C"를 사용하거나 .def 파일을 통해 이름을 지정하는 방식으로 이름 장식 문제를 해결해야 합니다.
참고: Visual Studio가 없다면, GNU Binutils의 objdump로도 Windows DLL의 export를 확인할 수 있습니다. 예를 들어 Linux 환경에서 objdump -p MyLibrary.dll를 실행하면 PE 포맷의 내보낸 심볼 (.edata 섹션) 정보를 읽을 수 있습니다. 출력에서 [Ordinal/Name Pointer] Table 아래에 함수 목록이 표시되며, Ordinal과 Name 열을 통해 export 심볼을 파악할 수 있습니다. 하지만 이 방법은 출력 해석이 다소 불편하므로, 일반적으로 Windows에서는 dumpbin이나 전용 GUI 툴을 사용하는 편이 낫습니다.
Dependency Walker 사용 (GUI)
Dependency Walker(depends.exe)는 Windows DLL의 의존성과 내보낸/불러오는 심볼을 그래픽 인터페이스로 보여주는 오래된 도구입니다. 마이크로소프트 공식 툴은 아니지만 개발자 사이에 널리 사용됩니다. 사용 방법은 다음과 같습니다.
- Dependency Walker 프로그램(depends.exe)을 실행합니다. (공식 웹사이트에서 다운로드한 후 별도 설치 없이 실행 가능)
- 메뉴에서 File > Open 을 클릭하고 검사하고자 하는 DLL 파일(MyLibrary.dll)을 엽니다.
- 잠시 분석이 진행된 후, 화면에 해당 DLL의 내보낸 함수 목록이 표시됩니다. 보통 상단 창에 “Exports”로 불리는 목록이 나타나고, 하단 창에는 해당 DLL이 의존하는 다른 DLL들(Imports)이 나열됩니다.
- 상단 Exports 창에서 함수 이름, ordinal, 그리고 Hint 등을 확인할 수 있습니다. Dependency Walker는 dumpbin과 유사한 정보를 보여주지만, GUI로 제공하므로 스크롤과 정렬이 가능합니다.
예를 들어 MyLibrary.dll을 열었을 때 Dependency Walker의 Export 창에 MyFunction, MyVariable 등이 나타나면, 해당 심볼들이 외부로 export되었음을 쉽게 확인할 수 있습니다. 만약 의도한 함수가 리스트에 없으면, 앞서 언급한 대로 빌드 설정을 확인해야 합니다.
추가 도구: 이 외에도 DLL Export Viewer(NirSoft 제공)와 같은 GUI 툴도 있습니다. Dependency Walker와 비슷하게 DLL을 열면 export 심볼을 나열해주며, 좀 더 현대적인 Windows에서도 동작합니다. 그러나 기본 원리는 동일하므로 여기서는 자세한 설명을 생략합니다.
Windows에서의 빌드 옵션과 export 설정
Windows에서는 DLL을 생성할 때 어떤 심볼을 export할지 명시적으로 지정해야 합니다. 기본적으로 __declspec(dllexport) 키워드를 사용하여 함수나 변수를 선언하면, 컴파일 시 해당 심볼이 DLL의 export 테이블에 추가됩니다. 반대로 DLL을 사용하는 쪽에서는 __declspec(dllimport)를 통해 import 라이브러리 사용을 명시하지요. 간단한 예를 들면:
// dllheader.h (DLL의 공개 헤더)
__declspec(dllexport) int myFunction(int x);
// dll.c (DLL 구현)
__declspec(dllexport) int myFunction(int x) { return x * 2; }
static int internalUtil(int y) { return y * 3; }
위 코드에서 myFunction은 __declspec(dllexport)로 선언되었으므로 DLL의 내보낸 심볼 테이블에 포함됩니다. 반면 internalUtil은 static이며 dllexport가 없으므로 외부에 노출되지 않습니다. 이 DLL을 빌드한 뒤 dumpbin /exports나 Dependency Walker로 확인해보면 myFunction만 export 목록에 나타나고 internalUtil은 보이지 않게 됩니다.
만약 __declspec(dllexport)를 쓰지 않고 DLL을 빌드하면, 기본적으로 export되는 심볼이 없습니다. (예외적으로 DllMain처럼 시스템이 암묵적으로 참조하는 엔트리 포인트나, C++의 extern "C"로 내보낸 객체를 .def 파일에 기재한 경우 등이 있지만, 일반적인 C 코드에서는 지정하지 않는 한 export되지 않습니다.) 따라서 Windows DLL에서 API를 제공하려면 반드시 헤더에 export할 함수/변수를 __declspec(dllexport)로 선언하거나, 모듈 정의 파일(.def)의 EXPORTS 절에 해당 이름을 나열해야 합니다. .def 파일을 사용하는 방법은 대규모 프로젝트에서 유용하며, 이를 사용하면 소스코드에 컴파일 지시문을 넣지 않고도 export 심볼을 관리할 수 있습니다. .def 파일이 사용된 경우에도 dumpbin /exports 출력에 해당 함수들이 나타납니다 (이 때 함수 이름 옆에 ordinal만 지정되고 hint는 0으로 표시되는 등 출력 형태만 약간 다릅니다).
마지막으로, C++ 함수를 DLL에서 export할 때는 name mangling에 주의해야 합니다. C++ 함수명을 그대로 export하면 dumpbin이나 Dependency Walker에 맹글된 이름이 보이고, 호출 시에도 그 맹글된 이름을 알아야 합니다. 이를 피하려면 함수 선언을 extern "C"로 감싸서 C링케이지로 만들거나, .def 파일에서 Alias를 지정하여 이름을 변경할 수 있습니다. 일반적으로 DLL의 공개 API는 C 인터페이스로 구성하거나, C++ 클래스라도 공용 API에는 C 방식의 래퍼를 제공하기도 합니다.