디스크파티션과 파일시스템

디스크파티션과 파일시스템

2025년 6월 5일

파일시스템이 마운트 되기까지 #

이전에 BIOS의 MBR, UEFI의 GPT 방식의 디스크 파티션 방식을 정리한 적이 있다. 하나의 블록 디바이스 전체 공간을 여러개의 영역(파티션)으로 분할하여 사용목적에 따라 관리하기 쉽게 만든다.
하나의 파티션을 파일시스템 구조로 만들어 여러 방식으로 파일을 관리할 수 있도록 한다.

파티션을 나누는 이유는 목적에 따라 데이터를 나누기 위함도 있지만, 운영체제별로 다른 파일 관리 방법(파일시스템)을 사용하기 때문에 여러 운영체제를 지원하기 위함도 있다.


MBR + FAT32 #

MBR + FAT32 디스크 만들기 #

 1### 디스크 만들기
 2$ dd if=/dev/zero of=fake_disk.img bs=1M count=50
 3$ sudo parted fake_disk.img mklabel msdos
 4
 5###  파티션 만들기
 6## 512byte까지 MBR인데, 1MiB로 Alignment 권장
 7$ sudo parted fake_disk.img mkpart primary fat32 1MiB 40MiB
 8$ parted fake_disk.img print
 9Number  Start   End     Size    Type     File system  Flags
10 1      1049kB  41.9MB  40.9MB  primary               lba
11
12### 마운트해서 FAT32로 포맷하기
13$ sudo losetup -fP fake_disk.img
14$ sudo mkfs.fat -F 32 /dev/loop0p1    # 안되면 sudo losetup -l 로 확인
15$ sudo mount /dev/loop0p1 /mnt
16$ df -h /mnt
17Filesystem      Size  Used Avail Use% Mounted on
18/dev/loop0p1     39M   512   39M   1% /mnt

여기까지 만들고 확인하면 첫번째 파티션의 테이블의 정보가 담겨지고 00100000 부터는 fat32로 포맷팅된 것을 볼 수 있다.
흰색으로 블록한 영역이 첫번째 파티션테이블이고, 여기에서 첫번째 바이트가 0x00 으로 부팅 불가능한 파티션이라는 의미를 갖게되며 이후에 부팅 가능한 파티션으로 플래그를 세팅하면 0x80이 된다. bed17aa5-935f-4de2-942e-93e05fcee989


부트코드 디컴파일 #

부트코드를 nasm 으로 디컴파일해서 읽다보면 이해가 된다.
처음엔 0x7c00에 로드해서 실행되지만, 0x600으로 복사한뒤 실행한다.
이후 파티션 테이블을 읽어 0x80 으로 시작하는 파티션을 찾고 섹터단위로 읽어서 VBR을 0x7c00 으로 복사한 후 점프한다.

 1### install-mbr은 필요 없는 작업이지만, 좀 더 읽기 쉬워진다.
 2$ sudo apt-get install mbr
 3$ sudo install-mbr /dev/loop0
 4$ sudo dd if=fake_disk.img of=mbr.bin bs=512 count=1
 5$ objdump -D -b binary -m i8086 -M intel mbr.bin
 6   b:   be 00 7c                mov    si,0x7c00           # MBR을 0x600으로 자가복제
 7   e:   bf 00 06                mov    di,0x600
 8  11:   b9 00 01                mov    cx,0x100
 9  14:   f3 a5                   rep movs WORD PTR es:[di],WORD PTR ds:[si]
