what is va_start in printf

문득 printf 문의 구현이 궁금해서 한번 봤더니

C
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 에 구현되어 있으며,

C
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 가 사용되는 것이다.

C
printf()  ▶  va_start()  
            ▶  vfprintf()
                ▶  __vfprintf_internal()
                    ▶  va_arg() → format 해석 → 출력
                    ▶  va_end()

대충 흐름은 다음과 같다.