漫談相容核心之六:二進位映像的類型識別

來源:互聯網
上載者:User

 除了某些嵌入式系統之外,一般而言作業系統都有個在建立(或轉化成)新進程時如何裝入目標程式的二進位映像並啟動其啟動並執行問題。由於在電腦技術的發展曆史中並沒有形成某種單一的、為所有作業系統和編譯/串連工具所共同遵循的標準,這個裝入/啟動的過程就不可避免地呈現出多樣性。而且,即使是同一種作業系統,也會在其發展的過程中採用多種不同的目標映像格式和裝入機理。而動態串連庫技術的出現,則又使這個過程進一步地複雜化了,因為此時需要裝入的不僅是目標程式的映像,還有動態串連庫的映像,並且還要解決目標程式與具體庫函數的動態串連問題。至於這個過程的重要性,那是不言而喻的,要不然作業系統就要麼實際上不能做“有用功”,要麼失去了通用性和靈活性。
    以Linux應用軟體為例,就既有a.out格式,又有ELF格式,又支援動態串連庫。我在“情景分析”一書中只講了a.out映像的裝入和啟動,是因為a.out相對比較簡單,否則篇幅太大。讀者也許會問,既然有了更複雜、功能更強的ELF格式,為什麼還要保留a.out格式呢?這當然是為了向後相容,一種技術一旦被廣泛採用以後就不會很快消失。與Linux相比,Windows採用的格式就更多了,因為它還需要支援DOS時代的應用軟體。
    相容核心既要支援Linux和Windows兩種作業系統的應用軟體,這個問題當然就更複雜、難度更大了。幸而Wine已經在我們之前以它的方式解決了這個問題,使我們至少有了可以借鑒的榜樣。
    在講述Wine的軟體映像的裝入/啟動過程之前,我們先考察一下,為了相容Windows軟體,Wine需要支援那一些映像格式,以及如何識別一個映像所屬的格式和類型。為此,我們看一下Wine的一段代碼,這段代碼在dlls/kernel/module.c中,是在使用者空間執行的。
    這是一個名為MODULE_GetBinaryType()的函數,其作用是辨認一個已開啟檔案所屬的映像格式並進而判定其類型,已定義的類型有:

CODE:

enum binary_type
{
    BINARY_UNKNOWN,
    BINARY_PE_EXE,
    BINARY_PE_DLL,
    BINARY_WIN16,
    BINARY_OS216,
    BINARY_DOS,
    BINARY_UNIX_EXE,
    BINARY_UNIX_LIB
};
除BINARY_UNKNOWN表示無法辨認/判定以外,這裡定義了7種映像類型。其中BINARY_PE_EXE和BINARY_PE_DLL是Windows的32位“PE格式”映像,前者為目標應用程式,後者為動態串連庫DLL。注意前者是“有源”的主體,可以成為一個進程;而後者是“無源”的庫程式,不能獨立成為一個進程。BINARY_WIN16和BINARY_OS216則為16位Windows應用;後者實際上是OS/2作業系統的應用程式,但是因為微軟和IBM曾經緊密合作,所以Windows也支援OS/2的應用程式。再往下BINARY_DOS顯然是DOS的應用軟體,但是DOS上的可執行程式有.exe和.com兩種,這裡並未加以區分,其原因留待以後再說。最後是BINARY_UNIX_EXE和BINARY_UNIX_LIB,Linux是Unix的繼承者,所以也適用於Linux的應用程式和動態串連庫。
    下面可以看代碼了,我們分段閱讀。

CODE:

