본문 바로가기

Programming

윈도우에서 유닉스로 이식하기, Part 1: C/C++ 이식

Rahul Kardam, 선임 소프트웨어 개발자, Synapti Computer Aided Design Pvt Ltd
2008 년 3 월 04 일

때로 소프트웨어 프로그램은 프로그램을 작성하거나 개발한 시스템과는 완전히 동떨어진 시스템에서 동작하도록 만들어집니다. 시스템에 맞춰 소프트웨어를 수정하는 과정을 이식이라 부릅니다. 여러 가지 이유 때문에 소프트웨어를 이식할 필요가 생깁니다. 최종 사용자들이 다양한 유닉스(UNIX®) 버전 같은 새로운 환경에서 소프트웨어를 사용하기를 원하거나 조직에서 사용하는 플랫폼에 맞춰 최적화하는 과정에서 개발자가 손수 만든 코드를 해당 소프트웨어에 통합할지도 모릅니다.

윈도우에서 유닉스 환경으로 이식하기

대다수 마이크로소프트 윈도우(Microsoft® Windows®) 기반 제품은 마이크로소프트 비주얼 스튜디오(Microsoft Visual Studio®)를 사용해 만들어진다. 비주얼 스튜디오는 복잡한 통합 개발 환경(IDE)을 제공해 개발자를 위한 전체 빌드 과정을 거의 대부분 자동화해 준다. 한 술 더 떠, 윈도우 개발자들은 윈도우 플랫폼에 밀접한 응용 프로그램 인터페이스(API), 헤더, 언어 확장을 사용한다. SunOS, OpenBSD, IRX와 같은 대다수 유닉스(UNIX®) 시스템은 IDE 또는 윈도우에 밀접한 헤더나 확장을 제공하지 않으므로 종종 이식은 시간 소모적인 작업으로 변한다. 설상가상으로, 기존에 사용하던 윈도우 기반 코드는 16비트나 32비트 x86 하드웨어에서 동작한다. 유닉스 기반 환경은 종종 64비트이며 대다수 유닉스 회사는 x86 명령어 집합을 지원하지 않는다. 2회 연재 중 첫 번째인 이 기사에서는 윈도우 운영체제에 기반을 둔 전형적인 비주얼 C++ 프로젝트를 SunOS에 기반한 g++ 환경으로 이식하는 베일을 벗기는데, 앞서 말한 몇 가지 쟁점에 대해서는 세부 내용까지 설명하겠다.

비주얼 스튜디오에서 지원하는 C/C++ 프로젝트 유형

비주얼 C++를 사용하면 (단일 스레드나 다중 스레드를 지원하는) 세 가지 프로젝트 종류 중 하나를 생성할 수 있다.

  • 동적 링크 라이브러리(DLL 또는 .dll)
  • 정적 라이브러리(LIB 또는 .lib)
  • 실행 파일(.exe)

좀 더 복잡한 프로젝트 종류가 필요하다면, 비주얼 스튜디오 .NET을 사용한다. 이 프로그램은 다중 프로젝트를 생성해 관리하도록 지원한다. 다음 이어지는 절에서는 동적 라이브러리와 정적 라이브러리 프로젝트를 윈도우 환경에서 유닉스 환경으로 이식하는 데 초점을 맞춘다.

DLL을 유닉스 환경으로 이식하기

윈도우에서 .dll 파일은 유닉스에서 .so(shared object) 파일이다. 하지만 .so 파일 생성 과정은 .dll 파일 생성 과정과 조금 다르다. Listing 1에 나온 예제는 main.cpp 파일에 있는 main에서 호출하는 간단한 함수인 printHello를 정의한다.


Listing 1. printHello 루틴 선언을 포함하는 파일인 hello.h
                
#ifdef BUILDING_DLL
  #define PRINT_API __declspec(dllexport)
#else
  #define PRINT_API __declspec(dllimport)
#endif

extern "C" PRINT_API void printHello();

Listing 2는 hello.cpp 원시 코드다.


