안녕하세요..

UEFI라고 들어보신 분들 많으실겁니다..

UEFI는 "통일 확장 펌웨어 인터페이스"로 기존 BIOS를 대체하기 위해 나온건데요.. 쉽게 말해서 BIOS가 진화한겁니다.

EFI에서 시작해서, 지금은 UEFI로 온거거고요.

기존 BIOS의 단점들을 뜯어고치는게 큰 목적이였겠죠.

그중 하나가.. OS와 펌웨어 사이의 인터페이스를 구축하는 것이였습니다.. 그 인터페이스란게 BIOS와 어떤 차이인진 몰라도.. 뭐.. 그렇다고 합니다.(기존의 SWI에서 C 인터페이스로 바뀌는거 말하는거려나요..?)


아마도 개인용 PC쪽에서 봤을땐, 대중화되기 시작한게 Windows 8부터일겁니다.

이때부터 Windows 7과는 달리 거의 모든 PC들이 UEFI를 달고 나왔습니다.

참고로, UEFI를 지원하기 시작한건 비스타 SP1 64비트라 합니다.


개인용 컴퓨터에서 초반부터 쓴건 잘 알려진 회사중 고르자면 Apple입니다.

Apple은 Mac들을 PowerPC 프로세서에서 Intel로 전환하면서

기존의 OpenFirmware를 대체할 놈이 필요했는데 그렇게 결정된게 EFI였죠.

덕분에 Mac에 Windows를 돌리게 하는 기술인 Boot Camp의 핵심중 하나가 EFI 환경에 기존 BIOS 호환층을 박는거였습니다.

(여담이지만.. OpenFirmware는 핵심 부분을 제외하면 모든게 바이트코드여서.. Sun과 Mac 사이에 확장카드들이 호환될 정도로 호환성이 엄청납니다.)


여담이지만, PC에서 UEFI가 대중화되었지만.. 여전히 Mac OS를 돌리는데에는 추가 부트로더가 필요합니다. Apple은 뒤에서 언급할 EFI 시스템 파티션을 잘 안쓰기 때문이죠.(주로 System/Library/CoreServices/boot.efi같은곳에서 불러와서 그렇습니다..만.. 제 컴퓨터는 Mac OS X를 감지하더군요??!! 물론 부팅은 안되었습니다.)


글고.. MBR로 개발하는 사람에겐 조금 슬픈 소식이지만..

지금은 UEFI와 레거시 모드를 선택할 수 있는 PC가 많지만.. 기존 BIOS에 대한 호환성도 언젠간 사라집니다.


이 글에선 MINT64 개발시 UEFI와 기존 BIOS의 차이를 중심으로 이야기합니다. 안타깝게도 코드는 많이 없어요.


스크린샷(34).png

이건 요즘 하고있는 짓입니다.. EFI OS죠..
사실 콘솔이 너무 답답해서 초반부터 그래픽 모드로 갔습니다.

UEFI 사용시 장점이..
1. (일반적으로 쓰이는 64비트 UEFI에선) 프로그램 구동시 이미 64비트 모드입니다.
 => 덕분에 16비트에서 보호모드를 거쳐 64비트까지 올 필요가 없습니다...
2. 초기 부팅 코드부터 어셈블리어 없이 C나 C++로 쓸 수 있습니다. 메인함수 원형이 이렇게 되고요:
 => EFI_STATUS main(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable)
참고로 SystemTable은 각종 서비스 접근에 쓰이므로 어딘가에 static 변수로 두는걸 추천합니다.
3. 주소를 직접 대입할 일이 없습니다.
 => 각종 호출부터 특정 영역 접근까지 자료구조와 함수를 통해 이루어지며
