3.5부터 스킵해서
현재 3.7부터 하겠다
당신의 불만 3.7로 대체되었다
and I also 씨에스 좋아
프로시저는 소프트웨어에서 중요한 추상화(Abstraction) 수단이다
특정 기능을 수행하는 코드 블록이란 뜻이다
특정 기능을 구현하는 코드를 인자(arguments) 및 선택적인 반환 값(return value)과 함께 묶어 프로그램의 여러 지점에서 호출할 수 있도록 한다
이러한 프로시저를 호출 할 때에는 몇가지 메커니즘이 필요하다
제어 (어디서 실행을 시작하고 끝낼지)
데이터 (인자와 반환값)
메모리 (임시 변수 저장 공간)
간단히 말해 프로시저(함수)는 코드 재사용을 위한 장치다
그리고 이를 호출할때 제어, 데이터, 메모리 관리를 효율적으로 처리해야 한다
상위 프로시저가 하위 프로시저 호출시 하위 프로시저만 작동 그리고 다시 반환될 떄 할당했던 로컬 저장 공간 해제 가능
명시적으로 초기화되지 않은 데이터를 위한 공간은 스택 포인터를 적절한 양만큼 감소시킴으로써 할당되며,
함수가 완료되면 스택 포인터를 다시 증가시킴으로써 해제
각 프로시저 호출은 스택에 자신만의 스택 프레임 생성
스택에 할당되는 독립적 메모리 영역
함수 호출 시 스택프레임 추가 되고 함수 종료 시 제거됨
현재 실행 중인 함수의 스택 프레임이 항상 스택의 최상단(Top)에 위치
포함가능 정보:
리턴 주소(return address): 함수 실행이 끝난 뒤 돌아갈 위치.
함수 인자(Arguments): 레지스터로 다 못 넘길 경우, 나머지 인자가 저장됨.
로컬 변수(Local Variables): 함수 내부에서만 사용하는 변수.
저장된 레지스터(Saved Registers): 함수 실행 전 레지스터의 값 백업(필요한 경우).
임시 데이터(Temporary data): 함수 실행 중 필요한 임시 공간.
각 프로시저 호출은 스택에 자신만의 스택 프레임을 생성하며, 실행 중인 함수의 스택 프레임이 항상 스택의 맨 위에 쌓인다.
함수가 끝나면 프레임 전체가 한 번에 제거되어, 메모리가 효율적으로 회수된다.
x86-64 시스템은 최대 6개 인자까지 레지스터로, 그 외 인자는 스택 프레임에 저장하는 최적화 방식을 쓴다.
즉, 함수 호출/복귀와 임시 데이터, 로컬 변수 관리는 스택과 스택 프레임을 통해 체계적으로 이루어진다.
call
및 ret
명령어를 통해 이루어지며, 이는 스택을 활용하여 프로그램 실행 흐름을 제어한다call
명령어
프로시저 호출 시, 다음 명령어의 리턴 주소를 스택에 푸시하고,
호출할 프로시저의 시작 주소로 점프하여 제어를 넘김
ret
명령어
스택의 맨 위에 저장된 리턴 주소를 팝(pop)해서,
프로그램 카운터(PC)에 로드함으로써 호출자 함수로 제어를 돌려줌
이 구조 덕분에 함수 실행이 끝나면 항상 “정확한 위치”로 안전하게 복귀할 수 있다
함수의 반환 값은 일반적으로 %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 |
&
)가 사용되는 경우 스택에 공간이 할당됨컴파일러는 모든 지역 데이터를 레지스터 보관 할 수 없을 경우, 스택에 지역 저장 공간 할당
레지스터만으로 부족하거나, 주소/배열/구조체 변수 등은 스택 프레임 내 로컬 변수 영역에 저장
프로시저가 완료되면 스택 프레임은 스택 포인터를 증가시켜 해제
런타임 스택은 필요할 때 로컬 저장 공간을 쉽게 할당/해제할 수 있는 단순한 메커니즘을 제공
지역 변수의 주소(&var
), 배열, 구조체 등은 반드시 메모리(스택)에 위치해야 하므로,
함수 진입 시 스택 포인터를 감소시켜 공간을 할당한다.
함수가 종료되면, 스택 포인터를 원래대로 되돌려 자동으로 모든 지역 저장 공간을 해제한다.
즉, x86-64 런타임 스택은 함수별로 필요한 지역 변수를 안전하고 효율적으로 관리하는 기본 장치다.
요점 : 레지스터는 빠르지만 수가 제한적이므로, 프로시저 간에 레지스터 값을 어떻게 보존할지에 대한 규칙(호출 수신자-저장/호출자-저장)이 정의되어 있다
누구나 빠른 걸 쓰고 싶지만 제한적이다
그러니 규칙을 정해 사용한다
레지스터 %rbx
, %rbp,
그리고 %r12
~%r15
는 피호출자 저장(callee-saved) 레지스터로 분류
스택 포인터 %rsp
를 제외한 다른 모든 레지스터는 호출자 저장(caller-saved) 레지스터로 분류
뭔개소리지 할 수 있는데
피호출자 저장은 호출된 함수가 보존해야 한다는 거다
호출자 저장은 호출하는 함수가 보존해야 한다는 거고 말이다
말할수록 개소리 같지만 이걸 기억해라
피호출자 저장 레지스터에 저장된 함수의 경우 마음대로 사용하더라도 리턴할때 호출 전 값으로 되돌려 놓으면된다
호출자 저장 레지스터는 호출자가 저장해놓아야 한다는 뜻이기에 피호출자가 깽판 쳐놔도 상관 없다… 가 핵심이다
피호출자 저장 = 썼어도 끝나고 돌려놓으면 ㄱㅊ
호출자 저장 = 누가 망가뜨려도 책임못지니 지가 챙겨야 함
한문장으로 요약하자면…
이름에 저장이 붙은 주체가 책임지면 된다
기존에 정해놓은 규칙들 덕택에 각 프로시저 호출은 서로 간섭하지 않는
개인 공간을 보장 받았다
그 덕에 재귀 함수의 호출도 일반 함수 호출과 다를 바 없다
코드만 봐선 나도 잘 모르겠어서 관련 해설 덧붙였다
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; |
함수 시작하면, n
이 %rdi
에 들어옴
%rbx
는 앞으로 n
을 보관하는 용도(재귀 호출에 값이 덮어씌워지는 것을 방지)
n
이 1 이하라면(기저조건) 바로 1 반환
n
이 2 이상이면, n-1
로 자기 자신을 재귀 호출
재귀 호출에서 돌아온 값을 n
과 곱해서 반환
함수가 끝나면 스택에서 %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