[App 개발] OS X 를 지원하지 않는 기종을 위한 커널 해킹 (6)
본문
멤버 변수 외에도 Mac OS X 드라이버에는 IOService (모든 Mac OS X 드라이버의 수퍼클래스) 로부터 상속받은 프로퍼티 테이블을 가지고 있습니다. 프로퍼티를 억세스하는 멤버는 퍼블릭이므로 필요할 때 변경하기 용이합니다.
이 기법은 Mac OS X 10.3 팬써에서 베이지 G3 기종을 포기할 때 필요했습니다. 팬써에서 공식 지원하는 기종은 USB 가 내장된 파워맥이었습니다. 베이지 G3 기종에서 팬써 인스톨 CD 를 부팅하면 인스톨 과정에서 멈추게 되어 있습니다. 인스톨러는 해당 기종이 미 지원 기종임을 알리며 인스톨을 거부하게끔 되어 있었지요.
하지만, Mac OS X 인스톨 CD 가 부팅되고 인스톨러가 미 지원 기종임을 알려주었다는 것인즉, 무엇인가 그 속에서 실행되었다는 뜻이지요. 실제로 애플은 팬써에서 베이지 기종을 위한 드라이버를 그다지 많이 제거한 것은 아니었습니다. 인스톨러는 그저 해당 기종이 미 지원 기종인지만을 확인한 후 인스톨을 거부했던 것이지요.
인스톨러는 어떻게 해당 기종을 알아낼 수 있었을까요? 장치 트리의 루트에 위치한 IOPlatformExpertDevice 에는 "model" 이라는 프로퍼티 (와 "compatible" 프로퍼티) 가 있어서 해당 기종을 확인하여 줍니다. 저는 인스톨러가 아마도 그 프로퍼티를 확인하여 인스톨 과정을 진행할 것인지를 판단하고 있다고 짐작하였습니다. 그렇다면 만약 그 프로퍼티를 인스톨러가 인식하지 못하는 값으로 변경하면 인스톨 과정이 속행될 것이라 생각했습니다.
플랫폼 엑스퍼트 GossamerPE 의 서브클래스를 만드는 것이 정석이었습니다. 하지만 ApplePMU 의 경우와 마찬가지로 GossamerPE 역시 더 이상 오픈 소스가 아니었습니다. 역시나 ApplePMU 에서 했던 것처럼, 플랫폼 엑스퍼트의 최후 오픈 소스 버젼을 이용할 수도 있었습니다. 하지만, GossamerPE 가 계속 이용될 것이기 때문에 저도 이것을 계속 이용하기로 하였습니다.
GossamerPE 를 치환하는 대신 GossamerPE 와 같이 움직이는 드라이버를 만드는 방법을 택하였습니다. GossamerPE 의 특성 (Info.plist에 정의된) 과 매우 유사한 GossamerDeviceTreeUpdater 드라이버를 작성하였습니다. 비 기본 IOMatchCategory 를 갖고 있다는 것이 차이점이었지요. Mac OS X 커널은 부팅 과정에서 각각의 드라이버와 장치들을 연결 시도할 것이므로, 만약 기본 연결 카테고리 (특정하지 않은) 를 이용하면 애플 드라이버를 우회할 수 있습니다. 하지만 비 기본 카테고리를 이용하면 애플 드라이버와 동시에 연결됩니다. GossamerDeviceTreeUpdater 의 Info.plist 는 다음과 같습니다.
GossamerDeviceTreeUpdater
CFBundleIdentifier
com.macsales.iokit.GossamerDeviceTreeUpdater
IOClass
GossamerDeviceTreeUpdater
IOMatchCategory
GossamerDeviceTreeUpdater
IONameMatch
AAPL,Gossamer
AAPL,PowerMac G3
AAPL,PowerBook1998
AAPL,PowerBook1999
PowerBook1,1
PowerBook2,1
iMac
IOProviderClass
IOPlatformExpertDevice
Listing 13. Extract from Info.plist file for GossamerDeviceTreeUpdater.kext
IOMatchCategory 를 제외하면 이 드라이버의 특성은 기본적으로 GossamerPE 와 동일합니다. IOMatchCategory 는 기존 드라이버를 대체하는 대신, 다른 드라이버들과 함께 본 드라이버를 커널이 읽게 합니다.
GossamerDeviceTreeUpdater 코드는 전체 드라이버 연결 과정이 끝날 때까지 기다렸다가, IOPlatformExpertDevice 내의 name, model 그리고 compatible 프로퍼티를 변경하여 인스톨러가 인스톨 과정을 속행하게 만듭니다. 기다리게 만든 이유는 몇몇 드라이버들이 compatible 프로퍼티를 참조하여 동작을 변경하게 만들어져 있기 때문에, 해당 과정이 끝날 때까지 원래 값을 유지하도록 하는 과정이 필요했습니다.
이 과정과 몇 가지 자잘한 변경을 Mac OS X 10.3 (그리고 10.4) 에 가하여 베이지 G3 에 무사히 동작하게 되었습니다.
그 외에도 Mac OS X 10.3 에서 모든 스커지 드라이버가 분리 가능하게끔 취급하도록 고치기 위하여 몇 가지 프로퍼티를 조절해야 했습니다. 이 변경은 eject 버튼이 나타나게끔 하는 미적인 측면도 있었고, 어떤 세팅에서는 로그 인/아웃 과정에서 드라이버 마운트/언마운트 시 문제를 일으키는 실용적 측면도 있는 변경이었습니다.
이 문제는 어떤 한 드라이버가 설정하는 "Physical Interconnect Protocol" 프로퍼티에 있었습니다. 대부분 스커지 컨트롤러는 이 값을 "Internal" 이라고 하지 않고 "Internal/External" 로 정의하는데, 왜냐하면 스커지 체인에는 내 외장 하드디스크를 모두 연결할 수 있기 때문입니다. 해결 방법은 간단히 올바른 레지스트리 값을 넣어주는 것이었습니다. setProperty 메쏘드가 퍼블릭이었기 때문에 드라이버를 변경할 필요 없이 간단히 프로퍼티를 새로 넣어줌으로써, 다른 드라이버에서, 혹은 루트 계정을 갖고 있다면 유저 코드에서나, 어디서나 변경이 가능했습니다.
따라서, 장치 트리 내 객체의 프로퍼티 변경만 하면 되는 일이라면 매우 쉽게 구현이 가능합니다.
- 필요한 곳에서의 변경: 기본 방식이 실패했을 경우
애플이 IOKit 드라이버 시스템에서 채용한 기본 방식을 이용하면 커널 및 커널 익스텐션의 매우 세밀한 부분까지 조절할 수 있습니다. NVRAM 의 경우처럼 커널 내 포함된 드라이버를 우회하는 드라이버를 작성할 수 있었습니다. 두 개의 저장매체 경우처럼 기존 클래스의 서브클래스를 작성한 다음 우리 드라이버가 선택되게끔 할 수도 있었습니다. 기본적으로 저는 문제 해결을 위해서 가장 적게, 최소한의 변경만으로 가능한가를 알아내려 하였습니다.
하지만 경우에 따라서는 드라이버 시스템의 기본 방식만으로는 우리가 원하는 결과를 얻어낼 수 없고, 상당한 변경이 요구되는 경우가 있습니다. 그 중에 하나가 Mac OS X 10.4 의 NVRAM 에서 필요한 작업이었습니다.
이 문제는 NVRAM 과 직접적인 연관은 없었습니다. 이 문제는 Mac OS X 인스톨러가 구 기종의 ATA 드라이버에 인스톨할 때 발생하는 "8GB 한계" 지정 문제였습니다. 인스톨러는 드라이버의 최초 8GB 내에 파티션이 자리하는지를 검사하여, 만약 그렇지 않은 경우 해당 파티션에 인스톨을 거부하는 일이었습니다.
이렇게 "8GB 한계" 를 지정하는 이유는 구 기종의 내장 ATA 버스용 오픈 펌웨어 ATA 드라이버의 버그 때문이었습니다. 드라이버가 하드디스크의 8GB 이상을 읽어낼 수 없었고, 그곳에 Mac OS X 를 인스톨하는 것은 불안정하기 때문이었습니다. 만약 BootX 자신과 BootX 가 필요로 하는 커널, 커널 익스텐션, 커널 캐시가 모두 최초 8GB 내에 위치해 있다면 운용에 문제가 없습니다. 하지만 인스톨 시나 업데이트 시에 해당 파일들이 항상 최초 8GB 내에 자리하고 있으리라는 보장이 없었습니다.
문제는 이 "8GB 한계" 를 인스톨 프로그램이 매우 강압적이어서, 8GB 한계가 없는 고유 오픈 펌웨어를 내장한 PCI 카드에 연결한 ATA 드라이브까지도 그 한계를 적용한다는 점이었습니다. 이 정도로도 성가신 일이었지만, 문제는 Mac OS X 10.4 에서 더 심각했습니다. 인스톨러는 모든 드라이버가 내장 ATA 드라이버에 연결되어 있으며, 초기 파티션이 모두 8GB 를 초과하는 것으로 간주해 버렸습니다. 이렇게 되면서 Mac OS X 10.4 를 구 기종에 설치하는 문제가 더욱 더 어렵게 되었습니다.
만약 Mac OS X 인스톨러가 오픈 소스였다면, 인스톨러가 어째서 모든 드라이브가 ATA 에 연결되어 있으며 파티션은 8GB 를 초과한다고 인식하는지를 알아내기는 쉬웠을 것이고, 어떤 방식으로든 그 문제를 해결할 수 있었을 것입니다. 하지만 인스톨러는 오픈 소스가 아니므로, 정확히 어떻게 문제가 생긴 것인지를 알아낼 수 없었습니다.
하지만 이 문제는 신 기종에는 영향을 미치지 않습니다. Mac OS X 인스톨러는 신 기종에게 8GB 한계를 적용하지 않았습니다. 그렇다면 신 기종인 척 하면 어떨까요? 이렇게 할 경우, 정말로 8GB 한계가 문제가 되는 기종에 대해서도 인스톨을 진행하게 될 수도 있으므로 문제가 있습니다. 하지만 적어도 8GB 한계가 문제가 되지 않는 기종에 인스톨을 진행할 수 있게 됩니다.
저는 인스톨러가 hw.epoch sysctl 의 값을 참조하여 (hw.epoch 가 0 이면 구 기종, 1 이민 신 기종) 구 기종과 신 기종을 구별한다고 가정하였습니다. 따라서 만약 그 sysctl 을 변경하기만 하면 인스톨러가 해당 기종을 신 기종으로 인식하게 할 수 있을 것입니다.
커널 소스코드를 분석한 결과 hw.epoch sysctl 은 PEGetPlatformEpoch() 함수의 리턴값에 따라 정해지고 있었고, 이것은 플랫폼 엑스퍼트 내에 getBootROMType() 함수를 호출하는 것으로 구현되어 있었습니다. 플랫폼 엑스퍼트를 작성하였던 적이 있었으므로, (다행히도 가상 함수인) getBootROMType 를 오버라이드 하여 리턴값을 우리가 원하는대로 변경하였습니다.
하지만 문제는 그렇게 간단하지 않았는데, 약간의 부작용이 있었습니다. NVRAM 구조가 구 기종과 신 기종간에 차이가 있었고, NVRAM 구조를 담당하는 커널 코드가 기종을 구분하기 위하여 getBootROMType 을 호출하고 있었던 것입니다. 이 함수가 잘못된 값을 리턴해 주었기 때문에 NVRAM 억세스를 완전히 망쳐 버렸던 것입니다.
고려해 본 방법 한 가지는 hw.epoch sysctl 을 처리하는 코드를 변경하여 getBootROMType 함수가 계속해서 구 기종임을 알려주더라도 신 기종인 척 하게 하는 것이었습니다. 이것은 hw.epoch 셀렉터 (커널 내의 sysctl__hw_epoch) 를 처리하는 구조를 변경하는 것으로 가능할 수 있었습니다. 하지만 이것은 완전한 해결책은 아니었습니다. 구 기종과 신 기종의 차이는 단지 NVRAM 구조만 다른 것이 아니라 내용까지도 차이가 있었고, 이것은 사용자 레벨에서도 정의될 수 있었기 때문에 구 기종과 신 기종의 구분을 sysctl 에 의존하고 있었던 것입니다. 물론 우리는 단지 신 기종인 척 하도록 하는 것이므로 우리 응용프로그램에서 그것을 보상해 줄 수 있었습니다. 하지만 저는 가능하면 좀 더 일반적인 해결책을 원했습니다.
그래서 개입을 위한 다른 부분을 찾아보게 되었습니다. 가장 이상적인 부분은 장치 트리의 /options 노드에 위치한 드라이버인 IODTNVRAM 클래스로서, NVRAM 버퍼의 내용과 구조를 해독하는 일을 담당하고 있습니다. 이 클래스는 버퍼 입출력을 담당하는 IONVRAMController 클래스와 같이 동작하고 있지만, 버퍼의 내용과 구조는 IODTNVRAM 이 담당하고 있습니다. 따라서, 구 기종과 신 기종 NVRAM 의 차이를 알고 있는 클래스가 IODTNVRAM 이며, IONVRAMController 클래스는 하드웨어로부터 읽거나 쓰는 버퍼만 가지고 있습니다.
따라서 만약 IODTNVRAM 클래스를 커스텀 서브클래스로 대체한다면 getBootROMType 가 잘못된 값을 돌려주더라도 제대로 일을 하게끔 만들 수 있을 것입니다. 하지만 드라이버가 기본 방식을 따르고 있지 않았습니다. 일반적으로 플랫폼 엑스퍼트는 IODTNVRAM 인스턴스를 직접 생성하지 않아야 합니다. 그 대신 (앞에서 설명한) 드라이버와 연결되는 nub 을 생성해야 합니다. 그리하여 플랫폼 엑스퍼트를 변경하지 않고, 평소에는 IODTNVRAM 이 연결되었다가 필요에 따라 우회할 수 있어야 합니다.
하지만 플랫폼 엑스퍼트는 그렇게 동작하지 않고 다음과 같이 작성되어 있습니다.
void IODTPlatformExpert::processTopLevel (IORegistryEntry *rootEntry) {
…
// Publish an IODTNVRAM class on /options.
options = rootEntry->childFromPath("options", gIODTPlane);
if (options) {
dtNVRAM = new IODTNVRAM;
if (dtNVRAM) {
if (!dtNVRAM->init(options, gIODTPlane)) {
dtNVRAM->release();
dtNVRAM = 0;
} else {
dtNVRAM->attach(this);
dtNVRAM->registerService();
}
Listing 14. How not to instantiate a driver, if you want to maintain flexibility
최신글이 없습니다.
최신글이 없습니다.
댓글목록 2
hongjuny님의 댓글
이제 한 편만 더 번역하면 끝이군요. 이번 섹션에서 그간 구형 G3 기종에서 Mac OS X 인스톨 시 발생하던 8GB 파티션의 비밀이 밝혀져 있어서 매우 유익했습니다.
용가리님의 댓글
조용하던 개발실 게시판의 단비 같은 글이군요.
유익한 글 잘 보고있습니다. 감사합니다.