10  1b:   ea 20 06 00 00          jmp    0x0:0x620           # 자가복제된 코드로 점프
11  20:   80 3e b2 07 ff          cmp    BYTE PTR ds:0x7b2,0xff  # 여기로 점프됨
12
13  7e:   be be 07                mov    si,0x7be            # 파티션테이블 시작위치 (0x1be)
14  81:   b0 00                   mov    al,0x0
15  83:   b9 04 00                mov    cx,0x4              # 파티션테이블만큼 4번 반복
16  86:   80 3c 00                cmp    BYTE PTR [si],0x0   # bootable 플래그가 0인지 확인
17  89:   75 66                   jne    0xf1
18  8b:   fe c0                   inc    al
19  8d:   83 c6 10                add    si,0x10             # 다음 파티션테이블로 이동
20  90:   e2 f4                   loop   0x86
21
22  f1:   e8 83 00                call   0x177
23 117:   b4 41                   mov    ah,0x41             # EDD(디스크 확장 RW) 지원여부 확인
24 119:   bb aa 55                mov    bx,0x55aa
25 11c:   52                      push   dx
26 11d:   cd 13                   int    0x13
27 12a:   f6 c1 01                test   cl,0x1              # cl이 1이라면 지원. & 연산으로 확인
28 12d:   74 13                   je     0x142               # & 연산 결과가 zero 플래그 세팅되면 미지원
29 12f:   8b 44 08                mov    ax,WORD PTR [si+0x8]
30 132:   8b 5c 0a                mov    bx,WORD PTR [si+0xa]
31 135:   be 90 07                mov    si,0x790
32 138:   89 44 08                mov    WORD PTR [si+0x8],ax
33 13b:   89 5c 0a                mov    WORD PTR [si+0xa],bx
34 13e:   b4 42                   mov    ah,0x42             # AH=42h=함수번호(DAP)
35 140:   eb 0c                   jmp    0x14e
36 142:   8a 74 01                mov    dh,BYTE PTR [si+0x1]
37 145:   8b 4c 02                mov    cx,WORD PTR [si+0x2]
38 148:   b8 01 02                mov    ax,0x201            # AH=02h=함수번호(CHS), AL=1 sector
39 14b:   bb 00 7c                mov    bx,0x7c00           # destination 주소
40 14e:   50                      push   ax
41 14f:   c6 06 92 07 01          mov    BYTE PTR ds:0x792,0x1
42 154:   cd 13                   int    0x13                # 어쨌든 VBR을 0x7c00으로 복사
43
44 15f:   81 3e fe 7d 55 aa       cmp    WORD PTR ds:0x7dfe,0xaa55  # VBR 시그니쳐 확인
45 172:   ea 00 7c 00 00          jmp    0x0:0x7c00          # 대충 맞으면 VBR로 점프

지금 VBR은 아직 부팅가능한 파티션이 아니기 때문에 “This is not a bootable …” 을 출력하는 코드가 포함되어있다.

e571da58-d21d-4000-a2d5-fca04d401151


부팅가능한 파티션 설정 #

 1### 부트 플래그 설정
 2$ sudo fdisk /dev/loop0
 3Command (m for help): a
 4Selected partition 1
 5The bootable flag on partition 1 is enabled now.
 6
 7Command (m for help): w
 8The partition table has been altered.
 9Syncing disks.
10
11### DOS Bootloader 설치. 사실 FAT32는 33MB 이상 크기여야 인식된다. 
12$ sudo apt-get install syslinux
13$ sudo syslinux --install /dev/loop0p1
14$ ls /mnt
15ldlinux.c32  ldlinux.sys
16
17### 확인 및 마운트 해제
18$ sudo dd if=/dev/loop0p1 bs=512 count=1 of=vbr.bin
19$ sudo dd if=/dev/loop0 bs=512 count=1 of=mbr.bin
20$ objdump -D -b binary -m i8086 -M intel --start-address=0x7c5a --adjust-vma=0x7c00 vbr.bin
21$ objdump -D -b binary -m i8086 -M intel --start-address=0x8030 --adjust-vma=0x8000 /mnt/ldlinux.sys
22
23$ sudo umount /mnt
24$ sudo losetup -d /dev/loop0

확인해보면 FAT32 파티션 영역에 SYSLINUX가 설치된 것을 볼 수 있다. 맨앞은 VBR 영역이고, 점프한 이후엔 ldlinux.sys 를 디스크에서 찾아 실행시킨다.
여기까지는 아직 파일시스템을 해석하지 않고 디스크의 raw sector를 직접 읽어서 판단한다.

b145ce1e-a76d-4a20-a228-559884c7f902

코드를 분석해보면 대충 인터럽트를 통해 디스크를 섹터단위로 읽어서 ldlinux.sys를 메모리의 0x8000 주소에 로드 후 실행시키는 역할을 한다.

 1    7d01:       b4 41                   mov    ah,0x41
 2    7d03:       e8 cb 00                call   0x7dd1
 3    7d0e:       f6 c1 01                test   cl,0x1        ; 여기에서도 EDD가 지원되는지 확인
 4    7d11:       74 05                   je     0x7d18
 5    7d13:       c6 06 46 7d 00          mov    BYTE PTR ds:0x7d46,0x0    ; [7d46] = 0 세팅
 6    7d18:       66 b8 ef 04 00 00       mov    eax,0x4ef
 7    7d1e:       66 ba 00 00 00 00       mov    edx,0x0
 8    7d24:       bb 00 80                mov    bx,0x8000
 9    7d27:       e8 0e 00                call   0x7d38        ; CHS 방식으로 파티션의 섹터 성공할때까지 읽기 (int 13h)
