경일게임아카데미 멀티 디바이스 메타버스 플랫폼 개발자 양성과정 20220506 2022/04/04~2022/12/13

Jinho Lee·2022년 5월 6일
0

경일 메타버스 20220506 5주차 4일 수업내용. C언어 함수, 함수 스택 프레임, 함수 호출 규약, 빌드, 전처리, 전처리기, 매크로, 링크, 함수 포인터

함수 - 스택 프레임

  • 함수의 식별자는 유일해야 함. ⇒ 함수는 재정의 할 수 없다.
  • Stack : 정적 할당 영역 ⇒ 함수 ⇒ 이미 얼마나 메모리를 사용해야 할지 알고 있다.

  • Heap : 동적 할당 영역 ⇒ malloc()

  • Data : 정적 데이터

  • Code : 프로그램 명령어

  • 정적 : 한번 정해놓으면 변하지 않고 계속 유지되는 성질

    • 동적 : 프로그램 실행 중, 끊임없이 변하는 성질
  • 스택은 할당할수록 메모리 주소가 감소

    • 힙은 할당할수록 메모리 주소가 증가

프로세스_어드레스

Instructions → Code

  • bp (base pointer) : 스택의 시작 지점

  • sp (stack pointer) : 할당 받은 영역 끝

  • ebp : x86 아키텍처의 bp. 함수가 끝난 후 돌아갈 지점이 된다.

  • esp : x86 아키텍처의 sp. 함수가 할당 받는 만큼 뒤(아래)로 이동한다.

e : x86, r : x64

스택 프레임

  • 함수와 관련된 데이터를 말한다.
  • 스택 프레임은 임시 데이터 / 매개변수 / 반환 주소로 구성되어 있다.

호출 규약

  • 호출 규약(Calling Convention)
    함수가 어떻게 호출자로부터 인자를 받을 것이며, 결과값을 호출자에게 어떻게 돌려줄 것인지 규정해 놓은 것

  • 인자 전달 / 반환값 받는 것 / 스택 포인터 정리에 대한 규칙

  • 호출 규약이란

  • MSVC에서의 호출 규약

알아 두어야 할 세 가지 호출 규약 (x86)

  1. __cdecl
    • C, C++ 프로그램의 기본 호출 규약
    • 오른쪽에서 왼쪽으로 인자를 스택에 넣는다.
    • 스택 정리는 호출자가 하기에 가변 인자를 사용할 수 있다.
    • __stdcall보다 실행파일 크기가 커지게 된다.
  2. __stdcall
    • Win32 API 함수에서 사용하는 호출 규약.
    • 오른쪽에서 왼쪽으로 인자를 스택에 넣는다.
    • 스택 정리는 피호출된 함수가 한다.
    • 가변 인자를 사용할 수 없다.
  3. __fastcall
    • 첫 2개의 인자(크기가 4바이트보다 작아야 함)까지는 순서대로 ECX와 EDX 레지스터에 저장한다.
    • 나머지 인자는 오른쪽에서 왼쪽으로 스택에 넣는다.
    • 스택 정리는 피호출된 함수가 한다.
  • 위의 내용은 모두 x86 아키텍처 한정으로 x64 아키텍처 호출 규약은 또 다르다.

