전에 UEFI 기반으로 OS를 만들어보다, GDT와 IDT 세팅을 하고 쓰다 보니 깨달은게

UEFI는 32나 64비트이다 보니 부트로더/커널 진입 전부터 GDT 관련 설정이 이미 된 상태이고, 새로 정의한 셀렉터를 가리켜주는 작업이 필요하다는걸 알게 되었습니다.


사실 DS, ES, FS, GS, SS 교체는 어렵지 않습니다.


mov ax, 0x10

mov ds, ax

mov es, ax

mov fs, ax

mov gs, ax

mov ss, ax


그냥 MOV로 대입해주면 되기 때문이죠. 하지만 CS는 모두들 아시다시피 MOV로 대입이 안되고, Far-Jump를 통해 CS를 교체하게 됩니다.

문제는 64비트 환경에선 Far-Jump를 어떻게 해야할지 모르겠더라고요.

사실 성공한지 꽤 되어서, 정확히 제가 어떤것들을 시도해봤는진 모르겠지만


jmp [셀렉터]: [주소]


이런식의 코드로는 안됩니다. 컴파일시 instruction not supported in 64-bit mode라고 떠요.

그러고 구글을 뒤지다 Far-Return이라는걸 알게 되었고

몇번의 삽질 끝에 성공한 코드는 아래와 같습니다(적어도 "성공했다고 믿는" 코드...)


push [세그먼트 셀럭터]

lea rax, [rel .RELOADEDCS]

push rax

o64 retf

.RELOADEDCS:

; CS 변경 완료


위에서 가장 복잡(?)해 보이는게 LEA 줄인데

mov rax, [SOMETHING]을 하면, SOMETHING에 해당하는 영역의 값을 가져옵니다. 허나 MOV를 LEA로 바꾸면, 값 대신 SOMETHING의 주소가 RAX에 들어갑니다(CPU 내에서 메모리 어드레싱 처리 후 최종 주소값이 들어갑니다. 그러니까, 예를 들어 상대 주소를 썼다 해도 결과값은 절대 주소죠. LEA의 의미가 Load Effective Address인것 같아요).


그래서 lea rax, [rel .RELOADEDCS]는 rel .RELOADEDCS에 해당하는 주소값을 RAX에 넣어줍니다.

rel을 붙이는건 정확힌 모르겠지만 아마도 RIP 상대 어드레스와 관련이 있지 않나 싶어요. 그러니까, 기본 오퍼렌드 크기가 32비트이다 보니(87p 내용) rel을 통해서 .RELOADEDCS의 상대적 주소를 알려주고, RAX에는 CPU 내에서 RIP 상대주소에 대한 어드레싱 처리를 끝내고 난 결과 주소를 담는것 같고요.


마지막 o64 retf에서 o64는 따로 설명이 필요 없을것 같고, retf가 Far-Return 명령어입니다.


저는 EFI 환경에서 한거다 보니, C에서 호출하는 어셈블리 함수 형태로 작성을 했는데

kResetSegmentSelectors:

  ; DS, ES, FS, GS, SS 교체

  mov ax, 0x10

  mov ds, ax

  mov es, ax

  mov fs, ax

  mov gs, ax

  mov ss, ax

  ; CS를 변경하기 위해 Far-return을 수행

  push 0x08

  lea rax, [rel .RELOADEDCS]

  push rax

  o64 retf

.RELOADEDCS:

  ret


전체적으로 이런식으로 썼습니다.