« Previous : 1 : 2 : 3 : 4 : Next »

1. make, build

요즘 소프트웨어라는 건 여러 개의 실행 파일들로 구성되고, 그 각각의 실행 파일들도 수십~수백 개에 달하는 소스 코드들로 구성된다. 이를 빌드하려면 단순 배치 파일이나 스크립트 수준으로는 감당하기 어려울 정도로 많은 옵션과 입력 파일 리스트들을 컴파일러 및 링커에다가 일일이 전해 줘야 한다. 기존 소스 코드들을 빌드하는 시나리오를 짜는 것조차도 일종의 프로그래밍처럼 된다.

그래서 이런 빌드 시나리오를 기술하는 파일을 makefile이라고 하며, 이 시나리오대로 컴파일러와 링커를 호출해서 빌드를 수행해 주는 별도의 유틸리티가 make라는 이름으로 따로 존재한다. 얘는 이전 빌드 때 만들어져 있는 obj 파일과 소스 파일과의 날짜를 비교해서 새로 바뀐 파일만 다시 컴파일 하는 정도의 지능도 갖추고 있다.
그리고 이름이 저렇게 고정 불변이며, 한 디렉터리에 하나씩만 존재하는 것으로 여겨진다. 프로젝트는 디렉터리별로 독립적이므로..

그런데 소스 말고 헤더 파일은? 조금 어렵다. 이게 수정되면 역으로 얘를 인클루드 하는 소스 파일들도 재컴파일이 돼야 하는데, make 유틸이 C/C++ 컴파일러나 전처리기는 아닌지라, 그걸 자동으로 파악하지는 못한다. 이건 makefile 스크립트 내부에서 각 소스별 헤더 파일 의존성을 사람이 수동으로 지정해 줘야 한다. 이를 기술하는 문법이 따로 있다.
이건 매번 풀 빌드 명령을 내리는 것보다 분명 편리하지만 그래도 사람이 의존성을 잘못 지정할 경우 빌드가 꼬일 수 있는 잠재적 위험 요인이다.

이렇듯 C/C++ 공부 좀 해서 본격적인 프로그램을 개발하거나 기존 제품을 유지 보수하려면, 언어 자체 말고도 다른 툴이나 스크립트를 알아야 할 것이 이것저것 생긴다. 이 바닥도 체계가 정말 복잡하기 때문에, 잘 모르는 사람은 말 그대로 소스까지 다 차려 놓은 오픈소스 프로젝트를 멀쩡히 받아 놓고도 빌드를 못 해서 돌려보지 못하곤 한다. 최소한 Visual C++ 솔루션 파일 하나 달랑 열어 놓고 F7만 누르면 바로 짠~ 빌드 되는 물건은 아니기 때문이다.

물론 그런 복잡한 시스템들은 훨씬 더 복잡한 상황을 간편하게 제어하고 관리하고 프로세스를 자동화하기 위해 도입되었겠지만.. 그마저도 초보 입문자에게는 쉬운 개념이 아니다.
Visual Studio 같은 개발툴들이 그런 make 절차를 얼마나 단순화시키고 프로그램 개발을 수월하게 만들어 줬는지 짐작이 된다. 당장 include 의존성을 자동으로 파악하는 것만 해도 말이다.

이런 개발툴 덕분에 프로그래머가 makefile 스크립트를 일일이 건드려야 할 일이 없어졌다. makefile은 해당 개발툴이 읽고 쓰는 프로젝트 파일로 대체됐으며, 얘는 비록 텍스트 포맷이긴 하지만 사람이 수동으로 편집해야 할 일은 거의 없다. 한때는 포맷이 제각각이었는데 요즘은 xcode건 비주얼이건.. 껍데기는 XML 형태인 것이 대세가 됐다. 스크립트라기보다는 설정 데이터 파일에 더 가까워진 셈이다.

Visual C++도 지금 같은 번듯한 IDE가 갖춰진 버전은 적어도 1995년의 4.0이다. 그때의 IDE 이름은 Developer Studio이었다. 이 시절에는 얘도 IDE와 별개로 유닉스 유틸과 비슷한 스타일의 make를 따로 갖추고 있었으며, 프로젝트 파일로부터 make 스크립트를 export해 주는 기능도 갖추고 있었다. 그러나 그 기능은 후대의 버전에서 곧 없어졌다. 명령 프롬프트로 빌드를 하는 건 그냥 IDE 실행 파일의 기능으로 흡수되었다.

2. cmake

유명한 대규모 크로스 플랫폼 오픈소스 프로젝트를 받아 보면 분명 Windows를 지원하고 Visual C++로 빌드도 가능하다고 명시돼 있는데, 그 빌드라는 게 내가 생각하고 이해 가능한 방식으로 행해지는 건 아닌 경우가 있다.
한때 직장에서 이미지 처리와 인식 때문에 OpenCV며 Tesseract며 머신러닝 라이브러리까지 C/C++에서 돌리겠답시고 삽질을 좀 한 적이 있었는데.. 이때 이런 식으로 지금까지 듣도 보도 못했던 프로젝트 구조와 빌드 방식 때문에 식겁을 하곤 했다.

압축을 풀거나 git으로 생성된 저장소를 아무리 들여다봐도 sln, vcxproj 같은 파일은 보이지 않는다. 먼저 MinGW에다 cmake 같은 유닉스 냄새가 풍기는 런타임을 설치해야 한다. 그래서 cmake를 돌리고 나면 자기 혼자 무슨 라이브러리 같은 걸 한참을 받더니 그제서야 디렉터리 한구석에 Visual C++용 솔루션과 프로젝트 파일이 생긴다.

소스를 사용자 자리에서 일일이 빌드해서 쓰는 것도 모자라서 빌드 스크립트 자체도 사용자 자리에서 즉석에서 동적 생성되는 모양이다. 흠..;
그 생성된 솔루션 파일을 Visual C++에서 열어서 빌드를 해 보면.. 비록 컴파일러는 마소 것을 쓰더라도 소스 파일이 선택되고 빌드되는 방식은 절대로 Visual C++ IDE의 통상적인 스타일대로 진행되는 게 아니다.

솔루션/클래스 view에는 아무것도 뜨는 게 없으며, 빌드되는 파일을 열어도 인텔리센스 따위 나오는 게 없다. 이 상태로 Visual C++ IDE에서 곧장 코드를 읽으면서 편집할 수 있지 않다. IDE에서는 그냥 debug/release나 win32/x64 같은 configuration을 변경하고 빌드 명령만 내릴 수 있을 뿐이다.

이런 프로젝트는 Visual Studio도 반드시 거기서 쓰라고 하는 버전만 써야 한다. 가령, 2017을 쓰라고 했으면 IDE까지 꼭 2017을 깔아야 한다. 2019에다가 컴파일러 툴킷만 2017을 설치하는 식으로는 안 통한다. 도대체 프로젝트를 어떻게 꾸며야 이런 빌드 환경이 만들어지는지 나로서는 알 길이 없다.

알고 보니 얘는 프로젝트의 Configuration type이 Utility 내지 Makefile로 잡혀 있었다. Visual C++에서 빌드되는 일반적인 프로젝트라면 저건 EXE, DLL, static library 중 하나로 지정하는 속성인데, 그런 것으로 지정돼 있지 않다.

그렇기 때문에 이 프로젝트에서 Visual Studio IDE는 그냥 명령줄을 실행해 주는 셔틀 역할밖에 안 한다. Visual C++ 컴파일러가 호출되는 것도 IDE가 원래 동작하는 방식으로 호출되는 게 아니다. 세상에 C/C++ 프로젝트를 이런 식으로 만들 수도 있다는 것을 어렴풋이 경험하게 됐다.

요컨대 cmake는 기존 make 툴의 또 상위 계층이며, 얘만으로도 기능이 굉장히 많고 덩치가 큰 프로그램이다. qt가 소스 레벨 차원에서 Windows와 리눅스와 맥을 모두 지원하는 범용 GUI 프레임워크로 유명하다면, cmake는 범용 빌드 시스템 관리자인 셈이다. qt를 기반으로 개발되는 GUI 앱의 프로젝트를 cmake 기반으로 만들면 진짜로 한 소스와 한 프로젝트로 Visual C++과 xcode와.. 음 리눅스용 IDE는 뭔지 모르겠지만 아무튼 진정한 크로스플랫폼 프로그램을 개발하고 관리할 수 있을 것으로 보인다.

맥OS야 요즘은 다 유닉스 스타일의 터미널을 갖추고 있으니 빌드 내지 패키지 관리 툴이 Windows보다는 이질감이 덜하다. 그러나 맥도 리눅스와 완전히 동일하게 호환되는 건 아니라는 건 감안할 필요가 있다.
그나저나 같은 x64 환경이면 GUI 말고 a.out급의 명령 프롬프트 실행 파일은 리눅스와 맥이 바이너리 차원에서 호환되나?? 아마 그렇지는 않지 싶다.

3. Source Insight

Source Insight라고 프로그래밍 및 소프트웨어 개발로 먹고 사는 사람이라면 다들 알 만한 유명한 개발툴이 있다. 단순 텍스트 에디터보다는 코드 구조 분석과 심벌 조회 기능이 훨씬 더 정교하게 갖춰져 있지만, 그렇다고 Visual Studio 같은 급으로 특정 플랫폼용 컴파일러나 디버거와 밀접하게 연결돼 있는 IDE도 아니다. 위상이 둘의 중간쯤에 속하는 독특한 물건이다.

즉, Source Insight는 각종 언어들 컴파일러의 ‘프런트 엔드’ 계층에만 특화돼 있다.
얘가 굉장히 독특한 점이 뭐냐 하면.. 전문 IDE와 달리, 실제 컴파일 결과에 꼭 연연하지 않고 유도리가 있다는 점이다. 그래서 코드에 컴파일 에러가 좀 있더라도 괜찮고, 심지어 #if #else로 갈라지는 부분까지 개의치 않고 특정 심벌이 정의된 부분을 몽땅 한꺼번에 조회 가능하다.

그래서 프로젝트와 configuration이라는 걸 꼭 바이너리를 빌드하는 단위로 만들 필요 없이, 전적으로 사용자가 심벌을 조회하고 코드를 분석하고 싶은 큼직한 단위로 만들 수 있다. 생각해 보니 이게 Source Insight의 강점이다.
Visual Studio나 Android Studio 같은 IDE만 쓰면 되지 이런 게 왜 필요하냐고..?? 응, 필요하고 유용하더라. 틈새시장을 잘 공략한 제품 같다.

그나저나 최근에 회사 업무 때문에 SI 3.5 버전을 쓸 일이 있었는데.. 본인은 또 한 번 굉장히 놀랐다.
2019년 11월에 릴리스 됐다는 프로그램이 알고 보니 구닥다리 노인학대의 종결자인 무려 Visual C++ 6으로 빌드돼 있었기 때문이다.;; ㅠㅠㅠㅠ 실행 파일 헤더에 기록돼 있는 링커 버전, 섹션간의 4KB 단위 패딩(옛날 스타일), 생성돼 있는 기계어 코드의 패턴으로 볼 때 확실하다.

게다가 유니코드 기반도 아니었다. 도움말을 보니 여전히 Windows 9x를 지원한다고 쓰여 있다. 요즘 같은 시대에 레거시 OS 종결자인 프로그램이 날개셋 말고 더 있었구나;;
회사에서만 쓰는 프로그램이어서 많이 다뤄 보지는 못했지만 쟤들도 자기 제품에다가 분명 최신 C++1x 문법을 구현했을 텐데, 그걸 자기들이 제품 코딩을 할 때 좀 써 보고 싶은 생각은 하지 않았을까..?? 피치 못할 사정이 있어서 VC6을 그렇게 오랫동안 써 온 건지 궁금하다.

그나마 2020년에 출시된 SI 4.0에서는 유니코드를 지원하고 많은 변화가 있었다고 한다. 거기서는 자기네 개발툴도 새 버전으로 갈아타지 않았겠나 추측해 본다.

4. Visual C++

그리고 나의 사랑하는 툴인 Visual Studio.. 얘는 2019 이후로 202x이 나오려나 모르겠다. 지난 2년 동안 꾸준히 소규모 업데이트 형태로만 버전업을 거듭한 끝에, 무려 16.9.x 버전에 진입했다.
업데이트가 너무 잦아서 좀 귀찮은 감이 있긴 했지만, IDE 자체의 안정성은 야금야금 눈에 띄게 강화되어 왔다. 그 예를 들면 다음과 같다.

  • 예전에는 컴에 절전/최대 절전을 반복하다 보면 IDE의 글꼴이 내가 변경하기 전의 것으로 되돌아가곤 했는데 그 오동작이 어느 샌가 발생하지 않게 됐다. 상당히 성가신 버그였다.
  • 가끔 대화상자 리소스 편집기를 열 때 IDE가 응답이 멎던 현상이 이제 더는 발생하지 않는다.
  • 또 가끔은 프로젝트 대렉터리 내부에 RCxxxx, *.vc.db-??? 등 임시 쓰레기 파일이 프로젝트를 정상적으로 닫은 뒤에도 지워지지 않고 남아 있었던 것 같은데.. 이제는 그런 문제가 확실히 해결됐다.

예전에도 언급한 적이 있지 싶은데, 난 Visual Studio IDE가 서로 다른 프로세스 인스턴스끼리도 연계가 더 자연스럽게 됐으면 좋겠다.

  • 다른 인스턴스에서 이미 열어 놓은 솔루션을 또 열려고 시도한다면 그냥 그 인스턴스로 이동하기
  • 다른 인스턴스에서 만들어 놓은 문서창끼리도 한 탭으로 묶거나 떼어내기 지원 (크롬 브라우저처럼)

그리고...

  • BOM이 없는 파일의 인코딩, 또는 새 파일을 첫 저장할 때의 기본 인코딩을 utf-8로 인식해 줬으면 좋겠다.
  • 탭이 설정된 대로뿐만 아니라, 주변 파일의 모양을 보고 탭인지 공백 네 칸인지 얼추 분위기를 파악해서 동작하는 기능이 있으면 좋겠다.
  • 프로젝트별로 소스 파일 곳곳에 지정된 책갈피와 breakpoints들의 세트들을 여럿 한꺼번에 저장하고 불러오는 기능이 있으면 좋겠다. 디버그를 위해 실행할 프로그램과 인자도 여러 개 한꺼번에 관리하고 말이다.

끝으로.. Visual C++은 2015부터가 Windows 10과 타임라인을 공유한다. 이때 CRT 라이브러리의 구성 형태가 크게 바뀌었다. vcruntime이 어떻고 ucrtbase가 어떻고.. 그리고 Visual Studio 2015~2019는 재배포 패키지도 한데 통합됐다.

그래서 그런지 요즘은 Visual C++이 설치되어 있지 않아도 시스템 디렉터리를 가 보면 msvcp140, mfc140 같은 DLL은 이미 들어있다.
20여 년 전의 msvcrt와 mfc42 이래로 운영체제의 기본 제공 DLL과 Visual C++의 런타임 DLL이 일치하는 나날이 찾아온 건지 모르겠다.

Posted by 사무엘

2021/04/03 08:34 2021/04/03 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1872

C/C++은 성능을 위해 컴파일러나 언어 런타임이 프로그래머의 편의를 위해 뭔가를 몰래 해 주는 것을 극도로 최소화한 언어이다. 꾸밈 없고 정제되지 않은 '날것 상태 raw state'에다가 고급 프로그래밍 언어의 패러다임과 문법만을 얹은 걸 추구하다 보니, '초기화되지 않은 쓰레기값'이라는 게 존재하는 거의 유일한 언어이기도 하다.

같은 프로그램이라도 이제 막 선언된 변수나 할당된 메모리 안에 무슨 값이 들어있을지는 컴퓨터 운영체제의 상태에 따라서 그때 그때 달라진다. 반드시 0일 거라는 보장은 전혀 없다. 오히려 0 초기화는 별도의 CPU 부담이 필요한 인위적인 작업이다. global/static에 속하는 메모리만이 무조건적인 0 초기화가 보장된다.