정리

  • 함수가 사용하는 메모리 : 스택 (Stack)
  • 함수와 관련된 내용(스택 프레임)

    1. 얼마나 메모리를 사용할 것인지 ⇒ 임시 데이터
    2. 매개변수(인자)
    3. 반환 주소
    • 스택 관리는 bp, sp를 이용함.
      • bp : 함수가 사용하는 메모리의 시작 주소
      • sp : 함수가 사용할 수 있는 메모리의 끝 주소
  • 호출 규약(Calling Convetion)

    • 인자를 어떻게 전달할 것이며, 반환값을 어떻게 돌려줄 것인가
  • x86

    • __cdecl : 기본 호출 규약, 인자는 오른쪽부터 왼쪽 순서대로 스택에 넣고, 스택 정리는 호출자가 한다. 가변 인자 사용 가능
    • __stdcall : 인자를 오른쪽부터 왼쪽 순서대로 스택에 넣고, 스택 정리는 피호출자가 한다. 가변 인자 사용 불가능
    • __fastcall : (가능하다면) 첫 번째 인자와 두 번째 인자를 레지스터에 저장하고, 나머지 인자는 오른쪽에서 왼쪽 순서대로 스택에 넣음. 그리고 스택 정리는 피호출자가 한다.
  • x64

    • 공식적으로 한 가지 호출 규약만 있다.
    • 4번째 인자까지 레지스터에 저장하고, 스택 정리는 피호출자가 한다.

빌드

전처리

  • 전처리(preprocessing)란 컴파일 전 일어나는 동작을 의미한다.
  • C언어에서는 다양한 전처리기를 지원하고 있다.
  • 전처리기는 #으로 시작한다.

다른 파일 포함하기 #include

  • #include는 다른 파일을 포함하는 전처리기다.
  • 헤더 파일(Header File)을 추가해 다른 소스 코드 파일에서 다른 파일에 있는 함수나 객체를 사용하기 위한 선언을 포함시키는 데 사용한다.
  • https://en.cppreference.com/w/c/preprocessor/include
// <>는 표준 라이브러리 혹은 써드파티 라이브러리를 포함하는 데 사용한다.
#include <stdio.h>
 
// ""는 내가(혹은 팀원이) 작성한 헤더 파일을 포함하는 데 사용한다.
#include "MyCode.h"

매크로 정의하기 #define

  • #define은 매크로를 작성하는 데 사용한다.
  • 매크로가 사용된 곳은 전처리 과정에 모두 교체된다.
  • https://en.cppreference.com/w/c/preprocessor/replace
    1. 매크로는 기호 상수처럼 사용할 수 있다.

    2. 함수처럼 사용할 수도 있다.

      2-1. 함수처럼 사용할 때는 매개변수에 꼭 ()를 사용하자.

    3. 매개변수에 #을 붙이면 문자열 리터럴로 인식한다.

    4. 매개변수에 ##을 붙이면 문자끼리 잇는다.

    5. 가변 인자도 사용할 수 있다.

      5-1. 똑같이문자열 리터럴로 인식한다.

      5-2. __VA_ARGS__는 ...이 들어갈 곳이다.

    6. #undef를 이용하면 정의했던 매크로를 해제할 수 있다.

    7. MSVC에서 미리 정의한 매크로도 있는데, 그 중 유용한 건

      7-1. __FILE__, __LINE__, __DATE__, __TIME__ 정도가 있다.

    8. MSVC에서 미리 정의해 놓은 매크로는 다음과 같다.

#include <stdio.h>
 
// 매크로는 기호 상수처럼 사용할 수 있다.
#define SPEED_OF_LIGHT 299792458
 
// 함수처럼 사용할 수도 있다.
// 함수처럼 사용할 때는 매개변수에 꼭 ()를 사용하자.
#define MIN(a, b) ((a) < (b) ? a : b)
 
// 매개변수에 #을 붙이면 문자열로 인식한다.
#define PRINT(msg) puts(#msg)
 
// 매개변수에 ##을 붙이면 문자끼리 잇는다.
#define DECLARE_MYTYPE(typename) struct MyType##typename    
 
// 가변 인자도 사용할 수 있다.
// __VA_ARGS__는 ...이 들어갈 곳이다.
#define SIMPLE_PRINT(...) puts(#__VA_ARGS__)
 
#define FORMAT_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
 
