In C programming, variadic functions are functions that accept a variable number of arguments. The most common example is the printf() function. While the standard implementation uses <stdarg.h>, it is possible to replicate the variadic argument mechanism using pointer arithmetic and custom macros.
This document explains how variadic arguments work in C and demonstrates a complete, low-level implementation of a custom printf() function, including manual memory management and format parsing.
A variadic function can accept a varying number of arguments. In C, such functions declare their fixed parameters followed by ... to indicate additional arguments.
Example:
int printf(const char *format, ...);C provides a standard library header <stdarg.h> with these key macros:
va_list– type to hold the variable argument listva_start– initializes the argument listva_arg– retrieves the next argumentva_end– cleans up the list
typedef char* va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))
#define va_arg(ap, type) (*(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)))
#define va_end(ap) (ap = (va_list)0)va_listis simply achar*pointer used to traverse the argument list._INTSIZEOF(n)ensures proper alignment by rounding the size up to a multiple ofint.va_start(ap, v)sets theva_listto the first variadic argument (after parameterv).va_arg(ap, type)retrieves the next argument and advances the pointer.va_end(ap)resets the pointer to NULL.
This simulates what the compiler and <stdarg.h> do behind the scenes.
The printf() function parses a format string containing specifiers like %d, %f, %s, etc. It handles each character:
- If it's a regular character, it is printed.
- If it’s
%, the next character determines how to interpret the argument.
Each specifier uses va_arg() to fetch the correct type:
int x = va_arg(args, int); // for %d
char *str = va_arg(args, char*); // for %sThese helper functions convert arguments to printable strings:
itoa(int, char*)→ Integer to stringutoa(unsigned int, char*)→ Unsigned integeritoh(unsigned int, char*, int)→ Hexadecimalitoo(unsigned int, char*)→ Octalftoa(double, char*, int)→ Floating point
printf("Integer: %d\n", 12345);
printf("Hex: %x\n", 255);
printf("Float: %f\n", 3.14);
printf("Char: %c\n", 'A');Integer: 12345
Hex: ff
Float: 3.140000
Char: A
All achieved without using stdio.h.
The implementation includes a mini malloc() using a fixed-size heap:
#define HEAP_SIZE 1024
static char heap[HEAP_SIZE];This enables dynamic string generation and storage during formatting.
- Variadic functions allow flexible interfaces like
printf(). - Behind the scenes,
va_listand pointer arithmetic traverse arguments. - Manual implementations are valuable for understanding calling conventions and memory layout.
- This low-level approach is foundational in systems programming and embedded systems.
- Stack memory layout
- Calling conventions (ABI)
- Format string vulnerabilities
__builtin_va_listin GCC
To compile the full implementation:
gcc -o my_printf custom_printf.c
./my_printfCrafted with ❤️ by a low-level C enthusiast exploring the internals of printf().