리눅스 포팅의 기본

@codemaru · February 04, 2013 · 15 min read

예나 지금이나 리눅스의 백미는 여자 연예인 배위에서 코딩하는 그 맛~ 보고있나 Visual Studio !!!

리눅스를 처음 본 게 아마 95년인가 96년인가 그랬던 것 같다. 당시 커널이 1.2였던 시절이었다. 디스켓 2-30장을 넣어가면서 설치하던 시절은 아니었고 CD를 통해서 설치하던 시절이었다. 물론 그렇다고 지금만큼 편한 시절은 아니었다. 온갖 설정들을 다 해줘야 뭐라도 동작하던 시절이었다. 수많은 밤을 지새면서 X-Window를 처음 띄우고는 그 단순하다 못해 허무한 X 아이콘에 눈물을 흘리기도 했었고, 구형 사블 호환 카드에서 소리가 나오는 것을 보고는 탄성을 지르기도 했었다. 그 당시만 해도 리눅스를 쓴다고 하면 아무도 모를 뿐더러 진정한 괴짜 취급을 받던 시절이었다.

내가 바로 국내 리눅스 전도사. 알짜 리눅스!!!

그렇게 세월이 흘렀고 1997년이 도래했다. 이때가 국내 리눅스의 일대 부흥기라 할 수 있다. 알짜 리눅스를 필두로 여기저기 배포판 만드는 사람들이 늘어났다. 마소나 프세같은 잡지에도 종종 리눅스 특집 기사가 실리곤 했었다. 상황이 그렇게 바뀌자 어디가서 리눅스 쓴다고 하면 고수 대접을 받는 웃지 못할 일도 있었다. 그러다 대학교를 들어가면서 부터는 아예 리눅스를 쓰지 않았다. 그때부터 소프트웨어는 너무 구하기 쉬워졌고 더이상 불편한 리눅스를 사용할 필요가 없었기 때문이었다.

그리고 또 결정적으로 리눅스계를 떠나 사건이 있었다. 병특 자리를 구하던 중에 학교 내에 있는 리눅스 벤처에서 알바를 했었는데, 거기서 내 평생내 정말 쿨~하게 돈을 떼이는 경험을 했기 때문이었다. 그 사장은 어처구니 없게도 리누스 토발즈도 이런걸 만들어서 공짜로 공개하는데 니까짓게 뭐한다고 돈을 받아야 하느냐는 토나오게 말도 안 되는 마인드의 소유자였다. 여튼 그래서 윈도우로 옮겨탔는데 그때나 지금이 그 선택에 후회는 없다.

그렇게 나의 기억속에서 영원히 잊혀질 것 같았던 리눅스가 최근에 다시 등장했다. 리눅스를 사용하는 게임 서버 프로그램들을 마주하면서 XIGNCODE 코드를 리눅스로 포팅할 필요가 있었기 때문에다. 사실 뭐 포팅 작업 자체는 크게 힘들지 않았는데 엉뚱한 부분들에서 엄청 헤맸다. 아주 사소한 타입 차이를 간과했다거나 UCS2/4의 변환 문제 같은 것들이었다. 포팅을 하고나서 이런 것들을 미리 알았더라면 하는 몇 가지를 여기에다 끄적여 본다.

#0

우선 여러분이 제법 큰 소프트웨어를 만들고 있었다면 당연히 여러분만의 타입시스템을 가지고 있어야 한다. 소프트웨어의 모든 부분을 관통하는 타입시스템이 없다면 좀 곤혹을 치를 수 있다. 그러니 무엇을 만들던지 규모가 좀 있다면 사전에 반드시 먼저 타입시스템을 설계하도로 하자. 뭐 타입시스템 같은건 모르겠다 싶으면 stdint.h같은 호환성 있는 타입시스템을 사용하는 것이 여러분의 포팅을 수월하게 해 줄 것이다. win32 타입시스템이라도 썼다면 그걸 기준으로 리눅스용 헤더를 작성하면 된다. 그런데 그 조차도 없이 진짜 네이티브 타입시스템(int, char, long 따위)을 썼다면 흠~ 생각 좀 해보자. 눈물 좀 닦고.

타입시스템을 쓰는 가장 큰 이유는 우리가 사용하는 타입의 크기가 시스템마다 다르기 때문이다. 흔한 예로 int는 16비트 시스템에서는 16비트, 32비트 시스템에서는 32비트가 된다. 따라서 시스템이 변경되면 타입의 크기가 달라지면서 많은 문제가 생길 수 밖에 없다. 전 16비트 시스템을 사용하지 않는데염, 따위의 소리를 해도 소용없다. 지금은 64비트가 있다. 너무나 당연한 이야기겠지만 리눅스와 윈도우는 서로 다른 64비트 타입 시스템을 채용하고 있다. 윈도우는 아래 표에 나오는 LLP64 시스템을, 리눅스는 LP64 시스템을 사용한다. 따라서 생각없이 long을 사용했다면 불지옥을 맛볼 수 있다.

Type           ILP64   LP64   LLP64
char              8      8       8
short            16     16      16
int              64     32      32
long             64     64      32
long long        64     64      64
pointer          64     64      64

주저리 주저리 떠들었는데 타입시스템이 있으면 무엇을 해야 하느냐? 그걸 리눅스에 맞도록 변경해주면 된다. 여러분이 윈도우에서 typedef long mylong;과 같이 사용했다면 64비트 리눅스에서는 typedef int mylong;을 해야 한다는 이야기다.

#1

