vivi與Linux kernel的參數傳遞情景分析(上)

在上一部分提到過了,vivi作爲bootloader,向內核傳遞啓動參數是其本職工作之一。要把這個情景分析清楚,不僅僅需要分析vivi 的參數機制,而且要分析Linux kernel的接收機制。因爲這是一個簡單的通信過程,比起本科所學習的TCP/IP來簡單的多,但是因爲簡單,所以在協議上並不規範,理解上反而不如 TCP/IP協議。下面就分爲兩個方面對此情景分析。
 
一、綜述內核參數傳遞機制
 
    現在內核參數傳遞機制有兩種:一種是基於struct param_struct,這種已經比較老了。缺點是該結構每個成員的位置是固定的,受限比較大。另外一種就是新的struct tag way。說新是相對的,Linux kernel 2.4.x都希望採用這種tag的方式。關於這方面的資料,可以有如下參考(所給出的目錄是基於linux-2.4.18的內核,以頂層Makefile 所在目錄爲當前目錄。這裏基於ARM架構的S3C2410,其他的SoC可以類比很容易得到):
 
1、關於bootloader的理解--【Documentation/arm/booting】
 
    此文檔詳細的講述了bootloader的作用,具體內容如下:
 

[armlinux@lqm arm]$ cat Booting 
                        Booting ARM Linux
                        =================

Author: Russell King
Date : 18 May 2002

The following documentation is relevant to 2.4.18-rmk6 and beyond.

In order to boot ARM Linux, you require a boot loader, which is a small
program that runs before the main kernel. The boot loader is expected
to initialise various devices, and eventually call the Linux kernel,
passing information to the kernel.

Essentially, the boot loader should provide (as a minimum) the
following:

1. Setup and initialise the RAM.
2. Initialise one serial port.
3. Detect the machine type.
4. Setup the kernel tagged list.
5. Call the kernel image.


1. Setup and initialise RAM
---------------------------

Existing boot loaders: MANDATORY
New boot loaders: MANDATORY

The boot loader is expected to find and initialise all RAM that the
kernel will use for volatile data storage in the system. It performs
this in a machine dependent manner. (It may use internal algorithms
to automatically locate and size all RAM, or it may use knowledge of
the RAM in the machine, or any other method the boot loader designer
sees fit.)


2. Initialise one serial port
-----------------------------

Existing boot loaders: OPTIONAL, RECOMMENDED
New boot loaders: OPTIONAL, RECOMMENDED

The boot loader should initialise and enable one serial port on the
target. This allows the kernel serial driver to automatically detect
which serial port it should use for the kernel console (generally
used for debugging purposes, or communication with the target.)

As an alternative, the boot loader can pass the relevant 'console='
option to the kernel via the tagged lists specifing the port, and
serial format options as described in

       linux/Documentation/kernel-parameters.txt.


3. Detect the machine type
--------------------------

Existing boot loaders: OPTIONAL
New boot loaders: MANDATORY

The boot loader should detect the machine type its running on by some
method. Whether this is a hard coded value or some algorithm that
looks at the connected hardware is beyond the scope of this document.
The boot loader must ultimately be able to provide a MACH_TYPE_xxx
value to the kernel. (see linux/arch/arm/tools/mach-types).


4. Setup the kernel tagged list
-------------------------------

Existing boot loaders: OPTIONAL, HIGHLY RECOMMENDED
New boot loaders: MANDATORY

The boot loader must create and initialise the kernel tagged list.
A valid tagged list starts with ATAG_CORE and ends with ATAG_NONE.
The ATAG_CORE tag may or may not be empty. An empty ATAG_CORE tag
has the size field set to '2' (0x00000002). The ATAG_NONE must set
the size field to zero.

Any number of tags can be placed in the list. It is undefined
whether a repeated tag appends to the information carried by the
previous tag, or whether it replaces the information in its
entirety; some tags behave as the former, others the latter.

The boot loader must pass at a minimum the size and location of
the system memory, and root filesystem location. Therefore, the
minimum tagged list should look:

        +-----------+
base -> | ATAG_CORE | |
        +-----------+ |
        | ATAG_MEM | | increasing address
        +-----------+ |
        | ATAG_NONE | |
        +-----------+ v

The tagged list should be stored in system RAM.

The tagged list must be placed in a region of memory where neither
the kernel decompressor nor initrd 'bootp' program will overwrite
it. The recommended placement is in the first 16KiB of RAM.

