1. 程式人生 > >[uboot] uboot啟動kernel篇(三)——uboot解析uImage的kernel資訊

[uboot] uboot啟動kernel篇(三)——uboot解析uImage的kernel資訊

一、說明

從《[uboot] (番外篇)uboot啟動kernel篇(二)——bootm跳轉到kernel的流程》我們知道了bootm的過程中,從uImage解析出kernel資訊主要是在bootm_find_os中實現的。 這裡注意,這些資訊主要是在mkimage工具生成uImage的時候附加上去的,並不是kernel原生映象的內容。  因為uImage有兩種型別,Legacy-uImage和FIT-uImage ,這兩個型別的格式是不一樣的,因此,uboot從這兩種uImage解析出kernel資訊的方式也是不一樣的。  後面會分別介紹這兩種型別的解析流程。

二、kernel資訊的存放位置

1、存放位置

kernel資訊主要包括兩方面內容:

  • kernel的映象資訊(包括其載入地址):放在bootm_headers_t images -> image_info_t os中
  • kernel的入口地址:放在bootm_headers_t images -> ulong ep中

如下(過濾掉一些無關部分)  include/image.h

typedef struct bootm_headers {
    image_info_t    os;     /* os image info */
    ulong       ep;     /* entry point of OS */
}

因此,bootm_find_os的主要目的是實現bootm_headers_t images中的image_info_t os和ulong ep的成員。

2、image_info_t

image_info_t用來描述kernel的映象資訊。包括頭部資訊、載入地址、kernel映象地址和長度等等。  其資料結構如下:  include/image.h

typedef struct image_info {
    ulong       start, end;     /* start/end of blob */   // 包括附加節點資訊之內的整個kernel節點的起始地址和結束地址
    ulong       image_start, image_len; /* start of image within blob, len of image */ // kernel映象的起始地址,映象長度
    ulong       load;           /* load addr for the image */ // 映象的載入地址
    uint8_t     comp, type, os;     /* compression, type of image, os type */ // 映象的壓縮格式、映象型別、作業系統型別
    uint8_t     arch;           /* CPU architecture */ // CPU體系結構
} image_info_t;

type對應於“kennel”,os對應於“linux”。

綜上,後續我們解析uImage的時候的主要目的是填充image_info_t os和ulong ep,這句話多強調幾遍。

三、Legacy-uImage中kernel資訊的解析

Legacy-uImage中kernel資訊的解析相對較為簡單。

1、Legacy-uImage的生成

首先要知道Legacy-uImage是怎麼生成的。  如下:

mkimage -A arm -O linux -C none -T kernel -a 0x20008000 -e 0x20008040 -n Linux_Image -d zImage uImage 

Usage: mkimage -l image
          -l ==> list image header information
       mkimage [-x] -A arch -O os -T type -C comp -a addr -e ep -n name -d data_file[:data_file...] image
          -A ==> set architecture to 'arch'  // 體系
          -O ==> set operating system to 'os' // 作業系統
          -T ==> set image type to 'type' // 映象型別
          -C ==> set compression type 'comp' // 壓縮型別
          -a ==> set load address to 'addr' (hex) // 載入地址
          -e ==> set entry point to 'ep' (hex) // 入口地址
          -n ==> set image name to 'name' // 映象名稱,注意不能超過32B
          -d ==> use image data from 'datafile' // 輸入檔案
          -x ==> set XIP (execute in place) 

通過上述,可以知道kernel的資訊都是在這個時候定義的(而不是kernel原生映象原來就有的)。  生成uImage會包含64Byte的格式頭,如下:

[email protected]:boot$ od -tx1 -tc -Ax -N64 uImage
000000  27  05  19  56  5a  f3  f7  8e  58  45  0d  3d  00  17  cd  f8
         ' 005 031   V   Z 363 367 216   X   E  \r   =  \0 027 315 370
000010  20  00  80  00  20  00  80  40  e2  4b  43  b6  05  02  02  00
            \0 200  \0      \0 200   @ 342   K   C 266 005 002 002  \0
000020  4c  69  6e  75  78  5f  49  6d  61  67  65  00  00  00  00  00
         L   i   n   u   x   _   I   m   a   g   e  \0  \0  \0  \0  \0