10    7d2a:       66 81 3e 1c 80 dc 50    cmp    DWORD PTR ds:0x801c,0x5fa150dc     ; ldlinux.sys 의 0x1c 오프셋의 값을 비교
11    7d31:       a1 5f                                        ; 9byte짜리 명령이 해석이 잘못된 것이지만, 포함해도 해석이 달라지진 않음. 무시
12    7d33:       75 74                   jne    0x7da9        ; 읽어왔는데도 다르다면 에러 출력
13    7d35:       e9 f8 02                jmp    0x8030        ; ldlinux.sys 의 0x30 (entry) 으로 점프
14
15; 얘는 그냥 중간중간 호출하면서 0x13 인터럽트 호출해주는 함수이다. 
16    7dd1:       8a 16 74 7b             mov    dl,BYTE PTR ds:0x7b74   ; dl에는 드라이브인덱스를 넣어둬야함
17    7dd5:       06                      push   es
18    7dd6:       cd 13                   int    0x13                    ; 인터럽트로 드라이브의 특정섹터 읽기
19    7dd8:       07                      pop    es
20    7dd9:       c3                      ret

ldlinux.sys 의 코드 일부는 아래와 같다.

a13d977b-cc3f-4353-b962-c73e10bb7aed


전체적인 동작 흐름 #

  1. BIOS는 MBR 구조의 LBA 0인 boot code를 실행한다.
  2. boot code는 부팅가능한 파티션을 찾고 sector 0 으로 점프한다.
  3. VBR의 boot sector에는 디스크의 현재 파티션을 raw sector 형태로 ldlinux.sys(운영체제 로더)를 읽어 메모리에 로드하고 점프하는 코드가 있다.
    • 0x1c 시그니쳐를 읽어서 ldlinux.sys가 제대로 올라왔는지 확인한다.
  4. ldlinux.sys 의 엔트리가 실행되며 파일시스템을 파싱 후 읽고 syslinux.cfg 파일에 따라 커널(또는 다음스테이지)을 실행한다.
  5. 커널이 실행되며 initrd.img 를 메모리에 압축을 해제하여 임시 루트파일시스템(램 디스크) 으로 사용한다.
  6. 램디스크에 있는 /init 스크립트가 실행되며 switch_root 과정이 진행된다.
    • root= 인자로 전달한 파티션을 루트로 마운트하고 램디스크는 언마운트
    • 사실 이때부터 /boot 파티션은 필요없다.
    • /sbin/init 을 실행
  7. init프로세스가 새로 root에 마운트된 파일시스템에서 /etc/fstab 에 저장된 구조대로 기기나 파티션들이 마운트된다.

전반적인 동작 흐름은 부트 방식 마다 다를 수 있다. (syslinux, grub 등)

부팅 이후에는 사실 /boot 파티션이 필요 없기 때문에 별도의 파일시스템에 /boot를 빼두고 마운트하지 않게해서 유저가 볼 수 없도록 해도된다.
하지만 부팅할때 syslinux.cfg를 보고 부팅방식을 결정하기 때문에 유저가 마음대로 부팅할 수 있도록 하려면 /boot 에 마운트하고 부팅 설정을 수정할 수 있도록 하는게 좋다.

ff5157e6-300c-47c3-b9f1-70afc9bdc51a


NTFS에서 차이 #

사실 큰 차이는 없다. 파티션 내부의 구조는 다를지 몰라도 똑같이 sector 0 이 BootSector 영역이고, MBR의 boot code가 파티션의 이 코드를 로드하고 실행하는 것은 같다. cbcc8ce5-eaa9-464e-867c-a35e3dd9d752


GPT + FAT32 #

GPT + FAT32 디스크 만들기 #

 1$ dd if=/dev/zero of=fake_disk.img bs=1M count=50
 2$ sudo parted fake_disk.img mklabel msdos
 3$ sudo losetup -fP fake_disk.img
 4$ sudo parted /dev/loop0 --script mklabel gpt
 5
 6### 파티션 테이블에 ESP 파티션을 만든다.
 7$ sudo parted fake_disk.img mkpart ESP fat32 1MiB 40MiB
 8$ parted fake_disk.img print
 9Number  Start   End     Size    File system  Name  Flags
10 1      1049kB  41.9MB  40.9MB               ESP   msftdata
11
12### 첫번째 파티션에 EFI System Partition GUID 설정하고 파일시스템 생성
13$ sudo parted /dev/loop0 set 1 esp on
14$ sudo mkfs.vfat -F 32 -n ESP /dev/loop0p1
15
16$ sudo umount /mnt
17$ sudo losetup -d /dev/loop0

GPT는 UEFI에서 사용되며, 트릭을 사용하여 Protective MBR(LBA 0)에 부트코드를 집어넣으면 BIOS에서도 사용할 수 있지만 일반적인 방식은 아니다.

