Journey to Security/C언어

C 주소 전달 메커니즘: scanf 원형과 작동 원리, 메모리 구조

Cordilog 2026. 2. 14. 14:56

C언어에서 scanf 함수를 사용할 때 막연하게 scanf("%d", &data); 식으로 문자열 리터럴과 주소를 인수로 전달하면 된다고 외우기만 했다면 사실 완전히 이해한 것이라고 볼 수 없다. 

scanf 함수의 원형을 보면 상수 개념과 포인터 개념이 모두 녹아있다.

이를 이해하기 위해서는 C언어가 메모리 상에서 문자열과 일반 변수를 어떻게 취급하는지, 그리고 함수의 원형이 요구하는 데이터의 형태가 무엇인지 알아야 한다.

 

1. 기본 개념: 변수와 상수, 그리고 문자열 리터럴

 

데이터는 프로그램 내에서 변경 가능 여부에 따라 변수와 상수로 구분된다.

  • 변수(Variable): 선언된 메모리 공간의 값이 실행 중에 변할 수 있다.
  • 상수(Constant): 한 번 정해진 값을 변경할 수 없다. const 키워드를 사용하거나 #define 매크로 상수를 사용한다.
  • 문자열 리터럴(String Literal): 소스 코드 내에 " "로 감싸여 직접 입력된 문자열 값을 의미한다. 예를 들어 "%d"가 이에 해당한다.

문자열 리터럴의 특징 문자열 리터럴은 프로그램이 실행될 때 메모리의 읽기 전용 데이터 영역(ReadOnly Data Segment)에 저장된다. C언어에서 문자열 리터럴을 사용하는 것은 해당 문자열이 저장된 메모리의 시작 주소를 사용하는 것과 동일하게 취급된다.

 

2. scanf 함수의 원형과 작동 원리

표준 입력 함수인 scanf는 가변 인자를 받는 구조로 설계되어 있다.

int scanf(const char *format, ...);
  1. const char *format : 데이터를 읽어들일 형식을 지정하는 문자열의 주소를 받는다.
  2. ... (가변 인자) : 입력받은 데이터를 저장할 변수들의 주소를 순차적으로 받는다.

scanf는 첫 번째 인자로 전달받은 주소에 가서 "어떤 형식으로 읽을지"를 파악한 뒤, 두 번째 인자로 전달받은 주소에 "입력된 값을 직접 기록"하는 방식으로 작동한다.

 

3. 코드 예제: myscanf.c

scanf의 동작을 직접 확인하기 위해 사용자 정의 함수 myscanf를 작성한다.

// myscanf.c
#include <stdio.h>

// a: 문자열 리터럴의 주소를 받을 포인터
// b: 정수형 변수의 주소를 받을 포인터
void myscanf(const char *a, int *b);

int main()
{
    int num = 1;
    
    // "%d"는 그 자체로 주소값이므로 &를 붙이지 않음
    // num은 일반 변수이므로 주소를 전달하기 위해 &를 붙임
    myscanf("%d", &num);
    
    printf("num = %d\n", num);
    return 0;
}

void myscanf(const char *a, int *b)
{
    // 전달받은 두 주소를 scanf에 인자로 넘김
    scanf(a, b);
}

 

  • myscanf("%d", &num); 호출 시, "%d"라는 문자열 리터럴이 저장된 메모리 주소num이라는 변수의 메모리 주소가 각각 함수 인자로 복사된다.
  • myscanf 내부의 a와 b는 포인터 변수로서 이 주소들을 보관하고, 이를 다시 scanf에게 전달하여 원본 데이터를 수정하게 한다.

4. 메모리 구조와 주소 전달

5. GDB를 이용한 메모리 변화 분석

디버거를 사용하면 함수 호출 시 실제로 주소가 어떻게 오가는지 명확히 확인할 수 있다.

1️⃣ 컴파일 및 디버깅 시작

32비트 환경을 가정하여 컴파일하고 GDB를 실행한다.

$ gcc -m32 -g -o myscanf myscanf.c
$ gdb -q ./myscanf

 

2️⃣ 중단점 설정 및 변수 정보 확인

 

main 함수에 중단점을 걸고 실행하여 num의 주소를 파악한다.

(gdb) b main          # main 함수에 중단점 설정
(gdb) r              # 프로그램 실행 (run)
(gdb) n              # 다음 줄 실행 (next)
(gdb) p &num         # num의 메모리 주소 출력 (print)
$1 = (int *) 0xffffcf08
(gdb) x/xw &num      # 주소의 실제 데이터 확인 (examine)
0xffffcf08: 0x00000001

 

3️⃣ 함수 진입 및 인자 추적

 

s 명령어myscanf 함수 내부로 진입하여 인자값을 확인한다.

(gdb) s              # 함수 안으로 진입 (step)
(gdb) p a            # 첫 번째 인자(형식 문자열)의 주소 확인
$2 = 0x56557008 "%d"
(gdb) p b            # 두 번째 인자(num의 주소) 확인
$3 = (int *) 0xffff7f08
(gdb) bt             # 현재 함수 호출 스택 확인 (backtrace)
#0  myscanf (a=0x56557008 "%d", b=0xffff7f08) at myscanf.c:17
#1  0x565561e8 in main () at myscanf.c:9

 

p a의 결과인 0x56557008이 문자열 리터럴 "%d"의 시작 주소임을 알 수 있다.

 

 

4️⃣값의 변화 확인

 

scanf 실행 후 포인터 b가 가리키는 위치의 값이 바뀌었는지 검증한다.

(gdb) n              # scanf 실행 (입력값으로 '5' 대입)
5
(gdb) p *b           # 포인터 b의 역참조 값 확인
$4 = 5
(gdb) c              # 프로그램 계속 실행 (continue)

 

5️⃣ main 함수 복귀 및 결과 검증

함수가 종료된 후 main으로 돌아가 num의 최종 상태를 확인한다.

(gdb) n               # myscanf 함수 종료
main () at myscanf.c:10
10          printf("num = %d\n", num);
(gdb) p num           # main의 지역 변수 num 값이 바뀌었음을 확인
$5 = 5
(gdb) c               # 프로그램 종료까지 진행
Continuing.
num = 5
[Inferior 1 (process 4274) exited normally]

 

 

6. 정리

  1. 메모리 주소의 복사: myscanf의 매개변수 bmain&num과 같은 값을 가지지만, 이는 주소라는 데이터가 복사된 것이다. 이 복사된 주소를 통해 main의 스택 영역에 직접 접근(Dereference)할 수 있게 된다.
  2. 형식 지정자의 정체: "%d"는 메모리의 읽기 전용 영역에 존재하는 문자열 데이터의 시작 주소다.
  3. 데이터 무결성 보호: const char *a와 같이 포인터를 상수로 선언함으로써, 함수 내부에서 원본 포맷 문자열이 의도치 않게 변경되는 것을 방지한다.