✏️ 절차적 함수 호출(Procedure Call) 지원 CPU 모델

  • 함수 호출 시 키워드
    1) 인자 전달
    2) 지역 변수
    3) 실행의 이동
    ex) A 함수가 B 함수 호출 시 B 함수로 실행이 이동됨

💻 스택 프레임(Stack Frame)이란?

스택 프레임(Stack Frame): 임의의 함수 내에 선언된 메모리 공간.
a, b 변수는 는 메인 함수에서 선언되었기 때문에 메인 함수 스택 프레임.
스택 프레임은 어떻게 겹치지 않고 위로 차곡 차곡 쌓아 올려지는걸까?
임의의 관찰자가 첫번째 메모리 주소를 가리킨다고 가정해보자. 실행과 동시에 a라는 변수가 선언이 되면 관찰자는 다음 메모리 주소를 가리킬 것. 이처럼 어디까지 메모리 공간을 할당했는지 기억해주는 누군가가 있기 때문에 스택 프레임을 쌓아올리는 것이 가능했던 것. 이는 레지스터의 역할.

💻 SP 레지스터(Stack Pointer Register)

SP 레지스터는 변수 하나를 선언될 때마다 가리키는 위치를 바꾼다.
이처럼 쌓는 것은 어렵지 않으나, 반환에 문제가 있다.
레지스터는 32비트 시스템 기준 4바이트 주소밖에 저장할 수 없기 때문에, 반환 요청을 받았을 때 되돌아갈 주소의 위치까지 기억해둘 수 없다.
따라서 되돌아갈 주소 위치를 기억해줄 새로운 레지스터를 고용. => fp 레지스터

💻 FP 레지스터(Frame Pointer Register)

SP 레지스터의 백업 역할.
A 함수내에서 B 함수가 호출되면, 호출된 B 함수 내에서 스택을 또 쌓을 것. 그리고 B 함수가 끝나면 호출되기 이전의 상태의 스택으로 다시 돌아와야 함.

  • 따라서 SP 레지스터는 B 함수를 호출 하기 전에 자신이 가지고 있는 값, 즉 현재 가리키는 스택 프레임 내용을 FP 레지스터에게 저장을 부탁한다.
  • B 함수가 끝난 뒤 FP 레지스터가 가지고 있던 값을 다시 가져오면 SP 레지스터는 함수호출 이전의 위치의 스택 프레임을 가리키게 된다.


함수 호출이 끝난 후 메모리 공간을 반환한다 = 스택 포인터가 가리키던 위치를 함수 호출 이전 위치로 되돌리는 것.
메모리 공간을 덮어 씌우는 건 지우는 것과 같다.

💻 FP 레지스터의 문제점과 해결책

  • FP 레지스터의 문제점

    백업이 필요한 이유 -> 함수 호출 때문
    함수의 호출이 꼬리에 꼬리를 무는 상황이 벌어질 때, SP가 FP에게 요구하는 백업이 많아진다. 그러나 FP는 하나의 레지스터이기 때문에 모든 정보를 담을 수 없고, 새로운 정보가 들어오면 기존의 값이 지워지는 문제가 생긴다.

  • 해결책

    백업 역할의 메모리를 둬서 해결할 수 있다. 이 때 메모리 또한 스택 프레임.

1) main()함수에서 a와 b변수의 선언과 동시에 SP는 8번지 스택 프레임을 가리킨다.
2) fct1() 함수를 호출하면 SP는 자신이 가리키는 값을(8번지) FP 레지스터에 저장한 뒤 20번지를 가리킨다.
3) fct2() 함수를 호출하면 SP는 자신이 가리키는 값(20번지)를 FP에 백업 요청해야 하는데. FP에서는 현재 8이 저장되어 있기 때문에 그냥 저장하면 기존 저장된 값이 지워진다. 따라서 FP는 자신의 값을 현재 SP가 가리키는 위치에 저장해두고(20번지), SP는 그 다음 위치를 가리킨다. (24번지)그리고 변수가 선언됨에 따라 SP의 현재 위치는 32번지.
4) fct2() 함수가 반환되면 FP가 가지고 있던 값(20번지)을 다시 SP에게 돌려줌. 따라서 SP는 fct2()함수가 호출되기 직전의 위치인 20번지를 가리키게 된다. 그리고 20번지에 저장되어 있던 값을(8번지) 다시 FP에 저장.
5) FP는 8번지를 가리키게 됨. 즉, main() 함수가 fct1()함수를 호출하기 전 이전 위치의 스택 프레임으로 가기위한 정보가 FP에 저장된다.

정리하자면,

  • 함수 호출 시,
    • FP는 자신이 가지고 있는 값을 스택에 백업
    • SP는 자신이 가지고 있는 값을 FP에 백업
  • 함수 반환 시.
    • FP가 가지고 있는 값을 SP로 다시 저장
    • 스택에 저장된 값을 FP로 다시 저장

✏️ 함수 호출 인자의 전달과 PUSH & POP 명령어 디자인

함수 호출 시 전달되는 인자와 지역 변수는 둘 다 선언과 동시에 함수 내에서만 접근 가능하고 함수 반환 시 사라지는 특성을 가짐.
그리고 인자는 지역 변수와 마찬가지로 스택 프레임에 쌓인다. 이처럼 스택 프레임 데이터 저장이 빈번하게 일어나는 곳.
PUSH: 스택에 데이터를 쌓는 명령어

💻 함수 호출 인자의 전달 방식

  1. SP가 가리키는 현재 위치에 전달되는 인자값 저장
  2. SP를 증가시켜 다음 메모리 주소를 가리키게 함

