어제 급히 Application Level, 즉 User Level을 추가했다가 낭패를 크게 봤습니다. ㅡ_ㅡa...  첫번째는 아무 생각없이 Supervisor Mode로 맵핑되어있는 Paging 때문에... 두번째는 요상하게 설정된 User Level Descriptor 때문에... 세번째는 Segment Selector에서 잘못된 RPL Field 때문에... 네번째는 RFLAGS에 있는 IOPL 때문에... 다섯번째는 Interrupt 발생 시, User Level에서 Kernel Level로 변경되어 자동으로 발생하는 Stack Switching 때문이었습니다. 헥헥... 나열하니 이거 끝이 없군요. ;)

 원래의 목적은 Application Level에서 커널 함수를 사용할 때 Interrupt를 사용해서 Kernel 레벨로 바꾸고, Interrupt를 활성화 한 상태에서 커널 코드를 실행하는 것이었습니다. 이렇게 하면 인터럽트 불가로 동작하는 시간이 줄어들기 때문에 인터럽트에 좀 더 기민하게 반응할 수 있습니다. 만약 윈도우 전체를 다시 그리는 커널 코드를 실행한다고 가정하면 그 시간이 얼마나 걸릴지... 그리고 그동안 인터럽트 처리가 안되면 어떻게 될지는 금방 상상이 되실겁니다. ^^;;;;;

 그런데 이게 Stack Switching이 발생하다보니, Kernel Level Stack을 Application들 끼리 공유하게 되더군요. 32bit 같은 경우는 Task 별로 TSS 영역이 존재해서 별도의 Kernel Stack을 할당해 줄 수 있었지만, 64Bit에서는 TSS가 Task에 종속적으로 존재하는 것이 아닙니다. 거의 Global하게 공통적으로 사용한다고 해도 될만큼 말입니다. ㅠㅠ

 하지만 커널 코드 수행 시, 인터럽트를 활성화하려면 태스크 별로 커널 스택을 별도로 가져야 하는데... 어떻게 할까 고민하다가 결국 Interrupt Service Routine에서 System Call용은 Kernel Stack에서 Application Stack으로 돌아가도록 했습니다. Stack Switching을 한번 더 한다는 이야기지요. 이렇게 하면 System Call 호출로 인해 Kernel Mode에 진입해 있지만 Stack은 Application의 Stack을 사용하기 때문에 커널 함수를 수행하는 도중 Task Switching이 발생해서 다른 Application이 Kernel로 진입해도 문제 없습니다.

 일단 대충 코드를 구현했는데... 오늘은 일찍 자는 날이라서 내일 테스트 해야겠군요. 별것 아닌 것 같은데... 해보니 별거(?)였다는... ㅠㅠ

 아직 내공이 많이 부족한가 봅니다. 크윽... 저의 애마(?) 마제스터치 키보드와 함께 계속 달려야겠군요.
 다들 그럼 좋은 밤 되세요. ;)


 드디어 응용프로그램을 외부에서 전송하여 실행하는데 성공했습니다. 외부 즉 윈도우와 가상 머신은 Serial Port로 연결했습니다. QEMU의 옵션 중에 가상 Serial Port를 열어주는 옵션이 있는데, 그것을 이용해서 가상머신에 Serial을 만들고 제가 만든 OS에서 그 Serial을 사용함으로써 통신에 성공했습니다. @0@)/~

 QEMU에서 TCP/IP로 Serial Port를 생성할 수 있는데, 아주 편하더군요. 덕분에 간단한 윈도우 소켓 프로그램으로 파일을 OS에 넘겨줄 수 있게 됬습니다. 아래는 제가 사용하는 QEMU 옵션인데, 맨 뒷줄 -serial 부터가 TCP/IP로 Serial Port를 생성해주는 옵션입니다. ^^

qemu-system-x86_64.exe -L . -fda d:/64bitos/disk.img -hda 64BitOS.img -m 32 -localtime -M pc -smp 2 -boot a -serial tcp::4444,server,nowait


 아래는 QEMU에서 돌고 있는 제 OS에 파일 매니져가 응용프로그램 바이너리(elf64 파일 포맷)을 넘겨주는 화면입니다. OS에서 Serial로 열심히 데이터를 다운받아서 "."을  찍고있는 걸 볼 수 있습니다. ^^;;;;
사용자 삽입 이미지

 다운로드가 끝나면 OS에서 ELF64 파일 포맷인가 확인한 뒤, Relocation을 수행합니다. 그후 메모리를 할당받아 Relocation된 실행 파일을 복사하고 프로세스를 생성합니다. 원래는 ELF64 파일 포맷을 가공하려고 했습니다만, Relocation을 분석하는 과정에서 크게 복잡한 부분이 없어서 그냥 쓰기로 했습니다. ^^;;;

 아래는 여러 개의 Application을 OS에 업로드하여 실행한 화면입니다. 각자 자신의 Task ID를 표시하게 되어있습니다.
사용자 삽입 이미지

 어휴... 진짜 이것 저것 많이 했군요. 이제 내일은 GUI 쪽 System Call을 추가하고 좀 복잡한 Application을 로딩해서 정상적으로 동작하나 확인해 봐야겠습니다. >ㅁ<)-b

 이크~ 벌써 시간이 이렇게 됬군요. 그만 자야겠습니다. ㅎㅎ
 그럼 다들 좋은 밤 되세요 ;)


ELF64 Relocation 처리(1) - ELF64 구조

원문 : http://kkamagui.springnote.com/pages/1709220

 

들어가기 전에...

 

0.시작하면서...

 이 문서는 ELF64 파일을 Relocation하는 데 필요한 정보가 기술된 문서입니다. 이 문서는 2부분으로 나누어져있으며, 첫번째 문서는 ELF64 파일 포맷에 대한 내용을 다루고 있고, 두번째 문서는 예제 코드를 통해 Relocation을 실제로 수행하는 내용을 담고 있습니다.

 

1.데이터 타입(Data Type)

 ELF64의 경우 데이터 타입은 아래와 같습니다. ELF32와 다른 점은 8Byte 크기의 데이터 타입이 추가되었다는 것입니다.

DataType.PNG

 

 데이터 타입은 ELF의 각 파트를 설명할 때 값의 표현범위를 나타내기위해 사용할 것이므로, 한번쯤 훓어보면 됩니다. ^^;;;

 

2.ELF 기본 구조(ELF Basic Structure)

 ELF64 또는 ELF32는 크게 아래와 같이 두가지 구조를 가집니다( 이 글에서 다룰 구조는 Relocation File 형태의 구조입니다).

elf구조.PNG

 

 Relocation File은 일반적으로 Relocation에 대한 정보를 포함하고 있으며, 각 Section 마다 해당 Section의 Relocation 정보를 담당하는 Section이 별도로 존재합니다. 따라서 특수한 Section만 골라서 쉽게 Relocation을 수행할 수 있습니다.

 

3.File Header

 File Header는 ELF 파일 포맷의 가장 앞쪽에 위치하는 정보로 ELF 파일 포맷에 대한 각종 정보를 포함하고 있습니다. File Header에 포함된 데이터는 아래와 같습니다.

  1. typedef struct
    {
        unsigned char e_ident[16]; /* ELF identification */
        Elf64_Half e_type; /* Object file type */
        Elf64_Half e_machine; /* Machine type */
        Elf64_Word e_version; /* Object file version */
        Elf64_Addr e_entry; /* Entry point address */
        Elf64_Off e_phoff; /* Program header offset */
        Elf64_Off e_shoff; /* Section header offset */
        Elf64_Word e_flags; /* Processor-specific flags */
        Elf64_Half e_ehsize; /* ELF header size */
        Elf64_Half e_phentsize; /* Size of program header entry */
        Elf64_Half e_phnum; /* Number of program header entries */
        Elf64_Half e_shentsize; /* Size of section header entry */
        Elf64_Half e_shnum; /* Number of section header entries */
        Elf64_Half e_shstrndx; /* Section name string table index */
    } Elf64_Ehdr;

 

 여러 필드가 있지만, 몇가지 특징적인 부분과 Relocation에 관련된 부분만 살펴보겠습니다.

  • e_ident[ 0 ~ 3 ] : 0x7F, 'E', 'L', 'F' 가 각각 들어가있음. ELF  파일 포맷임을 인식하기위함
  • e_ident[ 4 ] : 1이면 ELF32, 2면 ELF64를 의미함
  • e_ident[ 5 ] : 1이면 Little Endian, 2이면 Big Endian을 의미
  • e_type : 1이면 Relocatable Object File, 2이면 Execute File을 의미
  • e_shoff : Section Header Table이 존재하는 곳의 파일 Offset을 의미함
  • e_ehsize : ELF Header의 크기
  • e_shnum : Section Header Table에 포함된 Section의 개수
  • e_shstrndx : Section Header에서 Section Name String Table의 Index

 

 우리는 코드가 존재하는 Section(.text)을 돌면서 Relocation을 수행해야하므로, 위에서 언급한 것 중에 파란색으로 마킹한 부분만 유심히 보면 됩니다.

 

4.Section Header Entries

  1. typedef struct
    {
        Elf64_Word sh_name; /* Section name */
        Elf64_Word sh_type; /* Section type */
        Elf64_Xword sh_flags; /* Section attributes */
        Elf64_Addr sh_addr; /* Virtual address in memory */
        Elf64_Off sh_offset; /* Offset in file */
        Elf64_Xword sh_size; /* Size of section */
        Elf64_Word sh_link; /* Link to other section */
        Elf64_Word sh_info; /* Miscellaneous information */
        Elf64_Xword sh_addralign; /* Address alignment boundary */
        Elf64_Xword sh_entsize; /* Size of entries, if section has table */
    } Elf64_Shdr;

 역시나 많은 필드가 있지만, 특징적인 몇가지와 Relocation에 필요한 부분 위주로 살펴보겠습니다. ^^;;; (자세한 내용이 궁금하신분을은 ELF64  파일 포맷에 대한 Spec 문서를 참조하세요).

  • sh_name : Section Name Table 내에서 Section Name이 존재하는 Offset
  • sh_type : Section의 타입. 자세한 타입은 아래 테이블 참조
  • sh_flag : Section의 속성. 자세한 속성값은 아래 테이블 참조
  • sh_addr : Section이 메모리에 로드될 때, Base 주소를 의미함
  • sh_offset : 파일 내에서 Section이 존재하는 Offset. 위의 sh_addr의 값과 같을 수도 있으며, 다를 수도 있음
  • sh_size : Section의 실제 크기. Byte 수로 표현
  • sh_link : 현재 Section과 연결된 Section의 정보. sh_type에 따라서 이 필드는 다양한 의미를 가짐. 자세한 의미는 아래 테이블 참조
  • sh_info : 현재 Section에 대한 정보. 위의 sh_link와 마찬가지로 sh_type에 따라 다양한 의미를 가짐. 자세한 의미는 아래 테이블 참조
  • sh_entsize : Section에 포함되는 데이터, 즉 여러 개의 엔트리가 동일한 크기를 가진다면 0이 아닌 값으로 설정. 그렇지 않다면 0으로 설정. Symbol table 같은 경우 동일한 크기의 엔트리가 반복되므로 이 값이 0x18을 가짐

 

4.1 Section Type, sh_type

 sh_type은 Section의 타입을 단적으로 보여주는 값입니다. Section의 목적에 따라 아래와 같이 다양한 값을 가질수 있습니다.

SectionType.PNG

 

 일반적으로 코드 및 데이터, 그리고 초기화되지 않은 데이터 Section을 의미하는 .text, .data, .bss의 경우는 SHT_PROGBITS를 가집니다. 즉 Program이 정의한 데이터를 삽입하는 영역이라는 의미입니다.

 하지만 Section의 Relocation 정보를 담고있는 Section의 경우는 SHT_RELA를 가지며, Symbol 정보를 포함하는 Symbol Table Section의 경우는 SHT_SYMBTAB의 값을 가집니다.

 이 타입에 따라서 sh_link 및 sh_info의 의미가 달라지는데, SHT_PROGBITS가 설정된 Section의 경우는 특별한 의미를 지니지 않습니다. 하지만 SHT_SYMTAB이나 SHT_RELA의 경우는 다릅니다. 다름 챕터를 같이 한번 볼까요?

 

