實驗1後篇——引導linux與uboot命令仿真



        經過了實驗1洗禮,會讓人感覺只是只是打開了一個玩具操作系統的模擬過程,雖然能讓人由一個直觀與形象的理解,但是還是沒有實在的用途。當然了,可以這麼說了。所以如上所調侃而言,我們爲了讓我們的實驗進行的更有意義,進而推出後篇,主要將實驗1所學到的內容與實際使用相結合,達到融會貫通的地步,進而能夠更理解實驗內容。

        現在的開源代碼有兩個很有用的部分而且是系統級的——linux內核與uboot。幾乎所有嵌入式的電子設備都會用到它們。uboot用於初始化開發板,同時引導linux內核。而linux內核作爲操作系統的核心存在也是我們不得不去理解。而我們引導pc的實驗,就是爲了引導操作系統而存在,所以我們一個實際的用途就是嘗試去引導linux內核。而uboot一個很重要與實用的功能就是有友好的終端調試功能,而它的實現也是值得讓人去分析與模擬的,因爲對於編寫uboot命令有幫助,同時在linux內核的代碼中也會經常用到,所以它對於操作系統的代碼理解有很好的幫助。

       針對如上描述,本文從兩個方面來實現實驗的擴展——引導linux內核(我用的版本爲4.0.2)與模擬uboot命令行實現。

       )引導linux內核(4.0.2)

       根據實驗1的引導內核的流程,我們需要做的事情是:編譯內核,製作包含內核的鏡像,然後加載內核到內存,最後再運行之。

     a)編譯內核與製作內核鏡像

   (1)下載內核——從內核官網上下載,url:https://www.kernel.org/pub/linux/kernel/v4.x/

     (2)編譯內核:make menuconfig配置內核,保存之後再make一下,生成的內核在arch/x86_64/boot/bzImage。我的系統是x86_64位的,所以默認編譯出來再x86_64位下,而且這是一個壓縮的內核。

   (3)製作內核鏡像

    根據實驗的方法寫成腳本,將編譯出來的bzImage與我們寫的boot-loader直接連接成內核鏡像。具體如下:

<span style="font-size:12px;">if [ -z "$1" -o -z "$2" ];then
	echo "usage: create-bkl-img.sh boot-where-is kernel-where-is"
	exit
fi
boot_path=$1
kl_path=$2
echo "第一步:創建爲空的鏡像bkl0.img——大小爲兩個之和"
boot_size=$(stat -c%s $boot_path)
kl_size=$(stat -c%s $kl_path)
img_size=$(( ($boot_size+$kl_size)/512+1 ))
echo "img_size:$img_size"
dd if=/dev/zero of=bkl0.img count=$img_size bs=512 
echo "第二步:將boot程序放入bkl0.img"
dd if=$boot_path of=bkl0.img count=1 bs=512 conv=notrunc
echo "第三步:將kl放入bkl0.img"
dd if=$kl_path of=bkl0.img count=$(( $img_size -1 )) bs=512 seek=1 conv=notrunc</span>
<em><strong>
</strong></em>

         b)加載內核與運行之

    因爲我們編譯的linux是壓縮的,而內核是會自解壓然後運行的,所以我們要做的工作就將內核加載到對應的內核地址,然後創建對應的執行環境,運行之就可以了。但這些信息應該怎麼知道呢?通過linux官方文檔來詳細描述。

        (1)linux的引導協議——根據內核源碼中的文檔:Document/x86/boot.txt。

       Linux引導協議有兩種協議內存映像,對於傳統的內核或者沒有壓縮的內核(或者引導協議版本<2.02)有如下的映像,目前我們沒有使用它,而是使用後面的:

引導協議<2.02的映像圖

        如上圖所示:

    0x00000-0x10000爲MBR使用

    0x10000-0x90000爲保護模式的內核所在,所以內核最大爲512kByte;

    0x90000-0x90200爲內核傳統的引導扇區;

    0x90200-0x98000爲內核實模式代碼;

    0x98000-0x9A000爲內核實模式的參數,堆棧空間;

    因爲此種模式我們沒有使用,所以我們只做介紹。


引導協議>=2.02的映像圖

       如上圖所示:

       (a)0x00000-0x10000MBR使用

       (b)X-X+0x08000爲內核傳統的引導扇區與內核實模式代碼(32KByte)

       (c)X+0x08000-X+0x10000爲內核實模式的堆棧空間(8KByte)

      (d)X+10000-0xA0000之間設置傳遞給內核的參數;

      (e)0x100000之後的爲保護模式的內核所在,這就沒有限制內核的大小。

       這裏出現的X爲一個地址,由以上的內存分配,可以發現它僅僅是一個內核映像的偏移地址;而官方給出的解釋儘可能的低,只要boot-loader引導允許。因爲我們的MBR已經佔據了0x7C000x7E00-1的範圍,所以我們的代碼將X設置爲0x7E00即可。

      對於如上的內存映像分析,可以發現它是在BIOS的內存映像的基礎上,更加細化了RAM區域。同時對MBR的代碼運行空間提出了要求,只能使用X地址之下的空間——即範圍a的描述。

      爲了更簡單與直觀的介紹如上內存空間範圍分佈,我們需要去了解內核文件(bzImage)的構成以及運行模式的切換。但是爲了瞭解內核文件的構成,就必需去分析內核的編譯流程。

       在分析開始,先看一下圖,用於跟蹤編譯流程打印的圖:

       (a)瀏覽編譯的makefile可以發現:執行make,默認爲的目標在arch/x86/Makefile中。