그렇다고 쓰레기값이라는 게 완벽하게 예측 불가이면서 통계적인 질서를 갖춘 난수인 것도 물론 아니다.
그리고 Visual C++에서 프로그램을 debug 세팅으로 빌드하면 쓰레기값이라는 게 크게 달라진다. C++ 개발자라면 이 사실을 이미 경험적으로 충분히 알고 있을 것이다.

1. 갓 할당된 메모리의 기본 쓰레기값: 0xCC(스택)와 0xCD(힙)

가장 먼저, 스택에 저장되는 지역변수들은 자신을 구성하는 바이트들이 0xCC로 초기화된다. 다시 말해 디버그 빌드에서는 int x,y라고만 쓴 변수는 int x=0xCCCCCCCC, y=0xCCCCCCCC와 얼추 같게 처리된다는 것이다. 디스어셈블리를 보면 의도적으로 0xCC를 대입하는 인스트럭션이 삽입돼 들어간다.

0은 너무 인위적이고 유의미하고 깔끔한(?) 값 그 자체이고, 그 근처에 있는 1, 2나 0xFF도 자주 쓰이는 편이다. 그에 비해 0xCC는 형태가 단순하면서도 현실에서 일부러 쓰일 확률이 매우 낮은 값이다. 그렇기 때문에 여기는 초기화되지 않은 쓰레기 영역이라는 것을 시각적으로 곧장 드러내 준다.

int a[10], x; a[x]=100; 같은 문장도 x가 0으로 깔끔하게 자동 초기화됐다면 그냥 넘어가지만, 기괴한 쓰레기값이라면 곧장 에러를 발생시킬 것이다.
또한, 복잡한 클래스의 생성자에서 값이 대입되어 초기화된 멤버와 그렇지 않은 멤버가 뒤섞여 있을 때, 0xCC 같은 magic number는 탁월한 변별력을 발휘한다.

타겟이 align을 꼼꼼하게 따지는 아키텍처라면, 쓰레기값을 0x99나 0xDD 같은 홀수로 지정하는 것만으로도 초기화되지 않은 포인터 실수를 잡아낼 수 있다. 32비트에서 포인터값의 최상위 비트가 커널/사용자 영역을 분간한다면, 최하위 비트는 align 단위를 분간할 테니 말이다. 뭐 0xCC는 짝수이다만..

0xCC라는 바이트는 x86 플랫폼에서는 int 3, 즉 breakpoint를 걸어서 디버기의 실행을 중단시키는 명령을 나타내기도 한다. 그래서 이 값은 실행 파일의 기계어 코드에서 align을 맞추기 위해 공간만 차지하는 잉여 padding 용도로 위해 듬성듬성 들어가기도 한다.
Visual C++ 6 시절엔 거기에 말 그대로 아무 일도 안 하는 nop를 나타내는 0x90이 쓰였지만 2000년대부터는 디버깅의 관점에서 좀 더 유의미한 작용을 하는 0xCC로 바뀐 듯하다. 정상적인 상황이라면 컴퓨터가 이 구역의 명령을 실행할 일이 없어야 할 테니까..

다만, 힙도 아니고 스택 지역변수의 내용이 데이터가 아닌 코드로 인식되어 실행될 일이란 현실에서 전혀에 가깝게 없을 테니, 0xCC는 딱히 x86 기계어 코드를 의식해서 정해진 값은 아닐 것으로 보인다.

Visual C++의 경우, 스택 말고 malloc이나 new로 할당하는 힙(동적) 메모리는 디버그 버전에서는 내용 전체를 0xCD로 채워서 준다. 0xCC보다 딱 1 더 크다. 아하~! 이 값도 평소에 디버그 하면서 많이 보신 적이 있을 것이다.
힙의 관리는 컴파일러 내장이 아니라 CRT 라이브러리의 관할로 넘어기도 하니, 0xCD라는 값은 라이브러리의 소스인 dbgheap.c에서도 _bCleanLandFill이라는 상수명으로 확인할 수 있다.

2. 초기화되지 않은 스택 메모리의 사용 감지

C/C++ 언어는 지역변수의 값 초기화를 필수가 아닌 선택으로 남겨 놓았다. 그러니 해당 언어의 컴파일러 및 개발툴에서는 프로그래머가 초기화되지 않은 변수값을 사용하는 것을 방지하기 위해, 디버그용 빌드 한정으로라도 여러 안전장치들을 마련해 놓았다.
그걸 방지하지 않고 '방치'하면 같은 프로그램의 실행 결과가 debug/release별로 달라지는 것을 포함해 온갖 골치아픈 문제들이 발생해서 프로그래머를 괴롭히게 되기 때문이다.

int x; printf("%d", x);

이런 무식한 짓을 대놓고 하면 30년 전의 도스용 Turbo C에서도 컴파일러가 경고를 띄워 줬다. Visual C++의 에러 코드는 C4700. 그랬는데 한 Visual C++ 2010쯤부터는 이게 경고가 아니라 에러로 바뀌었다.
그리고 그 뿐만이 아니다.

int x;
if( some_condition_met(...)) x=0;
printf("%d", x);

이렇게 문장을 약간만 꼬아 놓으면 초기화를 전혀 안 하는 건 아니기 때문에 컴파일 과정에서의 C4700을 회피할 수 있다. 하지만 보다시피 if문의 조건이 충족되지 않으면 x가 여전히 초기화되지 않은 채 쓰일 수 있다. 이건 정적 분석 정도는 돌려야 감지 가능하다.
(그런데, 글쎄.. 함수의 리턴이 저런 식으로 조건부로 불완전하게 돼 있으면 컴파일만으로도 C4715 not all control paths return value 경고가 뜰 텐데.. 비초기화 변수 접근 체크는 그 정도로 꼼꼼하지 않은가 보다.)

Visual C++은 /RTC라고 디버그용 빌드에다가 run-time check라는 간단한 검사 기능을 추가하는 옵션을 제공한다. 함수 실행이 끝날 때 스택 프레임 주변을 점검해서 버퍼 오버런을 감지하는 /RTCs, 그리고 지역변수를 초기화하지 않고 사용한 것을 감지하는 /RTCu.

사용자 삽입 이미지

저 코드를 Visual C++에서 디버그 모드로 빌드해서 실행해서 if문이 충족되지 않으면 run-time check failure가 발생해서 프로그램이 정지한다. 다만, 이 메모리는 초기화만 되지 않았을 뿐 접근에 법적으로 아무 문제가 없는 스택 메모리이다. 할당되지 않은 메모리에 접근해서 access violation이 난 게 아니다. 심각한 시스템/물리적인 오류가 아니라 그저 의미· 논리적인 오류이며, 쓰기를 먼저 하지 않은 메모리에다가 읽기를 시도한 게 문제일 뿐이다.

그러니 이 버그는 해당 메모리 자체에다가 시스템 차원의 특수한 표식을 해서 잡아낸 게 아니며, 논리적으로 매우 허술하다. (0xCC이기만 하면 무조건 스톱.. 이럴 수도 없는 노릇이고!)
문제의 코드에 대한 디스어셈블리를 보면 if문이 만족되지 않으면 printf으로 가지 않고 그냥 곧장 RTC failure 핸들러를 실행하게 돼 있다.

void do_nothing(int& x) {}

int x; do_nothing(x); printf("%d", x);

그렇기 때문에 요렇게만 해 줘도 RTC를 회피하고 x의 쓰레기값을 얻는 게 가능하다. 글쎄, 정교한 정적 분석은 이것도 지적해 줄 수 있겠지만, 포인터가 등장하는 순간부터 메모리 난이도와 복잡도는 그냥 하늘로 치솟는다고 봐야 할 것이다.

하물며 처음부터 포인터로만 접근하는 힙 메모리는 RTC고 뭐고 아무 안전 장치가 없다. int *p에다가 new건 malloc이건 값이 하나 들어간 것만으로도 초기화가 된 것이거늘, 그 주소가 가리키는 p[0], p[1] 따위에 쓰레기값(0xCD)이 있건 0이 있건 알 게 무엇이겠는가????

나도 지금까지 혼동하고 있었는데, 이런 run-time check failure는 run-time error와는 다른 개념이다. 순수 가상 함수 호출 같은 건 C/C++에 존재하는 얼마 안 되는 run-time error의 일종이고 release 빌드에도 포함돼 들어간다. 하지만 RTC는 debug 빌드 전용 검사이다.

그러니 버퍼 오버런을 감지하는 보안 옵션이 /RTC만으로는 충분하지 않고 /GS가 따로 있는 것이지 싶다. /GS는 release 빌드에도 포함돼 있으며, 마소에서는 보안을 위해 모든 프로그램들이 이 옵션을 사용하여 빌드할 것을 권하고 있다.

3. 해제된 힙 메모리: 0xDD(CRT)와 0xFEEE(???)

일반적인 프로그래머라면 동적으로 할당받은 힙 메모리를 free로 해제했을 때, 거기를 가리키는 메모리 영역이 실제로 어떻게 바뀌는지에 대해 생각을 별로 하지 않는다. 사실, 할 필요가 없는 게 정상이기도 하다.
우리 프로그램은 free를 해 준 주소는 신속하게 영원히 잊어버리고, 그 주소를 보관하던 포인터는 NULL로 바꿔 버리기만 하면 된다. free 해 버린 주소를 또 엿보다가는 곧바로 메모리 에러라는 천벌을 받게 될 것이다.

그런데 실제로는, 특히 디버그 모드로 빌드 후 프로그램을 디버깅 중일 때는 free를 한 뒤에도 해당 메모리 주소가 가리키는 값을 여전히 들여다볼 수 있다. 들여다볼 수 있다는 말은 *ptr을 했을 때 access violation이 발생하지 않고 값이 나온다는 것을 의미한다.
이 공간은 나중에 새로운 메모리 할당을 위해 재사용될 수야 있다. 하지만 사용자가 디버깅의 편의를 위해 원한다면 옵션을 바꿔서 재사용되지 않게 할 수도 있다. (_CrtSetDbgFlag(_CRTDBG_DELAY_FREE_MEM_DF) 호출)

뭐, 메모리를 당장 해제하지 않는다고 해서 free 하기 전의 메모리의 원래 값까지 그대로 남아 있지는 않는다. Visual C++의 디버그용 free/delete 함수는 그 메모리 블록의 값을 일부러 0xDD (_bDeadLandFill)로 몽땅 채워 넣는다. 여기는 할당되었다가 해제된 영역임을 이런 식으로 알린다는 것이다.

실제로, free된 메모리가 곧장 흔적도 없이 사라져서 애초에 존재하지도 않았던 것처럼 접근 불가 ?? 로 표시되는 것보다는 0xDD라고 디버거의 메모리 창에 뜨는 게 dangling pointer 디버깅에 약간이나마 더 도움이 될 것이다. 이 포인터가 처음부터 그냥 쓰레기값을 가리키고 있었는지, 아니면 원래는 valid하다가 지칭 대상이 해제되어 버린 것인지를 분간할 수 있으니 말이다.

그런데 본인은 여기서 개인적으로 의문이 들었다.
본인은 지난 20여 년에 달하는 Visual C++ 프로그래밍과 메모리 문제 디버깅 경험을 떠올려 봐도.. 갓 할당된 쓰레기값인 0xCC와 0xCD에 비해, 0xDD를 본 적은 전혀 없는 건 아니지만 매우 드물었다.

dangling pointer가 가리키는 메모리의 값은 0xD?보다는 0xF?였던 적이 훨씬 더 많았다. 더 구체적으로는 2바이트 간격으로 0xFEEE (0xEE, 0xFE)이다.

사용자 삽입 이미지

인터넷 검색을 해 보니.. 이건 놀랍게도 CRT 라이브러리가 채워 넣는 값이 아니었다. free/delete가 궁극적으로 호출하는 Windows API 함수인 HeapFree가 메모리를 정리하면서 영역을 저렇게 바꿔 놓았었다. 더구나 CRT에서 0xDD로 먼저 채워 넣었던 영역을 또 덮어쓴 것이다.
이 동작에 대해서 놀라운 점은 저게 전부가 아니다.

(1) 0xFEEE 채우기는 프로그램을 Visual C++ 디버거를 붙여서(F5) 실행했을 때만 발생한다. debug 빌드라도 디버거를 붙이지 않고 그냥 Ctrl+F5로 실행하면 0xFEEE가 생기지 않는다. 그리고 release 빌드라도 디버거를 붙여서 실행하면 0xFEEE를 볼 수 있다.

(2) 더 놀라운 점은.. 내가 집과 직장 컴퓨터를 통틀어서 확인한 바로는 저 현상을 볼 수 있는 건 Visual C++ 2013 정도까지이다. 2015부터는 debug 빌드를 디버거로 붙여서 돌리더라도 0xFEEE 채움이 발생하지 않고 곧이곧대로 0xDD만 나타난다~!

운영체제가 정확하게 어떤 조건 하에서 0xFEEE를 채워 주는지 모르겠다. 인터넷 검색을 해 봐도 정확한 정보가 나오는 게 의외로 없다.
하필 Visual C++ 2015부터 저런다는 것은 CRT 라이브러리가 Universal CRT니 VCRuntime이니 하면서 구조가 크게 개편된 것과 관계가 있지 않으려나 막연히 추측만 해 볼 뿐이다.

여담이지만 HeapAlloc, GlobalAlloc, LocalAlloc은 연달아 호출했을 때 돌아오는 주소의 영역이 그리 큰 차이가 나지 않으며, 내부 동작 방식이 모두 비슷해진 것 같다. 물론 뒤의 global/local은 fixed 메모리 할당 기준으로 말이다.

4. 힙 메모리 영역 경계 표시용: 0xFD와 0xBD

0xCD, 0xDD, (0xFEEE) 말고 heap 메모리 주변에서 볼 수 있는 디버그 빌드용 magic number 바이트로는 0xFD _bNoMansLandFill와 0xBD _bAlignLandFill가 더 있다.

얘들은 사용자가 요청한 메모리.. 즉, 0xCD로 채워지는 그 메모리의 앞과 뒤에 추가로 고정된 크기만큼 채워진다. Visual C++ CRT 소스를 보면 크기가 NoMansLandSize인데, 값은 4바이트이다. 사용자가 요청한 메모리 크기에 비례해서 채워지는 0xCD와 0xDD에 비하면 노출 빈도가 아주 작은 셈이다. 특히 0xBD는 0xFD보다도 더욱 듣보잡인 듯..

애초에 얘는 사용자가 건드릴 수 있거나 건드렸던 공간이 아니며 그 반대이다. 사용자는 0xCD로 채워진 공간에다가만 값을 집어넣어야지, 앞뒤  경계를 나타내는 0xFD를 건드려서는 안 된다.
CRT 라이브러리의 디버그용 free/delete 함수는.. 힙을 해제할 때 이 0xFD로 표시해 놨던 영역이 값이 바뀌어 있으면 곧장 에러를 출력하게 돼 있다.

그리고 예전에 메모리를 해제해서 몽땅 0xDD로 채워 놨던 영역도 변조된 게 감지되면 _CrtCheckMemory 같은 디버깅 함수에서 곧장 에러를 찍어 준다. 그러니 0xDD, 0xFD, 0xBD는 모두 오류 검출이라는 용도가 있는 셈이다. 0xCC와 0xCD 같은 쓰레기값 영역은 쓰지도 않고 곧장 읽어들이는 게 문제이지만, 나머지 magic number들은 건드리는 것 자체가 문제이다.

그리고 얘들은 heap 메모리를 대상으로 행해지는 점검 작업이다. 이런 것 말고 스택 프레임에다가 특정 magic number를 둬서 지역변수 배열의 overflow나 복귀 주소 변조를 감지하는 것은 별도의 컴파일러 옵션을 통해 지원되는 기능이다. 요것들은 힙 디버그 기능과는 별개이며, 보안 강화를 위해 release 빌드에도 포함되는 게 요즘 추세이다.

이상이다.
파일 포맷 식별자 말고 메모리에도 디버깅을 수월하게 하기 위해 쓰레기값을 가장한 이런 특수한 magic number들이 쓰인다는 게 흥미롭다. Windows의 Visual C++ 외의 다른 개발 환경에서는 디버깅을 위해 어떤 convention이 존재하는지 궁금해진다.