4.2 sh_link 및 sh_info의 의미

 sh_link 및 sh_info는 sh_type 값에 따라 아래와 같은 의미를 가집니다.

sh_link.PNG

sh_info.PNG

 

 그렇다면 우리가 Relocation을 수행할 때는 어떤 정보를 봐야 할까요?

 일단 SHT_RELA 타입을 가지는 Section을 검색해서 찾은 후, 해당 Section의 sh_link 필드를 이용해서 Symbol Table Section을 찾습니다. 그리고 SHT_RELA 타입을 가지는 Section의 sh_info 필드를 이용하면 Relocation을 적용해야하는 Section을 찾습니다. 그리고 SHT_RELA 타입을 가지는 Section의 Relocation 정보를 가지고 Relocation을 수행하면 됩니다.

 그런데 Relocation 정보를 가지는 Section의 Relocation 정보와 sh_info가 가리키는 Relocation을 적용해야할 Section 정보만 알면 되지, 왜 Symbol 정보를 봐야할까요? 눈치가 빠른 분들은 이미 감을 잡으셨을 겁니다. ^^ 그것은 바로 Relocation Section에 포함되는 Relocation 엔트리 정보는 Symbol Table에 있는 Symbol 엔트리를 참고해서 실질적으로 얼마를 더하고 뺄지가 정해지기 때문입니다. 자세한 내용은 뒤쪽 챕터에서 설명하도록 하고, 지금은 SHT_RELA 타입을 가지는 Section을 찾고 그 Section의 sh_link 및 sh_info 정보를 이용한다는 것만 알고 넘어가겠습니다. ^^

 

4.3 Section Attribute, sh_flags

 속성 값은 아래와 같이 쓰기가 가능한지, 메모리를 할당받아서 실제로 로딩해야하는 Section인지, 코드가 포함되어있어서 실행가능한지에 대한 내용을 포함합니다. 우리가 Relocation 해야하는 Section(.text) 같은 경우,  .text Section은 메모리에 로딩되어 실행되야하는 Section이기 때문에 SHF_ALLOC 및 SHF_EXECINSTR 값을 설정합니다.  sh_flag는 Relocation 수행 시에는 크게 중요한 필드가 아니므로, 이런 것이 있다는 것만 알고 넘어가겠습니다. ^^;;;

sh_flag.PNG

 

4.4 Special Section

 Section의 속성은 위에서 언급한 각가지 필드를 이용해서 설정할 수 있지만, 관습적으로 사용하는 Section 이름들이 있습니다. 예를 들면 실행파일 코드가 있는 Section은 .text, 데이터가 존재하는 Section은 .data, 초기화 되지 않은 데이터는 .bss와 같이 말이지요. ^^

 아래는 Elf 파일 포멧에서 정의해놓은 Special Section들의 이름과 Type, 그리고 Attribute를 나타낸 테이블입니다.

Special_Section.PNG

 주의해서 볼 점은 .relname과 .relaname 라는 이름을 가진 Section이 있다는 겁니다. Section 이름에서 알 수 있듯이 name이 의미하는 Section의 Relocation 정보를 포함하고 있습니다. Section Type 뿐만 아니라 이름에서까지 Relocation Section 임을 알 수 있게 해놓다니... 여차하면 Section Name으로도 Relocation Section을 찾을 수 있을 것 같습니다(물론 권장하지는 않아요~!!! ^^;;;).

 

5.Symbol Table

 Symbol Table은 코드에서 사용된 각종 변수명 및 함수명, 그리고 Section 이름까지 많은 정보를 포함할 수 있습니다. Symbol Table은 아래와 같은 구조체의 배열로 구성되어 있습니다.

Symbol_Table.PNG

 

 역시나 각 필드의 의미를 살펴봐야겠지요? ^^ Symbol Table은 특별히 모든 필드를 살펴보겠습니다.

  • st_name : String Table Section 내에서 Section 이름이 존재하는 Offset. String Table은 SHT_SYMTAB의 경우 sh_link 값이 가리키는 Section 임
  • st_info : 상위 4bit는 Symbol Binding 정보를 나타내고 하위 4bit는 Symbol Type을 나타냄. Symbol Binding 및 Symbol Type에 대한 정보는 아래 참조
  • sh_other : 추후 사용을 위해 예약된 영역. 반드시 0으로 설정
  • st_shndx : Symbol이 정의된 Section의 Index. 함수이름 같은 경우 .text Section의 Index를 가리킬 것이고, 변수 이름 같은 경우는 .data Section의 Index를 가리킬 것임
  • st_value : Relocatable 파일의 경우 Section 내에서 Symbol이 정의된 Offset을 의미. Executable 및 Shared Object 파일일 경우 Symbol이 정의된 Virtual Address를 의미
  • st_size : Symbol의 크기를 의미. 함수의 경우 함수의 크기를 나타내며, 변수의 경우 변수가 차지하는 메모리 공간의 크기를 나타냄

 

 Symbol Table의 경우 각 필드의 의미를 정확하게 알고 있어야 Relocation을 수행할 수 있기 때문에, 모든 필드를 다 살펴봤습니다. 이제 Symbol Table 중에 남은 것은 st_info의 상위 4bit 및 하위 4bit에 대한 내용입니다만, 그리 중요한 부분은 아니니 슬쩍 보고 넘어가겠습니다. ^^;;; Relocation을 수행하는데 Symbol Binding(상위 4bit)과 Symbol Type(하위 4bit)은 그리 중요하지 않기 때문이지요. 일단 Symbol이 있으면 코드 내에 참조하는 곳이 있을테니 Relocation 준비를 (거의 무조건) 해야합니다. 다만 st_shndx의 값이 SHN_ABSSHN_COMMON 일 때는 좀 생각을 해야합니다.

 SHN_ABS의 의미는 Value의 값이 Relocation의 영향을 받지않는 절대값이라는 의미이므로 이 Symbol은 Relocation을 하면 안됩니다. SHN_COMMON의 의미는 아직 메모리 공간이 할당되지 않은 Section이라는 의미이므로, 아래에서 언급할 일반적인 방식으로는 Relocation을 수행할 수 없습니다. 만약 하려면 SHN_COMMON으로 설정된 Symbol을 다른 Section에 공간을 할당해주고 Relocation 해야하는데, 이 작업 자체가 쉽지 않고 임의로 할당했다가는 다른 Section의 데이터를 엉망으로 만들 위험이 있습니다.

 따라서 수동으로 Relocation하려면 SHN_COMMON을 Section Index로 갖는 Symbol을 줄일 필요가 있는데, 이것은 배열이나 구조체를 "0"으로 초기화 함으로써 해결할 수 있습니다. ^^;;;; 구조체나 배열을 0으로 초기화하면 .bss나 .data 쪽으로 들어가기 때문이지요. 초기화하지 않으면 대용량의 배열이나 구조체 같은 경우는 SHN_COMMON으로 들어갈 확률이 높습니다.

 

 Symbol Binding(st_info의 상위 4bit) 정보는 아래와 같은 값들이 있습니다(하지만 패스~!!).

Symbol_Bindings.PNG

 Global Symbol인지 Local Symbol인지를 나타내는 값이 정의되어있군요. STB_LOCAL이 Object 파일 내에서만 보인다는 것을 보니 static으로 사용한 변수명쯤 되는 것 같습니다.

 

 Symbol types(st_info의 하위 4bit) 정보는 아래와 같은 값들이 있습니다(역시 중요하지 않으므로 패스~!!).

Symbol_Types.PNG

 변수명 같은 경우는 STT_OBJECT로 표시되고, 함수명 같은 경우는 STT_FUNC로 표시됨을 추측할 수 있습니다. Section 이름도 Symbol Table에 표시되는데, 이런 경우는 STT_SECTION 타입으로 표시되더군요. ^^;;;

 

6.Relocations

 드디어 Relocation 과정에서 핵심이 되는 Section인 Relocation Section까지 넘어왔습니다. Relocation Section은 Relocation에 대한 정보를 가지고 있는  Elf64_Rel 및 Elf64_Rela 구조체의 배열로 이루어져 있으며, 각각은 아래 그림과 같이 구성되어 있습니다.

Relocation_Entry.PNG

 

 Relocation Entry는 아주 중요하므로 전 필드를 다 살펴보겠습니다.

  • r_offset : Relocation 파일인 경우, 이 값은 Relocation을 수행해야하는 대상 Section 내에서 Relocation이 적용되어야하는 시작 위치를 나타냄. Executable 파일 또는 Shared Object 파일인 경우, Virtual Address를 의미함
  • r_info : 상위 32bit는 Symbol Table 내의 Symbol Offset을 나타냄. 하위 32bit는 Relocation Type을 나타냄. Relocation Type에 대한 정보는 아래 참조
  • r_addend : Relocation이 적용될때 더해줘야하는 값

 

 Relocation Entry의 필드중에 r_info를 보시면, Symbol Table의 Symbol 값을 이용하는 것을 볼 수 있습니다. 그럼 Symbol 값과 r_addend 값을 더해서 r_offset이 가리키는 영역에다가 덮어쓰면 될까요? 정답은 "아니요" 입니다. ^^;;;;

 Relocation도 단순히 Symbol 값을 대입하는 것부터, r_addend를 더하고 r_offset 값을 빼는 것같은 다양한 방식으로 계산을 수행합니다. 그럼 어떠한 연산을 수행해야 하는지 어떻게 알 수 있을까요? 그렇습니다. 바로 r_info의 하위 32bit 값인 Relocation Type을 보면 됩니다.

 

 Relocation Type은 아래와 같이 정의되어있습니다. ^^

Relocation_Types.PNG

 표의 가장 오른쪽에 있는 Calculation 항목에 있는 각 항목들은 아래와 같은 의미를 가지고 있습니다.

  • S : Symbol Table에 정의된 Symbol의 값
  • A : Relocation Entry의 r_addend 필드 값
  • P : Relocation Entry의 r_offset 필드 값
  • G : Global Offset Table 안에 Relocation Symbol이 위치한 Offset
  • GOT : Global Offset Table의 Offset
  • B : 프로그램 실행 시에 Shared Object가 메모리에 로드된 Base Address
  • L : Procedure Linkage Talbe 안에 Relocation Symbol이 위치한 Offset

 

 위 항목 중에 G, GOT, B 그리고 L 항목은 공유 라이브러리(Shared Library)나 특수한 옵션(-fPic 등)을 사용하지 않으면 보기 힘든 항목이므로, 일반적으로 많이 사용되는 S, A, P를 위주로 보시면 됩니다. 위 테이블에 나와있는 계산 과정을 가만히 보면 지극히 간단한 규칙을 가지는 것을 알 수 있습니다. 대부분 계산 방법이 Symbol 값에 r_addend 값을 더하고 r_offset의 값을 빼서 Field 항목에 나와있는 크기만큼을 덮어쓰는 것으로 끝입니다. 다음 문서의 Relocation 수행 예제를 보시면 너무 간단해서 허무해 하실지도 모르겠군요. ^^;;;;

 다음 챕터로 넘어가기 전에 한마디 덧붙이자면, 정의를 보시면 비슷한 방식으로 계산하지만 Relocation이 수행되어야할 크기에 따라 다르게 분류되어 있습니다.. 이것은 x86 어셈블리어 코드가 다양한 크기의 메모리 및 레지스터를 가지고 연산할 수 있기 때문이며, 그로인해 Relocation을 수행해야하는 영역의 크기도 다양해졌기 때문입니다. 이부분은 값을 실제로 삽입할때 잘라서 넣어주면 처리할 수 있습니다.

 