# Default kernel to build
all: bzImage #默認的編譯目標

# KBUILD_IMAGE specify target image being built
KBUILD_IMAGE := $(boot)/bzImage

bzImage: vmlinux #bzImage依賴於vmlinux
	@echo "$(obj)>>make vmlinux end;"
ifeq ($(CONFIG_X86_DECODER_SELFTEST),y)
	$(Q)$(MAKE) $(build)=arch/x86/tools posttest
endif
	@echo "$(obj)>> $(KBUILD_IMAGE)>>>>"
	$(Q)$(MAKE) $(build)=$(boot) $(KBUILD_IMAGE) #當編譯vmlinux完之後,再到arch/x86/boot中編譯$(boot)/bzImage
	$(Q)mkdir -p $(objtree)/arch/$(UTS_MACHINE)/boot
	$(Q)ln -fsn ../../x86/boot/bzImage $(objtree)/arch/$(UTS_MACHINE)/boot/$@

        當執行make或者make all時,最終就會生成bzImage,而bzImage又是依賴vmlinux(它實際是內核代碼編譯出來未壓縮的內核)。我們現在就需要分析vmlinux如何轉換成bzImage的過程。如下爲分析源碼頂層Makefile的生成vmlinux的過程:

      i)編譯所有的目錄文件:head-y/init-*/core-*/driver-*/net-*/libs-*,鏈接生成vmlinux

       ii)執行+$(call if_changed,link-vmlinux),執行cmd_link-vmlinux命令。

       (b)瞭解bzImage的形成過程:

       根據如上的makefile的編譯流程,當編譯vmlinux完成之後,就進入到arch/x86/boot中執行 arch/x86/boot/bzImage目標如下:

<span style="font-size:12px;">cmd_image = $(obj)/tools/build $(obj)/setup.bin $(obj)/vmlinux.bin \
                               $(obj)/zoffset.h $@

$(obj)/bzImage: $(obj)/setup.bin $(obj)/vmlinux.bin $(obj)/tools/build FORCE
        $(call if_changed,image)
        @echo 'Kernel: $@ is ready' ' (#'`cat .version`')'</span>

       由此發現:bzImage是由tools下的build工具將setup.bin,vmlinux.bin,zoffset.h合併而成。

       所以我們需要分析setup.bin,vmlinux.bin的構成。首先分析setup.bin的形成,如下:

<span style="font-size:12px;">SETUP_OBJS = $(addprefix $(obj)/,$(setup-y))
……….
LDFLAGS_setup.elf       := -T
$(obj)/setup.elf: $(src)/setup.ld $(SETUP_OBJS) FORCE
        @echo "<<$(obj)link setup.elf>>"
        $(call if_changed,ld)

OBJCOPYFLAGS_setup.bin  := -O binary
$(obj)/setup.bin: $(obj)/setup.elf FORCE
        @echo "<<1>>setup.elf to setup.bin"$(obj)
        $(call if_changed,objcopy)</span>

       由以上的編譯流程發現:setup.bin是由setup-y下所包含的源文件編譯後,再通過鏈接腳本 setup.ld生成setup.elf,最後由 objcopyelf文件轉換爲二進制文件。分析鏈接腳本——setup.ld:

<span style="font-size:12px;">OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(_start)/*進入點*/

SECTIONS
{
	. = 0;
	.bstext		: { *(.bstext) }
	.bsdata		: { *(.bsdata) }

	. = 495;/*從1EF開始鏈接header分區,在header.S中定義,從保證hdr爲0x1f1*/
	.header		: { *(.header) }
	.entrytext	: { *(.entrytext) }
	.inittext	: { *(.inittext) }
	.initdata	: { *(.initdata) }
	__end_init = .;/*如上爲setup.bin頭信息*/

	.text		: { *(.text) }
	.text32		: { *(.text32) }

	. = ALIGN(16);
	.rodata		: { *(.rodata*) }

	.videocards	: {/*顯示器的驅動*/
		video_cards = .;
		*(.videocards)
		video_cards_end = .;
	}

	. = ALIGN(16);
	.data		: { *(.data*) }

	.signature	: {
		setup_sig = .;
		LONG(0x5a5aaa55)
	}


	. = ALIGN(16);
	.bss		:
	{
		__bss_start = .;
		*(.bss)
		__bss_end = .;
	}
	. = ALIGN(16);
	_end = .;

	/DISCARD/ : { *(.note*) }

	/*
	 * The ASSERT() sink to . is intentional, for binutils 2.14 compatibility:
	 */
	. = ASSERT(_end <= 0x8000, "Setup too big!");/*通過這裏發現setup.elf的大小一定小於0x8000-32K,這就是內核引導扇區的部分**/
	. = ASSERT(hdr == 0x1f1, "The setup header has the wrong offset!");/*linux內核引導頭的地址必需爲0x1f1,在header.S中*/
	/* Necessary for the very-old-loader check to work... */
	. = ASSERT(__end_init <= 5*512, "init sections too big!");/*初始化的分區的大小限制**/

}</span>

         然後分析vmlinux.bin的構成,如下:

<span style="font-size:12px;">OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S
$(obj)/vmlinux.bin: $(obj)/compressed/vmlinux FORCE
        @echo "$(obj)>>change vmlinux to vmlinux.bin"
        $(call if_changed,objcopy)
