[C] variadic function: Implementation
[!info] C99 기준으로 작성되었습니다.
Overview
C에서 함수는 기본적으로 유한 개의 지정된 argument만을 받게 되어있지만, parameter lists의 마지막에 ‘...
’(ellipsis) 키워드를 두면 가변적인(variable) 개수를 받도록 할 수 있다.
variable argument lists를 사용하는 대표적인 함수로는 printf
가 있다. 이렇게 variable한 개수의 arguments를 받을 수 있는 함수를 variadic function이라 한다.
A function that takes a variable number of arguments is called a variadic function. In C, a variadic function must specify at least one fixed argument with an explicitly declared data type. Additional arguments can follow, and can vary in both quantity and data type. In the function header, declare the fixed parameters in the normal way, then write a comma and an ellipsis: ‘, …’. Here is an example of a variadic function header:
int add_multiple_values (int number, ...)
Implementation
각 플랫폼에 따라 저수준 동작이 상이하기 때문에, portable한 C 프로그램을 작성하려면 반드시 C 표준에 정의되어있는 공식 인터페이스인 <stdarg.h>
헤더를 사용해야한다.
<stdarg.h>
#include <stdarg.h>
void va_start(va_list _ap_, _argN_);
void va_copy(va_list _dest_, va_list _src_);
type va_arg(va_list _ap_, _type_);
void va_end(va_list _ap_);
메뉴얼을 읽어보면, 사용법 자체는 굉장히 간단하다. 우리는 제공되고 있는 함수 매크로들을 적재적소에 활용만 하면 된다.
위 함수들의 prototype을 보면, 모두 va_list
구조체를 인자로 받고 있다. 그리고 내부에서 va_list
를 적절히 사용/조작한다.
이는 객체지향 언어에서 캡슐화(encapsulation)의 원칙 아래 클래스에 여러 메서드를 제공하고, 내부 동작을 사용자에게 노출시키지 않는 것과 매우 유사한 구조이다. 그러나 클래스의 메서드와는 큰 차이점이 있는데, 바로 사용하고 있는 ‘내부의 어떤 저장소’가 stack과 register라는 점이다.
온갖 추상화된(abstracted) 언어가 범람하는 이 시대에 C는 비교적 low-level의 언어로 인식되지만, C 또한 stack과 register, CPU를 다루는데 있어서는 추상화된 언어이다. C가 직접 다룰 수 있는 저장소는 main-memory의 1바이트 단위의 주소 공간으로, C는 stack과 register를 직접 다루는 어셈블리어의 상위 추상화 계층이다. 이렇듯 stack과 register 공간의 제어는 C언어의 영역을 벗어나며, 따라서 실제 동작 원리를 제대로 이해하려면 각 플랫폼의 ABI에 명시된 저수준의 규칙(Calling convention 등)을 참고해야한다.
[!note] C언어가 익숙한 독자라면 위 prototype들을 보고 이상한 점을 느꼈을 것이다.
바로 매개변수가 포인터가 아니라는 것이다. 그 동작 원리는 후술할 opaque type in C 섹션에서 확인할 수 있다.
x86/x86-64
[!warning] 세상에는 수많은 시스템이 있으며 아래 설명이 모든 구현을 대변하지 않습니다.
x86 그리고 x86-64, 크게 두 방식으로 구현되어 있다.
Where argument passed | Definition of va_list |
implementation of va_ functions |
|
---|---|---|---|
x86 | 모든 인자를 stack에 순서대로 전달 | 포인터 구현 (컴파일러 built-in을 wrapping하는 경우도 있음) |
매크로 함수 구현 (컴파일러 built-in을 wrapping하는 경우도 있음) |
x86-64 | 인자를 register에 우선 전달, 부족하면 stack 사용 | 컴파일러 built-in 구현 (크기 1인 구조체 배열 wrapping) |
컴파일러 built-in 구현 |
x86-64에서 컴파일러 built-in으로 구현한 이유는 ‘C코드로는 register 관련 동작을 정확히 정의할 수 없기 때문‘이다. 단순 포인터 연산으로 가능했던 x86과 달리 x86-64에서의 구현은 컴파일러의 도움이 필요할 수밖에 없었다.

