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