歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Unix知識 >> Unix教程

FreeBSD的Loader和內核初始化

FreeBSD
  loader也是一個 BTX 客戶,在這裡不作詳述。已有一部內容全面的手冊 loader(8) ,由Mike Smith書寫。比loader更底層的BTX的機理已經在前面討論過。 loader 的主要任務是引導內核。當內核被裝入內存後,即被loader調用:
  sys/boot/common/boot.c:
  
  /* 從loader中調用內核中對應的exec程序 */
  
  module_formats[km->m_loader]->l_exec(km);loader跳轉至哪裡呢?那就是內核的入口點。讓我們來看一下鏈接內核的命令:sys/conf/Makefile.i386:
  ld -elf -Bdynamic -T /usr/src/sys/conf/ldscript.i386 -export-dynamic -dynamic-linker /red/herring -o kernel -X locore.o
  在這一行中有一些有趣的東西。首先,內核是一個ELF動態鏈接二進制文件,可是動態鏈接器卻是/red/herring,一個莫須有的文件。其次,看一下文件sys/conf/ldscript.i386,可以對理解編譯內核時ld的選項有一些啟發。閱讀最前幾行,字符串sys/conf/ldscript.i386:
  ENTRY(btext)
  
  表示內核的入口點是符號 `btext'。這個符號在locore.s中定義:sys/i386/i386/locore.s:
  .text
  /**********************************************************************
  *
  * This is where the bootblocks start us, set the ball rolling...
  * 入口
  */
  NON_GPROF_ENTRY(btext)
  
  首先將寄存器EFLAGS設為一個預定義的值0x00000002,然後初始化所有段寄存器:sys/i386/i386/locore.s
  
  /* 不要相信BIOS給出的EFLAGS值 */
  pushl  $PSL_KERNEL
  popfl
  
  /*
  * 不要相信BIOS給出的%fs、%gs值。相信引導過程中設定的%cs、%ds、%es、%ss值
  */
  mov %ds, %ax
  mov %ax, %fs
  mov %ax, %gs
  
  btext調用例程recover_bootinfo(),identify_cpu(),create_pagetables()。
  
  這些例程也定在locore.s之中。這些例程的功能如下:recover_bootinfo
  
  這個例程分析由引導程序傳送給內核的參數。引導內核有3種方式:
  
  由loader引導(如前所述), 由老式磁盤引導塊引導,無盤引導方式。
  
  這個函數決定引導方式,並將結構struct bootinfo存儲至內核內存。
  
  identify_cpu 這個函數偵測CPU類型,將結果存放在變量_cpu中。
  
  create_pagetables 這個函數為分頁表在內核內存空間頂部分配一塊空間,
  
  並填寫一定內容 下一步是開啟VME(如果CPU有這個功能):
  
  testl  $CPUID_VME, R(_cpu_feature)
  jz 1f
  movl  %cr4, %eax
  orl $CR4_VME, %eax
  movl  %eax, %cr4
  然後,啟動分頁模式:/* Now enable paging */
  movl  R(_IdlePTD), %eax
  movl  %eax,%cr3      /* load ptd addr into mmu */
  movl  %cr0,%eax      /* get control word */
  orl $CR0_PE|CR0_PG,%eax   /* enable paging */
  movl  %eax,%cr0      /* and let's page NOW! */
  
  由於分頁模式已經啟動,原先的實地址尋址方式隨即失效。
  
  隨後三行代碼用來跳轉至虛擬地址:  pushl  $begin
  /* jump to high virtualized address */
  ret
  
  /* 現在跳轉至KERNBASE,那裡是操作系統內核被鏈接後真正的入口 */
  begin:
  
  函數init386()被調用;隨參數傳遞的是一個指針,指向第一個空閒物理頁。
  
  隨後執行mi_startup()。init386是一個與硬件系統相關的初始化函數,
  
  mi_startup()是個與硬件系統無關的函數(前綴'mi_'表示Machine Independent,
  
  不依賴於機器)。內核不再從mi_startup()裡返回;調用這個函數後,
  
  內核完成引導:sys/i386/i386/locore.s:
  movl  physfree, %esi
  pushl  %esi    /* 送給init386()的第一個參數 */
  call  _init386  /* 設置386芯片使之適應UNIX工作 */
  call  _mi_startup /* 自動配置硬件,掛接根文件系統,等 */
  hlt   /* 不再返回到這裡! */
  
  1.7.1 init386()init386()定義在sys/i386/i386/machdep.c中,它針對Intel 386芯片進行低級初始化。loader已將CPU切換至保護模式。
  
  loader已經建立了最早的任務。譯者注: 每個"任務"都是與其它“任務”相對獨立的執行環境。
  
  任務之間可以分時切換,這為並發進程/線程的實現提供了必要基礎。對於Intel 80x86任務的描述,在這個任務中,內核將繼續工作。在討論其代碼前,
  
  我將處理器對保護模式必須完成的一系列准備工作一並列出:
  
  初始化內核的可調整參數,這些參數由引導程序傳來准備GDT(全局描述符表)
  
  准備IDT(中斷描述符表)初始化系統控制台初始化DDB(內核的點調試器),如果它被編譯進內核的話初始化TSS(任務狀態段)准備LDT(局部描述符表)建立proc0(0號進程,即內核的進程)的pcb(進程控制塊)init386()首先初始化內核的可調整參數,這些參數由引導程序傳來。先設置環境指針(environment pointer, envp)調用,再調用init_param1()。
  
  envp指針已由loader存放在結構bootinfo中:sys/i386/i386/machdep.c:
  
  kern_envp = (caddr_t)bootinfo.bi_envp + KERNBASE;
  
  /* 初始化基本可調整項,如hz等 */
  
  init_param1();
  
  init_param1()定義在sys/kern/subr_param.c之中。這個文件裡有一些sysctl項,
  
  兩個函數,init_param1()和init_param2()。這兩個函數從init386()中調用:sys/kern/subr_param.c
  
  hz = HZ;
  TUNABLE_INT_FETCH("kern.hz", &hz);
  
  TUNABLE__FETCH用來獲取環境變量的值:/usr/src/sys/sys/kernel.h
  
  #define TUNABLE_INT_FETCH(path, var)  getenv_int((path), (var))
  
  Sysctlkern.hz是系統時鐘頻率。同時,這些sysctl項被init_param1()設定:
  
  kern.maxswzone, kern.maxbcache, kern.maxtsiz, kern.dfldsiz, kern.dflssiz,
  
  kern.maxssiz, kern.sgrowsiz。然後init386() 准備全局描述符表(Global Descriptors Table, GDT)。
  
  在x86上每個任務都運行在自己的虛擬地址空間裡,這個空間由"段址:偏移量"的數對指定。
  
  舉個例子,當前將要由處理器執行的指令在 CS:EIP,那麼這條指令的線性虛擬地址就是“代碼段虛擬段地址CS” + EIP。為了簡便,段起始於虛擬地址0,終止於界限4G字節。所以,在這個例子中,指令的線性虛擬地址正是EIP的值。
  
  段寄存器,如CS、DS等是選擇符,即全局描述符表中的索引(更精確的說,索引並非選擇符的全部,而是選擇符中的INDEX部分)。譯者注: 對於80386,選擇符有16位,INDEX部分是其中的高13位。
  
  FreeBSD的全局描述符表為每個CPU保存著15個選擇符:sys/i386/i386/machdep.c:
  union descriptor gdt[NGDT * MAXCPU];  /* 全局描述符表 */
  
  sys/i386/include/segments.h:
  /*
  * 全局描述符表(GDT)中的入口
  */
  #define GNULL_SEL  0  /* 空描述符 */
  #define GCODE_SEL  1  /* 內核代碼描述符 */
  #define GDATA_SEL  2  /* 內核數據描述符 */
  #define GPRIV_SEL  3  /* 對稱多處理(SMP)每處理器專有數據 */
  #define GPROC0_SEL 4  /* Task state process slot zero and up, 任務狀態進程 */
  #define GLDT_SEL  5  /* 每個進程的局部描述符表 */
  #define GUSERLDT_SEL  6  /* 用戶自定義的局部描述符表 */
  #define GTGATE_SEL 7  /* 進程任務切換關口 */
  #define GBIOSLOWMEM_SEL 8  /* BIOS低端內存訪問(必須是這第8個入口) */
  #define GPANIC_SEL 9  /* 會導致全系統異常中止工作的任務狀態 */
  #define GBIOSCODE32_SEL 10 /* BIOS接口(32位代碼) */
  #define GBIOSCODE16_SEL 11 /* BIOS接口(16位代碼) */
  #define GBIOSDATA_SEL  12 /* BIOS接口(數據) */
  #define GBIOSUTIL_SEL  13 /* BIOS接口(工具) */
  #define GBIOSARGS_SEL  14 /* BIOS接口(自變量,參數) */
  
  請注意,這些#defines並非選擇符本身,而只是選擇符中的INDEX域,
  
  因此它們正是全局描述符表中的索引。例如,內核代碼的選擇符(GCODE_SEL)的值為0x08。
  
  下一步是初始化中斷描述符表(Interrupt Descriptor Table, IDT)。
  
  這張表在發生軟件或硬件中斷時會被處理器引用。例如,執行系統調用時,用戶應用程序提交INT 0x80 指令。這是一個軟件中斷,處理器用索引值0x80在中斷描述符表中查找記錄。
  
  這個記錄指向處理這個中斷的例程。在這個特定情形中,這是內核的系統調用關口。
  
  譯者注: Intel 80386支持“調用門”,可以使得用戶程序只通過一條call指令就調用內核中的例程。
  
  可是FreeBSD並未采用這種機制,也許是因為使用軟中斷接口可免去動態鏈接的麻煩吧。
  
  另外還有一個附帶的好處:在仿真Linux時,當遇到FreeBSD內核不支持的而又並非關鍵性的系統調用時,內核只會顯示一些出錯信息,這使得程序能夠繼續運行;而不是在真正執行程序之前的初始化過程中
  
  就因為動態鏈接失敗而不允許程序運行。中斷描述符表最多可以有256 (0x100)條記錄。
  
  內核分配NIDT條記錄的內存給中斷描述符表,這裡NIDT=256,是最大值:sys/i386/i386/machdep.c:
  
  static struct gate_descriptor idt0[NIDT];
  struct gate_descriptor *idt = &idt0[0]; /* 中斷描述符表 */
  
  每個中斷都被設置一個合適的中斷處理程序。系統調用關口INT 0x80也是如此:sys/i386/i386/machdep.c:
  setidt(0x80, &IDTVEC(int0x80_syscall),
  SDT_SYS386TGT, SEL_UPL, GSEL(GCODE_SEL, SEL_KPL));
  
  所以當一個用戶應用程序提交INT 0x80指令時,全系統的控制權會傳遞給函數_Xint0x80_syscall,這個函數在內核代碼段中,將被以管理員權限執行。然後,
  
  控制台和DDB(調試器)被初始化:sys/i386/i386/machdep.c:
  cninit();
  /* 以下代碼可能因為未定義宏DDB而被跳過 */
  #ifdef DDB
  kdb_init();
  if (boothowto & RB_KDB)
  Debugger("Boot flags requested debugger");
  #endif
  
  任務狀態段(TSS)是另一個x86保護模式中的數據結構。當發生任務切換時,任務狀態段用來讓硬件存儲任務現場信息。局部描述符表(LDT)用來指向用戶代碼和數據。
  
  系統定義了幾個選擇符,指向局部描述符表,它們是系統調用關口和用戶代碼、用戶數據選擇符:/usr/include/machine/segments.h
  #define LSYS5CALLS_SEL 0  /* Intel BCS強制要求的 */
  #define LSYS5SIGR_SEL  1
  #define L43BSDCALLS_SEL 2  /* 尚無 */
  #define LUCODE_SEL 3
  #define LSOL26CALLS_SEL 4  /* Solaris >=2.6版系統調用關口 */
  #define LUDATA_SEL 5
  /* separate stack, es,fs,gs sels ? 分別的棧、es、fs、gs選擇符? */
  /* #define LPOSIXCALLS_SEL 5*/ /* notyet, 尚無 */
  #define LBSDICALLS_SEL 16 /* BSDI system call gate, BSDI系統調用關口 */
  #define NLDT    (LBSDICALLS_SEL + 1)
  
  然後,proc0(0號進程,即內核所處的進程)的進程控制塊(Process Control Block)
  
  (struct pcb)結構被初始化。proc0是一個struct proc 結構,描述了一個內核進程。
  
  內核運行時,該進程總是存在,所以這個結構在內核中被定義為全局變量:sys/kern/kern_init.c:
  struct proc proc0;
  
  結構struct pcb是proc結構的一部分,它定義在/usr/include/machine/pcb.h之中,內含針對i386硬件結構專有的信息,如寄存器的值。1.7.2 mi_startup()這個函數用冒泡排序算法,將所有系統初始化對象,然後逐個調用每個對象的入口:sys/kern/init_main.c:
  for (sipp = sysinit; *sipp; sipp++) {
  
  /* ... 省略 ... */
  
  /* 調用函數 */
  (*((*sipp)->func))((*sipp)->udata);
  /* ... 省略 ... */
  }
  盡管sysinit框架已經在《FreeBSD開發者手冊》中有所描述,我還是在這裡討論一下其內部原理。
  
  每個系統初始化對象(sysinit對象)通過調用宏建立。讓我們以announce sysinit對象為例。
  
  這個對象打印版權信息:sys/kern/init_main.c:
  static void
  print_caddr_t(void *data __unused)
  {
  printf("%s", (char *)data);
  }
  SYSINIT(announce, SI_SUB_COPYRIGHT, SI_ORDER_FIRST, print_caddr_t, copyright)
  
  這個對象的子系統標識是SI_SUB_COPYRIGHT(0x0800001),數值剛好排在SI_SUB_CONSOLE(0x0800000)後面。所以,版權信息將在控制台初始化之後就被很早的打印出來。
  
  讓我們看一看宏SYSINIT()到底做了些什麼。它展開成宏C_SYSINIT()。
  
  宏C_SYSINIT()然後展開成一個靜態結構struct sysinit。
  
  結構裡申明裡調用了另一個宏DATA_SET:/usr/include/sys/kernel.h:
  #define C_SYSINIT(uniquifier, subsystem, order, func, ident) static struct sysinit uniquifier ## _sys_init = { \ subsystem, order, \ func, \ ident \ }; \ DATA_SET(sysinit_set,uniquifier ##
  _sys_init);
  
  #define SYSINIT(uniquifier, subsystem, order, func, ident) C_SYSINIT(uniquifier, subsystem, order,     (sysinit_cfunc_t)(sysinit_nfunc_t)func, (void *)ident)
  宏DATA_SET()展開成MAKE_SET(),宏MAKE_SET()指向所有隱含的
  sysinit幻數:/usr/include/linker_set.h
  #define MAKE_SET(set, sym)           static void const * const __set_##set##_sym_##sym = &sym;  __asm(".section .set." #set ",\"aw\"");       __asm(".long " #sym);            __asm(".previous")
  #endif
  #define TEXT_SET(set, sym) MAKE_SET(set, sym)
  #define DATA_SET(set, sym) MAKE_SET(set, sym)
  回到我們的例子中,經過宏的展開過程,將會產生如下聲明:
  static struct sysinit announce_sys_init = {
  SI_SUB_COPYRIGHT,
  SI_ORDER_FIRST,
  (sysinit_cfunc_t)(sysinit_nfunc_t) print_caddr_t,
  (void *) copyright
  };
  
  static void const *const __set_sysinit_set_sym_announce_sys_init =
  &announce_sys_init;
  __asm(".section .set.sysinit_set" ",\"aw\"");
  __asm(".long " "announce_sys_init");
  __asm(".previous");
  
  第一個__asm指令在內核可執行文件中建立一個ELF節(section)。這發生在內核鏈接的時候。
  
  這一節將被命令為.set.sysinit_set。這一節的內容是一個32位值——announce_sys_init結構的地址,這個結構正是第二個__asm指令所定義的。第三個__asm指令標記節的結束。
  
  如果前面有名字相同的節定義語句,節的內容(那個32位值)將被填加到已存在的節裡,這樣就構造出了一個32位指針數組。用objdump察看一個內核二進制文件,也許你會注意到裡面有這麼幾個小的節:% objdump -h /kernel
  7 .set.cons_set 00000014 c03164c0 c03164c0 002154c0 2**2
  CONTENTS, ALLOC, LOAD, DATA
  8 .set.kbddriver_set 00000010 c03164d4 c03164d4 002154d4 2**2
  CONTENTS, ALLOC, LOAD, DATA
  9 .set.scrndr_set 00000024 c03164e4 c03164e4 002154e4 2**2
  CONTENTS, ALLOC, LOAD, DATA
  10 .set.scterm_set 0000000c c0316508 c0316508 00215508 2**2
  CONTENTS, ALLOC, LOAD, DATA
  11 .set.sysctl_set 0000097c c0316514 c0316514 00215514 2**2
  CONTENTS, ALLOC, LOAD, DATA
  12 .set.sysinit_set 00000664 c0316e90 c0316e90 00215e90 2**2
  CONTENTS, ALLOC, LOAD, DATA
  
  這一屏信息顯示表明節.set.sysinit_set有0x664字節的大小,所以0x664/sizeof(void *)個sysinit對象被編譯進了內核。
  
  其它節,如.set.sysctl_set表示其它鏈接器集合。
  
  通過定義一個類型為struct linker_set的變量,節.set.sysinit_set將被“收集”
  
  到那個變量裡:sys/kern/init_main.c:
  extern struct linker_set sysinit_set; /* XXX */
  struct linker_set定義如下:/usr/include/linker_set.h:
  struct linker_set {
  int ls_length;
  void  *ls_items[1];    /* ls_length個項的數組, 以NULL結尾 */
  };
  
  譯者注: 實際上是說,用C語言結構體linker_set來表達那個ELF節。
  
  第一項是sysinit對象的數量,第二項是一個以NULL結尾的數組,數組中是指向那些對象的指針。回到對mi_startup()的討論,我們清楚了sysinit對象是如何被組織起來的。函數mi_startup()將它們排序,並調用每一個對象。最後一個對象是系統調度器:/usr/include/sys/kernel.h:
  
  enum sysinit_sub_id {
  SI_SUB_DUMMY    = 0x0000000,  /* 不被執行,僅供鏈接器使用 */
  SI_SUB_DONE   = 0x0000001,  /* 已被處理*/
  SI_SUB_CONSOLE   = 0x0800000,  /* 控制台*/
  SI_SUB_COPYRIGHT  = 0x0800001,  /* 最早使用控制台的對象 */
  ...
  SI_SUB_RUN_SCHEDULER  = 0xfffffff /* 調度器:不返回 */
  };
  
  系統調度器sysinit對象定義在文件sys/vm/vm_glue.c中,這個對象的入口點是scheduler()。
  
  這個函數實際上是個無限循環,它表示那個進程標識(PID)為0的進程——swapper進程。
  
  前面提到的proc0結構正是用來描述這個進程。
  
  第一個用戶進程是init,由sysinit對象init建立:sys/kern/init_main.c:
  static void
  create_init(const void *udata __unused)
  {
  int error;
  int s;
  
  s = splhigh();
  error = fork1(&proc0, RFFDG | RFPROC, &initproc);
  if (error)
  panic("cannot fork init: %d\n", error);
  initproc->p_flag |= P_INMEM | P_SYSTEM;
  cpu_set_fork_handler(initproc, start_init, NULL);
  remrunqueue(initproc);
  splx(s);
  }
  SYSINIT(init,SI_SUB_CREATE_INIT, SI_ORDER_FIRST, create_init, NULL)
  
  create_init()通過調用fork1()分配一個新的進程,但並不將其標記為可運行。
  
  當這個新進程被調度器調度執行時,start_init()將會被調用。
  
  那個函數定義在init_main.c中。它嘗試裝載並執行二進制代碼init,先嘗試/sbin/init,然後是/sbin/oinit,/sbin/init.bak,最後是/stand/sysinstall:sys/kern/init_main.c:
  
  static char init_path[MAXPATHLEN] =
  #ifdef INIT_PATH
  __XSTRING(INIT_PATH);
  #else
  "/sbin/init:/sbin/oinit:/sbin/init.bak:/stand/sysinstall";
  #endif
Copyright © Linux教程網 All Rights Reserved