7.마치면서...

 지금까지 Relocation을 수행하기위해 알아야할 ELF64 파일 포맷에 대한 몇가지 내용들을 봤습니다. Relocation을 수행하기 위해서 위 내용들을 달달 외울 필요는 없습니다. 그냥 "알고" 있으면 되는 것지요. 지금까지 표와 말로 많은 설명을 했습니다만, 아직 감이 잘 안오실 겁니다. ^^;;;; 프로그래머에게는 백마디 말보다 한줄의 코드가 더 좋은 법~!!!! 다음 문서(~!!!)에서는 예제 코드를 작성해서 Relocation을 실제로 해보겠습니다. >ㅁ<)-b

 

 

이 글은 스프링노트에서 작성되었습니다.

 오늘은 즐거운(??) 예비군 훈련날이라 야근없이 바로 집에왔습니다. 그리고 밥 한그릇 빨리 뚝딱하고 ELF64 파일 포맷에 대한 문서를 작성하기 시작했습니다. 사실 시작은 어제부터 했지만, 본격적으로 시작한 건 오늘부터라서... ^^;;;;

 문서는 Relocation 방법에 대해서 초점이 맞춰져 있지만, 기반 설명을 위해 앞쪽에 ELF64 파일 포멧에 대한 내용을 넣는게 좋겠다는 생각이 들더군요. 그래서 스펙 문서를 긁어 붙이면서 설명 첨부하는 식으로 간단히 넘어갔습니다.

 그런데 그 동안 테스트 프로그램으로 ELF64를 분석할때는 별 문제 없는 것 같던 Relocation Type이라는 값이, 문서를 작성하려고 보니까 뭔가 이상한겁니다. ㅠㅠ 제가 보고 있는 ELF64 문서의 버전이 낮아서 그런지 타입에 대한 설명을 건너뛰었더군요. 그래서 ELF32에 문서쪽을 찾아보니  32bit 기준으로 설정되어 있어서, 뭔가 일치하지 않는 부분이 있었습니다. 한가지 예를 들면 objdump.exe로 Relocation Entry를 읽으면 Relocation Type이 0x0B인 것이 나오는데, ELF32에는 0x0A까지만 정의되어 있더군요. ㅡ_ㅡ;;;;;;

 도저히 이건 아니다 싶어서 다시 구글 검색을 시작했습니다. 결국 AMD쪽 사이트에서 문서를 구했습니다. 생각보다 자료를 찾기가 쉽지 않네요. 이런쪽을 파보는 사람이 별로 없어서 그런가... ㅡ_ㅡa... 앞으로 테스트 프로그램을 짤때도 정리를 꼭 해야겠습니다. 정리를 안하니 그냥 아는 척(??) 하고 넘어가게 되네요. ㅠㅠ



 이런 이유로... 오늘 하루종일 결국 삽질만 하고 정녕 Relocation 처리는 완료하지 못했습니다. 크윽... 내일은 금요일이니 좀 신경써서 마무리를 하고 64Bit Test OS에 올려봐야겠네요. ;)

 그럼 다들 좋은 밤 되시길~ ;)

ps) 어디 고정 폭 폰트 중에서 저작권 없는 깜찍한 폰트 없나요? ㅠㅠ

정 안되면 폰트 홍보해주겠다고 하고 저작권료를 안주는 방법을 고민해봐야할지도... ㅠㅠ

살려주세요~!!! ㅠㅠ



 와우~ 거의 일주일 동안 작업해서 GUI 프로토타입을 완성했습니다. ㅠㅠ 삽질한걸 생각하면 눈물이 앞을... ㅠㅠ 흑흑... 오늘 윈도우 매니져가 개별 윈도우 메시지 큐에 메시지를 전달하는 기능을 추가하면서, 프로토타입을 겨우 완성했습니다.
 
 항상 추가된 기능은 어떻게라도 테스트를 해야하는 법~!! 테스트는 마우스가 윈도우 위를 지날때 윈도우 매니져가 마우스 이벤트를 윈도우 큐에 넣고,  윈도우는 큐에 데이터가 있는지 체크하는 방식으로 했습니다. 만약 마우스 데이터가 큐에 있다면 윈도우 가운데 있는 사각형 색깔을 바꾸도록 말이지요. ^^

 궁금하신 분들은 아래 파일을 다운 받으셔서 압축을 푸신 다음 "qemu64MyOs.bat" 파일을 실행하시면 GUI 프로토타입을 테스트 할 수 있습니다. ^^ 마우스가 클릭되거나 윈도우 위를 지나면 사각형이 반짝이는 걸 보실 수 있습니다.
아래는 스크린샷입니다. ㅎㅎ
사용자 삽입 이미지


 마지막으로... QEMU에서 마우스를 빠져나오게 하려면 Ctrl 과 Alt를 몇번 동시에 누르면 됩니다. ^^;;; 한번에 빠져나오는 경우도 있지만 몇번 눌러야 빠져나오는 경우도 있으니 당황하지 마시고 몇번 더 시도해보세요 ;)

 아아~ 이제 그만 자야겠습니다. 이번 주는 대충 여기까지~!!!
 그럼 좋은 밤 되세요 ^^)/~

 어제 오늘 직선을 쉽게, 그리고 제대로 그려볼려고 머리를 굴리고 있습니다. 그런데 생각보다 깔끔하게 나오지 않네요. ^^;;;; 구불구불해지거나 아니면 아예 이상해지거나... ㅠㅠ

 원래는 Bresenham 알고리즘을 사용했었는데, 좀 복잡해보여서 더 간단하게 그릴 수 있는 방법을 모색했습니다만.... ㅡ_ㅡa... 역시나 실패... 괜히 그런 알고리즘이 나온게 아니군요 ㅎㅎ

 아아... 그냥 GG 치고 Bresenham으로 가야겠습니다. ㅠㅠ 우측 상단에 X 버튼 그릴려고 별짓을 다 했군요. ㅠㅠ)-b
사용자 삽입 이미지

 직선 만세 ㅠㅠ)-b


ps) 이게 무슨 삽질인지... 다음 부터는 그냥 있는 거 써야겠습니다. ㅠㅠ
 어제 오늘 열심히 삽질해서 GUI라고 부를 수 있는 기능을 넣었습니다. 아래는 데모 화면입니다. ㅎㅎ 고전인 Hello World를 한번 넣어봤습니다. @0@)-b
사용자 삽입 이미지
 
 일단 만들긴 만들었는데... 이게 알고리즘이 후지다보니 굉장히 느리군요. ㅠㅠ 특히 큰 윈도우가 겹쳐져서 이동하는 경우, Draw를 상당히 많이 해야되서 거의 죽음입니다. ㅠㅠ 이거 무슨 수를 내던지 해야겠군요. 나름대로 적은 부분만 그릴려고 노력했는데... 아직은 역.부.족. ㅎㅎ

 어휴... 벌써 4시... ㅠㅠ 비빔면이나 하나 먹고 자야겠습니다. 내일은 꼭 어떻게든 속도를... ㅠㅠ)-b


 요 며칠 계속 알고리즘 고민한다고 거의 코딩 안하고 그림만 계속 그렸습니다. 자꾸 마음 속에 메모리를 적게 사용하고픈 하는 욕망이 타올라서 어떻게 하면 메모리 사용량을 줄일까만 고민했습니다.ㅠㅠ 메모리를 적게 사용하려면 결국 개별 윈도우가 임시 버퍼를 가지지 않고 프레임 버퍼에 바로 그려줘야 하는데, 이때 발생하는 깜빡임과 화면 업데이트 속도때문에 결정을 못하겠더군요. ㅠㅠ

 메모리를 적게 쓰려다 속도를 포기하는 상황까지 오게되자, 결국 개별 윈도우가 임시 버퍼를 가지는 방식으로 확!! 기울었습니다. ㅡ_ㅡa... 어플리케이션 몇개 실행하지 않을 건데 굳이 메모리를 아껴야하는 생각도 들고, 전체 화면으로 생성하는 어플리케이션이 몇 안되면 크게 문제가 안될 것 같은... ^^;;;; 어차피 간단한 GUI를 구현하고 잘 동작함을 보여주는 것이 목적이라서, 간단히 구현할 수 있는 방향으로 가는 것이 옳은 것 같습니다. ㅎㅎ

 아래는 간단히 구현해 본 윈도우 겹침 테스트 화면입니다. 총 3가지 색의 윈도우가 있고 마우스 포인터(파란색 작은 사각형)로 클릭하면 윈도우를 움직일 수 있도록 했습니다. memcpy, memset 함수가 대충 구현되어있을 때는 무시무시하게 느리던데(그리는게 다보여요 ㅠㅠ), 약간 튜닝해주니 광속으로 움직이는군요. @0@)-b 역시 64bit 짱~!!! 1024 * 768 화면 전체를 다시 그리는 최악의 경우에도 볼만하네요. ;)
사용자 삽입 이미지

 이제 윈도우 메니져와 응용 프로그램 사이에 Lock 처리만 해주고 API만 구현하면 대충 될 것 같습니다. 아휴~ 이거 원 정신이 하나도 없네요. ㅠㅠ 왜 이렇게 할 게 많은지... 어서 빨리 마무리해야 할텐데 말이죠. ;) 이렇게 나가다간 끝이 없겠습니다. 그런데 초보자용으로 시작한건데... OS 코드가 점점 복잡해지는군요. ㅠㅠ 어휴~ 이일을 어째... ㅠㅠ

 끄응~ 밤이 깊었으니 나머지 구현은 내일해야겠습니다.
 그럼 다들 좋은 밤 되시길 ;)


 
아유... 이것 참... 머리가 아픕니다. ㅠㅠ GUI Architecture를 구상하고있는데 어떤 것이 더 간단하고 효과적일지 고민하고 있습니다. GUI를 구현하는 방법에는 여러가지가 있겠지만 크게 아래와 같이 세가지를 생각하고 있습니다.

  1. 윈도우를 그릴때 Application에서 직접 Graphic Buffer에 접근해서 그린다.
  2. Window Manager가 Application의 Draw 함수를 호출해서 Serialize 해서 그린다. 즉 Kernel이 개별 Application의 함수를 호출한다.
  3. Application은 자신의 윈도우를 위한 임시 Graphic Buffer를 가지고 있고, 다 그리고 난 후 Graphic Buffer로 바로 복사해준다.

 1번처럼 구현하는 것이 가장 효과적일 것 같은데, 가장 마음에 걸리는 부분이 윈도우 겹침 처리더군요. 각 Application이 그리는 시간이 틀릴텐데, 마구잡이로 그리다가는 겹치는 부분에 대한 처리가 정상적으로 될 것 같지 않더군요. 아우... ㅠㅠ

 일단 좀 고민을 더 해봐야겠습니다. ㅠㅠ 어흑... 정말 산 넘어 산이군요.

