간단한 인트린직 이야기

요즘 컴파일러에는 인트린직(intrinsic)이라고 정의된 내장 함수들의 집합이 있다. 그래서 그런 함수를 호출한 것은 실제 함수 호출이 아니라 특정 기계어 코드로 곧장 치환된다. 함수의 몸체가 직접 삽입된다는 점에서는 함수 인라이닝과 비슷하지만, 인트린직은 그 몸체가 컴파일러에 의해 내장되어 있으니 개념과 용도가 그것과는 살짝 다르다.

인트린직은 굳이 인라인 어셈블리 같은 거창한 문법 없이 간단한 C 함수 호출 스타일로 특정 기계어 인스트럭션을 곧장 집어넣거나 컴파일러의 확장 기능을 사용하기 위한 목적이 강하다. #pragma가 특수한 의미를 지닌 지시를 내리기만 하는 전처리기로 비실행문이라면, 인트린직은 실행문이다.

또한, 기존의 표준 C 함수가 인트린직 형태로 몰래 처리되기도 한다. memset, strcpy처럼 간단한 메모리 조작은 컴파일러가 아예 직통 대입 코드를 집어넣는 식으로 최적화를 하기도 하며, 각종 수학 함수들도 FPU 명령 하나로 곧장 치환되는 게 보통이다. 가령 제곱근을 구하는 sqrt함수는 곧바로 fsqrt 인스트럭션으로 말이다.

C 라이브러리 DLL인 msvcr*.dll은 여전히 수학 함수 심벌들을 제공한다. 그러나 요즘 컴파일러가 수학 연산을 인트린직 대신 일일이 그런 함수 호출 형태로 곧이곧대로 사용하는 경우란, 수학 함수들을 함수 포인터 형태로 접근해야 할 때밖에 없다.

비주얼 C++이 제공하는 여러 인트린직 함수 중에는 '일을 하지 않음'(no operation)을 의미하는 __noop이라는 함수가 있다. x86의 nop 인스트럭션(코드 바이트 0x90)과 비슷한 발상인데, nop를 생성하기라도 하는 것도 아니다. 컴파일러는 파싱만 해 주지 코드 생성은 안 하고 넘긴다. 파이썬으로 치면 pass와 비슷한 물건이다.

파이썬은 세미콜론 같은 구분자도 없고 공백 들여쓰기가 유효한 의미를 갖기 때문에 empty statement를 표현하려면 pass 같은 별도의 문법이 반드시 필요하다. 그러나 C/C++은 ; 하나만 찍어 줌으로써 empty statement쯤이야 얼마든지 만들 수 있는데 굳이 저런 잉여로운 인트린직은 왜 필요한 걸까?

__noop의 주 용도는, 가변 인자를 받는 디버그 로그를 찍는 함수의 컴파일 여부를 제어하는 것이다.

#ifdef _DEBUG
    #define WRITE_LOG   WriteLog
#else
    #define WRITE_LOG   __noop
#endif

WRITE_LOG("Start operation");
WRITE_LOG("Operation ended with code %d", nErrorCode);

함수가 가변 인자가 아니라면, WRITE_LOG 자체를 매크로 상수가 아닌 매크로 함수로 선언하면 된다.

#ifdef _DEBUG
    #define WRITE_LOG1(msg,a1)  WriteLog(msg,a1)
#else
    #define WRITE_LOG1(msg,a1)  0
#endif

이렇게 해 주면 디버그가 아닌 릴리즈 빌드에서는 WriteLog가 호출되지 않을 뿐더러, 매개변수들의 값 평가도 전혀 발생하지 않게 된다.
그러나 매크로 함수는 가변 인자를 받을 수 없기 때문에 임의의 개수의 인자를 모두 커버하려면 결국 함수 이름 자체만 치환하는 매크로 상수를 써야 한다. 매크로 상수는 함수의 호출만 없앨 수 있지 함수로 전달되는 매개변수들의 값 평가를 없앨 수는 없다. 뭐, 상수들의 나열이야 컴파일러의 최적화 과정에서 제거되겠지만 side effect가 남는 함수 호출은 어떡하고 말이다.

