서言
C 언어에서 제공되는 표준 출력 함수들은 데이터의 형식화된 출력을 담당한다. 이 함수들을 잘못 사용하면 버퍼 오버플로우나 보안 취약점이 발생할 수 있으므로, 각 함수의 특성과 사용 시 주의사항을 명확히 이해하는 것이 중요하다.
1. 포맷 출력 함수의 종류
C 표준 라이브러리에서 제공하는 주요 형식 출력 함수들은 다음과 같다:
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
2. 함수 간의 관계
2.1 공통점
모든 함수는 포맷 문자열의 제어를 받아 데이터를 지정된 출력 장치로 내보낸다. 포맷 지정자는 후속 인수의 변환 방식을 결정한다:
printf("%x", 100); /* 16진수로 출력 */
printf("%d", 100); /* 10진수로 출력 */
2.2 차이점
출력 대상에 따른 분류:
| 함수 | 출력 대상 |
|---|---|
| printf(), vprintf() | 표준 출력 스트림(stdout) |
| fprintf(), vfprintf() | 지정된 스트림 |
| sprintf(), snprintf(), vsprintf(), vsnprintf() | 문자열 버퍼 |
버퍼 크기 지정 여부에 따른 분류:
| 함수 | <þ>설명|
|---|---|
| snprintf(), vsnprintf() | 최대 size 바이트(NUL 포함)까지 쓰기 |
| 기타 함수 | 버퍼 크기 제한 없음 |
va_list 기반 함수:
vprintf(), vfprintf(), vsprintf(), vsnprintf()는 각각 printf(), fprintf(), sprintf(), snprintf()와 동일한 기능을 수행하지만, 가변 인자 리스트(va_list)를 사용한다는 차이가 있다. 한 번 호출이 완료되면 va_list는 "정의되지 않은" 상태가 된다.
2.3 반환값
정상적인 경우, 함수는 성공적으로 출력된 문자 수(NUL 문자 제외)를 반환한다. 오류 발생 시 음수를 반환한다.
snprintf()과 vsnprintf()는 출력 문자 수가 size 바이트(NUL 포함)를 초과하지 않도록 제한한다. 만약 size가 실제 문자열 길이(NUL 제외)보다 작으면 문자열이 잘리고, 반환값은 원본 문자열의 길이(NUL 제외)가 된다:
int length;
char buffer[100];
length = snprintf(buffer, 10, "%s", "1234567890123");
// length: 13, buffer: "123456789"
만약 size가 버퍼 크기보다 작다면:
*** stack smashing detected ***: ./test terminated
Aborted (core dumped)
3. 사용 시 주의사항
주의사항 1: 버퍼 오버플로우 방지
sprintf()와 vsprintf()를 사용할 때는 출력 데이터가 str 버퍼 크기를 초과하지 않도록 반드시 보장해야 한다. 초과할 경우 버퍼 오버플로우가 발생하여 심각한 문제를 초래할 수 있다. 데이터 크기를 보장할 수 없다면 대신 snprintf() 또는 vsnprintf()를 사용해야 한다.
주의사항 2: 자기 자신의 버퍼를 덮어쓰기
기존 문자열에 새 내용을 추가하는 다음과 같은 코드는 피해야 한다:
sprintf(buffer, "%s appended text", buffer);
일부 컴파일러版本에서는 예상대로 동작할 수 있지만, 다른 버전에서는 잘못된 결과를 낳는다. 더욱 중요한 것은 C 표준에서 sprintf(), snprintf(), vsprintf(), vsnprintf()를 호출할 때 원본과 대상 주소가重叠되면 결과가 정의되지(undefined) 한다고 명시하고 있다는 점이다:
C99 및 POSIX.1-2001은 sprintf(), snprintf(), vsprintf(), 또는 vsnprintf() 호출이 중첩된 객체 간 복사를 야기하는 경우(예: 대상 문자열 배열과 제공된 입력 인수가 동일한 버퍼를 참조하는 경우) 결과가 정의되지 않음을 지정한다.
주의사항 3: 형식 문자열 조작 위험
다음과 같은 코드는 버그를 유발할 수 있다:
print/user_input); /* 위험한 코드 예시 */
user_input에 "%n"이 포함되어 있으면 printf()가 메모리에 쓰기를 시도하여 보안 취약점이 발생할 수 있다:
printf("%n"); /* 세그멘테이션 폴트 발생 */
사용자 입력을 직접 printf()의 인자로 전달하기 전에 반드시 적절한 이스케이프 처리를 수행해야 한다.