CSAPP/3장

[CSAPP] 3-7 프로시저

넌뭐가그렇게중요해 2025. 4. 9. 22:51

컴퓨터 프로그램은 여러 작업을 하나의 함수(프로시저)로 묶어 필요할 때마다 호출합니다.
하지만 단순히 함수를 호출하는 것만이 아니라, 함수가 호출될 때마다 스택 프레임(활성화 레코드)이 생성되어 함수의 인자, 반환 주소, 지역 변수 등이 저장되고, 함수 실행이 끝나면 원래의 상태로 복귀하게 됩니다.

이 글에서는 C와 어셈블리어 예시를 통해 프로시저 호출의 내부 동작, 스택 프레임 구성, 그리고 함수 호출과 반환 과정에 대해 자세히 알아보겠습니다. 또한, 프로시저라는 개념이 추상화(abstraction)의 한 형태임을 이해하기 위해, 추상화의 개념과 수학적 함수로서의 관점을 함께 살펴보겠습니다.


1. 추상화와 프로시저

추상화

 

  • 추상화는 복잡한 시스템을 간단하게 만들어, 사용자가 필요한 핵심 기능만을 이용할 수 있도록 하는 기법입니다.
  • 예를 들어, 객체 지향 프로그래밍에서 Person 클래스를 정의하면, 이름, 나이, 생일 등의 필드(속성)와 걷기, 말하기 등의 메서드(동작)로 사람의 특성을 추상화합니다. 이렇게 하면 내부 구현의 복잡성을 감추고, 필요한 기능만 노출할 수 있습니다.

프로시저와 함수의 추상화

  • 프로시저(함수)는 이러한 추상화를 구현하는 중요한 도구입니다.
  • 수학적으로 함수는 입력을 받아 출력을 반환하는 관계입니다.
  • 프로그래밍에서 프로시저는 입력(매개변수)을 받아 특정 작업을 수행한 후 결과를 반환합니다.
  • 이를 통해 복잡한 연산이나 작업을 하나의 단위로 묶어, 코드의 재사용성과 유지보수성을 높일 수 있습니다.

즉, 프로시저는 내부 동작의 복잡성을 감추고, 단순한 인터페이스(입력과 출력)만 노출함으로써, 프로그램 전체를 보다 쉽게 이해하고 관리할 수 있도록 돕습니다.


2. 프로시저(함수) 호출의 기본 개념

C 언어에서의 함수 호출

C 언어에서는 함수 호출 시, 호출되는 함수의 인자들이 전달되고, 함수가 실행된 후 결과값이 반환됩니다.

e.g. in C

int add(int a, int b) {
    int result = a + b;
    return result;
}

int main() {
    int sum = add(3, 4);
    return 0;
}

 

 

여기서 add 함수는 두 인자 a와 b를 받아 덧셈을 수행한 후 결과를 반환합니다.
이처럼, 함수를 사용하면 복잡한 연산을 한 번에 처리할 수 있어 추상화의 효과를 누릴 수 있습니다.

함수 호출의 필요성

  • 코드 재사용성: 동일한 작업을 여러 번 수행할 때, 함수를 호출하면 코드를 중복하지 않아도 됩니다.
  • 모듈화: 각 함수는 독립적인 기능을 수행하므로, 전체 프로그램의 구조를 체계적으로 구성할 수 있습니다.
  • 상태 보존: 함수 호출 시 스택 프레임을 통해 각 함수의 인자, 반환 주소, 지역 변수가 별도로 관리되어 데이터 충돌을 방지합니다.

3. 어셈블리어에서의 프로시저 호출

어셈블리어에서는 C의 함수 호출이 call 명령어와 ret 명령어를 통해 구현됩니다.
함수 호출은 크게 **프로시저 진입(Prologue)**과 **프로시저 종료(Epilogue)**로 나뉩니다.

