[App 개발] OS X 를 지원하지 않는 기종을 위한 커널 해킹 (5)
본문
기존 드라이버에서 서브클래스를 만들어 우회하는 기법의 가장 큰 장점은 애플이 Mac OS X 를 업데이트 하더라도 커널 익스텐션을 계속 사용할 수 있다는 점입니다. 애플이 제공하는 드라이버를 변경하는 대신 커널 익스텐션을 이용해 왔으므로 사용자가 새로운 Mac OS X 업데이트를 하더라도 커널 익스텐션은 계속 사용 가능합니다.
커널 익스텐션이 Mac OS X 의 메이저 업데이트 (10.2 에서 10.3 으로 업그레이드하는) 에도 계속 사용이 가능한 데에는 몇 가지 커널 작성 기술이 필요합니다. 일반적으로 C++ 로 작성된 드라이버는 수퍼클래스의 변경에 매우 민감합니다. 수퍼클래스에 새로운 데이터 저장 장소가 추가되는 경우 (새로운 멤버 변수) 혹은 새로운 메쏘드 (특히 가상 메쏘드) 가 추가될 경우 서브클래스도 덩달아 재컴파일을 해야 합니다. 이렇게 되면 설치된 Mac OS X 버젼에 따라 다른 버젼의 드라이버를 배포하고 설치해야 하기 때문에, 드라이버 입장에서는 매우 난처한 일이 됩니다.
다행히도 Mac OS X 에서는 몇 가지 기준과 커널 작성 기술을 통해서 이 문제를 해결하고 있습니다. 기준 측면에서는, 대부분 IOKit 드라이버들은 드라이버가 런타임으로 생성하는 구조체의 포인터를 담고 있는 "expansion" 변수를 가지고 있습니다. 이렇게 하면 수퍼클래스 자신의 크기를 변경하지 않고도 구조체 크기를 늘려서 변수를 추가할 수 있습니다. 그리고 많은 IOKit 드라이버들이 미사용 가상 메쏘드를 가지고 있습니다. 그렇게 해서 마찬가지로 자신의 크기를 변경하지 않고도 필요한 메쏘드를 추가할 수 있습니다. 커널은 서브클래스 링크 시 업데이트된 수퍼클래스에 따라 필요한 내용을 조절하는 기능을 가지고 있습니다 (이것은 gcc 2.95 에서 3 으로 업데이트 되었을 때 ABI 의 변경을 다룰 때에서 사용된 기술입니다).
이러한 기법에 의해서 커널 익스텐션은 Mac OS X 에 대한 호환성을 유지하고 있습니다. 예를 들어서 만약 여러분이 커널 익스텐션을 Mac OS X 10.2 헤더를 이용해서 작성하더라도 대부분은 10.2 에서 10.4 까지 모두 동작할 것입니다.
하지만 Mac OS X 업데이트를 다루는 데에는 몇 가지 문제가 있습니다. 경우에 따라서는 커널 메쏘드가 없어지는 일이 있습니다. 이렇게 되면 해당 메쏘드에 의존하던 코드는 더 이상 링크가 되지 않습니다. 이런 경우 대부분 다른 메쏘드를 이용하여 코드를 재 작성해 줌으로써 Mac OS X 구 버젼과 신 버젼에서 동시에 작동하게끔 만들어줄 수 있습니다. 하지만 이것이 불가능한 경우에는 어쩔 수 없이 커널 익스텐션을 두 버젼 따로 만들어야 합니다.
어떤 경우는 코드 내에서 new 명령으로 객체를 만들었는데 그 객체의 크기가 변경되는 경우도 있으니 주의해야 합니다. Mac OS X 10.3 이 발표되던 당시 저는 미 지원 기종 부팅을 실험할 개발자 프리뷰를 구할 수가 없었습니다. 패닉이 되더라도 백트레이스 정보에서 얻을 수 있는 정보가 거의 없었지요. Mac OS X 10.3 이 발표된 이후 저는 IOPMrootDomain 클래스의 크기가 변경된 것을 알았습니다. 문제는 저의 platform expert 가 new 구문을 이용하고 있었다는 것이죠 (new IOPMrootDomain 요렇게요). 이런 경우 당연히 객체 크기가 서로 달랐습니다 (구 버젼 헤더로 컴파일했던 크기로 고정되었으니까요).
여기서 딜레마에 빠졌습니다. 만약 새 헤더로 드라이버를 새로 컴파일하면 Mac OS X 10.3 에서는 잘 동작하겠지만, 구 버젼에서는 동작하지 않겠지요. 다행히도 IOKit 객체를 컴파일 시 고정된 크기로 생성하지 않는 다른 방법이 있었습니다. new IOPMrootDomain 명령을 쓰는 대신 다음과 같이 간편하게 생성합니다.
(IOPMrootDomain *) IOPMrootDomain::metaClass->alloc ();
Listing 10. Handy way to create IOKit objects without worrying about size changes.
기본적으로 이 명령은 IOPMrootDomain 메타클래스로부터 IOPMrootDomain 객체를 생성하는 것입니다. 이렇게 하면 객체 크기가 달라져도 상관이 없습니다. 메타클래스는 객체의 올바른 크기를 알고 있으니까요.
- 갖다 붙히기: 서브클래스를 만들 수 없을 때 애플 클래스인 척 하는 법
애플 드라이버에서 서브클래스로 만들 때 장점 한 가지는, 일반적으로 애플 드라이버의 내용을 서브클래스에서 쉽게 참조할 수 있다는 점입니다. 몇 가지 경우 서브클래스를 만들 수 없을 때가 있습니다만, 그런 경우에도 도움이 될 만한 몇 가지 Mac OS X 의 기법이 있습니다.
먼저, 만약 애플 클래스의 헤더 파일이 없으면 서브클래스를 작성할 수 없습니다. 대부분의 Mac OS X 드라이버들은 오픈 소스이지만, 모두 다 그렇지는 않습니다. 그 중의 한 가지 예가 바로 ApplePMU 드라이버입니다. 이 모듈은 원래 오픈 소스였지만, Mac OS X 10.2 에서부터 클로즈드 소스가 되었습니다. 그리고 문제는 이 클로즈드 소스 버젼이 미 지원 기종에서 잘 동작하지 않는다는 점이었죠. 소스코드가 없으면 왜 작동이 안 되는지를 찾아내는 것은 어려운 일입니다. 그래서 저는 ApplePMU 의 오픈 소스 버젼을 이용한 대체 클래스인 OpenPMU 를 작성해 주었습니다. 클로즈드 소스 ApplePMU 헤더파일이 없었으니 서브클래스를 만들 수 없었죠.
서브클래스를 만들 수 없었던 또 다른 이유는 ApplePMU.kext 의 Info.plist 에는 OSBundleCompatibleVersion 엔트리가 없었습니다. 커널 익스텐션을 읽어들일 때 커널은 커널과 커널 익스텐션 간의 심볼을 이용하여 커널 익스텐션을 링크합니다. 커널 익스텐션의 Info.plist 는 OSBundleLibrarities 프로퍼티로 커널 익스텐션이나 커널 일부를 나타내는 심볼을 지정합니다. OSBundleLibraries 프로퍼티는 여러분의 커널 익스텐션이 필요로 하는 커널 익스텐션이나 커널 일부의 특정 버젼을 지정합니다. 물론 필요로 하는 커널 익스텐션이 하위 호환 가능한 새로운 버젼으로 업데이트 되었을 수도 있습니다. 이런 경우 OSBundleCompatibleVersion 항목을 참조하여 호환 가능한 가장 낮은 버젼을 확인할 수 있습니다. 하지만 만약 OSBundleCompatibleVersion 엔트리가 없어진다면 커널 익스텐션은 그 커널을 어떤 심볼로도 링크할 수가 없게 됩니다. 따라서 ApplePMU.kext 에 OSBundleCompatibleVersion 엔트리가 없어지면 이 클래스로부터 서브클래스를 만들어낼 수 없습니다.
이번에는 서브클래스를 만드는 것이 아니므로, OpenPMU 는 ApplePMU 와 동일한 수퍼클래스가 됩니다. 여기서 어려운 점은, 혹시 PMU 드라이버와 데이터를 교환해야 할 다른 드라이버들이 있을 수 있습니다. ApplePMU 객체를 원했는데 OpenPMU 객체가 돌아왔다면, 어떻게 통신을 해야 하는지 알 수 있을까요?
어떤 경우 드라이버들은 "패밀리" 수퍼클래스에 속해 있으며, 클라이언트는 수퍼클래스와 통신하는 법을 알고 있습니다. 하지만 ApplePMU 는 이런 경우에 속하지 않습니다. 클라이언트가 연결할 패밀리 수퍼클래스가 없었습니다.
다행히도 애플은 필요한 경우 특정한 클래스 타입으로부터 호출할 수 있는 메쏘드 수신 기법을 제공합니다. 모든 Mac OS X 드라이버들은 callPlatformFunction 메쏘드를 갖고 있습니다. 왜냐하면 이것은 모든 Mac OS X 드라이버의 수퍼클래스인 IOService 클래스에 선언되어 있으니까요. callPlatformFunction 의 전달 인수로는 어떤 동작을 지시하는지를 나타내는 OSSymbol 과, 계수들을 전송할 수 있는 여러 개의 void 포인터로 구성됩니다. 따라서 OpenPMU 가 ApplePMU 와 동일한 심볼로 수신한다면, 클라이언트 측에서는 객체가 무엇인가에 상관없이 callPlatformFunction 을 호출할 수 있습니다. 오리처럼 걷는다면 오리인 것이죠 (적어도 클라이언트 측에서 이 방식에 협조해 준다면 말이지요).
다음은 OpenPMU 에 구현된 callPlatformFunction 메쏘드입니다. ApplePMU 의 구현과 차이가 있을 수도 있습니다. ApplePMU 는 이제 클로즈드 소스이니까요. 하지만, 모르죠.
IOReturn
OpenPMUInterface::callPlatformFunction( const OSSymbol
*functionName, bool waitForFunction, void *param1,
void *param2, void *param3, void *param4 )
{
// Dispatches the right call:
if (functionName->isEqualTo(kSendMiscCommand)) {
…
}
else if (functionName->isEqualTo(kRegisterForPMUInterrupts))
{
…
}
else if (functionName->isEqualTo(kDeRegisterClient)) {
…
}
else if (functionName->isEqualTo(kSetLCDPower)) {
setLCDPower(param1 != NULL);
return kIOReturnSuccess;
}
else if (functionName->isEqualTo(kSetHDPower)) {
setHDPower(param1 != NULL);
return kIOReturnSuccess;
}
else if (functionName->isEqualTo(kSetMediaBayPower)) {
setMediaBayPower(param1 != NULL);
return kIOReturnSuccess;
}
else if (functionName->isEqualTo(kSetIRPower)) {
setIRPower(param1 != NULL);
return kIOReturnSuccess;
}
else if (functionName->isEqualTo(kSleepNow)) {
putMachineToSleep ();
return kIOReturnSuccess;
}
// If we are here it means we failed to find the correct call
// so we pass the parametes to the function above:
return super::callPlatformFunction (functionName,
waitForFunction, param1, param2, param3, param4);
}
Listing 11. Partial contents of OpenPMU::callPlatformFunction
여기에도 한 가지 문제가 더 남아있습니다. 어떻게 클라이언트에서 OpenPMU 드라이버와 통신을 시킬까요? PMU 하드웨어 nub 을 참조하여 어떤 드라이버가 연결되어 있는지를 볼 수도 있겠지요. 기본적인 방법은 waitForService( serviceMatching( "ApplePMU" ) ) 를 호출하여 커널로부터 ApplePMU 나 서브클래스 객체를 얻어내는 것입니다. 하지만 OpenPMU 는 서브클래스가 아니므로 이런 방식으로는 얻어낼 수 없습니다.
다행히도 waitForService 는 레지스트리 내의 객체들을 일일이 호출하여 객체가 ApplePMU 서브클래스인지를 확인합니다. 따라서 우리 클래스가 그런 척을 해 주면 됩니다.
bool
OpenPMU::passiveMatch (OSDictionary *matching, bool changesOK)
{
// The problem is that other drivers call waitForService
// (serviceMatching ("ApplePMU")). That would be OK if we could
// inherit from ApplePMU, but we can't, since ApplePMU.h is not
// available. So, instead, we just override passiveMatch so that
// this will match ApplePMU. This works OK so long as the other
// drivers use callPlatformFunction for talking to ApplePMU.
OSString *str = OSDynamicCast (OSString, matching->getObject
(gIOProviderClassKey));
if (str && str->isEqualTo ("ApplePMU")) return true;
return super::passiveMatch (matching, changesOK);
}
Listing 12. Pretending to be an ApplePMU driver
이렇게 하면 ApplePMU 를 얻으려는 클라이언트에게 OpenPMU 를 찾아오게 되고, callPlatformFunction 메쏘드로 구현된 것을 호출할 수 있습니다.
최신글이 없습니다.
최신글이 없습니다.
댓글목록 0