…….
$(obj)/compressed/vmlinux: FORCE
        @echo "$(obj)>>create vmlinux@compressed"
        $(Q)$(MAKE) $(build)=$(obj)/compressed $@</span>

        由上分析可以看出,vmlinux.bin是由arch/x86/boot/compressed/vmlinux進行objcopy而來。從而我們需要分析arch/x86/boot/compressedMakefile下分析vmlinux.bin的實現,詳情見對應的makefile如下爲總結的步驟:

         a)如上編譯內核源文件得到的vmlinux,通過objcopy -R .comment -S vmlinux vmlinux.bin

         b)如果需要重定向:則通過vmlinux生成vmlinux.relocs

         c)再將vmlinux.binvmlinux.relocs合併成vmlinux.bin.all,同時壓縮爲vmlinux.bin.gz

         d)根據vmlinux.bin.gz,通過mkpiggy生成彙編文件piggy.S

         e)piggy.S編譯到vmlinux(壓縮之後的內核被放在程序段.rodata..compressed中)

         f)最後將vmlinux通過objcopy -R .comment -S vmlinux vmlinux.bin

        通過如上分析得到vmlinux.binsetup.bin剩下的就是如何將兩個bin檔合併成bzImage,這就需要分析arch/x86/boot/tools/build.c的源文件。簡單分析build功能爲將setup.binvmlinux.bin進行合併,同時也計算相關大小與校驗和,從而修改相關文件中的設置。

       如上分析流程我已經在相關Makefile中添加調試信息,make時就會將如上流程給打印出來;如上流程只是大致的縷了一下思路,而細節的分析過程,需要讀者自己去分析;這不是本節的重點。 通過對內核編譯流程的分析可以看出內核文件的基本結構與:由內核的編譯流程可以發現bzImagesetup.binvmlinux.bin兩部分組成,vmlinux.bin主要包含兩部分:引導代碼與壓縮內核。

       所以如上的範圍b其實就是setup.bin,而範圍c就是爲setup.bin執行時的堆棧空間(因爲用到c語言了),範圍d爲傳遞給內核的參數,比如:”console=ttyS1;root=/dev/sda1”等;範圍evmlinux.bin

       當理解了引導協議映像,還需要介紹的是內核引導頭配置在setup.bin1F1處,其中包含了內核啓動的必要參數,如下:


<span style="font-size:12px;">Offset	Proto	Name		Meaning
/Size