ps) 혹시 Linux나 Windows의 GUI 구현에 대한 문서가 나온 게 있나요? 혹시 보신분 있으시면 덧글로 제보 부탁드립니다. ㅠㅠ


 요즘 야근이 너무 잦아서 생각보다 진도를 빨리 못나가고 있습니다. ㅎㅎ 그래서 밤잠을 줄여가며 코딩하고 테스트하고 있는데, 아흙 죽을 것 같군요. 내일도 회사에 출근해야해서 더 죽겠다는... ㅠㅠ

  현재 OS 진행 상황은 그래픽 모드로 전환까지는 완료한 상태입니다. 이제 남은 것은 Mouse를 사용하는 간단한 GUI를 만드는 것 정도인데... ^^;;; GUI를 만드는 것은 둘째치고 지금 Mouse 인터럽트 처리 때문에 고전하고 있습니다. ㅠㅠ

 아시는 분도 계시겠지만, PS/2의 Mouse 같은 경우 Keyboard의 데이터 포트와 같이 사용합니다. 즉 데이터 포트를 읽었을때 이것이 마우스 데이터가 될수도 있고 키보드 데이터가 될수도 있는 것이지요. ;) 자~ 그럼 어떻게 하면 가장 빨리 구분할 수 있을까요? 권장하는 방법은 인터럽트를 사용해서 인터럽트 서비스 루틴에서 바로 읽는 겁니다. 인터럽트는 데이터를 포트에 밀어넣은 즉시 발생하므로 정확하게 구분해서 읽을 수 있습니다.

 하지만 멀티 프로세서 환경에서 인터럽트를 사용하니, 기존에 없던 문제가 생기더군요. ㅡ_ㅡa... 인터럽트를 각 프로세서에 브로드캐스팅(Broad Casting)하므로, 프로세서 #0 이 마우스 인터럽트를 처리하는 동안 프로세서 #1은 키보드 인터럽트를 처리할 경우가 생기는 겁니다. @0@)-b

 위와 같은 상황에서 싱글 프로세서는 인터럽트 서비스 루틴에서 인터럽트가 불가로 설정하고, 순차적으로 데이터를 읽는 방법으로 처리할 수 있습니다. 하지만 멀티 프로세서의 경우는 동일한 포트에 순차적으로 접근하는 것을 보장해주는 기능이 추가적으로 필요합니다. 즉 프로세서 #0이 마우스 데이터를 읽는 동안, 프로세서 #1이 먼저 접근해서 키보드 데이터인줄 착각하고 읽는 일을 막아야한다는 것이지요. ㅡ_ㅡa...

 어떻게 처리할 지 상당히 고민됩니다. 같은 포트를 사용하는 서로 다른 인터럽트 소스의 경우에 싱글 프로세서처럼 하나의 프로세서에 할당해주면 간단히 해결할 수 있는데... 더 좋은 방법이 있을 것 같아서 ^^;;;; 아우... 생각보다 빡시군요. ㅎㅎ

 일단 고민을 좀 더 해봐야겠습니다. ;)
 그럼 다들 좋은 밤 되시길~

ps) Qemu 같은 경우 키보드 컨트롤러와 CPU의 키보드 인터럽트 간에 타이밍 문제(?) 비슷한게 있는 것 같더군요. 각 인터럽트에서 데이터를 읽을 때, 데이터 포트에 데이터가 있는지 확인하도록 했더니 마우스와 키보드를 동시에 움직이는 경우 간혹 문제가 발생했습니다. ㅡ_ㅡa... 키보드 데이터가 마우스 인터럽트에서 나오더군요. 싱글 프로세서인데 말이지요. ㅡ_ㅡa... 결국 데이터가 있는지 확인 안하도록 해서 해결했습니다. 이건 뭐... ㅎㅎㅎ


 주말에 살짝 작업해서 시리얼 통신과 그래픽 모드로 전환하는 기능을 추가했습니다. @0@)/~ 예전에 썼던 코드들을 그대로 긁어 붙였더니 진도는 잘나가는데... 왠지 좀... 찔리는 것이... ㅎㅎㅎ

 아래는 1024 * 768에 16bit Color로 설정해서 파란 사각형을 그린 화면입니다. ;) 아우 신납니다. ㅎㅎ 이제 NDS에서 사용했던 그래픽 라이브러리를 올려볼까 생각 중입니다. 그래픽 라이브러리를 다시 만들려니 머리가 아파서 ^^;;;;
사용자 삽입 이미지

 밤이 늦었으니 일단 자고 나머지는 내일해야겠군요. ㅎㅎ
 아래는 그래픽 모드 전환에 대한 문서인 VBE 3.0 스펙입니다. ;)

 그럼 다들 좋은 밤 되세요 ;)


 ELF64의 Relocation 쪽을 실컷 파다가 문득 정신을 차려보니 잘 시간이 넘었더군요. 그래서 블로그의 덧글이나 확인하려고 들어갔는데, 요 며칠 포스팅한 글을 보니 잡담에 가까운 글이 많았습니다. ^^;;;; 아무래도 최근에 하고 있는 삽질 때문인 것 같습니다. ㅎㅎ

 언능 삽질을 끝내고 빨리 정리해야 할텐데... 욕심이 점점 많아져서 이것 저것 많이 하게 되는군요. 일단 지금은 간단한 GUI까지 해서 결과를 정리할까 생각 중입니다. GUI라고 해봤자 Frame Buffer 모드 비슷하게만 만들면 간단한 알고리즘으로 윈도우 비슷하게 흉내낼 수 있으니, 모양만 갖추는 거지요. ;) 아니면 닌텐도 DS에 올린 GUI 관련 소스를 사용해도 될 것 같습니다. ㅎㅎ

어휴, 할 것은 많고... 고민할 것도 많아서 머리가 지끈거립니다. 연이은 야근으로 인해 머리도 복잡하고 ㅎㅎ... 그래도 머리가 복잡한 덕분에 얻은 주옥같은 아이디어가 있어서 그나마 위로가 되는 것 같습니다.@0@)-b  주옥같은 아이디어가 뭔고 하니, 바로 한글 입력 처리에 대한 알고리즘입니다. ^^)/~

 일반적으로 한글 입력 처리는 오토마타를 사용해서 해당 상태에 따라 한글을 출력하는 방식을 사용합니다. 오토마타를 이해하면 쉽게 구현할 수 있는 부분이긴 하지만, 한글 입력 오토마타라는게 간단히 몇줄로 설명할 수 있는 내용이 아닙니다. 그래서 오토마타를 사용하지 않고 처리할 수 있는 부분에 대해서 고민하고 있었습니다. ㅠㅠ 그런데 갑자기 테이블을 사용해서 매치하는 방법이 떠오르더군요. @0@)-b

 아이디어를 간단히 설명하면 키 순서와 일치하는 한글을 테이블로 만들어서 입력된 키값과 테이블에 있는 값을 비교해서 처리하도록 하는 것입니다. 완성형 한글의 경우 숫자도 그리 많지 않으므로 충분히 테이블로 커버할 수 있을 거라는 생각이 듭니다. 구현 또한 아주 간단하고 오토마타보다는 더 직관적이지요. ;)

 아아~ 또 즐거운 주말이 다가오는군요. 이번 주말은 좀더 빡시게 굴러서 대충 마무리를 해야겠습니다. ;)
 그럼 다들 좋은 밤 되세요. ^^)/~



 고민 끝에 Ultra DMA쪽은 다음에 하는 걸로 미루고, ELF64의 Relocation 처리 부분을 하고 있습니다. PCI 부분을 다루자니 배보다 배꼽이 더 커지는 것 같고, 또한 굳이 DMA를 사용하지 않아도 HDD를 읽는데 문제가 없기때문에 작업량을 늘릴 필요가 있겠나 하는 생각이 들었습니다. ㅠㅠ 그래서 결국 다음에 시간나면 천천히 하는 걸로 결정했습니다.

 ELF64 File Format Spec을 보면서 느낀건데, PE File Format과 굉장히 비슷하군요. 차이점이라면 Section 별로 Relocation 정보를 가진 Section이 딸려오는 것이랄까요... r_link 및 r_info Field를 이용해서 Relocation 및 Symbol 정보가 포함된 Section으로 찾아갈 수 있습니다.

 아래는 간단히 VC로 만들어본 ELF64 Analyzer의 화면입니다. 각 Section 정보를 추출해서 Symbal 및 Relocation 정보를 나열하고 있습니다.
사용자 삽입 이미지

 이제 실제로 Relocation해서 메모리에 올리는 일만 남았군요. ;) 점점 삽질하는 속도가 빨라지고 있습니다. ㅠㅠ 좋은 건지 나쁜 건지... 일단 ELF64에 대한 내용은 천천히 정리해서 포스팅하겠습니다. ;)

 요거 끝나고 나면 GUI를 살짝 만들어보고 바로 작업(?)에 들어가야겠습니다. ㅎㅎ 아웅~ 잘되야할텐데... 걱정입니다. ㅎㅎ
 그럼 다들 좋은 밤 되세요 ^^)/~



 며칠째 DMA만 계속 파고 있는데, 이거 아무래도 가상 머신에는 ISA DMA 3번과 HDD가 연결되지 않은 것 같습니다. 아무리해봐도 DMA가 시작되지 않는 걸 보니, 문제가 좀 있는 것 같군요. ㅠㅠ 분명 코드에는 별로 이상이 없는 것 같은데....

 답답한 마음에 이래 저래 뒤지다가 PCI쪽에 DMA를 써서 여기저기 데이터를 전송할 수 있다는 걸 알아냈습니다. PCI를 사용하면 ISA보다 빠르니 훨씬 더 빠른 전송이 가능할 거라는 생각이 들어서 PCI쪽으로 방향을 돌렸습니다. ;)

 사실 HDD에서 데이터를 읽어드리는데 DMA를 사용하지 않고 PIO를 사용해도 되지만... 뭐랄까요, 지금까지 OS를 만들면서 계속 PIO만 사용했고, 갑자기 오기같은게 생겨서... ^^;;;; 해보다 안되면 그냥 GG 치고 ELF Relocation이나 마져해야겠습니다. ㅠㅠ

 혹시 HDD와 DMA를 연결하는 방법에 대해서 아시는 분 계신가요? 계신다면 댓글 부탁드립니다.
 그럼 좋은 하루 되세요 ;)

 요 며칠간 일이 좀 있어서 소스코드를 깊게 볼 시간이 없었습니다. ㅎㅎ 그러다보니 오늘에서야 겨우 소스를 뒤져볼 수 있게 됬습니다만, 간만에 소스를 보니 너무 테스트 코드가 많아서 눈이 빙빙 돌더군요. @0@ 그래서 코드 정리에 돌입했습니다.

 사실 그동안 PIT(Programmable Interval Timer)때문에 삽질을 좀 많이 했습니다. MP Spec에 따르면 INIT -> SIPI -> SIPI를 보내는 동안 각각 10ms, 200us, 200us 씩 Delay를 하게 되어있습니다. 따라서 이 Delay를 맞추기 위해 생각한게 Timer를 사용하는 것인데... 생각보다 에뮬레이터들이 완벽하지 않더군요.

 Timer의 Interval을 변경할때 종종 문제가 발생했습니다. ㅠㅠ 그래서 Timer이 Interval을 변경하지 않고 Tick을 계속 읽어들여서 Counting하는 방법으로 해결했습니다. 약간 꼼수긴하지만 어쨋든 되긴하니 ^^;;;; 패스~

 정리하는 김에 Dual Processor만 지원하도록 되어있던 부분을 실행 시에 Processor의 수를 읽어서 동적으로 셋팅하는 코드로 변경했습니다. 커널 스택을 1M 정도로 잡아놔서 Processor가 많아지면 많아질수록 할당될 스택의 크기가 줄어드는 문제가 있긴하지만... 커널에서 큰 지역변수를 쓰지 않는다면 괜찮을 것 같습니다.

 현재 QEMU를 이용해서 16개까지 해봤는데, 큰 문제가 없는 것으로 보아 잘되는 것 같습니다. ^^)/~ 물론 더 많이도 지원할 수 있습니다만, 아직까지 그럴 필요성을 못느껴서 16개로 한정했습니다. 아래는 실제 실행한 화면입니다.
사용자 삽입 이미지

 Bootstrap Processor는 Task 1과 Task 2의 Multi-Tasking을 수행중이고, 나머지 15개의 Application Processor는 화면에 자신이 실행된 Count를 출력하도록 되어있습니다. 아래쪽에 각 Processor가 Keyboard Interrupt를 얼마나 처리했는가 하는 값이 출력되어있는데, 여러 Processor가 Interrupt를 처리하고 있음을 볼 수 있습니다. ^^

 아아~ 이제 ELF64에 Relocation만 보면 어느 정도 끝날 것 같습니다. ;) 오늘 저녁에 뚝딱 해치워야겠군요.
 그럼 다들 좋은 주말 보내시길~ ^^)/~


 삽질을 끊임없이 한 결과(??) 64bit + Dual Core + Symmetric I/O System 까지 진행했습니다. 일단 다른 Core도 깨워서 64bit Mode로 전환시켜놨고, Interrupt 처리는 IO APIC와 APIC를 손봐서 두 Core에 Broadcasting 하도록 설정했습니다.

 Interrupt가 두 Core에 모두 전달되니, 서로 Interrupt를 처리하겠다고 경쟁하게 되더군요.  키보드 버퍼에 이상한 값이 들어가고 버퍼를 잘못 건드려 엉망이 됬습니다. 그래서 Spin Lock으로 먼저 Interrupt Handler에 접근한 Core가 처리하고 다른 Core는 그냥 End Of Interrupt만 송신하도록 했더니 나름대로 잘 동작합니다. >ㅁ<)/~ 이렇게 하는게 정석인지는 모르겠습니다만... 별 문제 없으니 그냥 패스.. ^^;;; 나중에 시간나면 Linux쪽에서 처리하는 방식을 한번 봐야겠군요(혹시 Linux의 Symmetric I/O 처리에 대해서 아시는 분 덧글 부탁드립니다 ㅠㅠ)-b ).

 아래는 Qemu에서 동작시킨 화면입니다. "showinterruptcount" 명령으로 각 Core가 Keyboard Interrupt를 얼마나 처리했는가를 표시했는데, 비슷하게 나오는군요. 의도하지 않은 Load Balancing이... ^^;;; 아직은 Cache 관련 부분이 Disable 되어 동작합니다.
