1. 프로젝트 -- IDE의 관점과 빌드 스크립트의 관점

C/C++ 빌드 시스템에서 프로젝트란, 한 바이너리.. exe, dll, lib, so, a, out 따위를 만들어 내기 위한 1개 이상의 파일들의 묶음을 말한다. 그리고 여러 바이너리들을 생성하는 여러 프로젝트의 묶음을 Visual Studio 용어로는 솔루션이라고 부른다.

프로젝트를 구성하는 파일 중, 컴파일러가 처리하는 각각의 소스 파일(c/cc/cpp)은 '번역 단위'(translation unit)이라고 불린다. 1개의 번역 단위는 1개의 obj 파일로 바뀌게 된다.
그런데 요즘은 프로그래머의 편의와 작업 생산성을 위해 통합 개발 환경(IDE)이란 게 즐겨 쓰이며, 이런 IDE에서 취급하는 프로젝트는 make 같은 재래식 툴에서 취급하는 빌드 스크립트(makefile 같은)와는 완전히 일치하지 않는 관계이다.

프로젝트 파일에 들어있는 정보를 기계적으로 추출해서 makefile을 생성하는 것은 비교적 쉽게 가능하다. 그러나 makefile로부터 역으로 IDE용 프로젝트 파일을 재구성하는 것은 더 귀찮고 번거롭다.
프로젝트 파일에는 빌드가 아닌 IDE 내부에서 의미를 갖는 각종 설정 정보들이 더 들어있으며, makefile은 절차형 스크립트로서 프로젝트 파일만으로 표현할 수 없는 각종 조건부 빌드 로직이 들어있을 수 있기 때문이다.

일례로, IDE의 프로젝트 파일에는 소스 파일들을 다단계 폴더 형태로 묶고 분류해서 표시하는 기능이 있다. 이런 계층 구조 정보는 전적으로 사용자의 편의를 위해 존재할 뿐, 빌드할 때는 전혀 쓰이지 않는다. 어차피 다 똑같이 일렬로 늘어놓아서 컴파일 하고 링커로 넘겨주는 파일들일 뿐이기 때문이다.

또한 이 계층 구조는 그 소스 파일들이 놓여 있는 디렉터리 구조와는 전혀 무관하게 지정 가능하다. 하지만 현실에서는 프로젝트에서의 파일 grouping을 실제 디렉터리 구조와 동일하게 해 주는 게 사람을 덜 헷갈리게 하고 좋을 것이다. 특히 여러 사람이 유지 보수하는 프로젝트라면 더욱 말이다.

한 프로젝트를 구성하는 소스 코드들이 반드시 동일한 디렉터리에 있어야 할 필요는 없지만.. 특별한 사정이 없는 한 컴파일된 출력 파일은 오로지 한 곳에서만 생성된다.
그렇기 때문에 서로 다른 디렉터리에 있더라도 한 프로젝트에 이름이 동일한 파일이 여럿 있지는 않는 게 좋다.

오픈소스 DB 라이브러리인 sqlite는.. amalgamation이라고 해서 4MB짜리.. 거대한 sqlite3.c 파일 하나로 라이브러리 전체의 기능을 제공하는 엄청난 용자짓도 하던데..;;; 이건 극단적인 예이다.
들고 다니고 관리하기 편하고 빌드가 깔끔하고 최적화가 잘 되는 장점이 있지만, 컴파일러나 IDE가 파싱 하다가 체할 수 있고 코드 분석이나 디버깅이 잘 안 되는 단점도 있을 수 있다. 요즘도 보수적인 IDE나 디버깅 업계에서는 줄 수가 64K를 넘는 소스 파일을 좋아하지 않는 편이다.;;.

2. 정적 분석

어떤 프로그램에서 구조적인 메모리 오류나 보안 결함을 찾아내는 검증 도구 내지 방법은 크게 ‘동적 분석’과 ‘정적 분석’으로 나뉜다.
전자는 빌드한 프로그램을 가상의 샌드박스 안에서 직접 실행해 보면서 문제점을 찾는다. 그러나 후자는 프로그램을 실행하지 않고 소스 코드만 쭉 훑으면서 문제점을 찾아 낸다. 둘은 손실 압축과 무손실 압축, 실시간 렌더링과 오프라인 렌더링만큼이나 서로 영역이 다르다.