GPT에는 MBR처럼 부트코드가 따로 존재하지 않으며, 파티션이 생성될 때마다 파티션 엔트리에 GUID와 LBA위치, 파티션이름 등을 기록해두기만 한다.

부팅시에는 UEFI가 이미 GPT디스크 구조를 파싱하여 UEFI 실행환경을 마련해두기 때문에 파일시스템 파싱이 완료된 상황이며 일반적인 앱 실행처럼 OS부트로더(UEFI 어플리케이션)를 실행시켜 커널을로드한다.
그렇기 때문에 FAT32의 부트섹터는 파일시스템 정보만 읽고 실제코드는 실행하지 않는다.

85379c24-0398-4e53-9523-f03ff48d8e9e

fake_disk.img 에서 ESP 파티션 영역을 지정할때 1MiB 부터 40MiB 영역을 선택했기 때문에 0x00100000 영역부터 fat32 파티션이 된다.

324cb2e8-c293-459f-9524-3508fc6f456c


전체적인 동작 흐름 #

  1. 전원이 들어오면 BootROM의 UEFI 코드로 진입한다.
  2. UEFI의 부팅 과정을 따라 DXE 단계로 진입한다.
  3. 디스크 컨트롤러 드라이버를 로드하여 파일시스템을 사용할 수 있도록 초기화한다.
    • GPT 파티션 엔트리에서 ESP를 찾음
    • ESP 파티션의 파일시스템(FAT32)을 마운트
  4. DXE를 마무리하며 UEFI 어플리케이션 실행환경 세팅을 완료한다.
  5. ESP 파티션 내부의 \EFI\BOOT\bootx64.efi 어플리케이션을 실행
  6. bootx64.efi는 OS 부트로더이며, 커널이미지, 초기램디스크를 메모리에 로드하고 EntryPoint 호출
    • ESP만 마운트되어있는 상태이기 때문에 커널이미지가 다른 파티션에 있다면 부트로더에서 직접 마운트해야한다.

FAT32 구조 #

 1+-----------------------+ ← FAT32 sector 0 (0x00100000)
 2| Reserved Region       | Boot sector, FSInfo sector
 3+-----------------------+
 4| FAT Table 1           | 클러스터 체인 정보 (e.g. 2 → 5 → 8 …)
 5+-----------------------+
 6| FAT Table 2 (optional)| 백업용 FAT
 7+-----------------------+
 8| Data Region           | 실제 데이터가 저장되는 영역
 9| - Cluster #2          | 루트 디렉토리 시작
10| - Cluster #3          |
11| - Cluster #4          |
12| - ...                 |
13+-----------------------+

VBR (PBR) #

VBR은 PBR이라고도 부른다. 맨 앞에 점프코드가 있고 0x5a 오프셋 위치에 있는 boot sector 코드로 점프하게 된다.
점프코드와 boot sector 코드 사이에는 BIOS Parameter Block 영역이 있으며 파티션의 정보가 저장된다.

eb 58 90 명령은 JMP 0x5A; NOP 로 해석되지만, 캡쳐 이미지는 GPT 방식이라서 사실 의미없다.

347f68f9-4954-4c61-b470-525627c47434

d1f46d53-1d3f-434b-a7e3-abe2402a3806

  • Bytes Per Sector[0xb:] : 각 섹터당 크기를 의미한다. 보통 512byte, 4096byte가 많으며 이미지의 파티션은 0x200 = 512byte 로 설정되어 있다.
  • Sec Per Cluster[0xd:] : FAT는 클러스터 단위로 관리하며 클러스터당 섹터 수를 나타낸다. 1로 세팅되어 섹터로 관리되는것과 동일하다.
  • Total Sector 32[0x20:] : 현재 파티션의 블록 수를 의미한다. Total Sector 16과 이 필드 중 하나만 사용되며 하나는 0으로 설정된다. 파티션 엔트리에서 봤듯 0x800~0x13fff 영역을 사용하기 때문에 현재 파티션의 섹터 수는 0x13800이 맞다.
  • Root Directory Cluster[0x2c:] : 루트디렉터리가 어디에서 시작되는지 가리키는 클러스터 번호이다. FAT32는 디렉터리마다 클러스터범위가 체인 형태로 엮이는데 현재 파티션은 클러스터 2에서부터 루트 디렉터리의 데이터가 시작된다.

