10.1 절차적 함수 호출(Procedure Call) 지원 CPU 모델
Prologue) 10장은 결국 CPU가 어떻게 함수호출을 처리해 주는가를 배움.
함수 호출 시 필요한 작업으로는
1. 인자 전달
2. 지역 변수 처리
3. 실행의 이동
10.1-1 스택 프레임(Stack Frame)이란?
Def) 스택 프레임
임의의 함수 내에서 선언된 메모리 공간.
근데 우리가 궁금한건, 어떻게 스택 프레임이 겹쳐지지도 않고
붕 뜨지도 않고 차곡차곡 예쁘게 쌓아올라갈 수 있을까?
Note) 스택 프레임이 어떻게 다음 위치를 잘 지정할까?
누군가가 어디까지 메모리 공간을 할당했는지 기억하고 있어야 함.
이게 바로 SP 레지스터임.
10.1-2 SP 레지스터
Note) SP 레지스터는 스택 프레임의 바닥을 가리킴
그리고 지역 변수가 선언될 때마다 그 위치를 이동시켜가지고
즉, 어디 메모리 공간까지 사용 했는지 메모리 주소를 저장함.
이 덕분에 스택 프레임은 겹치치도, 붕뜨지도 않게됨.
Note) 쌓는건 이제 아주 쉬워짐. 근데 반환에 문제가 생김.
즉, 함수의 return 명령어를 만났을때 SP 레지스터는 어디로 가야할지 모름.
32 비트 시스템 기준, 4바이트 밖에 저장 못하기 때문에 주소 하나 밖에 저장 못함.
어떻게 해야할까? 마찬가지로 되돌아갈 주소를 기억 해 줄 레지스터를 하나 더 고용.
10.1-3 FP 레지스터
Note) FP 레지스터(Frame Pointer Register)는 SP 레지스터의 백업임.
fc1() 함수가 호출되고, c, d 지역 변수가 선언되면서
SP 레지스터는 계속 값이 바뀔 예정임.
그러다가 fc1() 함수가 종료되면 SP 레지스터는 fc1() 호출 위치로 돌아가야함.
따라서 fc1() 호출 전에 현재 SP 레지스터 내용을 FP 레지스터에 백업함.
그럼 SP 레지스터는 자유롭게 늘어나고 줄어들 수 있음.
Note) 이제 함수가 종료되면 SP 레지스터의 내용을 함수 호출 이전으로 되돌림.
따라서 FP 레지스터의 내용을 SP 레지스터로 옮김.
즉, 지운다 ~= 덮어쓰기 되게끔 옮기기만한다.
10.1-4 FP 레지스터의 문제점
Note) 근데 함수의 호출이 고리에 고리를 문다면,(ex. f1()->f2()->f3()->...)
SP 레지스터는 호출때마다 자신의 데이터를 FP 레지스터에게 백업 요청함.
근데 FP 레지스터도 하나임. 그래서 돌아갈 주소를 잃어버리게 됨.
그래서 FP 레지스터도 백업을 두게 됨. 근데 호출이 많아진다면
하나의 레지스터로는 백업 감당이 안됨. 몇개가 될 지 모름.
그래서 메모리를 백업으로 사용해야 함.
10.1-5 FP 레지스터의 해결책
Note) 결국 FP 레지스터의 해결책은 메모리고, 그 메모리는 스택 프레임임.
1. main()에서 sp가 하나씩 자라남.
2. f1()이 호출됨. fp는 가지고 있던 값을 sp가 가리키고 있는 스택 프레임 위치에 저장.
fp에는 sp가 가리키고 있는 스택 프레임 위치를 저장.
3. f1()의 스택 프레임이 자라남. f2()가 호출됨.
다시 한 번 fp에 있던 값이 sp가 가리키고 있던 스택 프레임 위치에 저장.
fp에는 sp가 가리키고 있던 스택 프레임 위치를 저장.
4. f2()의 스택프레임이 자라나다가, 종료됨.
sp에 fp가 가지고 있던 값을 옮김.
sp가 가리키고 있는 스택 프레임 위치에 있는 값을 fp에 저장.
5. f1()도 종료되면, sp에는 fp에 저장된 값을 옮김.
sp가 가리키고 있는 스택 프레임 위치에 있는 값을 fp에 옮김.
6. 다시 main() 도착.
Note) 정리하자면,
1. 함수 호출시,
FP 내용을 SP가 가리키는 스택프레임 위치에 백업 -> SP 내용을 FP에 백업
2. 함수 종료시,
FP 내용을 SP에 환원 -> SP가 가리키는 스택프레임 위치에 내용을 FP에 환원
10.2 함수 호출 인자의 전달과 PUSH & POP 명령어 디자인
Prologue) 함수 호출 시 필요한 작업 세 가지중, 이전 파트에선 지역변수 처리 관련을 다룸.
이번 시간에는 인자 전달에 대해 배울 예정.
전달되는 인자는 결국 매개변수에 저장되고, 매개변수도 사실은 스택 프레임에 쌓임.
즉, 스택 프레임에 데이터 저장은 아주 빈번하게 이뤄짐.
그래서 이걸 아에 명령어로 PUSH 명령어로 디자인 하고자 함.
10.2-1 함수 호출 인자의 전달 방식
Note) 아래 그림은 첫번째 전달인자가 스택에 먼저 들어가는 거처럼 보임.
근데 실제로는 두 번째 전달인자, 즉 역순으로 들어감.
이건 calling convention과 관련이 있음. 지금은 그냥 쉽게 설명하려고 순서대로 들어감.
Note) 우리가 해야 할 일은
1. sp가 가리키는 현재 위치에, 전달 받은 인자값을 저장하고
2. sp를 증가시켜서 다음 메모리 주소를 가리키게 함.
이렇게 하면 다음 인자값도 저장 가능해짐.
단순하게, 인자값 저장->sp 증가. 이걸 명령어로 디자인 하려함.
Note) 우리가 배운 명령어를 토대로 위 명령어를 디자인 해보자.
일단 "인자값 저장"은 STORE가 떠오름. STORE 7 SP 라고 하면 될까?
문제점이 있음.
10.2-2 함수 호출 인자의 전달방식의 문제점
Note) 컴퓨터 구조에 대한 두 번째 이야기에서, 아래와 같이 STORE를 디자인함.
따라서 문제점은 두 가지임.
1. 첫 번째 피연산자는 레지스터가 와야하는데 7이 옴.
2. 두 번째 피연산자는 메모리 주소값이 와야하는데 레지스터가 옴
Note)
10.2-3 함수 호출 인자의 전달방식 문제점 해결책
Note) 위에서 언급한 첫 번째 문제점 해결책은
7을 범용 레지스터에 저장해버리면 해결.
두 번째 문제점 해결책은 sp에 저장된 값을 메모리에 넣어주자.
그리고 그 메모리의 주소를 STORE의 두 번째 피연산자에 적어주자.
즉, 이 문제점들을 해결하기 위해서는 명령어를 3개 써야 함.
그리고 SP의 증가를 위해서 1개 더 필요. 그럼 완성함.
Note) 근데 왜 STORE r1, [0x40]에서 Indirect mode를 사용할까?
STORE r1, 0x40은 안될까?
만약 STORE r1, 0x40이라고 하면 0x40번지에 r1의 값을 저장하게됨.
우리가 원하는건 그게 아니고, r1의 값을 0x40번지에 적힌 메모리 주소로 가서
그곳에 저장하기를 원하는 것. 그 메모리 주소가 스택 프레임의 주소기 때문임.
그래서 Indirect mode로 피연산자를 적어야 함.
10.2-4 POP 명령어 디자인
Note) PUSH 명령어의 상대되는 개념으로 POP을 만듦.
구현은 쉽게 가능함.
Note) 주의해야 할 점은, SP가 8바이트 단위로 이동한다면
POP할때는 위와 같은 명령어를 두 번 실행해줘야 함.
"POP == ADD sp, sp, -4"가 항상 참은 아니라는 것.
10.3 함수 호출 규약과 실행의 이동
10.3-1 함수 호출에 의한 실행의 이동과 PC
Note) PC 란?
Program Counter의 약자. 다음에 실행해야할 명령어의 주소.
Note) 위 메모리 구조에서 Code 영역에 명령어들이 쌓여있다고 가정해 보자.
그럼, F D E 과정을 위해서 일단 IR 레지스터(Instruction Register)에
명령어를 Fetch 해옴. 이때 PC 레지스터는 어디까지 가져왔다는걸 알려줌.
달리 말하면, Fetch 할 때마다, 다음번 명령어를 가리키는게 PC 레지스터임.
즉, CPU는 PC 레지스터가 가리키는 명령어를 다음번에 실행하게된다.
Note) 함수 A가 실행 중에, 함수 B를 호출했다고 가정 해보자.
그 순간에 우리가 할 일은, PC 레지스터에 함수 B 명령어의 시작주소를 넣어주면
마치 함수 B가 호출된거마냥 프로그래머를 속일 수 있음.
즉, 우리가 이때까지 말한 함수 호출시 실행의 이동은 PC 레지스터의 값을 변경한 것.
Note) PC 레지스터가 하는 일을 정리하자면 두 가지임.
- 순차적인 실행
- 함수 호출시 실행의 이동
Note) 그럼 함수 호출 후 실행되다가 종료되면 어떻게 돌아갈까?
SP/FP/메모리의 관계처럼, PC도 어딘가에 백업되어야함.
그게 Link Register. 또한, 함수 호출이 빈번하다면 LR도 메모리에 백업되어야 함.
이부분도 완벽히 일치하기에, 추후에 혼자서 해 봐야함.
10.3-2 함수 호출 규약
Note) 함수 호출 규약이란, 어떻게 함수 호출을 실행할 것인가의 내용.
즉, 이전에 배운 지역변수 처리를 위한 SP/FP/StackFrame의 동작 코드를
Caller 함수가 컴파일 될때 넣을거냐 Callee 함수가 컴파일 될때 넣을거냐를
정해주어야 함. 그리고 그걸 둘 중 누군가가 책임지고 실행 해야함.
그래야 둘 중 하나는 꼭 할테니까 코드의 중복을 막을 수 있음.
ex. Caller가 인자값을 오른쪽부터 전달하면 Callee도 매개변수에 오른쪽부터
채워넣겠다고 약속해야함. 즉, 둘이 방향이 다르면 엉뚱한곳에 들어가게되니까 규약이 필요.
Note) 아래 표에 대한 해설
- Calling Convention(호출 규약)
32비트 시스템에서는 4가지, 64비트에서는 OS별로 정리됨.
_fastcall은 빠른 수행을 위해서, 매개변수를 두 개까지 레지스터에 저장함.
메모리가 아님. 레지스터를 사용했기 때문에 fastcall인거임.
- Parameters in registers(중요)
지금까지 배웠을 때, 매개변수는 지역변수처럼 스택 프레임에 쌓인다고 배움.
근데 64비트 시스템에서부터는 레지스터 여유가 생김.
그래서 무조건 4개까지는 레지스터에 저장함.
특히 리눅스 같은 경우에는 훨씬더 많은 개수의 레지스터를 사용함.
즉, 64비트 시스템에서는 한 클럭당 처리할 수 있는 명령어가 많아서
속도가 빨라질 수도 있지만, 이런식으로 레지스터를 더 많이
활용할 수 있어서 빨라지기도 함.
- Parameter order on stack
C는 C Style을 의미함. C Style이란, 오른쪽에서 왼쪽으로 쌓는 방식.
P도 있음. Pascal Style이란, 왼쪽에서 오른쪽으로 쌓는 방식.
- Stack cleanup by(함수 반환시 누가 스택 정리하나, Faller 아님 Caller)
Stack cleanup이나 Parameter order는 64비트 시스템으로 바뀌면서
모두 통일되어 버리기 때문에 그다지 중요한 내용은 아님.
오히려 Parameter in registers 항목이 훨씬 중요함.
'C > [서적] 뇌를 자극하는 윈도우즈 시프' 카테고리의 다른 글
Chapter 12. 쓰레드의 생성과 소멸 (0) | 2022.02.08 |
---|---|
Chapter 11. 쓰레드의 이해 (0) | 2022.02.06 |
Chapter 09. 스케줄링 알고리듬과 우선순위 (0) | 2022.02.05 |
Chapter 08. 프로세스간 통신(IPC) 2 (0) | 2022.02.04 |
Chapter 07. 프로세스 간 통신(IPC) 1 (0) | 2022.02.04 |
댓글