what is va_start in printf
문득 printf 문의 구현이 궁금해서 한번 봤더니
int
__printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = __vfprintf_internal (stdout, format, arg, 0);
va_end (arg);
return done;
}다음과 같이 되어 있었다. 근데 여기서 보면 va_list 나 va_start 같은 것이 보여 정의를 볼려고 했더니
glibc 상의 정의가 되어 있지 않았다. 이게 무엇인가 한번 같이 알아보자
printf문이 가변인자 인것은 모두가 알것이다. 가변인자는 모두 va_list 자료 구조로 인자들을 관리한다.
이 자료구조는 glibc가 아닌 GCC/Clang 에 구현되어 있으며,
typedef struct {
unsigned int gp_offset; // GP 레지스터에서 다음 읽을 위치
unsigned int fp_offset; // FP(XMM) 레지스터에서 다음 읽을 위치
void *overflow_arg_area; // 스택(overflow area)에서 다음 읽을 위치
void *reg_save_area; // rdi, rsi, rdx, rcx, r8, r9 / xmm0~7 저장된 영역
} __va_list_tag[1];대략 다음과 같다.
- gp_offset
rdi, rsi, rdx, rcx, r8, r9 같은 정수/포인터 레지스터에서 몇 번째 위치까지 읽었는지 오프셋을 저장.
- fp_offset
부동소수점(XMM) 레지스터에서 읽은 위치.
- reg_save_area
함수 시작 시 callee가 레지스터로 받은 인자들을 복사해둔 메모리.
va_arg가 여기서 값을 꺼냄.
- overflow_arg_area
레지스터를 다 쓰고 남은 인자들(또는 애초에 레지스터로 못 받은 큰 구조체들)이 놓인 스택 위치.
이런 구조체를 사용하고
va_start (arg, format); 에서 해당 구조체를 format에 따라 초기화 한다.
이후 초기화된 구조체로 __vfprintf_internal 을 시작하는 방식이다.
이후 va_end (arg); 으로 메모리를 정리해서 누수를 막는다.
여기서 중요한점은 가변인자인 함수는 rdi rsi rdx rcx r8 r9 와 같은 호출 규약 레지스터의 값들을
spill(복사) 하여 자기 스택 프레임에 저장을 한다. 그리고 위에 있는
- reg_save_area
함수 시작 시 callee가 레지스터로 받은 인자들을 복사해둔 메모리.
va_arg가 여기서 값을 꺼냄.
이런식으로 자신의 인자들을 정리한 스택을 기준으로 va_list 가 사용되는 것이다.
printf() ▶ va_start()
▶ vfprintf()
▶ __vfprintf_internal()
▶ va_arg() → format 해석 → 출력
▶ va_end()대충 흐름은 다음과 같다.