문자열 함수 실습 — strncpy·strncat 직접 구현과 오버플로 함정

NOTE

C 기초 강의의 문자열 실습(8-01 ~ 8-05)에서 만난 오류 패턴을 정리하고, strncpy·strncat·파일명 생성 함수를 직접 구현하며 겪은 버그를 기록한다. 환경은 Linux 컨테이너, gcc -g -O0 -Wall -Wextra.

IMPORTANT

코드는 컨테이너에서 컴파일·실행해 확인했고, 설명은 AI(Claude)와 함께 코드 리뷰하며 정리했다.

TIP

📂 원본 소스 (GitHub)

📌 한눈에 보기

실습문제핵심 원인처방
8-01strncpy 후 널 문자 없음strncpy는 원본이 n 이상이면 \0을 붙이지 않음snprintf 또는 마지막에 \0 직접 대입
8-02strlen에 signed char* 전달char*와 signed/unsigned char*는 다른 타입문자열은 char*로 통일
8-03문자열 리터럴 수정리터럴은 읽기 전용 영역const char*로 받고, 수정이 필요하면 배열로 복사
8-04get_dirname이 원본을 그대로 반환const 문자열을 잘라 쓸 수 없음출력 버퍼에 복사해서 자르기
8-05-a복사 루프 종료 시 1바이트 초과off-by-one (dest[i]='\0')널 문자 자리까지 크기에 포함
8-05-bgetchar 루프에 경계 검사 없음입력 길이 무제한버퍼 끝 검사 또는 fgets

1. 널 문자와 const — 8-01, 8-03

strncpy는 널 문자를 보장하지 않는다 (8-01)

요약

  • 문제: strncpy 뒤 문자열에 \0이 없음 (8-01)
  • 원인: 원본이 n 이상이면 strncpy는 널 문자를 붙이지 않음
  • 처방: snprintf(a, sizeof(a), "%s", src) 또는 마지막 칸에 \0 직접 대입, src NULL 검사
  • 결과: 항상 널 종료 + 잘림 안전

리터럴은 상수다 (8-03)

요약

  • 문제: 리터럴을 수정하면 SIGSEGV (8-03)
  • 원인: 리터럴은 읽기 전용 영역이고, 배열 크기는 글자 수 + 1
  • 처방: const char *로 받고, 수정이 필요하면 char buf[] = "..."로 복사
  • 결과: 읽기 전용 위반 없음

2. signed/unsigned char — 8-02

요약

  • 문제: strlen에 signed/unsigned char*를 넘기면 -Wpointer-sign 경고 (8-02)
  • 원인: char, signed char, unsigned char는 서로 다른 세 타입
  • 처방: 문자열은 char*로 통일, 바이트 데이터는 unsigned char* + 길이(size_t)
  • 결과: 경고 제거

3. const 문자열을 자를 수 없다 — 8-04

요약

  • 문제: get_dirname이 아무것도 못 자르고 원본을 반환 (8-04)
  • 원인: const/리터럴 문자열에는 \0을 쓸 수 없음
  • 처방: 출력 버퍼에 memcpy로 복사한 뒤 out[len] = '\0' (len + 1 > outsize면 실패)
  • 결과: /home/dir → /home (루트 바로 아래는 "/" 분기를 따로 둠)

4. off-by-one와 무제한 입력 — 8-05

8-05-a: 복사 루프가 1바이트를 넘긴다

요약

  • 문제: 복사 루프 종료 시 dest[i]='\0'가 1바이트 초과 (off-by-one)
  • 원인: n개를 다 복사한 뒤 dest[n]에 널 문자를 씀
  • 처방: dest를 n+1로 만들거나 루프를 i < n - 1로
  • 결과: size_t는 %zu로 출력

8-05-b: 입력 길이를 검사하지 않는다

요약

  • 문제: getchar 루프에 길이 상한이 없어 버퍼 초과
  • 원인: 입력 길이 무제한 + 마지막 *p++ = 0도 범위 초과 가능
  • 처방: p < buf + sizeof(buf) - 1 조건 추가 또는 fgets
  • 결과: 입력이 길면 남은 글자는 입력 버퍼에 남음

5. 직접 구현 — my_strncpy / my_strncat

가장 흔한 실수: 루프 안의 strlen(dst)

요약

  • 문제: strcat 직접 구현 결과가 "dsts"로 끝남
  • 원인: 루프 안의 strlen(dst)가 첫 글자를 쓰는 순간 3→4로 변함
  • 처방: 길이를 루프 전에 한 번만 구해 변수에 저장
  • 결과: dst[3], dst[5], dst[6]… 한 칸씩 건너뛰며 써짐

고친 구현

요약

  • 문제: my_strncat/my_strncpy의 크기 검사·널 문자 처리 오류
  • 원인: 널 문자 자리(+1) 누락, 검사 후 쓰기 순서, strlen을 루프 조건에 사용
  • 처방: dstsize < dlen + slen + 1 검사, i <= slen으로 \0까지 복사, 검사 통과 후 memset
  • 결과: gdb의 display strlen(dst)로 변하는 순간 확인 가능

6. 파일명 생성 — snprintf + time + “wx”

요약

  • 문제: base_YYYYMMDDhhmm[index] 이름을 중복 없이 만들어야 함
  • 원인: fprintf 오용, 쓴 뒤 크기 검사, "rw" 모드, fp 스코프, TOCTOU
  • 처방: snprintf로 쓰기 전 검사, fopen("wx")로 존재 확인과 생성을 원자적으로, errno != EEXIST면 즉시 종료
  • 결과: 있으면 index를 올려 재시도, 그 외 오류는 바로 실패

💡 실습 환경 팁 — gdb 디버깅과 man 페이지

gdb를 쓰려면 gcc에 -g 옵션이 필요하다

vim에서 man 페이지 보기 — 숫자 + Shift+K


📝 교육자료 추가 과제 (8-7) — 아직 구현하지 않음


✅ 정리 — 문자열 코드 점검 체크리스트