클러스터 번호는 0, 1이 FAT 관리용으로 예약되어 있기 때문에 Data Area 시작 부분이 2번 클러스터가 된다.
RootDirectory 클러스터 위치 (섹터) = Reserved Sec Count + (Num FATs * FAT Size 32) + ((Root Directory Cluster - 2) * SecPerClus) = 0x20 + (2 * 0x267) + 0 = 1262 sector


FAT Area #

이 영역은 디스크의 cluster 들이 어떻게 연결되어 있는지 기술된 테이블(배열)이다.
FAT32에서는 배열의 각 데이터가 4byte이며, FAT[클러스터번호] 로 연결된 다음 클러스터를 얻어올 수 있다.

현재 폴더 구조와 크기를 보면 디렉터리들은 512byte이고, jinx.jpg는 5812byte인 것을 볼 수 있다.
사실 디렉터리들도 각각의 파일로 취급되고, 담겨있는 파일 정보만 저장하고 있기 때문에 파일이 많아지지 않는다면 적은 크기가 유지된다.

4556b618-905e-4a23-9cec-d90f88850c77

그래서 루트 클러스터인 FAT[2] 를 접근하면 다음 클러스터가 필요 없기 때문에 0x0fffff8 (EOF) 를 표시하고 있고 FAT[7] 은 12개의 클러스터가 연결되어 있는데 jinx.jpg (5812byte) 파일이라는 것을 예상할 수 있다.

상위 4bit는 사용하지 않아서 0이며, EOC도 범위를 갖고있지만 표준은 0x0FFFFFF8 이상이면 EOC로 규정해두기만 했다.

값 (lower 28 bits) 의미
0x00000000 Free cluster (비어 있음)
0x00000002 ~ 0x0FFFFFEF Next cluster number
0x0FFFFFF8 ~ 0x0FFFFFFF End of chain (EOC) - 마지막 클러스터
0x0FFFFFF7 Bad cluster

Data Area #

각 Sector가 512byte이기 때문에 위에서 계산한대로 1262 sector 에 위치한 Root Cluster는 아래와 같이 덤프할 수 있다.
디스크전체(/dev/loop0)에서 덤프를 하려면 skip에 2048을 더해야한다.

4fd6244e-b758-49cf-949a-e160792916ff

jinx.jpg 를 의미하는 것으로 보이는 cluster 7 (1267 sector) 를 확인하면 아래와 같다. 실제 jinx.jpg 를 덤프해보면 값이 같은데 파일은 그냥 그대로 클러스터에 들어간다는 것을 의미한다.

a65c0150-8211-4d59-909e-aea7612595cf


Dictionary Entry #

FAT Area에서 말했듯 디렉터리도 그냥 내부의 파일에 대한 정보가 기록된 파일일 뿐이다.

루트 클러스터의 덤프를 보면 언뜻 파일 이름들이 보이는데 0x20 바이트 크기의 DirectoryEntry 배열 구조로 되어있고 파일명이 구조체에 포함되어있기 때문이다.

90059443-181b-4b88-b80b-d402b8a6f132

루트 클러스터의 첫번째 엔트리는 볼륨 라벨 슬롯인 경우도 있다.
파일명이 길어지는 경우 LFN(Long FileName)를 엔트리에 추가하여 파일명을 표현할 수 있는 구조로 되어있다.
각 슬롯의 맨 첫 바이트가 0xe5 값인 경우 해당 슬롯(파일 또는 LFN)은 삭제된 것을 의미한다.

DIR 구조체 #

이미지에서 노란색으로 표시되어 있으며 파일 정보가 담긴 구조체이다.

이름 오프셋 의미
DIR_NAME 0x0~0xa 11byte 크기의 짧은 파일이름(혹은 볼륨명)이다.
DIR_Attr 0xb 파일의 속성이 포함되며 READ_ONLY(0x1), HIDDEN(0x2), SYSTEM(0x4), VOLUME_ID(0x8), DIRECTORY(0x10), ARCHIVE(0x20), LFN(0xf) 로 표시한다.
DIR_NTRes 0xc Windows NT용 예약영역
DIR_CrtTimeTenth 0xd 파일 생성 시각의 밀리초부분
DIR_CrtTime 0xe~0xf 파일 생성 시각 (시, 분, 초)
DIR_CrtDate 0x10~0x11 파일 생성 날짜
DIR_LstAccDate 0x12~0x13 마지막 접근 날짜
DIR_FstClusHI 0x14~0x15 시작 클러스터의 상위 2byte
DIR_WrtTime 0x16~0x17 마지막 수정 시각 (시, 분, 초)
DIR_WrtDate 0x18~0x19 마지막 수정 날짜
DIR_FstClusLO 0x1a~0x1b 시작 클러스터의 하위 2byte
DIR_FileSize 0x1c~0x1f 파일의 크기

