본문 바로가기
IT 공부/Visual Studio 툴 사용하기

[Visual Studio] - CRT 런타임 라이브러리의 빌드 방식

by exdus3156 2024. 12. 26.

 

 

#1. 의문 : 링커는 어떻게 C/C++ 표준 라이브러리를 찾는가?

 

▷ Visual Studio 즉, MSBuild 환경에서 컴파일러에게 외부의 정적 라이브러리를 연결하는 방식은 간단히 요약해 아래와 같다.
 1) [추가 포함 디렉터리]에 헤더파일의 경로를 추가한다.
 2) 라이브러리의 경로를 링커에게 알려준다. 보통 [추가 라이브러리 디렉터리]에 추가한다.
 3) 라이브러리의 이름을 링커에게 알려준다. 보통 [추가 종속성]에 추가한다.

 

▷ 위와 같은 방식으로 MSBuild는 컴파일러(cl.exe)와 링커(link.exe)를 동작시킨다. C/C++ 표준 라이브러리나 Windows SDK 관련 헤더 파일은 환경 변수 INCLUDE(포함 디렉터리)에 실제로 존재한다. 헤더 파일을 찾는 것은 문제가 없다.

 

외부 정적 라이브러리의 경로를 알려주는 것도 문제가 없다. [추가 라이브러리 디렉터리]로 그 위치를 입력하면 되고, C/C++ 표준과 Windows SDK는 [포함 디렉터리]에 예약된 기본 경로가 있다. 라이브러리의 경로 설정도 문제가 없다.

 

▷ 그런데 외부 정적 라이브러리의 이름을 알려주는 과정에 있어서는 이상한 점이 하나 있다. 그것은 [추가 종속성]이란 옵션으로 라이브러리 이름을 지정해준다는 것이다. 무언가 의심쩍다. '추가'라는 단어가 무색하게 [종속성]이라는 설정 옵션은 없기 때문이다.

 

 

▷ 위와 같이 [라이브러리 디렉터리]에 설정된 기본 경로는 존재하는데, 정작 [종속성] 옵션으로 라이브러리 이름을 지정하지는 않고 있다. [라이브러리 디렉터리]에는 C/C++ 표준 라이브러리와 Windows SDK의 라이브러리들이 있다. 특히 C/C++ 표준 라이브러리 파일 중 하나인 msvcrt.lib도 이 경로에 있다.

 

 

[종속성]이라는 옵션이 없다면, [추가 종속성] 즉, 링커에게 명령줄로 msvcrt.lib 이름이 전달되는 것일까?

 

하지만 정작 프로젝트 빌드 옵션을 보면, 링커에게 명령줄로 전달하는 라이브러리 이름 목록을 볼 수 있다. 그런데 kernel32나 user32 등 무언가 살벌하고 중요해 보이는 라이브러리 파일들은 있는 반면, 정작 표준 라이브러리와 관련된 msvcrt.lib가 없다! 

 

 

DirectX와 같은 Windows SDK 관련 라이브러리는 전통적인 방식대로 빌드한다. 그런데 C/C++ 표준 라이브러리는 별도의 설정 없이도 동작했다... 그렇다면 도대체 어떻게 링커는 소스코드에 작성된 printf() 같은 C/C++ 표준 함수의 구현 라이브러리를 찾는 것일까

 

 

 

#2. CRT 런타임 라이브러리 옵션

 

표준 C/C++ 라이브러리는 Visual Studio에서 외부 라이브러리를 연결하는 방식과 다소 다르게 동작한다. 아예 설정 페이지에 별도의 옵션 자체가 할당될 정도인데 아래 그림과 같다.

 

 

[C/C++] 항목의 [코드 생성] - [런타임 라이브러리] 옵션이 바로 C/C++ 표준 라이브러리의 연결과 관련된 부분이다. 런타임 라이브러리란 말 자체가 CRT(C Run-Time)를 말하는 것이다. 

 

