<C++ study>

[C++] 가상함수(Virtual Function)

gosoeungduk 2023. 1. 2. 22:46
반응형

#C++ #가상함수


C++ 에는 가상함수라는 개념이 있다. 같은 OOP 언어인 자바의 가상함수와 유사한데, 기본 클래스에서 가상 함수를 정의하여, 해당 기본 클래스를 상속 받는 파생 클래스가 해당 가상함수를 재정의하여 사용하도록 유도하여 Runtime 에 함수의 다형성을 높이기 위한 문법이라고 한다.

실행타임에 함수를 재정의해서 유용하게 사용할 수 있다는 것은 이해했는데, 의문점이 그냥 가상함수말고 일반적으로 함수 정의한 다음에 상속받은 클래스 단에서 오버라이드(재정의) 하여 사용하면 되지않나??? 라는 의문이었다.

정말 원론(?) 적인 이유는 다른 게시글 에 설명이 되어있었는데 엉뚱한 소멸자를 호출시키지않게 하기 위해서이다.

C++ 개발 경험이 많이 없기 때문에 이러한 세세한 부분에 대해서는 생각해볼 기회가 없었는데 C++ 설계의 치밀함에 또 감탄할 수 있었다.

가상함수와 소멸자?

우선 아래와 같은 클래스가 존재한다고 가정한다.

class CPerson {
public:
    void Talk() {
        cout << "Person" << "\n";
    }
};

class CMan : public CPerson {
    void Talk() {
        cout << "Man" << "\n";
    }
};

CPerson 은 기본 클래스이고, CManCPerson 을 상속받은 파생 클래스가 된다.

단적인 예시지만, 아래와 같이 CPerson * 객체를 담는(사람들을 담는) 연결리스트가 있다고 하자.

CPerson * 만 받기 때문에 개발 흐름상 선언할 수도 있는 CMan * 에 대해서는 전혀 받지 못하는 굉장히 불편한 상황이 생긴다.

그렇기 때문에 위와 같이 개발자는 반강제적으로 CMan * 이라는 명확한 타입표기 대신에 부모 클래스인 CPerson * 자료형으로 바꾸어 리스트에 객체를 추가할 수 밖에 없다.

그리고 이런식으로 코드를 짜버리고, 각 클래스 내부에 존재하는 Talk() 함수를 아래와 같이 실행시키는 경우를 상정해보자.

int main() {
    CPerson* pMan = new CMan;
    pMan->Talk();
}
/*
결과는 "Person" 이다. (??!!)
*/

C++ 의 경우 멤버함수나 여러 요소에 대해서는 이미 런타임이 아닌 컴파일 타임에 바이너리에 결합시켜버리기 때문에 어쩔 수 없이 부모 클래스의 자료형으로 선언한 결과에 대해서 Talk() 함수 실행이 이루어지게 되는 것.

이거는 보안적인 관점에 있어서도 개발자가 전혀 생각지도 못한 메모리를 참조하여 execution 까지 이루어지는 OOB 취약점에 가까운 부분이 되겠다.

앞서 말한 소멸자 사용의 경우에도 같은 버그가 발생한다.

class CPerson {
public:
    void Talk() {
        cout << "Person" << "\n";
    }
    ~CPerson() {
        cout << "Person is died\n";
    }
};

class CMan : public CPerson {
    void Talk() {
        cout << "Man" << "\n";
    }
    ~CMan() {
        cout << "Man is died\n";
    }
};

우선 클래스에 소멸자들을 추가했다. 그리고 아래와 같이 소멸자 호출까지 해보겠다.

int main() {
    CPerson* pMan = new CMan;
    pMan->Talk();
    delete pMan;
}
/*
결과는
Person
Person is died
*/

Person is died 문구가 나오는 것을 확인할 수 있었다. 개발자의 의도와는 다르게 전혀 생판 다른 소멸자를 호출해버린 것이다 !!!

결론적으로 !! 여기까지 설명한 모든 버그들을 방지하고, 깔끔한 코드작성을 위해 우리는 가상함수 라는 것을 쓸 수 있는 것이다.

아래는 위에 참고한 블로그 글에 기반한 가상함수 활용 코드 작성이다. 스타크래프트를 하는 유저가 여러 유닛을 부대지정하고 공격을 하는 상황을 가정한 것이다.

가상함수를 안썼다면, 각 유닛을 담는 클래스를 부모 클래스로 정의하고 심지어 attack() 함수 실행문 또한 if 문으로 일일히 걸러야했을지도 모른다.

#include<iostream>
using namespace std;
class myUnit {
public:
    virtual void attack() {
        cout << "기본공격\n";
    }
};
class marineUnit : public myUnit {
public:
    void attack() {
        cout << "마린 따발총\n";
    }
};
class hydraUnit : public myUnit {
public:
    void attack() {
        cout << "히드라 침퉤퉤\n";
    }
};
class darkTemplerUnit : public myUnit {
public:
    void attack() {
        cout << "닼템 썰기\n";
    }
};
int main() {
    myUnit* teams[12];
    marineUnit* pMarine = new marineUnit;
    hydraUnit* pHydra = new hydraUnit;
    darkTemplerUnit* pDarkTempler = new darkTemplerUnit;
    teams[0] = pMarine;
    teams[1] = pHydra;
    teams[2] = pDarkTempler;
    for (int i = 0; i < 3; i++) {
        teams[i]->attack();
    }

}
/*
<결과>
마린 따발총
히드라 침퉤퉤
닼템 썰기

이 창을 닫으려면 아무 키나 누르세요...
*/

리버싱 적인 측면에서는???

가상함수의 경우에는 컴파일 타임에 정적으로 고정되는 것이 아니라, 런타임 에 동적으로 재정의 되기 때문에 일반 함수들처럼 .code 영역에 담겨있지 않다.

.rdata 영역에 함수 포인터 리스트 형태로 담겨있는 것을 최근 분석하는 바이너리에서 발견할 수 있었는데 모습은 다음과 같다.

MFC 바이너리 같은 경우에는 기본 템플릿 API 들을 상속받아 사용자 정의 클래스를 만드는 경우가 많기 때문에 분석 시, 요 부분을 주의깊게 보아야할 것 같다. (그냥 C++ 기반은 모두...)

반응형