지난 글에서는 스택 프레임의 개념에 대해 정리하였다.
스택프레임은 스택이라는 메모리 공간을 차지하는 기본 단위이다.
서브루틴을 호출할 때마다 레지스터와 스택프레임이 할당된다.
이때 할당되는 스택프레임의 크기는 save 명령어를 통해 지정할 수 있는데, 최소 사이즈는 64바이트였다.
(l-register, i-register 저장 용도)
만약 스택 프레임 내에서 또다른 함수를 호출한다면, 구조체 반환 포인터 크기 4byte와, 매개변수 전달 용도의 24byte 사이즈 공간이 추가로 더 필요해 최소 92byte 사이즈가 필요했다.
여기에 만약 지역변수를 추가로 사용한다면 사용할 지역변수의 총 사이즈만큼 추가로 할당이 필요했다.
마지막으로 총 사용하는 사이즈 크기를 8의 배수로 맞춰 생성해야 한다.
스택 프레임은 %fp (프레임포인터) 부터 %sp(스택포인터) 까지를 그 크기로 가진다.
스택 프레임의 구성은 %sp 부터 차례대로, 64byte -> 4byte -> 24byte -> 지역변수 공간
순으로 할당된다.
오늘 예제를 통해 보겠지만, 만약 a,b,c,d 의 지역변수를 선언한다면, %sp 부터 기준으로 d, c, b, a 순으로 선언되어 들어간다.
만약 4byte 크기의 a 지역변수에 접근한다면 %fp-4 주소로 접근하면 된다.
이번 글에서는 이렇게 정리한 스택 프레임의 개념을 실제로 사용해본다.
예제1
아래와 같은 C 코드가 있다고 해보자.
한번 이 C코드를 어셈블리로 직접 컴파일해보자.
f() {
int a, b, c;
a = 5;
b = 7;
c = a+b;
}
먼저 스택 프레임의 크기를 계산해보자.
함수 안에서 별도의 함수 호출은 없고, 함수 내에서 4byte 크기 지역변수 3개를 사용하였다.
64 + 4*3 = 64 + 12 = 76
이제 76을 8의 배수로 맞춰주자
-76 & -8 = - 80
80의 사이즈로 스택 공간을 할당해주면 된다.
구현 1
우선 간단하게 생각하면 이렇게 구현할 수 있다.
4byte 이므로 ld, st 명령어를 사용하였음에 유의하자.
(데이터 사이즈 별로 ld, st 명령어가 달라진다!)
출력 결과를 통해 스택에 잘 저장되었음을 알 수 있다.
지금은 직접 임의로 a, b, c 의 값을 %fp 와의 상대주소에 넣었는데, 실제 컴파일할 때 이렇게 된다는 것으로 이해하면 될 것 같다.
구현 2
그런데 %fp 에서 직접 숫자로 하드코딩해서 상대 주소를 빼니까, 확 와닿지 않는다고 느낄 수도 있다.
이 상대주소를 상수값으로 라벨링하여 아래와 같이 작성할 수도 있다.
어떤 변수의 값을 저장하는지 주석 없이도 직관적으로 이해하기가 좀 더 쉬워졌다.
마찬가지로 잘 출력된다.
예제 2
이번엔 다른 C 코드를 어셈블리로 바꾸어보자.
int main() {
int i = 1;
int s = 0;
while (i <= 10) {
s += i;
i++;
}
return;
}
1부터 10까지 합을 구하는 코드이다.
변수 i, s 는 모두 4byte 지역변수이며, i가 먼저 할당되고, 다음에 s 가 할당된다.
따라서 i 는 %fp-4 위치에, s는 %fp-8 위치에 저장될 것이다.
스택프레임의 크기는 기본 64byte + 4byte지역변수 2개 = 64 + 4*2 = 72byte 이다.
이 크기는 이미 8의 배수이므로 맞출 필요 없이 바로 사용하면 되지만,
이번엔 어셈블러에게 직접 스택 사이즈를 계산시키는 방법으로 코드를 작성해보고자 한다.
구현 1 - 모든 변수를 스택에 저장
먼저 i, s 변수를 모두 스택에 저장하는 방식으로 구현해보려고한다.
사실 굳이 구현을 해보지 않아도, 이 방식이 그렇게 좋은 방식이 아니라는 것은 알 수 있다.
메모리를 읽고 쓰는 것은 레지스터를 읽고 쓰는 것에 비해 월등히 느리기 때문이다.
i 라는 변수와 s 라는 변수 모두 자주 변하는 값인데, 이 값은 레지스터에 저장하는 편이 낫다.
하지만 ld, st 명령어에 익숙해지기 위해 메모리에 저장하고 불러오기를 해보았다
이렇게 구현하였다.
실행결과 55가 잘 나온다.
구현 2 - 자주 사용하는 변수를 레지스터에 고정하기
사실 i 변수도, s 변수도 모두 자주 변하지만 한번 i 변수를 레지스터에 고정해보자.
C 언어로 작성한다면 아래와 같이 될 것이다.
int main() {
register i = 1;
int s = 0;
while (i <= 10) {
s += i;
i++;
}
return;
}
이제는 잘 사용하지 않는 register 키워드로 변수를 선언하여 저장한 것과 같다.
이렇게 수정 되었다.
우선 필요한 스택 프레임의 사이즈가 4바이트 줄어들었다.
하지만 8의 배수에 맞춰주어야 하니 실제로는 그대로 80바이트가 선언되었다.
지역변수가 1개이므로 s 변수는 이제 %fp - 4 위치에 저장된다.
출력 결과도 아주 잘 나온다.
이 예제를 통해 아까 말했던 메모리와 레지스터 사용에 대해 고민이 필요함을 알 수 있었다.
만약 모든 변수 데이터를 메모리에 저장한다면, 그 데이터가 사용될 때마다 메모리에서 레지스터로 값을 불러오고 다시 쓰는 행위가 반복되어야 할 것이다.
그렇다고 모든 변수 데이터를 레지스터에 저장하기에는 레지스터의 개수가 한정되어 있기 때문에 데이터의 양이 많다면 이 또한 불가능하다.
제일 좋은 것은 메모리에서 레지스터로 값을 한번 불러온 다음, 해당 데이터에 대해 읽고 쓰기가 모두 끝나면 그때 다시 메모리에 쓰는 것일 것이다.
예제 3
이제 마지막 예제를 살펴보자.
지금까지는 예제에서 모두 'int' 라는 똑같은 사이즈의 데이터만 다루었다.
만약 다양한 사이즈의 변수가 선언된다면 이때는 어떻게 해야할까?
이때도 data 섹션에서 경계정렬을 했던 것처럼 정렬을 해주어야 한다.
하지만 스택 메모리의 경우, .align 같은 키워드가 없기 때문에, 우리가 직접 정렬을 해주어야 한다.
그리고 스택 프레임 사이즈를 정할 때도 그 정렬된 총 크기에 맞추어 스택 프레임 사이즈 선언을 해주어야 한다.
int main() {
signed char i = 1;
int s = 0;
while (i <= 10) {
s += i;
i++;
}
return;
}
예제 2와 같은 기능을 하는 코드이지만, 이렇게 변수가 선언되어있다고 해보자.
signed char 의 1바이트 변수 i 와 4바이트의 int 형 변수가 선언된 형태이다.
이 경우 스택 프레임의 크기는 어떻게 될까?
제일 좋은 것은 그림으로 그려서 확인해보는 것이다.
스택 프레임에서 변수 공간을 할당할 때는 수동경계정렬을 하듯이 순서를 바꿀 수가 없다.
먼저 선언된 변수가 %fp에 가깝게 위치하기 때문이다.
스택 사이즈를 선언할 때도 이를 고려하여 선언해야 한다.
나는 문득 그러면 그냥 선언된 모든 사이즈를 다 합친다음 그거에 대해 8의 배수를 취하면 되지 않을까? 하는 생각이 들었는데, 이는 적용할 수 없다.
각 메모리 사이즈가 1, 4, 1, 4, 1, 4 순으로 선언되는 경우가 있다고 해보면 모든 데이터 합은 15 이므로 16바이트만 선언하면 될 것 같지만, 실제로는 24바이트가 필요하다.
수동 경계정렬로 444111 로 할 수 없고, 141414로 해야만 하기 때문이다.
따라서 위 예제 코드도 기존 예제와 같이 4바이트 사이즈 공간 2개를 선언하여야 한다.
기존에 작성하였던 코드에서 i가 들어가는 부분만 stb, ldsb 로 바꾸어주고, 접근 주소값을 %fp-1로 바꾸어주면 된다.
실행결과도 잘 나온다.
이것으로 스택 프레임에 대한 내용 정리가 모두 끝났다.
다음 글에서는 1차원 배열을 스택 메모리에 어떻게 선언하는지 정리하고자 한다.
'CS > 어셈블리' 카테고리의 다른 글
[SPARC] ld: fatal: relocation error: ~~ symbol .data (section): value ~ does not fit 해결 방법 (0) | 2023.11.26 |
---|---|
[SPARC] 27. 일차원 배열 (0) | 2023.11.24 |
[SPARC] 25. Stack Frame (0) | 2023.11.17 |
[SPARC] 24. SPARC 메모리 맵 정리 ( + bss 영역) (0) | 2023.11.16 |
[SPARC] 23. label로 data 영역의 메모리주소 가져오기 (set, sethi) (0) | 2023.11.09 |