3.1 프로시저 진입 (Prologue)

  • 인자 전달:
    함수 호출 전에 인자들은 미리 지정된 레지스터(예: x86-64에서는 %rdi, %rsi, %rdx, ...) 또는 스택에 저장됩니다.
  • call 명령어:
    현재 명령어 다음 주소(복귀 주소)를 스택에 저장하고, 호출할 함수의 시작 주소로 점프합니다.
  • 스택 프레임 생성:
    현재 베이스 포인터(%rbp)를 스택에 저장하고, 현재 스택 포인터(%rsp)를 새로운 베이스 포인터로 설정합니다.
  • 지역 변수 공간 할당:
    필요에 따라 스택 포인터를 조정하여 함수 내에서 사용할 지역 변수 저장 공간을 확보합니다.

3.2 프로시저 종료 (Epilogue)

  • 지역 변수 공간 해제:
    함수 실행이 끝나면 스택 포인터를 이전 상태로 복원하여 지역 변수 공간을 해제합니다.
  • 프레임 포인터 복원:
    스택에 저장했던 이전 베이스 포인터를 복원합니다.
  • ret 명령어:
    스택에서 복귀 주소를 꺼내 해당 주소로 점프하여 함수 호출이 이루어졌던 위치로 복귀합니다.

4. 스택 프레임(활성화 레코드)의 구성

스택 프레임은 함수 호출 시 생성되는 작업 공간으로, 함수의 실행 상태를 보존하는 역할을 합니다.
아래 표는 일반적인 스택 프레임의 구성 요소와 각 요소의 역할을 정리한 것입니다.

구성 요소 설명
매개 변수(Parameters) 함수 호출 시 전달된 인자들이 저장됩니다.
리턴 주소(Return Address) 함수 호출 후 복귀할 주소가 저장됩니다. call 명령어가 자동으로 저장합니다. 
이전 프레임 포인터
(Old Frame Pointer)
이전 함수의 베이스 포인터를 저장하여 함수 종료 후 복원에 사용됩니다. 
지역 변수
(Local Variables)
함수 내부에서 선언된 변수들이 저장되는 공간입니다. 

e.g. in Assembly

multiply:
    pushq   %rbp            ; 이전 베이스 포인터 저장
    movq    %rsp, %rbp      ; 새 스택 프레임 설정
    subq    $16, %rsp       ; 지역 변수 공간 할당 (예: 16바이트)
    
    ; 함수 본문: 인자 a, b가 이미 적절한 레지스터 혹은 스택에 저장되어 있다고 가정
    movl    %edi, -4(%rbp)  ; 첫 번째 인자 a 저장
    movl    %esi, -8(%rbp)  ; 두 번째 인자 b 저장
    movl    -4(%rbp), %eax  ; a를 %eax에 로드
    imull   -8(%rbp), %eax  ; a와 b를 곱함 (결과는 %eax에 저장)
    
    movq    %rbp, %rsp      ; 지역 변수 공간 해제
    popq    %rbp            ; 이전 베이스 포인터 복원
    ret                     ; 복귀 주소로 점프

5. 예시를 통한 프로시저 호출의 전체 흐름(C, Assembly)

5.1 e.g. in C

int multiply(int a, int b) {
    int result = a * b;
    return result;
}

int main() {
    int prod = multiply(3, 4);
    return 0;
}

 

  • multiply 함수는 두 인자 a와 b를 받아 곱한 후 결과를 반환합니다.
  • main 함수에서는 multiply를 호출하여 결과를 prod에 저장합니다.

5.2 e.g. in Assembly

; multiply 함수 시작
multiply:
    pushq   %rbp              ; 이전 베이스 포인터 저장
    movq    %rsp, %rbp        ; 새 스택 프레임 설정
    subq    $16, %rsp         ; 지역 변수 공간 할당

    ; C에서는 첫 번째 인자 a는 %edi, 두 번째 b는 %esi에 전달됩니다.
    movl    %edi, -4(%rbp)    ; a 저장
    movl    %esi, -8(%rbp)    ; b 저장

    movl    -4(%rbp), %eax    ; a 값을 %eax로 로드
    imull   -8(%rbp), %eax    ; a와 b를 곱하여 결과를 %eax에 저장

    movq    %rbp, %rsp        ; 지역 변수 공간 해제
    popq    %rbp              ; 이전 베이스 포인터 복원
    ret                       ; 복귀 주소로 점프