01F1/1	ALL(1	setup_sects	The size of the setup in sectors
01F2/2	ALL	root_flags	If set, the root is mounted readonly
01F4/4	2.04+(2	syssize		The size of the 32-bit code in 16-byte paras
01F8/2	ALL	ram_size	DO NOT USE - for bootsect.S use only
01FA/2	ALL	vid_mode	Video mode control
01FC/2	ALL	root_dev	Default root device number
01FE/2	ALL	boot_flag	0xAA55 magic number
0200/2	2.00+	jump		Jump instruction
0202/4	2.00+	header		Magic signature "HdrS"
0206/2	2.00+	version		Boot protocol version supported
0208/4	2.00+	realmode_swtch	Boot loader hook (see below)
020C/2	2.00+	start_sys_seg	The load-low segment (0x1000) (obsolete)
020E/2	2.00+	kernel_version	Pointer to kernel version string
0210/1	2.00+	type_of_loader	Boot loader identifier
0211/1	2.00+	loadflags	Boot protocol option flags
0212/2	2.00+	setup_move_size	Move to high memory size (used with hooks)
0214/4	2.00+	code32_start	Boot loader hook (see below)
0218/4	2.00+	ramdisk_image	initrd load address (set by boot loader)
021C/4	2.00+	ramdisk_size	initrd size (set by boot loader)
0220/4	2.00+	bootsect_kludge	DO NOT USE - for bootsect.S use only
0224/2	2.01+	heap_end_ptr	Free memory after setup end
0226/1	2.02+(3 ext_loader_ver	Extended boot loader version
0227/1	2.02+(3	ext_loader_type	Extended boot loader ID
0228/4	2.02+	cmd_line_ptr	32-bit pointer to the kernel command line
022C/4	2.03+	initrd_addr_max	Highest legal initrd address
0230/4	2.05+	kernel_alignment Physical addr alignment required for kernel
0234/1	2.05+	relocatable_kernel Whether kernel is relocatable or not
0235/1	2.10+	min_alignment	Minimum alignment, as a power of two
0236/2	2.12+	xloadflags	Boot protocol option flags
0238/4	2.06+	cmdline_size	Maximum size of the kernel command line
023C/4	2.07+	hardware_subarch Hardware subarchitecture
0240/8	2.07+	hardware_subarch_data Subarchitecture-specific data
0248/4	2.08+	payload_offset	Offset of kernel payload
024C/4	2.08+	payload_length	Length of kernel payload
0250/8	2.09+	setup_data	64-bit physical pointer to linked list
				of struct setup_data
0258/8	2.10+	pref_address	Preferred loading address
0260/4	2.10+	init_size	Linear memory required during initialization
0264/4	2.11+	handover_offset	Offset of handover entry point</span>

    其中會用到的信息爲setup_sects,syssize,cmd_line_ptr,code32_start。setup_sects表示setup.bin所佔有的扇區數;syssize表示vmlinux.bin的大小以2Byte爲單位; cmd_line_ptr表示傳遞給內核參數的地址;code32_start表示保護模式開始地址0x100000.

        通過如上流程的解析知道我們要引導linux,首先要根據引導協議映像來分塊加載內核到內存,然後根據內容創建內核實模式執行環境,再設置相關的程序狀態然後執行setup.bin程序進入點即可。在加載內核時需要注意,因爲內核的保護模式的代碼位於0x100000處於8088無法訪問的地址,所以我們需要首先進入32位保護模式。當拷貝鏡像成功之後,需要將處理器運行模式從32位保護模式切換到8088的狀態,同時需要設置實模式運行環境如下:

<span style="font-size:12px;">seg = base_ptr >> 4;

cli();/* Enter with interrupts disabled! */

/* Set up the real-mode kernel stack */
_SS = seg;//設置堆棧段爲base_ptr指向的段,我們實際爲0x7e0
_SP = heap_end;//設置棧頂爲0xfffc的位置

_DS = _ES = _FS = _GS = seg;//設置所有段都爲0x7e0
jmp_far(seg+0x20, 0);/* Run the kernel *///跳轉到地址0x800執行即可(設置斷點b *0x8000即可進行內核的單步調試)。</span>

       所以我們總結如下流程,更詳細的就是看附件我寫的引導了主要爲main-linux.c與boot.S中的代碼:

//設置引導協議的偏移地址,因爲它要儘可能的低,所以我們只需要將放在MBR之後
#define KL_BOOT_START (0x7c00+512) //0x7c00+0x200=0x7e00
//內核實模式引導代碼的最大值——setup.bin
#define KL_BOOT_TEXT_LEN	0x8000
//內核實模式代碼所使用的最大空間
#define KL_BOOT_ALL_LEN	0x10000
//引導協議文件頭地址
#define BOOT_PRO_SETUP_SECTS_OFFSET 0X1F1
#define BOOT_PRO_SETUP_SECTS_SIZE   1//by sectors
//將引導頭轉換爲地址指針
#define BOOT_PRO_HEADER ((P_BootSectorsHeader)(KL_BOOT_START+BOOT_PRO_SETUP_SECTS_OFFSET))
...............
void
bootmain(void)
{
//讀取內核的引導分區到內存中[KL_BOOT_START,KL_BOOT_START+KL_BOOT_TEXT_LEN),因爲引導分區的最大值爲32k=0x20000
//設置傳遞給內核的命令行的地址——KL_BOOT_CMD_ADDR
BOOT_PRO_HEADER->cmd_line_ptr = KL_BOOT_CMD_ADDR;
//加載實際保護模式的內核部分到0x100000
readseg(0x100000,BOOT_PRO_HEADER->syssize<<4,(BOOT_PRO_HEADER->setup_sects+1)*SECTSIZE);
...........
}
...............
#如下爲boot.S的彙編
 call bootmain #調用c語言用拷貝內核映像——setup.bin/vmlinux.bin到對應的位置
  #如下爲切換到16位實模式下
  lgdt gdtdesc
movw    $GRUB_MEMORY_MACHINE_PSEUDO_REAL_DSEG, %ax
        movw    %ax, %ds
        movw    %ax, %es
        movw    %ax, %fs
        movw    %ax, %gs
        movw    %ax, %ss
   ljmp $GRUB_MEMORY_MACHINE_PSEUDO_REAL_CSEG, $tmpcseg
tmpcseg:
.code16
  movl %cr0,%eax
  andb 0xfe,%al
  movl %eax,%cr0
 # ljmp $0x0,$back2code16
back2code16:  
#設置堆棧段到0x7e00+0x10000的位置
  mov $0x07e0,%ax
  mov %ax,%ss
  mov $0xfffc,%sp
  #mov %ax,%es
  mov %ax,%fs
  mov %ax,%gs
#拷貝內核啓動參數到0x7e00+0x10000=0x17e000
copy_boot_cmd:
  xorw %cx,%cx
  movb boot_cmd_len,%cl
  movw $0x17e0,%bx
  movw %bx,%es
  movw $0x07c0,%bx
  movw %bx,%ds
  movw $(boot_cmd-0x7c00),%si
  xorw %di,%di

  rep
  movsb
#設置數據段爲0x7e0,根據引導協議需要將內核實模式的偏移地址KL_BOOT_START設置到段地址中
  mov %ax,%es
  mov %ax,%ds
 # pushw $0x07c0
 # pushw $0x0
 # lretw
#執行setup.bin的代碼爲0x7e00+0x200=0x8000.
  ljmp $0x800,$0x0

       爲了更具體的分析我們內核引導流程,我們還需要去查看相關代碼與內核早期的執行流程,這樣不僅能夠對代碼有深入的理解,同時當我們在實現內核引導時出現問題也能正常調試:

       (1)分析setup.bin的執行流程,根據setup.ld的分析,程序進入點爲0x200,當被加載到0x7e00時,需要從0x8000出執行,通過對setup.ld的分析,也以發現內核引導頭配置hdr0x1F1的位置(setup.ld決定),通過查看源代碼(arch/x86/boot/header.S)分析基本流程:

<span style="font-size:12px;">.......
        .section ".header", "a"
        .globl  sentinel
sentinel:       .byte 0xff, 0xff        /* Used to detect broken loaders */

        .globl  hdr#這裏定義了hdr位置爲0x1f1
hdr:
setup_sects:    .byte 0                 /* Filled in by build.c ,這些值會被build.c根據實際情況修改之*/
root_flags:     .word ROOT_RDONLY
syssize:        .long 0                 /* Filled in by build.c */
ram_size:       .word 0                 /* Obsolete */
vid_mode:       .word SVGA_MODE
root_dev:       .word 0                 /* Filled in by build.c */
boot_flag:      .word 0xAA55

        # offset 512, entry point #這裏爲setup.bin的進入點

        .globl  _start
_start:
                # Explicitly enter this as bytes, or the assembler
                # tries to generate a 3-byte jump here, which causes
                # everything else to push off to the wrong offset.
                .byte   0xeb            # short (2-byte) jump
                .byte   start_of_setup-1f
1:
.......…
code32_start:                           # here loaders can put a different
                                        # start address for 32-bit code.默認的32位執行地址
                .long   0x100000        # 0x100000 = default for big kernel
..........
   .section ".entrytext", "ax"
start_of_setup:#這裏纔是真正的執行點,由_start跳轉到此
# Force %es = %ds
        movw    %ds, %ax
        movw    %ax, %es
        cld
.....…
# Jump to C code (should not return) 跳入setup.bin的main函數
        calll   main</span>

   當程序運行到main函數(arch/x86/boot/main.c)後有如下流程:

    main——>arch/x86/boot/main.c:
void main(void)
{
	/* First, copy the boot header into the "zeropage" */
	copy_boot_params();

	/* Initialize the early-boot console */
	console_init();
	if (cmdline_find_option_bool("debug"))
		puts("early console in setup code\n");

	/* End of heap check */
	init_heap();

	/* Make sure we have all the proper CPU support */
	if (validate_cpu()) {
		puts("Unable to boot - please use a kernel appropriate "
		     "for your CPU.\n");
		die();
	}

	/* Tell the BIOS what CPU mode we intend to run in. */
	set_bios_mode();

	/* Detect memory layout */
	detect_memory();

	/* Set keyboard repeat rate (why?) and query the lock flags */
	keyboard_init();

	/* Query MCA information */
	query_mca();

	/* Query Intel SpeedStep (IST) information */
	query_ist();

	/* Query APM information */
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
	query_apm_bios();
#endif

	/* Query EDD information */
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
	query_edd();
#endif

	/* Set the video mode */
	set_video();
	//printf("<<before go_to_protected_mode>>\n");
	/* Do the last things and invoke protected mode */
	<strong>go_to_protected_mode();//main函數調用它進入保護模式,從而調用vmlinux.bin代碼</strong>
}</span>

   go_to_protected_mode-arch/x86/boot/pm.c:

void go_to_protected_mode(void)
{
	/* Hook before leaving real mode, also disables interrupts */
	printf("step1>>>\n");
	realmode_switch_hook();
	/* Enable the A20 gate */
	printf("step2>>>\n");
	if (enable_a20()) {
		puts("A20 gate not responding, unable to boot...\n");
		die();
	}

	printf("step333>\n");
	/* Reset coprocessor (IGNNE#) */
	reset_coprocessor();

	printf("step4>>>\n");
	/* Mask all interrupts in the PIC */
	mask_all_interrupts();

	printf("step5>>>\n");
	/* Actual transition to protected mode... */
	setup_idt();
	setup_gdt();
//	printf("step6>>>\n");
	protected_mode_jump(boot_params.hdr.code32_start,//這個地址爲0x100000,加載vmlinux.bin的地址
			    (u32)&boot_params + (ds() << 4));
}</span>

    這裏有一個細節需要注意的是c語言調用彙編語言定義的函數,通過寄存器傳遞參數的方式——通過反彙編可以知道如下:

 <span style="font-size:12px;">protected_mode_jump(boot_params.hdr.code32_start,
    164c:       66 81 c2 c0 47 00 00    add    $0x47c0,%edx
    1653:       66 a1 d4 49             mov    0x49d4,%eax
    1657:       66 e8 00 00 00 00       calll  165d <protected_mode_jump></span>

    protected_mode_jump-->arch/x86/boot/pmjump.S:

<span style="font-size:12px;">GLOBAL(protected_mode_jump)
	movl	%edx, %esi		# Pointer to boot_params table

	xorl	%ebx, %ebx
	movw	%cs, %bx
	shll	$4, %ebx
	addl	%ebx, 2f
	jmp	1f			# Short jump to serialize on 386/486
1:

	movw	$__BOOT_DS, %cx
	movw	$__BOOT_TSS, %di

	movl	%cr0, %edx
	orb	$X86_CR0_PE, %dl	# Protected mode
	movl	%edx, %cr0

	# Transition to 32-bit mode
	.byte	0x66, 0xea		# ljmpl opcode
2:	.long	in_pm32			# offset
	.word	__BOOT_CS		# segment
ENDPROC(protected_mode_jump)

	.code32
	.section ".text32","ax"
GLOBAL(in_pm32)
	# Set up data segments for flat 32-bit mode
	movl	%ecx, %ds
	movl	%ecx, %es
	movl	%ecx, %fs
	movl	%ecx, %gs
	movl	%ecx, %ss
	# The 32-bit code sets up its own stack, but this way we do have
	# a valid stack if some debugging hack wants to use it.
	addl	%ebx, %esp

	# Set up TR to make Intel VT happy
	ltr	%di

	# Clear registers to allow for future extensions to the
	# 32-bit boot protocol
	xorl	%ecx, %ecx
	xorl	%edx, %edx
	xorl	%ebx, %ebx
	xorl	%ebp, %ebp
	xorl	%edi, %edi

	# Set up LDTR to make Intel VT happy
	lldt	%cx

	jmpl	*%eax			# Jump to the 32-bit entrypoint,跳入到0x100000執行
ENDPROC(in_pm32)</span>

    由我們創建的映射引導映像可以發現當執行 protected_mode_jump完之後,程序會跳轉到vmlinux.bin進入點爲(0x100000)去執行,這個階段主要解壓縮內核,然後再運行實際的內核:

    vmlinux.bin的執行代碼從arch/x86/boot/compressed/head_64.S的第一行ENTRY(startup_32)開始執行,然後進入64位模式,跳轉到ENTRY(startup_64),最終執行如下代碼,進入c語言環境進行解壓縮內核:

 <span style="font-size:12px;">pushq   %rsi                    /* Save the real mode argument */
        movq    $z_run_size, %r9        /* size of kernel with .bss and .brk */
        pushq   %r9
        movq    %rsi, %rdi              /* real mode address */
        leaq    boot_heap(%rip), %rsi   /* malloc area for uncompression */
        leaq    input_data(%rip), %rdx  /* input_data */
        movl    $z_input_len, %ecx      /* input_len */
        movq    %rbp, %r8               /* output target address */
        movq    $z_output_len, %r9      /* decompressed length, end of relocs */
        call    decompress_kernel       /* returns kernel location in %rax */調用解壓縮內核代碼
        popq    %r9
        popq    %rsi

/*
 * Jump to the decompressed kernel.跳入解壓縮之後的內核。
 */
        jmp     *%rax</span>

   解壓縮內核decompress_kernel arch/x86/boot/compressed/misc.c實現。

<span style="font-size:12px;">asmlinkage __visible void *decompress_kernel(void *rmode, memptr heap,
                                  unsigned char *input_data,
                                  unsigned long input_len,
                                  unsigned char *output,
                                  unsigned long output_len,
                                  unsigned long run_size)
{
........
        debug_putstr("\nDecompressing Linux... ");
/**解壓縮內核**/
        decompress(input_data, input_len, NULL, NULL, output, NULL, error);
/**將解壓縮的elf文件對應的段放置到對應的地址上*/
        parse_elf(output);
        /*
         * 32-bit always performs relocations. 64-bit relocations are only
         * needed if kASLR has chosen a different load address.
         */
        if (!IS_ENABLED(CONFIG_X86_64) || output != output_orig)
                handle_relocations(output, output_len);
        debug_putstr("done.\nBooting the kernel.\n");
        return output;
}</span>
    當程序被解壓縮之後,因爲壓縮的內核爲elf文件,所以需要將對應段加載到相應的內核地址段,然後跳入到解壓縮後的內核進入點正式執行。

    以上的流程只是將編譯出來的內核加載到內存中,然後執行:真正的內核從startup_64(arch/x86/kernel/head_64.S)開始執行:

<span style="font-size:12px;">startup_64:
      .........
       movq    initial_code(%rip),%rax
        pushq   $0              # fake return address to stop unwinder
        pushq   $__KERNEL_CS    # set correct cs
        pushq   %rax            # target address in negative space
        lretq
......…
        __REFDATA
        .balign 8
        GLOBAL(initial_code)
        .quad   x86_64_start_kernel——程序跳入這裏執行。
        GLOBAL(initial_gs)</span>

   當從彙編語言執行完了之後,跳入到c語言的執行環境的入口 x86_64_start_kernelarch/x86/kernel/head64.c執行:

<span style="font-size:12px;">asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{
....…
        x86_64_start_reservations(real_mode_data);
}
void __init x86_64_start_reservations(char *real_mode_data)
{
        /* version is always not zero if it is copied */
        if (!boot_params.hdr.version)
                copy_bootdata(__va(real_mode_data));

        reserve_ebda_region();

        start_kernel();
}</span>

    當進入 x86_64_start_kernel執行之後,做了相關準備之後,就調用內核的正式進入點start_kernel()init/main.c,隨後就執行了內核的初始化代碼。

   二)模擬uboot的命令行實現

    接觸過uboot的軟件工程師,都會使用到命令行但是它的實現只有在系統的編程環境纔會用到。其實,它的實現原理很簡單,是使用了鏈接器的技巧——即由鏈接器將所有的特定分區的數據鏈接到一起,然後通過分區的開始地址與結束地址進行訪問。這樣做的好處是命令之間都是相互獨立的,可以單獨放單一的文件中實現,對於該分區的命令可以統一處理,簡單。詳細先由uboot(u-boot-2010.06)實例來直觀描述。

  (1)命令的定義,echo(common/cmd_echo.c)爲例:

<span style="font-size:12px;">U_BOOT_CMD(
	echo,	CONFIG_SYS_MAXARGS,	1,	do_echo,
	"echo args to console",
	"[args..]\n"
	"    - echo args to console; \\c suppresses newline"
);   
   //進一步查看宏U_BOOT_CMD的定義(include/common.h):
#define Struct_Section  __attribute__ ((unused,section (".u_boot_cmd")))

#ifdef  CONFIG_SYS_LONGHELP

#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}</span>

    由此可以發現該宏就是定義了一個數據結構Struct_Section的變量,而這個變量在.u_boot_cmd分區。而這個分區的定義在u-boot.lds(arch\arm\cpu\s3c44b0爲例)中:

<span style="font-size:12px;">__u_boot_cmd_start = .;
	.u_boot_cmd : { *(.u_boot_cmd) }
	__u_boot_cmd_end = .;</span>

(2)命令的實現:

<span style="font-size:12px;">int do_echo(cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
	int i;
	int putnl = 1;

	for (i = 1; i < argc; i++) {
		char *p = argv[i], c;

		if (i > 1)
			putc(' ');
		while ((c = *p++) != '\0') {
			if (c == '\\' && *p == 'c') {
				putnl = 0;
				p++;
			} else {
				putc(c);
			}
		}
	}

	if (putnl)
		putc('\n');

	return 0;
}</span>

(3)命令的引用:

<span style="font-size:12px;">static int complete_cmdv(int argc, char *argv[], char last_char, int maxv, char *cmdv[])
{
	cmd_tbl_t *cmdtp;
......
if (argc == 0) {
		/* output full list of commands */
		for (cmdtp = &__u_boot_cmd_start; cmdtp != &__u_boot_cmd_end; cmdtp++) {
			if (n_found >= maxv - 2) {
				cmdv[n_found++] = "...";
				break;
			}
			cmdv[n_found++] = cmdtp->name;
		}
		cmdv[n_found] = NULL;
		return n_found;
	}
.....
}</span>

   命令的使用最簡單的方法就是用一個指針來簡單遍歷即可。

   根據如上的實現過程我們也模擬實現之:

   (1)命令定義與實現——kerninfo爲例,詳細見附件的demo中的cmd_kerninfo.c的實現:

<span style="font-size:12px;">int do_kerninfo(int argc,char* const argv[])
{
	
	extern char _start[], entry[], etext[], edata[], end[];

	cprintf("Special kernel symbols:\n");
	cprintf("  _start                  %08x (phys)\n", _start);
	cprintf("  entry  %08x (virt)  %08x (phys)\n", entry, entry - KERNBASE);
	cprintf("  etext  %08x (virt)  %08x (phys)\n", etext, etext - KERNBASE);
	cprintf("  edata  %08x (virt)  %08x (phys)\n", edata, edata - KERNBASE);
	cprintf("  end    %08x (virt)  %08x (phys)\n", end, end - KERNBASE);
	cprintf("Kernel executable memory footprint: %dKB\n",
		ROUNDUP(end - entry, 1024) / 1024);
	return 1;
}
static __at_yiyecmd_section cmd_kerninfo ={
	.name="kerninfo",
	.do_cmd=do_kerninfo,
	.usage="kerninfo - Display information about the kernel",
};</span>

   對於__at_yiyecmd_section的定義,它將 cmd_kerninfo聲明到.yiye_cmd分區如下:

<span style="font-size:12px;">typedef int (*CmdFunc)(int argc, char * const argv[]);
typedef struct _YiyeCmd {
	char *name;
	CmdFunc do_cmd;
	char *usage;
}YiyeCmd,*p_YiyeCmd;
extern YiyeCmd section_yiye_cmd_start[],section_yiye_cmd_end[];
#define __at_yiyecmd_section YiyeCmd __attribute__((used,section(".yiye_cmd")))</span>

  當然這需要鏈接腳本的支持,在kernel.ld中添加如下分區:

<span style="font-size:12px;">SECTIONS
{
........
.yiye_cmd : {#增加yiye_cmd命令分區,用於鏈接yiye_cmd的結構體到一起
		PROVIDE(section_yiye_cmd_start = .);#標記分區的開始
		*(.yiye_cmd);
		PROVIDE(section_yiye_cmd_end = .);#標記分區的結束
	}
...…
}</span>

  (2)命令的運行:

<span style="font-size:12px;">int run_cmd(int argc,char * const argv[])
{
#ifdef DEBUG
	cprintf("<debug>%s,%d,section_yiye_start\n",__func__,__LINE__);
#endif
	YiyeCmd * one_cmd;
        for(one_cmd=section_yiye_cmd_start;one_cmd != section_yiye_cmd_end;one_cmd++){
		#ifdef DEBUG
		cprintf("<debug>%s,%d,%s\n",__func__,__LINE__,one_cmd->name);
		#endif
                if(strcmp(one_cmd->name,argv[0])==0){
                        return one_cmd->do_cmd(argc,argv);
                }
        }
        return -1;
}</span>

   只需要簡單的遍歷就可以對定義的命令進行操作。

   爲了進一步說明這項技巧的使用,我們將列舉兩項在linux內核中使用的實例,讓我們對它進行更進一步的理解。

     a)setup.bin中對視頻卡(video_cards)的選擇與操作

    (1)有了以上的經驗我們需要知道,鏈接腳本需要添加對分區支持,所以我們查看setup.ldarch/x86/boot/setup.ld)發現如下定義:

<span style="font-size:12px;">SECTIONS
{
      ......
.videocards     : {
                video_cards = .;
                *(.videocards)
                video_cards_end = .;
        }
...…
}</span>

   當然也需要定義在.videocards分區的數據結構,所以我們在文件arch/x86/boot/video.h中發現如下定義:

<span style="font-size:12px;">struct card_info {
        const char *card_name;
        int (*set_mode)(struct mode_info *mode);
        int (*probe)(void);
        struct mode_info *modes;
        int nmodes;             /* Number of probed modes so far */
        int unsafe;             /* Probing is unsafe, only do after "scan" */
        u16 xmode_first;        /* Unprobed modes to try to call anyway */
        u16 xmode_n;            /* Size of unprobed mode range */
};

#define __videocard struct card_info __attribute__((used,section(".videocards")))</span>

   另外,也需要使用我們定義的數據結構,所以我們可以在文件arch/x86/boot/video-*.c發現類似的定義:

<span style="font-size:12px;">static __videocard video_bios;
static __videocard video_vga;
static __videocard video_vesa;</span>

   (2)該分區的操作,raw_set_mode(arch/x86/boot/video-mode.c)方法中:

<span style="font-size:12px;">static int raw_set_mode(u16 mode, u16 *real_mode)
{
     ........
        /* Nothing found?  Is it an "exceptional" (unprobed) mode? */
        for (card = video_cards; card < video_cards_end; card++) {
                if (mode >= card->xmode_first &&
                    mode < card->xmode_first+card->xmode_n) {
                        struct mode_info mix;
                        *real_mode = mix.mode = mode;
                        mix.x = mix.y = 0;
                        return card->set_mode(&mix);
                }
        }

        /* Otherwise, failure... */
        return -1;
}</span>

    b)linux內核初始化時的initcall機制

    (1)linux內核爲編譯出來的vmlinux,它在初始化會用到initcall機制,根據如上經驗分析,我們先查看其依賴的鏈接腳本vmlinux.ldsarch/x86/kernel/vmlinux.lds,發現有如下的分區信息:

<span style="font-size:12px;">SECTIONS
{
........
__initcall_start = .; *(.initcallearly.init) __initcall0_start = .; *(.initcall0.init) *(.initcall0s.init) __initcall1_start = .; *(.initcall1.init) *(.initcall1s.init) __initcall2_start = .; *(.initcall2.init) *(.initcall2s.init) __initcall3_start = .; *(.initcall3.init) *(.initcall3s.init) __initcall4_start = .; *(.initcall4.init) *(.initcall4s.init) __initcall5_start = .; *(.initcall5.init) *(.initcall5s.init) __initcallrootfs_start = .; *(.initcallrootfs.init) *(.initcallrootfss.init) __initcall6_start = .; *(.initcall6.init) *(.initcall6s.init) __initcall7_start = .; *(.initcall7.init) *(.initcall7s.init) __initcall_end = .;

...…
}</span>

    如上分析我們發現了有一系列的initcall分區的定義。然後我們就該去分析這些分區中的數據結構,include/linux/init.h中可以發現相關定義:

<span style="font-size:12px;">typedef int (*initcall_t)(void);#定義函數指針
//如下根據不同的初始化時間來定義所在分區的宏
#define __define_initcall(fn, id) \
        static initcall_t __initcall_##fn##id __used \
        __attribute__((__section__(".initcall" #id ".init"))) = fn; \
        LTO_REFERENCE_INITCALL(__initcall_##fn##id)

/*
 * Early initcalls run before initializing SMP.
 *
 * Only for built-in code, not modules.
 */
#define early_initcall(fn)              __define_initcall(fn, early)

/*
 * A "pure" initcall has no dependencies on anything else, and purely
 * initializes variables that couldn't be statically initialized.
 *
 * This only exists for built-in code, not for modules.
 * Keep main.c:initcall_level_names[] in sync.
 */
#define pure_initcall(fn)               __define_initcall(fn, 0)

#define core_initcall(fn)               __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
......…
      //如何使用這些宏呢?在如下的文件列表中可發現它們的使用,以early_initcall爲例:
arch/alpha/kernel/perf_event.c                                                                                                                        
arch/x86/kernel/cpu/perf_event.c|1598| <<global>> early_initcall(init_hw_perf_events);
arch/x86/kernel/kvm.c|847| <<global>> early_initcall(kvm_spinlock_init_jump);
arch/x86/mm/kmemcheck/kmemcheck.c|74| <<global>> early_initcall(kmemcheck_init);
arch/x86/platform/efi/early_printk.c|40| <<global>> early_initcall(early_efi_map_fb);
arch/x86/realmode/init.c|122| <<global>> early_initcall(set_real_mode_permissions);
arch/x86/xen/spinlock.c|304| <<global>> early_initcall(xen_init_spinlocks_jump);
arch/xtensa/kernel/setup.c|450| <<global>> early_initcall(check_s32c1i);
drivers/bus/arm-cci.c|1469| <<global>> early_initcall(cci_init);
drivers/char/random.c|1323| <<global>> early_initcall(rand_initialize);</span>

   (2)還需要分析的就是這些函數如何被調用?在init/main.c中發現如下調用過程:

<span style="font-size:12px;">//定義鏈接腳本中的分區地址數組
static initcall_t *initcall_levels[] __initdata = {
        __initcall0_start,
        __initcall1_start,
        __initcall2_start,
        __initcall3_start,
        __initcall4_start,
        __initcall5_start,
        __initcall6_start,
        __initcall7_start,
        __initcall_end,
};
static void __init do_initcall_level(int level)
{
        initcall_t *fn;

        strcpy(initcall_command_line, saved_command_line);
        parse_args(initcall_level_names[level],
                   initcall_command_line, __start___param,
                   __stop___param - __start___param,
                   level, level,
                   &repair_env_string);
        //引用所有的特定分區內的所有函數,然後調用之。
        for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
                do_one_initcall(*fn);
}

static void __init do_initcalls(void)
{
        int level;
       //調用所有initcall的函數
        for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
                do_initcall_level(level);
}</span>

    如上分析可以發現 do_initcalls會將所有的initcall函數調用;另外,內核初始化調用該函數的流程如下:

    start_kernel--->do_basic_setup---> do_initcalls

    一葉說:工程技術應以“學以致用”爲本,而軟件本來就是一項實用的工程技術;而學到的內容,能以自己的理解去靈活運用,更能證明自己所學。而本文以代碼分析與文檔分析爲本,這樣是爲了更形象與具體的描述我們的學習對象;任何學習對象都應做到有理有據,而對於軟件學習,我們已經有很多的前輩總結了很多好的技法,這需要我們去學習,模仿與創造。隨着時間的流逝,好的技法總是被不斷反覆的使用,出現在各個場合,需要我們用心去理解與應用。學會總結與積累程序技法吧!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章