사용자 삽입 이미지

 여기까지 해놓고 Cache를 Enable 하려니 IO APIC 및 APIC가 Memory Mapped I/O 방식으로 되어있어서 이 영역을 Cache가 불가능하게 설정해야 하더군요. 그래서 INTEL 문서를 뒤져보니 MTRR(Memory Type Range Register)를 사용해야한다고 나와있었습니다. ㅡ_ㅡa... 이것만 할게 아니라 CR0와 Page Table쪽도 다 같이 변경해줘야하고, 다른 쪽 Core 또한 같은 MTRR 설정값을 공유해야하는 문제가... 그래서 잠시 보류했습니다. ^^;;;;

 이거 생각 좀 해보고 넣어야겠군요. Cache를 활성화한다고 가상머신에서 돌아가는 OS가 크게 빨라질까 하는 생각도 들고... 차라리 Cache를 다 날려버리고 간단하게 구현하는게 더 나을지도 모른다는 생각이 계속 머리에서 맴돌아서... ㅠㅠ...

 자료가 별로 없어서 무한 테스트로 자료를 만드는데... 완전 죽음입니다. ㅠㅠ 아흑... 진도도 빨리 안나가고... 그래도 많이 왔네요. ㅠㅠ)-b 조금만 더 파보면 본격적인 작업(??)을 진행할 수 있을 것 같습니다. ;)

 그럼 다들 좋은 밤 되세요 ;)



 
 오늘은 어플리케이션 실행때문에 elf64의 재배치(Relocation)에 대해서 보고 있습니다. 어디서 얼핏 elf64의 코드는 RIP를 Base로 사용하는 Addressing을 하기 때문에 재배치가 용의하다는 내용을 봤었는데, 용의하다는 뜻이 애매해서 한번 파봤습니다.

 테스트 결과 재배치 섹션(Relocation Section)이 역시 존재하며, 재배치를 수행해야 정상적으로 실행가능하다는 겁니다. 커널 코드를 4M 와 2M Base로 설정하여 빌드한 후 바이너리를 비교해 봤는데, 함수를 호출하는 부분의 코드는 변함이 없으나 데이터에 접근하는 부분 같은 경우는 역시나 Base Address에 영향을 받는 부분이 있더군요. Diff 프로그램으로 검사해본 결과 바이너리가 달랐습니다.

 이렇게 되면 결국 elf64 파일 포맷으로 어플리케이션을 빌드하고 OS에서 재배치해서 로딩하는 수 밖에 없겠군요. ㅎㅎ 약간 안습인 상황이... ㅠㅠ 아흑... 어찌해도 일거리가 줄어들지 않는군요.

 시간나면 elf 파일 포맷이나 봐야겠습니다. elf64 파일 포맷은 www.openwatcom.org/ftp/devel/docs/elf-64-gen.pdf 에서 보실 수 있습니다. ㅎㅎ

 오늘은 좀 일찍 컴퓨터를 꺼야겠습니다. 몸이 점점 축나서 회사 생활이 안된다는... ㅎㅎ
 그럼 다들 좋은 밤 되세요 ;)


 지금까지 진행된 소스 코드를 정리하고 있습니다. 테스트 코드를 워낙 많이 넣었더니 테스트 코드를 지우다가 실수로 원래 코드를 지우는 경우도 생겨서... ㅠㅠ 이거 표시를 제대로 해놔야겠더군요.

 제가 "변수명이나 매크로는 그 이름 자체가 자신을 설명할 수 있어야한다"는 주의라서 변수명/함수명 등등을 굉장히 길게쓰는 편입니다. ^^;;;; 특히나 커널 코드처럼 장래에 계속 볼일이 있는 코드 같은 경우는 더더욱 심한 편이지요.

 시간이 지나면 잊혀지기 마련인데, 주석이나 잡 문서로 아무리 잘 설명해 놓은들 변수나 함수 이름을 보고 무슨 뜻인지 한번에 알 수 없다면 문제가 있다고 생각합니다. 물론 각자의 스타일이 있으니 아니라고 생각하시는 분들도 있을거라 생각합니다(짧고 간결하게 쓰는게 더 낫다고 생각하시는 분들은 돌 던지지 말아주세요 ㅠㅠ 그냥 제 생각입니다. ㅠㅠ).

 그런데 너무 길게쓰니 함수 호출하거나 할 때 한 컬럼이 굉장히 길어지더군요. 저는 왠만하면 80 컬럼을 맞춰서 쓰려고 노력합니다. 따라서 개행을 해야하는데 결국 한 라인에 함수나 변수가 몇개 못 들어간다는... ㅠㅠ 크윽... 이거 최악의 조합인 것 같습니다. ㅠㅠ 소스코드가 좀 희한한 트리 모양으로 보이는 군요. ㅠㅠ

 어떻게 하는게 좋을지는 나중에 한번 의견을 나눠봐야하겠지만, 일단 길게 작업해두는 쪽으로 가야겠습니다. 일단 제가 이해하기 쉬워야 남들한테 설명하기도 쉬우니까요. ㅎㅎ

 어휴 벌써 또 시간이 이렇게 됬군요.
 다들 좋은 밤 되세요 ㅎㅎ ;)


 크윽... 죽는 줄 알았습니다. ㅠㅠ 지난 휴가와 이번주 주말을 모두 투자해서 드디어 Interrupt 처리 부분을 넣고, 나중에 사용할 APIC 와 IO APIC쪽 설정 기능을 넣었습니다. ㅠㅠ 사실 훨씬 더 일찍 끝났어야 하는 일이나... 가상 머신에 불완전함 때문에 코드를 고치고 테스트하다보니 많이 늦어졌습니다.

 현재 테스트 중인 가상머신은 총 3개인데, VMware, Bochs, 그리고 QEMU를 사용하고 있습니다. 재미있는건 각자 BIOS 설정이 조금씩 다르고, 같은 코드라도 동작하는 방식이 다르다는 것이죠. ㅠㅠ 이것 때문에 죽는 줄 알았습니다. 특히 Interrupt 처리부분에서 Bochs와 VMware에 문제가 있는 것 같더군요.

 테스트를 몇시간동안 한 결과 코드에서 Stack을 Align하지 않고 사용하는 경우, 간혹 Stack에 데이터가 덮어써져서 나중에 iretq로 돌아왔을 때 정상적으로 수행되지 않는 문제가 있었습니다. 원래 CPU가 realign을 해주게 되어있습니다만, 정상적으로 동작하지 않는 때가 있는 것 같습니다. 물론 꼼수로 해결했습니다만, 뭔가 좀 씁쓸하네요. ^^;;; 더 웃긴건 QEMU의 경우는 괜찮다는 것이죠. ;) 이 문제 때문에 거의 이틀을 꼬박 날렸습니다. 아흑 내시간...ㅠㅠ

 더 가관인 문제는 VMware에서 생겼습니다. Keyboard Interrupt를 발생하지 못하게 한 뒤에, 키를 마구 입력해서 키보드 버퍼를 체운 후 APIC쪽을 Enable 하니 Interrupt가 발생하지 않는 것이 아니겠습니까? @0@)/~!!! VMware의 APIC쪽하고 PIC쪽에 매치하는 부분에서 문제가 있는 것 같은데, 키보드 버퍼에서 데이터를 다 읽어내면 그때부터 인터럽트가 발생하기 시작합니다. ㅡ_ㅡa;;;;; 이것도 결국 땜빵코드로 해결... ^^;;;

 마지막으로 애교로 넘어가줄 수 있는 문제... Qemu에서 int 13, 즉 Disk Read에 문제가 있더군요. 여러 섹터를 읽도록 요청하면 간혹 덜 읽히고 종료되는 문제가 있었습니다. 이 문제는 어느정도 예상을 하고 있던 것이라 역시나 땜빵으로 해결... 예전에 만들면서 겪었던 문제라서 그냥 애교로 넘어갔습니다. ㅎㅎ

 결국 온갖 삽질 끝에 세가지 에뮬레이터에서 모두 동작시키는데 성공했습니다. 아래는 세가지 에뮬레이터에서 동작시킨 화면입니다.
사용자 삽입 이미지

사용자 삽입 이미지

사용자 삽입 이미지

 이제 대충 멀티 태스킹과 elf64의 재배치(Relocation) 쪽만 해결하면 되겠군요. ㅎㅎ 코어를 하나 더 활성화 시키는 것은 저번에 해봤으니 그냥 붙여넣기만 하면... (설마 이것도 쉽게 안되려나..ㅠㅠ)  그나저나 계속 삽질만 해서 큰일입니다. ㅠㅠ 크윽...

 제발 좀 한번에 가자 ㅠㅠ)/~~

 그럼 다들 좋은 밤 되시길~ ^^)-b



지난 주 목요일부터 휴가입니다만은... 어디 놀러가는 일에 취미가 없는지라 고향에 내려와서 OS를 만들고 있습니다. ㅡ_ㅡa... 누가 들으면 간단한 애플리케이션을 만드는 것 처럼 이야기한다고 욕을 할지도 모르겠습니다. 사실 말은 간단하게 해도 수많은 시행 착오를 거쳤습니다. ^^;;;; Intel 문서에 태그가 안 붙어있는 쪽이 거의 없을 정도고, 코드는 테스트 루틴 때문에 너덜너덜합니다. ㅎㅎ

 일단 64Bit로 부팅시키는 것까지 성공했고, 지금은 Interrupt Descriptor Table(IDT)를 만들어 넣고 있습니다. 이것만 추가되면 이제 키보드로부터 데이터를 받을 수 있으니 좀 더 Active한 작업이 가능해지겠지요. ;) OS 코드들은 기존에 만들어놓은 32Bit OS쪽에서 당겨쓰고 있습니다. 어셈블리어로 짜여진 부분은 일일이 손을 좀 봐야하지만, C 코드 부분은 거의 손댈 것이 없더군요. ㅜ_ㅜ 역시 C 만세.... ㅠ_ㅠ)-b

 이번 휴가 때 어느 정도 동작하는 64Bit OS를 만들어 놓을 생각입니다. 휴가가 며칠 안남았기 때문에 하드한 일정이 예상되나... 역시나 밖에 안나가는 오타쿠(??)적인 기질 때문에 가능할 듯도 합니다. ㅡ_ㅡa... 재미있는 건 어제 밤새도록 코딩해서 오늘 오전쯤에 64Bit Mode로 동작시켰는데, 오후에 후배 녀석이 64Bit OS 제작 중인데 인터럽트를 어떻게 처리하냐고 물어보더군요. ^^;;;; 이것 참... 뭔가 섬뜩한 것이... @0@

 아래는 오늘까지 작업된 따끈따끈한 OS의 스샷입니다. 64Bit인지 어떻게 믿냐구요? 아래의 코드를 실행한 결과를 화면으로 캡쳐했습니다. int 가 4byte, long이 8byte로 나오는 것을 보면 64bit임을 알 수 있습니다. ^^)/~~