; main 함수 시작 (간략화)
main:
    pushq   %rbp
    movq    %rsp, %rbp
    movl    $3, %edi          ; 첫 번째 인자: 3
    movl    $4, %esi          ; 두 번째 인자: 4
    call    multiply          ; multiply 함수 호출 (결과는 %eax에 저장)
    ; prod 변수에 %eax의 값이 저장된다고 가정
    popq    %rbp
    ret

 

이 예시에서 볼 수 있듯이,

  • C 코드에서는 함수 호출이 간단해 보이지만,
  • 어셈블리어에서는 call, push, mov, sub, pop, ret 명령어를 사용해 함수 호출과 스택 프레임 관리가 세밀하게 이루어집니다.

6. 프로시저 호출과 추상화의 연결

추상화는 복잡한 작업을 간단한 인터페이스로 감추어, 사용자가 내부 구현을 몰라도 기능을 사용할 수 있게 해 줍니다.

  • C 언어의 함수는 복잡한 계산이나 작업을 단순한 “입력 → 출력” 형태로 캡슐화하여 추상화를 구현합니다.
  • 프로시저 호출 시, 내부의 스택 프레임 관리와 인자 전달, 반환 등의 과정을 숨기고, 함수의 인터페이스만으로 작업을 수행할 수 있게 해 줍니다.
  • 수학적인 함수처럼, 프로시저는 입력을 받아 특정 출력을 반환합니다.
    예를 들어, multiply(3, 4)는 3과 4라는 입력을 받아 12라는 출력을 반환하며, 이 과정의 내부 동작(스택 프레임 생성, 인자 전달 등)은 사용자에게 감춰집니다.

이러한 추상화를 통해 프로그래머는 복잡한 시스템의 내부 구조를 신경 쓰지 않고, 필요한 기능만을 호출할 수 있으며, 시스템의 유지보수와 확장성이 크게 향상됩니다.


7. 프로시저 호출의 중요 포인트와 실무 적용

  • 상태 보존:
    스택 프레임은 함수 호출 시 기존 함수의 상태(인자, 지역 변수, 리턴 주소 등)를 안전하게 보존합니다.
  • 재귀 호출:
    각 호출마다 독립된 스택 프레임이 생성되어, 재귀 호출 시에도 서로의 상태가 섞이지 않습니다.
  • 최적화:
    컴파일러와 하드웨어는 함수 호출과 스택 프레임 관리를 최적화하여 실행 속도를 높입니다. 예를 들어, 인라인 함수나 레지스터 할당 최적화가 있습니다.
  • 추상화:
    프로시저는 복잡한 내부 과정을 추상화하여, 단순한 “함수 호출”이라는 인터페이스만으로 기능을 사용할 수 있게 해 줍니다. 이는 수학의 함수 개념과 유사하며, 사용자 입장에서는 내부 로직을 신경 쓰지 않고 결과만 얻으면 됩니다.

8. 마무리

이번 글에서는 CSAPP 3.7장의 프로시저 호출과 스택 프레임의 내부 동작을 자세히 살펴보았습니다.

  • C와 어셈블리어 예시를 통해 함수 호출 시 인자 전달, 스택 프레임 생성, 그리고 함수 종료 후 복귀 과정을 명확히 이해했습니다.
  • 또한, 추상화 개념을 추가하여, 함수(프로시저)가 복잡한 작업을 어떻게 단순화하고 모듈화 하는지, 수학적인 함수와 유사한 역할을 하는지 설명했습니다.
  • 이러한 이해는 나중에 프로그램 최적화, 디버깅, 시스템 프로그래밍 등 다양한 분야에서 중요한 기초 지식이 됩니다.

 

'CSAPP > 3장' 카테고리의 다른 글

[CSAPP] 3-8 배열의 할당과 접근  (0) 2025.04.15
[CSAPP] 3-6 제어문  (0) 2025.04.09
[CSAPP] 3-5 산술연산과 논리연산  (0) 2025.04.08
[CSAPP] 3-4 정보 접근하기  (0) 2025.04.08
[CSAPP] 3-3 데이터 형식  (0) 2025.04.08