디스크파티션과 파일시스템
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이 된다.
부트코드 디컴파일 #
부트코드를 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 …” 을 출력하는 코드가 포함되어있다.
부팅가능한 파티션 설정 #
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를 직접 읽어서 판단한다.
코드를 분석해보면 대충 인터럽트를 통해 디스크를 섹터단위로 읽어서 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 의 코드 일부는 아래와 같다.
전체적인 동작 흐름 #
- BIOS는 MBR 구조의 LBA 0인 boot code를 실행한다.
- boot code는 부팅가능한 파티션을 찾고 sector 0 으로 점프한다.
- VBR의 boot sector에는 디스크의 현재 파티션을 raw sector 형태로 ldlinux.sys(운영체제 로더)를 읽어 메모리에 로드하고 점프하는 코드가 있다.
- 0x1c 시그니쳐를 읽어서 ldlinux.sys가 제대로 올라왔는지 확인한다.
- ldlinux.sys 의 엔트리가 실행되며 파일시스템을 파싱 후 읽고
syslinux.cfg파일에 따라 커널(또는 다음스테이지)을 실행한다. - 커널이 실행되며 initrd.img 를 메모리에 압축을 해제하여 임시 루트파일시스템(램 디스크) 으로 사용한다.
- 램디스크에 있는 /init 스크립트가 실행되며 switch_root 과정이 진행된다.
- root= 인자로 전달한 파티션을 루트로 마운트하고 램디스크는 언마운트
- 사실 이때부터 /boot 파티션은 필요없다.
- /sbin/init 을 실행
- init프로세스가 새로 root에 마운트된 파일시스템에서 /etc/fstab 에 저장된 구조대로 기기나 파티션들이 마운트된다.
전반적인 동작 흐름은 부트 방식 마다 다를 수 있다. (syslinux, grub 등)
부팅 이후에는 사실 /boot 파티션이 필요 없기 때문에 별도의 파일시스템에 /boot를 빼두고 마운트하지 않게해서 유저가 볼 수 없도록 해도된다.
하지만 부팅할때 syslinux.cfg를 보고 부팅방식을 결정하기 때문에 유저가 마음대로 부팅할 수 있도록 하려면 /boot 에 마운트하고 부팅 설정을 수정할 수 있도록 하는게 좋다.
NTFS에서 차이 #
사실 큰 차이는 없다. 파티션 내부의 구조는 다를지 몰라도 똑같이 sector 0 이 BootSector 영역이고, MBR의 boot code가 파티션의 이 코드를 로드하고 실행하는 것은 같다.
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의 부트섹터는 파일시스템 정보만 읽고 실제코드는 실행하지 않는다.
fake_disk.img 에서 ESP 파티션 영역을 지정할때 1MiB 부터 40MiB 영역을 선택했기 때문에 0x00100000 영역부터 fat32 파티션이 된다.
전체적인 동작 흐름 #
- 전원이 들어오면 BootROM의 UEFI 코드로 진입한다.
- UEFI의 부팅 과정을 따라 DXE 단계로 진입한다.
- 디스크 컨트롤러 드라이버를 로드하여 파일시스템을 사용할 수 있도록 초기화한다.
- GPT 파티션 엔트리에서 ESP를 찾음
- ESP 파티션의 파일시스템(FAT32)을 마운트
- DXE를 마무리하며 UEFI 어플리케이션 실행환경 세팅을 완료한다.
- ESP 파티션 내부의
\EFI\BOOT\bootx64.efi어플리케이션을 실행 - 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 방식이라서 사실 의미없다.
- 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인 것을 볼 수 있다.
사실 디렉터리들도 각각의 파일로 취급되고, 담겨있는 파일 정보만 저장하고 있기 때문에 파일이 많아지지 않는다면 적은 크기가 유지된다.
그래서 루트 클러스터인 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을 더해야한다.
jinx.jpg 를 의미하는 것으로 보이는 cluster 7 (1267 sector) 를 확인하면 아래와 같다. 실제 jinx.jpg 를 덤프해보면 값이 같은데 파일은 그냥 그대로 클러스터에 들어간다는 것을 의미한다.
Dictionary Entry #
FAT Area에서 말했듯 디렉터리도 그냥 내부의 파일에 대한 정보가 기록된 파일일 뿐이다.
루트 클러스터의 덤프를 보면 언뜻 파일 이름들이 보이는데 0x20 바이트 크기의 DirectoryEntry 배열 구조로 되어있고 파일명이 구조체에 포함되어있기 때문이다.
루트 클러스터의 첫번째 엔트리는 볼륨 라벨 슬롯인 경우도 있다.
파일명이 길어지는 경우 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 파일이 에뮬레이터의 디스크 드라이브로 사용된다.
disk.img 를 덤프해보면 시작부터 FAT 포맷으로 보이는 점프코드가 반겨준다. 그리고 강조한 오프셋을 이용하여 루트 클러스터의 위치를 찾을 수 있다.
0x20 + (0x634*2) + (0x2-0x2)*0x2 = 0xC88 = 3208 sector
각 섹터는 512byte이기 때문에 3208개의 섹터를 스킵하고 덤프해보면 루트 디렉터리의 엔트리배열이 보인다.
MEMMAP 파일을 읽어보고 싶기 때문에 해당되는 엔트리의 클러스터 위치를 확인하면 된다.
cat으로 전달한 인자와 같은 이름의 엔트리를 가져와서 섹터정보를 꺼내오면 파일의 위치를 알 수 있다.
(0x225(cluster) - 2) * 2(sectors_per_cluster) + data_area의 섹터(루트클러스터)
= 1094(0x446) + 3208 = 4302 sector
이렇게 읽고나면 MEMMAP의 시작 클러스터를 찾을 수 있다.
추가로 fat 클러스터 체인도 살펴보면 총 6개의 클러스터로 만들어진 것을 알 수 있다.
32(reserved) + 0x225 / (0x200/4) = 32 + 4.28
= 36개를 스킵하고 출력하면 0x225 클러스터의 fat를 알 수 있다.
이렇게 cat 명령을 구현하면 파일을 읽을 수 있게 된다.