일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- Unreal Engine
- Unreal
- 포인터
- Unreal Engine5
- 언리얼
- 소멸자
- 데이터구조
- 게임개발
- cpp개발
- 게임프로그래밍패턴
- CPP
- 복사생성자
- 언리얼5
- BehaviorTree
- 언리얼 엔진5
- UE5
- 게임 개발
- 복사대입연산자
- 언리얼엔진
- 프로그래밍
- 언리얼엔진5
- C++
- C언어
- AI
- 생성자
- 배열
- 디자인패턴
- 프로세스
- effectivec++
- 자료구조
- Today
- Total
리얼 개발
[CS] 함수 호출 규약 (Function Calling Convention) 본문
함수 호출 규약 ( Calling Convention ) 이란, 함수를 호출하는 방식에 대한 약속이다.
함수를 호출하는데 뭘 약속까지 하냐? 라고 생각할 수도 있지만 함수 호출에는 꽤나 많은 일들이 일어난다.
기본적인 레지스터 stack frame 등은 설명하지 않고 넘어가겠다.
함수 호출 규약으로 정하는건 다음과 같다.
- 인자 전달 순서
- 인자 전달 방법
- Stack Frame을 정리하는 방법
먼저 함수 호출 규약을 보기 전에 함수가 호출되면 컴퓨터에선 어떤 일이 벌어지는지 확인해보자.
함수 호출 시 다음과 같은 일이 일어난다.
- call 명령어 실행
- push ip + jump [함수의 주소]
- atomic 명령어로 한 클락에 실행됨
- 함수 프롤로그
- 해당 함수의 stack frame을 만드는 일
- push ebp → mov ebp, esp
- 함수 에필로그
- 해당 함수의 stack frame을 없애는 일
- pop ebp → ret
- ret 명령어 실행
- pop ip + jump[ip]
- atomic 명령어로 한 클락에 실행됨
코드로 확인해보자.
컴파일 설정을 통해서 해당 코드를 컴파일 할때 dbg 툴로 보기 쉽게 만들었다.
빌드된 .exe 파일을 dbg 툴에 넣으면 코드가 실행될 때 내부적으로 어떤 기계 명령어들이 수행되는지 확인할 수 있다.
우리가 주목할 것은 call과 ret 명령어이다
call 부터 봐보자 함수가 호출되기 직전이다.
call 명령어가 실행되고 stack 맨 위에 어떤 값이 들어갔는데, 그 값을 보니 함수 호출 다음 줄의 명령어 주소이다. 이 값을 먼저 저장한 뒤 함수의 시작 주소로 ip가 이동한다.
그리고 push ebp와 mov ebp, esp 명령어를 통해서 호출한 함수의 bp를 정한 뒤 함수의 몸체에 있는 명령들을 실행한다.
함수의 몸체가 끝나면 pop ebp를 통해 함수를 부른 caller의 bp 주소를 bp 레지스터에 다시 넣어준다.
마지막으로 ret 명령어가 실행되면 stack 에 올라와있던 bp,ip 값이 사라져 있고 ip도 정확히 함수 호출 다음 줄을 가리키고 있는 것을 볼 수 있다.
이제 인자가 있는 경우의 함수를 알아보자. 코드는 다음과 같다.
dbg로 보면 다음과 같다.
변수를 정의하고 인자로 넣어주는 과정을 표시해두었다. 이때 인자를 반대로 넣어주고 있는데 그 이유는 함수 stack frame의 입장에서 인자들의 위치를 보면 아래로 내려다 보게 된다. 인자를 거꾸로 줘야 함수 stack frame 입장에선 순서대로 오는 것으로 보인다.
그리고 call 을 통해 함수를 호출하고( push ip, jump[func()] ), caller의 bp를 저장한다. 함수의 역할을 모두 마치고 caller의 bp를 꺼내며 ret를 통해 함수를 끝마친다.( pop ip, jump[ip] )
함수를 끝 마치고 나서 못 보던 명령어가 있다. add esp, c 인데, 이 명령어가 stack 을 정리하는 명령어이다. stack은 주소가 높은 곳에서 낮은 곳으로 자라기 때문에 sp에 add 명령어로 주소를 더 해줌으로써 스택을 정리한다.
이때 우리가 알던 쓰레기값의 진실이 나오는데, stack을 정리한다는 것은 단순히 stack pointer 를 이동시키는 것이다. stack에는 그대로 값이 남게 된다. 우리가 변수를 선언하고 초기화를 안해주었을 때 들어가는 쓰레기 값들이 이렇게 남아있는 값들이 들어가는 것이다.
이제 마지막으로 함수 호출 규약에 대해 알아볼 차례이다.
대표적으로 다음 3가지의 호출 규약이 있다.
- cdecl
- stdcall
- fastcall
참고로 위에서 예시로 든 코드는 cdecl 방식을 따르고 있다.
cdecl 방식
c언어에서 주로 사용되는 방식으로 Caller에서 stack 을 정리한다. 위에서 본 것과 같이 add 명령어로 sp에 인자를 넣은 바이트만큼 더해준다.
장점은 가변 길이의 파라미터를 전달할 수 있다.
단점은 함수의 호출이 많아지면 코드가 길어진다.
stdcall 방식
Win32 API에서 사용되는 방식으로 Callee에서 stack 을 정리한다.
함수 앞에 명시적으로 함수 호출 규약 키워드를 붙여 원하는 방식으로 설정할 수 있다.
dbg로 보면
ret 옆에 C가 생기는데 10진수로 하면 12 이다. 따라서 int형 변수 3개를 인자로 넘겼을 때 스택에 쌓인 12바이트만큼 Callee에서 sp를 움직이도록 한다.
장점은 Caller 의 크기가 작아지며 함수를 여러번 호출해도 한 줄로 정리할 수 있다.
단점은 가변 인자를 사용할 수 없다.
fastcall 방식
stdcall 방식과 유사하지만 함수에 전달하는 파라미터를 스택 메모리가 아닌 레지스터를 이용하여 전달한다.
장점은 스택 메모리 대신 레지스터를 사용하기에 속도가 빠르다.
'Computer Science > 기타' 카테고리의 다른 글
[CS] 프로그램이 실행되기 까지 과정 (0) | 2025.03.05 |
---|