Listing 2. hello.cpp
                
#include <iostream>
#include "hello.h"

void printHello
  {
  std::cout << "hello Windows/UNIX users\n";
  }

extern "C" PRINT_API void printHello();

x86 플랫폼을 위한 마이크로소프트 32비트 C/C++ 표준 컴파일러(cl.exe)를 사용한다면 hello.dll 파일을 만들기 위해 다음과 같은 명령을 내린다.

cl /LD  hello.cpp /DBUILDING_DLL

/LD 옵션은 .dll 파일을 생성하라고 지시한다(.exe나 .obj와 같은 다른 형식을 생성하도록 옵션을 바꿀 수 있다). /DBUILDING_DLL은 이 DLL에서 printHello 심볼을 공개하도록 빌드 과정에서 PRINT_API 매크로를 정의한다.

Listing 3은 main.cpp 원시 파일을 담고 있는데, 여기서 printHello 루틴을 호출한다. 이 글에서는 hello.h, hello.cpp, main.cpp가 모두 같은 폴더에 있다고 가정한다.


Listing 3. printHello 루틴을 호출하는 main 원시 코드
                
#include "hello.h"

int main ( )
  {
  printHello();
  return 0;
  }

main 코드를 컴파일해 링크하려면 다음과 같은 명령을 내린다.

cl main.cpp hello.lib

원시 코드와 생성된 결과물을 슬쩍 살펴보면 두 가지 중요한 사실이 드러난다. 첫째로 윈도우에 밀접한 구문인 __declspec(dllexport)가 함수, 변수, 클래스를 DLL에서 공개할 때 필요하다. 비슷하게, 윈도우에 밀접한 구문인 __declspec(dllimport)는 DLL에서 함수, 변수, 클래스를 불러쓸 때 필요하다. 둘째로 컴파일 결과로 printHello.dll과 printHello.lib라는 파일 두 개가 생긴다. printHello.lib는 main에서 링크할 때 사용하며, 공유 목적 파일을 위한 유닉스 헤더는 declspec 구문을 요구하지 않는다. 컴파일을 성공적으로 마치고 나면 .so 파일 하나만 남아 main과 링크할 때 사용한다.

g++를 사용해 유닉스 플랫폼에서 공유 라이브러리를 만들기 위해, fPIC 플래그를 g++에 붙이는 방법으로 모든 원시 파일을 재배치 가능한 공유 목적 파일로 컴파일한다. PIC는 위치 독립 코드(Position Independent Code)의 약어다. 공유 라이브러리는 메모리에 올라올 때마다 새로운 메모리 주소에 사상될 가능성이 있다. 따라서 라이브러리 내부에 존재하는 모든 변수와 함수 주소를 계산하기 쉬운 방식으로 라이브러리를 메모리에 올리는 시작 주소에 상대적으로 배치해야 한다. -fPIC 옵션으로 생성하면 재배치 가능한 코드가 나온다. -o 옵션은 출력 파일 이름을 지정하며, -shared 옵션은 미정의 심볼 참조를 허용하도록 공유 라이브러리를 만든다. hello.so 파일을 생성하려면, Listing 4와 같이 헤더 파일을 변경하자.


Listing 4. 유닉스에 밀접하도록 변경한 수정된 hello.h 헤더
                
#if defined (__GNUC__) && defined(__unix__)
  #define PRINT_API __attribute__ ((__visibility__("default")))
#elif defined (WIN32)
  #ifdef BUILDING_DLL
    #define PRINT_API __declspec(dllexport)
  #else
    #define PRINT_API __declspec(dllimport)
#endif

extern "C" PRINT_API void printHello();

그리고 hello.so 공유 라이브러리를 링크하기 위한 g++ 명령은 다음과 같다.

g++ -fPIC -shared hello.cpp -o hello.so

main 실행 파일을 생성하려면 다음과 같이 컴파일한다.

g++ -o main main.cpp hello.so

g++에서 심볼 감추기

