实验1后篇——引导linux与uboot命令仿真

Posted yiye_01

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了实验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)开始执行:<

以上是关于实验1后篇——引导linux与uboot命令仿真的主要内容,如果未能解决你的问题,请参考以下文章

嵌入式Linux开发-uboot常用命令介绍(上篇)

利用WSL2搭建Qemu仿真Vexpress-a9开发环境

利用WSL2搭建Qemu仿真Vexpress-a9开发环境

uboot什么意思(uboot fastboot)

uboot什么意思(uboot fastboot)

uboot是啥?uboot的命令是干嘛的?