5. Calling the kernel image
---------------------------

Existing boot loaders: MANDATORY
New boot loaders: MANDATORY

There are two options for calling the kernel zImage. If the zImage
is stored in flash, and is linked correctly to be run from flash,
then it is legal for the boot loader to call the zImage in flash
directly.

The zImage may also be placed in system RAM (at any location) and
called there. Note that the kernel uses 16K of RAM below the image
to store page tables. The recommended placement is 32KiB into RAM.

In either case, the following conditions must be met:

- CPU register settings
  r0 = 0,
  r1 = machine type number discovered in (3) above.
  r2 = physical address of tagged list in system RAM.

- CPU mode
  All forms of interrupts must be disabled (IRQs and FIQs)
  The CPU must be in SVC mode. (A special exception exists for Angel)

- Caches, MMUs
  The MMU must be off.
  Instruction cache may be on or off.
  Data cache must be off.

- The boot loader is expected to call the kernel image by jumping
  directly to the first instruction of the kernel image.

 
    可以看出bootloader最少具備5項功能,上面比較清晰。可以看出,現在2.4的內核都是希望採用tagged list的方式來進行傳遞的,這裏沒有提到比較老的方式。這裏要特別注意的是,r2 = physical address of tagged list in system RAM.,這裏的“必須”是針對於tagged list而言的,如果採用param_struct,則並沒有這個限制。這在後面將會詳細分析,而這正是可能導致疑惑的地方。
 
2、參數傳遞數據結構的定義位置【include/asm/setup.h】,在這裏就可以看到兩種參數傳遞方式了。可以說,現在 bootloader和Linux kernel約定的參數傳遞機制就是這兩種,必須嚴格按照這兩種機制進行傳輸,否則的話,kernel可能因爲無法識別bootloader傳遞過來的參數而導致無法啓動。關於這兩種方式,在這裏還有說明:
 

/*
 * linux/include/asm/setup.h
 *
 * Copyright (C) 1997-1999 Russell King
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * Structure passed to kernel to tell it about the
 * hardware it's running on. See linux/Documentation/arm/Setup
 * for more info.
 *
 * NOTE:
 * This file contains two ways to pass information from the boot
 * loader to the kernel. The old struct param_struct is deprecated,
 * but it will be kept in the kernel for 5 years from now
 * (2001). 
This will allow boot loaders to convert to the new struct
 * tag way.
 */

 
    這說明,現在參數傳遞必須要採用tag方式,因爲現在新的kernel已經不支持param_struct方式了。不幸的是,vivi還是採用的 param_struct方式。這裏暫時以param_struct爲主分析,考慮以後更新爲tag方式。在這裏你也可以參考【Documentation/arm/setup】,裏面有關於選項具體含義的詳細說明。(在這裏多說幾句。Linux的Documentation是一個很好的學習庫,幾乎所有的問題在這裏都能有初步的解答。如果要想繼續深入,那麼就要讀源代碼了。學習上,先看README,然後翻閱 Documentation,無疑是一條捷徑。而且,是否有完備的文檔,也是判斷這個軟件是否優秀的重要標準。)
 
二、vivi設置Linux參數分析
 
    上面對bootloader與Linux kernel之間參數傳遞的兩種方式已經有了一個總體的理解。下面就來先看vivi部分如何設置Linux參數。
 
【init/main.c】boot_or_vivi()-->run_autoboot()-->exec_string("boot")
 
    到此,也就是要執行boot命令。與命令相關部分都在【lib/command.c】中,找到boot_cmd,然後跟蹤至【lib/boot_kernel.c】,boot的執行行爲函數爲command_boot(),繼續分析:
 
【lib/boot_kernel.c】command_boot()-->
 
主要就是三步工作。
 
    · 獲取media_type。
 

media_type = get_param_value("media_type", &ret);

 
    media_type是重要的,因爲對於不同的存儲介質,底層的驅動函數是不同的。通過media_type這個頂層抽象,實現了與底層驅動的聯繫。
 

[armlinux@lqm include]$ cat boot_kernel.
#ifndef _VIVI_BOOT_KERNEL_H_
#define _VIVI_BOOT_KERNEL_H_