STORE 명령어 -> 내부에 가지고 있는 값을 메모리에 저장. 그러면 STORE를 사용할 수 있지 않을까?

  • STORE 명령어를 사용할 경우 문제점

대상으로 레지스터가 와야 하는데 7이라는 값이 오고, 목적지에 메모리 주소가 아닌 SP레지스터가 옴.
그러면 7을 레지스터에 저장한 뒤 해당 레지스터를 대상으로 넘겨주고, SP 레지스터가 가지고 있는 값을 메모리에 저장한 뒤 메모리 주소를 목적지로 넘겨준다면? 아래와 같은 명령어 3개가 필요.

1) 7을 레지스터에 저장하는 명령어
2) SP 레지스터에 있는 값을 메모리에 저장하는 명령어
3) STORE 명령어

  • 해결책 및 PUSH 명령어 디자인

1) 7 + 0 결과를 r1에 저장
2) 메모리의 0x40번지 주소에 SP 레지스터 값 저장
3) r1에 저장된 값을 0x40번지 주소에 기입된 주소에 저장

  • 피연산자 [0x40]가 Indirect 모드인 이유
    direct 모드일 경우 0x40번지에 r1의 값을 저장하게 됨. 그러나 지금은 0x40번지 주소에 저장된 SP 레지스터가 가리키는 스택 프레임의 위치에 r1의 값을 저장하는게 목적이기 때문에 피연산자는 Indirect 모드여야 한다.

4) SP의 값을 증가시켜 다음 메모리 공간 할당을 위한 기반을 마련.

이처럼 ADD와 STORE 명령어를 조합해 PUSH 명령어를 디자인할 수 있음.

  • POP 명령어 디자인

다음 두 가지 방식으로 POP 명령을 디자인할 수 있다.
그러나 실제 스택의 POP 기능은 스택 포인터 단위로 이루어지기 때문에 4바이트 고정이 아님에 주의.

✏️ 호출 규약과 실행의 이동

💻 함수 호출에 의한 실행의 이동과 PC

PC (Program Counter)
다음번 실행해야 할 명령어의 주소 값.

  • PC의 역할
    1) 명령어의 순차적인 실행
    2) 함수 실행의 이동

코드 영역에 명령어가 쌓여 있을 경우, 명령어를 하나씩 CPU 내부로 fetch해와서 IR 레지스터(Instruction Register) 저장해야 한다. 이 때 PC의 역할은 명령어를 어디까지 가져왔는지 저장하는 역할. 다시 말해 CPU는 PC가 가리키는 위치의 다음 위치 명령어를 실행한다.

A 함수가 B 함수를 호출했을 경우, A 함수의 실행의 이동이 B 함수로 옮겨가고 PC의 위치 정보가 B 함수의 시작 번지 주소로 갱신된다. CPU는 단순히 쌓인 명령어를 Fetch Decode Execution하는 과정을 거치기 때문에 PC에 저장된 값을 실행한다.
즉, 함수의 호출(실행의 이동)은 PC 레지스터의 값을 변경하는 것이라 할 수 있다.

  • Link Register
    SP가 계속 증가하기 위해서는 FP가 필요하고, FP는 메모리 공간을 활용해 백업하는 것처럼 PC도 호출한 함수가 반환된 뒤 돌아갈 위치를 저장할 곳이 필요.
    이는 Link Register의 역할. 함수 호출이 빈번하다면 Link Register 또한 메모리 공간을 활용해 자신의 값을 백업.

💻 함수 호출 규약

호출 규약: 어떻게 함수를 실행할지 정해둔 것.
A 함수가 B 함수를 호출할 때, 반환되는 코드(SP/FP/Stack Frame 혹은 PC/LR/Stack Frame의 동작 코드)를 A 함수가 컴파일될 때 넣어둘지, B 함수가 컴파일될 때 넣어줄지 정해놔야 한다. 양 쪽에서 모두 실행하면 문제가 됨.
또 A 함수가 B 함수를 호출하면서 인자를 전달할 경우, A 함수가 왼쪽부터 전달한다고 하면 B 함수 또한 왼쪽부터 매개 변수를 받아줘야 함.

  • Calling Convention(호출 규약)

    • 32비트 시스템에서는 다음 4가지, 64비트 시스템에서는 OS 별로 정리됨
    • _fastcall: 32비트 시스템 환경에서 _fastcall의 경우 매개 변수를 두 개까지 ecx, edx 레지스터에 저장하고 나머지는 스택에 쌓아둠. 레지스터를 사용했기 때문에 실행 속도가 빠르다.
  • Parameters in registers

    • 64비트 시스템 환경에서는 매개 변수를 4개까지 레지스터에 저장하고 나머지는 스택에 저장한다. 레지스터를 더 많이 사용하기 때문에 함수 호출이 더 빠르다.
    • 64비트 시스템의 경우 한 클럭 당 처리하는 명령어의 수가 많을 뿐만 아니라, 레지스터를 더 많이 활용하기 때문에 32비트 시스템보다 속도가 빠르다고 할 수 있다.
  • Parameter order on stack

    • 스택에 어떤 순서로 쌓을지
    • C Style: 오른쪽 -> 왼쪽 순서로 쌓는 방식
  • Stack cleanup by

    • 함수 반환 시 스택을 언제 비울지
    • 호출자가 비울지, 호출된 자가 비울지
      Parameter order on stack과 Stack cleanup by의 경우 64비트 환경의 OS에서는 통일.

0개의 댓글