서버처럼 무한 대기· 무한 루프를 돌며 반영구적으로 돌아가는 프로그램을 동적 분석으로 검증하는 건 쉽지 않다. 프로그램이 동일 지점에 돌아왔을 때 다른 메모리 문제 없이 항상성이 보장된다는 걸 겉으로 드러나는 상태만 보고 얼추 때려잡을 수밖에 없다.

그러나 정적 분석은 프로그램의 실행 형태와 전혀 무관하게.. 무한루프건 배배 꼬아 놓은 지수함수 시간 복잡도의 재귀호출이건 무관하게.. “코드의 양이 유한하다면 분석을 위한 시간 복잡도도 유한하다”, “동일한 코드를 컴파일하는 데 걸리는 시간의 최대 수십 배 정도”이니 신통하지 않을 수 없다.

물론 정적 분석은 100% 정확하지 못하며 오탐 오진도 많다.
그런데, 각종 구조체와 포인터를 넘나들면서 진짜 너무 복잡하게 꼬여 있는 메모리를 일일이 추적을 못 하는 건 차라리 수긍을 하겠다만.. 이거 뭐 사람만도 못한 너무 황당한 오진을 하거나 간단한 문제도 못 잡아 내는 경우가 있어서 좀 아쉬웠다.

정적 분석은 그 정의상 프로그램을 “실행해 보지 않고” 코드를 분석해 주는데..
개발툴과 연계해서 “빌드는 같이 하면서” 문제를 추적하는 놈이 있는가 하면, 빌드조차 없이 진짜 코드 외형만 들여다보고 분석하는 놈도 있는 것 같다. 둘은 개발 이념이 서로 다르다.
후자가 정확도가 더 떨어지겠지만, 그래도 사용하기는 더 쉽다. 프로젝트나 makefile 세팅 없이 그냥 방대한 h와 cpp/c 묶음을 압축해서 던져 주기만 하면 분석이 되기 때문이다. 마치 Soure Insight와 비슷한 유도리가 있다.

솔직히 정적 분석을 위해서는 코드가 특정 플랫폼용으로 반드시 빌드가 돼야 할 필요가 없을 것이다. 가령, 32비트에서는 괜찮은데 64비트에서만 메모리 오프셋 문제를 일으키는 코드라면.. 그건 어차피 이식성 문제가 있는 코드이니 정적 분석 툴이 지적해 줘야 할 것이다.

내가 C/C++ 정적 분석으로부터 기대하는 아이템들은 다음과 같은 것들이다. 그런데 이것도 생각보다 스펙트럼이 다양한 것 같다.

  • memcpy, malloc 같은 함수에서 버퍼 크기 계산 잘못한 것, 문자열의 경우 null문자 공간을 빼먹은 것, 0초기화를 하지 않은 것 등등 (C 코드 한정.. 제일 지저분)
  • 함수가 자기 지역변수의 주소를 리턴
  • memory leak 내지 dangling pointer 가능성이 있는 것
  • C++에서 아직 초기화되지 않은 멤버 변수를 다른 멤버의 초기화에 동원하는 것 (이거 굉장히 교묘한 실수인데 왜 컴파일러에서 지적해 주지 않을까?)
  • a=a++ 같은 이식성 떨어지는 코드, 잠재적인 코딩 실수

3. #include의 미묘한 면모

C/C++에서 #include가 하는 일은 말 그대로 다른 텍스트 파일을 현재 컴파일 중인 번역 단위에다가 끌어오는 게 전부이다. 외부 패키지나 라이브러리를 지정하는 기능이 없다. C/C++에는 Java의 import, C#의 using 같은 깔끔한 명령이 없다.
그 대신, #include를 남용하면 프로젝트에 정식으로 포함되어 있지 않은 파일을 끌어들여서 이에 대한 의존도를 생성할 수 있다.

개인적으로는 <xxx>가 아니라 "xxx" 형태의 include는.. 컴파일러가 프로젝트에 포함돼 있는 파일만 쓰도록 하고, 프로젝트에 없으면 파일이 디스크 상에 존재하더라도 없다는 에러를 내게 하는.. 그런 옵션이 좀 있었으면 좋겠다.
왜냐하면 의도하지 않았던 파일이 잘못 인클루드 되는 바람에 컴파일러가 난독증을 일으키고 사람은 사람대로 빡치는 일도 얼마든지 있을 수 있기 때문이다.
또한, 프로젝트에 포함되지 않은 채 #include 된 파일은 수정됐어도 걔를 #include하는 소스가 고쳐지지 않았다면 재컴파일 되지 않아서 다른 오동작을 유발할 수도 있다.