int main(void)
{
    printf("빛의 속도는 %d m/s이다.\n", SPEED_OF_LIGHT); // 전처리 과정 때 SPEED_OF_LIGHT 매크로는 299792458로 대체된다.
 
    if (MIN(10, 20))
    {
        printf("10은 20보다 작다.\n");
    }
 
    PRINT(안녕하세요);
 
    DECLARE_MYTYPE(Student)
    {
        int a;
    } a;
 
    SIMPLE_PRINT(1, 'a', "Hello");
 
    FORMAT_PRINT("Hello Macro : %d, %c, %s", 12, 'c', "Hello");
 
    return 0;
}

// #undef를 이용하면 정의했던 매크로를 해제할 수 있다.

#include <stdio.h>
 
#define SPEED_OF_LIGHT 299792458
#undef SPEED_OF_LIGHT // 매크로 해제
 
int main(void)
{
    printf("빛의 속도는 %d m/s이다.\n", SPEED_OF_LIGHT); // 컴파일 오류
 
    return 0;
}
 
#include <stdio.h>

#include "MyHeaderFile.h"

// 매크로는 기호 상수처럼 사용할 수 있다.
#define SPEED_OF_LIGHT 299792458
// 함수처럼 사용할 수도 있다.
// 함수처럼 사용할 때는 매개변수에 꼭 ()를 사용하자.
#define MIN(a, b) ((a) < (b) ? (a) : (b))
// #을 붙여서 문자열 리터럴로 인식한다.
#define PRINT(msg) puts(#msg);
// 매개변수에 ##을 붙이면 문자끼리 잇는다.
#define DECLARE_MYTYPE(typename) struct MyTape##typename
// 가변 인자도 사용할 수 있다.
// 똑같이 문자열 리터럴로 인식이 된다.
// __VA_ARGS__는 ...이 들어갈 곳이다.
#define SIMPLE_PRINT(...) puts(#__VA_ARGS__)
#define FORMAT_PRINT(fmt, ...) printf(fmt, __VA_ARGS__)
// #undef를 이용하면 정의했던 매크로를 해제할 수 있다.
// #undef SPEED_OF_LIGHT

// MSVC에서 미리 정의한 매크로도 있는데, 그 중 유용한 건 
// __FILE__, __LINE__, __DATE__, __TIME__ 정도가 있다.
#define PRINT_ERROR(msg) printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)
// 매크로를 잘 써야 C++도 C#처럼 생산성이 나온다.

int main(void)
{
	printf("빛의 속도는 %d m/s이다.\n", SPEED_OF_LIGHT);

	if (MIN(10 - 20, 20 < 4))
	{
		printf("10과 20 중 작은 수는 10입니다.");
	}

	PRINT(안녕하세요);

	DECLARE_MYTYPE(Student)
	{
		int a;
	} a;

	SIMPLE_PRINT(1, 'a', Hello);

	FORMAT_PRINT("Hello Macro : %d, %c, %s", 12, 'c', "Hello");
	
	printf("FILE : %s\n", __FILE__); // 파일 이름
	printf("FILE : %d\n", __LINE__); // 줄 번호
	printf("FILE : %s\n", __DATE__); // 날짜
	printf("FILE : %s\n", __TIME__); // 시간

	if (1)
	{
		PRINT_ERROR("오류가 발생했습니다.");
	}

	int a_ = 10;
	int b_ = 20;
	printf("Add_result = %d", Add(a_, b_));

	return 0;
}

// MSVC에서 미리 정의해 놓은 매크로도 있다.

조건부 컴파일 Conditional Inclusion

  1. 식이 참이라면 #endif까지의 내용을 포함한다.

    1-1. #if 류를 썼다면 반드시 #endif로 맺어야 한다.

    1-2. defined()를 이용해 매크로가 정의되었는지 체크할 수 있다.

    1-3. #else로 #if의 식이 만족하지 않았을 때의 내용을 만들 수 있다.

    1-4. 앞에 !를 붙여 부정도 가능하다. : !defined

  2. #if defined를 #ifdef로 줄일 수 있다.

    2-1. 마찬가지로 #if !defined를 #ifndef로 줄일 수 있다.

  3. #elif는 else if 쓰는 것과 비슷하다.