윈도우 기반 DLL에서 심볼을 공개하는 방법에는 두 가지가 있다. 첫 번째 방법으로 DLL에서 공개할 필요가 있는 선택된 항목(예를 들어 클래스, 전역 변수, 전역 함수)에만 적용되는 __declspec(dllexport)를 활용한다. 두 번째 방법으로 모듈 정의(.def) 파일을 활용한다. .def 파일은 독자적인 구문을 제공하며 DLL에서 공개할 필요가 있는 심볼을 포함한다.

g++ 링커의 기본 행동 양식은 .so 파일에서 모든 심볼을 공개한다. 이런 행동 양식이 바람직하지 않을지도 모르며, 다중 DLL 링크를 시간 소모적인 작업으로 만들어버린다. 공유 라이브러리에서 선택적으로 심볼을 공개하려면 g++에서 제공하는 속성 메커니즘을 활용한다. 예를 들어, 'void print1();''int print2(char*);' 메서드 두 개가 있을 때 print2만 공개하고 싶은 경우가 있다. Listing 5는 윈도우와 유닉스에서 이렇게 print2만 공개하는 방법을 보여준다.


Listing 5. g++에서 심볼 감추기
                
#ifdef _MSC_VER // Visual Studio specific macro
  #ifdef BUILDING_DLL
    #define DLLEXPORT __declspec(dllexport)
  #else
    #define DLLEXPORT __declspec(dllimport)
  #endif
  #define DLLLOCAL
#else
  #define DLLEXPORT __attribute__ ((visibility("default")))
  #define DLLLOCAL   __attribute__ ((visibility("hidden")))
#endif

extern "C" DLLLOCAL void print1();         // print1 hidden
extern "C" DLLEXPORT int print2(char*); // print2 exported

__attribute__ ((visibility("hidden")))을 사용하면 DLL에서 심볼 공개를 방지한다. (4.0.0 이상) 최신 g++ 버전은 또한 -fvisibility 스위치를 제공하는데, 공유 라이브러리에서 선택적으로 심볼을 공개하도록 허용한다. g++ 명령을 내릴 때 -fvisibility=hidden을 붙이면 공유 라이브러리에서 __attribute__ ((visibility("default")))로 선언된 심볼을 제외한 모든 심볼 공개를 중단한다. 이게 바로 명시적으로 visibility로 표시하지 않은 선언 전부를 숨김 속성으로 취급하도록 g++에 요청하는 깔끔한 방식이다. dlsym을 사용해 숨겨진 심볼을 추출할 경우 NULL이 반환된다.

g++에서 속성 메커니즘 개괄

C/C++ 위에 추가 구문을 다양하게 제공하는 비주얼 스튜디오 환경과 아주 흡사하게, g++도 언어에 비표준적인 확장을 다양하게 제공한다. 이 중 하나가 바로 g++에서 지원하는 속성 메커니즘으로 이식 과정에 도움을 준다. 직전 예제는 심볼을 감추는 방법을 다뤘다. 또 다른 속성 활용 방안으로 비주얼 C++에서 제공하는 cdecl, stdcall, fastcall과 같은 함수 유형을 g++로 이식하는 방법이 있다. 2회에서 속성 메커니즘을 심도있게 다룰 예정이다.

DLL이나 유닉스 환경에서 사용하는 공유 목적 파일을 명시적으로 메모리에 올리기

윈도우 시스템에서는 윈도우 프로그램이 .dll 파일을 명시적으로 메모리에 올린다. 예를 들어 인쇄 기능을 갖춘 복잡한 윈도우 기반 편집기를 생각해보자. 이런 편집기는 사용자가 요청할 때 처음으로 프린터 드라이버를 위한 DLL을 메모리에 올리게 된다. 윈도우 기반 개발자는 비주얼 스튜디오가 제공하는 LoadLibrary와 같은 API를 사용해 DLL을 명시적으로 메모리에 올리고, GetProcAddress를 사용해 DLL에서 심볼을 질의하고 FreeLibrary를 사용해 명시적으로 메모리에 올린 DLL을 내린다. 동일한 작용을 하는 유닉스 계열 함수는 각각 dlopen, dlsym, dlclose 루틴이다. 한술 더 떠, 윈도우에서는 DLL을 메모리에 올릴 때 처음으로 호출하는 특별한 DllMain 메서드도 있다. 유닉스 기반 시스템에서 대응하는 메서드는 _init이다.

