Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

Embedded System/C언어

[C언어] 함수의 타입변환은 무죄

sm_amoled 2025. 8. 21. 17:22

함수의 이름은 함수 포인터이다.

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

 

320x100