jun-wiki

View My GitHub Profile

Posts (Latest 10 updated) :
Read all
Contents:
  1. 3.7 프로시저
    1. 3.7.1 런타임 스택
      1. 요약
    2. 3.7.2 제어 전달
    3. 3.7.3 데이터 전달
      1. 데이터 크기 별 레지스터
    4. 3.7.4 스택에 할당되는 지역 저장 공간
      1. 재요약
    5. 3.7.5 레지스터에 저장되는 지역 데이터
    6. 3.7.6 재귀 프로시저
      1. 코드 예시 및 분석

3.5부터 스킵해서

현재 3.7부터 하겠다

당신의 불만 3.7로 대체되었다

and I also 씨에스 좋아


3.7 프로시저

프로시저는 소프트웨어에서 중요한 추상화(Abstraction) 수단이다

특정 기능을 수행하는 코드 블록이란 뜻이다


특정 기능을 구현하는 코드를 인자(arguments) 및 선택적인 반환 값(return value)과 함께 묶어 프로그램의 여러 지점에서 호출할 수 있도록 한다

이러한 프로시저를 호출 할 때에는 몇가지 메커니즘이 필요하다

  • 제어 (어디서 실행을 시작하고 끝낼지)

  • 데이터 (인자와 반환값)

  • 메모리 (임시 변수 저장 공간)


간단히 말해 프로시저(함수)는 코드 재사용을 위한 장치

그리고 이를 호출할때 제어, 데이터, 메모리 관리를 효율적으로 처리해야 한다



3.7.1 런타임 스택

  • 요점: 프로시저 호출 메커니즘은 후입선출(LIFO) 방식의 스택(stack) 메모리 관리를 활용하여 구현


  • 상위 프로시저가 하위 프로시저 호출시 하위 프로시저만 작동 그리고 다시 반환될 떄 할당했던 로컬 저장 공간 해제 가능

    • 이를 스택을 통해 관리 ㄱㄴ


명시적으로 초기화되지 않은 데이터를 위한 공간은 스택 포인터를 적절한 양만큼 감소시킴으로써 할당되며,
함수가 완료되면 스택 포인터를 다시 증가시킴으로써 해제


  • 각 프로시저 호출은 스택에 자신만의 스택 프레임 생성

    • 스택에 할당되는 독립적 메모리 영역

    • 함수 호출 시 스택프레임 추가 되고 함수 종료 시 제거

    • 현재 실행 중인 함수의 스택 프레임이 항상 스택의 최상단(Top)에 위치

    • 포함가능 정보:

      • 리턴 주소(return address): 함수 실행이 끝난 뒤 돌아갈 위치.

      • 함수 인자(Arguments): 레지스터로 다 못 넘길 경우, 나머지 인자가 저장됨.

      • 로컬 변수(Local Variables): 함수 내부에서만 사용하는 변수.

      • 저장된 레지스터(Saved Registers): 함수 실행 전 레지스터의 값 백업(필요한 경우).

      • 임시 데이터(Temporary data): 함수 실행 중 필요한 임시 공간.

요약

각 프로시저 호출은 스택에 자신만의 스택 프레임을 생성하며, 실행 중인 함수의 스택 프레임이 항상 스택의 맨 위에 쌓인다.
함수가 끝나면 프레임 전체가 한 번에 제거되어, 메모리가 효율적으로 회수된다.
x86-64 시스템은 최대 6개 인자까지 레지스터로, 그 외 인자는 스택 프레임에 저장하는 최적화 방식을 쓴다.

즉, 함수 호출/복귀와 임시 데이터, 로컬 변수 관리는 스택과 스택 프레임을 통해 체계적으로 이루어진다.




3.7.2 제어 전달

  • 요점 : 프로시저 호출 및 반환은 callret 명령어를 통해 이루어지며, 이는 스택을 활용하여 프로그램 실행 흐름을 제어한다


  • call 명령어

    • 프로시저 호출 시, 다음 명령어의 리턴 주소를 스택에 푸시하고,

    • 호출할 프로시저의 시작 주소로 점프하여 제어를 넘김

  • ret 명령어

    • 스택의 맨 위에 저장된 리턴 주소를 팝(pop)해서,

    • 프로그램 카운터(PC)에 로드함으로써 호출자 함수로 제어를 돌려줌


