linux引導程序解析
bootsect程序,駐留在磁盤的第一個扇區中(0磁道 0磁頭 1 扇區)。在BIOS加點檢測之后,該引導程序會自動地加載在內存的0x7c00處。
bootsect程序在運行時,會首先將自身移動到0x90000處開始執行,并將從第二個扇區開始的共4個扇區大小的setup程序移動到,緊緊挨著該程序的0x90200處。
然后會使用BIOS中斷int13 取當前引導盤的參數,接著在屏幕上顯示Loading System的字符串,最后把磁盤上setup后面的system模塊加載到內存0x10000開始的地方。隨后確定根文件系統的設備號,若沒有指定,則根據所保存的引導盤的每磁道扇區數目,判斷出盤的類型和種類,并保存在設備號root_dev中。
最后長跳轉到setup程序的開始處,執行setup程序。
下面為分析的源代碼:
!SYSSIZE = 0x3000
.global begtext , begdata , begbss , endtext , enddata , endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
SETUPLEN = 4 ! nr of setup-sectors
BOOTSEG = 0x07c0
INITSEG = 0x9000
SETUPSEG = 0x9020
SYSSEG = 0x1000
ENDSEG = SYSSEG + SYSSIZE
ROOT_DEV = 0x306
entry start
start:
!將bootsect自身移動到0x90000處,并跳轉開始執行
mov ax , #BOOTSEG
mov ds , ax
mov ax , #INITSEG
mov es , ax
mov cx , #256
sub si , si
sub di , di
rep
movw
jmpi go , INITSEG
!跳轉過后修改段寄存器
go:
mov ax , cs
mov ds , ax
mov es , ax
mov ss , ax
mov sp , #0xFF00
!利用BIOS中斷INT 13將 setup模塊,從磁盤第2個扇區開始讀到0x90200開始處,共讀4個扇區
load_setup:
mov dx , #0x0000 ! drive 0 , head 0
mov cx , #0x0002 ! sector 2 , track 0
mov bx , #0x0200 ! address = 512 , in INITSEG
mov ax , #0x0200 + SETUP_LEN ! service 2 , nr of sectors
int 0x13
jnc ok_load_setup
mov dx , #0x0000 !出錯則重新執行加載程序
mov ax , #0x0000
int 0x13
j load_setup
!利用int13 中斷,得到磁盤驅動器的參數,特別是每道磁道的扇區數量
mov ax , 0x0800
mov dl , 0x00
int 0x13
!重新設置es的值
mov sectors , cx
mov ax , #INITSEG
mov es , ax
!print some message
mov ah , #0x03 !讀光標位置,返回光標位置在dx中
xor bh , bh
int 0x10
mov cx , #24
mov bx , #0x0007
mov bp , #msg1
mov ax , #0x1301
int 0x10
! ok we have written the message ,現在開始將system模塊加載到0x10000開始處
mov ax , #SYSSEG
mov es , ax
call read_it !讀磁盤上system模塊,es為輸入參數
call kill_motor !關閉馬達
!確定根文件系統所在的設備號
seg cs
mov ax , root_dev
cmp ax , #0
jne root_defined
seg cs
mov bx , sectors
mov ax , #0x0208
cmp bx , #15
je root_defined
mov ax , #0x021c
cmp bx , #18
je root_defined
undef_root:
jmp undef_root
root_defined:
seg cs
mov root_dev , ax
jmpi 0 , SETUPSEG !此處跳進setup程序
!下面是將system模塊加載進內存的子函數
sread: .word 1 + SETUPLEN !sectors read of current track
head: .word 0
track: .word 0
!保證es在64kb處
read_it :
mov ax , es
test ax , #0xfff
die: jne die
xor bx , bx
rp_read: !接著判斷是否已經讀入全部的數據,比較當前所讀的段是否就是系統數據末端所處的段
mov ax , es !如果不是,就跳轉至下面的ok1標號處繼續讀數據
cmp ax , #ENDSEG
jb ok1_read
ret !如果到達了系統末端,就結束此循環
ok1_read: !計算和驗證當前磁道上需要讀取的扇區數目,放在ax寄存器中,根據當前磁道還未讀取的扇區數和
!段內數據字節開始偏移的位置,計算如果全部讀取這些未讀扇區,所讀的字節是否會超過64kb的限制
!若會超過,則根據此次最多能讀入的字節數,反算出需要讀取的扇區數。
seg cs
mov ax , sectors !取每磁道的扇區數
sub ax , sread !減去當前磁道已讀扇區數
mov cx , ax !cx = ax 為當前磁道的未讀扇區數
shl cx , #9 !當前未讀的字節數
add cx , bx !此次操作之后,段內偏移地址現在的值
jnc ok2_read !若沒有超過64kb,則跳轉至ok2_read
je ok2_read
!若加上此次將讀取的磁道上所有未讀扇區時會超過64kb,則反算出 可以最多加載多少 扇區數目
xor ax , ax
sub ax , bx
shr ax , #9 !轉換成扇區數目
ok2_read:
!讀當前磁道上指定開始扇區(cl)和需讀扇區數(al)的數據到es:bx開始處。然后將磁道上已經讀取的扇區數目
!與磁道最大扇區數sectors作比較,如果小于sectors說明當前磁道上還有扇區未讀
call read_track
mov cx , ax !cx等于當前操作以讀扇區數目
add ax , sread !加上當前磁道已讀扇區數目
seg cs
cmp ax , sectors !如果當前磁道上還有扇區未讀,則跳轉到ok3_read
jne ok3_read
!如果該磁道的當前磁頭面所有扇區已經讀完,則讀該磁道的下一磁頭面(1號磁頭)上的數據,如果已經讀完則去讀下一磁道
mov ax , #1
sub ax , head !判斷當前的磁頭號,如果是0磁頭,則去讀1磁頭
jne ok4_read !讀1號磁頭
inc track !讀下一磁道
ok4_read:
mov head , ax !保存當前的磁頭號
xor ax , ax !清除當前磁道的已讀扇區數
ok3_read:
!如果當前磁道上還有未讀的扇區,則首先保存當前磁道的已讀扇區數目,然后調整存放數據的開始位置,若小于64kb邊界值
!則跳轉到rp_read處,繼續讀數據
mov sread , ax !保存當前磁道的已讀扇區數
shl cx , #9 !上次已讀扇區數*512字節
add bx , cx !調整當前段內數據開始位置
jnc rp_read
!否則說明已經讀取64kb數據,此時調整當前段,為讀下一段數據作準備
mov ax , es
add ax , #0x1000
mov es , ax
xor bx , bx
jmp rp_read
!read_track 子程序,讀當前磁道上指定開始扇區和需讀扇區數的數據到es:bx開始處。
!int 0x13 , ah =2 ,al=需讀扇區數,es:bx 緩沖區開始位置
read_track:
push ax
push bx
push cx
push dx
mov dx , track
mov cx , sread
inc cx
mov ch , dl
mov dx , head
mov dh , dl
mov dl , #0
and dx , #0x0100
mov ah , #2
int 0x13
jc bad_rt
pop dx
pop cx
pop bx
pop ax
ret
bad_rt: mov ax , #0
mov dx , #0
int 0x13
pop dx
pop cx
pop bx
pop ax
jmp read_track
!關閉軟驅馬達的子程序
kill_motor:
push dx
mov dx , #0x3f2
mov al , #0
outb
pop dx
ret
sectors:
.word 0 !存放當前啟動軟盤每磁道的扇區數
msg1:
.byte 13 , 10
.ascii "Loading system
"
.byte 13 , 10 , 13 ,10
.org 508
root_dev:
.word ROOT_DEV !這里存放根文件系統的所在設備號
boot_flag:
.word 0xAA55
.text
endtext:
.data
enddata:
.bss
endbss:
2 setup.s程序分析
setup.s是一個操作系統的加載程序,他的主要作用就是利用BIOS的讀取機器系統數據,并將這些數據保存到0x90000開始的位置,(覆蓋了bootsect程序所在的地方)。這些參數將被內核中相關程序使用。參數諸如光標位置 ,顯存等信息。
然后setup程序將system模塊從 0x10000-0x8ffff(任務system模塊不會超過512kb) ,整體移動到絕對內存地址為0x0000處。
接著加載中斷描述表寄存器idtr和全局描述表寄存器gdtr, 開啟A20地址線,重新設置兩個中斷控制芯片8259A,將硬件中斷號重新設置為0x20---0x2f。最后設置CPU的控制寄存器CR0,從而進入32位保護模式運行,并跳入到system模塊最前面部分的head.s程序繼續運行。
為了能讓head.s在32位保護模式下運行,在本程序臨時設置了中斷描述符表(IDT)和全局描述符表(GDT),
在GDT中設置了當前代碼段的描述符和數據段的描述符,在head.s中會重新設置這些描述符表。必須使用lgdt把描述符表的基地址告知CPU,再將機器狀態字置位即可進入32位保護模式。
INITSEG = 0x9000 !we move boot here
SYSSEG = 0x1000
SETUPSEG = 0x9020 !本程序所在的段地址
.global begtext , begdata , begbss , endtext , enddata , endbss
.text
begtext:
.data
begdata:
.bss
begbss:
.text
entry start
start:
!保存光標位置已備以后需要
!這段代碼使用BIOS中斷取屏幕當前的光標位置,然后保存在內存0x90000處就可以使用
mov ax , #INITSEG
mov ds , ax
mov ah , #0x03
xor bh , bh
int 0x10 !利用BIOS中斷 將當前光標位置存檔到 dx
mov [0] , dx !將光標位置存放在0x90000處
!得到內存的大小值
!利用BIOS中斷0x15功能號 ah=0x88取系統所含擴展內存大小并保存在內存0x90002處。
mov ah , #0x88
int 0x15
mov [2] , ax
!得到顯示卡的屬性
!調用BIOS中斷0x10,功能號ah=0x0f
!返回:ah=字符列數;al=顯示模式;bh=當前顯示頁
!0x90004存放當前頁 ,0x90006存放顯示模式,0x90007存放字符列數
mov ah , #0x0f
int 0x10
mov [4] , bx !bh=display page
mov [6] , ax !al =video mode , ah = window width
!檢查顯示方式并取參數
mov ah , #0x12
mov bl , #0x10
int 0x10
mov [8] , ax
mov [10] , bx
mov [12] , cx
!取得第一個硬盤的信息
mov ax , #0x0000
mov ds , ax
lds si , [4 * 0x41]
mov ax , #INITSEG
mov es , ax
mov di , #0x0080
mov cx , #0x10
rep
movsb
!取得第二個硬盤
mov ax , #0x0000
mov ds , ax
lds si , [4 * 0x46] !取中斷向量0x46的值,即hd1的參數值 ------> ds:si
mov ax , #INITSEG
mov es , ax
mov di , #0x0090 !傳輸目的地址 0x9000:0x0090
mov cx , #0x10
rep
movsb
!檢查系統是否有第二個硬盤
mov ax , #0x01500
mov dl , #0x81
int 0x13
jc no_disk1
cmp ah , #3
je is_disk1
no_disk1: !第二塊硬盤不存在,所以清空參數表
mov ax , #INITSEG
mov es , ax
mov di , #0x0090
mov cx , #0x10
mov ax , #0x00
rep
stosb
is_disk1:
!從此開始進入了保護模式
cli
!首先把system模塊移動到正確的位置
mov ax , #0x0000
cld
do_move:
mov es , ax
add ax , #0x1000
cmp ax , #0x9000
jz end_move
mov ds , ax
sub di , di
sub si , si
mov cx , #0x8000
rep
movsw
jmp do_move
end_move:
!在此處加載段描述符表,這里需要設置全局描述符表和中斷描述符表
mov ax , #SETUPSEG
mov ds , ax
lidt idt_48
lgdt gdt_48
!打開A20地址線
call empty_8042
mov al , #0xD1
out #0x64 , al
call empty_8042
mov al , #0xDF
out #0x60 , al
call empty_8042
!8259芯片主片端口是0x20 - 0x29 ,從片的端口是0xA0 - 0xA9 。
mov al , #0x11
out #0x20, al
.word 0x00eb , 0x00eb
out #0xA0 , al
.word 0x00eb , 0x00eb
!8259芯片設置中斷號從0x20開始
mov al , #0x20
out #0x21 , al
.word 0x00eb , 0x00eb
mov al , #0x28
out #0xA1 , al
.word 0x00eb , 0x00eb
mov al , #0x04
out #0x21 , al
.word 0x00eb, 0x00eb
mov al , #0x02
out #0xA1 , al
.word 0x00eb, 0x00eb
mov al , #0x01
out #0x21 , al
.word 0x00eb , 0x00eb
out #0xA1 , al
.word 0x00eb , 0x00eb
mov al , #0xFF
out #0x21 , al
.word 0x00eb , 0x00eb
out #0xA1 , al
!下面設置并進入32位保護模式運行,首先加載機器狀態字,也稱控制寄存器cr0
!在設置該bit之后,隨后的一條指令必須是一條段間跳轉指令,一用于刷新當前指令隊列
!因為CPU在執行一條指令之前就已經從內存讀取該指令并對其進行解碼。
mov ax , #0x0001
lmsw ax
jmpi 0,8
!下面這個子程序檢查鍵盤命令隊列是否為空
empty_8042:
.word 0x00eb , 0x00eb
in al , #0x64
test al , #2
jnz empty_8042
ret
!全局描述符表開始處
gdt:
.word 0,0,0,0
!代碼段選擇符的值
.word 0x07FF
.word 0x0000
.word 0x9A00
.word 0x00C0
!數據段選擇符的值
.word 0x07FF
.word 0x0000
.word 0x9200
.word 0x00C0
idt_48:
.word 0
.word 0 , 0
gdt_48:
.word 0x800
.word 512 + gdt , 0x9
.text
endtext:
.data
enddata:
.bss
endbss:
三 head.s程序
功能描述:
head.s程序在被編譯生成目標文件之后會與內核其他程序一起被鏈接成system模塊,位于system模塊最前面,所以稱之為head程序的原因。system模塊將被放置在磁盤上setup模塊之后開始的扇區中,即從磁盤上第6個扇區開始位置。 linux內核一般大約有120KB ,在磁盤上大概占用240個扇區。
之后我們將在保護模式下編程,head.s使用 as 和ld 編譯器和連接器。 這段程序實際上處于絕對地址0處開始的地方,首先是加載各個數據段寄存器,重新設置中斷描述符表idt,共256項,并使各個表項指向一個只報錯誤的啞中斷子程序 ignore_int。
在設置了中斷描述符表之后,本程序又重新設置了全局段描述符表gdt,主要是把gdt表設置在比較合理的地方,接著設置管理內存的分頁處理機制,將頁目錄表放在絕對物理地址0開始處,緊隨后邊將放置可以尋址16MB內存的4個頁表,并設置它們的表項。
最后,head.s 程序利用返回指令將預先放置在堆棧中的 main.c程序的入口地址彈出,去執行main()程序。
以下是部分代碼分析
(1)首先是建立IDT和GDT表
#建立IDT表
setup_idt:
lea ignore_int , %edx
movl $0x00080000 , %eax
movw %dx , %ax
movw $0x8E00 , %dx
lea _idt , %edi
mov $256 , %ecx
rp_sidt:
movl %eax , (%edi) #eax的高16位是選擇符 ,低16位是段內偏移的低16位
movl %edx , 4(%edi) #edx的高16位是段內偏移地址的高16位,低16位是權限位
addl $8 , %edi
dec %ecx #重復設置總共256個中斷描述符
jne rp_sidt
lidt idt_descr #加載中斷描述符表寄存器
ret
setup_gdt:
lgdt gdt_descr #加載全局描述符表寄存器
ret
由代碼可知, 256個idt均指向了一個啞中斷ignore_int,加載gdt的過程更簡單,只是將gdt描述符表的基地址加載進gdtr寄存器。
(2) 頁目錄表和頁表之間的映射
在linux1.1中 , 在絕對內存地址的0x000000處是一個大小為4k的頁目錄表,然后在內存0x1000,0x2000,0x3000,0x4000處分別是4個頁表的首地址,也就是說linux0.11僅僅能訪問16M的 內存空間,內存映射的算法如下:
首先在內存0x00000即頁目錄表設置4個頁表首地址,注意添加權限屬性。 然后從最后一個頁表的最后一個表項,倒序的將物理地址添加進頁表中,最后一個頁表項的內容是 64M - 4096 + 7 (7表示頁面在內存,且用戶可讀可訪問)。
.align 2
setup_paging: #首先為5頁內存進行清空處理
#1個頁目錄表,4個頁表
movl $1024 * 5 , %ecx
xorl %eax , %eax
xorl %edi , %edi
cld ; rep ; stosl
#頁目錄中只需要4個頁目錄, 7是屬性,表示該頁存在內存中,且用戶可以訪問
movl $pg0 + 7 , _pg_dir
movl $pg1 + 7 , _pg_dir + 4
movl $pg2 + 7 , _pg_dir + 8
movl $pg3 + 7 , _pg_dir + 12
#從最后一項 倒序的寫入
movl $pg3 + 4092 , %edi #最后一頁的最后一項
movl $0xfff007 , %eax #16M - 4096 +7
std
stosl
subl $0x1000 , %eax
jge 1b
#設置頁目錄表基址寄存器cr3的值,指向頁目錄表。cr3中保存的是頁目錄表的物理地址
xorl %eax , %eax
movl %eax , %cr3
#設置啟動分頁處理
movl %cr0 , %eax
orl %0x80000000 , %eax
movl %eax , %cr0
#該返回指令執行先前壓入堆棧的main函數的入口地址
ret
(3)head中還需要為程序跳轉進main函數作準備,當完成了頁面設置的時候,上面代碼的最后一句ret,即
完成了跳入main函數中繼續執行。 設置main函數的代碼如下:
.org 0x5000 #定義下面的內存數據塊從偏移0x5000處開始
_tmp_floppy_area:
.fill 1024 , 1 ,0 #共保留1024項,每項1字節,
#下面這些代碼為跳轉到main函數中,做準備
after_page_tables:
pushl $0 #這些是main函數的參數
pushl $0
pushl $0
pushl $L6 #main函數的返回地址
pushl $_main #_main 是編譯程序對main的內部表示法
jmp setup_paging #跳轉到建立頁表映射
L6:
jmp L6
可以看出執行完setup_paging之后的ret指令,將把_main 加載進 指令寄存器,進行執行。
(4)完整的代碼如下
.text
.global _idt , _gdt , _pg_dir , _tmp_floppy_area
_pg_dir: #頁目錄將會設置在這里,所以該程序會被覆蓋掉
startup_32:
movl $0x10 , %eax #0x10已經是全局描述符的在描述符表中的偏移值
mov %ax , %ds
mov %ax , %es
mov %ax , %fs
mov %ax , %gs
lss _stack_start , %esp #設置_stack_start----->ss:esp
call setup_idt #調用設置中斷描述符表的子程序
call setup_gdt #調用設置全局描述符表的子程序
movl $0x10 , %eax #重新加載所有的段寄存器
mov %ax , %ds
mov %ax , %es
mov %ax , %fs
mov %ax , %gs
lss _stack_start , %esp
#以下代碼用來測試A20地址線是否已經打開,采用的方法是向內存0x000000處寫入任意的一個數值
#然后看內存地址0x100000是否也是這個數值,如果一樣的話,就說明A20地址線沒有打開
xorl %eax , %eax
1: incl %eax
movl %eax , 0x000000 #地址就不需要加$
cmpl %eax , 0x100000
je 1b
movl %cr0 , %eax #
andl $0x80000011 , %eax
orl $2 , %eax
movl %eax , %cr0
call check_x87
jmp after_page_tables
check_x87:
fninit #向協處理器發送初始化命令
fstsw %ax
cmpb $0 , %al
je 1f
movl %cr0 , %eax
xorl $6 , %eax
movl %eax , %cr0
ret
.align 2
1: .byte 0xDB , 0xE4
#建立IDT表
setup_idt:
lea ignore_int , %edx
movl $0x00080000 , %eax
movw %dx , %ax
movw $0x8E00 , %dx
lea _idt , %edi
mov $256 , %ecx
rp_sidt:
movl %eax , (%edi) #eax的高16位是選擇符 ,低16位是段內偏移的低16位
movl %edx , 4(%edi) #edx的高16位是段內偏移地址的高16位,低16位是權限位
addl $8 , %edi
dec %ecx #重復設置總共256個中斷描述符
jne rp_sidt
lidt idt_descr #加載中斷描述符表寄存器
ret
setup_gdt:
lgdt gdt_descr #加載全局描述符表寄存器
ret
#這里設置四張頁表,可以用來方位16M的內存空間
#每個頁表大小為4k,每項為4字節,一張頁表可以映射1024 * 4kb的內存空間 ,即4M
.org 0x1000
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000 #定義下面的內存數據塊從偏移0x5000處開始
_tmp_floppy_area:
.fill 1024 , 1 ,0 #共保留1024項,每項1字節,
#下面這些代碼為跳轉到main函數中,做準備
after_page_tables:
pushl $0 #這些是main函數的參數
pushl $0
pushl $0
pushl $L6 #main函數的返回地址
pushl $_main #_main 是編譯程序對main的內部表示法
jmp setup_paging #跳轉到建立頁表映射
L6:
jmp L6
#下面是默認的中斷向量句柄
int_msg:
.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:
pushl %eax
pushl %ecx
pushl %edx
push %ds #入棧占4個字節
push %es
push %fs
movl $0x10 , %eax
mov %ax , %ds
mov %ax , %es
mov %ax , %fs
pushl $int_msg #向printk函數傳遞參數
call _printk #該函數在/kernel/printk.c中
popl %eax #返回值
pop %fs
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret
.align 2
setup_paging: #首先為5頁內存進行清空處理
#1個頁目錄表,4個頁表
movl $1024 * 5 , %ecx
xorl %eax , %eax
xorl %edi , %edi
cld ; rep ; stosl
#頁目錄中只需要4個頁目錄, 7是屬性,表示該頁存在內存中,且用戶可以訪問
movl $pg0 + 7 , _pg_dir
movl $pg1 + 7 , _pg_dir + 4
movl $pg2 + 7 , _pg_dir + 8
movl $pg3 + 7 , _pg_dir + 12
#從最后一項 倒序的寫入
movl $pg3 + 4092 , %edi #最后一頁的最后一項
movl $0xfff007 , %eax #16M - 4096 +7
std
stosl
subl $0x1000 , %eax
jge 1b
#設置頁目錄表基址寄存器cr3的值,指向頁目錄表。cr3中保存的是頁目錄表的物理地址
xorl %eax , %eax
movl %eax , %cr3
#設置啟動分頁處理
movl %cr0 , %eax
orl %0x80000000 , %eax
movl %eax , %cr0
#該返回指令執行先前壓入堆棧的main函數的入口地址
ret
#140行將壓入堆棧的main指令彈出,并跳到main函數中去
.align 2
.word 0
idt_descr:
.word 256 * 8 - 1
.long _idt
.align 2
.word 0
gdt_descr:
.word 256 * 8 - 1
.long _gdt
.align 3
_idt: .fill 256 , 8 , 0 #共256項,每項8字節,初始化為0
_gdt: .quad 0x0000000000000000
.quad 0x00c09a0000000fff
.quad 0x00c0920000000fff
.quad 0x0000000000000000
.fill 252 , 8 , 0
當CPU運行在保護模式下,某一時刻GDT和LDT分別只能有一個,分別有寄存器GDTR和IDTR指定它們的表基址。在某一時刻當前LDT表的基址由LDTR寄存器的內容指定并且使用GDT中的某個描述符來加載,即LDT也是由GDT中的描述符來決定。但是在某一時刻同樣也只是由其中的一個被視為活動的。一般對于每個任務使用一個LDT,在運行時,程序可以使用GDT中的描述符以及當前任務的LDT中的描述符