C언어 가변인수함수(printf 등)

2021. 12. 15. 16:45C

가변 인수란 인수의 개수와 타입이 정해져 있지 않은 인수를 말한다. 대표적으로는 printf와 scanf 등이 있다.

 

printf("Hello, World!");

printf("%d:%d",a,b);

 

printf의 원형 : int _cdecl printf(const char*_Format, ...)

가변인수 함수에서는 반드시 고정 인수를 한 개 이상 가지고 있어야 하며, 가변 인수가 올 자리에는 ...가 있다.

Format가 문자열 상수를 의미하고 고정 인수다. 고정 인수는 const char* 타입의 문자열이어야만 한다.

 

cdecl은 함수를 호출할 때마다 호출원이 인수를 전달한 스택을 정리한다.

printf는 인수의 수가 정해져 있지 않기 때문에 호출된 함수가 스택을 정리하는 _stdcall을 쓸 수가 없다.

이것은 모든 가변 인수에서 동일하게 적용되며, 만약 _stdcall로 작성했다면 컴파일 시 _cdecl로 바꾸어 버린다. 

 

 

 

여기서 _stdcall 이란? 

함수 호출 규약 (_cdecl, _stdcall, _fastcall)

: 함수 실행 시 인자 값을 전달하고 스택을 정리하는 과정에서 함수를 호출하는 쪽과 호출당하는 함수 사이의 혼란을 방지하기 위해 함수 호출 과정에서는 일정한 규약이 존재한다. 이를 함수 호출 규약이라고 한다. 이러한 함수 호출 규약에는 _cdecl, _stdcall, _fastcall 등이 존재한다. 

_cdecl

: x86 구조에서 주로 사용하는 호출 규약으로 C/C++ 컴파일러에서 기본적으로 사용한다. 함수 호출 시 오른쪽 인자부터 스택에 전달하며, 호출자(caller)가 스택을 정리하는 특징이 있다.  이를 디버깅을 통해 좀 더 알아보면, _cdecl 은 push 명령어를 통해 인자 값을 역순으로 스택에 저장한 뒤 함수를 호출한다. 또한 함수 호출 이후에는 ADD ESP,8 명령어를 통해 인자 값 2개를 위해 사용했던 스택 공간을 함수 호출 이전의 상태로 복구한다.

_stdcall

:MS Win32API 표준 규약으로, 함수 호출 시 오른쪽 인자부터 스택에 전달하고 호출당한 함수(피호출자)가 사용한 스택을 정리한다는 특징이 있다. _stdcall 호출 규약 방식을 디버깅을 통해 보면  _cdecl과 달리 ADD ESP,8과 같은 호출자가 스택을 직접 정리하는 행위가 생략되어 있다. 

함수 내부를 확인해보면 내부에서 return과 동시에 스택을 정리하는 것을 확인할 수 있다. 그냥 ret명령만을 실행하는 것이 아닌 ret 8 명령어를 실행하여 함수 종료 전에 사용한 인자값을 정리하기 위해 할당된 스택 공간을 정리하는 것이다.

_fastcall

:매개 변수의 일부를 스택이 아닌 ECX, EDX와 같은 레지스터로 전달한다. 때문에 함수 호출 시 다른 규약에 비해 빠른 편이며, _stdcall 방식과 마찬가지로 호출 당한 함수가 사용한 스택을 정리하는 특징이 있다. 

 

가변인자를 사용하는 데 쓸 수 있는 매크로 함수들이 존재한다.

모두 <stdarg.h> 헤더파일을 호출하면 사용할 수 있다.

va_list 

: 함수로 전달되는 인수들은 스택에 저장한 뒤 거기서 매개변수를 꺼내 사용하는데 변수를 읽으려면 포인터 변수가 필요하며 그 포인터가 va_list형이다.

va_start

: 포인터 변수가 첫번째 가변인수를 가리키도록 초기화시킨다.

va_arg

:va_start로 포인터 변수가 첫번째 가변인수를 가리키고 읽을 때 가변 인자를 읽는다.

va_end

:가변 인수를 다 읽은 다음에 정리하는 역할인데 없어도 큰 상관은 없다

#include<stdarg.h>
int Func(int size, ...){
	va_list Nstart;
    va_start(Nstart,size);
    
    for(int a=0; a<size; a++){
    	va_arg(Nstart,int);
    }
    va_end(Nstart);

1. Func 함수가 첫 번째 인수로 size를 전달받았다.

2. va_list로 Nstart라는 포인터 변수(char*형)을 지정했다.

3. va_start로 Nstart가 첫번째 인수 size를 가리킨다.

4. for문이 돌아가며 모든 인수를 다 읽을 때까지 계속 반복하며 읽는다.

5. 모든 인수를 다 읽었으면 va_end로 마무리한다.