000030  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
        \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0

所以,uboot解析Legacy-uImage主要就是解析這64Byte的頭部的內容。

2、Legacy-uImage頭部資料結構

uboot使用了struct image_header來對應這uImage的64Byte的頭部。

typedef struct image_header {
    __be32      ih_magic;   /* Image Header Magic Number    */   // 幻數頭,用來校驗是否是一個Legacy-uImage
    __be32      ih_hcrc;    /* Image Header CRC Checksum    */ // 頭部的CRC校驗值
    __be32      ih_time;    /* Image Creation Timestamp */ // 映象建立的時間戳
    __be32      ih_size;    /* Image Data Size      */ // 映象資料長度
    __be32      ih_load;    /* Data  Load  Address      */ // 載入地址
    __be32      ih_ep;      /* Entry Point Address      */ // 入口地址
    __be32      ih_dcrc;    /* Image Data CRC Checksum  */ // 映象的CRC校驗
    uint8_t     ih_os;      /* Operating System     */ // 作業系統型別
    uint8_t     ih_arch;    /* CPU architecture     */ // 體系 
    uint8_t     ih_type;    /* Image Type           */ // 映象型別
    uint8_t     ih_comp;    /* Compression Type     */ // 壓縮型別
    uint8_t     ih_name[IH_NMLEN];  /* Image Name       */ // 映象名
} image_header_t;
#define IH_NMLEN        32  /* Image Name Length        */

所以,只需要將image_header的指標對應到uImage的起始地址,就可以得到image_header的實體了。  uImage的這個頭部指標image_header會被儲存在bootm_headers 中,如下:

