-
Notifications
You must be signed in to change notification settings - Fork 0
[RISC‐V 64] mcount.S 관련 사전 지식
-
Heap은 동적으로 메모리 공간을 할당받을 때 사용되는 영역
-
런타임 시점(프로그램이 실행되고 있는 중)에 결정됨
-
Stack은 함수 호출시 주어지는 인자와 지역 변수가 저장되는 영역
-
컴파일 시점에 결정됨
-
프로세스 메모리 구조 내부의 Stack은 자료구조에서 배웠던 Stack과 구조적으로 동일하며, 다만 위에서 아래로 자란다는 점이 다름
-
Push로 데이터를 쌓을 때 Stack 자료구조에서는 메모리 주소를 증가시키며 공간을 확보
-
프로세스 메모리 구조에서는 메모리를 감소시키며 공간을 확보
-
- 아래 그림은 프로세스의 메모리 구조를 나타낸 것으로, 우리가 여기서 봐야할 부분은 위에서 설명한 Stack 과 Heap 영역이다.
-
새로운 함수를 호출한다는 것은 이전 함수의 스택을 보관한채로 새로운 함수의 스택 공간을 확보한다는 것을 의미함
-
Stack Pointer는 현 시점에서 사용되는 스택이 확보한 공간의 끝 주소를 가리키고 있는 레지스터
-
위와 같은 이유로 Stack Pointer는 이전 함수의 Stack Pointer를 기억하지 않음 (누군가는 이전 함수의 Stack Pointer를 기억해야 함)
-
함수가 호출될 때 현재 Stack Pointer의 주소를 가지고 있다가 호출된 함수가 반환될 때 Stack Pointer가 돌아갈 위치의 주소를 알려줄 수 있는 친구가 필요
-
이 역할을 하는 레지스터가 Frame Pointer
- 조금 더 간략하게 말하자면 Frame Pointer는 이전 함수의 Stack Pointer 주소를 가리킴
-
Stack Alignment는 CPU가 메모리에 위치한 데이터에 접근하는 시점에 대한 속도 최적화를 위해 사용되는 기법
-
Memory Alignment에 대한 내용이 선행되어야 하는데, 결국 프로세스 메모리 구조의 Stack도 메모리에 들어가 있는 것이기 때문
-
결론적으로, mcount.S와 같이 어셈블리어를 사용해 스택을 저장하거나 복원할 때 각 CPU의 bit에 맞는 정렬 단위를 사용해야 속도 최적화가 됨
-
Memory Alignment를 설명하는 해당 예제에서는 32bit CPU에서 구조체의 데이터를 접근하는 것을 가지고 설명하고 있음
-
데이터를 정렬하는 것이 왜 속도 최적화와 관련이 있는지를 설명해주는 내용
-
32bit CPU에서는 4byte 단위로 메모리에 접근하고 64bit CPU에서는 8byte 단위로 메모리에 접근함
- 따라서, 각 CPU의 메모리 접근 단위의 배수가 되어야 한다는 내용
-
x86_64 기반의 운영체제들이 16byte Stack Alignment를 하는 이유는 아래 링크 참조
- 윈도우를 제외한 대부분의 OS에서 사용되는 x86_64 System V ABI(Application Binary Interface)에서는 Stack Alignmet로 16byte를 요구
- 링크 : https://stackoverflow.com/questions/49391001/why-does-the-x86-64-amd64-system-v-abi-mandate-a-16-byte-stack-alignment
-
aarch64 아키텍처에서 16byte Stack Alignment를 하는 이유는 아래 링크에서 “Shared Stack-Usage Rules”의 마지막 부분 참조
-
함수 호출 규약은 함수를 호출하는 시점과 반환하는 시점에 calller(함수를 호출하는 함수)와 calllee(다른 함수에 의해 호출되는 함수) 간 약속
- 함수 인자 전달 방법과, 호출 시 사용된 스택 공간의 정리는 누가 할지, ... 등을 정해놓음
-
함수 호출 규약을 알아야 하는 이유는 일반적으로 우리가 구현하는 프로그램은 어셈블리어로 다른 함수의 호출 없이 화면 출력부터 모든 기능의 코드를 구현하지 않는 이상 결국 함수를 호출하는 구조이고, 추가적으로 우리가 구현할 mcount.S에서 스택을 저장하고 복원하는 기능을 만들어야 하기 때문
-
함수 호출 규약은 아키텍처 별로 다르지만, 동일한 아키텍처여도 컴파일러에 따라 또 달라지기도 함
참고: [risc-v mcount 위치]
-
코드를 보면 caller에 해당하는 main 함수는 callee에 해당하는 foo 함수를 호출할 때 int 자료형 인자 하나와 char 자료형 인자 하나를 넘겨주는 구조
#include <stdio.h> void foo(int v1, char v2){ printf("foo()\n"); } int main(void){ foo(10, 'c'); }
-
이때 -pg를 주지 않고 컴파일한 실행파일의 objdump -d 명령어 결과는 아래와 같음
- foo 함수 호출 시 첫 번째 인자에 숫자 10이 들어가고 두 번째 인자는 아스키 코드의 ‘c’에 해당하는 0x63(10진수로 99)가 들어가는 것을 확인할 수 있음
0000000000000668 <foo>: 668: 1101 addi sp,sp,-32 66a: ec06 sd ra,24(sp) 66c: e822 sd s0,16(sp) 66e: 1000 addi s0,sp,32 670: 87aa mv a5,a0 672: 872e mv a4,a1 674: fef42623 sw a5,-20(s0) 678: 87ba mv a5,a4 67a: fef405a3 sb a5,-21(s0) 67e: 00000517 auipc a0,0x0 682: 04250513 addi a0,a0,66 # 6c0 <_IO_stdin_used+0x8> 686: f1bff0ef jal ra,5a0 <puts@plt> 68a: 0001 nop 68c: 60e2 ld ra,24(sp) 68e: 6442 ld s0,16(sp) 690: 6105 addi sp,sp,32 692: 8082 ret 0000000000000694 <main>: 694: 1141 addi sp,sp,-16 696: e406 sd ra,8(sp) 698: e022 sd s0,0(sp) 69a: 0800 addi s0,sp,16 # 인자 값 설정 후 foo 함수를 호출하는 부분 **69c: 06300593 li a1,99 6a0: 4529 li a0,10 6a2: fc7ff0ef jal ra,668 <foo>** 6a6: 4781 li a5,0 6a8: 853e mv a0,a5 6aa: 60a2 ld ra,8(sp) 6ac: 6402 ld s0,0(sp) 6ae: 0141 addi sp,sp,16 6b0: 8082 ret