[마소 플러스]
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 류의 함수 인자를 포워딩시켜야 한다면 조금 더 문제가 복잡해진다. 넘어온 문자열로부터 인자의 크기를 계산해야 하기 때문이다.