typedef struct bootm_headers {
    /*
     * Legacy os image header, if it is a multi component image
     * then boot_get_ramdisk() and get_fdt() will attempt to get
     * data from second and third component accordingly.
     */
    image_header_t  *legacy_hdr_os;     /* image header pointer */  // 頭部指標
    image_header_t  legacy_hdr_os_copy; /* header copy */   // 頭部資訊的備份的指標
    ulong       legacy_hdr_valid; // 用於表示Legacy-uImage的頭部指標是否可用,也就是這是否是一個Legacy-uImage。

3、解析Legacy-uImage中kernel資訊的程式碼流程

從bootm_find_os入口開始說明。  程式碼如下,過濾掉無關部分:  common/bootm.c

static int bootm_find_os(cmd_tbl_t *cmdtp, int flag, int argc,
             char * const argv[])
{
// uImage的地址對應傳進來的引數argv[0].
    const void *os_hdr;
    bool ep_found = false;
    int ret;

    /* get kernel image header, start address and length */
    os_hdr = boot_get_kernel(cmdtp, flag, argc, argv,
            &images, &images.os.image_start, &images.os.image_len);
        // 通過呼叫boot_get_kernel獲取到Legacy-uImage的頭部,並將其指標返回給os_hdr。
        // 以images.os.image_start和images.os.image_len為其引數,在boot_get_kernel中會自動設定其值

    /* get image parameters */
    switch (genimg_get_format(os_hdr)) {
#if defined(CONFIG_IMAGE_FORMAT_LEGACY)
    case IMAGE_FORMAT_LEGACY:
        images.os.type = image_get_type(os_hdr); // 從頭部image_header 中獲得映象型別並存儲到images.os.type中
        images.os.comp = image_get_comp(os_hdr);  // 從頭部image_header 中獲得壓縮型別型別並存儲到images.os.comp中
        images.os.os = image_get_os(os_hdr); // 從頭部image_header 中獲得作業系統型別並存儲到images.os.os中

        images.os.end = image_get_image_end(os_hdr); //獲得uImage的結束地址
        images.os.load = image_get_load(os_hdr); // 從頭部image_header 中獲得載入地址並存儲到images.os.load中
        images.os.arch = image_get_arch(os_hdr); // 從頭部image_header 中獲得cpu體系結構型別並存儲到images.os.arch中
        break;
#endif
    if (images.os.arch == IH_ARCH_I386 ||
        images.os.arch == IH_ARCH_X86_64) {
    } else if (images.legacy_hdr_valid) {
        images.ep = image_get_ep(&images.legacy_hdr_os_copy); //從頭部image_header 中獲得載入地址並存儲到images.ep中
        }
    images.os.start = map_to_sysmem(os_hdr);

    return 0;
}

通過上述程式碼就完成了bootm_headers_t images中的image_info_t os和ulong ep的成員的實現。  而這裡的程式碼的核心是boot_get_kernel,會實現uImage的型別的判斷、和Legacy-uImage的頭部image_header的設定,並且將image_header和bootm_headers進行關聯。

4、boot_get_kernel

解析uImage的頭部的核心函式。  程式碼如下,過濾掉無關部分:  common/bootm.c

static const void *boot_get_kernel(cmd_tbl_t *cmdtp, int flag, int argc,
                   char * const argv[], bootm_headers_t *images,
                   ulong *os_data, ulong *os_len)
{
#if defined(CONFIG_IMAGE_FORMAT_LEGACY)
    image_header_t  *hdr;
#endif
    ulong       img_addr;
    const void *buf;
    const char  *fit_uname_config = NULL;
    const char  *fit_uname_kernel = NULL;


    img_addr = genimg_get_kernel_addr_fit(argc < 1 ? NULL : argv[0],
                          &fit_uname_config,
                          &fit_uname_kernel);
        // 因為傳進來的是kernel地址argv[0]是一個字串型別,這裡會將其轉換成地址型別(長整型)

    /* check image type, for FIT images get FIT kernel node */
    *os_data = *os_len = 0;
    buf = map_sysmem(img_addr, 0);
        // 對映到實體地址,因為MMU沒有一般在uboot中沒有開啟,所以這裡img_addr一般不會發生變化

    switch (genimg_get_format(buf)) {
        // 在這裡會根據幻數來判斷uImage的型別,假設這裡已經判斷出是Legacy-uImage型別

#if defined(CONFIG_IMAGE_FORMAT_LEGACY)
    case IMAGE_FORMAT_LEGACY:
        printf("## Booting kernel from Legacy Image at %08lx ...\n",
               img_addr);
                // 這裡會在log中打印出“## Booting kernel from Legacy Image at 0x20008000”

        hdr = image_get_kernel(img_addr, images->verify);
        if (!hdr)
            return NULL;
                // 在這裡會將頭部指標hdr設定成img_addr,然後判斷幻數,CRC值是否正確,
               // 通過這一步,就已經成功設定了Legacy-uImage的頭部指標,其內容也就相應確定下來了!!!

        /* get os_data and os_len */
        switch (image_get_type(hdr)) {
        case IH_TYPE_KERNEL:
        case IH_TYPE_KERNEL_NOLOAD:
            *os_data = image_get_data(hdr); 
                        // 通過image_header獲得kernel映象的地址,儲存在os_data中,也就是images.os.image_start中
            *os_len = image_get_data_size(hdr);
                        // 通過image_header獲得kernel映象的長度,儲存在os_len中,也就是images.os.image_len中
            break;
        }

        memmove(&images->legacy_hdr_os_copy, hdr,
            sizeof(image_header_t)); // 為uImage的頭部做一個備份
        images->legacy_hdr_os = hdr;  // 儲存uImage的頭部指標到images->legacy_hdr_os中
        images->legacy_hdr_valid = 1; // 說明uImage的頭部指標指向的頭部是可用的,這是一個Legacy-uImage型別
        break;
#endif
    }
    return buf;
}

四、FIT-uImage中kernel資訊的解析

1、原理簡單介紹

flattened image tree,類似於FDT(flattened device tree)的一種實現機制。其通過一定語法和格式將一些需要使用到的映象(例如kernel、dtb以及檔案系統)組合到一起生成一個image檔案。 而kernel映象也是作為FIT的configure中的一個節點,其資訊則是以節點中的屬性來進行描述的。 而uboot的工作,就是要從FIT中提取相應的kernel節點,在節點中獲取相應的屬性,從而得到kernel的資訊。其方式和FDT相當類似。  得到的kernel的資訊之後填入bootm_headers_t images中的image_info_t os和ulong ep中即可。

2、生成說明

/ {
    images {
        [email protected] {
            description = "Unify(TODO) Linux kernel for project-x";
            data = /incbin/("/home/hlos/code/xys/temp/project-x/build/out/linux/arch/arm/boot/zImage");
            type = "kernel"; // 映象型別是kernel
            arch = "arm";    // 體系
            os = "linux";    // 作業系統
            compression = "none";    // 壓縮型別
            load = <0x20008000>;    // 載入地址
            entry = <0x20008040>;    // 入口地址
        };
    configurations {
        default = "[email protected]";
        [email protected] {
            description = "Boot Linux kernel with FDT blob";
            kernel = "[email protected]"; // 指定kernel為節點“[email protected]”描述的資訊
        };
    };

可以看出kernel資訊都在“[email protected]”節點中進行了描述,注意,就連kernel映象,也是作為這個節點的屬性“data”。  所以uboot解析FIT-uImage中的kernel資訊的原理是:

  1. 從itb(FIT-uImage)檔案中解析出configurations節點
  2. 從configurations獲取要使用的kernel的節點的路徑(偏移)
  3. 從kernel的節點中獲取各種屬性,這些屬性就是kernel的資訊。
  4. 包括kernel的映象也是在data屬性中的。

3、資料結構說明

  • 與struct bootm_headers之間的關係  FIT-uImage中kernel是以節點的方式進行描述的,其節點也有自己的頭部。  其節點資訊儲存在struct bootm_headers中,如下:
typedef struct bootm_headers {
#if IMAGE_ENABLE_FIT
    const char  *fit_uname_cfg; /* configuration node unit name */ // configuration節點名稱

    void        *fit_hdr_os;    /* os FIT image header */  // itb的頭部,對應就是FIT-uImage的起始地址
    const char  *fit_uname_os;  /* os subimage node unit name */ // kernel節點的名稱
    int     fit_noffset_os; /* os subimage node offset */ // kernel節點的節點偏移,直接代表了kernel節點
} bootm_headers_t;

4、解析FIT-uImage中kernel資訊的程式碼流程

從bootm_find_os入口開始說明。  程式碼如下,過濾掉無關部分:  common/bootm.c

static int bootm_find_os(cmd_tbl_t *cmdtp, int flag, int argc,
             char * const argv[])
{
// uImage的地址對應傳進來的引數argv[0].
    const void *os_hdr;
    bool ep_found = false;
    int ret;

    /* get kernel image header, start address and length */
    os_hdr = boot_get_kernel(cmdtp, flag, argc, argv,
            &images, &images.os.image_start, &images.os.image_len);
    if (images.os.image_len == 0) {
        puts("ERROR: can't get kernel image!\n");
        return 1;
    }

        // 通過呼叫boot_get_kernel,來獲取itb(FIT-uImage)的頭部指標,儲存在os_hdr中
        // 以images.os.image_start和images.os.image_len為其引數,在boot_get_kernel中會自動設定其值
        // 同時,在boot_get_kernel中會設定images關於itb中kernel節點的資訊fit_hdr_os、fit_uname_os和fit_noffset_os
        // 後續繼續說明

    /* get image parameters */
    switch (genimg_get_format(os_hdr)) {
#if IMAGE_ENABLE_FIT
    case IMAGE_FORMAT_FIT:
        if (fit_image_get_type(images.fit_hdr_os,
                       images.fit_noffset_os,
                       &images.os.type)) {
            return 1;
        }
                // 呼叫fit_image_get_type從itb的kerne節點中解析出“type”屬性,儲存在images.os.type中。

        if (fit_image_get_comp(images.fit_hdr_os,
                       images.fit_noffset_os,
                       &images.os.comp)) {
            return 1;
        }
                // 呼叫fit_image_get_comp從itb的kerne節點中解析出“comp”屬性,儲存在images.os.comp中。

        if (fit_image_get_os(images.fit_hdr_os, images.fit_noffset_os,
                     &images.os.os)) {
            return 1;
        }
                // 呼叫fit_image_get_comp從itb的kerne節點中解析出“os”屬性,儲存在images.os.os中。

        if (fit_image_get_arch(images.fit_hdr_os,
                       images.fit_noffset_os,
                       &images.os.arch)) {
            return 1;
        }
                // 呼叫fit_image_get_comp從itb的kerne節點中解析出“arch”屬性,儲存在images.os.arch中。

        images.os.end = fit_get_end(images.fit_hdr_os);

        if (fit_image_get_load(images.fit_hdr_os, images.fit_noffset_os,
                       &images.os.load)) {
            return 1;
        }
                // 呼叫fit_image_get_comp從itb的kerne節點中解析出“load”屬性,儲存在images.os.load中。
        break;
#endif
    if (images.os.arch == IH_ARCH_I386 ||
        images.os.arch == IH_ARCH_X86_64) {
...
#if IMAGE_ENABLE_FIT
    } else if (images.fit_uname_os) {
        int ret;

        ret = fit_image_get_entry(images.fit_hdr_os,
                      images.fit_noffset_os, &images.ep);
                // 呼叫fit_image_get_comp從itb的kerne節點中解析出“ep”屬性,儲存在images.os.ep中。
        if (ret) {
            puts("Can't get entry point property!\n");
            return 1;
        }
#endif
    } 

通過上述程式碼就完成了bootm_headers_t images中的image_info_t os和ulong ep的成員的實現。  而這裡的程式碼的核心是boot_get_kernel,會實現uImage的型別的判斷、和FIT-uImage的頭部節點資訊的設定,並且將FIT-uImage的kernel的節點資訊和bootm_headers進行關聯。

4、boot_get_kernel

解析uImage的頭部的核心函式。  程式碼如下,過濾掉無關部分:  common/bootm.c

static const void *boot_get_kernel(cmd_tbl_t *cmdtp, int flag, int argc,
                   char * const argv[], bootm_headers_t *images,
                   ulong *os_data, ulong *os_len)
{
    ulong       img_addr;
    const void *buf;
    const char  *fit_uname_config = NULL;
    const char  *fit_uname_kernel = NULL;
#if IMAGE_ENABLE_FIT
    int     os_noffset;
#endif

    img_addr = genimg_get_kernel_addr_fit(argc < 1 ? NULL : argv[0],
                          &fit_uname_config,
                          &fit_uname_kernel);
        // 因為傳進來的是kernel地址argv[0]是一個字串型別,這裡會將其轉換成地址型別(長整型)

    /* check image type, for FIT images get FIT kernel node */
    *os_data = *os_len = 0;
    buf = map_sysmem(img_addr, 0);
        // 對映到實體地址,因為MMU沒有一般在uboot中沒有開啟,所以這裡img_addr一般不會發生變化

    switch (genimg_get_format(buf)) {
        // 在這裡會根據幻數來判斷uImage的型別,假設這裡已經判斷出是FIT-uImage型別

#if IMAGE_ENABLE_FIT
    case IMAGE_FORMAT_FIT:
        os_noffset = fit_image_load(images, img_addr,
                &fit_uname_kernel, &fit_uname_config,
                IH_ARCH_DEFAULT, IH_TYPE_KERNEL,
                BOOTSTAGE_ID_FIT_KERNEL_START,
                FIT_LOAD_IGNORED, os_data, os_len);
                // 在fit_image_load中會去查詢IH_TYPE_KERNEL指定的節點
                // 對應kernel = "[email protected]"; 指定的節點,返回其節點偏移,並且將data屬性的值(代表了kernel映象)的地址和長度
                // 設定到os_data和os_len中,也就是images.os.image_start和images.os.image_len中
                // 具體自己參考程式碼
        if (os_noffset < 0)
            return NULL;

        images->fit_hdr_os = map_sysmem(img_addr, 0);
                // 設定 itb的頭部,對應就是FIT-uImage的起始地址
        images->fit_uname_os = fit_uname_kernel;
                // 設定kernel節點的名稱
        images->fit_uname_cfg = fit_uname_config;
                // 設定configuration節點名稱
        images->fit_noffset_os = os_noffset;
                // 設定kernel節點的節點偏移,直接代表了kernel節點
        break;
#endif
    return buf;
}

通過上述程式碼,就得到了itb的地址和itb(FIT-uImage)中kernel的節點偏移,類似於fdt的操作,後續就可以通過這兩個itb的地址和itb(FIT-uImage)中kernel的節點偏移來獲得kernel節點的屬性,也就是我們上述四、3中的一些內容。

到這裡,uboot的bootm命令中,從uImage提取kernel資訊的工作就完成了。