메모리 맵과 파일 쓰기
ref
https://blog.csdn.net/xiaopangzi313/article/details/109928878
https://blog.csdn.net/qq_44807736/article/details/142060638
메모리맵
메모리 주소에 어떤것이 매핑되어 있는지를 알 수 있는 테이블? 이다.
예를들어 안드로이드 프로세스 메모리맵은 가상메모리주소+파일+권한 이 매핑되어 있다.
UEFI에서 말하는 메모리맵은 실제 물리 메모리의 어떤 주소들이 어떤 역할을 하고있는지 정해져 있으며, 시작 오프셋(PhysicalStart), 메모리의 타입(Type), 페이지 수(NumberOfPages), 메모리 속성(Attribute) 이 포함된다.
GetMemoryMap API
typedef struct {
UINT32 Type;
EFI_PHYSICAL_ADDRESS PhysicalStart;
EFI_VIRTUAL_ADDRESS VirtualStart;
UINT64 NumberOfPages;
UINT64 Attribute;
} EFI_MEMORY_DESCRIPTOR;
EFI_STATUS GetMemoryMap(
IN OUT UINTN *MemoryMapSize,
IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap,
OUT UINTN *MapKey,
OUT UINTN *DescriptorSize,
OUT UINT32 *DescriptorVersion);
- Return
- EFI_SUCCESS: 메모리맵을 정상적으로 획득한 경우
- EFI_BUFFER_TOO_SMALL: 메모리 영역이 너무 작은 경우
- MemoryMapSize: 메모리맵을 저장할 메모리 영역의 크기를 전달하며, 함수가 정상반환되면 실제 리턴된 메모리맵의 사이즈가 저장된다.
- MemoryMap: 메모리맵을 저장할 메모리의 시작주소를 전달하면, 반환될 때 실제 데이터가 쓰여진다.
- MapKey: 메모리맵의 id 값을 말하며 메모리맵은 UEFI 실행도중 계속 메모리 레이아웃이 변하는데 레이아웃이 변하지 않았음을 식별하기 위한 id 값이 반환된다. 나중에
gBS->ExitBootServices를 호출할때도 사용된다. - DescriptorSize: 실제 물리메모리에 로드된 메모리 디스크립터 구조체의 크기를 말하는데, UEFI 펌웨어가 정렬 등 설정이나 버전에 따라 패딩을 포함시킬 수도 있어서 UEFI 컴파일러가 계산하는 소스코드 상 구조체 크기인
sizeof(EFI_MEMORY_DESCRIPTOR)와 다를 수 있다. - DescriptorVersion: 메모리 디스크립터 구조체의 버전을 리턴해준다.
예제코드
UEFI에는 부트서비스와 런타임서비스로 구성되는데 메모리 관련 서비스는 부트서비스이기 때문에 gBS 글로벌 변수를 사용해서 함수를 호출해야한다.
struct MemoryMap {
UINTN buffer_size; // 메모리 맵을 저장할 버퍼 크기
VOID* buffer; // 메모리 맵이 저장될 버퍼 포인터
UINTN map_size; // 실제 메모리 맵 크기
UINTN map_key; // 메모리 맵의 고유 키 (ExitBootServices에서 필요)
UINTN descriptor_size; // 각 메모리 디스크립터의 크기
UINT32 descriptor_version; // 디스크립터 버전
};
EFI_STATUS GetMemoryMap(struct MemoryMap* map) {
if (map->buffer == NULL) {
return EFI_BUFFER_TOO_SMALL;
}
map->map_size = map->buffer_size;
return gBS->GetMemoryMap(
&map->map_size, // IN OUT UINTN
(EFI_MEMORY_DESCRIPTOR*)map->buffer, // IN OUT EFI_MEMORY_DESCRIPTOR
&map->map_key, // OUT UINTN
&map->descriptor_size, // OUT UINTN
&map->descriptor_version); // OUT UINT32
}
EFI_STATUS EFIAPI UefiMain(
EFI_HANDLE image_handle,
EFI_SYSTEM_TABLE* system_table) {
Print(L"Hello, Mikan World!\n");
CHAR8 memmap_buf[4096 * 4];
struct MemoryMap memmap = {sizeof(memmap_buf), memmap_buf, 0, 0, 0, 0};
GetMemoryMap(&memmap);
// ...
}
가져온 메모리맵 출력 및 저장
EFI_STATUS EFIAPI UefiMain(
EFI_HANDLE image_handle,
EFI_SYSTEM_TABLE* system_table) {
// ...
EFI_FILE_PROTOCOL* root_dir;
OpenRootDir(image_handle, &root_dir);
EFI_FILE_PROTOCOL* memmap_file;
root_dir->Open(
root_dir, // 루트 디렉터리 핸들
&memmap_file, // 열 파일의 핸들을 받을 포인터
L"\\memmap", // 열 파일 경로
EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE, // 파일 모드
0);
SaveMemoryMap(&memmap, memmap_file);
memmap_file->Close(memmap_file);
while (1);
return EFI_SUCCESS;
}
부트서비스의 OpenProtocol 함수를 사용해서 핸들에 대한 프로토콜(드라이버 인터페이스 등)을 얻을 수 있고 이 프로토콜을 이용해서 어플리케이션이 저장된 디바이스 위치에 새로운 파일을 생성 후 파일핸들을 얻을 수 있다.
EFI_STATUS OpenRootDir(EFI_HANDLE image_handle, EFI_FILE_PROTOCOL** root) {
EFI_LOADED_IMAGE_PROTOCOL* loaded_image;
EFI_SIMPLE_FILE_SYSTEM_PROTOCOL* fs;
// 이미지 핸들은 현재 실행중인 UEFI 어플리케이션을 식별하는 핸들이며 EntryPoint 에서부터 받아온다.
// 현재 로드된 어플리케이션 관련 인터페이스를 가져올 수 있다.
// ex) DeviceHandle: 현재 어플리케이션이 저장된 장치 핸들 (파일시스템 접근시 사용)
// ex2) ImageBase, ImageSize, FilePath, LoadOptions 등
gBS->OpenProtocol(
image_handle, // 프로토콜이 존재하는 핸들 (디바이스, 드라이버 등)
&gEfiLoadedImageProtocolGuid, // EFI_LOADED_IMAGE_PROTOCOL 프로토콜의 GUID
(VOID**)&loaded_image, // 인터페이스를 전달 받을 포인터
image_handle, // 프로토콜을 여는 주체
NULL, // 컨트롤러에 바인딩할때만 사용
EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL); // 프로토콜을 여는 방식
// 어플리케이션이 저장된 장치에서 파일시스템 프로토콜을 가져온다.
gBS->OpenProtocol(
loaded_image->DeviceHandle,
&gEfiSimpleFileSystemProtocolGuid, // EFI_SIMPLE_FILE_SYSTEM_PROTOCOL
(VOID**)&fs,
image_handle,
NULL,
EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL);
fs->OpenVolume(fs, root); // 루트 디렉터리를 가리키는 핸들
return EFI_SUCCESS;
}
획득한 메모리 맵 배열에서 하나씩 꺼내 각 데이터를 파일에 저장한다.
EFI_STATUS SaveMemoryMap(struct MemoryMap* map, EFI_FILE_PROTOCOL* file) {
CHAR8 buf[256];
CHAR16 unicode_buf[256];
UINTN len;
CHAR8* header =
"Index, Type, Type(name), PhysicalStart, NumberOfPages, Attribute\n";
len = AsciiStrLen(header);
file->Write(file, &len, header);
Print(L"map->buffer = %08lx, map->map_size = %08lx\n",
map->buffer, map->map_size);
EFI_PHYSICAL_ADDRESS iter;
int i;
for (iter = (EFI_PHYSICAL_ADDRESS)map->buffer, i = 0;
iter < (EFI_PHYSICAL_ADDRESS)map->buffer + map->map_size;
iter += map->descriptor_size, i++) {
EFI_MEMORY_DESCRIPTOR* desc = (EFI_MEMORY_DESCRIPTOR*)iter;
len = AsciiSPrint(
buf, sizeof(buf),
"%4u | %08lx -> %08lx | %x, %-25ls, %lx, %lx\n",
i, desc->PhysicalStart, desc->VirtualStart, // 메모리타입은 값 -> 문자열로 치환
desc->Type, GetMemoryTypeUnicode(desc->Type), desc->NumberOfPages,
desc->Attribute & 0xffffflu);
DEBUG((DEBUG_INFO, buf));
// Ascii -> Unicode 로 변경 후 그대로 Print
AsciiStrToUnicodeStrS(buf, unicode_buf, sizeof(unicode_buf) / sizeof(CHAR16));
Print(L"%s", unicode_buf);
file->Write(file, &len, buf);
}
return EFI_SUCCESS;
}
실행결과
물리메모리와 가상메모리 매핑 부터 메모리에 대한 여러 정보를 얻을 수 있다.
파일은 qemu를 실행할때 사용한 disk.img 파일(사실상 부트로더 + 램디스크)을 마운트해보면 위에서 봤던 내용이 파일에 저장된 것을 볼 수 있다.