직전 예제를 조금 변경해 보자. Listing 6은 loadlib.h 헤더 파일이며 구현 파일을 호출하기 위해 필요한 정의를 담고 있다.


Listing 6. loadlib.h를 위한 헤더 파일
                
#ifndef  __LOADLIB_H
#define  __LOADLIB_H

#ifdef UNIX
#include <dlfcn.h>
#endif

#include <iostream>
using namespace std;

typedef void* (*funcPtr)();

#ifdef UNIX
#  define IMPORT_DIRECTIVE __attribute__((__visibility__("default")))
#  define CALL
#else
#  define IMPORT_DIRECTIVE __declspec(dllimport)
#  define CALL __stdcall
#endif

extern "C" {
  IMPORT_DIRECTIVE void* CALL LoadLibraryA(const char* sLibName);
  IMPORT_DIRECTIVE funcPtr CALL GetProcAddress(
                                    void* hModule, const char* lpProcName);
  IMPORT_DIRECTIVE bool CALL  FreeLibrary(void* hLib);
}

#endif

main 메서드는 이제 명시적으로 printHello.dll 파일을 메모리에 올려서 동일한 print 메서드를 호출한다. Listing 7을 살펴보자.


Listing 7. Loadlib.cpp를 위한 구현 파일
                

#include "loadlib.h"

int main(int argc, char* argv[])
  {
  #ifndef UNIX
    char* fileName = "hello.dll";
    void* libraryHandle = LoadLibraryA(fileName);
    if (libraryHandle == NULL)
      cout << "dll not found" << endl;
    else  // make a call to "printHello" from the hello.dll
      (GetProcAddress(libraryHandle, "printHello"))();
    FreeLibrary(libraryHandle);
#else // unix
    void (*voidfnc)();
    char* fileName = "hello.so";
    void* libraryHandle = dlopen(fileName, RTLD_LAZY);
    if (libraryHandle == NULL)
      cout << "shared object not found" << endl;
    else  // make a call to "printHello" from the hello.so
      {
      voidfnc = (void (*)())dlsym(libraryHandle, "printHello");
      (*voidfnc)();
      }
    dlclose(libraryHandle);
  #endif

  return 0;
  }

윈도우와 유닉스 환경에서 DLL 탐색 경로

윈도우 운영체제에서 DLL은 다음 순서로 탐색이 이뤄진다.

  1. 실행 파일이 있는 위치(예: 윈도우용 notepad.exe가 들어있는 경로)
  2. 현재 작업 디렉터리(예: notepad.exe를 실행한 경로)
  3. 윈도우 시스템 디렉터리(일반적으로 C:\Windows\System32)
  4. 윈도우 디렉터리(일반적으로 C:\Windows)
  5. PATH 환경 변수에 들어있는 경로

솔라리스와 같은 유닉스 시스템에서는 LD_LIBRARY_PATH 환경 변수로 공유 라이브러리 탐색 순서를 지정한다. 새로운 공유 라이브러리를 위한 경로는 LD_LIBRARY_PATH 변수에 추가되어야 한다. HP-UX 환경에서 검색 순서는 SHLIB_PATH를 먼저 점검하고 다음으로 LD_LIBRARY_PATH를 점검해 각 환경 변수에 열거된 디렉터리 목록을 찾는다. IBM AIX® 운영체제 환경에서는 LIBPATH 변수가 공유 라이브러리 탐색 순서를 결정한다.

정적 라이브러리를 윈도우에서 유닉스로 이식하기