#include <stdio.h>
 
int main(void)
{
    // 식이 참이라면 #endif까지의 내용을 포함한다.
#if 1
    puts("1. 이건 실행됨");
    // #if 류를 썼다면 반드시 #endif로 맺어야 한다.
#endif
 
// defined()를 이용해 매크로가 정의되었는지 체크할 수 있다.
#if defined(TEST)
    puts("2. 이건 실행 안됨");
    // #else로 #if의 식이 만족하지 않았을 때의 내용을 만들 수 있다.
#else
    puts("2. 이건 실행됨");
#endif
 
    // 앞에 !를 붙여 부정도 가능하다.
#if !defined(TEST)
    puts("3. 이건 실행됨");
#endif
 
    // #if defined를 #ifdef로 줄일 수 있다.
#define TEST
#ifdef TEST
    puts("4. 이건 실행됨");
#endif
 
    // 마찬가지로 #if !defined를 #ifndef로 줄일 수 있다.
#define T 10
#ifndef TEST
    puts("5. 이건 실행 안됨");
    // #elif는 else if 쓰는 것과 비슷하다.
#elif defined(T) && T > 2
    printf("5. 이건 실행됨");
#else
    printf("5. 이건 실행 안됨");
#endif
 
    return 0;
}

구현체 정의 동작

#pragma once

  • 헤더 파일은 포함 순서에 따라 꼬일 수 있다.
// A.h
#include "B.h"
struct A { int n; };
// A.h End
// B.h
#include "A.h"
struct B { int n; };
// B.h End
  • 위와 같은 상황이면 서로가 서로를 포함하게 돼 전처리 과정에서 수없이 많은 포함이 생기게 되고, 결국 빌드에 실패하게 된다.

  • 이런 이유 때문에 헤더 인클루드 가드(Header Include Guard)라는 기법을 활용했다.

// A.h
#ifndef __A_H__
#define __A_H__
#include "B.h"
struct A { int n; };
#endif
// A.h End
// B.h
#ifndef __B_H__
#define __B_H__
#include "A.h"
struct B { int n; };
#endif
// B.h End
  • 다만 매 헤더 파일마다 이런 작업을 하는 것이 쉽진 않다.
  • 그래서 대다수의 컴파일러에서는 헤더 파일을 단 한 번만 포함시키는 #pragma once를 지원하고 있다.
// A.h
#pragma once
#include "B.h"
struct A { int n; };
#endif
// A.h End
// B.h
#pragma once
#include "A.h"
struct B { int n; };
#endif
// B.h End

#pragma comment

  • 기존 라이브러리에 없는 헤더 파일을 사용할 때, 그 위치로 연결할 때 사용

링크

  • Link : 실제로 이 식별자가 어떤 메모리 주소를 의미하는지 알려줌.

함수 포인터 pointer to function

  • 함수는 암시적으로 포인터로 변환 됨. ex) Foo;

    • return-type (*identifier)(parameter-list);
  • 함수 포인터는 왜 필요한가? => 콜백(callback) 함수 때문

  • 콜백 : 함수 안에서 또다른 함수를 실행하는 것

정리

  • 빌드 : 프로그램을 만드는 과정
  1. 전처리 : 컴파일 전에 하는 처리

    • 조건부 컴파일 / 매크로 대체 / 다른 파일 포함 등
  1. 컴파일 : 사람의 언어로 작성된 소스 코드를 컴퓨터가 알 수 있는 기계어로 번역
  1. 링크 : 식별자가 의미하는 메모리 주소를 연결
  1. 함수 포인터 pointer to function

    • 함수는 암시적으로 포인터로 변환 됨.

      • return-type (*identifier)(parameter-list);
    • 함수 포인터는 왜 필요한가? => 콜백(callback) 함수 때문

      • 콜백 : 함수 안에서 또다른 함수를 실행하는 것

0개의 댓글