ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 커널 이미지
    프로젝트/운영체제 만들기 2023. 5. 31. 20:45

    글의 참고

    - https://linuxhint.com/combine-binary-files-linux/ 


    글의 전제

    - 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다.

    - 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다.


    글의 내용

    - Overview

    : 커널 이미지를 만드는 방법은 2가지가 있다.

    1" 플랫 바이너리를 단순히 붙어서 커널 이미지를 만든다.
    2" 링커를 통해 커널 이미지를 만든다.

     

     

    : CAT 명령어를 통한 커널 이미지 만들기

    " 첫 번째 방법부터 알아보자. YohdaOS는 커널 이미지를 BIN 파일 포맷을 사용한다. 이 말은 YohdaOS의 커널 이미지는 플랫 바이너리라는 뜻이다. 그래서 부트 로더(16비트), 32비트 커널, 64비트 커널을 모두 그냥 단순히 합쳐서 커널 이미지를 만든다. 어떻게 합칠까? cat 명령어를 쓰면 된다.

    $ cat f1.bin f2.bin f3.bin > f4.bin

    " f1.bin, f2.bin, f3.bin이 합쳐져서 f4.bin 이라는 새로운 파일이 생성된다. 배치의 순서는 cat 명령어의 나열된 파라미터 순서와 같다. 즉, f1.bin이 제일 앞에 배치되고 f3.bin이 제일 뒤에 배치된다. 그래서 f4.bin을 QEMU에서 실행하면 어떤 순서로 해당 파일들이 실행될까? 합쳐진 순서대로 실행된다. f1.bin이 제일 먼저 실행되고, f1.bin이 다 끝나면 바로 뒤에 f2.bin이 자동으로 실행된다. 앞에 파일이 끝나면 자동으로 실행이 되니, 점프 명령어로 굳이 이동할 필요가 없다. 대신 각 파일의 맨 앞은 텍스트 섹션이 배치되어야 하고, 텍스트 섹션의 맨 앞에는 각 파일의 엔트리 포인트가 존재해야 한다. 왜 텍스트 섹션이 맨 앞에 와야하고, 그 중에서 엔트리 포인트가 맨 앞에 와야 할까? 

     

    " 텍스트 섹션은 코드 섹션이다. 코드를 실행하려면 코드가 나와야 한다. 텍스트 섹션이 아닌 실행이 가능한 섹션은 없다. 텍스트 섹션이 아닌 다른 섹션이 파일의 맨 앞에 오면, PC는 해당 영역을 텍스트 영역이라 생각하고 실행을 하는데 실제로 이 값은 텍스트 섹션이 아니므로 결국 에러가 발생할 것이다. 그래서 텍스트 섹션이 제일 앞에 와야한다.

     

    " 그러면 엔트리 포인트가 왜 맨 앞이어야 할까? CAT 명령을 통해 파일을 단순하게 붙히는 작업은 파일의 순서만 중요한게 아니라, 각 파일의 코드 위치도 중요하다. 엔트리 포인트가 파일의 맨 앞에 있다는 것은 파일의 순서가 올바르다는 것을 전제한다. 왜냐면, 파일을 BIN 파일로 빌드하면 코드를 작성한 그대로 바이너리가 나온다. 작성한 코드의 위치가 바이너리에 그대로 대응된다. 우리는 코드를 작성할 때, 위에서 아래로 작성한다. 즉, 시작을 제일 위에서 한다는 말이다. 그러므로, 각 파일의 엔트리 포인트는 당연히 제일 앞에 있어야 한다. 

     

    " 또 중요한 사항은 데이터를 선언하는 코드가 코드 흐름에 있으면 절대 안된다. 데이터를 선언하는 코드를 디스어셈블 해보면 코드 흐름과는 값이 상당히 다르다. 그래서 이상한데로 점프할 수 있다. 그러니, 데이터 선언하는 부분은 코드 흐름이 닿지 않는 가장 뒷쪽에 배치하고, 데이터 영역에 코드 흐름이 닿지 않게, 개발자는 주의해서 코드를 짜야한다.   

     

     

    : 링커를 통한 커널 이미지 만들기

    " 두 번째는 링커를 통해 커널 이미지를 만들 수 있다. 가장 간단한 방법에 대해 논한다. 먼저 각 파일에 텍스트 섹션만 만든다. 그리고 링커 스크립트를 통해 텍스트 섹션안에 각각의 파일을 원하는 순서대로 배치한다. 이 때, 각 파일을 텍스트 섹션을 메모리상의 어디에 배치시킬지 정할 수가 있다. 링커를 통해 만들면, 기본적으로 플랫 바이너리가 아니다. 그래서 플랫 바이너리로 만들기 위해 OBJCOPY 명령어로 순수 바이너리(데이터와 코드)만 추출한다.

     

    " 링커를 쓰면 몇 가지 이점이 있다.

    1" 심볼을 공유할 수 있다.
    2" 디버깅이 편해진다.
    3" 섹션 배치를 유연하게 할 수 있다.

     

    - 커널 이미지 위치

    : 커널 이미지를 만들었다. 커널 이미지 파일은 기본적으로 디스크에 저장되어 있다. 부팅이 진행이되면, 디스크에서 커널 이미지를 읽어서 RAM에 로딩해야 한다.

     

    : 최초 부팅부터 생각해보면, 일단 BIOS가 저 이미지의 맨 앞 512B를 0x7C00에 로딩하고 실행시켜 준다. 그렇면, 이때부터 제어권이 우리에게 넘어온다. 그럼 우리가 해야 할 일은 BIOS로 부터 넘겨받은 실행 권한으로 512B 뒤쪽에 실행될 바이너리 이미지를 디스크에서 불러와서 램에 특정 위치에 로딩시켜야 한다. 이 위치는 어디가 좋을까? 이 때, 메모리 맵을 확인해봐야 한다.

     

     

    - 부트 로더 이미지와 커널 이미지를 나누는 이유

    : 만약, 부트 로더와 커널을 하나로 합칠 경우 아래와 같이 링커 스크립트가 작성된다.

    SECTIONS
    {
        .text 0x7C00 :
        {               
        	fbl.o (.text)
            sbl.o (.text)
        	. = 0x100000;
            pbl.o (.text)   
            main.o (.text)
            *(.text)
        }   
    
        .rodata :
        {   
            *(.rodata)
        }   
    
        .data :
        {   
            *(.data)
        }
    
        .bss :
        {   
            *(.bss)
            *(COMMON)
        } 
    }

    : 위의 링커 스크립트는 굉장히 큰 크기의 파일을 만들어 낸다. fbl.o은 512B로 고정이다. sbl.o은 기껏해야 16KB 라고 한다면, pbl.o 텍스트 코드가 등장하는 0x100000까지 0xF4400(0x100000 − (0x7c00 + 16⋅1024)) 만큼의 패딩이 들어간다. 이건 비효율적이다. 링커 스크립트는 하나의 실행 파일을 만들어 내기 때문에 저렇게 중간에 패딩이 크게 있으면 파일 사이즈만 커질 뿐이다. 

     

    : 그리고 문제가 하나 더 있을 수 있다. 16비트에서 32비트로 점프 시, 0x100000 점프는 링커에서 인식을 못할 수 있다. 0x100000는 16비트 세그먼트 방식에서는 오프셋으로 처리가 불가능한 범위다. 그러나 사실 이 부분은 테크닉적으로 해결이 가능하다.

     

    : 만약, 32비트 커널을 0x100000에 올리지 않고 뒤쪽에 붙히면 어떨까? 예를 들어, 중간에 `. = 0x100000`을 없애는 것이다. 당연히 가능하다. 그러면 위처럼 파일 사이즈가 쓸데 없이 커지는 문제가 사라진다. 근데 시나리오를 고려해서 그렇게 만들면 안된다. 왜냐면, YohdaOS는 멀티 부팅을 지원하기 위해서 GRUB도 지원할 것이다. 그런데, GRUB은 사용자의 커널의 엔트리 포인트를 0x100000에 로드하도록 강제한다. 그래서 YohdaOS도 0x100000에 로드하는 것이다.  

     

    : 그래도 YohdaOS는 아래와 같이 16비트 부트 로더와 32비트 커널 링크 스크립트를 아래와 같이 나눴다. 결국, 링커 스크립트가 2개이니 실행 파일도 2개(부트 로더, 커널)가 나오는 셈이다.

    SECTIONS                                                            
    {
        .text 0x7C00 :
        {   
            fbl.o (.text)
            sbl.o (.text)
            *(.text)
            *(.rodata)
        }   

        .data :
        {   
            *(.data)
        }   

        .bss :
        {   
            *(.bss)
            *(COMMON)
        }   
    }
    SECTIONS
    {
        .text 0x100000 :
        {                                                                                                                  
            pbl.o (.text)   
            main.o (.text)
            *(.text)
        }   

        .rodata :
        {   
            *(.rodata)
        }   

        .data :
        {   
            *(.data)
        }

        .bss :
        {   
            *(.bss)
            *(COMMON)
        }
    }

    : 부트 로더는 x86 크로스 컴파일러 링커에 의해서 0x7C00을 기반으로 명령어들이 생성된다. 컴파일러 자체는 소스를 기계어로 바꿔주는 역활만 한다. 실제 기계 명령어의 배치는 링커에 의해 진행된다. 커널 소스는 0x100000 주소 기반으로 기계 명령어가 배치된다. 이렇게 각각 만들면 2개의 파일 사이에 패딩이 필요없다. 왜냐면, 하나의 파일로 만들어지는게 아니기 때문에, 딱 자신들의 파일 사이즈에 맞게만 생성되는 것이다. 그리고 이 2개를 단순히 합치면 된다. 물론, 전제는 2개의 파일 모두 OBJCOPY를 통해서 순수 데이터와 명령어만 뽑아내야 한다. 그리고 CAT 명령어를 통해 붙히면 된다. CAT 명령어를 통해 붙이는 작업은 이 글 맨위에 존재한다.

     

    : 대신 부트 로더에서는 아래와 같이 커널의 엔트리 포인트 주소(0x100000)를 직접 지정해야 한다. 하나의 파일로 합쳤을 때는, 링커에 의해 파일이 합쳐질 때, 다른 파일들의 전역 심볼을 `extern` 키워드를 통해 파일내에서 사용할 수 있었다. 그러나, 위와 같이 링커스크립트가 따로 작성되면 2개의 파일의 심볼은 전혀 관련이 없게된다. 그래서 직접 주소로 점프를 해야 한다.

        ...
    _pmode: 
        mov eax, cr0 
        or al, 1
        mov cr0, eax    
        
        jmp 0x08:_penter

    [BITS 32] 
    _penter:
        jmp 0x100000;
        ...

    '프로젝트 > 운영체제 만들기' 카테고리의 다른 글

    GIT 명령어  (1) 2023.06.01
    메모리 맵  (0) 2023.06.01
    페이징  (0) 2023.05.30
    PIT [작성중]  (0) 2023.05.29
    [운영체제 만들기] Exception  (0) 2023.05.25
Designed by Tistory.