함수의 이름은 함수 포인터이다.
int func(int a, int b)
{
return a + b;
}
int main(void) {
printf("%d\n", func(10, 20));
printf("%d\n", (*func)(10, 20));
// 함수포인터에 별 아무리 찍어도 해당 함수포인터를 가리킴
printf("%d\n", (****func)(10, 20));
// 근데 이건 안됨. 함수 이름에는 크기가 없어서 그렇다.
// printf("%d\n", func[0](10, 20));
return 0;
}
ㅤ
ㅤ
함수포인터의 유용성
- 여러 동작에서 공통된 동작이 있으면 함수로 빼서 사용.
- 함수의 동작을 모두 구현하지 않고 일부를 비워둔 상태에서, 다른 사람이 로직을 구현하도록 할 수 있음.
- 동일한 알고리즘 / 다른 자료형 ⇒ 각각의 자료형에 대해 새로운 함수를 만드는 것이 불필요함.
- 비교하는 부분을 사용자가 작성해서 전달할 수 있도록 비워둠.
- ex) qsort
그럼 그냥 포인터에다가 (호출연산자) 를 붙이면 실행이 될까?
만약 함수명도 포인터라면, 단순히 int 변수에다가 함수의 주소를 담아두고 ( ) 호출연산자를 사용하면 함수가 실행되지 않을까? 궁금해서 한 번 시도해봤다.
void wow()
{
printf("WOW!!");
}
int main(void)
{
wow();
// 함수의 주소를 출력
void* test = wow;
printf("%d\n", test);
// int 에다가 함수의 주소를 저장
int hoxy;
scanf("%d", &hoxy);
// hoxy(); <- 이건 안됨
// 함수의주소에서 () 호출하기
((void(*)())hoxy)();
return 0;
}
>>> 콘솔
[출력] WOW!! // 함수 실행
[출력] 11211797 // wow 함수의 주소
[입력] 11211797 // int에 wow 함수의 주소를 담기
[출력] WOW!! // wow 함수의 주소에서 (호출) 실행!
ㅤ
시도해보니, 실제로도 동작했다!
물론 그냥 int 변수에다가 ( 괄호 ) 를 붙였다고 함수가 실행되지는 않았다. 이건 컴파일러 쪽에서 막아버렸다. ( 왜 정수에다가 괄호를 붙이니? )
ㅤ
int 변수를 wow 함수 타입인 void (*) () 타입으로 변경해주고 ( 괄호 ) 를 뒤에 붙여보았다.
((void(*)())hoxy)()
그랬더니 정상적으로 실행이 되었다~!
ㅤ
조금 더 욕심이 생겨서 좀 더 다양한 것들을 시도해봤다.
// void hoxy () 로 형변환
((void(*)())hoxy)();
// void hoxy (int, int) 로 형변환
((void(*)(int, int))hoxy)(3, 5);
// int hoxy (int, int) 로 형변환
int v = ((int(*)(int, int))hoxy)(3, 5);
printf("%d\n", v);
>>> 실행결과
WOW!!
WOW!!
WOW!!
6
이게 어차피 인자는 단순히 스택에 복사해서 넣는거고, 함수가 종료되고 나면 StackPointer를 이동하기만 하면 되니깐, 인자를 하나도 읽지 않는 wow 함수는 정상적으로 동작했다.
ㅤ
그리고 스택에서 Return Value를 받는 것으로 알고있는데, 혹시 타입 변환을 하면 값을 가져오려고 시도할까? 싶어서 한 번 시도해봤더니, 이것도 값을 쇽 뽑아왔다. 물론 함수에서는 이 return 값을 스택에 써준다는 동작이 없었으니, 의미없는 쓰레기값을 들고왔을테다.
ㅤ
그럼에도 우선 동작을 잘한다는게 나는 너무너무 놀라웠다! 😲😲😲😲
SP에서부터 offset 을 가지고 메모리에서 특정 데이터를 가져올텐데, 함부로 정의되지 않은 메모리의 값을 가져왔는데도 런타임 에러가 발생하지 않았다. 오호,,, 그저 운이 좋아서 그런걸까?
ㅤ
그렇다면 원래 int를 받아야 하는 함수에게 int를 안주는 것도 가능할까? 이래도 런타임 에러가 발생하지 않을까 궁금했다.
void want_int(int a)
{
printf("This is %d\n", a);
}
int main(void) {
// int 값을 파라미터로 필요로 하는 녀석에게 파라미터를 안주기
((void(*)())want_int)();
return 0;
}
>>> 실행 결과
3726181
ㅤ
런타임 에러가 발생하지 않았다!
인자의 전달과 이에 대한 검사는 함수를 호출하는 쪽의 책임이고, 호출된 함수는 그냥 신뢰하고 메모리에서 SP와 offset을 보고 값을 가져와 사용하는 건가보다. 오호라!
ㅤ
C의 세상은 참 신기하다…
---
+2025.09.03. 추가
ㅤ
정확하게 내가 이해한 대로 진행이 되고 있는지 궁금해서 클로드를 통해서 asm 코드를 작성해 디버깅을 해보았다. 사실 C에서 이렇게 어셈블리 코드를 작성해보는 것도 처음이라 짱짱 신기했다.
ㅤ
샘플 코드는 다음의 절차로 검증했다.
ㅤ
foo는 아무런 인자도 전달받지 않고, 리턴도 아무것도 하지 않는 심플한 함수이다. 그런데, 함수 내부에서는 인자를 전달받는 위치로 직접 메모리주소로 접근해서 3개의 인자값을 가져와 그 주소와 값을 출력해본다. 그 다음 return 으로 값을 반환하며 함수를 종료한 뒤, main 함수에서 return 값을 담는 레지스터의 값을 간접적으로 확인해본다. (* return 값은 메모리가 아니라 register로 넘어온다. 만약 reigster에 담을 수 없는 큰 값이라면 다른 메모리에 저장하는 방식으로 가져온다고 한다)
void foo() {
int first_arg, second_arg, third_arg;
void* ebp_value;
void* arg1_addr, * arg2_addr, * arg3_addr; // ★ 이 주소들이 핵심!
__asm {
mov eax, ebp
mov ebp_value, eax
// 스택에서 인자 값들 읽기
mov eax, [ebp + 8] // 1번째 인자
mov first_arg, eax
mov eax, [ebp + 12] // 2번째 인자
mov second_arg, eax
mov eax, [ebp + 16] // 3번째 인자
mov third_arg, eax
// 인자들의 실제 메모리 주소 계산
lea eax, [ebp + 8] // 1번째 인자 주소 ★
mov arg1_addr, eax
lea eax, [ebp + 12] // 2번째 인자 주소 ★
mov arg2_addr, eax
lea eax, [ebp + 16] // 3번째 인자 주소 ★
mov arg3_addr, eax
}
printf("=== foo function stack analysis ===\n");
printf("EBP: %p\n", ebp_value);
printf("1st arg: %d at address %p\n", first_arg, arg1_addr);
printf("2nd arg: %d at address %p\n", second_arg, arg2_addr);
printf("3rd arg: %d at address %p\n", third_arg, arg3_addr);
// 추가: 메모리 덤프로 실제 값 확인
printf("Memory dump around stack:\n");
int* stack_ptr = (int*)ebp_value;
for (int i = 0; i < 8; i++) {
printf(" [ebp + %2d]: %p = %d\n", i * 4, &stack_ptr[i], stack_ptr[i]);
}
}
int main(void) {
int t = 0;
((int(*)(int, int, int)) foo)(3, 4, 5);
__asm {
mov t, eax // EAX 값을 변수로 복사
}
printf("%d\n", t);
return 0;
}
그리고 작성한 foo 함수를 (int(\*) (int, int, int)) 로 type casting을 해서 실행해보면 실제로 이렇게 동작하는 것을 확인할 수 있다. 리턴값도 잘 넘어오는게 좀 흥미롭다!
ㅤ
와우!!
// 실행결과
=== foo function stack analysis ===
EBP: 0093F898
1st arg: 3 at address 0093F8A0
2nd arg: 4 at address 0093F8A4
3rd arg: 5 at address 0093F8A8
Memory dump around stack:
[ebp + 0]: 0093F898 = 9697672
[ebp + 4]: 0093F89C = 11687863
[ebp + 8]: 0093F8A0 = 3
[ebp + 12]: 0093F8A4 = 4
[ebp + 16]: 0093F8A8 = 5
[ebp + 20]: 0093F8AC = 11669539
[ebp + 24]: 0093F8B0 = 11669539
[ebp + 28]: 0093F8B4 = 7012352
8
'Embedded System > C언어' 카테고리의 다른 글
| [C언어] 상수를 적으면 일단 int32_t? (0) | 2025.10.18 |
|---|---|
| [C언어] Implementation-Defined Behaviour (0) | 2025.08.24 |
| [C언어] stdint.h 를 통한 타입 작성과 CLANG Header 읽기 개고생 (1) | 2025.08.24 |