타입을 넘어가면 다음으로 우리를 괴롭히는 녀석은 유니코드다. 기본적으로 Visual C++은 UCS2를 사용한다. 안타깝지만 너무나 당연하게도 gcc는 UCS4를 사용한다. 이 말이 의미하는 바는 다음과 같다. Visual C++에서 L”Hello”를 하면 6글자를(NULL 포함) 저장하기 위해서 12바이트를 사용하지만, gcc는 UCS4를 사용하기에 24바이트를 사용한다는 의미다. 이건 기본적으로 섬세하게 코딩됐다면 큰 문제를 일으키지는 않는다. 섬세하게 코딩되지 않았다는 말은 sizeof(wchar_t) 따위를 사용하지 않고 2를 하드코딩 하는 것과 같은 것을 의미한다. 그런 기본적인 실수만 없다면 프로그램 동작 상에 발생하는 큰 문제는 없다.

하지만 진짜 문제는 바이너리 파일을 사용할 때 발생한다. 여러분이 설계한 바이너리 파일이 UCS2의 데이터를 가정하고 있고 그걸 변환없이 Visual C++에서 읽어들이면서 재미를 봤다면 조금 골치가 아파진다. 그런 부분은 전부 별도로 처리를 해야한다. 이게 생각보다 골치아푸다. 괜히 UTF-8같은 걸 사용하는 게 아니다. 어쨌든 이런 부분이 있다면 아주 섬세하게 꼼꼼하게 생각해서 변환 루틴들을 적용시켜 주어야 하겠다.

윈도우는 MultiByteToWideChar나 WideCharToMultiByte 함수를 사용해서 문자열 변환을 쉽게 할 수 있지만 당연히 리눅스에는 그런 것 따위는 없다. iconv 라이브러리를 사용하면 동일한 버전을 구현할 수 있으니 해당 라이브러리를 사용해서 변환 함수를 구현해주면 되겠다.

_wfopen 같은 함수를 찾아 헤맬지도 모르겠다. 리눅스에는 애초에 그런 종류의 함수는 없다. 따라서 해당 함수가 필요하다면 직접 구현해야 한다. 윈도우의 A/W 전략과는 반대로 gcc는 대체로 기존 함수에다 UTF-8 문자열 입력 기능을 추가해서 유니코드에 대응하는 방식을 채택하고 있다.

#2

여러분이 fopen보다는 CreateFile에 더 익숙하고, fstat보다는 GetFileAttribute에 더 익숙해서 프로그램 전반에 이런 API를 사용하는 부분이 많이 있다면 일일이 API를 사용한 부분을 변경하는 것보다는 CreateFile, GetFileAttribute와 같은 함수의 리눅스 버전을 작성하는 것이 좋다. 이런 작업을 한다면 wine(리눅스에서 윈도우를 구동시켜주는 에뮬레이터 프로젝트) 소스 코드가 큰 도움이 될 것이다. 거의 모든 API가 구현돼 있기 때문에 내부를 살펴보면 어떻게 만들어야 하는지 대충 감을 잡을 수 있다.

다음으로 호환성 있는 함수라고 생각하고 썼는데도 함수명이 다른 경우도 있다. fstat 같은 함수도 그렇다. gcc에서는 fstat이지만 Visual C++에서는 _fstat이다. 이런 함수들은 당연히 호출 부분마다 전처리기를 사용해서 별도로 처리하기 보다는 mystat 같은 래퍼 함수를 만들거나 매크로를 선언해서 해당 함수를 호출하도록 만들어주는 것이 좋다.

#3

문자열에 관한 함정이 또 하나 있다. 바로 포매팅 문자열이다. 윈도우에서는 printf 같은 멀티바이트 함수에서 %s는 멀티바이트 문자열을 %S는 유니코드 문자열을 나타내고, wprintf 같은 유니코드 함수에서는 %s는 유니코드 문자열을, %S는 멀티바이트 문자열을 나타낸다. 근데 gcc에는 이렇지가 않다. printf, wprintf 모두 %s는 멀티바이트, %S는 유니코드를 나타낸다. 따라서 %s, %S 따위를 쓰면 십중팔구 바보가 될 수 있다. 전처리기로 처리하는 것 또한 좋은 생각이 아니다. 가장 좋은 방법은 애초에 %s, %S를 사용하지 않는 것이다. %hs, %hc, %ls, %lc를 사용하는 습관이 도움이 된다. 함수에 관계없이 %hs, %hc는 멀티바이트를, %ls, %lc는 유니코드를 나타낸다.

#4

끝으로 가장 골치 아픈 부분인데 여러분의 코드가 어떻게든 시스템의 도움을 받는 부분들이 있을 것이다. 이 부분이 가장 크리티컬하다. 예를 들면 윈도우 프로그램이라면 별 생각없이 스레드를 사용했을 것이다. 이런 경우에 그런 부분들에 해당하는 함수를 일일이 만드는 행위나 함수 호출 부분을 별도로 코딩하는 일은 실수를 많이 만들어낸다. 자신들과 비슷한 구조를 가진 검증된 라이브러리가 있다면 그걸 사용하는 것이 훨씬 좋다. 우리 같은 경우에는 윈도우 스레드를 자체적으로 추상화 시켜서 사용하고 있었는데 구조가 boost 스레드랑 유사했다. 그래서 해당 부분을 boost 스레드로 교체함으로써 플랫폼 중립적인 코드로 만들 수 있었다.

또 다른 흔한 예로 GUI가 있다. GUI 관련 코드도 플랫폼에 마다 별도로 직접 만드는 것보다는 QT같은 플랫폼 중립적인 라이브러리를 사용하는 것이 훨씬 정신 건강에 좋다. 그게 포팅 시간도 줄인다.

#5

아래 두 글도 더불어 같이 읽어보도록 하자. 분명 큰 도움을 줄 것이다.

http://www.ibm.com/developerworks/aix/library/au-porting/

http://www.ibm.com/developerworks/aix/library/au-porting2/

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중