LFN (LongFileName) #

파란색으로 표시된 엔트리이며, LFN슬롯인 경우 DIR_Attr 위치가 0x0F로 세팅된다.
HelloWorld.data 의 LFN 슬롯을 보면 낮은 인덱스에 뒤에 위치한 문자열이 저장된다.

이름 오프셋 의미
Order 0x0 LFN 조각의 순서를 의미하며, 마지막 조각은 0x40을 더한다. ex) 마지막이면서 3번째인 슬롯은 0x43
NAME 1 0x1~0xa 파일명의 첫번째 조각 (5자리, UTF-16)
Attribute 0xb 항상 0x0F (LFM 임을 표시)
reserved 0xc reserved
checksum 0xd 파일명 체크섬
NAME 2 0xe~0x19 파일명의 두번째 조각 (6자리, UTF-16)
reserved 0x1a~0x1b reserved
NAME 3 0x1c~0x1f 파일명의 세번째 조각 (2자리, UTF-16)

FAT32 파일시스템 구현 #

UEFI에서 볼륨 읽어오기 #

UEFI 펌웨어에서는 이미 블록디바이스의 데이터를 읽어올 수 있는 기능이 포함되어 있다. 파티션화된 볼륨을 읽어오기 위해서는 Block I/O Protocol을 사용해서 부트서비스에 요청하면 된다.

UEFI에서 지원하는 함수로 큰 영역을 읽게되면 부팅 속도가 느려지기 때문에 직접 디스크 장치 드라이버를 만들어도 된다.

UefiMain 에서 부트로더가 저장된 파일시스템의 root_dir 에서 fat_disk 라는 파일이 있는 경우 그걸 볼륨이라고 생각하고 읽어와서 커널로 전달한다.

없는 경우 BlockIoProtocol을 이용해서 볼륨 전체에서 16MiB 만큼 가져오고 그걸 커널에게 전달한다.

 1VOID* volume_image;
 2
 3EFI_FILE_PROTOCOL* volume_file;
 4status = root_dir->Open(
 5  root_dir, &volume_file, L"\\fat_disk",
 6  EFI_FILE_MODE_READ, 0);
 7if (status == EFI_SUCCESS) {
 8  status = ReadFile(volume_file, &volume_image);
 9  _IfErrorHalt(L"failed to open Block I/O Protocol", status);
10} else {
11  EFI_BLOCK_IO_PROTOCOL* block_io;
12  // 부팅미디어에 연결된 block_io를 획득
13  status = OpenBlockIoProtocolForLoadedImage(image_handle, &block_io);
14  _IfErrorHalt(L"failed to open Block I/O Protocol", status);
15
16  // media 에는 프로토콜과 연결된 블록디바이스의 정보가 담긴다. 
17  // 블록당 바이트 수와 블록번호를 이용해서 블록디바이스의 크기를 알아낸다. 
18  EFI_BLOCK_IO_MEDIA* media = block_io->Media;
19  UINTN volume_bytes = (UINTN)media->BlockSize * (media->LastBlock + 1);
20  if (volume_bytes > 16 * 1024 * 1024) {
21    volume_bytes = 16 * 1024 * 1024;
22  }
23
24  Print(L"Reading &lu bytes (Present %d, BlockSize %u, Lastblock %u)\n",
25    volume_bytes, media->MediaPresent, media->BlockSize, media->LastBlock);
26  // 모든 블록을 읽어와서 volume_image에 저장한다. 
27  status = ReadBlocks(block_io, media->MediaId, volume_bytes, &volume_image);
28  _IfErrorHalt(L"failed to read blocks", status);
29}
30
31// 커널에 volume_image(볼륨을 읽어온 바이트버퍼) 를 전달함
32typedef void EntryPointType(const struct FrameBufferConfig*,
33                            const struct MemoryMap*,
34                            const VOID*,
35                            VOID*
36                          );
37EntryPointType* entry_point = (EntryPointType*)entry_addr;
38entry_point(&config, &memmap, acpi_table, volume_image);

ReadFile 함수 #

파일 정보를 얻어오고 파일 크기만큼 메모리를 할당해서 읽어온다.

 1EFI_STATUS ReadFile(EFI_FILE_PROTOCOL* file, VOID** buffer) {
 2  EFI_STATUS status;
 3
 4  UINTN file_info_size = sizeof(EFI_FILE_INFO) + sizeof(CHAR16) * 12;
 5  UINT8 file_info_buffer[file_info_size];
 6  status = file->GetInfo(
 7    file, &gEfiFileInfoGuid,
 8    &file_info_size, file_info_buffer);
 9  if (EFI_ERROR(status)) {
10    return status;
11  }
12
13  EFI_FILE_INFO* file_info = (EFI_FILE_INFO*)file_info_buffer;
14  UINTN file_size = file_info->FileSize;
15
16  status = gBS->AllocatePool(EfiLoaderData, file_size, buffer);
17  if (EFI_ERROR(status)) {
18    return status;
19  }
20  return file->Read(file, &file_size, *buffer);
21}