사실, 16진수 표기용인 A~F에도 모음이 2개나 포함돼 있고 생각보다 다양한 영단어를 표현할 수 있다. 거기에다 0을 편의상 O로 전용하면 모음이 3개나 되며, DEAD, FOOD, BAD, FADE, C0DE 정도는 거뜬히 만들어 낸다. 거기에다 FEE, FACE, FEED, BEEF 같은 단어도.. 유의미한 magic number나 signature를 고안하는 창의력을 발산하는 데 쓰일 수 있다.
그러고 보니 아까 0xFEEE도 원래 free를 의도했는데 16진수 digit에 R은 없다 보니 불가피하게 0xFEEE로 대충 때운 건지 모르겠다.

Posted by 사무엘

2021/02/11 08:36 2021/02/11 08:36
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1853

메모리 leak 사냥 후기

본인은 얼마 전엔 생계를 위해 덩치 좀 있고 스레드도 여럿 사용하는 C++ 프로젝트에서 골치 아픈 메모리 leak 버그만 잡으면서 꼬박 두 주를 보낸 적이 있었다.
요즘 세상에 raw 포인터를 직접 다루면서 동적 메모리를 몽땅 수동으로 직접 관리해야 한다니, C/C++은 자동차 운전으로 치면 수동 변속기와 잘 대응하는 언어 같다.

의외로... Visual C++이 2012 무렵부터 제공하기 시작한 정적 분석 도구는 memory leak을 잡아 주지는 않았다. 제일 최신인 2019 버전도 마찬가지이다.
얘가 잡아 주는 건 잠재적으로 NULL 포인터나 초기화되지 않은 변수값을 사용할 수 있는 것, 무한 루프에 빠질 수 있는 것 따위이다. 개중에는 너무 자질구레하거나 심지어 false alarm으로 여겨지는 것도 있다. 하지만..

char *p;
p = new char[256];
p = (char *)malloc(100);
p = NULL;
*p = 32;

이렇게 코드를 짜면 지적해 주는 건 맨 아래에 대놓고 NULL 포인터를 역참조해서 대입하는 부분뿐이다.
앞에서 new와 malloc 메모리 블록이 줄줄 새는 것은 의외로 out of 안중이더라. 개인적으로 놀랐다.

리눅스 진영에는 Valgrind라는 툴이 있긴 한데, 얘도 프로그램을 직접 실행해 주는 동적 분석이지 정적은 아니다.
다른 상업용 3rd party 정적 분석 툴 중에는 메모리 leak도 잡아내는 물건이 있을지 모른다. 하지만 그런 것 없이 Visual C++ 순정만 쓴다면 메모리 leak 디버깅은 전통적인 인간의 동적 분석에 의존해야 할 듯했다. 그래서 고고씽..

처음에는 무식하게 여기저기 들쑤시면서 삽질하면서 시간을 많이 보냈지만, 나중엔 차츰 요령이 생겼다.
먼저, 안정성이 검증돼 있는 맨 아랫단의 각종 오픈소스 라이브러리들을 의심하고 무식하게 들쑤실 필요는 없다. 물론 겉으로 드러난 결과는 거기서 할당한 메모리들이 줄줄 새는 것이다. 하지만 근본 원인은 거기보다 더 위에 있다.

그렇다고 맨 위의 애플리케이션이 오브젝트 해제를 안 했다거나 한 것도 아니었다. 그 정도로 초보적인 실수였다면 금세 감지되고 잡혔을 것이다. 더구나 App들은 아랫단과 달리 C++을 기반으로 스마트 포인터 같은 것도 그럭저럭 활용해서 작성되어 있었다. 그러니 거기도 딱히 문제는 없었다.

대부분의 문제는 오픈소스를 우리 쪽에서 살짝 수정한 부분, 오픈소스로부터 호출되는 우리 쪽 콜백 함수, 그리고 우리가 작성한 중간 계층의 공유 라이브러리에서 발견되었다.
이 코드를 처음으로 작성한 전임자가 누구인지는 모르겠지만.. C++ 코딩을 너무 Java 코딩하는 기분으로 했다는 생각이 강하게 들었다.

std::string s = _strdup("ABCD");

이런 식으로만 해 놓고 그냥 넘어간다거나.. (저기요, R-value는 어떡하고..??)
함수 뒷부분에서 나름 메모리를 해제한답시고 p = NULL을 쓴 것을 보니.. 전임자는 정말 Java의 정신으로 충만했다는 게 느껴졌다. (p는 물론 스마트가 아닌 일반 포인터)

메모리 leak 디버깅을 위해 C 컴파일러들은 디버깅용 메모리 관리 함수들을 제공하며, 다른 라이브러리들은 보통 자신들이 사용하는 메모리 할당 함수를 자신만의 명칭으로 바꿔서 쓴다. 그 명칭만으로 자신의 메모리 사용 내역을 추적할 수 있게 하기 위해서이다. (매크로 치환 및 해당 함수의 구현 부분 수정)

Visual C++ 기준으로, 프로그램이 처음 실행됐을 때 _CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF )를 호출하고 나면, 종료 시에 아직 해제되지 않은 heap 메모리들 목록이 쭈욱 나열된다. 메모리 할당 번호와 할당 크기, 그리고 메모리의 첫 부분 내용도 일부 같이 덤프된다.

여기서 ‘할당 번호’라는 걸 주목하시길..
만약 프로그램을 여러 번 실행하고 종료하더라도 (1) 메모리 할당 번호가 동일한 leak을 일관되게 재연 가능하다면, 그건 운이 아주 좋은 상황이다.
_CrtSetBreakAlloc을 호출해서 나중에 그 번호에 해당하는 메모리 할당 요청이 왔을 때 프로그램 실행을 중단시키면 되기 때문이다. 그러면 게임 끝이다.

하지만 복잡한 멀티스레드 프로그램에서 이렇게 매번 동일한 번호로 발생하는 착한 leak은 그리 많지 않다. 이것만으로 이 메모리의 출처를 추적하고 문제를 해결하는 건 아직 모래사장에서 바늘 찾는 짓이나 마찬가지이다. 단서가 좀 더 필요하다.

그래서 메모리를 할당할 때 이 요청은 (2) 소스 코드의 어느 지점에서 한 것이라는 정보를 같이 주게 한다.
어떻게? Visual C++ 기준 _***_dbg라는 함수를 만들어서 뒤에 소스 코드와 줄 번호 인자를 따로 받게 한다. ***에는 malloc뿐만 아니라 변종인 realloc과 calloc, 내부적으로 이런 함수를 호출하는 strdup 같은 함수도 모두 포함된다. 심지어 C++용으로는 operator new 함수도 말이다.

C의 __FILE__과 __LINE__은 그야말로 디버깅용으로 만들어진 가변 매크로 상수인 셈이다. 이렇게 말이다.

#ifdef _DEBUG
#define malloc(n)   _malloc_dbg(n, __FILE__, __LINE__)

#define new    __debug_new
#define __debug_new   new(__FILE__, __LINE__)
void *operator new(size_t n, const char *src, int lin);
void *operator new[](size_t n, const char *src, int lin);
#endif

new operator가 오버로딩 되는 건 placement new를 구현할 때와 디버깅용 메모리 할당을 할 때 정도인 것 같다.
이렇게 메모리 할당 방식을 바꿔 주면.. 나중에 leak report가 뜰 때 그 메모리 블록에 대해서 할당되었던 지점이 같이 뜬다. 무슨무슨 c/cpp의 몇째 줄이라고..

물론 그 함수가 호출된 배경을 알 수 없으니 저것도 불완전하게 느껴질 수 있다. 또한 이미 자체적으로 malloc을 다른 명칭으로 감싸고 있는 코드에 대해서는 이런 매크로 치환이 곧장 통하지 않는다는 한계도 있다.

그래도 그 정보마저 없던 것보다는 상황이 월등히 더 나아진다.
참고로, 프로그램이 실행 중일 때에도 동적 할당된 임의의 메모리에 대해서 _CrtIsMemoryBlock을 호출하면 이 메모리의 할당 번호와 출처 정보를 얻을 수 있다. 이를 토대로 leak은 얘보다 전인지 후인지, 언제 할당되었는지를 유추 가능하다(할당 번호의 대소 비교).

이것만으로도 아직 막막할 때 본인이 사용한 최후의 방법은 (3) _CrtSetAllocHook을 사용해서 메모리 할당이 발생할 때마다 콜백 함수가 호출되게 하는 것이었다.
내가 작성하지도 않은 방대한 코드에서 malloc/calloc을 전부 내 함수로 치환하는 것은 위험 부담이 매우 큰데.. 그럴 필요 없이 Visual C++ CRT의 malloc이 디버깅을 위해 사용자의 콜백 함수를 직접 호출해 준다니 고마운 일이 아닐 수 없다.

이를 위해서는 한 프로세스 내의 모든 static library 및 DLL 모듈들이 동일한 Visual C++ CRT 라이브러리를 DLL로 링크하게만 맞춰 놓으면 된다. 어느 것 하나라도 CRT의 static 링크가 있으면 일이 많이 골치 아파진다. DLL로 해야 모든 모듈들이 사용하는 메모리가 한 CRT에서 통합적으로 관리된다.

콜백 함수는 메모리 할당 번호뿐만 아니라 할당 크기, 그리고 이 메모리를 요청한 스레드가 어느 것인지도 확인 가능하다.
개인적으로는 leak 중에서 크고(수백~수천 바이트 이상) 유니크한 바이트 수를 동일하게 요청하는 것을 콜백 함수를 통해서 잡아내고, 이걸 토대로 다른 leak들도 잡아냈다.
겨우 4바이트, 8바이트 같은 너무 평범하고(?) 자주 호출되는 할당 요청은 leak만 추려내기가 곤란할 것이다.

이 콜백 함수에서 또 메모리를 동적 할당하지는 않도록 주의해야 한다. 그러면 콜백 함수에서 호출된 메모리 할당 함수가 또 콜백을 호출하고.. stack overflow 에러가 발생할 수 있다.
로그를 찍기 위해 흔히 사용하는 sprintf 부류의 함수조차도 내부적으로 메모리를 동적 할당한다.

이 문제를 회피하기 위해 우리 콜백 함수 내부에서 중복 호출 방지 guard를 둘 수도 있지만.. 간단하게 C 라이브러리 대신 Windows API가 제공하는 wsprintfA/W 함수를 사용하는 것도 괜찮은 방법이다. Windows API 중에는 C 라이브러리를 사용할 수 없는 환경에서도 C 라이브러리의 기능을 일부 사용하라면서 저런 부류의 함수를 제공하는 경우가 있다.

이상이다.
memory leak은 여느 메모리나 스레드 버그처럼 프로그램을 당장 뻗게 만들지는 않는다.
오히려 메모리 관리를 잘못해서 원래는 dangling pointer가 됐어야 할 포인터로도 메모리 접근을 가능하게 만들어 주기도 한다(해제되지 않았기 때문에).

하지만 leak은 결국 컴퓨터의 메모리 자원을 소진시키고, 한 프로그램이 반영구적으로 동일한 상태를 유지하면서 돌아가지 못하게 하는 심각한 문제이다. 더 넓게 보자면 굳이 heap 메모리 말고도, 각종 커널 핸들이나 GDI 객체처럼 나중에 반드시 닫아 줘야 하는 일체의 리소스들도 제때 해제해 주지 않을 경우 leak이 발생할 수 있는 물건들이다. 상업용 툴은 이런 것들까지 다 모니터링을 해 주지 싶다.

이 주제 관련 다른 여담들을 좀 늘어놓으며 글을 맺고자 한다.

(1) leak은 새어나가는 그 메모리의 할당이 벌어지는 상황을 추적하는 게 핵심이다. 그런데 새고 있는지의 여부는 한참 뒤에 프로그램이 종료될 때에나 알 수 있다는 것이 큰 모순이며, 관련 디버깅을 어렵게 하는 요인이다.
또한 시작과 끝이 있는 게 아니라 언제나 돌아가는 서버/서비스 같은 프로그램도 있다. 이런 건 leak을 어떻게 찾아내야 좋을까? 그렇게 오랫동안 상시 가동되는 프로그램이야말로 memory leak이 절대로 없어야 하는데, 역설적이게도 그런 유형의 프로그램이 leak을 잡기가 더욱 어렵다. 뭔가 새로운 방법론을 찾아서 적용해야 한다.

(2) 컴퓨터에서 메모리 영역이란 건 용도에 따라 코드와 데이터로 나뉘는데, 코드를 저장하는 메모리가 새는 일은.. 무슨 가상 머신 급의 고도의 시스템 소프트웨어를 개발하는 게 아닌 이상 없을 것이다.
다만, 데이터도 다 같은 데이터는 아니어서 진짜로 쌩 문자열 같은 POD인지, 아니면 내부에 포인터가 들어있는 실행 객체의 인스턴스인지에 따라 체감 난이도가 달라진다. 후자는 그 자체가 코드는 아니지만 코드에 준한다는 느낌이 든다.

(3) a( b(), c() ) 이런 구문의 실행을 디버거로 추적한다면, step into는 b()의 내부부터 먼저 들어간다. step over는 이들을 통째로 다 실행하고 다음 줄로 넘어간다.
그 둘의 중간으로.. b()와 c()처럼 인자 준비 과정에서 발생하는 함수 호출은 몽땅 생략하고 a()로만 step into 하는 명령도 좀 있으면 좋겠다.
특히 smart pointer는 함수로 넘겨줄 때마다 trivial한 생성자나 연산자 오버로딩 함수로 먼저 진입하는 것이 굉장히 번거롭다. 이런 것을 생략할 수 있으면 디버깅 능률과 생산성이 더 올라갈 수 있을 것이다.

Posted by 사무엘

2021/01/07 08:35 2021/01/07 08:35
, , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1840

오늘날 마이크로소프트는 운영체제와 오피스뿐만 아니라 개발툴 분야도 세계를 석권해 있다.
걔들은 과거에 운영체제 쪽은 맥 내지 IBM OS/2와 경쟁했었고, 오피스는 로터스, 워드퍼펙, 한컴(...)과 경쟁했으며.. 개발툴 쪽은 볼랜드라는 쟁쟁한 기업과 경쟁했다.

마소와 볼랜드가 내놓았던 프로그램 개발툴은.. 먼저

1. IDE까지 있는 도스용 대중 보급형의 브랜드가 있었다.
볼랜드는 터보, 마소는 퀵.. 뭔가 스피디한 단어를 썼다는 공통점이 있다.
그리고 볼랜드는 브랜드명-언어명 사이를 띄었지만, 마소는 둘을 붙여 썼다.;;

Turbo Basic, Turbo C, Turbo Pascal
QuickBasic, QuickC, QuickPascal

다음은 볼랜드 말고 '마소'에서 개발했던 QuickC와 QuickPascal IDE의 스크린샷이다. 보기에 참 생소하다. 출처는 유명한 고전 소프트웨어 라이브러리인 WinWorld이다.

사용자 삽입 이미지

사용자 삽입 이미지

마소는 QuickBasic만 건지고 나머지는 다 망했다. QuickBasic이야.. 뭐 무료 축소판 QBasic을 MS-DOS와 Windows에다 포함시키기까지 했을 정도이고 말이다. 빌 게이츠가 베이식 언어를 아주 좋아했다.
그 반면 볼랜드는 Turbo Basic만 망하고 C와 Pascal을 건졌다. Turbo Basic의 개발진은 볼랜드를 퇴사하고 따로 회사를 차려 PowerBasic을 만들게 됐다.

2. 다음으로, 본가에 속하는 최상위 플래그십 제품군에는 그냥 자기 회사명을 붙였다.

Borland Pascal, C++
Microsoft Basic, C/C++

1990년대에 C에 이어 C++ 컴파일러가 개발되면서 자기 제품의 공식 명칭을 아예 C++이라고 바꿔 붙이는 곳이 있는가 하면, C와 겸용임을 내세우면서 C/C++이라고 붙이는 곳도 있었다.

볼랜드의 경우 C++을 C와는 완전 별개로 취급했는지 버전까지 1.0으로 도로 리셋하면서 Turbo C++ 내지 Borland C++이라고 작명했지만.. 마소는 C++을 기존 C 컴파일러의 연장선으로 보고 MS C 6.0 다음으로 7.0을 MS C/C++ 7.0이라고 작명했다. 사실, 연장선이라고 보는 게 더 일반적인 관행이었다.

