함수는 어떻게 저장되는가?
함수 포인터를 알아보다 작은 궁금증이 생겨서 찾아보고 정리해봤다.
다시 생각하보니 간단한 것이었는데 알아보면서 재밌었으니까~
함수는 어떻게 메모리에 저장되는가?
함수의 포인터는 함수의 원형과 거의 비슷하게 생겼다.
1
int (*pointer) (int, int)
변수의 포인터는 int*, double* 등으로 참조하는 변수의 크기를 알려준다.
주소에 접근해서 얼마만큼의 데이터를 읽어야하는지 알려주기 위해서이다.
하지만 함수의 포인터는 반환 자료형, 매개 변수 자료형밖에 존재하지 않는다.
함수는 어떻게 저장되길래 주소에 접근해서 값들만 넘겨주면 실행이 가능한걸까?
우선 함수와 지역 변수의 저장위치부터 다르다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
void add(int n, int m);
void minus(int n, int m);
void multiple(int n, int m);
void divide(int n, int m);
int main() {
int choice = 3;
int x=5, y=13;
void (*pointer)(int, int);
printf("함수 주소\nadd : %p minus : %p\n", add, minus);
printf("multiple : %p divide : %p\n", multiple, divide);
printf("지역 변수 주소\nchoice : %p x : %p y : %p\n", &choice, &x, &y);
return 0;
}
void add(int n, int m) {
printf("%d + %d = %d\n", n, m, n + m);
}
void minus(int n, int m) {
printf("%d - %d = %d\n", n, m, n - m);
}
void multiple(int n, int m) {
printf("%d * %d = %d\n", n, m, n * m);
}
void divide(int n, int m) {
if (m != 0) {
printf("%d / %d = %.2f\n", n, m, (double)n / m);
}
else {
printf("0으로 나눌 수 없습니다.\n");
}
}
함수의 주소와 지역 변수의 주소를 비교해보자.
visual studio 환경에서는 결과가 제대로 나오지 않기도 하고 디버깅도 할거라 ubuntu에서 진행했다.
test.c에 위의 코드를 저장하고 실행했다.
함수 주소는 지역 변수에 비해 낮은 주소에 저장되어 있고, 지역 변수는 높은 주소에 저장되어 있다.
(주소가 높다 낮다는 주소 값의 크기로 비교한 것이다.)
우선 지역 변수가 해당 주소에 어떻게 저장되어있는지 확인해보자.
디버깅 도구인 gdb를 이용했다. 참고로 주소는 실행할 때마다 다르다.
해당 주소에 접근해서 저장되어 있는 값을 확인하면 된다.
이렇게 choice의 값인 3, x의 값인 5, y의 값인 13이 16진수로 각각의 주소에 저장되어있다.
이번에는 함수의 주소에 접근해보자.
add함수인 0x555555555247부터 값들을 출력했다. 형광색으로 칠한 부분이 각 함수의 시작 부분이다.
이렇게 무슨 의미인지 모를 코드들이 적혀있다.
왜냐하면 함수는 우리가 적은 코드를 이진 코드로 변환해 저장하기 때문이다.
좀 더 알아볼 수 있는 어셈블리어로 확인해보자.
각각 add함수와 multiple 함수를 어셈블리어로 출력한 것이다.
이렇게 확인해도 무슨 의미인지는 모르겠지만 각 주소에 저장되어 있는 이진수들이 이런 의미다.
라는 것을 확인할 수 있다. 또한 두 함수의 어셈블리어 코드는 굉장히 비슷하다.
이 사실을 인지하고 add함수 0x555555555247와 multiple함수 0x5555555552ba의 값들을 비교해보자.
꽤나 비슷한 숫자들이 반복됨을 알 수 있다. 이렇게 어셈블리어가 이진 코드로 변환되어 있다.
이제 맨 처음 주제로 돌아가 어떻게 함수 포인터가 저장된 크기를 모른채 주소에 접근해 실행되는가?
이렇게 이진 코드를 실행하면서 ret(return)같은 코드를 만나면 실행을 종료하면 되는 것이다.
어? 그러면 return이 없는 void를 반환하는 함수는 어떻게 종료하는 것이지? 라는 의문이 들 수도 있다.
위의 코드를 보면 예시로 해본 코드들이 void를 반환하는 함수들이다.
컴파일러는 함수에서 명시적인 return이 없어도 자동으로 함수의 끝에 ret 명령어를 넣기에 잘 읽을 수 있다.
이렇게 변수와 함수가 메모리에 저장되는 공간과 방식이 다르다는 것을 알아 볼 수 있었다.
이해할 수 있는 만족스러운 결과가 나와서 뿌듯하다.