/* 
 * Media Type: A type of storage device that contains the linux kernel
 *
 * +----------------+-----------------------------------------+
 * | Value(Integer) | Type |
 * +----------------+-----------------------------------------+
 * | 0 | UNKNOWN | 
 * | 1 | RAM |
 * | 2 | NOR Flash Memory |
 * | 3 | SMC (NAND Flash Memory) on the S3C2410 |
 * +----------------+-----------------------------------------+
 */

enum {
        MT_UNKNOWN = 0,
        MT_RAM,
        MT_NOR_FLASH,
        MT_SMC_S3C2410
};

#endif /* _VIVI_BOOT_KERNEL_H_ */

 
    上面就是vivi支持的media_type,現在此開發板是MT_SMC_S3C2410,也就是nand flash memory的選擇部分。
 
    ·獲取nand flash的kernel分區信息,爲下載做好準備
 

kernel_part = get_mtd_partition("kernel");
                        if (kernel_part == NULL) {
                                printk("Can't find default 'kernel' partition\n");
                                return;
                        }
                        from = kernel_part->offset;
                        size = kernel_part->size;

 
    這裏獲得了kernel所在nand flash的起始地址和大小。這裏應該注意,雖然kernel_part->offset是偏移量,但是這個偏移是相對於0x00000000而言,所以這時的offset就是對應的起始地址。當然,對nand flash來說,這裏的地址並非是內存映射,需要做一系列的變化,具體是在nand_read_ll函數中,前面的基本實驗已經做過了。
 
    ·啓動內核
 

boot_kernel(from, size, media_type);

 
    利用前面得到的media_type,from,size就可以來啓動內核了,當然還有多步工作要去做。具體包括如下內容:
 
(1)獲取內存基地址
 

boot_mem_base = get_param_value("boot_mem_base", &ret);

 
    在vivi中,sdram是從0x30000000開始的,所以這裏的boot_mem_base就是0x30000000.
 
(2)把kernel映象從nand flash複製到sdram的固定位置
 

    to = boot_mem_base + LINUX_KERNEL_OFFSET;
    printk("Copy linux kernel from 0x%08lx to 0x%08lx, size = 0x%08lx ... ",
        from, to, size);
    ret = copy_kernel_img(to, (char *)from, size, media_type);

 
    這裏LINUX_KERNEL_OFFSET是0x8000,關於爲什麼是0x8000,這是歷史原因造成的,是Linux內核的一個約定,具體可以查看Linux內核的源代碼中的arch/arm/kernel/head_armv.S,如下:
 

/*
 * We place the page tables 16K below TEXTADDR. Therefore, we must make sure
 * that TEXTADDR is correctly set. Currently, we expect the least significant
 * "short" to be 0x8000, but we could probably relax this restriction to
 * TEXTADDR > PAGE_OFFSET + 0x4000
 *
 * Note that swapper_pg_dir is the virtual address of the page tables, and
 * pgtbl gives us a position-independent reference to these tables. We can
 * do this because stext == TEXTADDR
 *
 * swapper_pg_dir, pgtbl and krnladr are all closely related.
 */

 
    可以看出,TEXTADDR就是stext的地址,本開發板上爲0x30008000,在0x30008000往下,會放置16K的頁表,預計是 0x8000.不過此處可能會放鬆這個限制。另外,我們的一些參數也會放到內存起始區域。這在後面就可以看到。總之,這個地方的位置 boot_mem_base也就是kernel的第一條指令所在地,最後的程序跳轉要跳到這個位置。
 
(3)驗證magic number
 

    if (*(ulong *)(to + 9*4) != LINUX_ZIMAGE_MAGIC) {
        printk("Warning: this binary is not compressed linux kernel image\n");
        printk("zImage magic = 0x%08lx\n", *(ulong *)(to + 9*4));
    } else {
        printk("zImage magic = 0x%08lx\n", *(ulong *)(to + 9*4));
    }

 
    這個地方是判斷是否有zImage的存在,而zImage的判別的magic number爲0x016f2818,這個也是和內核約定好的。你可以用ultra-edit32查看一下zImage,這是我的zImage的頭的部分內容(注意,爲小端存放格式):
 