참고로 왓콤 역시 Watcom C 9.0의 다음 버전이 Watcom C/C++ 9.5가 돼서 마소와 비슷하게 작명과 버전 넘버링을 했다. 왓콤은 제품이 짬이 길다는 인상을 주기 위해 첫 버전을 일부러 1이 아닌 6.0부터 시작하는 기행을 벌였었다! 볼랜드의 버전 넘버링과 비교하면 극과 극 그 자체였다.

터보 C++이랑 볼랜드 C++의 차이는.. 더 덩치 큰 상업용 프로그램 개발을 위한 OWL/Turbo Vision 같은 자체 프레임워크 라이브러리를 제공하느냐 여부 정도였지 싶다. 프로페셔널 에디션이냐 엔터프라이즈 에디션이냐의 차이처럼 말이다. 그리고 이때쯤 Windows용 지원도 시작됐다.

3. 그랬는데, 1990년대 이후부터는 그 플래그십 제품군도 Windows 전용의 더 고급 브랜드로 대체됐다.

볼랜드는 90년대 중반의 Delphi와 C++Builder로,
마소는 그 이름도 유명한 비주얼 브랜드로 말이다. Visual Basic, Visual C++.
그리고 마소도 Visual C++부터는 C/C++ 대신 C++만 내걸기 시작했으며,

관계가 이렇게 된다.
Visual C++이 과거 MS C/C++을 계승한 거라는 흔적은 _MSC_VER 매크로 값이 Visual Studio 자체의 버전보다 더 크다는 점을 통해서나 유추할 수 있다.

1이 2를 거쳐 3으로 바뀌는 동안 주변에서는 C 대신 C++이 대세가 되고, 주류 운영체제가 도스에서 Windows로 완전히 넘어가고 거대한 프레임워크 라이브러리가 등장하는 등의 큰 변화가 있었다. 개발 환경도 단순히 코딩용 텍스트 에디터와 디버거 수준을 넘어서 RAD까지 추구하는 수준으로 발전했다.

또한, 이 3단계가 주류가 될 즈음부터 마소의 Visual 툴들이 볼랜드를 완전히 꺾고 제압해 버렸다.
마소가 운영체제 홈그라운드라는 이점을 갖고 있기도 했거니와, 또 근본적으로는 파스칼이라는 언어 자체가 볼랜드의 창업자인 필립 칸이 선호하거나 예상한 것만치 프로그래밍계의 주류가 되지 못하고 마이너로 밀려난 것이 크게 작용했다. 네이티브 코드 생성이 가능하면서 빌드 속도가 왕창 빠른 건 개인적으로 무척 마음에 들었는데 말이다..;;

그에 반해 마소의 베이식은 파스칼보다 그리 나은 구석이 없는 언어임에도 불구하고 자사 운영체제의 닷넷빨 있지, 레거시 베이식도 자사 오피스의 VBA 매크로 언어가 있으니 망할 일이 없는 지위에 올라 있다.

한때(1990년대 후반??)는 파스칼이 언어 구조가 더 깔끔하고 좋다면서 정보 올림피아드 같은 데서라도 각광 받았지만.. 지금은 그런 것도 없다. 그 바닥조차도 닥치고 그냥 C/C++이다.
델파이를 기반으로 이미 만들어진 유틸리티나 각종 DB 연계 프로그램들(상점 매출 관리 등등..), SI 쪽 솔루션을 제외하면 파스칼은 마치 아래아한글만큼이나 입지가 좁아져 있지 않나 싶다..;;.

범언어적인 통합 개발 환경이라는 개념을 내놓은 것도 마소가 더 일렀다. Visual Studio가 나온 게 무려 1997년이니까.. 개발툴계의 '오피스'인 셈이다. (Word, Excel 등 통합처럼 Basic, C++ 통합). 그에 비해 볼랜드 진영에서 Delphi와 C++Builder를 통합한 RAD Studio를 내놓은 것은 그보다는 훨씬 나중의 일이다.

Windows NT야 이미 있던 16비트 Windows와 버전을 맞추기 위해서 3.1부터 시작했는데, Visual Studio의 경우, 공교롭게도 1990년대 중반까지 Visual Basic과 Visual C++의 버전이 모두 4.x대였다.
그래서 첫 버전인 Visual Studio 97은 각각의 툴 버전과 Studio 버전이 모두 깔끔하게 5로 맞춰졌으며, 이듬해에 나온 차기 버전은 어째 98이라는 연도 대신, 버전인 6으로 맞춰질 수 있었다.

2010년대 이후로 C++이 워낙 미친 듯이 바뀌고 발전하고 있으니.. D 같은 동급 경쟁 언어들조차 기세가 꺾이고 버로우 타는 중이다. 도대체 지난 2000년대에 C++98, C++03 시절에는 C++ 진영이 export 병크 삽질이나 벌이면서 왜 그렇게 침체돼 있었나 의아할 정도이다. 그 사이에 Java나 C# 같은 가상 머신 기반 언어들이 약진하니, 뭘 모르는 사람들은 겁도 없이 "C++은 이제 죽었네" 같은 소리를 태연히 늘어놓을 지경까지 갔었다. (2000년대 중반이 Windows XP에, IE6에... PC계가 전반적으로 좀 '고인물'스러운 분위기로 흘러가던 때였음) 한때 잠시 그러던 시절이 있었다.

Posted by 사무엘

2020/01/20 08:34 2020/01/20 08:34
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1707

요즘 Visual C++ 사용 메모

1. 디버깅 관련

수 년 전에 본인은 Windows에서 명령 프롬프트와 디버그 로그(OutputDebugString)에 유니코드가 지원되는 날이 언제쯤 올까 푸념을 늘어놓은 적이 있었는데.. 이건 놀랍게도 Windows 10에서 명령 프롬프트의 유니코드화(특수한 여건이 갖춰졌을 때 부분적으로 한해서)와 더불어 그럭저럭 현실이 됐다.

디버거 툴에 대해서 본인이 더 원하는 것은..

(1) IDE가 디버거를 붙여서 직접 실행해 준 디버기 말고.. 타 프로세스에 의해 실행된 디버기도 자동으로 감지해서 breakpoint 내지 로그 출력을 잡아 주기
(2) breakpoint의 작동 조건으로, "임의의 타 지점을 먼저 지나쳤거나 그게 call stack 아래에 있을 것" 정도 지정하기

정도이다.
(1)을 위해서 Attach to process 같은 기능이 이미 있긴 하다. 하지만 내 프로그램이 아주 잠깐 동안만 짤막하게 실행되고 마는 상황이라면(정상적인 종료이든, 오류로 인한 종료이든) 사용자가 느릿느릿 일일이 저 명령을 내릴 겨를이 없다.
이건 EXE의 디버깅도 DLL의 디버깅과 비슷한 양상으로 만든다. 실행 인자를 사용자가 지정해 주는 게 아니라, 이 EXE는 다른 EXE로부터 어떤 인자를 받아서 실행됐는지를 디버거로부터 안내받게 될 것이다.

(2)는 물론 코드 자체를 고쳐서 상태 변수 같은 걸 global하게 추가하는 식으로 편법으로 구현할 수는 있다. 하지만 그건 몹시 귀찮고 불편하다.
디버깅을 해야 하는 코드가 여러 부분에서 호출되고 있는데 우리는 특정 상황에서 호출된 것에만 관심이 가 있는 거.. 생각보다 자주 있는 일이다. 이에 대한 지원이 더 잘 된다면 프로그래머의 생산성이 많이 향상될 수 있을 것이다.

글쎄, 위의 두 아이템은 오래 전에 이미 언급한 적도 있을 것이다.
이것 말고.. 딱히 기술적으로 어려울 것 전혀 없는데 좀 있었으면 좋겠다 싶은 기능으로는..
디버깅을 위해 실행할 프로그램과 인자(argument)를 여러 세트 등록해 놓고.. 사용자가 예전에 등록해 놨던 세트를 곧장 불러올 수 있으면 좋겠다.

지금도 Debug 탭의 Command 입력란의 콤보 상자를 눌러 보면.. 달랑 revsvr32, Edit, Browse 이런 몇 가지 고정적인 아이템밖에 없다. 거기에다가 사용자가 이전에 등록한 적 있는 세트들이 같이 나오면 된다. 이 얼마나 깔끔한가?
EXE라면 Command가 바뀔 일은 별로 없겠지만 인자에 대한 세트 관리 기능이 있다면 충분히 유용할 수 있다.
IDE에 이런 기능이 없으니 날개셋 같은 개인 작품에서나 회사 제품 코드에서나.. 디버깅을 위해 사용할 다양한 프로그램들 경로를.. 소스 코드 주석이나 별도의 텍스트 파일에다 따로 메모해 놓는 촌극이 벌어지고 있다.

세트 데이터는 굳이 해당 프로젝트 파일에다가 저장하지 않아도 된다. 프로젝트/솔루션에 의존할 필요 없이, 그냥 그 프로그램 자체의 history data 명목으로 관리하는 형태로 제공되어도 충분히 편리할 것 같다.

2. 코드 자동 서식 적용

요즘 Visual C++ IDE에는.. 코딩을 하면서 닫는 중괄호나 세미콜론이 입력됐을 때, 각종 변수와 연산자· 토큰 사이에 공백을 균일하게 삽입하거나 없애고 탭 들여쓰기도 일관되게 맞춰 주는 '자동 서식' 기능이 제공된다. 쉽게 말해 whitespace에 대한 formatting 말이다. 이 옵션이 기본적으로 켜져 있다.

내 기억이 맞다면 이건 Visual C++ 2013쯤부터 처음으로 도입됐다. 2012에는 아직 확실하게 없었다.
베이직은 1980년대 도스 시절 QuickBasic에서부터 있었으며 C#도 최소한 200x 버전에서는 들어간 기능이지 싶은데 C++은 이제야 도입됐다.

다른 언어들은 문장을 완전히 파싱해서 내부 representation tree로 바꾼 뒤, 그걸 텍스트로 재구성함으로써 서식도 덤으로 적용되는 것이겠지만, C++은 그럴 수는 없지 싶다. 진짜 기계적이고 lexical한 문자열 치환 수준에서만 서식이 적용되지 싶다.

자동 서식 기능이 전반적으로는 괜찮은 편인데.. int *a, *b는 왜 int* a, * b라고 공백을 어색하게 배치하나 모르겠다. D처럼 int* a,b라고 썼을 때 b까지 포인터형이 되는 언어라면 모를까, 포인터형 별표와 변수명 사이에 공백이 들어가야 할 필요는 느껴지지 않는다.

그리고 배열 delete인 delete[]도 토큰 배치가 약간 기괴하긴 하지만.. 개인적으로는 붙여서 delete[] ptr; 이러는 걸 선호한다. 거기까지는 괜찮은데 delete []a를 다 붙여서 delete[]a로 바꾸는 건 좀 의아하다. 차라리 delete[] a라고 해 주지..
비슷한 맥락으로로, 함수의 인자로 배열의 포인터를 전달하는데 TYPE(*arg)[4] 같은 것을 한데 다 붙여 버리니 이 또한 어색하고 이상하다.

이런 것들이 C++의 자동 서식은 완전한 파싱을 거쳐서 적용되는 게 아니기 때문에 발생하는 부작용이지 싶다. 그러니 매크로나 템플릿 내부 같은 데서도 정확한 동작을 기대하기 어렵다.

3. 2019, 대화상자 리소스 에디터 뻗음

Visual Studio IDE는 2012~2013 즈음부터 외형이 크게 바뀌지 않기 시작했기 때문에 특히 2015와 2017은 내 경험상 거의 분간이 안 된다. 영문판은 웬일로 FILE EDIT 등 메뉴 이름을 잠깐 몽땅 대문자로 표기하는 객기(?)를 부리기 시작했다가 후대 버전에서 객기를 접은 듯하다.
2019는 프로그램의 제목 표시줄이 없어지고 화면 첫 줄에 곧바로 메뉴가 표시되기 시작했다. 현재 열려 있는 솔루션의 이름은 메뉴의 오른쪽에 표시된다. 윕 브라우저들도 그렇고 요즘은 제목 표시줄을 없애는 게 유행이기라도 한가 보다. 게다가 쟤들은 메뉴조차 없애 버리고 Alt키를 눌렀을 때만 메뉴가 표시되게 해 놨다.

그렇게 프로그램의 외형이 야금야금 바뀌는 것이야 좋다고 치는데.. 왜 예전에는 경험한 적이 없던 버그까지 야금야금 끼어 들어가나 모르겠다.
우선 아주 불규칙하지만 분명한 빈도로.. 텍스트 에디터의 폰트가 본인이 수동으로 변경하기 전의 원래 폰트로 되돌아간다. 정확한 재연 조건은 모르겠다. Visual Studio를 열어 놓은 채로 며칠 간격으로 절전 모드에 들어갔다가 복구하기를 반복하다 보면 되돌아가 버린다.

그리고 C++ win32 리소스 중에서 대화상자 편집기만 제대로 안 열리고 프로그램이 무한 루프에 빠지며(= CPU 소모하면서) 응답이 멎는 문제가 있다.
잘 알다시피 Visual Studio 2012부터는 msi 파일을 생성하는 배포 패키지 프로젝트가 짤려서 기본 제공되지 않는다. 별도의 extension을 설치해야만 다시 지원된다. 본인은 회사에서는 그렇게 했다.

그런데 그 extension을 설치한 뒤부터 win32 프로젝트에서 대화상자 편집기가 열리지 않고 IDE가 얼어붙어 버렸다. 그래서 대화상자 리소스를 편집하는 작업을 할 수가 없어졌다.
뒤늦게 그 extension을 disable시키거나 아예 제거해도.. 버전 16.2.3 최신 업데이트를 적용해도, 심지어 Visual Studio를 재설치(복구)해도 그 문제는 해결되지 않았다! 이 VS 2019는 대화상자 리소스를 영원히 편집할 수 없는 절름발이 상태가 된 것 같다.

검색을 해 보니 이 문제는 VS 2019 초창기 시절부터 종종 보고되곤 했던 것 같다. 하지만 release candidate 수준의 옛날 일이지 최신 업데이트에 이르기까지 문제가 발생하거나 해결됐다는 얘기는 딱히 발견하지 못했다.
이러니 Visual Studio는 최신 버전이 구버전의 용도를 완전히 흡수· 대체하지 못하고 구버전도 여전히 병행해서 사용돼야만 할 것 같다. 결국 회사에서도 2010을 따로 설치해야 했다.

4. 2010, 동작은 하지만 이상한 경고 메시지

그럼 구버전은 아무 이상이 없느냐 하면 불행히도 그것도 아니다.
Windows 10 초창기에는 안 그랬던 것 같은데.. 운영체제 업데이트를 몇 번 거치고 나니 VS 2010 devenv.exe는 정체를 알 수 없는 이상한 에러 메시지를 한번 내뱉은 뒤에 실행된다.

The file C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Microsoft.Vsa.tlb could not be loaded. An attempt to repair this condition failed because the file could not be found.
Please reinstall this program.


이미 알려진 문제이며 .NET Framework 3.5를 설치한 뒤에 Visual Studio도 복구(재설치)하면 이런 메시지가 없어질 거라고 하는데..
프로그램 사용을 못 할 정도의 치명적인 오류는 아니니 귀찮아서 안 하고 지낸다. 어차피 VS 2010을 C# 같은 .NET 플랫폼 개발용으로 사용하는 건 아니니 말이다.

5. 컴파일러의 버그

하루는 32비트 정수와 16비트 정수를 인자로 받아서 이걸 한데 뭉친 64비트 정수를 되돌리는 정말 간단한 인라인 함수를 구현한 적이 있었다. 이렇게 생성된 값을 저장하고 불러오게 했는데.. 문제가 발생했다. 불러온 결과가 이전에 저장했던 결과와 일치하지 않고 프로그램이 제대로 동작하질 않았다.

곳곳에다 변수값을 화면에다 찍어 봐도 내가 짠 코드에는 좀체 문제가 없는 것 같고.. 듣도 보도 못한 이상한 값은 전혀 예상치 못했던 곳에서 갑자기 생기고 있었다.
비유하자면 MAKELONG(16012, 76)의 계산 결과값이 저장할 때와 불러올 때가 서로 다르다는 게 믿어지시는가? high word 쪽의 값이 내가 지정한 값이 아니라 32766 같은 엉뚱한 값을 기준으로 계산되었다.