그런데 지금 [C/C++] 항목에서 설정하고 있기 때문에 이것은 아래와 같이 VC++ 컴파일러(cl.exe)에게 명령줄의 옵션으로 무언가를 설정하고 있다는 뜻이다. 즉, 링크 단계가 아닌 컴파일 단계에서 C/C++ 표준 라이브러리와 관련이 있는 설정을 하고 있는 것이다.

 

 

그렇다면 무언가 컴파일/전처리 단계에서 심오한 일이 벌어지고 있다는 뜻이다. 아래의 그림은 런타임 라이브러리 항목의 선택 사항들이다.

 

 

/MT, /MD 가 구분되어 있고, 뒤에 d가 붙은 /MTd 와 /MDd가 보인다. 모두 멀티스레드이므로 멀티스레드가 이들의 구분 기준이 되진 않는다.  d는 디버그 빌드와 관련이 있다. 핵심은 DLL의 유무다. 즉, 동적 라이브러리와 관련되어 있다는 뜻이다.

 

※ VS 2005 버전 이전에는 런타임 라이브러리가 싱글 스레드 기반으로도 동작했는데, 이것이 나중에 멀티스레딩 환경에서 충돌이 일어나 이제는 싱글스레드 기반 런타임 라이브러리는 지원되지 않는다. 이게 무슨 말이냐면, 예를 들어 프로그램이 멀티스레드를 사용한다면 각 스레드가 각자 런타임 라이브러리를 사용할 것이다. 이 순간 동기화 문제가 발생할 것이라 예상할 수 있다. 한 스레드가 malloc()으로 메모리를 할당하는 과정에서 제대로 락(lock)되지 않고 다른 스레드로 실행 흐름이 넘어가 그 스레드가 또 메모리를 할당하는 malloc()을 호출했다면...? 멀티스레딩 환경에서 싱글스레드 런타임 라이브러리를 사용한다는 게 얼마나 말이 안 되는지 알 수 있을 것이다.

 

마이크로소프트 공식 문서(링크)를 보면 아래의 설명이 있다.

 

 

_MT, _DLL, _DEBUG를 정의한다는 뜻은 매크로를 정의한다는 뜻이다. 실제로 #ifdef _MT 같은 코드로 확인해볼 수 있다. 이렇듯, 전처리 과정에 개입하고 있으므로 링커가 아닌 컴파일 단계에서 동작하는 이유가 다소 설명이 된다.

 

핵심은 컴파일러가 라이브러리 이름(msvcrt.lib, libcmt.lib)를 컴파일 단계의 산출물인 오브젝트 파일에 그 이름을 작성한다고 설명한 부분이다. 디버그 빌드를 위해서도 msvcrtd.lib, libcmtd.lib가 오브젝트 파일에 작성된다.

 

바로 이 기능 덕분에 링커에게 따로 라이브러리의 이름을 따로 알려주지 않아도 링커가 오브젝트 파일의 내부를 파싱하면서 참조해야 할 라이브러리 파일을 찾을 수 있는 것이다.

 

MD와 MT의 핵심적인 차이는 동적 링크냐 정적 링크냐의 차이다. 즉, 예를 들어 printf() 함수 호출 시 이 함수는 C/C++ 표준 함수이므로 구현체는 라이브러리에 있을 것이다. 놀랍게도 MSVC는 표준 런타임 라이브러리를 동적 링크와 정적 링크 둘 다 지원해주는 방향으로 설계되었다.

 

/MD 옵션은 표준 런타임 라이브러리의 함수 호출 시 DLL(동적 링크)을 호출한다. /MT 옵션은 정적 링크되므로 동적 링크와 상관 없이 자기 자신의 실행 파일(exe)에 함수가 포함되어 있다. 

 