OpenBlockIoProtocolForLoadedImage #

 1EFI_STATUS OpenBlockIoProtocolForLoadedImage(
 2    EFI_HANDLE image_handle, EFI_BLOCK_IO_PROTOCOL** block_io) {
 3  EFI_STATUS  status;
 4  EFI_LOADED_IMAGE_PROTOCOL* loaded_image;
 5
 6  // 메인에 전달된 image_handle 을 이용해서 현재 실행중인 부트로더(BOOTX64.EFI)의 정보를 획득한다.
 7  status = gBS->OpenProtocol(
 8    image_handle,
 9    &gEfiLoadedImageProtocolGuid,
10    (VOID**)&loaded_image,
11    image_handle,
12    NULL,
13    EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
14  if (EFI_ERROR(status)) {
15    return status;
16  }
17
18  // 부트로더가 저장된 기억장치에 대한 DeviceHandle을 이용해서 Block IO Protocol을 획득
19  status = gBS->OpenProtocol(
20    loaded_image->DeviceHandle,
21    &gEfiBlockIoProtocolGuid,
22    (VOID**)block_io,
23    image_handle,
24    NULL,
25    EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
26  return status;
27}

ReadBlocks #

동적으로 할당한 메모리 버퍼 공간에 Block Io Protocol로 블록을 그대로 읽어온다.

 1EFI_STATUS ReadBlocks(
 2    EFI_BLOCK_IO_PROTOCOL* block_io, UINT32 media_id,
 3    UINTN read_bytes, VOID** buffer) {
 4  EFI_STATUS status;
 5
 6  status = gBS->AllocatePool(EfiLoaderData, read_bytes, buffer);
 7  if (EFI_ERROR(status)) {
 8    return status;
 9  }
10
11  status = block_io->ReadBlocks(
12    block_io,
13    media_id,
14    0,
15    read_bytes,
16    *buffer);
17  return status;
18}

디렉터리 파싱 #

위에서 공부했던 내용들을 하나씩 구현하면 볼륨을 읽어서 디렉터리 내부 데이터를 읽어올 수 있다.

구조체 #

BPB는 그냥 부트섹터의 구조를 담았고, Attribute는 Directory Entry의 DIR_Attr 을 지정할 플래그 enum 값이다.

 1struct BPB {
 2  uint8_t jump_boot[3];
 3  char oem_name[8];
 4  uint16_t bytes_per_sector;
 5  uint8_t sectors_per_cluster;
 6  uint16_t reserved_sector_count;
 7  uint8_t num_fats;
 8  uint16_t root_entry_count;
 9  // ...
10} __attribute__((packed));
11
12enum class Attribute : uint8_t {
13  kReadOnly  = 0x01,
14  kHidden    = 0x02,
15  kSystem    = 0x04,
16  kVolumeID  = 0x08,
17  kDirectory = 0x10,
18  kArchive   = 0x20,
19  kLongName  = 0x0f,
20};
21
22struct DirectoryEntry {
23  unsigned char name[11];
24  Attribute attr;
25  uint8_t ntres;
26  uint8_t create_time_tenth;
27  // ...
28  uint16_t first_cluster_low;
29  uint32_t file_size;
30
31  // 현재 엔트리의 파일이 실제로 저장된 시작 클러스터
32  uint32_t FirstCluster() const {
33    return first_cluster_low |
34      (static_cast<uint32_t>(first_cluster_high) << 16);
35  }
36} __attribute__((packed));

유틸함수 #

 1// 이 함수는 SFN을 기준으로 작성된 코드이다. 
 2void ReadName(const DirectoryEntry& entry, char* base, char* ext) {
 3  memcpy(base, &entry.name[0], 8);
 4  base[8] = 0;
 5  for (int i = 7; i >= 0 && base[i] == 0x20; --i) {
 6    base[i] = 0;
 7  }
 8
 9  memcpy(ext, &entry.name[8], 3);
10  ext[3] = 0;
11  for (int i = 2; i >= 0 && ext[i] == 0x20; --i) {
12    ext[i] = 0;
13  }
14}
15
16// 섹터의 주소를 가져오는 함수인데, 템플릿으로 만들어져 있다. 
17// 해당 섹터가 Directory Entry 배열일 수도 있고, 파일이 들어있을수도 있기 때문이다.
18template <class T>
19T* GetSectorByCluster(unsigned long cluster) {
20  return reinterpret_cast<T*>(GetClusterAddr(cluster));
21}
22
23// 클러스터 계산하는 방식대로 계산하고 실제 주소니까 바이트만큼 곱해주면 된다.
24// boot_volume_image 는 UEFI에서 할당받은 주소라서 메모리상 베이스주소를 더해줘야한다. 
25uintptr_t GetClusterAddr(unsigned long cluster) {
26  unsigned long sector_num =
27    boot_volume_image->reserved_sector_count +
28    boot_volume_image->num_fats * boot_volume_image->fat_size_32 +
29    (cluster - 2) * boot_volume_image->sectors_per_cluster;
30  uintptr_t offset = sector_num * boot_volume_image->bytes_per_sector;
31  return reinterpret_cast<uintptr_t>(boot_volume_image) + offset;
32}

ls 커맨드 #

 1  } else if (strcmp(command, "ls") == 0) {
 2    // root 클러스터에서 디렉터리 엔트리를 가져온다. 
 3    auto root_dir_entries = fat::GetSectorByCluster<fat::DirectoryEntry>(
 4        fat::boot_volume_image->root_cluster);
 5    // 클러스터 당 엔트리배열 수를 가져온다. 
 6    auto entries_per_cluster =
 7       fat::boot_volume_image->bytes_per_sector / sizeof(fat::DirectoryEntry)
 8       * fat::boot_volume_image->sectors_per_cluster;
 9
10    char base[9], ext[4];
11    char s[64];
12    // 전체 엔트리 순회 
13    for (int i = 0; i < entries_per_cluster; ++i) {
14      // SFN을 읽어서 base, ext에 넣는다. 
15      ReadName(root_dir_entries[i], base, ext);
16      // 0x00: 저장되지 않음, 0xe5: 삭제됨, LFN 엔트리 제외
17      if (base[0] == 0x00) {
18        break;
19      } else if (static_cast<uint8_t>(base[0]) == 0xe5) {
20        continue;
21      } else if (root_dir_entries[i].attr == fat::Attribute::kLongName) {
22        continue;
23      }
24
25      // 확장자가 있다면 base.ext 형태로 출력 
26      if (ext[0]) {
27        sprintf(s, "%s.%s\n", base, ext);
28      } else {
29        sprintf(s, "%s\n", base);
30      }
31      Print(s);
32    }

파일 읽기 #

이것도 그냥 위에서 공부했던 내용을 구현하면 된다. 코드는 사실 의미가 없기 때문에 파일 읽는 방법을 따라가보자.
이 운영체제에서는 disk.img 파일이 에뮬레이터의 디스크 드라이브로 사용된다.

5dc89306-82f3-473e-af5a-31c339c61e3b

disk.img 를 덤프해보면 시작부터 FAT 포맷으로 보이는 점프코드가 반겨준다. 그리고 강조한 오프셋을 이용하여 루트 클러스터의 위치를 찾을 수 있다.

0x20 + (0x634*2) + (0x2-0x2)*0x2 = 0xC88 = 3208 sector

244995e7-114e-40d0-8430-1add41a4fa3f

각 섹터는 512byte이기 때문에 3208개의 섹터를 스킵하고 덤프해보면 루트 디렉터리의 엔트리배열이 보인다.
MEMMAP 파일을 읽어보고 싶기 때문에 해당되는 엔트리의 클러스터 위치를 확인하면 된다.

cat으로 전달한 인자와 같은 이름의 엔트리를 가져와서 섹터정보를 꺼내오면 파일의 위치를 알 수 있다.

(0x225(cluster) - 2) * 2(sectors_per_cluster) + data_area의 섹터(루트클러스터)
= 1094(0x446) + 3208 = 4302 sector

90a12a37-0f33-460d-b7ff-3c4a75508ebe

이렇게 읽고나면 MEMMAP의 시작 클러스터를 찾을 수 있다.

0dcdabfa-8db7-429c-9f52-04c1693a3b61

추가로 fat 클러스터 체인도 살펴보면 총 6개의 클러스터로 만들어진 것을 알 수 있다.

32(reserved) + 0x225 / (0x200/4) = 32 + 4.28
= 36개를 스킵하고 출력하면 0x225 클러스터의 fat를 알 수 있다.

95c93d2d-539b-4e2d-9b79-fe0db0cb01f3

이렇게 cat 명령을 구현하면 파일을 읽을 수 있게 된다.

fe908458-978e-4ce3-95d8-3df5fd59974d

comments powered by Disqus