사용자 삽입 이미지

 어휴 작업을 너무 오래까지 했더니만 눈이 따갑네요. 이만 자야겠습니다. ^^)/~
 다들 좋은 밤 되세요 ;)


 Cywin에서 크로스 컴파일러를 만들기위해서는 먼저 소스 패키지를 설치해야합니다. setup.exe 를 실행해서 Devel 부분의 Binutil gcc-core 의 소스 패키지를 선택하고 설치를 수행하면 소스가 /usr/src 폴더에 설치될 겁니다. ^^

1.Binary Utility Build

 제일 먼저 해야할 일은 binutil을 빌드하는 일입니다. x86_64용 크로스컴파일러를 빌드하기 위해서는 x86_64용 ld와 as 등등 각종 링커와 어셈블러들이 필요하기 때문이지요 ;) bintul을 빌드하는 방법은 아래와 같습니다.
  • export PREFIX=/usr/cross
  • export TARGET=x86_64-pc-linux
  • ./configure --target=$TARGET --prefix=$PREFIX --enable-64-bit-bfd --disable-shared
  • make configure-host
  • make LDFLAGS="-all-static"
  • make install
 위와 같이 실행하고나면 정상적으로 build된 경우 /usr/cross 라는 폴더가 생성되어있을 겁니다. 그 폴더에 들어가보시면 빌드 결과물을 확인할 수 있습니다. ;) 현재 x86_64 용 glib가 생성되어있지 않은 상태이므로, 공유라이브러리를 사용하지 않도록 --disable-shared-all-static 옵션을 줘야합니다. 주지 않을 경우 나중에 gcc 빌드할 때 문제가 발생합니다. ㅜ_ㅜ crti.o가 없다고 계속 에러가... ㅠㅠ

2. GCC Build

 그 다음 해야할 일은 gcc를 생성하는 일입니다. cygwin에 포함된 gcc 소스에는 소스 파일과 패치파일을 포함하고 있습니다. 정상적으로 빌드하기위해서는 패치를 수행해야 하는데, 다행이도 gcc-3.4.4.sh 라는 스크립트가 있습니다. 아래의 빌드를 수행하기 전에 먼저 아래와 같이 입력해서 패치를 수행합니다.
  • gcc-3.4.4.sh prepare
 위 과정이 정상적으로 끝나고나면 gcc-3.4.4-3 이라는 폴더로 이름이 변경될겁니다. 그럼 해당 폴더로 이동해서  아래의 순서대로 실행합니다.
  • export PATH=$PREFIX/bin:$PATH
  • ./configure --target=$TARGET --prefix=$PREFIX --disable-nls --enable-languages=c --with-newlib --without-headers --disable-shared --disable-threads
  • make configure-host
  • make LDFLAGS="-static" all-gcc
  • make install-gcc
 위와 같이 실행하고나면 /usr/cross에서 gcc관련 파일들을 보실 수 있습니다.

 이 간단한걸 하려고 며칠을 해매었는지 모르겠군요. ㅜ_ㅜ)-b
 다른 분들은 이런 걸로 고생 안하셨으면 합니다. ㅜ_ㅜ
아아~ 진짜 거의 3일만에 성공했습니다. ㅜ_ㅜ 주말 내내 붙잡고 있다가 해결 못하고 있었는데... 구글의 자료를 뒤지다가 --disable-shared 옵션 및 LDFLAGS="-static" 으로 해결했습니다.

 ㅜ_ㅜ 진짜 되고 안되고가 한끗 차이더군요. 저런 옵션 하나때문에 몇시간을 날렸는지... ㅜ_ㅜ... 아래는 빌드가 끝나고 cygwin에 설치된 64bit 크로스 컴파일러(cross complier) 입니다. 감동해서 스샷 하나 찍어 올립니다(반갑다 애들아~ 우리 이제 잘해보자꾸나...). ㅜ_ㅜ)-b

사용자 삽입 이미지

 자세한 빌드 방법은 내일 정리해서 올리겠습니다. 앞으로는 저처럼 삽질하는 사람이 없었으면 하는 생각이 드는군요. 진짜 어의없습니다. ㅜ_ㅜ

 그럼 다들 좋은 밤 되시고, 내일 허접한 문서로 다시 찾아 뵙겠습니다. ㅜ_ㅜ)-b
 아아 이거 또 실패했습니다. ㅜ_ㅜ ln 까지는 갔으나... crti.o를 자꾸 찾는 바람에.... ㅜ_ㅜ cygwin에서 컴파일하는데 저 파일을 찾는 걸 보니 --target=x86_64-linux 옵션으로 줘서 그런 것 같습니다.

사용자 삽입 이미지

 크윽... 왜 Cygwin Package에는 64bit로 크로스 컴파일하는 옵션이 없을까요... ㅜ_ㅜ --target=x86_64-cygwin 요런거라도 있으면 좋을 텐데 말입니다. ㅜ_ㅜ

 당체 이 삽질을 언제 끝낼 수 있을지... 크윽... ㅜ_ㅜ 눈물이 앞을 가립니다. 흑흑...



 아아~ 이거 주말을 다 날렸습니다. 결국 cygwin으로 옮겨타고 나서야 binutil을 컴파일 할 수 있었는데... gcc를 컴파일 하려니 이것 저것 문제가 많아서 결국 실패했습니다. ㅜ_ㅜ

 컴파일을 한참 진행하다보니 거의 마지막 단계인 nm을 사용하는 부분과 ar을 사용하는 부분이 문제인것 같던데... makefile 안에 있는 아래와 같은 부분이 정상적으로 동작하지 않는 것 같습니다.

AR_FOR_TARGET = ` \
  if [ -f $(objdir)/../binutils/ar ] ; then \
    echo $(objdir)/../binutils/ar ; \
  else \
    if [ "$(host)" = "$(target)" ] ; then \
      echo ar; \
    else \
       t='$(program_transform_name)'; echo ar | sed -e $$t ; \
    fi; \
  fi`
AR_FLAGS_FOR_TARGET =
AR_CREATE_FOR_TARGET = $(AR_FOR_TARGET) $(AR_FLAGS_FOR_TARGET) rc
AR_EXTRACT_FOR_TARGET = $(AR_FOR_TARGET) $(AR_FLAGS_FOR_TARGET) x

RANLIB_FOR_TARGET = ` \
  if [ -f $(objdir)/../binutils/ranlib ] ; then \
    echo $(objdir)/../binutils/ranlib ; \
  else \
    if [ "$(host)" = "$(target)" ] ; then \
      echo $(RANLIB); \
    else \
       t='$(program_transform_name)'; echo ranlib | sed -e $$t ; \
    fi; \
  fi`

NM_FOR_TARGET = ` \
  if [ -f ./nm ] ; then \
    echo ./nm ; \
  elif [ -f $(objdir)/../binutils/nm-new ] ; then \
    echo $(objdir)/../binutils/nm-new ; \
  else \
    if [ "$(host)" = "$(target)" ] ; then \
      echo nm; \
    else \
       t='$(program_transform_name)'; echo nm | sed -e $$t ; \
    fi; \
  fi`

 원래의 의도대로라면 if else에 의해서 x86_64-linux-nm 이나 x86_64-linux-ar과 같이 치환되어야할 것들이 그대로 쉘에 출력되어 난데없는 rc.exe가 실행되는 안습인 상황이 벌어지더군요. ㅡ_ㅡa... 쉘이 문제인지 makefile이 문제인지 확실하게 모르겠습니다만... 정상적으로 실행이 안되는 것은 사실인 것 같습니다. ㅎㅎ

 아아~ 이거 주말을 다 날렸네요. ㅜ_ㅜ 내일은 꼭 성공해야할텐데... 큰일입니다. ㅜ_ㅜ
 그럼 좋은 밤 되세요 ;)

ps) cygwin에서 x86_64로 크로스 컴파일 해보신 분 있으시면 팁 좀 부탁 드립니다. (_ _)


 32bit 윈도우라서 64bit 코드를 생성하는게 상당히 힘들군요. ㅜ_ㅜ 되도록이면 Open Source를 사용하려고 MinGW나 Cygwin을 보고 있습니다만... 다들 m64 옵션이 먹지 않는 것 같네요. ㅜ_ㅜ

 MinGW에 GCC 소스를 받아서 컴파일 해보니 이것도 에러가 발생... ㅜ_ㅜ... 아놔 천지 쉬운게 없군요. 이거 하루종일했는데, 결국 실패했습니다.

 어디 m64 옵션 먹게 컴파일된 GCC 없나요? ㅜ_ㅜ


 크윽... 이럴수가... 64bit 코드를 생성할 일이 있어서 MinGW를 설치했는데, m64 옵션이 먹지 않더군요. ㅜ_ㅜ 구글을 뒤져보니 디폴트로 컴파일되서 32bit 만 지원하나 봅니다. ㅜ_ㅜ

 결국 한 2시간을 찾아 해매다가... 결국 gcc 소스를 받아서 새로 컴파일하기로 했습니다. 어휴... 이거 초장부터 쉽지 않습니다. ㅜ_ㅜ... 아흑... 오늘 하루종일하겠군요.

 일단 컴파일되서 테스트까지 끝나면 포스팅하겠습니다. ;)
 다들 즐거운 주말 되시길 ^^)/~


 거의 4일 동안 문서를 뒤지고 직접 테스트를 수행한 결과, IO APIC와 APIC간의 관계와 8259A(PIC) 칩간의 관계에 대해서 알아냈습니다. 더불어 BIOS에 존재하는 MP 관련 정보와 IO APIC의 Redirection Table, CPU의 APIC의 레지스터에 의미에 대해서도 대충 알아냈습니다. ㅜ_ㅜ)-b

 어떻게 테스트 했는지는 나중에 올리겠습니다. 나름 복잡한 내용을 포함하고 있고, 또 시간이 너무 늦은지라 ㅎㅎ 대신 아래의 스샷을 첨부합니다. 제가 만든 OS(KKAMAGUI OS)에서 IO APIC를 통해 BSP(Bootstrap Processor)에 키보드와 타이머 이벤트를 밀어넣는 화면입니다. 볼품없지만 이게 4일 동안 작업한 것이라는... OTL....
사용자 삽입 이미지

 그래도 IO APIC와 Local APIC에 대해 잘 모르면 Sysmmetric I/O로 갈 수 없기 때문에 어쩔 수 없었습니다. ㅜ_ㅜ)-b 결국 하긴 했군요. 흑흑.... 8259A(PIC) 컨트롤러의 타이머/키보드 인터럽트를 발생 못하도록 mask 했는데도 인터럽트 방식으로 잘 동작하는 화면을 보니 신기하군요. ^^;;;;

 그럼 이만 자야겠습니다. 좋은 밤 되시길~ ;)
 요 며칠째 계속 VMWare하고 Multiple Processor 스펙, 그리고 IO APIC 스펙을 함께 보며 삽질을 하고 있습니다. 그런데 아무리봐도 뭔가 이상하군요.

 Multiple Processor 스펙에 보면 MP Floating Pointer Structure의 Feature 2 정보에 IMCRP 비트가 셋팅되어있지 않으면 IMCR 레지스터가 존재하지 않는 것이고...이런 경우 기본 모드는 Virtual Wire 모드라고 되어있습니다.

 앞서 http://kkamagui.tistory.com/497 에서 설명 드렸듯이 Vritual Wire 모드는 2가지 모드가 존재하고, 테스트해본 결과 Local APIC는 다 Enable 되어있고 IO APIC의 Redirect Table이 모두 Default 값으로 Disable 되어 있으므로 Virtual Wire mode with Local APIC 라고 볼 수 있습니다(아래 그림 참조).