이 구조 덕분에 함수 실행이 끝나면 항상 “정확한 위치”로 안전하게 복귀할 수 있다



3.7.3 데이터 전달

  • 요점 : 프로시저 간의 인자와 반환 값은 주로 레지스터를 통해 전달되며, 필요한 경우 스택을 사용


함수의 반환 값은 일반적으로 %rax 레지스터에 저장

x86-64에서는 최대 6개의 정수형(정수와 포인터) 인자를 레지스터를 통해 전달 가능

6개 초과시 스택에 저장하여 전달

데이터 크기 별 레지스터

인자 번호 1 2 3 4 5 6
64비트(8바이트) %rdi %rsi %rdx %rcx %r8 %r9
32비트 %edi %esi %edx %ecx %r8d %r9d
16비트 %di %si %dx %cx %r8w %r9w
8비트 %dil %sil %dl %cl %r8b %r9b



3.7.4 스택에 할당되는 지역 저장 공간

  • 요점 : 모든 지역 변수가 레지스터에 저장될 수 있는 것은 아니며, 특히 지역 배열이나 주소 연산자(&)가 사용되는 경우 스택에 공간이 할당됨


컴파일러는 모든 지역 데이터를 레지스터 보관 할 수 없을 경우, 스택에 지역 저장 공간 할당

레지스터만으로 부족하거나, 주소/배열/구조체 변수 등은 스택 프레임 내 로컬 변수 영역에 저장

프로시저가 완료되면 스택 프레임은 스택 포인터를 증가시켜 해제

런타임 스택은 필요할 때 로컬 저장 공간을 쉽게 할당/해제할 수 있는 단순한 메커니즘을 제공


재요약

  • 지역 변수의 주소(&var), 배열, 구조체 등은 반드시 메모리(스택)에 위치해야 하므로,
    함수 진입 시 스택 포인터를 감소시켜 공간을 할당한다.

  • 함수가 종료되면, 스택 포인터를 원래대로 되돌려 자동으로 모든 지역 저장 공간을 해제한다.

  • 즉, x86-64 런타임 스택은 함수별로 필요한 지역 변수를 안전하고 효율적으로 관리하는 기본 장치다.



3.7.5 레지스터에 저장되는 지역 데이터

  • 요점 : 레지스터는 빠르지만 수가 제한적이므로, 프로시저 간에 레지스터 값을 어떻게 보존할지에 대한 규칙(호출 수신자-저장/호출자-저장)이 정의되어 있다

    누구나 빠른 걸 쓰고 싶지만 제한적이다
    그러니 규칙을 정해 사용한다


  • 레지스터 %rbx, %rbp, 그리고 %r12~%r15피호출자 저장(callee-saved) 레지스터로 분류

  • 스택 포인터 %rsp를 제외한 다른 모든 레지스터는 호출자 저장(caller-saved) 레지스터로 분류

뭔개소리지 할 수 있는데

  • 피호출자 저장호출된 함수가 보존해야 한다는 거다

  • 호출자 저장호출하는 함수가 보존해야 한다는 거고 말이다


말할수록 개소리 같지만 이걸 기억해라

피호출자 저장 레지스터에 저장된 함수의 경우 마음대로 사용하더라도 리턴할때 호출 전 값으로 되돌려 놓으면된다

호출자 저장 레지스터는 호출자가 저장해놓아야 한다는 뜻이기에 피호출자가 깽판 쳐놔도 상관 없다… 가 핵심이다

피호출자 저장 = 썼어도 끝나고 돌려놓으면 ㄱㅊ

호출자 저장 = 누가 망가뜨려도 책임못지니 지가 챙겨야 함


한문장으로 요약하자면…

이름에 저장이 붙은 주체가 책임지면 된다



3.7.6 재귀 프로시저

  • 요점 : 각 재귀 호출은 자신만의 스택 프레임을 가지므로, 로컬 변수와 상태 정보가 서로 간섭하지 않고 안전하게 저장·복원