해당 함수를 #pragma를 줘서 최적화를 끄고, 인라이닝을 해제하는 등 별짓을 해도 계산값이 교정되지 않았다. 컴파일러가 구형인 것도 전혀 아니고, 갓 업데이트 받았던 따끈한 Visual C++ 2019 16.3.2였다.
신기한 것은.. { return X|(Y<<32); } 대신

{
    auto ret = X|(Y<<32);
    TRACE("%d %d\n", X,Y);
    return ret;
}

이렇게 함수 인자를 강제로 화면에다 찍게 하면 버그가 발생하지 않고 계산이 맞게 되었다는 것이다.
하지만 저렇게 하지 않고 함수를 아예 #define 매크로 형태로 고쳐도 문제가 동일하게 발생하니.. 이 정도면 변수를 참조하는 코드 자체가 단단히 잘못 생성되고 있는 것이나 마찬가지였다.

수 년 전엔 bit rotation을 구현한 암호화 알고리즘에서도 release와 debug의 동작이 다르고 최적화 적용 여부에 따라 동작이 달라지는 현상을 발견하긴 했는데.. 이 문제는 그것보다도 더 심각한 문제였다.
물론 비트 연산이라는 공통점은 있다. 컴파일러가 << >> | 같은 연산자를 다루는 데서 무리하게 최적화를 시도하는가 보다.

결국 이 버그는 memcpy라는 무식하기 짝이 없는 물건을 동원함으로써 겨우 회피할 수 있었다. 64비트 정수에다가 일단 32비트 값을 대입한 뒤, 4바이트 오프셋에다가 16비트 정수를 강제로 복사하게 했다. 컴파일러가 memcpy는 어째 제멋대로 최적화를 안 했는지 이렇게 하니 프로그램이 깔끔하게 돌아가기 시작했다. 비트 엔디언 독립성은 물론 포기했다.

memcpy는 예전에 align이 맞지 않는 임의의 단위로 메모리를 읽고 써야 할 때.. x86 계열에서는 아무 문제 없다가 ARM 같은 CPU에서 멀쩡한 프로그램이 뻗을 때도 유용하고 사용한 적이 있다.. CPU 특성이나 컴파일러의 특성을 가리지 않고 제일 무식하고 확실하게 메모리를 읽고 쓰는 게 보장돼야 할 때 최후의 보루 역할을 하는 듯하다.
그나저나 컴파일러의 버그임이 명백한 이 현상은 도대체 왜 발생하는지, 해결할 방법이 없나 궁금하다.

이상이다.
본인은 예나 지금이나 개인용 컴터에는 VS 2003, 2010, 2019를 나란히 설치해 놓고 지낸다. 즉, 최신 버전 말고도 2003과 2010은 고정 설치라는 뜻이다.

한때는 최신 API에 대한 설명 때문에 201x의 도움말을 하드에 설치해 놓았으나, 요즘은 마소에서 로컬 도움말은 2015 이후로 업데이트도 안 하고 거의 버린 자식 취급하길래..
그건 포기하고 그냥 옛날 200x 시절의 MSDN을 고전 Windows API 및 기본 C/C++ 레퍼런스용으로 사용한다. 이걸로 충당이 안 되는 최신 정보는 인터넷 조회로 해결하고 말이다.

Visual C++ 201x 버전들에서 본인의 기억에 남아 있는 인상적인 변화 사항은 다음과 같다.

  • 2012: 흰 스킨 도입. Windows XP 타겟 지원을 최초로 중단했다가 별도의 툴킷으로 따로 제공 시작. Syntax coloring이 더 세분화됨. 정적 분석 기능 도입. 예전 같은 서비스 팩 대신, 업데이트 n 형태로 수시로 업데이트 되기 시작
  • 2013: 약간 푸르스름하면서 흰 스킨 도입. 코드 자동 서식 적용 시작, 커뮤니티 에디션 도입.
  • 2015: C 런타임 라이브러리 구조가 개편됨
  • 2017: 설치/업데이트 체계가 전면 개편됨. 안드로이드 등 별별 환경 개발까지 다 지원하기 시작. 오프라인 도움말 앱을 사실상 지원 중단
  • 2019: 프로그램 제목 표시줄이 없어짐. 스플래시 화면이 더 간지나게 바뀜. 색깔이 채도가 약간 더 올라가고 산뜻해짐. 처음 실행했을 때나 기존 솔루션을 닫은 직후에 통상적인 시작 페이지 대신, "원하는 작업을 선택하세요" 대화상자가 표시됨.

Posted by 사무엘

2020/01/04 08:34 2020/01/04 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1701

Windows 운영체제에서 쓰이는 EXE나 DLL 같은 실행 파일들은 잘 알다시피 Portable executable이라는 포맷을 따라 만들어져 있다. 그래도 맨 앞에는 MZ로 시작하는 MS-DOS EXE 헤더가 호환성 차원에서 여전히 있으며, 거기서 가리키는 오프셋을 따라가 보면 그제서야 PE 헤더가 시작된다.

그런데 아무 EXE/DLL이나 찍어서 바이너리 에디터로 들여다 보시라. 도스 EXE stub과 실제 Windows용 실행 파일의 사이 공간에는 뭔가 4바이트 간격으로 규칙성이 느껴지는 정체불명의 데이터가 수십~수백 바이트가량 있으며, 끝에는 약속이나 한 듯 "Rich"라는 문자열이 있다. 이건 도스/Windows 어느 플랫폼에서도 쓰이지 않는 잉여 데이터이다.
이놈의 정체는 도대체 무엇일까...??

사용자 삽입 이미지

더욱 괴이한 것은.. 이건 Visual Basic, Visual C++ 같은 마소의 개발툴(링커)로 생성한 바이너리에만 존재한다는 것이다.
게다가 마소 개발툴조차도 처음부터 이랬던 게 아니다. Visual C++의 경우, 5.0의 마지막 서비스 팩(아마 3)부터 적용된 거라고 한다. 그러니 지금으로부터 20여 년 전, 대략 1997~98년 사이부터이고.. 그냥 편의상 6.0부터라고 생각해도 무방하다.

본인의 고딩 시절, Visual C++ 4.2로 빌드된 날개셋 한글 입력기 1.0 내지 PentaCombat 오목 게임의 실행 파일을 들여다보면.. 아니나다를까 일명 "Rich 헤더"가 존재하지 않는다. MZ와 PE 사이에 아무 공백이 없다.

사용자 삽입 이미지

그러나 VC++ 6으로 빌드된 날개셋 1.2부터는 응당 Rich 헤더가 추가되어 있다.

과거 Windows 9x의 경우, 내부의 프로그램들을 빌드할 때 Visual C++이 아닌 다른 자체 컴파일러를 사용했다. kernel32, gdi32, user32 같은 DLL이라든가 메모장 같은 간단한 프로그램 말이다.
여기서 유래된 바이너리들은 Rich 헤더가 존재하지 않으며, 이 관행은 Visual C++ 6이 출시된 뒤에도 Windows 98/ME까지 변함없이 이어졌다.

하지만 내장 프로그램 중에서 워드패드와 그림판, EUDC 편집기처럼 Program Files\Accessories에 들어있던 프로그램은 MFC도 사용하고 나름 Visual C++ 냄새가 났었다.
얘들은 Windows 95 시절엔 대외적으로 공개된 적이 없는 MFC 짝퉁을 사용하다가, Windows 98부터 깔끔하게 Visual C++ 5 sp3으로 빌드되기 시작했다. 그래서 얘들은 예외적으로 Rich 헤더가 포함되기 시작했다.

이런 9x와 달리 Windows NT는 운영체제 차원에서 처음부터 Visual C++ 팀과 잘 연계하는 편이었다.
9x 계열은 98에 와서야 msvcrt와 mfc42같은 Visual C++ 출신의 배포용 DLL들이 최초로 정식 포함돼 들어간 반면, NT 계열은 처음부터 메모장도 진작부터 msvcrt를 사용해 왔다. NT4는 직접 확인해 보지 않아서 모르겠지만, 2000은 모든 EXE/DLL의 내부에 Rich 헤더가 존재한다.

한편, Visual C++ 말고 델파이 같은 타 개발툴로 빌드된 실행 파일에는 Rich 헤더 같은 건 당연히 존재하지 않는다.

사용자 삽입 이미지

그럼 본론으로 들어가도록 하겠다.
마소에서는 이 헤더? 데이터? chunk?를 왜 집어넣기 시작했으며, 이 정보의 의미는 도대체 무엇일까? 엄밀히 말하면 헤더라고도 볼 수 없는 단순 데이터일 뿐인데 말이다.
놀랍게도 마소에서는 이에 대해서 지금까지 공식적인 답변을 한 번도 제공하지 않았으며 undocumented, 묵묵부답으로 일관한 듯하다. 빌드 시에 이 헤더를 제외시키는 옵션도 없다.

그래서 일각에서는 흉흉한 음모론까지 나돌기 시작했다. 제일 유명한 게 뭐냐 하면, 이건 이 바이너리를 빌드한 컴퓨터 환경을 식별하는 정보라는 것이다.
그래서 이 exe/dll이 악성 코드로 밝혀져서 경찰에 수사를 의뢰하게 되면.. 이 정보로부터 개발자의 컴퓨터를 추적할 수 있고, 따라서 악성 코드를 만든 사람도 아무 단서가 없는 것보다는 더 용이하게 색출할 수 있다고 한다..;;

이거 마치 컬러 복사기 얘기처럼 들린다. 컬러 복사기의 결과물에는 아주 정교한 워터마크가 사람 눈에 안 띄게 몰래 새겨진댄다. 그래서 어설픈 컬러 복사 위조지폐가 발견되면 그 워터마크를 토대로 복사기의 일련번호를 추적할 수 있으며, 이를 통해 범인도 색출할 수 있다고 한다. 허나 본인은 그런 게 실제로 존재한다고 믿지는 않는다.

뭐, 복사기는 그렇다 치고.. 실행 파일의 경우, 상식적으로 생각해 봐도 저건 개발 컴퓨터의 색출 목적으로 사용하기에는 보안이 너무 허술하다.
진짜 나쁜 마음 품은 악성 코드 개발자라면.. 그 Rich 헤더 부분을 후처리로 몽땅 0으로 칠해서 일부러 지워 버리기만 해도 자기 정체를 숨길 수 있으며, 그래도 악성 코드의 동작에는 하등 문제될 것 없다.

이 경우 PE 헤더의 다른 필드에 존재하는 checksum 정보가 어긋나서 파일이 변조되었다는 것이 감지되겠지만, 이것도 일부러 기재하지 않았다고 0을 집어넣어 버리면 그만이다. 더구나 보안을 위해 소프트웨어에서 이미 있던 이스터 에그도 다 없앴고 요즘은 바이너리 차원에서 철저히 예측 가능한 reproducible build까지 추구하는 마당에, 이런 식의 비밀 식별 정보는 마소의 개발 방침 이념과도 어울리지 않는다.

그러니 프로그램을 빌드할 때마다 내 컴퓨터의 맥 어드레스가 유출된다는 식으로 불안해할 필요는 없다. 하지만 Rich 헤더의 정체는 여전히 베일에 싸여 있었다.
그래서 전세계의 많은 컴덕과 해커들이 의문을 품기 시작했으며, 어떤 용자는 MS에서 개발한 링커 프로그램을 아예 근성으로 리버스 엔지니어링까지 했다. 그래서 이 데이터에 대한 여러 사실들을 밝혀냈다.

가장 먼저.. Rich 헤더는 4바이트 덩어리 단위로

A B B B X1 Y1 X2 Y2 ... "Rich" B

대체로 요런 형태로 돼 있다.
끝의 Rich 다음에 나오는 마지막 double word인 B가 일종의 난수이며, 암호화 key이다. 그리고 Rich 앞에 있는 숫자들은 바로 그 B와 xor을 해 주면 실제값을 얻을 수 있다.

그러면 맨 첫째 값 A는 언제나 0x44 0x61 0x6E 0x53.. 문자 형태로 늘어놓으면 "DanS"라는 시그니처가 된다.
다음으로 시그니처 뒤에 이어지는 몇 개의 B는 16바이트 단위 padding을 맞추기 위한 0값인 것 같다. 자기 자신을 xor 하면 결과는 언제나 0이 되니 말이다.

그 뒤 이어지는 같은 값들은 숫자 2개가 X, Y 형태로 한 pair를 이룬다. X는 이 바이너리를 생성하는 데 쓰인 툴(C 컴파일러, C++ 컴파일러, 리소스 컴파일러, 어셈블러 등..)과 버전(빌드 번호)을 나타내며, Y는 그 도구를 이용하여 생성된 아이템.. 이를테면 obj 파일의 개수를 나타낸다고 한다.

그럼 이런 정보들은 링커가 어디에서 얻어서 집어넣는가 하면.. 당연히 자기가 input으로 받아들이는 obj 파일로부터이다. 그렇잖아도 1997~98년을 전후해서 obj 파일 포맷이 바뀌었다고 한다.
obj야 소스 코드를 번역한 기계어 코드의 뭉치일 뿐이니 20년 전이나 지금이나 컴퓨터 아키텍처가 변함없는 한 바뀔 일이 없으며, 내부 구조가 바뀌어 봤자 새 버전의 컴파일러/링커가 인식하는 새로운 chunk 정도나 추가되는 게 전부일 것 같은데.. 꼭 그렇지는 않은 모양이다.

하긴, 같은 양의 코드를 빌드해도 요즘 컴파일러는 예전에 비해 obj의 파일 크기가 점점 더 커지고 있어 보이긴 하다. 디버깅 또는 최적화와 관련된 온갖 힌트와 메타정보들이 첨가되어서 그런 것 같다.
그래도 obj는 소스 코드를 빌드할 때마다 새로 생성되는 일회용 임시 파일일 뿐이니, 포맷에 하위 호환성 걱정 따위는 할 필요가 없을 것이다. 날려 봤자 빌드만 다시 하면 될 일이고..

아무튼 흥미로운 사실을 알게 됐다.
개인 정보 유출은 아니고, 그렇다고 디버깅과 관련된 힌트도 아니고 저런 정보가 마소의 개발툴에서 도대체 왜 들어갔는지는 여전히 오리무중이다만.. 그래도 내역을 까맣게 모르던 시절보다는 상황이 나아졌다.
본인이 이 글을 쓰기 위해 검색하고 참고한 외국 사이트는 다음과 같으니,  더 자세한 관심이 있는 분은 참고하시기 바란다.

The devil’s in the Rich header
Microsoft's Rich Signature (undocumented)
The Undocumented Microsoft "Rich" Header
Article: Things They Didn't Tell You About MS LINK and the PE Header

저 외국 사이트들의 설명에 따르면, Rich와 DanS는 모두 1990년대에 마소의 Visual C++ 팀에서 근무하고 이 데이터의 구현에 직접 관여했던 프로그래머의 이름에서 유래되었을 가능성이 높다고 한다. 그 이름은 각각 Richard Shupak와 Dan Spalding이다. ㄷㄷㄷ;;
MZ, PK만큼이나 프로그래머의 이름이 파일 포맷에 각인된 사례라 하겠다.

참고로 전자 리처드의 경우, Windows SDK에서 psapi.h의 작성자로 이름이 나와 있기도 하다.
PSAPI는 Windows NT 계열용으로 실행 중인 프로세스 정보를 조회하는 EnumProcesses, GetModuleFileNameEx 등의 함수를 정의하는 라이브러리인데, 작성 날짜가 무려 1994년이라고 적혀 있다.

Posted by 사무엘

2019/05/21 08:31 2019/05/21 08:31
,
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1621

1.
먼 옛날에 기술이란 게 지금보다 비싸고 희귀하던 시절에는 컴파일러 자체가 유료화 대상이었다. Windows 플랫폼 SDK에 같이 들어있는 무료 기본 컴파일러는 상업용으로 따로 판매되는 Visual C++의 컴파일러와 같은 빡센 최적화 기능이 없었다. 사용자가 작성한 코드대로 돌아가는 실행 파일은 만들어 주지만 최적의 성능을 발휘하는 형태로 만들지는 않았다는 것이다.

