메모리 맵과 파일 쓰기

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

e6e5fd61-2e3e-4bf5-a30c-b8d3492ba2be
e6e5fd61-2e3e-4bf5-a30c-b8d3492ba2be

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;
}

실행결과

물리메모리와 가상메모리 매핑 부터 메모리에 대한 여러 정보를 얻을 수 있다.

762592e4-ea98-41be-b22e-fe0ebd14d79a
762592e4-ea98-41be-b22e-fe0ebd14d79a

파일은 qemu를 실행할때 사용한 disk.img 파일(사실상 부트로더 + 램디스크)을 마운트해보면 위에서 봤던 내용이 파일에 저장된 것을 볼 수 있다.

9e6de493-25f7-45e6-8c0d-35abe5d8b62d
9e6de493-25f7-45e6-8c0d-35abe5d8b62d

ESC
Type to search...