实验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-0x10000为MBR使用
(b)X-X+0x08000为内核传统的引导扇区与内核实模式代码(32KByte);
(c)X+0x08000-X+0x10000为内核实模式的堆栈空间(8KByte);
(d)X+10000-0xA0000之间设置传递给内核的参数;
(e)0x100000之后的为保护模式的内核所在,这就没有限制内核的大小。
这里出现的X为一个地址,由以上的内存分配,可以发现它仅仅是一个内核映像的偏移地址;而官方给出的解释尽可能的低,只要boot-loader引导允许。因为我们的MBR已经占据了0x7C00到0x7E00-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,最后由 objcopy将elf文件转换为二进制文件。分析链接脚本——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/compressed的Makefile下分析vmlinux.bin的实现,详情见对应的makefile如下为总结的步骤:
a)如上编译内核源文件得到的vmlinux,通过objcopy -R .comment -S vmlinux vmlinux.bin
b)如果需要重定向:则通过vmlinux生成vmlinux.relocs
c)再将vmlinux.bin与vmlinux.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.bin与setup.bin剩下的就是如何将两个bin档合并成bzImage,这就需要分析arch/x86/boot/tools/build.c的源文件。简单分析build功能为将setup.bin与vmlinux.bin进行合并,同时也计算相关大小与校验和,从而修改相关文件中的设置。
如上分析流程我已经在相关Makefile中添加调试信息,make时就会将如上流程给打印出来;如上流程只是大致的缕了一下思路,而细节的分析过程,需要读者自己去分析;这不是本节的重点。 通过对内核编译流程的分析可以看出内核文件的基本结构与:由内核的编译流程可以发现bzImage有setup.bin与vmlinux.bin两部分组成,vmlinux.bin主要包含两部分:引导代码与压缩内核。
所以如上的范围b其实就是setup.bin,而范围c就是为setup.bin执行时的堆栈空间(因为用到c语言了),范围d为传递给内核的参数,比如:”console=ttyS1;root=/dev/sda1”等;范围e为vmlinux.bin。
当理解了引导协议映像,还需要介绍的是内核引导头配置在setup.bin的1F1处,其中包含了内核启动的必要参数,如下:
<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的分析,也以发现内核引导头配置hdr在0x1F1的位置(由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实现。
当程序被解压缩之后,因为压缩的内核为elf文件,所以需要将对应段加载到相应的内核地址段,然后跳入到解压缩后的内核进入点正式执行。<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>
以上的流程只是将编译出来的内核加载到内存中,然后执行:真正的内核从startup_64(arch/x86/kernel/head_64.S)开始执行:<
以上是关于实验1后篇——引导linux与uboot命令仿真的主要内容,如果未能解决你的问题,请参考以下文章