이런 이유로 인해 가변인자를 받는 디버그 함수를 제어하는 매크로는 범용적인 버전뿐만 아니라 매개변수 고정 버전도 같이 딸려 나오는 게 관행이었다. 대표적인 게 MFC의 TRACE 매크로이다. TRACE0~TRACE3도 있다. 한번 함수를 호출할 때 전달되는 매개변수의 개수가 런타임 때 매번 바뀌는 것도 아니니, 가능하면 고정 버전을 쓰는 게 프로그래밍 언어의 관점에서 더 안전했기 때문이다.

그런데 비주얼 C++의 __noop은, 하는 일은 없으면서 0개부터 n개까지 아무 개수로 그 어떤 type의 인자를 넘겨줘도 되는 훌륭한 페이크 함수이다. 그래서 매크로 상수를 이용해서 그 어떤 함수도 __noop로 치환하면 그 함수의 호출은 깔끔하게 무시되고 코드가 생성되지 않는다.

게다가 코드는 생성되지 않지만 컴파일러가 각 토큰에 대한 구문 분석은 해 준다는 점에서 __noop은 매크로 상수뿐만 아니라 매크로 함수보다도 더 우월하다.
WRITE_LOG1에다가 아예 얼토당토 않은 선언되지 않은 변수를 집어넣을 경우, 이것을 그냥 0으로만 치환해 버리는 코드는 디버그 버전에서만 에러가 발생하고 릴리즈 버전은 그냥 넘어간다.

그러나 __noop으로 치환하면 효과는 0 치환과 동일하면서 릴리즈 버전에서도 컴파일 에러가 뜬다. 그러니 더욱 좋다.
이 인트린직은 #pragma once만큼이나, 그야말로 기존 C/C++ 문법만으로는 뭔가 2% 부족한 면모가 있는 걸 채워 준 물건이 아닐 수 없다. 컴파일러 개발사들이 괜히 비표준 꼼수를 집어넣는 게 아니다. 그 꼼수가 정말 타당하다고 여겨지면 다음 표준 때 정식으로 반영까지 될 테고.

아무 일도 안 하는 null instruction은 코드 바이트도 0으로 하는 게 직관적이지 않나 싶은데..
아까도 얘기했듯이 x86은 의도적으로 쉽게 떠오르지 않는 값에다 그 명령을 할당했다.
크기 최적화를 하지 않고 프로그램을 빌드하는 경우(디버그 또는 속도 최적화), 비주얼 C++은 machine word align을 맞추거나 edit and continue용 공간을 확보해 두기 위해 코드 바이트를 약간 듬성듬성하게 배치하는데, 과거의 6.0은 그 빈 틈에 nop(0x90)를 집어넣었다.

그러나 언제부턴가 닷넷부터는 그 버퍼 코드가 int 3(0xCC)으로 바뀌었다.
정상적인 경우라면 거기는 도달해서는 안 되는 곳인데, 그냥 아무 일 없이 nop로 넘어가는 게 아니라 breakpoint가 걸리게 보안을 강화한 게 아닌가 싶다.

말이 나왔으니 말인데, int 3을 집어넣어 주는 인트린직도 응당 존재한다. 바로 __debugbreak 함수이다. 이건 사실 Windows API에도 DebugBreak()로 있기도 하고 말이다.
이걸 쓰면 비주얼 C++ IDE에서 F9를 누른 것과 동일한 효과를 낼 수 있다. 디버거를 붙여서 실행할 경우, 그 지점에서 프로그램의 실행이 멈춘다.

Posted by 사무엘

2013/09/20 08:30 2013/09/20 08:30
, , , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/879

Trackback URL : http://moogi.new21.org/tc/trackback/879

Comments List

  1. 사포 2016/04/20 10:12 # M/D Reply Permalink

    아니 이런 꿀팁이 있었군요...!

    1. 사무엘 2016/04/20 20:01 # M/D Permalink

      noop는 로그 찍는 함수를 없앨 때, debugbreak는 모종의 이유로 IDE의 F9 breakpoint가 제대로 동작하지 않는 상황에서(의외로 자주 발생함~) 개인적으로 무척 유용하게 쓴답니다. ^^

Leave a comment
« Previous : 1 : ... 1430 : 1431 : 1432 : 1433 : 1434 : 1435 : 1436 : 1437 : 1438 : ... 2204 : Next »

블로그 이미지

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

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/12   »
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:
3049029
Today:
49
Yesterday:
2142