[App 개발] OS X 를 지원하지 않는 기종을 위한 커널 해킹 (4)
본문
Mac OS X 정품을 미 지원 시스템에 설치하는 또 다른 어려운 과제가 바로 스커지 CD-ROM 장치에 CD 를 넣을 경우 시스템 패닉이 일어난다는 점입니다. 이것은 좀 어려운 문제인데요, BootX 가 CD 로부터 커널을 읽어서 부팅을 해야 하는데, 커널이 인스톨 CD 를 마운트하는 순간 시스템이 뻑나기 때문입니다.
문제는 IOSCSIDriver 클래스의 readTOC 메쏘드에서 발생하는 것으로 나타났습니다. 이 메쏘드가 버퍼 포인터를 인수로 받아들인 다음 이것을 구조체에 저장을 하는데, 나중에 이 구조체 메모리가 해제됩니다. 문제는 이 과정에서, readTOC 메쏘드에서는 아직 다 작업이 끝나기도 전에, 버퍼까지 덩달아 해제되어 버리는 것입니다. 아주 기초적인 메모리 관리 문제이죠. 호출하기 전에 버퍼 유지 카운트를 변경해 주면 해결되는 문제입니다 (한 줄짜리 문제).
AppleNVRAM 의 경우처럼 수정된 클래스를 만들어서 Info.plist 의 값을 조절하여 커널에 있는 드라이버 대신 이 수정된 클래스를 선택하게끔 하는 것은 가능합니다. 하지만 이번에는 애플 드라이버를 우회하는 데에 어려운 문제가 있었습니다. AppleNVRAM 의 경우에는 애플이 원 클래스를 수정하는 것을 그리 걱정하지 않아도 되었습니다. AppleNVRAM 은 한 가지 일만 수행하는 짧은 클래스이고, 애플이 이 내용을 심하게 변경할 일도 거의 없으니까요. 따라서 기존 클래스를 무시한 채 수정된 클래스를 계속 사용할 수 있습니다.
하지만 IOSCSIDrive 클래스의 경우는 조금 복잡합니다. 이 클래스는 애플이 변경할 가능성이 높고, 매번 그 변경사항을 적용하기 위해서 매번 재컴파일과 배포를 거듭해야만 하지요. 따라서 가장 좋은 방법은 클래스 전체를 바꿔치기 하는 것보다, 문제를 일으키는 해당 메쏘드만 치환하는 것입니다.
다행스럽게도 드라이버를 처음부터 다시 짜 줄 필요가 없습니다. 애플 드라이버에서 서브클래스로 상속을 받는 것이 가능한 경우가 있으니까요. 다음이 바로 서브클래스의 헤더입니다 (또 다른 버그와 관련된 내용이 삭제되었습니다).
#include "IOSCSICDDrive.h"
#include "IOSCSICDDriveNub.h"
class PatchedIOSCSICDDrive : public IOSCSICDDrive {
OSDeclareDefaultStructors(PatchedIOSCSICDDrive)
public:
virtual bool deviceTypeMatches(UInt8 inqBuf[],UInt32
inqLen,SInt32 *score);
virtual IOReturn readTOC(IOMemoryDescriptor * buffer);
};
Listing 4. Partial contents of PatchedIOSCSICDDrive.h
수퍼클래스를 선언하기 위하여 원 헤더파일을 include 하고 치환되는 메쏘드를 서브클래스에 선언하였습니다. 어떤 경우는 문제를 일으키는 바로 그 메쏘드만(readToc 처럼) 새로 작성해 주면 됩니다. 이번 경우는 deviceTypeMatches 메쏘드도 같이 작성하였습니다.
IOReturn
PatchedIOSCSICDDrive::readTOC (IOMemoryDescriptor *buffer)
{
// This works around a bug that may be present in our
// superclass (it may release buffer without retaining it).
// But the bug may be fixed, so we check for that.
buffer->retain();
int bufferRetainCount = buffer->getRetainCount();
IOReturn result = super::readTOC(buffer);
if (buffer->getRetainCount() == bufferRetainCount) {
buffer->release();
}
return result;
}
bool
PatchedIOSCSICDDrive::deviceTypeMatches (UInt8 inqBuf[], UInt32
inqLen, SInt32 *score)
{
bool retVal = super::deviceTypeMatches (inqBuf, inqLen, score);
if (retVal) *score = 20000;
return retVal;
}
Listing 5. Partial contents of PatchedIOSCSICDDrive.cpp
이번 경우는 Info.plist 의 IOProbeScore 를 변경해주는 대신 deviceTypeMatches 메쏘드를 만들어 주었습니다 (수퍼클래스의 probe 메쏘드로부터 호출됩니다). 이렇게 해 주면 커널은 애플 드라이버(수퍼클래스 자신) 대신 이 클래스를 선택하게 될 것입니다.
수퍼클래스의 readTOC 메쏘드를 통째로 서브클래스의 readTOC 메쏘드로 다시 작성해줄 필요는 없습니다. 서브클래스는 버퍼 포인터를 인수로 받아서 그것을 수퍼클래스로 전달하기 전에 유지 카운트만 변경해 주면 되는 것이니까요. 그 다음 super::readTOC 가 유지 카운트를 변경하였는지를 검사합니다. 이렇게 해 주지 않으면 나중에 혹시 수퍼클래스의 버그가 고쳐졌을 때 우리 서브클래스가 문제를 일으킬 수 있으니까요.
이렇게 애플 드라이버를 서브클래스로 해서 애플의 문제있는 메쏘드를 간단한 오류 검사 루틴으로 포장한, 몇 줄짜리 간단한 오류 수정 드라이버를 만들 수 있었습니다.
- 패밀리 처리: 스커지 장치가 사라지는 경우
유사한 문제가 Mac OS X 10.3 초기버젼에 있었습니다. 매번 그렇지는 않지만 가끔 시동시 스커지 장치가 나타나지 않는 것입니다. Verbose mode 로 부팅할 경우 조금 나아지는 것을 보면 이 문제는 아마도 부팅 과정에서 타이밍 문제가 발생하여 그런 것이었습니다.
문제는 IOBlockStorageDriver 클래스와 IOBlockStorageServices 클래스와의 사이에서 발생하였습니다. 이 문제를 이해하기 위해서는 장치와 드라이버의 매치를 위하여 커널이 작성하는 장치 레지스트리의 구조에 대해서 더 깊이 파고들어야 합니다. AppleNVRAM 의 경우, 장치 레지스트리에 등록된 nvram 장치가 있고, 장치와 연결되는 드라이버가 선택됩니다. 이것이 레지스트리 수준에서 장치와 드라이버를 연결하도록 고안된 기본 방식입니다. 이 방법은 장치와 드라이버 간의 느슨한 연결을 만들어주는 유용한 기법입니다. 일반적으로 어떤 드라이버가 또 다른 드라이버의 인스턴스를 직접 생성하는 것은 좋은 방법이 아닙니다. 반대로, 등록되어 있는 장치 목록으로부터 커널이 어떤 드라이버가 이 장치에 적합한지를 판단하게끔 하여야 합니다. 여기서 장치란 반드시 하드웨어 장치만을 뜻하지는 않습니다. 어떤 경우는 드라이버간의 느슨한 연결을 만들어주기 위한 유사 장치가 될 수도 있습니다.
애플은 이러한 드라이버 연결 방식을 데이터 저장 장치에 특별히 확대 적용하고 있습니다. 파워맥 7300 내부 스커지 버스에서 작성된 레지스트리 구조를 생각해 봅시다.
+-o mesh@18000
+-o meshSCSIController
+-o IOSCSIParallelDevice@0
+-o IOSCSIParallelInterfaceProtocolTransport
+-o IOSCSIPeripheralDeviceNub
+-o IOSCSIPeripheralDeviceType00
+-o IOBlockStorageServices
+-o IOBlockStorageDriver
+-o IBM DDRS-39130W Media
+-o IOMediaBSDClient
+-o IOApplePartitionScheme
+-o Apple@1
| +-o IOMediaBSDClient
+-o Macintosh@2
| +-o IOMediaBSDClient
+-o Macintosh@3
| +-o IOMediaBSDClient
+-o Macintosh@4
| +-o IOMediaBSDClient
+-o Patch Partition@5
| +-o IOMediaBSDClient
+-o untitled@6
+-o IOMediaBSDClient
Listing 6. Partial ioreg output related to SCSI drive
목록 중에 mesh 는 바로 마더보드의 스커지 컨트롤러를 뜻합니다. meshSCSIController 클래스가 바로 이 장치와 연결됩니다. IOSCSIParallelDevice 는 스커지 ID 0 번 장치를 의미합니다. meshSCSIController 드라이버는 스커지 버스에서 발견된 모든 장치에 IOSCSIParallelDevice nub 을 연결합니다. 하지만 meshSCSIController 는 직접 드라이버를 생성하지 않고, 대신 커널이 그 일을 수행합니다. 일반적으로 말해서, 각각의 드라이버 클래스는 대응하는 장치와 그 장치에 딸려있는 것들만 알면 됩니다. 묶여있는 다른 드라이버에 대해서는 알 필요가 없습니다.
스커지 장치가 사라지는 경우 레지스트리 구조상 IOBlockStorageDriver 에서 끊어지게 되고 IOMedia 클래스까지 당도할 수 없습니다. IOBlockStorageDriver 코드를 살펴본 결과, IOMedia nub 의 생성에는 두 가지 방법이 있었습니다. 첫번째로, 위에서 IOBlockStorageDriver 객체로 미디어가 연결되었거나 혹은 떨어지는 것을 알려주는 message 메쏘드가 있었습니다. 이 경우 IOBlockStorageDriver 객체가 생성되기 전에는 미디어가 연결되었는지를 전달받을 수가 없습니다. 이런 경우, IOBlockStorageDriver 의 start 메쏘드가 checkForMedia 를 호출하여, 바로 reportMediaState 를 호출합니다.만약 미디어 상태가 변경되었다면 코드에서는 IOMedia nub 을 생성하여 변경 사항을 처리합니다. IOBlockStorageDriver 의 checkForMedia 메쏘드는 다음과 같습니다.
IOReturn
IOBlockStorageDriver::checkForMedia(void)
{
IOReturn result;
bool currentState;
bool changed;
IOLockLock(_mediaStateLock);
result = getProvider()->
reportMediaState(¤tState,&changed);
…
if (changed) {
result = mediaStateHasChanged (currentState ?
kIOMediaStateOnline : kIOMediaStateOffline);
}
IOLockUnlock(_mediaStateLock);
return(result);
}
Listing 7. IOBlockStorageDriver::checkForMedia
IOBlockStorageServices 의 reportMediaState 는 다음과 같이 구현되었습니다.
IOReturn
IOBlockStorageServices::reportMediaState ( bool * mediaPresent,
bool * changed )
{
*mediaPresent = fMediaPresent;
*changed = false;
return kIOReturnSuccess;
}
Listing 8. IOBlockStorageServices::reportMediaState
두 번째 문제는 변경된 코드와 같이 살펴보시면 매우 알기 쉬운 문제입니다. reportMediaState 메쏘드는 미디어의 상태 변경을 기억해 두지 않고 false 를 리턴합니다. 그렇다고 해서 checkMediaState 메쏘드가 쓸데없는 놈은 아니고, 만약 reportMediaState 로부터 미디어 상태가 변경되었음을 알릴 때에만 어떤 작업을 수행합니다. 따라서, IOBlockStorageDriver 클래스는 객체가 생성되기 전에 미디어 상태가 변경되는 경우에는 정상적으로 반응하지 못하고, 생성된 이후에 message 메쏘드에 반응합니다. 이것이 바로 부팅 과정의 타이밍에 민감했던 이유입니다. IOBlockStorageDriver 객체가 셋업된 이후에 미디어 장치를 찾는 작업이 끝난 스커지 장치만이 나타나게 됩니다.
이 문제를 수정하려면 IOBlockStroageDriver 클래스를 수정하거나 IOBlockStorageServices 클래스를 변경해야 합니다. 저는 전자를 선택했습니다. IOBlockStorageDriver 를 상속받아 checkMediaState 메쏘드의 수정판을 만들었습니다 (changed 메쏘드에서 리턴된 값에 의존하지 않고, 현재 미디어 상태와 기존에 기록해 둔 미디어 상태를 비교하도록 하였습니다).
이 문제는 애플 엔지니어들도 역시 발견하여 수정판을 Mac OS X 10.3.4 에 반영하였습니다. 그런데 애플은 다른 방법을 선택하였습니다. checkMediaState 가 changed 값을 기억하게 하지 않고, 그 대신 IOBlockStorageServices 를 수정하여 올바른 값을 토해 내게끔 만들었습니다.
이렇게 되면 제가 수정한 방법과 충돌이 발생하게 되므로, 저는 애플의 버그 수정판이 설치되었는지 아닌지를 판별하도록 커널 익스텐션을 변경하여야 했습니다. 아마도 시스템 전체가 10.3.4 가 설치되었는지를 확인해도 되었을 것입니다만, 저는 IOBlockStorageServices 가 담겨있는 커널 익스텐션의 버젼을 검사하도록 했습니다. 코드는 다음과 같습니다.
IOService*
PatchedBlockStorageDriver::probe (IOService *provider, SInt32
*score)
{
kmod_info_t *scsiAMF = kmod_lookupbyname
("com.apple.iokit.IOSCSIArchitectureModelFamily");
if (!scsiAMF) return NULL;
UInt32 targetVersion, installedVersion;
VERS_parse_string ("1.3.3", &targetVersion);
VERS_parse_string (scsiAMF->version, &installedVersion);
if (installedVersion >= targetVersion) return NULL;
return super::probe (provider, score);
}
Listing 9. Code to check installed version of kernel extension
이렇게 해서 애플이 해당 문제점을 해결하였는지 여부에 따라 우회할 수 있게 되었습니다.
checkForMedia 의 수정에는 한 가지 난제가 더 있었습니다. 원 코드에는 _mediaStateLock 이라는 lock 변수가 checkForMedia 를 시리얼라이즈 하기 위하여 사용되었습니다. 만약 그 lock 변수가 checkForMedia 내에서만 사용되었다면 서브클래스 내에서 변수를 따로 선언하였을 것입니다. 하지만, 이 변수는 다른 곳에서도 사용되는 것이었고, 따라서 수퍼클래스가 사용하는 lock 변수와 동일하게 사용하는 것이 중요한 것 같았습니다. 문제는 이 _mediaStateLock 변수가 헤더에 private 멤버로 선언되었기 때문에, 서브클래스에서 상속받아 사용할 수 없었습니다.
이 문제를 해결하기 위해서 먼저 헤더 파일을 복사한 다음 _mediaStateLock 변수를 private 에서 protected 로 변경하였습니다. 컴파일러는 이제 서브클래스에서 그 변수를 사용할 수 있었고, 실제로 public/protected/private 구분은 런타임에서는 큰 영향을 받지 않는 것으로 생각됩니다. 따라서 이러한 문제는 필요한 부분의 헤더를 변경하는 것으로 해결할 수 있을 것 같습니다 (적어도 이 경우에는 해결되었습니다).
최신글이 없습니다.
최신글이 없습니다.
댓글목록 1
hongjuny님의 댓글
켁~ 스코프 오퍼레이터가 메롱이 되었네요... ㅠㅠ