사용자 삽입 이미지

 그런데 웃긴건, BIOS가 MP를 초기화하고 그 정보를 저장해놓는 MP Configuration Table의 I/O Interrupt Assignment Entries 정보를 보면 IO APIC가 활성화 되어있으며 8259 컨트롤러가 IO APIC의 첫번째 pin에 연결되어 있다고 나옵니다. 나머지 타이머나 키보드 등등도 다 IO APIC에 연결된 것으로 설정되있습니다. ㅡ_ㅡa...

 만약 그렇게 연결되어있다면 IO APIC의 Redirect Table에도 동일한 설정으로 저장되어있어야 맞는 것 같은데... IO APIC의 Redirect Table에는 아무것도 안들어있으니... ㅡ_ㅡa... 이게 당체 어디가 맞는 건지 알 수가 없군요. ^^;;;; Redirect Table에 아무런 값이나 막 넣어봐도 별 에러없이 동작하던데... VMWare의 BIOS가 신경을 안쓰는 건지... 아니면 원래 그런 것인지 모르겠습니다. ㅜ_ㅜ

 그리고 Symmetric I/O는 당체 어떻게 ON 하는 것인지... ㅡ_ㅡa... 그냥 Local APIC를 다 활성화 시키고 I/O APIC의 Redirect Table에서 Destination을 손보면 되는 것인지... 이것 참... Redirect Table의 비밀이 풀려야 다음 진도를 나갈 수 있을 것 같은데... 이게 쉽지 않군요. ^^;;;;

 에혀... 죽겠습니다. 혹시 아시는 분 계시면 제보 부탁드립니다. ^^)/~
 그럼 좋은 밤 되시길~ ;)


 예비군 내내 Intel Architecture Volume 3을 봤습니다. 그리고 고향에 내려오는 도중에 PSP로 Multile Processor(MP) Spec을 계속 봤었는데... 뭔가 좀 이상하더군요. Intel 문서에는 I/O APIC에 대해서 아주 조금 언급하고 있습니다. 그냥 APIC와 연결되있다는 정도로 말이지요. ^^;;;;
 
 그런데 MP Spec에 보면 PIC, Virtual PIC, Symmetric I/O의 각 방식에 대해 아래와 같이 자세히 그려놓고 있습니다.
사용자 삽입 이미지
<PIC 모드>
사용자 삽입 이미지
<Virtual Wire Mode With Local APIC>
사용자 삽입 이미지
<Virtual Wire Mode With IO APIC>
사용자 삽입 이미지
<Symmetric IO Mode>

  보시면 아시겠지만 CPU의 Boundary 안쪽에 IO APIC가 있는지 바깥쪽에 있는지 좀 불분명합니다. 어렴풋이 버스 배열 상태나 위치를 봐서 바깥쪽에 있다고 추측합니다만... 이것만 봐서는 확실히 알수가 없었습니다. 그래서 이것 저것 뒤지다보니 intel-82093-apic 문서를 찾았습니다. IO APIC 컨트롤러에 대한 문서더군요. ^^;;; 거의 바깥쪽에 있는게 확실한 것 같습니다. 그런데 이게 왜 중요했던건지... ㅡ_ㅡa;;;;

 생각난 김에 VMWare의 부팅했을때 모드가 위의 그림 중에 어떤 것인지 확인을 좀 해봤습니다. 몇가지 테스트가 있는데... 테스트 과정은 좀 복잡해서 생략하고 결론만 이야기하면 Virtual Wire Mode With Local APIC 인 것 같습니다. 왜냐하면 I/O APIC쪽에 Interrupt Remapping Table인가 하는 녀셕이 Default 값으로 되어있거든요. 즉 안쓴다는 이야기지요. ^^;;; 그동안 다른쪽 코어에는 인터럽트쪽 설정을 안했기 때문에 별 신경을 안쓰고 있었는데, 이제 다른 코어에 인터럽트를 활성화하려면 이것이 중요한 단서가 되서 한번 읽어봤습니다.

 음... 이제 몇가지 코딩을해서 Symmetric IO Mode 로 전환하고 테스트를 좀 더 진행해볼 생각인데... 이것 참... 할일이 많네요. ^^;;; 언제쯤 또 할 수 있을지... 요즘 들어 시간이 부쩍 부족한 것 같습니다. ㅎㅎ 그래도 잘 쪼개서 이것 저것 해봐야겠지요 ;)

 그럼 다음에 또 테스트해서 결과가 나오면 올리겠습니다. ㅎㅎ 다들 좋은 하루 되세요 ;)

ps) APIC 관련 자료가 참 없더군요. 참 불모지(?)스럽다는.... ㅡ_ㅡa...


Multiple Processor System(Multicore System)의 Cache 효율 높이기

원문 : http://kkamagui.springnote.com/pages/1092170

참고 1 : http://softwarecommunity.intel.com/articles/eng/2760.htm

참고 2 : http://minjang.egloos.com/1848130

 

들어가기 전에...

 

OS 개발에 관한 서적

하위 페이지에 있는 자료들은 OS를 개발하면서 관련된 자료들을 일부 정리해 놓은 것이다. OS 개발에 대한 보다 자세한 내용은 "64비트 멀티코어 OS 원리와 구조"를 참고하기 바란다.

크기변환_크기변환_836_1.jpg

   <64비트 멀티코어 OS 원리와 구조>

 