그러다가 Visual C++ 2005 Express를 기점으로 고성능 컴파일러도 무료화되고 기본 제공되기 시작했다.
요즘 컴파일러의 최적화 테크닉은 학교에서 배우는 것처럼 (1) 쓰이지 않는 변수나 코드 제거, (2) 값이 뻔한 수식은 한 번만 미리 계산해 놓고 loop 밖으로 옮기기, (3) 변수는 적절하게 레지스터 등재, (4) 함수는 적절하게 인라이닝, (5) 최대한 병렬화, (6) CPU 명령 하나로 처리 가능한 단위로 묶기, (7) switch 분기를 매번 번거로운 if문이 아니라 테이블 참고 방식으로 변경... 같은 미시적인 것만이 그림의 전부가 아니다.

그야말로 번역 단위(소스 코드) 간의 경계를 넘나들고 컴파일러와 링커의 역할까지 재정립하면서까지 저런 최적화 테크닉을 적용할 부위를 판단할 필요가 있다. 그래서 컴파일러에는 전역 최적화라는 옵션이 도입되고, 링커에는 link time code generation이라는 옵션이 추가되어 서로 연계한다. 뭐, 대단한 기능이긴 하지만 이 정도로 최적화를 쥐어짜서 성능이 더 나아진 것 대비 프로그램의 빌드 타임이 너무 길어지는 건 별로 마음에 안 든다.

그리고 세상에 C++ 컴파일러가 MS 것만 있는 것도 아니고, 경쟁사들의 제품도 갈수록 성능이 좋아지고 가격이 저렴해지고 있으니 optimizing compiler 정도는 충분히 대중적인 영역으로 내려가게 됐다.

2.
한편, Visual Studio Express 에디션은 기업에서 사용이나 상업용 소프트웨어 개발까지 아무 제약 없는 무료인 대신, 리소스 편집기와 MFC 라이브러리가 없다. 지금도 그런지는 모르겠지만 처음엔 심지어 64비트 빌드 기능도 없었다.
그 뒤 VC++ 2013부터는 MFC와 리소스 편집기가 다 있는 Community 에디션이란 게 나왔다. 얘는 기능 제약이 없는 대신 개인 개발자나 스타트업 중소기업 수준까지만(인원 얼마 이하, 연 매출 얼마 이하..) 무료이다.

서로 조건이 다른 무료이기 때문에 VC++ 2013, 2015, 2017까지는 한 버전에 대해서 express와 community 에디션이 모두 나왔다. 하지만 express는 장기적으로는 community로 흡수되고 없어질 것으로 보인다.

3.
이렇게 프로그램을 기본적으로 만들고 빌드하는 데 필요한 도구들은 슬슬 무료화 단계에 들어섰고, 그 다음으로는 그냥 빌드만 하는 게 아니라 제품의 품질을 높이는 데 도움을 주는 도구, 대규모 공동 작업과 테스트를 위해 필요한 전문 도구들이 아직 유료이다. 어떤 것은 Visual Studio의 엔터프라이즈 같은 제일 비싼 에디션에서만 제공된다.

Visual C++ 2008에서는 GUI 툴킷이 feature pack을 통해 무료로 풀렸더니 2012부터는 정적 분석 도구가 무료로 풀렸다. 이걸 처음 써 본 소감은 꽤 강렬했다.

사실, 본인도 그저 닥치는 대로 코딩과 디버깅만 하는 것 말고 소프트웨어공학적인 개발 프로세스라든가 기술 문서 잘 쓰는 요령, 변수와 함수의 이름 잘 짓는 요령, 전문적인 테스트 절차와 프로파일링 같은 것을 잘 알지 못한다. 더 발전하려면 그저 무료로 풀려 있는 도구들만 쓰는 게 아니라 그 너머에 있는 도구들도 뭔지 알고 필요성을 공감할 정도는 돼야 할 텐데..

그렇잖아도 날개셋 한글 입력기도 어떤 형태로든 버그가 없었던 적은 거의 없었다. 그리고 개인 프로젝트뿐만 아니라 회사 업무도 늘 깔끔하게 마무리 짓지는 못해서 버그가 있는 채로 제품을 내고 후회한 적이 적지 않았다.
만들어진 제품의 품질을 검증하는 절차 자체는 소프트웨어뿐만 아니라 건축· 건설을 포함해 공학의 어느 분야에서나 다 필요하고 존재할 것이다. 그 가운데에 무형의 지적 산물인 소프트웨어만이 갖는 특수성 때문에 이 바닥에서만 통용되는 방법론도 있겠지만 말이다.

아무튼, 프로그래밍 툴의 제작사들이 흙 파서 장사하는 건 아닐 테니.. 이런 어마어마한 컴파일러쯤은 딴 데서 개발 비용을 회수할 통로가 다 있으니까 무료로 풀어 놓는 게 가능할 것이다.

4.
(1) 이렇듯, Visual Studio를 포함해 개발툴들은 갈수록 기능이 좋아지고 기술들이 상향평준화되고 있다. 일례로, 2017인가 2019부터는 코드 편집기에 일반 컴파일러의 경고/에러뿐만 아니라 정적 분석 결과까지 초록색 밑줄로 띄워 주는 걸 보고 무척 감탄했다. (NULL 포인터 역참조가 일어날 수 있습니다 따위..)

(2) 2017/2019부터는 도움말과 API 레퍼런스는 몽땅 인터넷으로 처리하고 로컬 오프라인용 Help Viewer를 더 관리하지 않으려는지? 설치 화면에서는 기본 선택돼 있지도 않으며, 컨텐츠 역시 2015 내용 이후로 달라진 게 없어 보인다.

(3) 그리고 Spy++의 64비트 버전은 왜 도구 메뉴에서 숨겨 버린 걸까?
한 프로그램만으로 32비트와 64비트를 통합해서 잘 동작하는 것도 아니고, "이 창의 메시지를 들여다보려면 64비트 Spy++를 실행해 주십시오" 안내를 해 주는 것도 아닌데 왜 이런 조치를 취했는지 이해할 수 없다. 사실은 32와 64비트를 가리지 않고 아주 seamless하게 동작하는 Visual Studio의 디버거가 정말 예술적인 경지의 대단한 도구이긴 하다.

(4) 시대 유행과 별 관계 없는 부분은 계속해서 안 바뀌기도 하는 것 같다.
도구상자를 customize할 때 콤보 박스의 길이 조절을 200x 시절처럼 마우스 드래그로 할 수 없고.. 2010 이래로 그냥 픽셀수 입력으로 때운 걸로 굳히려는가 보다.

무려 7년 전 글에서 지적했던 2. 메뉴 편집기의 우클릭 버그, 3. 툴바 편집기의 화면 잔상 역시, Visual Studio 2019 현재까지도 전혀 고쳐진 게 없다.
그리고 예전엔 안 그랬던 것 같은데 2019는 Visual Studio 텍스트 에디터의 폰트를 딴 걸로 바꾸고 껐다가 다시 켜서 곧장 반영되는 걸 확인까지 했는데.. 켠 채로 절전 상태로 몇 번 갔다가 돌아오면 폰트가 컴터에 따라 아주 가끔은 돋움이나 Courier New 같은 기본 글꼴로 돌아오는 것 같다.

Posted by 사무엘

2019/05/10 08:36 2019/05/10 08:36
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1617

1. Windows

과거에 Windows 95에는 워드패드, 그림판, 메모장, 계산기, 지뢰 찾기 같은 친근한(?) 프로그램 말고, 디스크 검사나 조각 모음, 남은 리소스 표시기 같은 프로그램도 같이 제공되었다. 이런 건 도스 시절부터 유틸리티 내지 '툴'이라는 카테고리로 분류되어 온 프로그램들이다.

Windows 98에는 간략히 보기(QuickView)라는 유틸리티도 있어서 탐색기의 우클릭 메뉴를 통해 실행할 수 있었다. 주요 문서· 이미지 파일들을 본격적인 편집 프로그램을 실행하지 않고 내용만 재빨리 들여다볼 수 있었으며, 제공되는 COM 인터페이스를 확장하면 제3자가 임의의 파일 포맷에 대해서 간략히 보기 기능을 추가로 제공해 줄 수도 있었다.

또한 이 QuickView는 자체적으로 exe/dll의 내부 구조를 분석해서 보여주는 기능도 있었다. 32비트 바이너리에 대해서는 PE 헤더에 기록된 내용과, import/export 심벌의 내용을 보여줬으며, 16비트 바이너리에 대해서는 각종 resident/non-resident 문자열 테이블의 내용을 보여줬기 때문에 파일 내부를 들여다보는 용도로 꽤 괜찮았다.

다만, Visual C++ 6부터는 빌드하는 바이너리에서 import 및 export 심벌들을 고유한 섹션 없이 그냥 rdata 섹션에다가 집어넣기 시작했는데, 이건 QuickView가 제대로 감지해서 보여주지 못했다. 그리고 QuickView는 Windows 2000/ME 같은 후대 버전에서는 더 존재하지 않고 없어졌다.
그 대신 Windows 95가 아닌 98에서 첫 도입되어 지금까지 전해져 오는 유틸리티는 (1) 시스템 정보(msinfo32), 그리고 (2) DirectX 진단 도구인 dxdiag이다.

시스템 정보는 도스 시절부터 PC-Tools나 Norton 같은 3rd-party 유틸리티들이 단골로 제공하던 기능이었는데, Windows용으로는 딱히 마땅한 게 없었다. 단순히 운영체제의 버전이나 남은 메모리 양 말고 컴퓨터 프로세서의 명칭이나 정확한 속도 벤치마킹 같은 건 쉽게 얻을 수 있는 정보가 아니었다.

사실, 시스템 정보는 Windows 팀이 아니라 Office 팀에서 먼저 개발해서 독자적으로 제공하고 있었다. Windows 95보다도 더 이른 1994년에 출시된 Word 6.0의 About 대화상자를 보면 System Info라는 버튼이 있다. 그러던 것이 Windows 98부터는 운영체제 기능으로 옮겨진 것이다.

그래서 그런지 System Info의 초기 버전에는 현재 실행 중인 Office 프로그램이 있는 경우, 그 제품이 현재 열어 놓은 문서 같은 시시콜콜한 정보도 표시해 주는 기능이 있었다. 그러나 그 기능은 Windows XP인가 Vista 즈음부터 삭제됐다.
지금은 시스템 정보에서 본인이 종종 유용하게 열람하는 것은 CPU 관련 정보, 그리고 현재 실행 중인 모든 exe/dll들을 조회하는 기능 정도이다.

한편, dxdiag도 처음 도입됐을 때와 달리 인터페이스가 갈수록 단순해지고, 그냥 '드라이버 상태 이상 무'만 출력하는.. 있으나마나 한 유틸이 돼 간다.
처음 도입됐을 때는 간단한 예제 그래픽과 애니메이션, 음악을 출력하면서 상태를 점검하는 기능이 있었다. 그랬는데 DirectDraw는 그냥 운영체제의 기능으로 흡수되고, DirectMusic나 DirectPlay 같은 건 짤리고 DirectX는 오로지 Direct3D에만 올인을 하면서 진단 도구도 지금처럼 바뀌게 됐다. 그래픽 쪽도 데모 기능은 없어졌다.

2. Visual Studio

Visual Studio (더 정확히는 Visual C++)는 개발툴이다 보니, 프로그래머의 입장에서 도움이 되는 자그마한 유틸리티들이 이것저것 같이 제공되곤 했다.

가장 대표적인 물건은 (1) Spy++이다. 응용 프로그램이 생성하는 모든 윈도우들과 거기 내부에서 발생하는 메시지들을 들여다볼 수 있으니 역공학 분석 용도로도 아주 좋다. 이 프로그램은 Visual C++ 초창기 버전부터 지금까지 거의 모든 버전에서 개근하고 있다. 아이콘에 그려져 있는 스파이(?) 아저씨도 처음에는 초록색 복장이다가 분홍색, 보라색을 거쳐 검정색까지 여러 번 변모해 왔다.

다만, 최신 운영체제에서 새로 추가된 메시지를 지원하는 것 외에 딱히 추가적인 개발이 되고 있지는 않으며, 도움말도 2000년대 초에 chm 기반으로 바뀌고 나서 딱히 변화가 없는 것 같다. VC++ 2005 즈음 버전에서, 트리 구조가 리스트 박스를 쓰던 것이 진짜 트리 컨트롤 기반으로 바뀐 것이 그나마 큰 변화였다.

그리고 (2) Dependency Walker도 아주 훌륭한 도구이다. 어떤 EXE/DLL이 외부로 제공하는(export) 심벌, 그리고 자신이 import하는 심벌들을 모두 분석해서 모듈별 dependency tree를 딱 구축해 준다. 아까 소개했던 QuickView와 달리, 최신 버전 바이너리들도 잘 지원한다.
심지어 Profiling이라고, 프로그램이 실행 중에 동적으로 불러들이는 dll과 심벌까지 찾아 주는 기능도 있다.

얘는 Visual C++ 6에서 1.x대의 구버전이 같이 제공되었지만 2000년대 이후부터는 같이 제공되지 않고 있다. 개발자의 홈페이지에서 최신 버전을 따로 받아야 한다. 다만, 이 프로그램은 2000년대 중반, Windows Vista 타이밍 이후로 개발과 지원이 사실상 중단된 듯하다.

(3) GUID 생성기는 프로그램 짜면서 내 오브젝트의 GUID를 생성할 일이 있을 때 사용하면 되는 물건이고..
(4) Error lookup도 매번 winerror.h를 뒤지는 수고를 덜어 주니 좋다. 소켓 API처럼 특정 모듈을 불러들인 뒤에야 내역을 알 수 있는 에러 코드값도 의미를 알 수 있다.

옛날에, 2003 정도 시절까지는 뭐랄까 COM 기술과 관계가 있는 도구가 더 있었다.
(5) ActiveX Test Container는 운영체제에 등록돼 있는 ActiveX 컨트롤들을 마치 Visual Basic처럼 클라이언트 영역 여기저기에 설치해 놓고는 이벤트 로그를 확인하고, 속성값을 변경하고 메소드들을 invoke할 수 있는 툴이었다.

ActiveX 컨트롤은 기술적으로 따지자면 무슨 비주얼 베이직이나 델파이의 컴포넌트를 개발툴 밖에서 어디서나 쉽게 사용할 수 있게 해 놓은 열린 규격에 가까웠다. 취지 자체는 나쁘지 않은데 후대에서 적극적으로 쓰이지를 않았으며, 기껏 웹에서나 비표준으로 잔뜩 쓰이다가 지금은 마소에서도 사실상 버린 자식 신세가 됐으니 참 안습하다.

본인도 실무에서 ActiveX 컨트롤을 삽입한 건 IE 웹브라우저 컨트롤과 플래시 컨트롤 딱 둘밖에 없었다. 원래는 Word/Excel 문서조차도 ActiveX 형태로 내 프로그램에다 삽입할 수 있다. 하지만 지금은 ActiveX 없이 웹 브라우저 화면에서 그런 문서 편집 화면을 띄우는 세상이 됐다. 거 참...

(6) 그리고 OLE/COM Object Viewer는 HKEY_CLASSES_ROOT 레지스트리를 싹 뒤져서 시스템에 등록돼 있는 COM 객체들을 몽땅 출력하고, 이들 프로그램 바이너리에 내장된 type library를 추출해서 해당 객체들의 구체적인 스펙을 보여줬다. 이건 문서화되지 않고 COM 형태로만 몰래 제공되는 운영체제의 숨은 기능들을 찾는 데 굉장히 큰 도움이 됐다. 이놈의 GUID는 무엇이고 어떻게 불러 오고 어떻게 호출하면 되는지를 말이다.

(5)와 (6)은 언제부턴가 짤려서 Visual Studio 201x에서부터는 찾아볼 수 없게 됐다. 맥은 레지스트리도 없고 COM, OLE 같은 것도 존재하지 않겠지만, 그래도 Spy++나 Dependency Walker의 맥 버전에 해당하는 유틸은 없는지 궁금해진다. Windows 프로그래밍 공부에 굉장히 큰 도움이 되었던 프로그램인걸 말이다.

