-절차적 함수 호출 지원 CPU 모델
함수 호출도 CPU의 도움을 받아야만 가능함. 함수 호출이라는것은 소프트웨어적인 그 무엇인가에 의해 제공되는 기능으로 이해하는 경향이 강함. 그러나 함수 호출이라는 기능은 하드웨어 종속적인 부분이 상당수 존재한다.
함수가 호출되는 방식은 CPU에 따라서 차이를 보인다. 한 예로 ARM코에서는 ATPCS라는 것을 정의하였는데, 이는 함수의 전달 인자와 리턴 어드레스를 레지스터에 저장하기로 결정하고, 그 저장방식에 대한 표준을 정의한 것이다.따라서 이 표준을 고려하여 ARM 코어의 레지스터들도 디자인되어 있고, 또 ARM 컴파일러도 이 표준에 맞게 바이너리 코드를 생성하도록 디자인되어 있다.
-스택 프레임 구조
함수 내에 선언된 변수가 스택에 할당된다는 것은 이미 알고 있다. 다음 그림은 함수 호출과 스택관계를 보여준다.
스택 프레임 : 함수 호출 과정에서 할당되는 메모리 블록(지역변수의 선언으로 인해 할당되는 메모리 블록)
함수 호출이 완료(return)되면 주소를 알고있다 해도 기존에 선언된 지역변수에 접근이 불가능. 이는 할당되었던 메모리가 반환되었음을 의미
-sp 레지스터
지역변수를 위한 메모리 공간을 스택이라 이름 붙인 이유는 메모리의 FILO 특성 때문.
스택 프레임은 가장 먼저 할당되면, 가장 나중에 반환된다.
스택에 데이터를 쌓거나 반환하기 위해서는 현재 어느 위치까지 데이터를 저장했는지 기억해야만 한다.다시 말해서 쌓아 올린 스택 위치를 기억해야만 함.
이를 위해 CPU내에 sp라는 이름의 레지스터가 존재
sp 레지스터값은 변수가 하나씩 할당될떄마다 증가하면서 다음 변수가 할당될 메모리 우치를 가리키게됨.
반면에 호출된 함수가 종료될 경우. 스택 프레임 단위로 sp 레지스터값을 이동시켜야함. 호출된 함수가 종료될 경우 그 안에서 선언된 변수들을 동시에 모두 반환 해야 하기 때문. 변수가 선언되면 현재 sp가 가리키는 위치에할당하기 떄문에,sp 위치를 아래로 이동 시키는것만으로도 이전에 선언된 변수를 반환할 수있다. 때문에 sp 가 가리키는 위치를 아래로 이동시키는 방식으로 스택프레임을 반환한다.
하지만 여기에 한 가지 문제가 있다. 스택엑 메모리를 할당할 때 sp값의 증가분은 계산 가능하다 . 할당하려는 순간 할당하고자흔 타입에따라서 sp를 이동시킨다 (int 라면 4바이트) 하지만 호출이 완료된 함수를 빠져 나오는 시점에서 얼마만큼 sp를 이동시켜야할지 알수없다. 그렇기때문에 앞서 얼마 만큼의 메모리 공간을 할당했는지 저장해야함
레지스터 하나를 추가로 할당해서 새로운 함수가 호출될 때마다 레지스터값을 0으로 초기화 하고 변수가 선언될 때마다 그 크기만큼 값을 증가시키는 방식이 있지만,
이 방법은 변수를 선언할 때마다 덧셈 연산을 해야만 한다는 단점이 있다. 이는 스택 연산에 드는 비용을 상당히 늘린다.
따라서 되올아갈 sp의 위치를 저장해놓자 이 역할을 하는 레지스터를 가리켜 fp 레지스터라 한다.
하지만 스택프레임이 다수인경우 뒤에 fp값을 저장하기위해 전값을 날려버리는 문제점이있음
덮어쓰는 문제가 발생하기 전에 fp에 저장된 값을 어딘가에 저장해두자!
즉 함수 호출이 일어날 때마다 fp 레지스터에 저장되어 있는 값을 스택에 저장하는 것이다. 그리고나서 새로운 값으로 fp 레지스터를 채운다.
함수 호출 인자의 전달과 PUSH & POP 명령어 디자인
어셈블리 언어라고 해서 레지스터들을 일일이 직접 컨트롤 해야하는 것이 아닌 잘 정의 된 명령어를 제공함으로써 레지스터를 직접 컨트롤 하는 수고를 덜어 주는 경우도 있다.
스택에 관련된 연산을 보다 용이하게 하기위한 추가적인 명령어 두가지를 정의
함수호출 : 입력에 대한 출력이 반환값으로 존재
프로시저 호출 : 출력에 해당하는 반환값 없이 모듈화해 놓은 서브 루틴의 실행을 위한호출
함수 호출 시 전달되는 인자를 어디에다 둘 것이냐에 대한 해답도 CPU마다 다르다.
전달되는 인자가 함수 내에서는 유효하고, 함수 호출이 끝나고 나면 사라지는 것으로 봐서 지역변수와 마찬가지로 스택에 할당된다고 용기 내어 말할 수도 있다.
그러나 모든 전달인자들이 반드시 스택에 할당되는 것은 아니다. 성능 향상을 위해서 일부 전달인자들은 레지스터를 할당해서 이곳에 저장하도록 제품의 표준을 정의하기도 한다.
하지만 우리가 디자인한 CPU구조에선 레지스터의 갯수가 여유롭지않다. -> 함수 호출 시 전달되는 인자들을 모두 스택에 저장
함수 내부 지역변수 + 호출 수 전달되는 인자값 -> 스택프레임의 경계 정보 까지 스택에 저장되는 구조
"sp가 가리키는 현재 위치에 전달되는 인자값을 저장하고 나서 sp를 증가시켜 다음 메모리 주소를 가리키게 한다."
STORE 명령어는 레지스터에 저장된 데이터를 메모리에 저장하는 명령어로서 다음과 같은 구조를 가진다.
STORE 대상(레지스터), 목적지 (메모리 주소)
STORE 7, sp 의 두가지 문제점
"STORE 명령어의 첫 번째 피연산자로 숫자 7이 등장하였다. 그러나 이 위치에는 레지스터 정보가 와야 한다."
"STORE 명령어의 두 번째 피연산자로 레지스터 정보 sp가 왔다. 그러나 이 위치에는 주소 정보가 와야 한다."
[첫 번째 문제점에 대한 해결책]
숫자 7을 임의의 레지스터에 저장한다.
ADD r1,7,0
STORE r1, sp
[두 번째 문제점에 대한 해결책]
Indirect 모드의 도움을 받아야만 한다.
1.STORE 명령어를 사용해 sp가 지니고 있는 값을 0x40번지에 저장
2.레지스터 정보 sp를 대신해서 주소 정보를 피연산자로 둠
ADD r1, 7, 0
STORE sp, 0x40
STORE r1, [0x40]
ADD sp,sp,4
데이터를 스택에 넣고자 하는 경우 다음과 같은 형태로 명령어를 사용
"PSUH 0x02" or "PUSH r1"
현재 sp 값을 참조해서 해당 위치에 데이터 0x0(혹은 레지스터 r1의 값)을 저장하고 sp의 값 또한 자동으로 증가하는 명령어
POP : sp 레지스터에 저장된 값을 감소시키는것
함수 호출에 의한 실행의 이동
-다시 살펴보는 메모리 구조와 프로그램 카운터
코드 영역 : 프로그램이 동작하기 위한 프로그램 코드(컴파일된 명령어들의 집함)가 올라가는 위치이다.
프로그램을 실행시키면 위와 같은 메모리 구조가 형성되고 코드 영역에, 실행되어야 할 명령어들이 올라가서 순차적인 실행이 이뤄지게 됨
명령어의 실행이 세 단계 (Fetch, Decode, Execution) 로 구분되어 진행됨
명렁어를 CPU내부로 가져오는 Fetch 단계에서 명렁어를 가져오게 되는 위치는 프로그램 코드가 존재하는 코드영역임
명령어의 길이가 4바이트 실행 중인 프로그램이 현재 1036번지에 있는 명령어라면 다음에는 1040번지에 있는 명령어가 Fetch되어야함.
이때 어느 위치에 있는 명령어까지 가져와 실행했는지 기억하고 있어야만 다음 번에 실행할 명령어를 가져올 수 있음
명령어를 순차적으로 fetch 하기 위해서 프로그램 카운터라 불리는 "PC레지스터"를 둔다.
CPU는 Fetch,Decode,Execution 과정을 계속해서 진행하도록 구현되어 있기 때문에,Fetcg 연산이 일어날 때마다 자동적으로 pc값이 증가한다. 떄문에 우리가 직접 pc값을 컨트롤 하지 않아도됨
여기서 ir은 명령어를 가져오기 위해서 사용되는 레지스터
-함수 호출과 함수 종료
32비트 명령어 기준으로 pc는 명령어를 실행할 때마다 4씩 증가함. 이 pc에 함수 호출로 인해 이동해야 할 주소값을 저장해 두면 자연스럽게 실행의 위치는 이동.
현재 pc값을 백업해두지 않으면 함수 호출이 완려된 이후에 돌아오는 길이 막연해짐
그리고 이값은 스택에 저장되어야함 fp와 마찬가지로 lr을통해 이를 관리
함수 호출 규약
함수 호출과정에서 할당된 스택 프레임을 반환하는 방법에도 두가지가 존재(하이브리를 포함하면 3가지). 반환은 함수 호출이 완료된 이후의 동작(sp 레지스터값을 복원하는등)을 의미하는데 이 주체는 호출자가 될수도 있고,호출이 된 함수가 될 수도 있다.
예를 들어서 A함수가 B함수를 호출하는 프로그램코드가있다면 A함수는 호출자가되고
이코드를 컴파일했을때 스택 프레임을 정리하는 명령어들이 A함수에 있을수도 있고 B함수에 있을수도 있을 수 있다는 것이다.
이처럼 함수 호출 시 인자를 전달하는 방식과 스택 프레임을 반환하는 방식을 약속해 놓은 것을 가리켜 함수 호출 규약이라 부른다.
int stdcal func(int a) -> 이는 stdcall 호출규약에 따라서 func 함수의 호출과 반환을 처리하라는 뜻
Windows 시스템함수 선언에서는 키워드 __stdcall를 직접 사용하지 안흔ㄴ다 이보다는 CALLBACK이나 WINAPI라는 또 다른 이름을 부여해서 그 함수의 특성 파악에 도움을 주도록 하고있다.
__cdecl : c/c++의 디폴트 호출규약. 인자 전달방식은 C언어스타일을 따름
이는 오른쪽에 전달되는 인자가 먼저 스택에 쌓이는 방식. 반환 시에는 함수를호출하는 호출자가 스택 프레임을 반환하도록 정의되어있음
stdcall와 cdecl의 차이점은 스택 프레임을 반환하는 주체이다. __stdcall은 호출된 함수 내에서 스택 프레임을 반환하도록 정의되어 있다.
! 콜백함수란 : Windows 시스템에 의해 자동으로 호출되는 함수를 의미한다. 특정 상황에서 호출되어야 할 함수를 등록시키는 것이 가능한데, 이때 등록이 되는 함수를 가리켜 콜백 함수로 한다.
__fastcall : 함수 호출을 빠르게 처리하기 위한 호출 규약. 위 표에서 "Parameters in registers"부분은 전달되는 인자를 저장할 때 레지스터의 사용유무를 설명한다. 첫 번째 전달인자와 두 번쨰 전달인자는 레지스터 ecx와 edx를 통해 저장된다. 여기서 ecx와 edx는 레지스터의 이름이다.
이 호출큐약에서 가장 주목할 부분은 레지스터를 사용하고 있다는 점이다. 이것이 함수 호출이 빨라지는 근거가 된다. 물론 두개를 넘어서는 인자에 대해서는 스택을 활용하게 된다.
64비트 시스템에서는 함수 호출규약이 운영체제에 따라서 나뉘게됨
Windows 기반에서는 총 8개의 레지스터를 활용해서 전달되는 인자를 저장하게 되는데, 실제로 레지스터에 저장되는 전달인자의 개수는 4개에 지나지 않는다.
rcx/xmm0
이는 첫 번째 전달인자가 rcx 혹은 xmm0 레지스터에 저장된다는 것을 의미한다.
이것만은 알고 갑시다
이번 장에서는 특별한 용도로 지정되어 있는 sp 레지스터와 fp레지스터,그리고 pc에 대해서 설명하였다. 스택에는 단순히 지역변수만 쌓이는 것이 아님을 알았고, 함수가 호출되는 과정에서 얼마나 많은 일들이 일어나는가를 아랐다.
함수의 호출방식도 일종의 규약이다. 다양한 함수 호출규약이 존재하며, 어떠한 호출규약을 따를 것인가를 선언하게 되어 있다. 그런데 우리는 지금까지 함수를 정의함에 있어서 호출규약을 명시한 적이 없다 이런한 경우 디폴트선언으로 지정되어 있는 호출규약을 따르게 된다.