《linux核心完全剖析》筆記01-啟動分析
啟動程式碼分析-《linux 0.12核心完全剖析》筆記
導語:
linux 0.12的啟動程式碼能夠給我們分析最新的linux程式碼給予一定的啟示,啟動程式碼雖然只有三個檔案,但是對讀者分析能力的要求比較高,主要是在對組合語言以及x86程式設計體系的理解
一. 對linux 0.12啟動程式碼的分析
- bootsect.S檔案主要的目的是載入setup.S和核心模組
- setup.S 主要目的是通過biso中斷讀取機器的系統資料
- head.s 主要目的是核心初始化之前的環境配置,也就是32位保護模式執行做準備
筆記重點:
1. 三段程式碼都涉及程式碼的移動
主要目的是為了空間複用,程式碼是從0x7C00開始執行,第一段程式碼就將bootsect.S的程式碼移動到絕對地址0x9000處然後再執行
entry start
start:
mov ax,#BOOTSEG "BOOTSEG為0x7C0
mov ds,ax
mov ax,#INITSEG "0x9000
mov es,ax
mov cx,#256 "512位元組
sub si,si "si = 0x0000
sub di,di "di = 0x0000
rep "cx遞減1 直到cx為0
movw "移動一個字
jmpi go,INITSEG "跳轉到0x9000執行
2. 機器的系統資料都是通過BIOS的功能獲取到的,核心初始化的時候都要利用到這些資料
- 最初讀取載入核心程式碼是使用的BIOS的INT 0x13
- setup.S中利用BIOS的INT 0x15功能讀取記憶體的大小
- 其他硬體資料
3. setup.S的主要目的是設定中斷向量表述表和全域性描述符表,以開啟核心的32位保護模式
描述符表的定義是在setup.S的567行開始
gdt:
.word 0,0,0,0 ! dummy
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L
gdt_48:
.word 0x800 ! gdt limit=2048, 256 GDT entries
.word 512+gdt,0x9 ! gdt base = 0X9xxxx
設定GDT暫存器和IDT暫存器是在139行
lidt idt_48
lgdt gdt_48
idt_48h和gdt_48就是上面定義的
是一個6位元組長的資料,前2位元組是表的長度,後4位元組是表的基地址,設定cr0的第0位為1開啟保護模式
4. 這裡有非常詳細的8259A的程式設計資料
沒有學過微機介面的同學可能對8259A的結構不夠了解也是沒有關係,瞭解這些對中斷系統的瞭解比較關鍵
5. head.s最關鍵的地方是在32位保護模式下開啟記憶體的分頁處理機制
cr3暫存器是記錄頁目錄的基地址,cr0的PG位置1就是開啟分頁處理,然後重新設定IDT和GDT,第一次設定IDT和GDT是為了head.s執行32位保護模式,設定頁目錄而臨時設定,這個時候還不是分頁模式,不能分配記憶體給GDT和IDT,第一次設定時IDT是空表,GDT只有3個描述符,這次設定GDT的大小是256*8-1,IDT的大小也是256,這是我認為的最主要區別。
idt_descr:
.word 256*8-1 "idt 包含256項
.long _idt
.align 2
.word 0
gdt_descr:
.word 256*8-1 "gdt 包含256項
.long _gdt "地址為下面的_gdt標記
.align 3
_idt:
.fill 256,8,0 "idt存放的地方
_gdt:
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00c09a0000000fff /* 16Mb */
.quad 0x00c0920000000fff /* 16Mb */
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */
結論:通過對linux 0.12啟動程式碼的分析,可以知道linux啟動的三大步驟。
- 首先是通過BIOS提供的功能,載入linux初始化之前的環境初始化程式碼,setup.S和head.s;
- 然後setup.S通過BIOS的中斷服務獲取系統硬體的一些引數並儲存,準備給linux進行初始化提供參考;
- 然後通過head.s設定系統的32位保護模式,並開啟記憶體的分頁模式
二. linux 4.9啟動程式碼分析
導語:
根據以上對linux 0.12的分析,知道啟動必須經過的三個階段,現在的啟動過程,bootsect.S和setup.S部分
的功能已經由grub等bootloader來承擔,現在主要分析等同於linux 0.12的head.s部分功能的程式碼,現在的核心程式碼因為歷史原因,也因為需要對有限記憶體的嵌入式的支援,對核心進行了壓縮,在這裡暫不對這些程式碼進行分析
程式碼分析之前有幾點說明
首先是所有標號地址都在編譯核心的時候,多偏移3G
目的是在設定虛擬地址以後,核心對映在程序的3G~4G的位置上
因此不管是pa巨集還是其他標號地址都在未啟用虛擬地址之前,要減去PAGE_OFFSET
1.先來看.data段中對GDT和IDT的定義
linux/arch/x86/kernel/head_32.S的727行
- 734行定義了GDT全域性描述符,前兩個位元組表示表的長度,後四個位元組是表的基地址
boot_gdt - __PAGE_OFFSET為實際的記憶體實體地址 - 739行定義IDT描述符,中斷表描述符
- 740行IDT_ENTRIES為中斷表的大小
- 755行GDT_ENTRY_BOOT_CS為全域性描述符表大小
720 /*
721 * The IDT and GDT 'descriptors' are a strange 48-bit object
722 * only used by the lidt and lgdt instructions. They are not
723 * like usual segment descriptors - they consist of a 16-bit
724 * segment size, and 32-bit linear address value:
725 */
726
727 .data
728 .globl boot_gdt_descr
729 .globl idt_descr
730
731 ALIGN
732 # early boot GDT descriptor (must use 1:1 address mapping)
733 .word 0 # 32 bit align gdt_desc.address
734 boot_gdt_descr:
735 .word __BOOT_DS+7
736 .long boot_gdt - __PAGE_OFFSET
737
738 .word 0 # 32-bit align idt_desc.address
739 idt_descr:
740 .word IDT_ENTRIES*8-1 # idt contains 256 entries
741 .long idt_table
742
743 # boot GDT descriptor (later on used by CPU#0):
744 .word 0 # 32 bit align gdt_desc.address
745 ENTRY(early_gdt_descr)
746 .word GDT_ENTRIES*8-1
747 .long gdt_page /* Overwritten for secondary CPUs */
748
749 /*
750 * The boot_gdt must mirror the equivalent in setup.S and is
751 * used only for booting.
752 */
753 .align L1_CACHE_BYTES
754 ENTRY(boot_gdt)
755 .fill GDT_ENTRY_BOOT_CS,8,0
756 .quad 0x00cf9a000000ffff /* kernel 4GB code at 0x00000000 */
757 .quad 0x00cf92000000ffff /* kernel 4GB data at 0x00000000 */
2.初始化頁目錄表和頁表
linux/arch/x86/kernel/head_32.S的727行
- 217行是得到核心__PAGE_OFFSET位置的頁目錄項, 首先虛擬地址的組成是
10位 | 12位 | 10 位 |
---|---|---|
頁目錄項索引 | 頁目錄表索引 | 物理偏移 |
實體地址偏移向右偏移22位得到頁目錄項索引,每項頁目錄項索引佔4位元組,左移2位得到特定的頁目錄項索引
* 228行~227行是填充頁目錄項
* 229行~231行是填充頁表項
* 238行~245行是填充最後一頁的對齊處理
216
217 page_pde_offset = (__PAGE_OFFSET >> 20);
218
219 movl $pa(__brk_base), %edi /* 將__brk_base地址給%edi*/
220 movl $pa(initial_page_table), %edx /* 將initial_page_table實體地址給%edx*/
221 movl $PTE_IDENT_ATTR, %eax /* PTE_IDENT_ATTR 是定義頁目錄的屬性的*/
222 10:
223 leal PDE_IDENT_ATTR(%edi),%ecx /* Create PDE entry */
224 movl %ecx,(%edx) /* Store identity PDE entry */
225 movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
226 addl $4,%edx /* 頁目錄項每項佔4位元組*/
227 movl $1024, %ecx /* 一個頁目錄項佔1024項頁表*/
228 11:
229 stosl /* 將eax的內容填充到%edi地址處,並%edi + 4,%eax是頁表項的內容*/
230 addl $0x1000,%eax /* 0x1000 = 2^12 = 4096*/
231 loop 11b
232 /*
233 * End condition: we must map up to the end + MAPPING_BEYOND_END.
234 */
235 movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp
236 cmpl %ebp,%eax
237 jb 10b
238 addl $__PAGE_OFFSET, %edi
239 movl %edi, pa(_brk_end)
240 shrl $12, %eax
241 movl %eax, pa(max_pfn_mapped)
242
243 /* Do early initialization of the fixmap area */
244 movl $pa(initial_pg_fixmap)+PDE_IDENT_ATTR,%eax
245 movl %eax,pa(initial_page_table+0xffc)
3.啟動分頁
linux/arch/x86/kernel/head_32.S的398行
393 enable_paging:
394
395 /*
396 * Enable paging
397 */
398 movl $pa(initial_page_table), %eax
399 movl %eax,%cr3 /* set the page table pointer.. */
400 movl $CR0_STATE,%eax
401 movl %eax,%cr0 /* ..and set paging (PG) bit */
402 ljmp $__BOOT_CS,$1f /* Clear prefetch and normalize %eip */
403 1:
404 /* Shift the stack pointer to a virtual address */
405 addl $__PAGE_OFFSET, %esp
4.設定IDT
406
407 /*
408 * start system 32-bit setup. We need to re-do some of the things done
409 * in 16-bit mode for the "real" operations.
410 */
411 movl setup_once_ref,%eax
412 andl %eax,%eax
413 jz 1f # Did we do this already?
414 call *%eax
5.設定核心執行環境並跳轉到start_kernel
447 is486:
448 movl $0x50022,%ecx # set AM, WP, NE and MP
449 movl %cr0,%eax
450 andl $0x80000011,%eax # Save PG,PE,ET
451 orl %ecx,%eax
452 movl %eax,%cr0
453
454 lgdt early_gdt_descr
455 lidt idt_descr
456 ljmp $(__KERNEL_CS),$1f
457 1: movl $(__KERNEL_DS),%eax # reload all the segment registers
458 movl %eax,%ss # after changing gdt.
459
460 movl $(__USER_DS),%eax # DS/ES contains default USER segment
461 movl %eax,%ds
462 movl %eax,%es
463
464 movl $(__KERNEL_PERCPU), %eax
465 movl %eax,%fs # set this cpu's percpu
466
467 movl $(__KERNEL_STACK_CANARY),%eax
468 movl %eax,%gs
469
470 xorl %eax,%eax # Clear LDT
471 lldt %ax
472
473 pushl $0 # fake return address for unwinder
474 jmp *(initial_code)
initial_code的定義如下
654 ENTRY(initial_code)
655 .long i386_start_kernel
656 ENTRY(setup_once_ref)
657 .long setup_once
6.啟動入口分析
第97行
96 __HEAD
97 ENTRY(startup_32)
98 movl pa(initial_stack),%ecx #將堆的初始地址給ecx
99
100 /*
*esi存放了boot_param結構體的地址
*OFFSET(BP_loadflags,boot_params,hdr.loadflag)
*位於arch/x86/kernel/asm-offsets.c的84行
*從bootloader傳過來的引數告訴核心
*是否需要設定保護模式
*不需要則跳轉到下標為的語句
*/
102 testb $KEEP_SEGMENTS, BP_loadflags(%esi)
103 jnz 2f
104
105 /*
106 * 重新設定一下保護模式
* 載入ds,es,fs,gs,ss
107 */
108 lgdt pa(boot_gdt_descr)
109 movl $(__BOOT_DS),%eax
110 movl %eax,%ds
111 movl %eax,%es
112 movl %eax,%fs
113 movl %eax,%gs
114 movl %eax,%ss
115 2:
116 leal -__PAGE_OFFSET(%ecx),%esp
117
118 /*
119 * 清空BSS
120 */
121 cld
122 xorl %eax,%eax
123 movl $pa(__bss_start),%edi
124 movl $pa(__bss_stop),%ecx
125 subl %edi,%ecx
126 shrl $2,%ecx
127 rep ; stosl
128 /*
129 * 拷貝boot_params結構體的內容到核心中
135 */
136 movl $pa(boot_params),%edi
137 movl $(PARAM_SIZE/4),%ecx
138 cld
139 rep
140 movsl
141 movl pa(boot_params) + NEW_CL_POINTER,%esi
142 andl %esi,%esi
143 jz 1f # No command line
144 movl $pa(boot_command_line),%edi
145 movl $(COMMAND_LINE_SIZE/4),%ecx
146 rep
147 movsl
148 1:
149
結論:
經過分析,基本瞭解head_32.S是linux進入start_kernel之前,為核心初始化準備環境,設定GDT,IDT,開啟32位保護模式,開啟分頁模式。和linux 0.12的head.s的功能如出一轍。