0.시작하면서...

 이 글은 Multiple Processor System에서 발생할 수 있는 Shared Cache의 문제를 어떻게 해결할 수 있는지를 설명하는 문서이다. 참고 자료의 내용 중에 테스트 가능한 부분만 요약하고 추가적인 내용을 덧붙였으니, 원문이 궁금하면 참고 1(http://softwarecommunity.intel.com/articles/eng/2760.htm) 문서를 보면 된다.

 

1.Processor, Core, Cache

 최근의 Processor는 Core를 2개 이상 탑재하는 방식으로 제작되고 있으며, 그에 따라 Cache의 중요성이 점점 높아지고 있다. CPU 업계를 이끌어 나가는 INTEL과 AMD는 Cache 구성에 대해 서로 다른 정책을 펴고 있으나, 공통적으로 Cache의 크기를 점차 넓혀가는 추세이다.

 Processor에 포함된 Core가 많고 Cache의 크기가 커지면 커질수록 성능이 배로 좋아질 것 같지만, 실제로는 각 Cache Level(L1, L2, L3)간의 동기화 문제가 발생하기때문에 정비례하지 않는다. 다시말하면 Core 0와 Core 1이 동일한 Data에 접근하거나 같은 Cache Line에 있는 데이터에 접근해서 Read/Write를 하게 될 경우, 각 Cache Level의 Data를 Syncronization(동기화)해야하기 때문에 성능이 저하되는 것이다. 이에대한 자세한 내용은 다음 장에서 살펴보자.

 아래는 2개의 Core를 가지는 Processor 2개를 연결한 그림이다. Processor 0에 L2 Cache가 Core 0와 Core 1에 공통으로 사용되고 있으며, L1 Cache는 각 Core에 할당되어 공유되지 않음을 볼 수 있다. L1 Cache 공유되지 않기 때문에 위에서 언급했던 L1 Cache와 L2 Cache간에 Syncronization 문제가 발생하게 된다.

Cache_구조.gif

<2 Dual Core Processor>

 

 그럼 지금부터 Cache를 효율적으로 사용할 수 있는 몇가지 방법에 대해 알아보자.

 

2.Cache Optimization Tip

2.1 Processor Affinity

  Multithread 프로그램의 경우, 개별 Thread는 개별 Core에 할당되서 동시에 실행될 수 있다. Core에 할당되는 Thread는 Scheduling Algorithm에 따라 달라지며, 하나의 Core에서 실행되거나 여러 Core를 돌며 실행될 수 있다. Thread A가 Core A와 Core B에서 교대로 실행되는 경우, Thread A가 사용하는 Data를 매번 L2 Cache에서 L1 Cache로 옮겨오는 최악의 상황이 발생할 수 있으며 그로 인해 성능이 저하될 수 있다.

 이러한 경우 Thread를 특정 Core에 할당해서 실행함으로써 Overhead를 줄일 수 있다. 또한 각 Thread가 Data를 공유하지 않는다면, 개별 Thread를 특정 Core에서만 수행되도록 할당함으로써 성능을 높이는 것이 가능하다.

 Windows 환경과 Linux 환경에서는 아래의 2가지 API를 사용해서 Thread와 Process의 Processor Affinity를 설정할 수 있다.

  1. // Windows API
  2. BOOL SetProcessAffinityMask(
      HANDLE
    hProcess,
      DWORD_PTR
    dwProcessAffinityMask
    );
    DWORD_PTR SetThreadAffinityMask(
      HANDLE
    hThread,
      DWORD_PTR
    dwThreadAffinityMask
    );
  3. // Linux API
  4. /* Get the CPU affinity for a task */
    extern int sched_getaffinity(
      pid_t pid,
  5.   size_t cpusetsize,
  6.   cpu_set_t *cpuset
  7. );
  8. /* Set the CPU affinity for a task */
    extern int sched_setaffinity(
      pid_t pid,
  9.   size_t cpusetsize, 
      const cpu_set_t *cpuset
  10. );

 하지만 Affinity가 좋은 점만 있는 것은 아니다. 할당된 Core에 Load가 몰리는 경우, Scheduling 순서를 기다리느라(Waiting Queue에 대기) 오히려 더 지연 될 수 있으니 주의해야 한다.

2.2 Cache Blocking(Data Tiling)

 Cache Blocking 또는 Data Tiling 기법은 Loop를 수행할 때, Loop에서 사용되는 Data가 Cache Line에 존재하도록하는 기법이다. 만약 Data Block이 크다면, Cache에 Load된 Data가 Loop시에 한번만 사용되고 버려지고 현상이 반복될 수 있다. 이러한 최악의 상황은 Cache의 Hit Ratio를 떨어뜨리며 성능을 저하시키는 요인이 된다. 이러한 현상을 해결하는 좋은 방법은 Data를 Tile 처럼 잘게 쪼개서 작은 Loop로 처리하는 것이다.

 아래의 그림과 같이 (A)와 같이 큰 Data를 사용하는 Loop를 (B)처럼 작은 Data를 사용하는 Loop로 나눔으로써, Data Block을 Cache에 올려 성능을 높일 수 있다.

Cache_Blocking.gif

<Cache Blocking(Data Tiling)>

 

2.3 Hold Approach

 Hold Approach는 빈번하게 Read/Write하는 것을 Local Copy를 사용하여 줄임으로써 Cache 효율을 높이는 방법이다. Thread Local Storage나 Local Variable을 사용하여 작업한 후, 결과를 Reporting하는 것이 이에 해당한다. 아래의 그림은 Hold Approach를 그림으로 표현한 것이다.

Holding_Approach.gif

<Holding Approach>

 좌측의 (A)와 같이 빈번하게 Read/Write하던 것을 Local Copy를 통해 (B)와 같이 줄여서 효율을 높일 수 있다.

2.4 Avoid False Sharing

 False Sharing이란 Multiple Processor System의 유명한 문제이다. 각 Core에서 수행중인 Thread간의 공유하는 Data가 없지만, 동일한 Cache Line에 있는 Data를 접근함으로써 매번 Evict와 Load가 반복되는 현상이다. 실제로는 Cache Coherency Problem(협업 문제)가 전혀 발생하지 않지만, Cache의 정보가 변경되었으므로 어쩔 수 없이 L1 Cache의 내용을 L2 Cache에 옮기고 다시 다른 Core의 L1 Cache로 옮겨야 한다. 아래의 그림은 Flase Sharing을 그림으로 표현한 것이다.

 

286506_286506.gif

<False Sharing>

 Core 0의 Thread 0와 Core 1의 Thread 1은 서로 공유하는 Data가 없지만 동일한 Cache Line에 위치함으로 Evict와 Load가 반복되게 된다.

 이러한 False Sharing을 피하는 방법은 아래와 같다.

  • 공유되지 않는 Data는 다른 Cache Line에 둔다. 즉 Padding Data를 두거나 다른 Memory Address에 할당한다. 단, 너무 많은 Cache Line을 할당하지 않도록 Data를 잘 묶어서 적당히 배열한다.
  • Hold Approach와 마찬가지로 Data를 Local 영역에 저장하고 필요할때만 Read/Write하여 Evit/Load 회수를 줄인다.
  • Thread의 Affinity를 설정하여 Thread 0와 Thread 1이 동일한 Core에서 수행되도록 한다.

 

3.Test

 아래는 Cache Optimazation Test에 사용한 간단한 코드이다. 윈도우즈 환경에서 VC++ 6.0으로 테스트했으며 Release 모드에서 Optimization 옵션은 Debug 모드와 같이 사용하지 않는 것으로 설정하고, 8Byte Aling, MultiThread DLL을 사용하도록 설정했다.

 테스트에 사용된 PC는 INTEL Core 2 Duo 2.6GHz에 4G의 RAM으로 구성되어있다.

3.1 Affinity & Cache Blocking Test

 아래는 Affinity와 Cache Blocking을 테스트하는 간단한 코드이다.

  1. #include <windows.H>
    #include <stdio.H>
    #include <tchar.H>
  2. #define UNIT      32
    #define MULTIPLE  200000
    #define MAX_DATA  UNIT * MULTIPLE
    #define MAX_LOOP  200
  3. // 계산할 데이터가 들어있는 배열
    volatile int g_iData[ MAX_DATA ] = { 1, 2, };
  4. // 실제 메인 함수
    int main(int argc, char* argv[])
    {
        DWORD dwSum;
        int i;
        int j;
        int k;
        int l;
        DWORD dwStartTime;
        DWORD dwEndTime;
        DWORD dwTimeSum;
  5.     // Processor High Priority로 만든다.
        SetPriorityClass( GetCurrentProcess(), HIGH_PRIORITY_CLASS );
  6.     // Affinity 설정
        SetProcessAffinityMask( GetCurrentProcess(), 0x1 );
  7.     // 카운팅 시작
        dwSum = 0;
        dwTimeSum = 0;
        printf( "Start Processing...\n" );
        for( l = 0 ; l < 20 ; l++ )
        {
            dwStartTime = GetTickCount();
           
            // Normal Loop
            for( j = 0 ; j < MAX_LOOP ; j++ )
            {
                for( i = 0 ; i < MAX_DATA ; i++ )
                {
                    dwSum += g_iData[ i ];
                }
            }
  8.         /*
            // Cache Blocking
            for( k = 0 ; k < MULTIPLE ; k++ )
            {
                for( j = 0 ; j < MAX_LOOP ; j++ )
                {
                    for( i = k * UNIT ; i < ( k + 1 ) * UNIT ; i++ )
                    {
                        dwSum += g_iData[ i ];
                    }
                }
            }
            */
           
            //결과 출력
            dwEndTime = GetTickCount() - dwStartTime;
            dwTimeSum += dwEndTime;
            printf( "%d Time = %d ms\n", l + 1, dwEndTime );
        }
  9.     printf( "Average Time = %0.2f\n", dwTimeSum / 20.0 );
        return 0;
    }

아래는 위의 코드를 이용해서 테스트한 결과이다.

  • Normal Loop

    • Affinity 미사용

      • Average Time = 4225.00
      • Average Time = 4201.55

    • Affinity 1 사용 - 하나의 Core에서만 동작시킴

      • Average Time = 4275.00
      • Average Time = 4256.25

  • Cache Blocking

    • Affinity 미사용

      • Average Time = 3836.70
      • Average Time = 3818.70
    • Affinity 1 사용 - 하나의 Core에서만 동작시킴

      • Average Time = 3875.80
      • Average Time = 3860.90

 

 테스트 결과 Data Block 크기가 클수록, 즉 Cache Evit가 발생할 확률이 높으면 높을수록 Cache Blocking의 효과가 커졌다. Processor Affinity 부분은 오히려 Affinity를 적용한 결과가 근소하게 더 느리게 나왔다. 이것은 Thread가 Core를 옮겨다니며 Cache를 Reflesh하는 시간 보다 할당 받은 Core의 Wait Queue에서 대기하는 시간이 더 길기 때문에 발생한 듯하다. 이 결과는 어디까지나 테스트 프로그램의 결과이고, 만약 더 큰 Data에 접근하고 보다 복잡한 코드로 이루어져 있다면 충분히 달라질 수 있는 부분이다.

3.2 Affinity & Avoid False Sharing Test

 아래는 Affinity와 False Sharing을 테스트하는 간단한 코드이다.

  1. #include <windows.H>
    #include <stdio.H>
    #include <tchar.H>
  2. // 각 스레드에서 사용하는 전역 변수
    volatile int g_iData1 = 0;
    volatile int g_iData2 = 0;
  3. // Cache line을 맞추기위해 정의한 구조체
    // 강제로 공간을 할당한다.
    typedef struct dataStruct
    {
        volatile int iData1;
        int viData[32];
        volatile int iData2;
    } DATA;
  4. // Cache line 정렬용 구조체
    DATA g_stData = { 0, {0,}, 0 };
  5. // 테스트 Thread 1
    DWORD CALLBACK TestThread1( void* pData )
    {
        /*
        // 지역변수 사용
        int i;
        int iTemp;
  6.     iTemp = g_iData1;
  7.     for( i = 0 ; i < 0x1FFFFFFF ; i++ )
        {
            iTemp = iTemp + 1;
        }
        g_iData1 = iTemp;
        return g_iData1;
        */
  8.     /*
        // 바로 접근
        int i;
  9.     for( i = 0 ; i < 0x1FFFFFFF ; i++ )
        {
            g_iData1 = g_iData1 + 1;
        }
        return g_iData1;
        */
        // False Sharing
        int i;
  10.     for( i = 0 ; i < 0x1FFFFFFF ; i++ )
        {
            g_stData.iData1 = g_stData.iData1 + 1;
        }
        return g_stData.iData1;
    }
     
    // 테스트 Thread 1
    DWORD CALLBACK TestThread2( void* pData )
    {
        /*
        // 지역변수 사용
        int i;
        int iTemp;
  11.     iTemp = g_iData2;
  12.     for( i = 0 ; i < 0x1FFFFFFF ; i++ )
        {
            iTemp = iTemp + 1;
        }
        g_iData2 = iTemp;
        return g_iData2;
        */
  13.     /*
        // False Sharing
        int i;
  14.     for( i = 0 ; i < 0x1FFFFFFF ; i++ )
        {
            g_iData2 = g_iData2 + 1;
        }
        return g_iData2;
        */
  15.     // False Sharing
        int i;
  16.     for( i = 0 ; i < 0x1FFFFFFF ; i++ )
        {
            g_stData.iData2 = g_stData.iData2 + 1;
        }
        return g_stData.iData2;
    }
     
    // 실제 메인 함수
    int main(int argc, char* argv[])
    {
        HANDLE vhThread[2];
        int i;
        DWORD dwStartTime;
        DWORD dwEndTime;
        DWORD dwTimeSum;
  17.     // Processor High Priority로 만든다.
        SetPriorityClass( GetCurrentProcess(), HIGH_PRIORITY_CLASS );
  18.     dwTimeSum = 0;
        printf( "Start Processing...\n" );
        for( i = 0 ; i < 20 ; i++ )
        {
            // Thread 생성
            vhThread[0] = CreateThread( NULL, 0, TestThread1, ( LPVOID ) 0, CREATE_SUSPENDED, NULL );
            vhThread[1] = CreateThread( NULL, 0, TestThread2, ( LPVOID ) 0, CREATE_SUSPENDED, NULL );
  19.         // Affinity 설정
            SetThreadAffinityMask( vhThread[ 0 ], 0x1 );
            SetThreadAffinityMask( vhThread[ 1 ], 0x1 );
  20.         // 카운팅 시작
            dwStartTime = GetTickCount();
            ResumeThread( vhThread[ 0 ] );
            ResumeThread( vhThread[ 1 ] );
  21.         // Thread 종료
            WaitForMultipleObjects( 2, vhThread, TRUE, INFINITE );
  22.         //결과 출력
            dwEndTime = GetTickCount() - dwStartTime;
            printf( "%d Time = %d ms\n", i + 1, dwEndTime );
            dwTimeSum += dwEndTime;
        }
  23.     printf( "Average Time = %0.2f\n", dwTimeSum / 20.0 );
        return 0;
    }

 아래는 위의 코드를 이용해서 각 옵션으로 테스트한 결과이다.

  • Global Variable에 직접 접근

    • Affinity 미사용

      • Average Time = 2841.40
      • Average Time = 2842.20
    • Affinity 1, 1 사용 - 하나의 Core에서만 동작시킴

      • Average Time = 2653.15
      • Average Time = 2651.55
    • Affinity 1, 2 사용 - 각각 Core에 할당해서 동작시킴

      • Average Time = 2842.95
      • Average Time = 2843.75
  • Cache Line 정렬을 통한 Avoid False Sharing

    • Affinity 미사용

      • Average Time = 1514.10
      • Average Time = 1514.80
    • Affinity 1, 1 사용 - 하나의 Core에서만 동작시킴

      • Average Time = 3043.75
      • Average Time = 3042.20
    • Affinity 1, 2 사용 - 각각 Core에 할당해서 동작시킴

      • Average Time = 1520.30
      • Average Time = 1519.55
  • Local Variable을 사용한 Hold Approach

    • Affinity 미사용

      • Average Time = 1330.50
      • Average Time = 1332.00
    • Affinity 1, 1 사용 - 하나의 Core에서만 동작시킴

      • Average Time = 2671.85
      • Average Time = 2672.65
    • Affinity 1, 2 사용 - 각각 Core에 할당해서 동작시킴

      • Average Time = 1335.95
      • Average Time = 1335.95

 

 위의 굵은 숫자는 가장 해당 파트에서 가장 빠른 수행 결과를 나타낸 것이다. 결과는 당연히 Local Variable(Hold Approach)을 사용하는 것이 가장 빠르게 나왔다. 첫번째 Global Variable에 직접 접근하는 결과에서는 두 Thread 모두 Core 0에 할당해서 수행한 결과가 제일 빨랐다. 이는 Thread 수행 시에 발생하는 Cache Evit/Load가 상당하기 때문에 발생한 결과인 듯 하다. 다른 테스트의 경우는 하나의 Core에 할당한 결과가 가장 느렸다.

 이 테스트에서 눈 여겨볼 것은 Avoid False Sharing 테스트이다. Cache Line 정렬을 사용한 것과 하지 않은 것의 차이가 거의 1.8 배의 속도 차이가 난다. 다시 말하면 Data Structure를 손보는 것 만으로도 MultiCore 환경에서의 Performance를 1.8배 향상 시킬 수 있는 것이다. Performance가 중요시되는 프로그램이라면 Cache Line 정렬은 충분히 고려해 볼만 하다.

 

4.결론

 Processor Affinity는 윈도우즈 환경에서 기대만큼 큰 효과를 발휘하지 못했지만, Cache Blocking과 Hold Aproach 및 Avoid False Sharing 기법은 충분히 효과적인 것으로 나타났다. Processor Affinity가 윈도우즈 환경에서 큰 효과를 발휘하지 못하는 것은 윈도우즈의 Scheduler가 Affinity를 어느정도 보장해주며, Wait Queue에서 대기하는 시간이 Cache Evit & Laod 시간보다 더 길기 때문인 것으로 추측된다.

 테스트 결과 Cache Blocking과 Avoid False Sharing은 성능 향상에 충분한 효과가 있는 것으로 나타났다. Cache Blocking과 Avoid Flase Sharing은 Data Structure와 밀접한 관련이 있으며, 이는 Data Structure를 Cache-Friendly 하게 설계하는 것이 Multiple Processor의 성능을 향상시키는 좋은 포인트임을 말해주는 것이다. 따라서 Multiple Processor System에서 Cache-Friendly한 Data Structure를 사용하면 프로그램의 성능을 일정 수준 이상 높일 수 있다.

 

5.마치면서...

 이미 우리는 Multiple Processor의 시대에 살고 있다. 프로그래머에게 또 하나의 장을 열어준 Multiple Processor System, Multiple Processor-Friendly한 Architecture로 프로그램의 Performance를 높이자.

 

이 글은 스프링노트에서 작성되었습니다.

+ Recent posts