#define뿐만 아니라 #include로도.. 파일 내용 전체를 꼼꼼하게 파싱하지 않고 편의 시설을 제공하는(syntax coloring, 간단한 문법 체크, 선언/정의로 가기, 함수 목록 추출 따위) IDE 에디터를 농락하고 오동작을 유발할 수 있다.
가령, "}" 요 문자 하나만 달랑 들어있는 소스 파일을 하나 만든 뒤,

void func
{
  ......
#include "right_curling_bracket.c"

이렇게만 하면 얘는 문법에 맞는 코드가 된다.
또한, 따옴표로 둘러싸인 문자열을 잔뜩 넣은 뒤,

static const char BIG_STRING_DATA[] =
  "XXXXX"
#include "more_string_dadta.c"
  "ZZZ";

이런 식으로.. 거대한 테이블 데이터의 내용을 외부 파일 인클루드를 통해 조달할 수도 있다.
단지, #include는 자기 안의 코드만 대치 가능할 뿐, 같은 전처리기의 레벨을 넘나들지는 못한다. 즉,

#ifdef
#include "file_containing_sharp_endif.c"

이렇게 때우는 건 허용되지 않는다. 저 #if에 상응하는 #else나 #endif 따위는 반드시 지금 소스 파일에 존재해야 한다.

끝으로.. #include 대상인 "xxx"나 <yyy>는 C언어의 관할을 받는 문자열 리터럴이 아니다. 그렇기 때문에 \ 탈출문자가 적용되지 않으며, 디렉터리를 표현할 때 역슬래시를 두 번 \\ 찍을 필요가 없다. 사실은 Windows건 어디에서건 더 보편적인 / 를 쓰는 게 더 좋을 것이다.

#include 대상으로 매크로 상수를 지정해 줘도 된다. 이걸 사용한 예는 본인의 경험으로는 FreeType 라이브러리가 지금까지 유일하다.
다만, #include 경로는 C 문자열 리터럴이 아닌 관계로, "aaa" "bbb" 라고 끊어서 썼을 때 자동으로 "aaabbb"라고 이어지는 처리도 되지 않는다. 이런 식의 변태적인(?) 활용은 가능하지 않다는 걸 유의하자.

4. 빌드 절차의 디버깅

뭔가.. 빌드 스크립트와 컴파일러의 동작을 디버깅 하는 기능이 좀 있었으면 좋겠다.
breakpoint를 잡고 나서 F5 Run을 하는 게 아니라, F7 '빌드'를 누른다.
일반적인 디버깅이라면 빌드된 프로그램이 그 지점을 실행할 때 break가 걸리겠지만, 이때는 컴파일러가 그 지점을 읽기 시작했을 때 break가 걸린다.

break가 걸리고 나면 이 시점에서 현재 정의돼 있는 #define 심벌들을 몽땅 조회하고 실제 값과 정의된 곳(헤더 파일? 컴파일러 옵션?)을 추적할 수 있다. 치환 결과에 또 매크로가 들어있더라도 당연히 계속 까 볼 수 있다.
각종 #pragma 옵션이 지정된 내역, 옵션 스택, #line이 적용된 것도 당연히 확인 가능하다.

프로그램 실행 디버깅에서 step into / over / out이 있는 것처럼..
#include에 대해서는 마치 함수 호출처럼 step into를 할 수 있다. 어느 디렉터리에 있는 헤더 파일이 선택됐는지, 현재 컴파일러의 스택 상으로 include 깊이가 얼마나 되는지를 살펴볼 수 있다.
경우에 따라서는 <>, ""에 따라서 탐색 순서도 추적 가능하다. 요 디렉터리에 없어서 다음으로 이 디렉터리, 다음으로 저 디렉터리 같은 순이다.

#error나 #pragma warning 같은 건 아예 별도의 로그 창으로 찍히게 할 수도 있다.
흠, 좀 잉여력이 풍부해 보이긴 하지만, 그럴싸하지 않은가? =_=;;
웹브라우저에서 '개발자 모드'가 있는 것처럼.. 이런 기능이 있으면 개발자가 자기가 내력을 다 알지 못하는 방대한 프로젝트와 빌드 시스템에 처음 적응할 때 도움이 될 것 같다.

Posted by 사무엘

2023/03/13 08:35 2023/03/13 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2136


블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2023/03   »
      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 31  

Site Stats

Total hits:
2676169
Today:
737
Yesterday:
2124