x86-64: <stdarg.h>에서의 va_list 정의
앞서 x86-64에서의 구현은 컴파일러 built-in이라고 하였다. 실제로 header의 경로를 따라가 확인해보면 va_list는 위와 같이 정의되어있다. 즉 x86-64 환경에서 <stdarg.h>
라이브러리는 인터페이스(껍데기)만 제공할 뿐이고 내부는 컴파일러의 built-in으로 구현되어 있다.
C코드로는 여기까지 밖에 볼 수 없으며, 오직 ABI 문서 명세상으로만 x86-64의 va_list
구조를 확인할 수 있다.
va_list
의 구조에도 아래와 같은 차이가 있다.
x86 시스템에서는 인자를 저장하는데 있어 오직 스택만 사용했기 때문에 void 포인터로 단순하게 구현되었지만, x86-64에 들어서는 레지스터를 추가로 사용하기 때문에 별도의 offset 변수들이 있다.

x86: Intel386 System V

x86-64: AMD64
x86-64 시스템은 가변 인자를 관리하는데 있어 정수 레지스터와 부동소수점 레지스터를 모두 사용한다. gp_offset
은 general-purpose register의 offset, fp_offset
은 floating-point register의 offset이다.
시스템마다 레지스터의 개수는 상이하지만, 거의 대부분의 시스템에서 위와 동일한 구조이다. 만약 레지스터에 담지 못한 인자가 있다면 stack에 담는다. overflow_arg_area
변수는 이때 사용한다.
opaque type in C
앞서 ‘va_
함수들의 parameter lists에서 va_list
를 포인터로 받고 있지 않는 설계에 주목하라’ 하였는데, 위 코드를 보면 그 원리를 명확히 알 수 있다. 결국 va_list
는 포인터였다. 애초에 type 자체가 포인터였기 때문에 parameter를 구태여 포인터로 선언할 필요가 없는 것이었다.
[!info] 엄밀히 말하면 ‘배열의 이름’은 포인터가 아니라 ‘런타임시 배열의 시작 주소로 해석되는 symbol’이지만, 너무 길기 때문에 편의상 포인터라고 칭하겠습니다.
왜 포인터로 구현된걸까? 심지어 x86-64 환경에서는 va_list[1]
라는 기이한(?) 형태로 정의됐다.
이렇게 C에서 포인터로 선언된 변수(특히 구조체)와 그 변수를 조작하는 interface 함수들을 제공할 때, 여러 장점이 있다.
- 사용자는 그저 API 함수의 사용 방법만 익히면 된다. 변수의 내부는 몰라도 된다.
- 사용자가 코드에
&
,*
와 같은 포인터 연산자를 사용하지 않아도 된다. - (특히 구조체로 선언할 경우에 유용) 사용자는 변수를 함부로 복사할 수 없다.
va_list va1 = va2;
와 같은 value copy (shallow copy)를 방지해 UB(undefined behavior)을 막을 수 있다.
결국, 이는 C언어의 문법적 특성을 이용한 특유의 은닉 기법이라고 할 수 있다.
References
- https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Variable-Number-of-Arguments.html
- https://pubs.opengroup.org/onlinepubs/000095399/basedefs/stdarg.h.html
- https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf (3.5.7절)
- https://wiki.osdev.org/System_V_ABI
- https://github.com/ARM-software/abi-aa/blob/c51addc3dc03e73a016a1e4edf25440bcac76431/aapcs64/aapcs64.rst
- https://refspecs.linuxfoundation.org/elf/mipsabi.pdf
- https://en.wikipedia.org/wiki/X86_calling_conventions#