동적 링크 라이브리리와는 달리 정적 라이브러리를 위한 목적 코드는 응용 프로그램 컴파일 시점에서 링크가 이뤄지며 응용 프로그램 일부가 된다. 유닉스 시스템에서 정적 라이브러리는 라이브러리 이름에 lib 접두어가 앞에 붙고 .a 확장자가 뒤에 붙는 이름 관례를 따른다. 예를 들어, 윈도우 시스템에서 user.lib라고 이름을 붙인 파일은 유닉스 시스템에서 일반적으로 libuser.a라고 이름을 붙인다. 정적 라이브러리를 생성하기 위해 운영체제가 제공하는 ar이나 ranlib 같은 명령어를 사용한다. Listing 8은 user_sqrt1.cpp와 user_log1.cpp 원시 파일에서 libuser.a라는 정적 라이브리러를 생성하는 방법을 예시한다.


Listing 8. 유닉스 환경에서 정적 라이브러리 만들기
                
g++ -o user_sqrt1.o -c user_sqrt1.cpp
g++ -o user_log1.o -c user_log1.cpp
ar rc libuser.a user_sqrt1.o user_log1.o
ranlib libuser.a

ar 도구는 정적 라이브러리인 libuser.a를 만들고 user_sqrt1.o와 user_log1.o 목적 파일을 여기에 복사한다. 라이브러리 파일이 이미 존재할 경우 목적 파일은 라이브러리 파일에 추가된다. 추가 대상 목적 파일이 라이브러리 내부에 이미 존재할 경우에는 옛날 목적 파일을 새 목적 파일로 덮어쓴다. r 플래그는 동일한 목적 파일이 존재할 경우 새 버전으로 예전 버전을 대체한다. 아직 라이브러리가 존재하지 않을 경우 c 옵션을 붙여 라이브러리를 만들도록 한다.

새로운 라이브러리가 생성되거나 기존 라이브러리가 변경될 경우 라이브러리 내용 색인이 추가된다. 색인은 재배치 가능한 목적 파일인 라이브러리 멤버가 정의한 각 심볼을 열거한다. 색인은 정적 라이브러리 링크 속력을 높여주며 호출되는 라이브러리 루틴을 라이브러리 내부의 실제 위치에서 독립하도록 분리시킨다. GNU ranlibar 도구에 붙어 있는 확장이며, s 옵션을 붙여 ar을 수행하면(ar -s) ranlib 호출과 똑같은 효과를 얻는다.

미리 컴파일된 헤더

비주얼 스튜디오에서 C/C++ 기반 응용 프로그램은 종종 미리 컴파일된 헤더를 사용한다. 미리 컴파일된 헤더는 비주얼 스튜디오에서 cl.exe와 같은 특정 컴파일러가 컴파일 속력을 높이기 위해 사용하는 성능 향상 기능이다. 복잡한 응용 프로그램은 종종 헤더(.h나 .hpp) 파일을 사용하는데, 하나 이상 원시 파일의 일부로 인클루드되는 코드 조각을 의미한다. 헤더 파일은 프로젝트 수행 기간 동안 변경이 드물게 일어난다. 따라서 컴파일 속력을 높이려면, 이런 헤더 파일을 컴파일러가 이해하기 쉬운 중간 형식으로 변환해서 이어지는 컴파일 속력을 높이도록 만든다. 중간 형식은 비주얼 스튜디오 환경에서 미리 컴파일된 헤더 파일이나 PCH로 불린다.

앞서 Listing 1Listing 2에서 소개한 hello.cpp를 다시 한번 생각해보자. iostream 인클루드와 EXPORT_API 매크로 정의는 프로젝트 수행 기간 동안 코드가 바뀌지 않을 파일 내용으로 취급할 수 있다. 따라서 헤더 파일 포함은 미리 컴파일된 헤더를 위한 훌륭한 후보가 된다. Listing 9는 미리 컴파일된 헤더를 사용하기 위해 적절한 변경을 가한 코드다.


Listing 9. precomp.h 내용
                
