实验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

    一叶说:工程技术应以“学以致用”为本,而软件本来就是一项实用的工程技术;而学到的内容,能以自己的理解去灵活运用,更能证明自己所学。而本文以代码分析与文档分析为本,这样是为了更形象与具体的描述我们的学习对象;任何学习对象都应做到有理有据,而对于软件学习,我们已经有很多的前辈总结了很多好的技法,这需要我们去学习,模仿与创造。随着时间的流逝,好的技法总是被不断反复的使用,出现在各个场合,需要我们用心去理解与应用。学会总结与积累程序技法吧!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值