-
Real mode프로젝트/운영체제 만들기 2023. 8. 5. 16:30
글의 참고
- https://en.wikipedia.org/wiki/Real_mode
- http://www.c-jump.com/CIS77/ASM/Memory/lecture.html
- https://wiki.osdev.org/Segmentation
- https://stackoverflow.com/questions/33827474/why-enable-a20-line-in-protected-mode
- https://en.wikipedia.org/wiki/A20_line
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.
- `글의 참조`에서 빨간색 볼드체로 체크된 링크는 이 글을 작성하면 가장 많이 참조한 링크다.
- `운영체제 만들기` 파트에서 퍼온 모든 참조 글들과 그림은 반드시 `이 글과 그림을 소스 코드로 어떻게 구현을 해야할까` 라는 생각으로 정말 심도있게 잠시 멈춰서 생각해봐야 실력이 발전한다.
글의 내용
- 리얼 모드 초기화
: `RESET#`, `Power-up`과 같은 하드웨어 리셋 동작들이 프로세서에 가해지면, x86 프로세서는 리얼 모드로 상태로 전환하고 `0xFFFFFFF0`에 있는 `software-initialization code`를 실행한다. 보호 모드 및 롱 모드와는 리얼 모드에서 필수적인 시스템 자료 구조는 `IDT` 밖에 없다. 기본적으로, 리얼 모드의 `IDT`는 0H 번지에 위치하고 있다. 이 위치는 `LIDT` 명령어를 통해서 변경이 가능하다. 그리고, 리얼 모드에서 인터럽트 및 익셉션 핸들러는 반드시 1MB 안쪽에 로드되어야 한다.
- 리얼 모드 레지스터
: 리얼 모드의 레지스터중에서 범용 목적의 레지스터는 2가지가 있다.
1" 데이터 레지스터 - AX, BX, CX, DX
2" 주소 레지스터 - SI, DI, SP, BP: 각 데이터 레지스터는 2개의 상위, 하위 레지스터로 나눠질 수 있다. 예를 들어, AX 레지스터는 리얼 모드에서만 존재한다. 이 레지스터는 16비트이다. 그런데, 코드상에서 AH, AL을 통해서 상위 8비트, 하위 8비트로 나눠서 사용할 수 있다.
: 주소 레지스터와 데이터 레지스터의 차이는 기본 연산의 차이에 있다. 데이터 레지스터는 `[]`을 사용하지 않아도, 해당 레지스터에 들어가 있는 값 자체는 주소가 아닌 데이터다. 그러나, 주소 레지스터는 값이 들어오면 그 값을 주소로 인식한다. 그래서 주소 레지스터에 []로 묶으면, 자신이 가리키고 있는 주소에 있는 값을 꺼낸다.
: 범용 목적 레지스터가 아닌, 특수 목적용 레지스터 또한 있다.
1" 세그먼트 레지스터 - CS, DS, ES, SS
2" 기타 레지스터 - IP, FLAGS: IP 레지스터는 명령어 레지스터라고 한다. 이 레지스터는 현재 프로그램이 실행되고 있는 명령어를 가리킨다. 흔히, 컴퓨터 구조책에서 `프로그램 카운터`라고 불리는 레지스터다. FLAGS 레지스터는 각 CPU 코어 상태를 저장하는 레지스터다. 대개 이 FLAGS 레지스터를 통해서 `비교 후 점프`를 하게 된다. 아래의 코드는 AX와 BX 데이터 레지스터가 서로 다른 값을 가지고 있으면 do_something 심볼로 점프하는 코드이다. 여기서 AX와 BX 값의 비교 결과를 FLAGS 레지스터에 저장한다. jne 명령어는 이 FLAGS 레지스터에 저장된 결과값을 체크하여 점프 여부를 결정한다.
cmp ax, bx jne do_something
: 리얼 모드에서 중요한 레지스터는 세그먼트 레지스터다. 왜냐면, 리얼 모드는 메모리 동작 방식이 리얼 모드의 전부라 봐도 무방하기 때문이다. 리얼 모드는 정말 특별한 기능이 없다. 특정 기능을 만들기 위해서는 BIOS 기능을 빌려야 하고, 그 외에는 메모리 관리 방식만 있을 뿐이다. 보호 기능은 존재하지 않는다.
: 리얼 모드에서 레지스터 사용 방법이 상당히 독특하다. 데이터 레지스터 및 주소 레지스터를 사용하면 암묵적으로 세그먼트 레지스터는 생략되서 처리가 된다. 예를 들어, `mov [si], ax` 명령어는 ax 레지스터에 있는 값을 DS:SI에 쓰게 된다. 대개 짝은 아래와 같이 이룬다.
1" DS:AX
2" SS:SP
3" SS:BP
4" ES:DI
5" DS:SI
6" CS:IP: 이 표기법은 개인적으로 굉장히 안좋다고 생각한다. 예를 들어, 스택의 시작을 0x7C00부터 설정하고 싶은 경우, 방법이 무수히 많다.
{ ; 1 mov ss, 0x07C0 mov bp, 0x0000 mov sp, 0x0000 ;2 mov ss, 0x0700 mov bp, 0x00C0 mov sp, 0x00C0 ;3 mov ss, 0x0000 mov bp, 0x07C0 mov sp, 0x07C0 ... }
: 세그먼트 레지스터의 값을 기준 주소로 잡고, 움직이지 않는다. 그리고 오프셋 값들은 세그먼트 레지스터의 기준 주소를 기준으로 `상대 주소*16 + 오프셋` 계산하기 때문에 경우의 수가 굉장히 많아진다.
: 그리고 CS 레지스터의 값을 직접적으로 바꾸는 것은 허용하지 않는다. jmp 명령을 통해서 CS 레지스터의 값은 자동으로 바뀐다.
- 주소 설정
: 위에서 언급했지만, 세그먼트 주소에만 `<< 4`가 적용된다. 오프셋에는 적용되는 것이 아니다. 세그먼트 레지스터인 DS, ES, SS, CS, FS, GS 에만 `<< 4`가 적용이 되고, 나머지 범용 레지스터에는 적용이 되지 않는다. 아래의 코드를 보자.
mov ax, 0x07C0 mov ds, ax mov bp, 0x07C0 mov sp, bp
: 위의 시나리오는 DS, BP, SP를 0x7C00으로 설정하려고 하는 내용이다. 그러나 실제로는 DS에만 0x7C00으로 설정된다. 왜냐면, DS는 세그먼트 레지스터 이므로, `<< 4`가 적용이 되었다. 그러나, BP와 SP는 세그먼트 레지스터가 아니다. 그래서 SP, BP는 0x7C00이 아닌 0x07C0으로 되버린다.
: 그리고 리얼 모드에서는 함수를 자주 사용하지는 않는다. 그렇다면 스택 설정은 의미가 없는 것일까? 그렇지 않다. BIOS INT(서비스)를 이용한다는 것 자체가 스택을 사용하고 있다는 증거다. 우리가 작성한 코드가 BIOS INT를 호출하면 코드 흐름이 넘어간다. 여기서 우리 코드로 복귀하기 위해서는 스택이 필요하다.
- 스택 세그먼트 레지스터
: 스택 세그먼트 레지스터(SS)도 다른 세그먼트 레지스터인 DS, ES와 같은 데이터 레지스터이다. 그런데 SS에는 어떤 주소를 설정해야 할까? SS는 스택 포인터 레지스터인 SP와의 관계를 통해 주소를 설정해야 한다. SP의 기본 동작은 설정된 주소를 기준으로 PUSH를 하면 아래로 내려가고, POP을 하면 위로 올라온다. 그리고 리얼 모드에서는 한 세그먼트의 최대 크기는 0xFFFF다. 만약, SS와 SP가 같다면 어떻게 할까? 스택을 사용하는 순간 에러가 발생할것이다. 왜냐면, SP가 SS보다 내려같기 때문이다. 이렇면 세그먼트 폴트 에러가 발생할 거라고 추측된다. 그래서 일반적으로는, SS는 0으로 넉넉하게 잡고 SP의 범위는 16비트 이므로, 0xFFFF 안쪽으로 범위를 설정한다.
- 리얼 모드 인터럽트(IVT)
: 리얼 모드에서 인터럽트 테이블의 이름은 `인터럽터 벡터 테이블(IVT)` 라고 불렀다. 총 사이즈는 0x400으로 1024B이고, 엔트리(인터럽트 핸들러 주소가 들어있는 공간) 하나당 4B 크기를 차치했다. 즉, 256개의 인터럽트를 처리할 수 있었다. 리얼 모드에서는 16비트만 존재했는데, 어떻게 인터럽트 주소가 4B 일 수가 있을까?
: 위 그림에서 0번지에 `DIVIDE ERROR` 엔트리의 구성을 보자. CS:IP 구조이다. 즉, `세그먼트 주소[2바이트]:IP 오프셋[2바이트]` 해서 총 4바이트가 되는 것이다.
: 80286 까지 IVT는 항시 0x0000 ~ 0x03FF에 위치에 있었다. 하드웨어 인터럽트들은 PIC를 통해서 IVT의 특정 엔트리들에 맵핑될 수 있다.
: 리얼 모드에서 IVT의 인터럽트들과 BIOS 인터럽트를 동일하게 보면 안된다. BIOS 인터럽트는 인터럽트라고 보다는 하드웨어 기능이라고 보는게 맞다. 즉, BIOS 인터럽트는 하드웨어 기능들을 사용하기 위해 운영 체제 및 소프트웨어 프로그램이 호출하는 함수와 같다. 그리고 BIOS 인터럽트는 하드웨어 인터럽트가 아닌, 소프트웨어 인터럽트다. IVT에는 소프트웨어 인터럽트 뿐만 아니라 하드웨어 인터럽트들이 들어가 있다. 그리고 IVT 맨 앞에 존재하는 20개 인터럽트는 CPU가 예약한 인터럽트를 의미한다. 즉, CPU가 특정 상황이 되었을 때, 발생시키는 인터럽트라는 것이다.
- A20
: x86의 A20은 21번째 주소 라인의 번호를 의미한다. IBM-AT(Intel 80286)가 등장하면서 주소 라인이 21번째 까지 늘어남에 따라 16MB 까지 접근이 가능하다. 초기 x86 PC인 8086, 8088, 80186은 모두 주소 라인이 20개였다(A0-A19). 즉, 메모리 영역의 최대 접근이 가능한 지점이 2^20 까지였다. 즉, 0x00000 ~ 0x10FFEF(0xFFFF0:0xFFFF) 까지다. 이 값은 1MB하고 64KB이다. 그런데 8086, 8088, 80186 아키텍처 기반으로 만들어진 프로그램들이 1MB가 넘어가는 주소에 접근하면 어떻게 될까? 8086, 8088, 80186 아키텍처들의 주소 라인은 20까지 이므로, 0xFFFF:FFFF(0x10FFEF)까지만 주소값이 유효하고 그 이상의 값은 `wrap around` 라고 해서, 마치 나머지 연산과 같은 연산을 통해 주소를 또 다른 주소로 맵핑하게 된다.
주소 % (0x100000) = 실제 사용되는 주소
: 예를 들어, 0x102FDD에 접근을 하면, 0x002FDD 주소에 접근하는 것과 같다. 왜? 8086, 8088, 80186 아키텍처들은 실제 물리 주소 라인이 20번째 까지밖에 없으므로, 1MB 이상에 접근을 할 수가 없다. 그래서 8086, 8088, 80186 아키텍처를 기반으로 만든 프로그램들은 모두 주소가 저렇게 맵핑된다는 것을 전제로 코딩을 해버렸다. 이게 문제가 되었다. 80286부터 16MB로 확장됨에 따라 물리 주소가 21번째 까지 늘어놨다. IBM-AT 이전 모델에서 개발했던 개발자들은 1MB 이상의 접근에 대해 `wrap around` 방식으로 코딩을 해버려서, 0x12FFDE에 접근하면 0x02FFDE에 접근을 해줘야 한다. 그런데, IBM-AT 부터는 0x12FFDE에 접근하면 그대로 0x12FFDE에 접근을 해버리는 것이다. 즉, 호환성의 문제가 발생했다. 그래서 인텔은 호환성을 위해 IBM-AT 이전 모델들이 제대로 동작할 수 있게 A20을 스위칭 할 수 있도록 설계했다.
: A20 라인은 기본적으로 비활성 상태로 부트업 되기 때문에, IBM-AT 이전에 만들어진 애플리케이션들은 A20 라인을 비활성화 상태로 유지시켰다. 그러나 IBM-AT 이후에 개발된 애플리케이션과 IBM-AT 이전 애플리케이션은 같이 공존할 수 있는 방안은 아니었다. 결국 이 솔루션은 좋지 못한 아키텍처 사례로 꼽히고 있다.
: A20은 비활성화 하면 여러 가지 문제가 있지만, 메모리를 제대로 사용하지 못하는 문제가 발생한다.
: 위에서 보다시피 A20 라인은 21번째 라인으로 1MB를 담당하고 있다. 80286은 원래라면 16MB를 지원해야 한다. 그러나, A20이 비활성화 되면, 홀 수 번째 메모리에는 접근이 안된다.
" 0 000X(0) 0000 0000 0000 0000 0000 : 0x0000_0000(0x0000_0000)
" 0 000X(0) FFFF FFFF FFFF FFFF FFFF : 0x000F_FFFF(0x00F_FFFF)
" 0 000X(1) 0000 0000 0000 0000 0000 : 0x0000_0000(0x10_0000)
" 0 000X(1) FFFF FFFF FFFF FFFF FFFF : 0x000F_FFFF(0x1F_FFFF)
" 0 001X(0) 0000 0000 0000 0000 0000 : 0x0020_0000(0x0020_0000)
" 0 001X(0) FFFF FFFF FFFF FFFF FFFF : 0x002F_FFFF(0x002F_FFFF)
" 0 002X(1) 0000 0000 0000 0000 0000 : 0x0200_0000(0x0300_0000)
" 0 002X(1) FFFF FFFF FFFF FFFF FFFF : 0x020F_FFFF(0x03FF_FFFF)
...
...: 위를 보면 0MB ~ 2MB가 모두 0MB ~ 1MB로 맵핑되고 있다. 2MB ~ 4MB도 모두 2MB ~ 3MB에 맵핑되고 있다. 즉, 1MB ~ 2MB, 3MB ~ 4MB, 5MB ~ 6MB ... 15MB ~ 16MB를 못쓰는 것이다. 즉, 16MB에서 반밖에 사용하지 못한다. A20은 80286이 초음 나왔을 때, 도입된 개념이다. 지금은 무조건 활성화를 하는 것이 맞다. 보호 모드에 진입하기 전 A20은 반드시 활성화를 해야 한다. 위에 이유가 바로 그것이다. 참고로, 현재 대부분의 컴퓨터에서는 부트업시에 자동으로 A20이 활성화된다. 그리고 A20을 비활성화해서 테스트를 해보고 싶었는데, 현재 QEMU나 BOCHS에서는 부트업시에 A20을 비활성화 상태로 하는 방법이 없는듯 하다.
: `Intel 64 프로세서`에서는 아예 `A20M#` 핀이 존재하지 않는다.
`A20M# pin` — On an IA-32 processor, the `A20M#` pin is typically provided for compatibility with the Intel 286 processor. Asserting this pin causes bit 20 of the physical address to be masked (forced to zero) for all external bus memory accesses. Processors supporting Intel Hyper-Threading Technology provide one `A20M# pin`, which affects the operation of both logical processors within the physical processor.
The functionality of `A20M#` is used primarily by older operating systems and not used by modern operating systems. On newer Intel 64 processors, `A20M#` may be absent.
- 참고 : [ 8.7.13.4 External Signal Compatibility ]- HMA
: `High Memory Area` 약자로 위의 A20과 함께 나온 용어다. 리얼 모드에서는 소프트웨어적으로 0x000000 ~ 0x10FFEF 까지 주소 지정이 가능하지만, 실제로는 주소라인이 A0 ~ A19 까지라 `wrap around` 라는 방식으로 주소를 내려서 맵핑한다고 했다. 즉, 0x10FFEF는 0x00FFEF에 대응한다. 여기서 실제 0x100000 ~ 0x10FFEF 까지의 영역을 HMA 이라고 한다. 전제가 있다. HMA 용어는 IBM AT 및 해당 모델과 호환되는 모델에서만 사용되는 용어다. 초창기 리얼 모드에서 저 영역에 접근할 수 없었기 때문에, 1MB이하를 LMA로 명하고, 1MB 이상을 HMA라고 명했었다.
: 그리고 A20 라인이 활성화된다고 해서 무조건 16MB 까지 접근이 가능한 것이 아니다. 리얼 모드에서 A20 라인을 활성화 시켜봤자 HMA(0x000000 ~ 0x10FFEF)까지밖에 사용하지 못한다. 왜냐면, 20번째 비트를 활성화하면 물리적으로 0x000000 ~ 0x1FFFFF 까지 접근이 가능해지기 때문이다. 그러나, 리얼 모드에서는 소프트웨어적으로 `(FFFF << 4) : FFFF` 방식때문에 최대 0x10FFEF 까지만 접근이 가능하다. 그러니, 그 이상에 접근하고 싶다면 얼른 보호 모드로 넘어가야 한다.
: MBR 부트 섹터를 사용하는 아키텍처에서는 wrap around를 이용해서 A20의 활성화 여부를 알 수 있다. 바로 0x7DFE와 0x107DFE[0xFFFF:0x7E0E]를 비교하는 것이다. A20이 활성화되어 있다면, 0x107DFE도 유효한 주소가 된다. 근데 아무것도 쓰지 않았기 때문에 0이 나와야 한다. 그리고, MBR 부트 섹터에서 0x7DFE는 부트 시그니처가 있는 위치다. 그래서 2개의 값이 같다면, wrap around가 일어났으니 A20이 비활성화 된 것이고, 다르면 A20이 활성화 되었다고 볼 수 있다.
'프로젝트 > 운영체제 만들기' 카테고리의 다른 글
[운영체제 만들기] Buddy Allcator (0) 2023.08.07 [메모리] Fixed-size blocks allocation (0) 2023.08.05 Protected mode (0) 2023.08.05 [운영체제 만들기] FAT (0) 2023.08.05 GDT (1) 2023.08.05