/MD는 msvcrt.lib와 연결된다. 이 정적 라이브러리는 mxvcrt.dll 동적 라이브러리를 링크하기 위한 import 라이브러리다. 배포 시 사용자의 시스템에 있는 dll 파일과 연결되며, 표준 라이브러리 함수 호출은 이 dll에 있는 함수를 호출한다.

 

/MT는 libcmt.lib와 연결된다. 이 정적 라이브러리는 그 자체로 이미 표준 라이브러리의 구현체다. 따라서 이 정적 라이브러리와 링크된 실행 파일은 부피가 커지긴 하지만 자체적으로 함수를 단독적으로 호출할 수 있다.

 

이렇게 두 가지 방향을 모두 지원한 이유는 각각 별도의 장단점이 있기 때문이며, C/C++ 개발 환경에서 표준 라이브러리는 100% 사용되기 때문에 개발자에게 선택권을 준 것이라 볼 수 있다. 동적 링크를 사용하는 경우, CRT를 사용하는 많은 모듈을 함께 빌드할 때 특히 유리하다. 만약 수많은 프로젝트와 소스코드가 표준을 호출할 때 MT(정적 링크)로 설정했다면 각각의 파일들이 너무 부피가 커졌을 것이다.

 

#3. MD, MT 관련 빌드 시 발생하는 문제에 대하여... 

 

MD는 동적, MT는 정적으로 CRT와 연결된다는 설명 자체는 매우 쉽지만, 실제 빌드 시 이것이 문제를 일으키는 경우가 정말 많다. 공식 문서에서도 서로 다른 옵션으로 빌드된 라이브러리를 함께 빌드하지 말라고 아래와 같이 경고하고 있으니 말이다.

 

 

빌드 시 에러가 자주 발생하는 부분은 라이브러리 충돌 문제다. 특히 링크 단계에서 에러가 많이 발생한다. 한 모듈은 MD(동적) 방식이고, 다른 모듈은 MT(정적) 방식이라면 이들이 링커에 의해 통합되는 과정에서 무슨 문제가 발생할까?

 

바로 기호의 충돌이 발생한다. 잘 생각해보면 이 둘은 서로 논리적으로 앞뒤가 맞지 않는다. 동적 방식은 런타임에 DLL 파일이 exe 실행 프로세스의 논리 주소 일부로 바인딩된다. 그런데 이미 실행 파일에 통합된 모듈에서 내부적으로 DLL과 같은 함수들이 이미 있다면? DLL 파일도 printf()를 지원하고, 정적으로 연결된 실행파일 내부에도 printf()가 있다면 누굴 호출해야 할지 알 수 없다.

 

게다가 두 종류의 런타임 라이브러리 모두 별도의 힙 메모리(동적 할당 영역)를 사용한다. 생각해보면 당연하다. 둘 모두 멀티스레드 환경에서 동작할 수 있도록 동작하지만, 근본적으로 정적 라이브러리는 실행 파일(exe) 자체의 영역 안에서 독자적으로 존재하는 반면, DLL은 메모리에 상존하는 모든 프로세스가 공유하고 있기 때문에 메모리 관리 방식과 영역이 다르다. 서로 힙이 다르다. 따라서 MD(DLL)에서 malloc()을 호출해 메모리를 할당받고 이것을 MT(staitc lib)의 free()가 해제하려고 하면 서로 다른 힙 영역을 침범하게 되어 문제가 발생한다.

 

따라서 MD는 MD끼리, MT는 MT끼리 연결하자. 각각 별도의 모듈이라도 같은 런타임 라이브러리 옵션을 사용했다면 참조하는 라이브러리가 같기 때문에 링크 과정에서 문제가 발생하지 않지만, 만약 MD와 MT를 섞어 버린다면 서로 참조하는 라이브러리가 다르므로(msvcrt, libcmt) 혹시나 빌드가 되어 실행 도중에 프로세스가 뻗을 지도 모른다. 보통은 링커 단계에서 에러를 찾지만, 구글링을 해보니 실행 도중에 뻗는 경우도 종종 있는 것 같다.