뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우)
01. 절차적 함수 호출Procedure Call 지원 CPU 모델
함수 호출은 소프트웨어에서 제공되는 기능으로 이해하는 경향이 강하나, 하드웨어 종속적인 부분이 상당수 존재한다. 함수가 호출되는 방식은 CPU에 따라서 차이를 보인다.
01. A. 스택 프레임Stack Frame 구조
함수 호출 과정에서 할당되는 메모리 블록을 스택 프레임이라고 한다. 함수 호출의 완료 시에는 주소를 알고 있다고 하더라도 기존에 선언된 지역변수에 접근이 불가능하다. 할당되었던 메모리가 반환되었기 때문이다.
01. B. sp 레지스터
스택은 지역변수를 위한 메모리 공간이다. 이름의 유래는 메모리의 구조적 특성(Last In, First Out)에서 비롯됐다. 스택 프레임은 Last In, First Out의 성격을 띠기 때문이다.
스택에 데이터를 쌓거나 반환하기 위해, 현재 어느 위치까지 데이터를 저장했는지 알아야 한다. 쌓아 올린 스택 위치를 기억해야 하는데 이를 위해 CPU 내에 sp(Stack Pointer)라는 이름의 레지스터가 존재한다.
sp 레지스터 값은 변수가 할당될 때마다 증가한다. 증가 후의 값은 다음 변수가 할당될 메모리 위치를 가르키고 있다. 반면에 호출된 함수가 종료될 경우 그 안에서 선언된 변수들을 동시에 모두 반환해야 하기 때문이다. 변수의 선언 시 현재 sp가 가리키는 위치에 할당하기 때문에 sp의 위치를 아래로 이동시키켜 이전에 할당된 변수의 값을 덮어씀으로써 이전 값을 반환할 수 있다. 때문에 스택 프레임은 sp가 가리키는 위치를 아래로 이동시켜 반환한다.
하지만, 호출이 완료된 함수를 빠져 나오는 시점에서 할당된 메모리 공간을 반환하기 위해 스택 프레임 단위로 sp를 아래로 이동시킬 때는 문제가 생긴다. 얼마만큼 sp를 이동시켜야 할지 알 수 없기 때문이다. 이러한 정보는 프레임 포인터 레지스터에서 담당한다.
01. C. 프레임 포인터Frame Pointer 레지스터
단순하게 생각해서는 새로운 레지스터를 사용하는 방법을 생각할 수 있다. 새로운 함수가 호출될 때마다 이 레지스터 값을 0으로 초기화하고 변수가 선언될 때마다 크기만큼 값을 증가시킨다. 하지만 이러한 방법은 변수 선언 시에 덧셈 연산이 동반되므로 스택 연산에 드는 비용을 상당히 늘리는 결과가 생긴다.
다시 문제의 원점으로 돌아가, 다른 방법을 생각해보자. 단순히 sp 위치를 함수 호출 이전으로 되돌리기 위함이라면 되돌아갈 sp의 위치만 저장해 두어도 된다. 이러한 역할을 하는 레지스터를 가리켜 fp 레지스터라 한다.
위 그림과 같은 과정을 통해서 sp 레지스터와 fp 레지스터가 작동한다. fp 레지스터를 통해서 문제를 일부 해결했지만, 완전히 해결했다고 할 수 없다. 해결하지 못한 남은 문제는 함수 호출이 중첩되었을 경우에 발생한다.
위와 같은 함수 fct2가 있고, 함수 fct1에서 호출한다고 가정했을 경우, fct1 함수의 스택 프레임 반환을 위해 저장해둔 주소값이 fct2 스택 프레임 반환 값으로 덮어써진다. fct2의 함수 반환에서는 문제가 없지만 fct1 함수의 반환에서 문제가 생기게 된다.
01. D. 프레임 포인터Frame Pointer
이러한 문제의 해결 방법은 덮어쓰는 문제가 발생하기 전에 fp에 저장된 값을 어딘가에 저장하면 된다. 함수 호출이 생길 시, fp 레지스터에 저장된 값을 스택에 저장하는 방법으로 해결할 수 있다.
02. 함수 호출 인자의 전달과 Push&Pop 명령어 디자인
스택에 관련된 연산을 보다 용이하게 하기 위하여 Push&Pop이라는 명령어를 디자인해보자. 지금은 함수 호출에 대해서 이야기를 하고 있고 보통 함수 호출과 프로시저 호출로 구분하는데 입력에 대한 출력이 반환값으로 존재하면 함수 호출이고, 출력에 해당하는 반환 값 없이 모듈화 해놓은 서브 루틴Sub-Routine의 실행을 위한 호출을 프로시저 호출이라 한다.
02. A. 함수 호출 인자의 전달 방식
함수 호출 시 전달되는 인자를 어디에 둘 것인지도 CPU마다, CPU의 제조사 표준마다 달라진다. 함수 호출이 끝나면 사라지므로, 지역변수처럼 스택에 할당된다고 볼 수 있다. 그러나, 모든 전달인자들이 반드시 스택에 할당되는 것은 아니다. 성능 향상을 목적으로 일부 전달인자들은 레지스터를 할당해서 이곳에 저장하도록 제품의 표준을 정의하기도 한다.
우리도 성능 향상을 위해서 네 번째 전달인자까지는 레지스터로, 다섯 번째 전달인자부터는 스택에 저장하도록 디자인하면 좋겠지만 앞서 디자인했던 CPU 구조가 무척 단순하기 때문에 선택의 여지가 없다.
함수 호출 시, 전달되는 인자들은 모두 스택에 저장하는 방식으로 디자인을 이어가보자.
02. B. Push & Pop 명령어 디자인
sp가 가리키는 현재 위치에 전달되는 인자값을 저장하고, sp를 증가시켜 다음 메모리 주소를 가리키게 하는 명령어를 구성해 보자. 이전에 정의했던 STORE 명령어는 레지스터에 저장된 데이터를 메모리에 저장하며, 구조는 STORE 대상, 목적지로 이루어져 있다.
STORE 7, sp와 같은 명령어를 작성한다면 '숫자 7을 sp가 가리키는 메모리 위치에 저장하라!'로 이해할 수 도 있겠지만 STORE의 첫 번째 피연산자는 레지스터 정보가 오도록 디자인되어 있기에 불가하다.
STORE 7, sp 명령어의 문제점은 다음과 같다.
1. STORE 명령어의 첫 번째 피연산자로 숫자 7이 아닌, 레지스터 정보가 와야한다.
- 해당 문제는 숫자 7을 임의의 레지스터에 저장하는 방법으로 해결할 수 있다. 어셈블리에 존재하는 MOV(MOVE) 명령어나 유사한 기능으로 대입 연산을 진행할 수 있다.
2. STORE 명령어의 두 번째 피연산자로 레지스터 정보 sp가 왔다. 그러나 이 위치에는 주소 정보가 와야 한다.
- Indirect 모드를 통해서 sp가 지닌 값을 0x40번지에 저장한다. 그리고 STORE 명령어를 STORE r1, [0x40]으로 구성한다.
추가적으로 sp 레지스터 값을 증가시키기 위해서 ADD sp, sp, 4라는 명령어를 대입하여 이룰 수 있다.
지금까지 명령어는 다음과 같다.
ADD r1, 7, 0
STORE sp, 0x40
STORE r1, [0x40]
ADD sp, sp, 4
03. 함수 호출에 의한 실행의 이동
지금까지 sp 레지스터와 fp 레지스터의 용도에 대해서 작성했다. 이번에는 프로그램이 실행되는 원리와 프로그램 작성 시 정의하고 호출되는 함수의 원리를 살펴보자. 이를 통해서 우리는 pc 레지스터의 역할을 살펴볼 수 있다.
03. A. 다시, 메모리 구조와 프로그램 카운터Program Counter
메모리 구조에서 코드 영역은 프로그램이 동작하기 위한 프로그램 코드가 올라가는 위치다. 명령어의 실행은 세 단계(Fetch, Decode, Execution)로 구분되어 있는데, 이 중에서 첫 단계인 Fetch는 명령어를 CPU의 내부로 가져온다.
명령어 길이가 4바이트일 경우, 그리고 실행 중인 프로그램이 1036번지에 있는 명령어라면, 다음에는 1040번지에 있는 명령어가 Fetch 되어야 한다. 어느 위치에 있는 명령어까지 실행했는지 기억해야 다음번에도 명령어를 실행할 수 있다.
앞선 sp 레지스터와 같이 명령어를 순차적으로 Fetch하기 위해 프로그램 카운터라 불리는 pc 레지스터를 둔다. 이때, 증가되어야 하는 pc 값은 자동으로 증가하기 때문에, 컨트롤 할 필요가 없다면 프로그래머가 직접 pc 값을 컨트롤하지 않아도 된다.
03. B. 함수 호출과 함수 종료
함수 호출은 순차적인 실행만으로는 부족하다. 특정 위치에 이동할 수 있는 기능이 있어야 한다. 함수 호출 발생 시, 호출된 함수에서 복귀할 때 특정 위치로의 이동이 필요하다. 이런 문제는 Program Counter를 통하면 해결된다. 32비트 명령어 기준, pc는 명령어 실행 시 4씩 증가한다. 함수 호출로 인해 이동해야 하는 주소값을 저장해 두면 자연스럽게 실행 위치가 이동하게 되는 것이다. 물론 이동 전에는 현재 pc 값을 저장하는 것이 필요하다.
sp 레지스터와 같은 이슈가 생길 수 있기 때문이다.
04. 함수 호출규약Calling Convertion
04. A. 함수 호출규약이란?
스택 프레임에서 스택에는 데이터가 순리대로 혹은 역순으로 저장되는 방법들이 존재한다. 이와 같이 함수 호출과정에서 할당된 스택 프레임을 반환하는 방법도 두 가지 이상이 존재한다. 함수 호출이 완료된 이후의 동작은 스택 프레임의 반환인데, 이 주체는 호출자 혹은 호출이 된 함수가 될 수 있다. 이처럼 인자 전달 방식과 스택 프레임 반환 방식을 약속해 둔 것을 함수 호출규약이라 말한다.
04. B. __cdecl, __stdcall + α
__stdcall이라는 키워드를 통해서 함수 호출규약을 지정하는 것이다. 다음과 같이 선언하여 사용할 수 있다.
int__stdcall STDCallFunction(int a, int b, int c);
'독서 > 뇌를 자극하는 윈도우즈 시스템 프로그래밍' 카테고리의 다른 글
[스터디] 시스템 프로그래밍 - Chapter 12. 쓰레드의 생성과 소멸 (2) | 2023.11.01 |
---|---|
[스터디] 시스템 프로그래밍 - Chapter 11. 쓰레드의 이해 (3) | 2023.10.30 |
[스터디] 시스템 프로그래밍 - Chapter 9. 스케줄링 알고리즘과 우선순위 (2) | 2023.10.04 |
[스터디] 시스템 프로그래밍 - Chapter 8. 프로세스간 통신(IPC) 2 (2) | 2023.10.04 |
[스터디] 시스템 프로그래밍 - Chapter 7. 프로세스간 통신(IPC) 1 (2) | 2023.10.04 |