#ifndef __PRECOMP_H
#define __PRECOMP_H

#include <iostream>

#  if defined (__GNUC__) && defined(__unix__)
#    define EXPORT_API __attribute__((__visibility__("default")))
#  elif defined WIN32
#    define EXPORT_API __declspec(dllexport)
#  endif

Listing 10은 적절하게 변경한 DLL 원시 코드다.


Listing 10. 새로운 hello.cpp 파일 내용
                
#include "precomp.h"
#pragma hdrstop

extern "C" EXPORT_API void printHello()
  {
  std::cout << "hello Windows/UNIX users" << std::endl;
  }

이름이 의미하는 바와 같이 미리 컴파일된 헤더 파일은 헤더 중단점에 앞서 인클루드되는 컴파일된 형식으로 만든 목적 파일을 포함한다. 원시 파일에서 중단점은 일반적으로 선행처리기가 처리하지 않는, 즉 선행처리 지시자가 아님을 의미하는, 어휘 항목으로 표시된다. 대안으로 이런 헤더 중단점을 #pragma hdrstop으로 지시할 수도 있다. 이 지시자는 컴파일러에 특정한 행동을 하도록 지시하는 특수한 선행처리 구문으로, #pragma 이후 내용은 선행처리 언어에 포함되지 않는다.

솔라리스에서 빌드할 경우 미리 컴파일된 헤더 기능은 컴파일 과정에서 #include를 찾는다. 인클루드된 파일을 탐색할 때 컴파일러는 해당 디렉터리에서 인클루드 파일을 찾기 앞서 각 디렉터리에서 미리 컴파일된 헤더를 찾아본다. 찾는 이름은 #include로 명시된 이름 뒤에 .gch 확장자가 덧붙은 형식이다. 미리 컴파일된 헤더 파일 기능을 사용하지 않는다면 이 파일은 무시된다.

윈도우에서 사용하는 미리 컴파일된 헤더 기능을 활성화하는 명령 예는 다음과 같다.

cl /Yc precomp.h hello.cpp /DWIN32 /LD

/Yc는 precomp.h에서 미리 컴파일된 헤더를 생성하라고 cl.exe 컴파일러에 지시한다. 솔라리스에서 동일한 기능을 수행하려면 다음과 같이 명령을 내린다.

g++ precomp.h
g++ -fPIC -G hello.cpp -o hello.so

첫 명령은 precomp.h.gch라는 미리 컴파일된 헤더를 생성한다. 나머지 절차는 앞서 설명한 방법과 동일하게 공유 목적 파일을 생성한다.

주의: g++에서 미리 컴파일된 헤더 파일 지원은 버전 3.4 이상에서 가능하다.

결론

윈도우와 유닉스처럼 완전히 다른 두 시스템 사이에서 오가는 이식 작업은 결코 쉽지 않으며, 수많은 미세 조정과 인내가 필요하다. 이번 기사에서는 비주얼 스튜디오 환경에서 사용하는 가장 기본적인 프로젝트 유형을 g++/솔라리스 기반으로 이식하는 핵심을 설명했다. 두 번째로 이어지는 기사에서는 비주얼 스튜디오 환경에서 사용 가능한 다양한 옵션, g++가 제공하는 등가 옵션, g++ 속성 메커니즘, (일반적으로 윈도우 환경인) 32비트에서 (유닉스 환경인) 64비트로 이식하는 과정에서 발생하는 몇 가지 문제점, 멀티스레드를 다룬다.


<출처 : http://www.ibm.com/developerworks/kr/library/au-porting/index.html>

'Programming' 카테고리의 다른 글

코드 검색 사이트  (0) 2008.11.08
윈도우에서 XSLT 사용시 주의할점  (0) 2008.10.28
윈도우 압축파일 폴더로 보기 기능 해제(탐색기 속도 향상)  (0) 2008.10.09
한글 NLP 연구 연황  (0) 2008.09.29
WinCVS  (0) 2008.09.04