기존에 정해놓은 규칙들 덕택에 각 프로시저 호출은 서로 간섭하지 않는
개인 공간을 보장 받았다

그 덕에 재귀 함수의 호출도 일반 함수 호출과 다를 바 없다




코드 예시 및 분석

코드만 봐선 나도 잘 모르겠어서 관련 해설 덧붙였다


C코드

long rfact(long n)
{
    long result;
    if (n <= 1)
        result = 1;
    else
        result = n * rfact(n-1);
    return result;
}

설명:

  • long rfact(oong n):
    n을 입력받아, 팩토리얼 값을 반환하는 함수

  • long result;
    결과를 저장할 변수

  • if (n <= 1) result = 1;
    n이 1 이하라면(즉, 0이나 1이면) 팩토리얼은 1이니까 result를 1로 저장.

  • else result = n * rfact(n-1);
    그렇지 않으면(즉, n이 2 이상이면) result에 n × rfact(n-1) (즉, n × (n-1)!) 값을 저장.

  • return result;
    계산한 값을 반환.




생성된 어셈블리 코드

long rfact(long n)        // n in %rdi
1 rfact:
2   pushq %rbx            // %rbx 저장
3   movq %rdi, %rbx       // n 피호출자 저장 레지스터에 저장
4   movl $1, %eax         // 반환값 = 1 설정
5   cmpq $1, %rdi         // n:1 비교
6   jle .L35              // n <= 1이면 done으로 점프
7   leaq -1(%rdi), %rdi   // n-1 계산
8   call rfact            // rfact(n-1) 호출
9   imulq %rbx, %rax      // 결과값에 n 곱하기
10  .L35: done:
11  popq %rbx             // %rbx 복원
12  ret                   // 반환



C코드와의 연계

어셈블리 의미 연결되는 C 코드/설명
1 rfact: 함수 시작 long rfact(long n)
2 pushq %rbx %rbx(레지스터) 값 임시 저장(스택에 push) %rbx는 이 함수에서 n 보관용, 다른 함수의 %rbx 값 보호
3 movq %rdi, %rbx n의 값을 %rbx에 복사 n을 %rbx에 저장해서, 재귀 호출 이후에도 n 값 보존
4 movl $1, %eax %eax = 1 (결과값 기본 세팅) result = 1;을 미리 준비
5 cmpq $1, %rdi n과 1 비교 if (n <= 1)
6 jle .L35 n <= 1이면 10번(.L35)으로 점프 조건문(1 이하) 충족 시 1 반환
7 leaq -1(%rdi), %rdi n-1 값을 %rdi에 저장 재귀 호출 준비: rfact(n-1)
8 call rfact rfact 함수 재귀 호출 result = n * rfact(n-1);rfact(n-1) 호출
9 imulq %rbx, %rax %rbx(n) × %rax(재귀 호출 결과) 재귀 호출 반환값 × n = 팩토리얼
10 .L35: done: 조건 분기 후 도착 if/else 끝, 반환 준비
11 popq %rbx 저장했던 %rbx 값 복원 함수 진입 시 push했던 값 되돌리기
12 ret 함수 반환 return result;



간단 요약 (흐름)

  1. 함수 시작하면, n%rdi에 들어옴

  2. %rbx는 앞으로 n을 보관하는 용도(재귀 호출에 값이 덮어씌워지는 것을 방지)

  3. n이 1 이하라면(기저조건) 바로 1 반환

  4. n이 2 이상이면, n-1로 자기 자신을 재귀 호출

  5. 재귀 호출에서 돌아온 값을 n과 곱해서 반환

  6. 함수가 끝나면 스택에서 %rbx 값을 복원 후 리턴


흐름 다시 한번 보기

rfact(long n) 함수는

  • n이 1 이하일 때1을 반환하고

  • n이 2 이상일 때n * rfact(n-1)을 반환


즉,

  • rfact(3)이면

    • 3 * rfact(2)

      • 2 * rfact(1)

        • 1 (기저조건)
      • 2 * 1 = 2

    • 3 * 2 = 6