<컴퓨터 구조에 대한 세 번째 이야기>
[공부했던 것을 되짚어보며]
이제 3장의 시작입니다.
책에서는 항상 새로운 장을 시작하면서 컴퓨터 구조에 대한 이야기를 짚어보고 넘어가는 구조로 되어있습니다.
이번에 다룰 내용은 지난 2장의 시작이었던 컴퓨터 구조에 대한 두 번째 이야기에서 이어집니다.
https://sevenshards.tistory.com/40
이 내용을 알고 있어야 이번 챕터에서 설명하는 내용을 이해하실 수 있습니다.
그러니 복습을 한 번 하시고 따라오시면 좋을 것 같습니다.
그리고 이번 챕터는 가급적이면 그림을 그려가면서 이해하시는 것을 권장드립니다.
[절차적 함수 호출(Procedure Call) 지원 CPU 모델]
이전 컴퓨터 구조 이야기에서는 CPU를 설계(엄밀히 따지면 레지스터 설계)를 했었습니다.
그리고 사칙연산 명령어와 CPU와 메모리 사이에 데이터를 주고 받기 위해서 LOAD, STORE 명령어까지 설계했고요.
이번에는 더 나아가서 '함수 호출'에 대한 부분을 생각해보려고 합니다.
우리가 지금까지 프로그램을 코드로 작성하면서 만들었던 함수의 호출은 어떻게 이뤄질까요?
한 가지 확실한 것은 함수호출 역시 CPU의 도움을 받아야 수행이 가능합니다.
다시 말해서 함수호출은 소프트웨어적인 기능이 아니라 하드웨어에 종속적인 부분이 크다는 것입니다.
그래서 함수가 호출되는 방식은 CPU에 따라 차이를 보이기도 합니다.
실제로 ARM 코어에서는 ATPCS(ARM-Thumb Procedure Call Standard)라는 것이 있습니다.
이 표준은 함수 전달인자와 리턴 어드레스(함수 호출 완료 후 돌아갈 주소)를 레지스터에 저장하기로 결정합니다.
그리고 어떻게 저장할 것인지에 대한 저장방식에 대한 표준을 정의한 것입니다.
따라서 이 표준을 고려하여 ARM 코어의 레지스터들도 설계가 되어있습니다.
그리고 ARM 컴파일러도 이 표준에 맞게 바이너리 코드를 생성하도록 설계되어 있습니다.
앞으로 설명할 부분은 '컴퓨터 구조 두 번째 이야기'에서 이어집니다.
그래서 여기서 예로 드는 모델은 우리가 사용하는 상용 컴퓨터와 완전히 일치하는 모델이 아닙니다.
하지만 컴퓨터의 구조 자체를 이해하기 위해서 대부분의 CPU에 해당하는 부분을 설명하게 됩니다.
지금 개념을 잘 알아두면 앞으로 인텔이나 AMD같은 특정 CPU를 공부하는데 큰 도움이 될 것입니다.
[스택 프레임(Stack Frame) 구조]
함수 내에 선언된 변수가 '스택(Stack)'영역에 할당된다는 것은 다들 알고 있는 사실입니다.
위 그림은 함수 호출과 스택의 관계입니다.
함수 호출 과정에서 할당되는 메모리 블록(지역 변수 선언으로 인해 할당되는 메모리 블록)이 있습니다.
이를 '스택 프레임(Stack Frame)'이라고 합니다.
그래서 main 함수 내에서 선언된 변수 a, b는 main 스택 프레임을 구성하게 됩니다.
이제 함수 호출이 완료된 경우(함수가 return을 한 경우)를 한 번 생각해봅시다.
우리는 함수 호출이 완료되면 지역 변수에는 더 이상 접근할 수 없다는 사실을 알고 있습니다.
주소값을 알고 있더라도 접근이 불가능합니다.
이게 의미하는 것은 할당되었던 메모리가 반환되었다는 것입니다.
그래서 정리하자면 다음과 같습니다.
"임의의 함수가 호출되면서 이 함수 내에 선언된 변수가 스택에 할당이 된다.
이 메모리 블록을 가리켜 스택 프레임이라고 하는데, 해당 함수가 반환되면 이 스택 프레임은 모두 반환된다."
[SP 레지스터]
지역변수를 위해 사용되는 메모리 공간이 스택이라고 불리우는 이유는 메모리의 구조적 특성에 있습니다.
자료구조를 공부하신 분들은 아시겠지만 스택은 Last In First Out(LIFO)의 구조를 가지게 됩니다.
나중에 들어온 것이 먼저 나가기 때문입니다.
마찬가지로 스택 프레임도 가장 나중에 들어온 것이 가장 먼저 반환됩니다.
그래서 스택에 계속 데이터를 쌓고 반환을 하려면 현재 어느 위치까지 데이터를 저장했는지 기억하고 있어야 합니다.
즉, 쌓아 올린 스택의 위치를 기억하고 있어야 한다는 것입니다.
그래서 CPU 내에서 레지스터 중에 이름을 붙였던 것이 있는데 sp라는 것이 있습니다.
SP는 Stack Pointer로, 스택 프레임의 top 위치(어디까지 메모리를 할당했는지)를 기억하는 레지스터입니다.
그래서 변수가 하나씩 할당될 때마다 증가하여 다음 변수가 할당될 메모리 위치를 가리키게 됩니다.
마찬가지로 스택 프레임이 반환이 되면 스택 프레임 단위로 이동을 해서 스택 프레임을 반환하는 역할을 합니다.
여기서 '반환한다'라는 것이 '해당 데이터를 싹 비워야된다'는 생각을 하시는 분들이 있을겁니다.
스택 포인터를 이동만 시켜도 되는 이유는, 새로운 변수가 할당이 되면 그 자리에는 새로운 데이터로 덮어씌워집니다.
따라서 반환 후에 스택 포인터를 이전 위치로 이동을 시키기만 해도 스택 프레임은 반환된 것으로 볼 수 있습니다.
그런데 위에 있는 스택 포인터는 한 가지 문제가 있습니다.
메모리가 할당되어 스택에 쌓아 올리는 과정에서는 문제가 없습니다.
문제는 함수가 반환되면 어느 프레임까지 반환하면 될지 알 방법이 없는 것입니다.
우리는 그림을 통해서, 그리고 계산을 했기 때문에 어디까지 반환하면 되는지 알고 있습니다.
하지만 CPU는 그걸 모릅니다.
스택 포인터는 지금까지 할당된 메모리의 위치만 알고 있을 뿐이지, 얼마만큼의 메모리 공간을 할당했는지는 모릅니다.
[프레임 포인터(Frame Pointer) 레지스터]
그래서 스택 포인터의 문제를 해결하기 위해 도입된 것이 바로 프레임 포인터 레지스터(Frame Pointer, 이하 FP)입니다.
SP의 위치를 함수 호출 이전으로 되돌리기 위해 사용하는 레지스터입니다.
쉽게 말하면 'SP의 백업 == FP 레지스터'입니다.
이제 문제가 다 해결된 것 같습니다만...
아직도 남은 문제가 있습니다.
SP를 백업하는 FP 레지스터의 경우에도 연속적인 함수 호출이 이뤄진다면?
FP의 값이 덮어씌워져버려서 문제가 생기게 됩니다.
위의 그림을 예로 들면 바로 이해가 될겁니다.
fc1을 호출한 이후 fc2가 호출하고 fc2가 반환이 되면 스택 프레임을 반환하게 되죠.
그런데? fc1 스택 프레임을 반환할 방법이 아예 사라집니다.
값이 덮어씌워져서 그렇습니다.
[프레임 포인터를 스택에 저장해봅시다!]
이래도 문제 저래도 문제.
도대체 뭘 어떻게 하라는 것일까요?
근데 이것도 생각보다 간단하게 해결이 됩니다.
아까 SP의 문제를 해결하기 위해서는 어떻게 해결했는지 기억나시죠?
백업으로 FP를 뒀습니다.
그러면 FP도 마찬가지로 백업을 하면 되는 것입니다.
그럼 또 레지스터를 갖다 붙일 것이냐? 그건 아닙니다.
이 FP의 값을 메모리, 다시 말해서 스택에 저장하자는 것입니다.
지금부터 과정을 하나하나 설명하겠습니다.
이 글을 시작하면서도 말했던 부분이지만 가급적이면 그림을 그려가면서 이해하시는 것이 가장 좋습니다.
SP는 실선, FP는 점선으로 표현하겠습니다.
[함수 호출 과정(main부터)]
1. 맨 처음 main 함수가 호출되면서 a와 b가 메모리에 할당되기 전까지는 똑같은 0x00을 가리킵니다.
2. main 함수의 변수 a, b가 할당이 되면서 sp는 0x08을 가리키게 됩니다.
3. 이후 fc1 함수를 호출하게 됩니다.
여기서 FP의 주소값을 스택에 저장하게 됩니다.
SP가 가리키는 메모리 공간에 fp의 값을 저장하고, FP에는 SP의 값을 저장합니다.
그리고 SP의 값을 증가시키면 됩니다.
이후 과정은 동일하기 때문에 fc2까지 호출했다고 하면 다음과 같이 됩니다.
[함수 반환 과정(main까지)]
1. fc2 함수 호출이 완료되면(반환하면) FP에 저장된 값을 SP에 저장합니다.
그리고 SP가 가리키고 있는 메모리 공간의 값을 FP에 저장합니다.
2. fc1 함수 호출도 완료되면 위와 같은 과정을 거칩니다.
FP에 저장된 값을 SP에 저장, 그리고 SP가 가리키는 메모리 공간의 값을 FP에 저장합니다.
3. 마지막으로 main 함수 호출까지 끝이 나면 맨 처음 상태인 SP와 FP 모두 0x00을 가리키는 상태가 됩니다.
[함수 호출 인자의 전달과 PUSH & POP 명령어 설계]
이번 챕터의 핵심 내용은 '함수 호출'입니다.
그리고 함수의 지역 변수가 함수 호출 시 어떻게 저장되고 반환이 되는지에 대해서 알아보았고요.
보통은 '함수 호출'과 '프로시저(Procedure) 호출'을 구분하게 됩니다.
'입력에 대한 출력이 반환값으로 있는 경우'를 '함수 호출'이라고 합니다.
반면에 '반환값이 없이 모듈화해놓은 서브 루틴(Sub-Routine)의 실행을 위한 호출'을 '프로시저 호출'이라고 합니다.
아마 데이터베이스를 공부하면 프로시저 부분에서 이 개념을 좀 더 잘 이해하실 수 있을거라고 봅니다.
그런데 이런 정의를 구분하는 것보다 중요한 것은 실질적인 부분입니다.
1. "함수 호출 시 실행위치의 이동은 어떻게 이뤄지는가?"
2. "함수 호출 시 전달되는 인자들은 어떻게 함수 내부로 전달되는가?"
3. "함수 호출이 끝나고 나면 어떻게 이전 실행위치로 복귀하는가?"
이런 부분을 고려하기 위해서 함수 호출과 프로시저 호출에 대한 구분은 위의 문제를 해결하는데 고려할 대상이 아닙니다.
일단은 문제가 세 가지입니다.
두 번째 문제에 대한 답부터 찾아보겠습니다.
[함수 호출 인자의 전달 방식]
앞서 말했듯이 "함수 호출 방법은 CPU마다, 또는 CPU를 제조한 제조사의 표준에 따라 달라진다"고 했습니다.
우리가 알고 있는 것은 '함수 호출 인자 == 매개변수'라는 사실입니다.
그리고 '매개변수 == 지역변수의 일종'라는 사실도 알고 있죠.
이 말은 바꿔말하면 '지역변수와 마찬가지로 스택에 할당된다'라고 볼 수 있습니다.
그런데 이게 꼭 그렇지만도 않습니다.
맨 처음에 말했던 "함수 호출 방법은 CPU마다, 또는 CPU를 제조한 제조사의 표준에 따라 달라진다"라고.
그래서 함수의 모든 전달 인자들이 반드시 스택에 할당되는 것은 아닙니다.
성능의 향상을 위해서 일부 전달인자들은 레지스터를 할당해서 이곳에 저장하도록 제품의 표준을 정의하기도 합니다.
이제 우리는 어떤 방식을 사용할까를 고민해봐야 합니다.
레지스터 8개 중에서 4개는 이미 사용할 용도가 정해져있습니다.
그리고 프레임 레지스터로 쓰기 위해서 급하게 하나를 붙이기도 했고요.
"그럼 똑같이 레지스터를 더 붙이면 되지 않겠느냐?" 라고 할 수도 있습니다.
근데 그렇게 되면 우리는 처음부터 설계했던 모든 것을 다 갈아엎어야합니다.
여러분들도 아시다시피 단순한 사칙연산을 하는데에도 최소 하나 이상의 레지스터가 필요합니다.
못해도 범용적으로 쓰일 수 있는 레지스터는 여유가 있어야합니다.
그러니 선택의 여지는 없습니다.
과감하게 다음과 같은 결론을 내리겠습니다.
"함수 호출 시 전달되는 인자들은 모두 스택에 저장합시다!"
이제 호출된 함수 내부의 지역변수와 호출 시 전달되는 인자값, 스택 프레임의 경계값까지 스택에 저장됩니다.
전달되는 인자는 지역변수가 스택에 할당되는 것과 동일하게 할당됩니다.
우리가 지역 변수가 스택에 할당되는 과정을 이해한 것은 인자의 전달을 이해하기 위한 과정이었습니다.
[PUSH & POP 명령어 설계]
이제 인자 전달과 관련한 핵심 연산을 말로 풀어보겠습니다.
이 연산은 지역변수가 스택에 할당하는 것과 동일합니다.
"SP가 가리키는 현재 위치에 전달되는 인자값(지역변수값)을 저장한다.
그리고 SP를 증가시켜 다음 메모리 주소를 가리키게 한다."
이와 같은 명령어를 구성하기 위해서 우리가 이전에 만들었던 사칙연산과 LOAD, STORE를 이용해보겠습니다.
일단 STORE를 생각해보면 이런 명령어를 떠올릴 수 있겠네요.
STORE 7, sp
이 명령어는 "숫자 7을 sp가 가리키는 위치에 저장하라" 라는 의미지만 유감스럽게도 이 명령어는 사용할 수 없습니다.
우리가 이전에 STORE 명령어를 정의할 때, [STORE 대상(레지스터) 목적지(메모리주소)] 로 정의를 했습니다.
즉, 숫자 7은 첫 번째 피연산자로 올 수가 없고 sp도 마찬가지로 두 번째 피연산자로 올 수 없습니다.
이제 이 문제를 하나씩 해결해봅시다.
일단 첫 번째로는 숫자 7은 저 위치에 있으면 안됩니다.
그러면 레지스터가 와야할텐데 이걸 어떻게 해결하면 될까요?
숫자 7을 다른 레지스터에 저장하고 그 레지스터를 저 자리에 넣으면 됩니다.
ADD r0, 7, 0
이렇게 하면 r0에는 7이라는 값이 들어가있게 됩니다.
그리고 STORE r0, sp 로 바꿀 수 있습니다.
이제 두 번째 문제를 해결해봅시다.
여기서는 약간의 응용이 필요한데, 이전에 공부했던 Indirect Addressing 모드로 해결이 가능합니다.
우선 sp가 가지고 있는 값(현재 가리키고 있는 스택 위치)를 어딘가에 저장해줍니다.
STORE sp, 0x30
이제 Indirect Addressing 모드로 0x30번지를 참조해서 데이터를 저장하는 것이 가능해졌습니다.
그래서 레지스터인 sp를 대신해서 다음과 같이 쓸 수 있습니다.
STORE r0, [0x30]
이제 이걸 합치면 다음과 같이 구성됩니다.
ADD r0, 7, 0
STORE sp, 0x30
STORE r0, [0x30]
sp가 가리키는 스택 메모리 공간에 데이터를 저장했으니 sp의 값을 증가시켜줄 필요가 있습니다.
저장되는 데이터는 4바이트라 하고 4를 추가한다고 하겠습니다.
ADD r0, 7, 0
STORE sp, 0x30
STORE r0, [0x30]
ADD sp, sp, 4
이 명령어들을 조합하면 하나의 PUSH가 이뤄진 것입니다.
그런데 매번 PUSH를 할 때마다 이렇게 번거로운 연산을 할 수는 없습니다.
프로그램 내에서는 함수 호출이 빈번하게 일어나기 때문에 스택 연산에 도움이 되는 PUSH라는 명령어를 만듭니다.
"PUSH 0x30" 또는 "PUSH r1"
PUSH 명령어는 현재 sp 값을 참조해서 해당 위치에 데이터 0x30 또는 레지스터 r1의 값을 저장합니다.
그리고 SP의 값도 자동으로 증가하는 명령어라고 정의하겠습니다.
다음은 POP 명령어에 대해서 정의해보겠습니다.
POP은 스택에 들어가있는 가장 마지막 데이터를 꺼내는 명령어입니다.
그래서 SP의 값을 감소시키는 연산이 전부입니다.
위에서 저장되는 데이터는 4바이트라고 했으니 4바이트만큼 감소시키기만 하면 됩니다.
ADD sp, sp, -4 또는 SUB sp, sp, 4
이는 POP과 마찬가지로 동작을 하지만, POP 명령어는 피연산자가 필요없습니다.
POP
위와 같이 피연산자를 지정하지 않아도 사용할 수 있고, PUSH와 짝을 이루므로 보다 직관적이게 됩니다.
그래서 POP이라는 명령어도 추가하는 것으로 하겠습니다.
이제 사칙연산과 LOAD, STORE와 스택 관련 명령어인 PUSH와 POP까지 8개의 명령어 설계가 끝났습니다.
[함수 호출(Procedure Call)에 의한 실행의 이동]
이제 해결해야 할 질문이 두 가지가 남았습니다.
1. "함수 호출 시 실행위치의 이동은 어떻게 이뤄지는가?"
2. "함수 호출 시 전달되는 인자들은 어떻게 함수 내부로 전달되는가?" - 스택을 통해서 해결
3. "함수 호출이 끝나고 나면 어떻게 이전 실행위치로 복귀하는가?"
우리가 해결해야 할 질문은 1번과 3번입니다.
그리고 이를 해결하기 위해서는 PC(Program Counter)라는 레지스터의 역할을 알아볼 필요가 있습니다.
[다시 살펴보는 메모리 구조와 프로그램 카운터(Program Counter)]
이번에는 스택이 아닌 '코드 영역'이 핵심입니다.
코드 영역에는 프로그램이 동작하기 위한 프로그램 코드(컴파일된 명령어들의 집합)이 올라가는 영역입니다.
프로그램을 실행시키면 위의 그림과 같은 메모리 구조가 형성이 됩니다.
그리고 코드 영역에는 실행되어야 할 명령어들이 올라가게 되고 순차적으로 실행을 하게 됩니다.
이전에 컴퓨터 구조를 이야기하면서 명령어의 실행은 Fetch, Decode, Execution의 세 단계로 이뤄진다고 했습니다.
여기서 명령어를 CPU로 가져오는 Fetch 단계에서 명령어를 가져오는 위치가 바로 코드 영역입니다.
스택 때는 SP가 메모리가 할당될 다음 위치를 가리키는 역할을 했었습니다.
마찬가지로 다음에 수행할 명령어를 가리킬 무언가가 필요합니다.
이 때 사용되는 것이 'PC 레지스터'입니다.
스택에서는 PUSH 연산을 하면 sp의 값이 자동으로 증가했었죠?
마찬가지로 PC도 Fetch 연산을 수행할 때마다 자동으로 값이 증가합니다.
그래서 명령어를 순차적으로 실행하게 되고 프로그래머가 일일이 PC 값을 조작할 필요가 없습니다.
추가로 우리가 이전에 미리 이름을 붙여놓은 레지스터 중 ir이라는 레지스터가 있습니다.
이는 Instruction Register로 현재 실행해야 할 명령어를 담는 레지스터입니다.
그래서 CPU에서 Fetch를 수행하면 PC가 가리키고 있는 명령어를 IR에 저장하고 PC의 값이 증가하게 됩니다.
실제로 CPU는 Fetch, Decode, Execution의 순차적인 연산이 하는 일의 전부입니다.
우리가 프로그래밍을 하는 것은 Fetch될 명령어들을 잘 조합해서 원하는 연산을 하도록 만드는 것이 핵심입니다.
그래서 다음에 실행할 명령어를 가져오게끔 PC값을 증가시킬 필요는 없습니다.
하지만! 필요에 따라서는 프로그램 상에서 PC의 값을 직접 조작해야하는 경우도 있습니다.
[함수 호출과 함수 종료]
이제 위에서 했던 질문에 대한 답을 할 때가 온 것 같습니다.
1. "함수 호출 시 실행위치의 이동은 어떻게 이뤄지는가?"
3. "함수 호출이 끝나고 나면 어떻게 이전 실행위치로 복귀하는가?"
우리는 지금까지 PC에 대한 이야기를 했었고, 이 질문에 대한 답 역시 PC에 있습니다.
다음과 같은 코드가 있다고 치면, 코드 영역에는 순서대로 main부터 fc1, fc2의 명령어가 저장이 됩니다.
그리고 main을 실행하던 중 fc1을 실행하게 되고 fc1을 실행하던 중 fc2를 실행하게 됩니다.
마찬가지로 fc2의 실행이 끝나면 fc1을 실행하던 위치로 돌아가야하고, fc1의 실행이 끝나면 main으로 돌아와야 합니다.
즉, 특정 위치로 이동의 필요하게 됩니다.
하지만 CPU는 순차적으로 명령어를 실행시킬 뿐입니다.
여기서 필요한 것이 바로 PC입니다.
함수가 호출될 때 PC의 값을 이동해야할 주소값을 저장해두면 자연스럽게 실행의 위치는 이동하게 됩니다.
여기서 1번 질문에 대한 문제는 해결되었습니다.
이제 3번 질문에 대한 문제의 해결입니다.
함수 인자의 전달 과정에서 SP와 FP가 있었듯 PC의 값을 백업해둘 필요가 있습니다.
함수 호출이 끝나고 나면 PC는 원래 실행하던 위치로 다시 이동해야 합니다.
그래서 필요한 것이 LR(Link Register)입니다.
LR도 FP와 마찬가지로 PC가 실행하고 있었던 위치를 저장하게 됩니다.
그리고 함수의 호출이 계속되면 LR의 값을 스택에 백업을 하는 것도 같습니다.
[함수의 호출과 반환 과정에 대한 정리]
이제 길었던 이야기를 정리를 해볼까 합니다.
함수의 호출과 반환 과정에 대해서는 크게 두 가지의 관점으로 볼 수 있습니다.
1) 스택의 관리 방법 (SP, FP)
2) 프로그램의 실행 위치 관리 방법 (PC, LR)
1의 관점에서는 SP와 FP, 그리고 FP의 값을 스택에 저장하는 방식으로 해결을 했습니다.
2의 관점에서는 PC와 LR, 그리고 LR의 값을 스택에 저장하는 방식을 사용한다고 했습니다.
둘은 굉장히 유사한 구조를 지녔고, 실제로 기능과 스택을 활용하는 방법도 유사합니다.
그래서 정리하면 다음과 같이 볼 수 있습니다.
"SP → FP → 스택 순으로 백업 / PC → LR → 스택순으로 백업"
[함수 호출규약(Calling Convention)]
이제 함수 호출규약에 대해서 설명을 해보려고 합니다.
앞에 있던 내용들이 많이 어려웠을겁니다.
그래도 잘 이해를 하셨다면 이 내용을 이해하시는 데에 큰 부담이 없으실겁니다.
[함수 호출규약이란?]
도대체 함수 호출규약이 뭐길래 이런 이야기를 꺼내나 싶으실겁니다.
지금까지 공부했던 부분 중에서 "함수 호출 시 전달되는 인자들은 모두 스택에 저장합시다!" 라고 한 부분이 있습니다.
여기서 전달되는 인자는 왼쪽의 인자부터 스택에 저장될 수도 있고, 오른쪽부터 저장될 수도 있습니다.
마찬가지로 함수 호출과정에서 스택 프레임을 반환하는 방법에도 두 가지가 있습니다.
스택 프레임의 반환은 여러분들도 알고 있다시피 함수의 호출이 끝난 상황에서 일어납니다.
함수 호출이 완료된 이후의 동작(SP의 값과 FP의 값을 복원하는 일련의 과정)을 의미하죠.
이 동작의 주체는 함수를 호출한 호출자(Caller)가 될 수도 있고, 호출을 받은 함수(Function)가 될 수도 있습니다.
둘 중 하나의 함수에서만 수행하면 되기 때문입니다.
그래서 함수 호출이 완료된 이후 스택 프레임을 정리하는 코드가 어디에 존재하는가에 대해서도 나뉘게 됩니다.
이처럼 함수 호출 시 인자를 전달하는 방식과 스택 프레임을 반환하는 방식을 정의한 것을 '함수 호출규약'이라고 합니다.
[호출규약의 종류와 의미]
https://www.agner.org/optimize/calling_conventions.pdf
호출규약의 종류는 위의 문서를 참고하여 정리를 하였습니다. (2023년 2월 1일 기준)
1) Calling Convention: 호출규약
호출규약의 이름으로 크게 32bit와 64bit로 나뉘게 됩니다.
그 중에서 32bit에서 사용하는 호출규약인 cdecl과 stdcall이 앞으로 주로 보시게 될 겁니다.
2) Parameters in registers: 인자 저장에 사용되는 레지스터
레지스터의 이름이 명시되어 있는 호출규약은 해당 레지스터에 저장을 합니다.
레지스터의 수보다 인자가 많을 경우에는 나머지 인자는 스택에 저장하게 됩니다.
fastcall이나 thiscall는 인자 2개까지 레지스터에 저장하고 나머지는 스택에 저장합니다.
그래서 cdecl과 stdcall에 비해서 레지스터를 사용하기 때문에 빠르다고 볼 수 있습니다.
64bit OS는 다음과 같습니다.
Windows: 4개의 레지스터(rcs, rdx, r8, r9 또는 xmm0-3)를 인자 저장에 활용
Linux, BSD, Mac: 14개의 레지스터를 인자 저장에 활용
3) Parameter on stack: 스택에 인자를 전달하는 방식
위의 표에는 전부 C라고 되어있습니다.
이는 C언어 스타일을 따른다는 것으로 오른쪽에 전달되는 인자가 먼저 스택에 쌓이는 방식을 의미합니다.
그리고 Pascal이라는 방식이 있는데 이는 왼쪽에서 오른쪽으로 인자가 먼저 스택에 쌓이는 방식입니다.
Pascal을 이용하여 프로그래밍을 할 일은 거의 없으니 참고용으로 알아두시면 됩니다.
4) Stack cleanup by: 스택 프레임 반환의 주체
앞에서 이야기했던 것처럼 스택 프레임 반환의 주체에 따라 규약이 나뉘게 됩니다.
cdecl의 경우에는 Caller(호출자)에서, stdcall의 경우에는 Function(피호출자)으로 나뉘어져 있습니다.