(7) regsvr32는 개발툴이 아니라 운영체제에서 제공하는 유틸이긴 하지만, Visual Studio의 외부 도구 메뉴에다가 등록해 놓을 만한 물건이다. 내가 코딩해서 빌드한 바이너리에 대해 곧장 DllRegisterServer를 호출해서 등록을 할 수 있게 말이다.

얘는 운영체제의 system 디렉터리에 32비트용과 64비트용이 제각각 존재하는데, 아무 프로그램에다가 32/64비트 아무 바이너리를 넘겨 줘도 잘 동작한다. 64비트 운영체제를 개발하면서 regsvr32에 그 정도 유도리는 같이 구현돼 있다.
COM 객체의 등록이나 제거를 위해서는 HEKY_CLASSES_ROOT 레지스트리를 건드려야 하니, 관리자 권한 실행은 언제나 필수이다.

끝으로, 독립된 프로그램은 아니지만.. Visual Studio의 200x대 버전부터는 (8) 전용 명령 프롬프트를 띄우는 기능이 외부 도구 메뉴에 등록돼 있다. 즉, 어느 디렉터리에서나 CL 같은 컴파일러와 링커를 띄울 수 있도록 LIB, INCLUDE, VCINSTALLDIR 같은 환경 변수들이 맞춰진 명령 프롬프트이다.

Visual Studio가 버전이 올라갈수록 IDE와 플랫폼 SDK가 분리되고, IDE와 컴파일러 툴킷 계층이 분리되고, 한 컴퓨터에서 여러 버전이 설치되는 것에도 유연하게 대응 가능하게 변모해 온 것은 매우 바람직한 현상으로 보인다.

Posted by 사무엘

2018/11/28 08:34 2018/11/28 08:34
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1559

1. rc 파일의 유니코드화

Visual C++ 2008까지만 해도 안 그랬던 것 같은데.. 2010쯤부터는 새로 만드는 프로젝트들의 리소스 스크립트(*.rc) 파일의 기본 인코딩이 유니코드(UTF-16LE)로 바뀌었다는 걸 본인은 최근에야 알아차렸다. 어쩐지 구버전에서는 파일을 열지를 못하더라.
그러니 이런 rc 파일의 내부에는 #pragma code_page(949) 같은 구차한 지시문도 없다.

리소스는 Windows의 실행 파일 포맷 차원에서 유니코드인데, 그걸 생성하는 스크립트 파일은 왜 유니코드가 아닌지 본인은 오랫동안 의아하게 생각해 왔다. 물론, 다국어 리소스는 언어별로 다 따로 만들지, 한 리소스 내부에 갖가지 외국어가 섞여 들어갈 일은 거의 없기 때문에 이런 방식이 크게 문제 되지 않았을 뿐이다.

리소스 파일 관련 속성을 보면.. MFC 모드로 동작할지 말지를 지정하는 옵션이 있다. 이건 rc 파일 내부에 저장되는 추가적인 옵션/플래그 같은 건 아니고, 그냥 Visual C++ IDE의 동작 방식을 결정하는 것이다. 프로젝트 내부의 설정으로 저장되는 것 같다.

이 옵션의 지정 여부에 따라 대표적으로 달라지는 것은 초기에 콤보박스 내부에다 집어넣을 데이터 목록을 지정하는 기능이다. 이것은 Windows API가 자동으로 해 주는 게 아니라, MFC가 추가적으로 구현해 놓은 기능이다. 그렇기 때문에 리소스 파일이 MFC mode로 지정돼 있지 않으면 해당 기능을 사용할 수 없다.

리소스 파일을 들여다 본 분은 아시겠지만 이 초기화 데이터는 Dialog 리소스 템플릿 안에 내장돼 있는 게 아니라, 240 (RC_DLGINIT)이라는 custom 리소스 타입에 따로 들어있다.
할 거면 리스트박스에다가도 같은 기능을 넣어 줄 것이지 왜 하필 콤보박스에다가만 넣었는지는 잘 모르겠다.

굳이 MFC를 사용하는 프로젝트가 아니더라도, Visual C++ 리소스 에디터가 저장해 놓은 대로 콤보박스를 초기화하는 기능을 내 프로그램에다가 넣고 싶으면 MFC의 소스 코드를 참고해서 직접 구현하면 된다.

그런데 리소스 스크립트 전체의 포맷은 유니코드로 바뀌었음에도 불구하고, 이 초기화 데이터는 기존 코드/프로그램과의 호환성 문제 때문에 여전히 CP_ACP이니 참 애석하다.
MFC 소스를 보면 문자열을 CB_ADDSTRING 메시지로 등록하는 부분에서 의도적으로 SendDlgItemMessageA라고 A 버전을 호출한 것을 볼 수 있다.

Windows는 여러 모로 UTF-8과는 친화적이지 않은 게 느껴진다. "UTF-8 + 32비트 wchar_t"를 전혀 찾아볼 수 없는 환경이다.

2. 소스 코드의 유니코드화, UTF-8 지원

앞서 언급한 바와 같이, Windows는 유니코드 계열이건 그렇지 않은 계열이건 2바이트(...) 단위의 문자 인코딩을 굉장히 좋아해서 전통적으로 UTF-8에 친화적이지 않았다. 친화도는 "UTF-16 > BOM 있는 UTF-8 > BOM 없는 UTF-8"의 순이다.

물론 Visual Studio의 경우, 먼 옛날의 200x대부터 소스 코드를 UTF-8 방식으로 불러들이고 저장하고, 파일 형식을 자동 감지하는 것 자체는 잘 지원한다. 하지만 한글 같은 게 전혀 없고 BOM도 없어서 일반 ANSI 인코딩과 아무 차이가 없는 파일의 경우 기본적으로 UTF-8이 아니라 ANSI 인코딩으로 간주하며, 디폴트 인코딩 자체를 UTF-8로 맞추는 기능은 기본적으로 제공되는 않는다는 점이 아쉽다.

다시 말해 새로 만드는 소스 코드라든가, 처음엔 한글이 없었다가 나중에 한글· 한자가 추가된 파일의 경우 실수로 여전히 cp949 같은 재래식 인코딩으로 파일이 저장될 여지가 있다는 것이다. 일일이 저장 옵션을 바꿔 줘야 된다.

Windows 환경에서 UTF-8 인코딩의 C++ 소스 코드는 (1) 주석을 다국어로 작성해서 온전히 보존 가능하고, (2) 동일 파일을 xcode 같은 타 OS에서도 깨지는 문자 없이 공유 가능하다는 것 정도에나 의미를 둬야 할 것이다.
L"" 문자열이 아니라 printf나 WM_SETTEXTA 같은 곳에 쓰이는 "" 문자열은 소스 코드의 인코딩이 무엇이냐에 따라 값 자체가 달라지기 때문이다.

Windows가 진정한 UTF-8 친화적인 환경이 되려면 시스템 코드 페이지 자체를 65001 UTF-8로 지정할 수 있고 명령 프롬프트에서도 그게 지원돼야 할 것이다. 하지만 UTF-8은 (1) 특정 로케일이나 언어에 속해 있지 않다는 점, 그리고 (2) 기존 multibyte 인코딩들과는 달리 한 글자가 3바이트 이상의 길이를 가질 수 있다는 점으로 인해 Windows는 이를 지원하지 않고 있었다. 그래도 요즘 마소가 워낙 파격적으로 변화하고 있고 Windows 10로 하루가 다르게 달라지고 있으니 이런 금기가 앞으로 깨지지 말라는 법도 없을 것 같다.

3. 디버그 로그의 유니코드화

Windows가 10이 나온 이래로 많이 바뀌긴 했다. 완전히 새로운 기능들만 추가되는 게 아니라, 이미 만들어졌고 앞으로 영원히 바뀌지 않을 것처럼 여겨지던 기능까지도 말이다.

가령, OutputDebugString 함수는 통상적인 다른 API 함수들과는 반대로, W가 내부적으로 A 버전을 호출하고 유니코드를 수십 년째 전혀 지원하지 않고 있었다. 이 때문에 굉장히 불편했는데.. 언제부턴가 Visual C++의 디버그 로그 출력창에 surrogate(확장 평면)까지 포함해 유니코드 문자열이 안 깨지고 온전히 찍혀 나오는 걸 보고 개인적으로 굉장히 놀랐다. 나중에 개선된 거라고 한다.

그리고 Windows의 에디트 컨트롤은 개행 문자를 오로지 \r\n밖에 지원하지 않기 때문에 메모장에서 유닉스(\n) 방식의 파일을 열면 텍스트가 개행 없이 한 줄에 몽땅 몰아서 출력되는 문제가 있었다.
그런데 이것도 Windows 10의 최신 업데이트에서는 개선되어서 \n도 제대로 표시 가능하게 되었다.
수백 KB~수 MB 이상 큰 파일을 여는 게 너무 오래 걸리던 고질적인 문제가 개선된 데 이어, 또 장족의 발전이 이뤄졌다.

Windows가 제공하는 유니코드 관련 API 중에는 주어진 텍스트가 유니코드 인코딩처럼 보이는지 판별하는 IsTextUnicode도 있고, 한자 간체-번체를 전환하는 LCMapString 같은 함수도 있다.
IsTextUnicode의 경우, 1바이트 아스키 알파벳 2개로만 이뤄진 아주 짧은 텍스트를 UTF-16 한자 하나로 오진(?)하는 문제가 있어서 내 기억이 맞다면 한 2000년대 Windows XP 시절에 버그 패치가 행해지기도 했다. 사실, 이런 휴리스틱은 정답이 딱 떨어지는 문제가 아니기도 하다.

저런 식으로 알고리즘이 일부 개정되는 경우가 있긴 했지만, Windows에서 데이터 기반으로 동작하는 유니코드 API들은 대개가 한 1990년대 말, 겨우 Windows NT4와 유니코드 2.0 정도나 있던 시절 이후로 그 데이터와 알고리즘이 업데이트 된 내역이 없다.

그래서 간체/번체를 변환하는 테이블에 등재된 한자는 내가 세어 본 기억에 맞다면 2300개 남짓밖에 되지 않으며, IsTextUnicode도 21세기 이후에 유니코드에 새로 추가된 수많은 글자들까지 고려하지는 않고 동작한다.
이 와중에 그래도 Windows 10에 와서 OutputDebugStringW가 제 구실을 하기 시작하고 메모장의 동작이 바뀌기도 한 것이 놀랍게 느껴진다.

한편, UTF-8은 그래도 첫 바이트로 등장할 만한 글자와 그 이후 바이트로 등장할 만한 글자가 형태적으로 무조건 정해져 있다. 그래서 데이터니 통계니 휴리스틱 없이도 자기가 UTF-8이라는 게 딱 티가 나며, UTF-8을 타 인코딩으로 오인한다거나 타 인코딩을 UTF-8로 오인할 가능성이 거의 없는 것이 매우 큰 장점이다.

4. 문자형의 부호 문제

컴퓨터에서 문자열의 각 문자를 구성하는 단위 타입으로는 전통적으로 char가 쓰여 왔다. 그러다가 유니코드가 등장하면서 이보다 공간이 더 커진 wchar_t가 도입되었으며, 언어 표준까지 채택됐다. 값을 다룰 때는 같은 크기의 정수와 다를 게 없지만, 포인터로는 서로 곧장 호환되지 않게 type-safety도 강화되었다.

그런데, 처음에 1바이트짜리 문자열의 기본 타입을 왜 괜히 부호 있는 정수형인 char로 잡았을까 하는 게 아쉬움으로 남는다. 물론 매번 unsigned를 붙이거나 typedef를 하는 건 귀찮은 일이며, 영미권에서는 문자 집합 크기가 7비트만으로도 충분했다는 그 상황은 이해한다. 하지만 문자 코드를 저장할 때는 애초에 부호 따위는 전혀 필요하지 않다. char보다 더 큰 wchar_t도 결코 부호 있는 정수형과 대응하지 않는다.

크기가 겨우 8비트밖에 안 되는 '바이트'는 양수 음수를 따지기에는 너무 작은 타입이기도 하다. 파일이나 메모리 데이터를 바이트 단위로 읽으면서 2의 보수 기반의 부호를 따질 일이 과연 있던가..?

이런 구조로 인해.. UTF-8이건 -16이건 -32이건 모두 대응 가능한 문자열 템플릿을 만들 때, char형에 대해서만 코드값 범위 검사를 할 때 예외를 둬야 하는 불편한 상황도 생긴다. 템플릿 인자로 주어진 어떤 타입에 대해서, 크기는 동일하면서 부호만 없는 타입을 자동으로 되돌리는 방법이 C++ 언어에는 존재하지 않기 때문이다.

그러니 다른 타입에서는 다 간단하게 >= 0x80을 검사하면 되는데, char만 <0을 봐야 한다. 이런 거 로직이 꼬이면 모든 타입에서 0xF0이 저장되어야 하는데 딴 타입에서는 0xFFF0이 저장되는 식의 문제도 생길 수 있다. 그렇다고 임의로 제일 큰 부호 없는 정수형으로 typecast를 하는 건 무식한 짓이다.

회사에서 일을 하다가 하루는 이게 너무 짜증 나서 std::basic_string<unsigned char>로부터 상속받은 클래스를 새로 만들어 버렸다. 베이스 클래스의 명칭이 딱 한 토큰 한 단어가 아니라 저렇게 템플릿 인자가 덕지덕지 붙은 형태인 게 특이했다만.. 생성자 함수에서 기반 클래스를 호출할 때는 __super를 쓰지도 못하더라.

내부적으로는 모든 처리를 unsigned char를 기준으로 하는데, 생성자와 덧셈 연산, 형변환 연산에서만 부호 있는 const char*를 추가로 지원하는 놈을 구현하는 게 목적이었다. 이런 생각을 나만 한 건 절대 아닐 텐데..
개인적으로 + 연산자를 만드는 부분에서 좀 헤맸었다. 얘는 +=와 달리 완전한 내 객체를 새로 만들어서 되돌리는 것이기 때문에 컴파일러 에러를 피해서 제대로 구현하는 게 생각보다 nasty했다. 그렇다고 도저히 못 할 정도는 아니었고.. 뭐 그랬다.

그러고 보니 Java는 기본적으로 부호 있는 정수형만 제공하지만, char만은 문자 저장용으로 부호 없는 16비트 정수를 쓰는 걸로 본인은 알고 있다.
그런데 얘도 그 크기로는 BMP 영역 밖은 표현할 수 없다. Java 언어가 처음으로 설계되고 만들어지던 때는 1990년대 중반으로, 아직 유니코드 2.0과 확장 평면 같은 개념이 도입되기 전이었다.
결국 범용적인 글자 하나를 나타내려면 부호 있는 정수인 int를 써야 한다. 상황이 좀 복잡하다..;;

5. Windows 문자열 변환 함수의 함정

Windows API 중에서 WideCharToMultiByte와 MultiByteToWideChar는 운영체제가 내부적으로 사용하는 2바이트 단위 UTF-16 방식의 문자열과 타 인코딩 문자열(UTF-8, CP949 등..)을 서로 변환하는 고전적인 함수이다.

이 두 함수는 크게 두 가지 모드로 동작한다. (1) 사용자가 넘겨준 문자열 버퍼 포인터에다가 변환을 수행하거나.. (2) 아니면 그렇게 write 동작을 하지는 않고, 이 원본 문자열을 몽땅 변환하는 데 필요한 버퍼의 크기만을 되돌린다.
일단 (1)처럼 동작하기 시작했는데 사용자가 넘겨준 버퍼 크기가 원본 문자열을 모두 변환해 넣기에 충분하지 못하다면 함수의 실행은 실패하고 0이 돌아온다.

이때도 기존 버퍼의 크기만치 변환을 하다가 만 결과는 확인할 수 있다. 하지만 원본 문자열의 어느 지점까지 변환하다가 끊겼는지를 이 함수가 알려 주지는 않는다. 그렇기 때문에 그 정보를 유의미하게 활용하기 어려우며, 이는 개인적으로 아쉽게 생각하는 점이다.

그러니 버퍼가 얼마나 필요한지 알 수 없는 일반적인 경우라면, WideCharToMultiByte를 기준으로 함수를 사용하는 방식은 이런 형태가 된다. 함수를 두 번 호출하게 된다.