거의 모든 핸들, 자료구조를 얻는 것이 EFI_SYSTEM_TABLE 자료구조를 거쳐서 이루어집니다.
펌웨어별로 메모리 맵이 달라질 수 있어서 주소를 직접 대입하는게 더 위험한 짓이 될수도 있습니다.
예를들어 콘솔에 문자열을 출력하고 싶다고 하면
SystemTable->ConOut->OutputString(L"Hello, world");
이런식이죠..(주의: OutputString을 비롯해 EFI에서 문자 관련해서는 char 대신 CHAR16(wchar_t)가 쓰입니다.)
4. 드라이버를 따로 구현하지 않아도 문자 입력정도는 됩니다.
=> SystemTable->ConIn을 통해 문자 입력정도는 가능합니다. 이벤트 기다렸다 눌린 키 얻는것도 가능하고요.
5. 64비트 기준으로, 커널 레벨이면 언제나 UEFI에 접근이 가능합니다.. 이론상으로는..
=> BIOS는 16비트에서 동작해서 보호 모드나 IA-32e로 오면 쓸 수가 없었지만 64비트 EFI는 이미 64비트까지 와서..
화면 해상도를 동적으로 바꾸는것도 별도 드라이버 없이 어렵지 않게 가능합니다.
6. 디스크를 따로 읽어서 메인 프로그램을 불러올 필요가 없습니다.
이미 EFI 파일 자체에 크기가 정해진게 아니라.. 물론 커널을 따로 읽어야 한다면 그건 어쩔 수 없이 해야하지만..
EFI 자체에 커널을 박을 생각이면 필요 없습니다.
이런 식으로 Linux를 별도 부트로더 없이 efi 파일째로 부팅하기도 합니다.(리눅스 커널을 EFI 파일이 나오게 제대로 빌드했다면, /EFI/BOOT/ 폴더에 BOOTX64.EFI란 이름으로 커널파일 넣고 부팅하면 부팅은 됩니다.. 옵션을 안줘서 리눅스 커널패닉이 날뿐이죠.)

다만.. BIOS와는 구조가 완전히 다르기 때문에 BIOS와 연관되는 부분들은 거의 다 갈아엎어야 한다는게 흠입니다.
일단 가장 큰 차이점으론 파티션 테이블, 부트로더 위치입니다.
기존 BIOS는 MBR 방식을 사용했고, 다들 알고 있는 것처럼 512바이트의 부트 섹터를 사용했습니다.
반면 UEFI는 GPT(GUID 파티션 테이블)이라는 방식을 쓰고, 수백메가정도 EFI 시스템 파티션(FAT32)에 efi 파일을 넣어서 부팅합니다.
다시말해, UEFI 버전으로 MINT64 OS를 만들었을때, 디스크 전체를 MINT 파일 시스템으로 둘 수 없다는 점입니다.

참고로, 보통 하드의 경우 EFI 시스템 파티션 내에서 EFI/BOOT/BOOTX64.EFI로 부팅합니다.(64비트 기준으로)

둘째로는 메모리 맵 구성입니다.
앞에서도 말했다시피 펌웨어별로 시스템 예약 공간이 달라질수가 있습니다..
이부분은 아직까진 저도 알아내진 못해서, 그나마 안전해 보이는 128MB를 베이스로 잡고 있습니다. EFI에 메모리 맵 구하는 함수가 있긴 한데 좀 더 알아봐야겠네요.

셋째로는.. 장점 3번에서도 이야기한 자료구조를 이용한 호출방식입니다.
EFI에서는 0xB80000 같이 주소를 직접 대입해서 시스템을 건들이는것보단 앞에서 언급한 EFI_SYSTEM_TABLE 자료구조를 이용하는게 안전합니다.
BIOS와 구조가 많이 다르기에, MP 설정 테이블도 EFI에서 동일한 기능을 하는걸 찾아야 하는데.. 아직 못찾았네요.


마지막으로 개발환경입니다.
EFI 파일은 PE32+ 포맷입니다. 이미 아시는 분들도 있겠지만, 윈도우에서 쓰는 포맷입니다.
즉, 개발에 리눅스 환경보단 윈도우가 더 적합합니다.
리눅스에서도 가능은 합니다만.. 리눅스에서는 링커 스크립트 쓰고.. 시스템 호출할때마다 uefi_call_wrapper 씁니다.(이걸 쓰는 이유는.. 아마도 펌웨어 내부적으로도 윈도우 호출규약을 따르기 때문같습니다.)
윈도우는 EFI 개발 관련 파일 준비하고, 인클루드 경로 설정하고 컴파일러랑 링커 옵션 좀 손보면 준비 끝입니다.

