가변인자 포워딩 시키기

@codemaru · September 07, 2007 · 7 min read

[마소 플러스]
C 프로그래밍 테크닉
가변인자 포워딩시키기

신영진 http://www.jiniya.net | 웰비아닷컴에서 보안 프로그래머로 일하고 있다. 시스템 프로그래밍에 관심이 많으며 다수의 PC 보안 프로그램 개발에 참여했다. 현재 데브피아 Visual C++ 섹션 시삽과 Microsoft Visual C++ MVP로 활동하고 있다. C와 C++, Programming에 관한 이야기를 좋아한다.

C 언어 함수의 가장 큰 특징 중의 하나는 가변 인자를 지원한다는 점이다. 가변 인자란 인자의 개수가 가변적이란 의미다. func란 함수가 가변 인자 함수라고 한다면 func(1), func(1,2), func(a,b,str) 등으로 호출할 수 있다. 이러한 특징을 가장 잘 활용한 대표적인 함수가 printf다.

<리스트 1>의 코드에 이번 달에 언급할 문제가 나와 있다. mysum은 단순히 인자를 realsum으로 전달하는 기능을 담당하고 있다. 이번 달의 문제는 mysum을 구현하는 것이다. <리스트 1>의 mysum처럼 구현하면 될 것 같지만, 이 코드는 컴파일되지 않는다.  realsum을 <리스트 1>과 같이 호출하는 것이 불가능하기 때문이다. 나머지 부분을 읽기 전에 이 문제를 어떻게 해결할 수 있을지를 한번쯤 고민해 보자.

<리스트 1> 다른 함수로 가변 인자 전달

int realsum(int count, ...)  
{  
    // 인자들의 합을 구하는 작업들  
    return sum;  
}  
  
int mysum(int count, ...)  
{  
    return realsum(count, ...);  
}

문제를 해결하기 위해서는 가변 인자가 동작하는 구조에 대해 정확하게 이해할 필요가 있다. 가변 인자를 이해하기 위해서는 __cdecl 함수 호출 규약과 함수 호출에 스택이 어떻게 사용되는지에 대해 먼저 이해해야 한다(http://www.jiniya.net/lecture/techbox/callconv.html 참고). <리스트 2>에는 가변 인자에 사용되는 매크로의 구현 소스가 소개되어 있다. 이를 살펴보면 알 수 있겠지만 va_start, va_end, va_arg에는 사실 그다지 대단한 내용이 들어 있지 않다.

<리스트 2> 가변 인자 관련 매크로 구현 소스

#define \_INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )  
  
#define va\_start(ap,v)  ( ap = (va\_list)\_ADDRESSOF(v) + \_INTSIZEOF(v) )  
#define va\_arg(ap,t)    ( \*(t \*)((ap += \_INTSIZEOF(t)) - \_INTSIZEOF(t)) )  
#define va\_end(ap)      ( ap = (va\_list)0 )

가변 인자를 조작하는 매크로의 핵심은 va_list다. 이 인자로부터 모든 가변 인자를 구할 수 있기 때문이다. 따라서 문제를 해결하는 가장 손쉬운 방법은 va_list를 인자로 받아들이는 내부 함수를 만드는 것이다. 이렇게 해결한 코드가 <리스트 3>에 나와 있다. coresum 함수는 원래 realsum 함수가 가진 역할을 옮겨놓은 함수다. 이제 realsum, mysum 함수는 coresum 함수를 호출해 동일한 기능을 할 수 있다.

<리스트 3> va_list를 이용한 가변 인자 전달

int coresum(int count, va\_list ap)  
{  
    // 작업들  
    return 0;  
}  
  
int realsum(int count, ...)  
{  
    va\_list ap;  
    va\_start(ap, count);  
    int r = coresum(count, ap);  
    va\_end(ap);  
    return r;  
}  
  
int mysum(int count, ...)  
{  
    va\_list ap;  
    va\_start(ap, count);  
    int r = coresum(count, ap);  
    va\_end(ap);  
    return r;  
}

앞의 해답의 경우에는 realsum 코드를 수정했기 때문에 우리가 처음 의도했던 정답이라고 할 순 없다. 그렇다면 realsum의 코드를 하나도 수정하지 않고 인자를 전달할 방법은 없을까? 정상적인 방법으로 할 순 없지만 앞에서 살펴본 가변 인자의 동작 원리와 약간의 어셈블리 지식을 결합하면 만들 수 있다. <리스트 4>는 이러한 방법으로 가변 인자를 통째로 전달하는 방법을 보여준다.

<리스트 4> 어셈블리를 통해 가변 인자를 전달하는 함수

#include <vector>  
#include <stdarg.h>  
  
int realsum(int count, ...)  
{  
    va\_list ap;  
    va\_start(ap, count);  
  
    int sum = 0;  
    for(int i=0; i<count; ++i)  
        sum += va\_arg(ap, int);  
  
    va\_end(ap);  
  
    return sum;  
}  
  
int mysum(int count, ...)  
{  
    int argsize = count \* 4;  
    int framesize = argsize + 4;  
      
    // 가변 인자를 저장할 스택 공간을 확보한다.  
    void \*data;  
    \_\_asm sub esp, argsize  
    \_\_asm mov data, esp  
  
    // 가변 인자를 확보한 공간에 복사한다.  
    va\_list ap;  
    va\_start(ap, count);  
    memcpy(data, ap, argsize);  
    va\_end(ap);  
  
    // count를 스택 저장하고 실제 함수를 호출한다.  
    \_\_asm push count  
    \_\_asm call realsum  
  
    // \_\_cdecl은 호출한 곳에서 스택을 정리해야 한다.  
    \_\_asm add esp , framesize  
  
    // 함수의 리턴 결과는 eax 저장된다.  
    int sum;  
    \_\_asm mov sum, eax;  
  
    return sum;  
}  
  
int main()  
{  
    printf("%d", mysum(4, 3, 4, 5, 9));  
    return 0;  
}

<리스트 4>의 코드는 가변 인자가 정수라는 가정을 하고 있다. 만약 printf 류의 함수 인자를 포워딩시켜야 한다면 조금 더 문제가 복잡해진다. 넘어온 문자열로부터 인자의 크기를 계산해야 하기 때문이다.

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