enum binary_type
MODULE_GetBinaryType( HANDLE hfile,  void **res_start,  void **res_end )
{
    union
    {
        struct
        {
            unsigned char magic[4];
            unsigned char ignored[12];
            unsigned short type;
        } elf;
        struct
        {
            unsigned long magic;
            unsigned long cputype;
            unsigned long cpusubtype;
            unsigned long filetype;
        } macho;
        IMAGE_DOS_HEADER mz;
    } header;

    DWORD len;

    /* Seek to the start of the file and read the header information. */
    if (SetFilePointer( hfile, 0, NULL, SEEK_SET ) == -1)
        return BINARY_UNKNOWN;
    if (!ReadFile( hfile, &header, sizeof(header), &len, NULL ) || len != sizeof(header))
        return BINARY_UNKNOWN;
無論是Linux還是Windows,在檔案系統的目錄項中都沒有關於檔案格式/類型的說明,所以只能採取在檔案的實際內容前面加上頭部的方法來表明。但是,不同可執行映像的頭部結構和大小又各不相同。而且,頭部還可能是級連或嵌套的,即先由一級的頭部進行大的分類,然後再由二級的頭部作進一步的細分。所以這裡定義了一個包含幾種一級頭部結構的Union。其中的elf當然是Linux的ELF格式映像的頭部(但是a.out格式不在內,所以也並不完整);macho大約是針對MACH作業系統的,我們並不關心;而mz是DOS以及Windows格式的一級頭部,這是個相對較大的資料結構:

CODE:

typedef struct _IMAGE_DOS_HEADER {
    WORD  e_magic;      /* 00: MZ Header signature */
    WORD  e_cblp;       /* 02: Bytes on last page of file */
    WORD  e_cp;         /* 04: Pages in file */
    WORD  e_crlc;       /* 06: Relocations */
    WORD  e_cparhdr;    /* 08: Size of header in paragraphs */
    WORD  e_minalloc;   /* 0a: Minimum extra paragraphs needed */
    WORD  e_maxalloc;   /* 0c: Maximum extra paragraphs needed */
    WORD  e_ss;         /* 0e: Initial (relative) SS value */
    WORD  e_sp;         /* 10: Initial SP value */
    WORD  e_csum;      /* 12: Checksum */
    WORD  e_ip;         /* 14: Initial IP value */
    WORD  e_cs;         /* 16: Initial (relative) CS value */
    WORD  e_lfarlc;       /* 18: File address of relocation table */
    WORD  e_ovno;       /* 1a: Overlay number */
    WORD  e_res[4];      /* 1c: Reserved words */
    WORD  e_oemid;      /* 24: OEM identifier (for e_oeminfo) */
    WORD  e_oeminfo;    /* 26: OEM information; e_oemid specific */
    WORD  e_res2[10];    /* 28: Reserved words */
    DWORD e_lfanew;     /* 3c: Offset to extended header */
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
這個資料結構提供了不少資訊,都是與DOS環境下的目標映像裝入/啟動密切相關的。例如e_ss和e_sp就說明了堆棧的位置是預定的(而不是動態分配的),而e_ss的使用又表明目標程式是在“實模式”下運行。如此等等,這裡就不詳說了。不過,當微軟從DOS發展到Windows和WinNT時,仍舊套用了這個資料結構作為其應用程式目標映像的一級頭部,而WinNT顯然是在“保護模式”下運行。所以,這裡的許多欄位對於Windows目標映像實際上已經不再使用。
    代碼中首先通過類似於lseek()的SetFilePointer()把目標檔案的讀/寫指標移到檔案的開頭,再按上述Union的大小讀出,這就可以把幾種頭部、特別是Linux和Windows目標映像的頭部都包含在內了。
    下面就來辨認識別:

CODE:

    if (!memcmp( header.elf.magic, "/177ELF", 4 ))
    {
        /* FIXME: we don't bother to check byte order, architecture, etc. */
        switch(header.elf.type)
        {
        case 2: return BINARY_UNIX_EXE;
        case 3: return BINARY_UNIX_LIB;
        }
        return BINARY_UNKNOWN;
    }

    ......先看是否Linux的ELF格式。ELF頭部的資料結構定義見上,其第一個欄位是4位元組的標識碼magic,也稱為“簽名”。ELF格式簽名的第一個位元組是八進位的‘/177’,即十六進位的‘0x7f’,然後是‘E’、‘L’、‘F’三個字元。ELF頭部的type欄位則進一步表明映像的性質,目前只定義了兩種類型,即BINARY_UNIX_EXE和BINARY_UNIX_LIB。
    我們跳過對macho頭部的辨認,往下看DOS/Windows頭部的辨認。DOS頭部的簽名定義於include/winnt.h:

CODE:

#define IMAGE_DOS_SIGNATURE   0x5A4D   /* MZ   */
#define IMAGE_OS2_SIGNATURE     0x454E   /* NE   */
#define IMAGE_OS2_SIGNATURE_LE  0x454C   /* LE   */
#define IMAGE_OS2_SIGNATURE_LX  0x584C   /* LX */
#define IMAGE_VXD_SIGNATURE   0x454C   /* LE   */
#define IMAGE_NT_SIGNATURE   . 0x00004550  /* PE00 */
數值0x5A4D實際上是‘M’、‘Z’兩個字元的代碼,因為Intel的CPU晶片採用“Little Ending”,所以次序是反的。注意這裡只有MZ用於一級頭部,其餘都用於二級頭部。
繼續往下看代碼:

CODE:

    /* Not ELF, try DOS */

    if (header.mz.e_magic == IMAGE_DOS_SIGNATURE)
    {
        union
        {
            IMAGE_OS2_HEADER os2;
            IMAGE_NT_HEADERS nt;
        } ext_header;

        /* We do have a DOS image so we will now try to seek into
         * the file by the amount indicated by the field
         * "Offset to extended header" and read in the
         * "magic" field information at that location.
         * This will tell us if there is more header information
         * to read or not.
         */
        if (SetFilePointer( hfile, header.mz.e_lfanew, NULL, SEEK_SET ) == -1)
            return BINARY_DOS;
        if (!ReadFile( hfile, &ext_header, sizeof(ext_header), &len, NULL ) || len < 4)
            return BINARY_DOS;

        /* Reading the magic field succeeded so we will try to determine what type it is.*/
        if (!memcmp( &ext_header.nt.Signature, "PE/0/0", 4 ))
        {
            if (len >= sizeof(ext_header.nt.FileHeader))
            {
              if (len < sizeof(ext_header.nt))  /* clear remaining part of header if missing */
                    memset( (char *)&ext_header.nt + len, 0, sizeof(ext_header.nt) - len );
              if (res_start) *res_start = (void *)ext_header.nt.OptionalHeader.ImageBase;
              if (res_end) *res_end = (void *)(ext_header.nt.OptionalHeader.ImageBase +
                     ext_header.nt.OptionalHeader.SizeOfImage);
              if (ext_header.nt.FileHeader.Characteristics & IMAGE_FILE_DLL)
                     return BINARY_PE_DLL;
              return BINARY_PE_EXE;
            }
            return BINARY_DOS;
        }
如果一級頭部的簽名是“MZ”,那就是DOS一族的目標映像了,Windows是從DOS發展過來的,所以目標映像同屬DOS一族。進一步的細分要根據二級頭部、或曰“擴充”頭部才能辨認,所以這裡又定義了一個Union,即ext_header。這一次的目的是要區分Windows和OS/2映像。我們在這裡只關心Windows的目標映像,所以只看IMAGE_NT_HEADERS資料結構的定義:

CODE:

typedef struct _IMAGE_NT_HEADERS {
  DWORD       Signature;  /* "PE"/0/0 */ /* 0x00 */
  IMAGE_FILE_HEADER    FileHeader;  /* 0x04 */
  IMAGE_OPTIONAL_HEADER  OptionalHeader; /* 0x18 */
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
這個“頭部”裡面又嵌套著兩個頭部,它們的資料結構定義都在winnt.h中,這裡就不一一列舉了。不過需要說明,對於可執行映像而言,IMAGE_OPTIONAL_HEADER可不是“可選”的,反倒是十分重要的,例如裡面有個欄位是AddressOfEntryPoint,還有BaseOfCode和BaseOfData;此外還有個可變大小的數組DataDirectory[ ];其重要性由此可見一斑。
    還要說明,IMAGE_OS2_HEADER並不如其名稱所示那樣僅僅是用於OS/2軟體映像的,實際上也用於一些16位Windows軟體的映像和DOS軟體的映像。
    IMAGE_DOS_HEADER結構中的最後一個欄位e_lfanew說明了擴充頭部在檔案中的位移,所以這一次把讀/寫指標移到這個位置上。
    讀入擴充頭部以後,首先就檢查是否有“PE”格式的簽名。如果是,並且頭部是完整的,就根據頭部中FileHeader.Characteristics欄位的IMAGE_FILE_DLL標誌位判定其為.exe映像還是DLL映像。此外,可能還要根據所讀入頭部提供的資訊修正兩個全域量res_start和res_end的數值,不過那與映像類型的識別是無關的。

    如果頭部簽名不是“PE”,那就可能是OS/2或其它Windows/DOS的可執行映像了,此類映像在擴充頭部中的簽名是“NE”。我們再往下看。

CODE:

        if (!memcmp( &ext_header.os2.ne_magic, "NE", 2 ))
        {
            /* This is a Windows executable (NE) header.  This can
             * mean either a 16-bit OS/2 or a 16-bit Windows or even a
             * DOS program (running under a DOS extender).  To decide
             * which, we'll have to read the NE header.
             */
            if (len >= sizeof(ext_header.os2))
            {
              switch ( ext_header.os2.ne_exetyp )
              {
              case 1:  return BINARY_OS216;  /* OS/2 */
              case 2:  return BINARY_WIN16;  /* Windows */
              case 3:  return BINARY_DOS;  /* European MS-DOS 4.x */
              case 4:  return BINARY_WIN16; /* Windows 386; FIXME: is this 32bit??? */
              case 5:  return BINARY_DOS;
                                            /* BOSS, Borland Operating System Services */
              /* other types, e.g. 0 is: "unknown" */
              default:
              return MODULE_Decide_OS2_OldWin(hfile, &header.mz, &ext_header.os2);
              }
            }
            /* Couldn't read header, so abort. */
            return BINARY_DOS;
        }

        /* Unknown extended header, but this file is nonetheless DOS-executable. */
        return BINARY_DOS;
    }

    return BINARY_UNKNOWN;
}
顯然,IMAGE_OS2_HEADER頭部中的欄位ne_exetyp進一步說明了具體的映像類型。從這裡也可以看出,DOS/Windows與OS/2真的是你中有我、我中有你。除1-5以外,還有些類型碼和頭部特徵是用於某些特別老的OS/2和Windows(版本3.0以前)目標映像的,那要進一步通過MODULE_Decide_OS2_OldWin()加以識別,這裡就不贅述了,有興趣的讀者可以自己閱讀和研究。
    最後,MODULE_GetBinaryType()的傳回值就是目標映像的類型代碼。
    有了這個函數,加上有關資料結構的定義,讀者完全可以自己寫個程式,列印出給定二進位映像檔案的映像類型,並進一步列印出該映像的許多特性和參數。在Wine代碼的tools/winedump目錄下那些檔案中有一些函數,包括dump_pe_header()、dump_le_header()、dump_ne_header()等等,就是用來列印出各種格式的映像頭部。下面不加說明地列出其中dump_pe_header()的代碼,供讀者自己閱讀,好處是可以從這些代碼中看出各個頭部(例如IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER)中許多欄位的作用和意義:

CODE:

static void dump_pe_header(void)
{
    const char   *str;
    IMAGE_FILE_HEADER  *fileHeader;
    IMAGE_OPTIONAL_HEADER *optionalHeader;
    unsigned   i;

    printf("File Header/n");
    fileHeader = &PE_nt_headers->FileHeader;

    printf("  Machine:                      %04X (%s)/n",
    fileHeader->Machine, get_machine_str(fileHeader->Machine));
    printf("  Number of Sections:           %d/n", fileHeader->NumberOfSections);
    printf("  TimeDateStamp:                %08lX (%s) offset %lu/n",
    fileHeader->TimeDateStamp, get_time_str(fileHeader->TimeDateStamp),
    Offset(&(fileHeader->TimeDateStamp)));
    printf("  PointerToSymbolTable:         %08lX/n", fileHeader->PointerToSymbolTable);
    printf("  NumberOfSymbols:              %08lX/n", fileHeader->NumberOfSymbols);
    printf("  SizeOfOptionalHeader:         %04X/n", fileHeader->SizeOfOptionalHeader);
    printf("  Characteristics:              %04X/n", fileHeader->Characteristics);
#define X(f,s) if (fileHeader->Characteristics & f) printf("    %s/n", s)
    X(IMAGE_FILE_RELOCS_STRIPPED,  "RELOCS_STRIPPED");
    X(IMAGE_FILE_EXECUTABLE_IMAGE,  "EXECUTABLE_IMAGE");
    X(IMAGE_FILE_LINE_NUMS_STRIPPED,  "LINE_NUMS_STRIPPED");
    X(IMAGE_FILE_LOCAL_SYMS_STRIPPED,  "LOCAL_SYMS_STRIPPED");
    X(IMAGE_FILE_16BIT_MACHINE,  "16BIT_MACHINE");
    X(IMAGE_FILE_BYTES_REVERSED_LO,  "BYTES_REVERSED_LO");
    X(IMAGE_FILE_32BIT_MACHINE,  "32BIT_MACHINE");
    X(IMAGE_FILE_DEBUG_STRIPPED,  "DEBUG_STRIPPED");
    X(IMAGE_FILE_SYSTEM,   "SYSTEM");
    X(IMAGE_FILE_DLL,    "DLL");
    X(IMAGE_FILE_BYTES_REVERSED_HI,  "BYTES_REVERSED_HI");
#undef X
    printf("/n");

    /* hope we have the right size */
    printf("Optional Header/n");
    optionalHeader = &PE_nt_headers->OptionalHeader;
    printf("  Magic                              0x%-4X         %u/n",
    optionalHeader->Magic, optionalHeader->Magic);
    printf("  linker version                     %u.%02u/n",
    optionalHeader->MajorLinkerVersion, optionalHeader->MinorLinkerVersion);
    printf("  size of code                       0x%-8lx     %lu/n",
    optionalHeader->SizeOfCode, optionalHeader->SizeOfCode);
    printf("  size of initialized data           0x%-8lx     %lu/n",
    optionalHeader->SizeOfInitializedData, optionalHeader->SizeOfInitializedData);
    printf("  size of uninitialized data         0x%-8lx     %lu/n",
    optionalHeader->SizeOfUninitializedData, optionalHeader->SizeOfUninitializedData);
    printf("  entrypoint RVA                     0x%-8lx     %lu/n",
    optionalHeader->AddressOfEntryPoint, optionalHeader->AddressOfEntryPoint);
    printf("  base of code                       0x%-8lx     %lu/n",
    optionalHeader->BaseOfCode, optionalHeader->BaseOfCode);
    printf("  base of data                       0x%-8lX     %lu/n",
    optionalHeader->BaseOfData, optionalHeader->BaseOfData);
    printf("  image base                         0x%-8lX     %lu/n",
    optionalHeader->ImageBase, optionalHeader->ImageBase);
    printf("  section align                      0x%-8lx     %lu/n",
    optionalHeader->SectionAlignment, optionalHeader->SectionAlignment);
    printf("  file align                         0x%-8lx     %lu/n",
    optionalHeader->FileAlignment, optionalHeader->FileAlignment);
    printf("  required OS version                %u.%02u/n",
    optionalHeader->MajorOperatingSystemVersion,
           optionalHeader->MinorOperatingSystemVersion);
    printf("  image version                      %u.%02u/n",
    optionalHeader->MajorImageVersion, optionalHeader->MinorImageVersion);
    printf("  subsystem version                  %u.%02u/n",
    optionalHeader->MajorSubsystemVersion, optionalHeader->MinorSubsystemVersion);
    printf("  Win32 Version       0x%lX/n", optionalHeader->Win32VersionValue);
    printf("  size of image        0x%-8lx     %lu/n",
    optionalHeader->SizeOfImage, optionalHeader->SizeOfImage);
    printf("  size of headers       0x%-8lx     %lu/n",
    optionalHeader->SizeOfHeaders, optionalHeader->SizeOfHeaders);
    printf("  checksum           0x%lX/n", optionalHeader->CheckSum);
    switch (optionalHeader->Subsystem)
    {
    default:
    case IMAGE_SUBSYSTEM_UNKNOWN:   str = "Unknown";  break;
    case IMAGE_SUBSYSTEM_NATIVE:    str = "Native";  break;
    case IMAGE_SUBSYSTEM_WINDOWS_GUI:  str = "Windows GUI";  break;
    case IMAGE_SUBSYSTEM_WINDOWS_CUI:  str = "Windows CUI";  break;
    case IMAGE_SUBSYSTEM_OS2_CUI:    str = "OS/2 CUI";  break;
    case IMAGE_SUBSYSTEM_POSIX_CUI:   str = "Posix CUI";  break;
    }
    printf("  Subsystem      0x%X (%s)/n", optionalHeader->Subsystem, str);
    printf("  DLL flags       0x%X/n", optionalHeader->DllCharacteristics);
    printf("  stack reserve size    0x%-8lx     %lu/n",
    optionalHeader->SizeOfStackReserve, optionalHeader->SizeOfStackReserve);
    printf("  stack commit size    0x%-8lx     %lu/n",
    optionalHeader->SizeOfStackCommit, optionalHeader->SizeOfStackCommit);
    printf("  heap reserve size     0x%-8lx     %lu/n",
    optionalHeader->SizeOfHeapReserve, optionalHeader->SizeOfHeapReserve);
    printf("  heap commit size     0x%-8lx     %lu/n",
    optionalHeader->SizeOfHeapCommit, optionalHeader->SizeOfHeapCommit);
    printf("  loader flags         0x%lX/n", optionalHeader->LoaderFlags);
    printf("  RVAs & sizes       0x%lX/n", optionalHeader->NumberOfRvaAndSizes);
    printf("/n");

    printf("Data Directory/n");
    printf("%ld/n",
          optionalHeader->NumberOfRvaAndSizes* sizeof(IMAGE_DATA_DIRECTORY));

    for (i = 0;  i < optionalHeader->NumberOfRvaAndSizes && i < 16;  i++)
    {
printf("  %-12s  rva: 0x%-8lX  size: %8lu/n",
         DirectoryNames[i],
         optionalHeader->DataDirectory[i].VirtualAddress,
         optionalHeader->DataDirectory[i].Size);
    }
    printf("/n");
}
代碼中的PE_nt_headers是個PE格式頭部資料結構指標,頭部的內容在此以前已經讀入。Rva是“Relative Virtual Address”的縮寫,表示一個符號相對於其所在浮動代碼塊起點的虛擬位址。
    現在,讀者應該已經大體上明白了如何識別目標映像,以及如何解釋映像頭部許多欄位和標誌位的作用和意義,為進一步考察目標映像的裝入和啟動打下了基礎。在以後的漫談中,我將講述Wine怎樣裝入和啟動目標映像,到那時這些欄位的作用和意義就更清楚了。
    Wine的上述代碼都是在使用者空間執行的,但是若要將這些代碼移到核心中也很容易。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.