00000000h: 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1
00000010h: 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1 00 00 A0 E1
00000020h: 02 00 00 EA 18 28 6f 01 00 00 00 00 DB 86 09 00
 
    至於爲什麼magic number在0x00000024這個位置,需要分析zImage是如何生成的,它的內容是什麼,起始的幾個字節是什麼,這部分內容放到Linux kernel端進行深入分析。不過在這裏應該提一句,此處的驗證是考慮到Linux kernel相對比較大,而嵌入式系統的資源受限,爲了節省資源,一般會將Linux kernel來壓縮成zImage格式(識別方式就是在第9個字後有magic number0x016f2818);但是應該明確,這步工作並非是必需的。因爲如果內核比較小,爲了加快啓動速度,我可以不使用壓縮的映象,直接採用非壓縮映象,那麼vivi此處應該把無法找到maigc number的提示更改爲printk("this binary is not compressed linux kernel image\n");。就Linux kernel來說,啓動中支持壓縮映象和非壓縮映象兩種啓動方式,不管是那種啓動方式,第一條指令的地址總是boot_mem_base,只不過放在這裏的指令並非一定是真正的kernel啓動指令。這個在後面會詳細分析Linux kernel啓動方式。
 
(4)設置Linux參數
 

setup_linux_param(boot_mem_base + LINUX_PARAM_OFFSET);

 
    現在看一下setup_linux_param的具體動作。
 

static void setup_linux_param(ulong param_base)
{
    struct param_struct *params = (struct param_struct *)param_base; 
    char *linux_cmd;

    //第一步:打印出param_base的基地址,這裏就是0x30000100

    //這裏的這個位置實際上是約定的,預留了256字節

    //然後初始化param_struct這個數據結構
    printk("Setup linux parameters at 0x%08lx\n", param_base);
    memset(params, 0, sizeof(struct param_struct));

    //填寫params的兩個成員

    //Linux kernel採用了頁表方式,設置頁表的大小,這裏是4K
    params->u1.s.page_size = LINUX_PAGE_SIZE;
    params->u1.s.nr_pages = (DRAM_SIZE >> LINUX_PAGE_SHIFT);


    /* set linux command line */
    linux_cmd = get_linux_cmd_line();
    if (linux_cmd == NULL) {
        printk("Wrong magic: could not found linux command line\n");
    } else {

        //把命令行參數複製到params的commandline成員
        memcpy(params->commandline, linux_cmd, strlen(linux_cmd) + 1);
        printk("linux command line is: \"%s\"\n", linux_cmd);
    }
}

   
    如上,把不相關部分去掉了,加了註釋。可以看出,這裏就設置了param_struct必需的三個成員,核心是commandline。關於param_struct在linux內核的【include/arm/setup.h】中,各個成員的含義是:
 

/*
 * Usage:
 * - do not go blindly adding fields, add them at the end
 * - when adding fields, don't rely on the address until
 * a patch from me has been released
 * - unused fields should be zero (for future expansion)
 * - this structure is relatively short-lived - only
 * guaranteed to contain useful data in setup_arch()
 */

#define COMMAND_LINE_SIZE 1024

/* This is the old deprecated way to pass parameters to the kernel */
struct param_struct {
    union {
        struct {
            unsigned long page_size; /* 0 */
            unsigned long nr_pages; /* 4 */
            unsigned long ramdisk_size; /* 8 */
            unsigned long flags; /* 12 */
#define FLAG_READONLY 1
#define FLAG_RDLOAD 4
#define FLAG_RDPROMPT 8
            unsigned long rootdev; /* 16 */
            unsigned long video_num_cols; /* 20 */
            unsigned long video_num_rows; /* 24 */
            unsigned long video_x; /* 28 */
            unsigned long video_y; /* 32 */
            unsigned long memc_control_reg; /* 36 */
            unsigned char sounddefault; /* 40 */
            unsigned char adfsdrives; /* 41 */
            unsigned char bytes_per_char_h; /* 42 */
            unsigned char bytes_per_char_v; /* 43 */
            unsigned long pages_in_bank[4]; /* 44 */
            unsigned long pages_in_vram; /* 60 */
            unsigned long initrd_start; /* 64 */
            unsigned long initrd_size; /* 68 */
            unsigned long rd_start; /* 72 */
            unsigned long system_rev; /* 76 */
            unsigned long system_serial_low; /* 80 */
            unsigned long system_serial_high; /* 84 */
            unsigned long mem_fclk_21285; /* 88 */
        } s;
        char unused[256];
    } u1;
    union {
        char paths[8][128];
        struct {
            unsigned long magic;
            char n[1024 - sizeof(unsigned long)];
        } s;
    } u2;
    char commandline[COMMAND_LINE_SIZE];
};

 
    如上,具體選項的含義,可以參考【Documentation/arm/setup】,這裏僅就用到的三個來進行解釋。
 

