표준 함수 printf 등이 어떤 원리로 여러개의 인자를 받을 수 있을까요?
C, C++에서 가변 인자 함수(Variadic function)를 만드는 방법을 배워보겠습니다.
가변 인자 함수를 만들기 위해서는
stdarg.h 헤더파일을 인클루드 해야합니다.
C++의 경우 stdarg.h 말고 cstdarg 파일을 인클루드 하셔도 됩니다.
알아 두셔야 할 것은
va_list, va_start, va_arg, va_end, va_copy 입니다.
va_list 는 자료형이며 매개변수들의 위치를 저장할 용도의 변수입니다.
실질적으로는 char* 변수 입니다.
va_start 는 va_list 변수의 값을 초기화 할 용도로 사용되는 매크로입니다.
인자를 2개 받는 첫 번째 인자는
va_list 자료형으로 선언한 변수의 이름이며
두 번째 인자는 가변인자 목록 이전에 있는 변수의 이름입니다. (... 이전의 마지막 변수)
va_arg 는 역시 매크로 이며, va_list 변수로부터 특정 타입의 값을 돌려 받고,
va_list 변수의 주소 위치를 다음 변수 위치로 이동시킵니다.
첫 인자로는 va_list 변수의 이름이며
두 번째 인자는 돌려 받을 타입입니다.
두 번째 인자로 지정해준 타입으로 값을 가져옵니다.
va_end 도 매크로인데, 해도 되고 안해도 되는 그런 것...
va_list 변수의 값을 0으로 대입합니다.
첫 인자로 va_list 변수의 이름을 입력받습니다.
va_copy 는 두 개의 인자를 입력받아 첫 번째 인자의 값을
두 번째 인자의 값으로 대체시킵니다.
va_list 의 값을 다른 곳에 복사할 용도로 만들어 둔 것인데.
사실상 그렇게 사용하는 경우도 적고 그냥 대입이랑 똑같아서 잘 안쓰입니다.
va_copy(dst, src);
이렇게 사용하면 dst = src; 와 같습니다.
다음은 이해를 돕기 위한 예제 소스 코드 입니다.
|
일단 printf 를 사용하기 위해 stdio.h 헤더파일을 인클루드 했으며,
가변 인자 함수를 만들기 위해 stdarg.h 헤더파일도 인클루드 했습니다.
가변 인자 기능을 이용해 Sum 함수를 만들었는데요,
count 개의 정수를 입력 받아 합을 구하는 함수입니다.
첫 인자로 count (갯수)를 입력 받으며,
이후 ... 을 사용하여 나머지는 가변갯수의 인자를 받도록 하였습니다.
main 함수를 보시면 총 3개의 정수를 출력합니다.
각각의 Sum 함수를 사용했을 때의 결과를 출력합니다.
Sum(2, 100, 200) 은 2개의 정수를 입력 받고 2개의 정수는 각각 100, 200 으로 주어집니다.
100 + 200 은 300이니 출력값은 300이 나옵니다.
Sum(3, 100, 200, 300) 은 3개의 정수를 입력 받고 정수는 각각 100, 200, 300 입니다.
합산 결과 600이 출력됩니다.
Sum(4, 100, 200, 300, 400) 은 4개의 정수 100, 200, 300, 400 을 합하여 1000을 출력합니다.
이제 Sum 함수를 구체적으로 파봅시다.
4 번째 줄
int Sum(int count, ...)
함수의 호출 결과가 int 자료형이고,
함수 이름은 Sum 이며
첫 번째 매개 변수로 int 자료형 을 입력받으며 (용도는 가변 인자의 갯수를 파악하기 위해)
두 번째 매개 변수 부터 가변 갯수로 입력받습니다.
8 번째 줄
va_list 자료형의 변수 vl 을 선언합니다.
9 번째 줄
va_start 를 사용하여 vl 변수를 초기화 합니다.
초기화 값은 두 번째 인자 값의 바로 다음 위치의 주소.
두 번째 인자 값이 count 이니까 count 변수 바로 다음 위치의 주소가 저장됩니다.
count 변수 바로 다음 위치의 주소는 가변 인자 ... 부분의 첫 주소죠.
10 번째 줄
count 만큼 for 안의 코드를 반복합니다.
12 번째 줄
va_arg 를 사용하여 vl 변수 위치에 있는 값을 가져옵니다.
가져올 값의 타입은 va_arg 의 두 번째 인자로 지정해 줍니다.
가져온 값을 result의 값과 더해 result에 대입합니다.
14 번째 줄
va_end 를 사용하요 vl 변수에 0 값을 대입합니다. vl 변수를 다썼다는 의미입니다.
사용하지 않아도 무방합니다.
주의점
가변 인자 함수의 인자로 넘겨주는 값들은 일부 자동 캐스팅이 발생합니다.
전달된 인수의 표현이 호출된 함수의 기대와 일치하게 만들어 프로그래머의 실수로
비정상적인 동작을 하는 경우를 줄이기 위함이라고 하네요.
char -> int
short -> int
float -> double
때문에 va_arg 할 때 문제가 발생하는데
va_arg(vl, char) 와 va_arg(vl, short) 할 때는 별다른 문제가 없습니다.
int 로 캐스팅 됐으니 int 크기로 읽지 않으면 문제가 발생해야되는것 아닌가
싶겠지만 컴파일 플랫폼에 따라 최소 이동 기준이 있더라구요.
그래서 va_arg(vl, char) 해도 char 값을 잘 받지만 포인터를 이동하는 크기는 char의 크기보다 더 크게 이동했습니다. (x86 에선 int 크기만큼).
정수의 경우에는 바이트 크기와 상관없이 순전히 2진 값이라서. 캐스팅 할 때 읽는 크기에 따른 문제가 일어나지 않는데.
실수의 경우는 애초에 지수부, 가수부 등 구역이 나뉘어져 있어서 중간에 자를 수 없는 구조라 va_arg(vl, float) 하면 비정상 적인 값을 받아오게 됩니다.
가변 인자 함수 만들 때 주의 하시기 바랍니다.
scanf 로 float 를 입력받을 때는 %f 이고 double 을 입력 받을 때는 %lf 인데,
printf 에서는 float 나 double 이나 %f 로 처리가 가능한지 이해되시나요?
포인터 타입과는 다르게 float는 double로 캐스팅 된 후 들어가 버려서
함수 내부에서는 float 와 double 을 따로 구분할 필요가 없기 때문에 그냥 %f 하면 다 double 값을 출력해 버리는 것이죠.
위험성
가변 인자 함수는 스택을 손상시킬 수 있는 위험성이 있습니다.
또한 이걸 이용한 버퍼 오버플로우 공격이라는 공격 기술도 있습니다.
예를들어 위쪽 Sum 함수에서 count 를 2로 넣고 그 다음 인수를 100까지만 넣었다면 어떻게 될까요?
전혀 이상한 값이 출력되겠죠. 출력 값에 따라서 동작을 달리 하는 프로그램을 만들었다면 미정의 동작이 일어날 수도 있습니다.
게임 서버인데 미정의 동작으로 DB 쿼리를 잘못날리거나 해서 유저 데이터를 손상시켜버리면 끔찍한 일이 벌어지겠죠.
버퍼 오버 플로우 공격은 문자열 입력에 관한 위험인데
scanf 함수에서 "%s" 를 사용할 때 입력 버퍼의 크기에 상관없이 긴 문자열도 입력 받기 때문에 버퍼 크기보다 큰 길이의 문자열을 입력 받으면 미정의 동작이 발생할 수도 있습니다.
미정의 동작으로 프로그램이 죽어버리면 다행일 수도 있습니다. 악의적인 목적으로 악성코드를 심어버릴 수도 있기 때문에 문제가 발생하기도 합니다.
때문에 scanf_s 등과 같이 비표준이지만 문자열을 입력 받을 때는 버퍼의 크기까지 지정해 주도록 하는 안전 함수가 생겼습니다. scanf_s("%s", 버퍼, 버퍼크기);
잡다
C++ 에서는 가변 인자 함수 말고 가변 인자 템플릿 이라는 것이 있습니다.
C++14 부터 사용가능하며, 가변 인자 함수와는 달리 입력받은 인자의 타입에 따라 사용한 함수만 컴파일 타임에 생성해서 링크해 주므로 앞서 설명드린 위험성이 존재하지 않습니다.
또한 float 가 double 로 자동 캐스팅 되는 현상도 없고, 특수화를 통해 포맷을 직접 입력 받지 않고도 타입에 따라 서로 다른 동작을 하도록 만드는것이 가능합니다.
댓글 없음:
댓글 쓰기