반면.. 이로 인해서 생기는 또다른 차이가.. 윈도우 호출 규약을 따르기 때문에 어셈블리어 사용시도 주의해야합니다.
윈도우 호출규약과 리눅스 호출규약의 차이는 저자분 블로그에 있으니.. 그걸 참고하시면 되고요.

하지만 예전에도 EFI OS를 시도해 본 적이 있는데..
그 글 자체만으론 커버가 힘들었습니다.

ok cpu-info <= Run command
CPU Vendor name: GenuineIntel <= Vendor string is good
CPU Brand: D: l(R) Core(TMD: CPU MD: <= What..?
ok

CPUID 명령어를 이용해 프로세서 정보를 구하는 부분인데요..(아마 후자는 확장 기능 CPUID 조회일겁니다..)
CPU Brand를 보시면, 정상적이라면
Intel(R) Core(TM) i5 CPU M 540 머시기
가 되어야 합니다.
근데, 대체 무슨일이 난걸까.. 하고


질문도 올려봤습니다.
답변의 내용으로는 대충..

"RAX, RCX, RDX, R8, R9, R10, R11는 휘발성이며 호출하는 쪽에서 보존되어야 합니다. 함수를 호출하고 나면 이들은 파괴되죠."
RCX, RDX, R8, R9는 알고 계신 분들도 있겠지만 각각 첫째, 둘째, 셋째, 넷째 파라메터입니다.
그런데, 문제는 RCX, RDX는 CPUID가 밀어버립니다.
따라서, 집이 공격받기 전에(?) 이 친구들은 다른 집으로 이사시켜 줘야 해요.

"RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15는 비휘발성이며 함수 내에서 보존되어야 합니다"
근데, 잘 보세요.. CPUID가 RBX를 파괴시킵니다.. 이런..
이놈은 PUSH랑 POP을 써서 저장했다 잘 복원해 줘야 합니다.

정리하자면 이렇습니다.

 RAX

 반환값, CPUID가 사용

 RBX

 비휘발성, CPUID가 사용

 RCX

 첫째 인자, CPUID가 사용, 휘발성

 RDX

 둘째 인자, CPUID가 사용, 휘발성

 RSI

 비휘발성

 RDI

 비휘발성

 RSP

 비휘발성

 RBP

 비휘발성

 R8

 셋째 인자, 휘발성

 R9

 넷째 인자, 휘발성

 R10

 휘발성

 R11

 휘발성

 R12

 비휘발성

 R13

 비휘발성

 R14

 비휘발성

 R15

 비휘발성


그럼 이렇게 모든 레지스터가 있는데 제 케이스는 어떻게 해결되었는지도 이야기할 겸.. kReadCPUID의 MASM 버전을 소개합니다..

NASM처럼 ;가 주석입니다.


option casemap:none ;이건 잘 모르겠습니다..
.code ;section .text와 같은 역활입니다.


;Cpuid 명령어
;ACpuid(Type, pEax, pEbx, pEcx, pEdx)

ACpuid Proc ;함수 정의입니다.
    ;Type => Rcx
    ;pEax => Rdx
    ;pEbx => R8
    ;pEcx => R9

    ;pEdx => [ rbp + 48 ]: 앞쪽에 스택 첫 항목이랑 RCX, RDX, R8, R9 예약해서 rbp + 48입니다.

    ;스택 설정(Base Pointer를 스택 포인터와 동일하게)
    push rbp
    mov rbp, rsp

    ;여기서부터 중요합니다..
    push rbx         ;RBX를 스택에 넣어서 백업합니다.

    mov r10, rdx     ;RDX에 pEax가 있으므로 R10으로 집을 옮깁니다.
    mov rax, rcx     ;RCX에 Type가 있으므로 RAX로 집을 옮깁니다.
    cpuid                ;CPUID 명령을 실행합니다.
    mov [ r10 ], eax ;R10에 있는 pEax를 가지고 저장합니다.
    mov [ r8 ], ebx ;R8에 있는 pEbx를 가지고 저장합니다.
    mov [ r9 ], ecx ;R9에 있는 pEcx를 가지고 저장합니다.
    mov r10, [ rbp + 48 ] ;pEdx를 R10에 잠시 담습니다.
    mov [ r10 ], edx ;R10에 있는 pEdx를 가지고 저장합니다.

    pop rbx ;두번째로 Push했던 RBX를 복원합니다.
    pop rbp ;맨처음 Push 했던 RBP를 복원합니다.
    ret ;반환
ACpuid Endp ;함수 끝

end


덤으로, 이걸로 당시 브랜드 스트링을 구하던 코드입니다.

            char brandString[48] = { 0, };
            ACpuid(0x80000002, &eax, &ebx, &ecx, &edx);
            *((Dword*)brandString) = eax;
            *((Dword*)brandString + 1) = ebx;
            *((Dword*)brandString + 2) = ecx;
            *((Dword*)brandString + 3) = edx;
            ACpuid(0x80000003, &eax, &ebx, &ecx, &edx);
            *((Dword*)brandString + 4) = eax;
            *((Dword*)brandString + 5) = ebx;
            *((Dword*)brandString + 6) = ecx;
            *((Dword*)brandString + 7) = edx;
            ACpuid(0x80000004, &eax, &ebx, &ecx, &edx);
            *((Dword*)brandString + 8) = eax;
            *((Dword*)brandString + 9) = ebx;
            *((Dword*)brandString + 10) = ecx;
            *((Dword*)brandString + 11) = edx;
            Console::Output.Write(L"CPU brand: ");
            for (int i = 0; i < 48; i++) {
                Console::Output.Write((wchar_t) brandString[i]);
            }
            Console::Output.WriteLine();


아마도 0x80000002, 0x80000003, 0x80000004가 각각 브랜드 스트링의 일부를 얻는 EAX 값이였던걸로 기억합니다.

각각 16자리(4바이트(DWORD) * 4개의 값) 문자열이 셋 모여서 48자리의 문자열이 생기죠.


아, 마지막으로 테스트 환경을 이야기하지 않았네요.

UEFI는 외부 드라이브의 경우에는.. 드라이브가 FAT32로 포맷되어 있으면(아마 MBR, GPT는 상관 없을겁니다.)

그 안에 아까 이야기한 EFI/BOOT에 경로에 BOOTX64.EFI란 이름으로 넣어주면 USB로 간단히 실제 PC에 테스트 가능합니다. 물론, PC가 UEFI를 지원하거나 안되는 경우엔 별도 부트로더를 써야 하며..

주의하셔야 할게 보통 이렇게 만들어진 놈들은 Secure Booting을 지원하지 않기 때문에 Secure Boot는 미리 셋업에서 꺼주셔야 빨간 메시지박스가 뜨면서 부팅을 거부한다던가.. 하는 사태를 막을 수 있습니다.


가상의 경우에는 디스크 구성시 윈도우의 VHD 기능을 쓰면 편합니다.

VHD 만들고 관리하는건 하드 디스크 관리하는 쪽에 있습니다. "하드 디스크 파티션 만들기 및 포맷" 혹은 Windows + R 누르고 diskmgmt.msc 누르면 떠요.

메뉴 잘 뒤져보면 만드는게 있고여.. 만들고 난 뒤에는 디스크(왼쪽)을 우클릭해서 초기화하는데 GPT로 해주시고요..

파티셔닝 적당히 하면 됩니다. EFI 시스템 파티션은 FAT32로 하는거 잊지 마시고요..

가상머신은 VMware Player 씁니다. VHD는 윈도우상으론 실제 디스크와 거의 동일하게 취급하기 때문에, 실제 디스크를 그대로 쓸 수 있는 VMWare Player랑 연동이 돼요. 가상머신 만들고 설정 들어가서 새 하드 디스크 추가하는데, 새로 만들거냐 물어보면 거기서 실제 디스크 쓰는 옵션 고른뒤 새로 만든 VHD에 해당하는 디스크를 고르면 됩니다.

보통 마지막으로 꽃은게 뒷번호로 가기도 하고, 디스크 관리 창에서도 확인하실 수 있습니다.