page_size

   This parameter must be set to the page size of the machine, and
   will be checked by the kernel.

 nr_pages

   This is the total number of pages of memory in the system. If
   the memory is banked, then this should contain the total number
   of pages in the system.

   If the system contains separate VRAM, this value should not
   include this information.
 commandline

   Kernel command line parameters. Details can be found elsewhere.

 
    可以看出,這步的設置工作還是非常簡單的。現在使用的頁表大小爲4K,也就是page_size的值。因爲現在使用的sdram是64M,總頁表項自然就是64M/page_size,也就是進行簡單的右移就可以了(4K等效右移12位)。後面就是獲取命令行參數的地址,然後填充comandline成員,最長的限度爲1024.
 
    至此,vivi設置參數就完成了,約定參數的起始地址爲boot_mem_base+0x0100處。這個地方是否需要作爲參數傳遞給kernel,就需要與內核配合了。如果像Linux kernel約定的boot_mem_base+0x8000處存放內核映象一樣,Linux kernel對s3c2410的支持同樣可以約定參數固定存放於boot_mem_base+0x0100。如果沒有此約定,那麼就需要傳遞參數首地址了。
 
(5)獲取機器號
 

mach_type = get_param_value("mach_type", &ret);

 
    這個號是固定的。可以參考arch/arm/tools/mach-types。這裏列出了所有支持的機器號。應該是按照先後的支持順序排列。可以看到smdk2410爲:
 

smdk2410        S3C2410_SMDK        SMDK2410        193

 
(6)啓動內核
 

call_linux(0, mach_type, to);

 
    到這裏纔算是真正啓動內核了,使用內嵌彙編寫的。這裏的三個參數,根據APCS原則,應該分別給R0,R1, R2.這樣也就是說,現在:
 
    · R0 設置爲0
    · R1 machine type number(193)
    · R2 內核的第一條指令的起始地址(注意,這裏並非參數表的首地址)
 

void call_linux(long a0, long a1, long a2)
{
    cache_clean_invalidate();
    tlb_invalidate();

__asm__(
    "mov    r0, %0\n"
    "mov    r1, %1\n"
    "mov    r2, %2\n"
    "mov    ip, #0\n"
    "mcr    p15, 0, ip, c13, c0, 0\n"    /* zero PID */
    "mcr    p15, 0, ip, c7, c7, 0\n"    /* invalidate I,D caches */
    "mcr    p15, 0, ip, c7, c10, 4\n"    /* drain write buffer */
    "mcr    p15, 0, ip, c8, c7, 0\n"    /* invalidate I,D TLBs */
    "mrc    p15, 0, ip, c1, c0, 0\n"    /* get control register */
    "bic    ip, ip, #0x0001\n"        /* disable MMU */
    "mcr    p15, 0, ip, c1, c0, 0\n"    /* write control register */
    "mov    pc, r2\n"
    "nop\n"
    "nop\n"
    : /* no outpus */
    : "r" (a0), "r" (a1), "r" (a2)
    );

}

 
    彙編很簡潔。參考前面booting文檔,就是做上述工作。現在對R0、R1、R2參數傳遞完成,不過R2在這裏並非tag的首地址,因爲採用的是 param_struct模式,所以可以猜測kernel的arch(實際上就是HAL層)肯定有對應的默認地址起始地址(這裏是 0x30000100)。其餘部分,中斷都關閉了,PID爲0,I cache和D cache都禁止,write buffer清理,I D TLBS也禁止,禁止MMU。最後mov pc, r2則跳轉到內核映象的第一條指令位置。
 
    到這裏,vivi的使命完全完成了!後續的工作就交給kernel了。
 
    爲了對參數傳遞這個情景分析清楚,所以還必須分析Linux kernel如何啓動。這部分不打算過多的深入細節,首先應該從整體上分析。然而,還是應該藉助代碼才能理解的更爲清晰。這裏,有taoyuetao的 Linux啓動分析系列文章可以參考,我想,他分析Linux啓動也是如同我分析vivi一樣,一步一步走過來的。借鑑一下,省去了我不少勞動,在此感謝。後續的描述中Linux kernel啓動部分借鑑taoyuetao的經驗,但是對其進行了擴展,增加了zImage如何生成的更爲詳細的解釋。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章