const wchar *pSrcBuf = L"....";
int len = WideCharToMultiByte(CP_***, pSrcBuf, -1, NULL, 0, ...); //(2) 크기 측정
char *pTgtBuf = new char[len];
WideCharToMultiByte(CP_***, pSrcBuf, -1, pTgtBuf, len, ...); //(1) 실제로 변환
...
delete []pTgtBuf;

그런데, 이들 함수가 (1)과 (2) 중 어느 모드로 동작할지 결정하는 기준은 버퍼 포인터(pTgtBuf)가 아니라, 버퍼의 크기(len)이다.
보통은 크기를 측정할 때 포인터도 NULL로 주고 크기도 0으로 주니, 함수가 내부적으로 둘 중에 뭘 기준으로 동작하든 크게 문제될 건 없다.

하지만 한 버퍼에다가 여러 문자열을 변환한 결과를 취합한다거나 해서 포인터는 NULL이 아닌데 남은 크기가 우연히도 딱 0이 될 수 있는 상황이라면 주의해야 한다. 포인터와 버퍼 크기가 pTgtBuf + pos, ARRAYSIZE(pTgtBuf) - pos 이런 식으로 정해진다거나 할 때 말이다.

저 식에서 pos가 우연히도 배열의 끝에 도달했다면, 남은 크기가 0이니까 프로그래머가 의도하는 건 이 함수의 실행이 무조건 실패하고 0이 돌아오는 것이다. 그럼 프로그램은 버퍼 공간이 부족해졌다는 걸 인지하여 지금까지 쌓인 버퍼 내용을 딴 데로 flush한 뒤, 변환을 재시도하면 된다.

하지만 이 함수는 프로그래머가 의도한 것처럼 동작하지 않는다. 문자열 변환을 하지 않지만, 마치 실행에 성공한 것처럼 변환에 필요한 버퍼 크기를 되돌린다. 쉽게 말해 "(1)에 대한 실패"가 아니라 "(2)에 대한 성공"으로 처리된다는 것이다.

이 문제를 피하려면, 버퍼를 관리하는 프로그램은 WideChar...함수가 실패했을 때뿐만 아니라 포인터가 버퍼의 끝에 도달했는지의 여부도 따로 체크해야 하며, 그 경우 버퍼를 flush해 줘야 한다. 변환 함수가 후자까지 같이 체크해 주지 않기 때문이다.
날개셋 편집기가 9.5의 이전 버전에, 바로 이것 때문에 대용량 파일을 저장할 때 낮은 확률로 데이터를 날려먹는 버그가 있었다.

Windows API가 입력값이 0일 때에 대해 일관된 유도리가 없어서 불편한 예가 GDI 함수에도 있다.
주어진 문자열의 픽셀 단위 길이와 높이를 구하는 GetTextExtentPoint32의 경우, 문자열 길이에다 0을 주면 가로는 0이고 세로 크기만 좀 구해 줬으면 좋겠는데.. 그러질 않고 그냥 실행이 실패해 버린다.
높이만 구하고 싶으면, 공백 하나라도 dummy로 전해 준 뒤 리턴값의 cx 부분은 무시하고 cy를 사용해야 한다.

Posted by 사무엘

2018/09/29 08:35 2018/09/29 08:35
, ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/1537

1. R-value 임시 객체를 일반 참조자 형태로 함수 인자로 전달하기

C/C++ 프로그래밍 요소 중에는 잘 알다시피 & 연산자를 이용해서 주소를 추출할 수 있으며 값 대입이 가능한 L-value라는 게 있고, 그렇지 않고 값 자체만을 나타내는 R-value라는 게 있다.
C언어 시절에는 R-value라는 게 함수의 리턴값이 아니면 프로그램 소스 코드 차원에서 리터럴 형태로 하드코딩되는 숫자· 문자열밖에 없었다. 하지만 C++로 와서는 생성자와 소멸자가 호출되는 클래스에 대한 임시 인스턴스 개체도 그런 범주에 속할 수 있게 되었다.

다른 변수에 딱히 대입되지 않은 함수의 리턴값 내지, 변수로 선언되지 않고 함수의 인자에다가 곧장 Class_name(constructor_arg) 이런 형태로 명시해 준 객체 선언은 모두 R-value이다.

R-value를 그 타입에 대한 포인터형으로 함수 인자로 전달하는 것이야 가능하지 않다. 주소를 얻을 수 없으니 말이다. C/C++에 &100 이런 문법 같은 건 존재하지 않는다.
하지만 참조자는 외형상 값으로 전달하는 것과 동일하기 때문에 저런 제약이 없다. 그런데 참조자가 내부적으로 하는 일은 포인터와 완전히 동일하다. 그럼 뭔가 모순/딜레마가 발생하지 않을까?

이런 오류를 막기 위해, R-value를 일반 참조자형으로 전달하는 것은 원래 금지되어 있다. 참조자 내지 포인터는 자신이 가리키는 대상이 당연히 대입 가능한 L-value라는 것을 전제로 깔기 때문이다.
임시 개체를 함수 인자로 전하려면..

  • 그냥 값으로 전달해야 한다. 이 방법은 객체의 크기가 크거나 복사 생성자에서 하는 일이 많다면, 성능 오버헤드가 우려된다.
  • 아니면 const 참조자형으로 전달해야 한다. R-value는 대입이고 변경이고 불가능한 놈이니 const 제약을 줘서 취급하는 것이 이치에 맞다.
  • 아니면 얘는 임시 R-value이지만 값이 보존되지 않아도 아무 상관 없다는 표식이 붙은, C++11의 R-value 참조자 &&를 써서 전달하면 된다.

사실, 정수 같은 아주 작고 간단한 타입이라면 모를까, 커다란 객체는 임시 객체라 해도 어차피 자신만의 주소를 갖고 있는 것이나 다름없다.
그래서 그런지 Visual C++은 200x 언제부턴가 R-value를 통째로 일반 참조자로 전달하는 것을 허용하기 시작했다. xcode에서는 에러가 나지만 Visual C++은 컴파일 된다. 이렇게 해 주는 게 특히 템플릿을 많이 쓸 때 더 편하긴 하다. VC++도 내 기억으로 6.0 시절에는 안 이랬다.

사실, 이걸 허용하나 안 하나.. &로 전달하나 &&로 전달하나 컴파일러 입장에서는 크게 달라지는 게 없다. 템플릿 인자로 들어간 타입이 객체가 아니라 정수라면 좀 문제가 되겠지만 어차피 C++은 서로 다른 템플릿 인자에 대해서 같은 템플릿을 매번 새로 컴파일 하면서 코드 생성과 최적화를 매번 새로 하는 불편한(?) 언어이다. 그러니 각 상황별로 따로 처리해 주면 된다.
물론 R-value 참조자가 C++에 좀 일찍 도입됐다면 Visual C++이 저런 편법을 구현하지 않아도 됐을 것 같아 보인다.

2. C++에서 함수 선언에다 리턴 타입 생략하기

엄청 옛날 유물 얘기이긴 하지만.. Visual C++이 2003 버전까지는 아래 코드가 컴파일이 됐었다는 걸 우연한 계기로 뒤늦게 알게 됐다. 바로 함수를 선언· 정의할 때 리턴 타입을 생략하는 것 말이다.

class foo {
public:
    bar(int x); //생성자나 소멸자가 아닌데 클래스 멤버 함수의 리턴 타입을 생략!! 여기서는 경고는 뜸
};

foo::bar(int x) //여기서도 생략했지만 그래도 int로 자동 간주됨
{
    return x*x;
}

main() { return 0; }

C++은 본격적인 클래스 다루는 거 말고 일상적인 코딩 분야에서 C와 달라지는 차이점이 몇 가지 있다. 그 차이점은 대부분 C보다 type-safety가 더 강화되고 엄격해진다는 것이다.

  • 임의의 type의 포인터에다가 void*를 대입하려면 형변환 연산자 필수. (경고이던 것이 에러로)
  • 함수도 prototype을 반드시 미리 선언해 놓고 사용해야 함. (경고이던 것이 에러로)
  • 그리고 함수의 선언이나 정의에서 리턴 타입을 생략할 수 없음. 한편, 인자 목록에서 ()은 그냥 임의의 함수가 아니라 인자가 아무것도 없는 함수, 즉 void 단독과 동치로 딱 정립됨.
  • 그 대신 C++은 지역 변수(+객체)를 굳이 {} 블록의 앞부분이 아니라 실행문 뒤에 아무 데서나 선언해도 된다.

이 개념이 어릴 적부터 머리에 완전히 박혀 있었는데, VC6도 아니고 2003의 컴파일러에는 C의 잔재가 아직 저렇게 남아 있었구나. 몰랐다.
cpp 소스에다가도 int main()대신 main()이라고 함수를 만들어도 되고, 심지어 클래스의 멤버 함수에다가도 리턴 타입을 생략할 수 있었다니... 완전 적응 안 된다.

물론 익명 함수(일명 람다)를 선언할 때는 꼭 리턴값 타입을 안 써 줘도 된다. void나 int 같은 간단한 것은 함수 내부의 return문을 통해서 컴파일러가 그럭저럭 유추해 주기 때문이다.
함수형이라는 완전히 다르고 새로운 패러다임이 C++에 한참 뒤에 추가되다 보니 일관성이 깨졌다면 깨진 듯이 보인다.

3. C/C++ 컴파일러 및 언어 문법 자체의 변화

내 경험상, 지난 2000년대에는 Visual C++ 컴파일러의 버전이 올라가면서 명칭의 scope 인식이 좀 더 유연해지곤 했다.
가령, 클래스 A의 내부에 선언된 구조체 B를 인자로 받는 멤버 함수 C의 경우, C의 몸체를 외부에다 정의할 때 프로토타입 부분에서 B를 꼭 A::B라고 써 줘야 됐지만, VC6 이후 버전부터는 그냥 B라고만 써도 되게 됐다.

람다가 최초로 지원되기 시작한 미래의 VC++ 2010에도 비슷한 한계가 있었다.
클래스 멤버 함수 내부에서 선언된 람다 안에서는 그 클래스가 자체적으로 정의해 놓은 타입이나 enum값에 곧장 접근이 안 됐다. A::value 이런 식으로 써 줘야 했는데.. 2012와 그 후대 버전부터는 곧바로 value라고만 써도 되게 바뀌었다. 2012부터 람다가 함수 포인터로 cast도 가능해졌고 말이다.

내 기억이 맞다면 템플릿 인자가 공급된 템플릿 함수가 함수 포인터로 곧장 연결되는 게 VC++ 2003쯤부터 가능해졌다. 상식적으로 당연히 가능해야 할 것 같지만 VC6 시절에는 가능하지 않았었다.
2000년대가 generic이라면 2010년대는 functional 프로그래밍이 도입된 셈이다.

아.. 또 무슨 얘기를 더 할 수 있을까?
C는 옛날에, 원래 처음에 1980년대까지는 K&R 문법인지 뭔지 해서 함수의 인자들의 타입을 다음과 같이 지정할 수도 있었다고 한다.

int foo(a, b) int a; float b; { return 0; }

같은 명칭을 이중으로 명시하고 체크하느라 괜히 분량 길어지고 파싱이 힘들어지고..
무슨 Ada나 Objective C처럼 함수를 호출할 때 foo(b=20, a=1)처럼 인자들 자체의 명칭을 지정하는 것도 아닌데..
저 문법이 도대체 무슨 의미가 있고 왜 저런 게 존재했는지 나로서는 좀 이해가 안 된다. C는 파이썬 같은 dynamic 타입 언어도 전혀 아닌데 말이다.

C 말고 C++도 1980년대까지는 문법이나 라이브러리가 제대로 표준화되지도 않았었고, 지금으로서는 상상하기 어려운 관행이 많았다고 한다. 유명한 예로는 소멸자가 존재하는 클래스 객체들의 배열을 delete 연산자로 제거할 때는 원소 개수를 수동으로 공급해 줘야 했다거나, static 멤버를 선언한 뒤에 굳이 몸체를 또 정의할 필요가 없었다거나 하는 것 말이다.

사실, 후자의 경우 또 정의할 필요가 없는 게 더 직관적이고 프로그래머의 입장에서 편하긴 하지만.. 컴파일러와 링커의 입장에서는 생성자 함수를 호출하는 실행문이 추가되는 부분이 따로 들어가는 게 직관적이니 불가피하게 생긴 변화이긴 하다.
이런 저런 변화가 많은데 C++은 표준화 이전의 격변의 초창기 시절에 대한 자료를 구하기가 어려워 보인다.

객체를 생성하는데 메모리 주소는 정해져 있고 거기에다 생성자 함수만 호출해 주고 싶을 때..
지금이야 placement new라는 연산자가 라이브러리의 일부 겸 사실상 언어의 일부 요소로 정착해서 new(ptr) C라고 쓰면 되지만, 옛날에는 ptr->C::C() 이런 표기가 유행이었다. . ->를 이용해서 생성자와 소멸자 함수를 직접 호출하는 건 지금도 금지되어 있지는 않지만.. 사용이 권장되는 형태는 아닌 듯하다.

4. macOS와 Windows의 관점 차이

macOS의 GUI API(Cocoa)와 Windows의 GUI API는 뭐 뿌리나 설계 철학에서 같은 구석을 찾을 수 없을 정도로 완전히 다르다.
좌표를 지정하는데 화면이 수학 좌표계처럼 y축이 값이 커질수록 위로 올라가며, NSRect는 Windows의 RECT와 달리 한쪽이 절대좌표가 아니라 상대좌표이다. 즉, 두 개의 POINT로 구성된 게 아니라 POINT와 SIZE로 구성된 셈이다.

또한 Windows의 사고방식으로는 도저히 상상할 수 없는 float 부동소수점을 남발하며, 처음부터 픽셀이라는 개념은 잊고 추상적인 좌표계를 쓴다. 얘들은 이 정도로 장치· 해상도 독립적으로 시스템을 설계했으니 high DPI 지원도 Windows보다 더 유연하게 할 수 있었겠다는 생각이 들었다.

그리고 대화상자나 메뉴 같은 리소스를 관리하는 방식도 둘은 서로 다르다.
Windows는 철저하게 volatile하다. 이런 것은 생성되는 시점에서 매번 리소스로부터 처음부터 load와 copy, 초기화 작업이 반복되며, 사용자가 해당 창을 닫은 순간 메모리에서 완전히 사라진다.

그 반면, 맥은 그런 것들이 사용자가 닫은 뒤에도 이전 상태가 계속 남아 있다.
내가 뭔가 설정을 잘못 건드렸는지는 모르겠지만, Windows로 치면 [X]에 해당하는 닫기 버튼을 눌러서 대화상자를 닫았는데도 호락호락 창이 사라지지 않고 DestroyWindow보다는 ShowWindow(SW_HIDE)가 된 듯한 느낌? 대화상자의 이전 상태가 계속 살아 있는 것 같다.

특히 메뉴의 경우 Windows는 내부 상태가 완전히 일회용이다. 메뉴가 화면에 짠 표시될 때가 되면(우클릭, Alt+단축키 등) 매번 리소스로부터 데이터가 로딩된 뒤, 체크 또는 흐리게 같은 다이나믹한 속성은 그때 그때 새로 부여된다.
그 반면 mac에서는 메뉴가 화면에 표시되지 않을 때에도 내부 상태가 쭉 관리되고 있는 게 무척 신기했다.

그러니 개발자는 시간과 여유만 된다면 프로그래밍 언어와 환경을 많이 알면 사고의 범위가 넓어질 수 있고 각각의 환경을 설계한 사람의 관점과 심정을 이해할 수도 있다.
하지만 뒤집어 말하면 프로그래머는 은퇴하는 마지막 순간까지도 공부하고 뒤따라가야 할 게 너무 많다.. -_-;; 이거 하나 적응했다 싶으면 그새 또 다른 새로운 게 튀어나와서 그거 스펙을 또 공부해야 된다.

Posted by 사무엘

2018/09/08 08:37 2018/09/08 08:37
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/1530

« Previous : 1 : 2 : 3 : 4 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2021/04   »
        1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30  

Site Stats

Total hits:
1581012
Today:
220
Yesterday:
404