1. 程式人生 > >3.作業系統引導——作業系統啟動

3.作業系統引導——作業系統啟動

1.setup.s

setup中,作業系統接手硬體,初始化
開機做兩件事:bootsect讀入系統,setup初始化

!
! setup.s (C) 1991 Linus Torvalds
!
! setup.s is responsible for getting the system data from the BIOS,
! and putting them into the appropriate places in system memory.
! both setup.s and system has been loaded by the bootblock.
!
! This
code asks the bios for memory/disk/other parameters, and ! puts them in a "safe" place: 0x90000-0x901FF, ie where the ! boot-block used to be. It is then up to the protected mode ! system to read them from there before the area is overwritten ! for buffer-blocks. ! ! setup.s 負責從BIOS 中獲取系統資料,並將這些資料放到系統記憶體的適當地方。 !
此時setup.ssystem 已經由bootsect 引導塊載入到記憶體中。 ! ! 這段程式碼詢問bios 有關記憶體/磁碟/其它引數,並將這些引數放到一個 ! “安全的”地方:0x90000-0x901FF,也即原來bootsect 程式碼塊曾經在 ! 的地方,然後在被緩衝塊覆蓋掉之前由保護模式的system 讀取。 ! entry start start: ! ok, the read went well so we get current cursor position and save it for ! posterity. ! ok,整個讀磁碟過程都正常,現在將游標位置儲存以備今後使用。 mov
ax,#INITSEG ! this is done in bootsect already, but... mov ds,ax !ds 置成#INITSEG(0x9000)。 !這已經在bootsect 程式中設定過,但是現在是setup 程式,Linus 覺得需要再重新設定一下。 mov ah,#0x03 ! read cursor pos ! BIOS 中斷0x10 的讀游標功能號 ah = 0x03,輸入:bh = 頁號,返回:ch = 掃描開始線,cl = 掃描結束線, ! dh = 行號(0x00 是頂端),dl = 列號(0x00 是左邊)。 xor bh,bh int 0x10 ! save it in known place, con_init fetches mov [0],dx ! it from 0x90000.dx0x90000 ! 上兩句是說將游標位置資訊存放在0x90000 處,控制檯初始化時會來取。 ! Get memory size (extended mem, kB) ! 下面3 句取擴充套件記憶體的大小值(KB)。 ! 是呼叫中斷0x15,功能號ah = 0x88 ! 返回:ax = 從0x100000(1M)處開始的擴充套件記憶體大小(KB)。 ! 若出錯則CF 置位,ax = 出錯碼。 mov ah,#0x88 int 0x15 ! BIOS中斷int 0x15,用於獲得實體記憶體的大小,放到axmov [2],ax ! 將擴充套件記憶體數值存在0x90002 處(1 個字)。 ! ax賦值給[2],[2]是間接定址,[2]前有預設的段暫存器,存的是0x9000 ! [2]是將0x9000左移4位加2,即0x90002,存的數值用於擴充套件記憶體的大小 ! 作業系統要管理記憶體,要知道記憶體有多大 …… ! now we want to move to protected mode ... ! 從這裡開始我們要保護模式方面的工作了。 cli ! no interrupts allowed ! ! 此時不允許中斷。 ! first we move the system to it's rightful place ! 首先我們將system 模組移到正確的位置。 ! bootsect 載入程式是將system 模組讀入到從0x10000(64k,SYSSEG=0x1000)開始的位置。由於當時假設 ! system 模組最大長度不會超過0x80000(512k),也即其末端不會超過記憶體地址0x90000, ! 所以bootsect 會將自己移動到0x90000 開始的地方,並把setup 載入到它的後面。 ! 下面這段程式的用途是再把整個system 模組移動到0x00000 位置,即把從0x10000 到0x8ffff ! 的記憶體資料塊(512k),整塊地向記憶體低端移動了0x10000(64k)的位置。 mov ax,#0x0000 cld ! 'direction'=0, movs moves forward ! 移動 do_move: mov es,ax ! destination segment ! es:di 目的地址(初始為0x0000:0x0) add ax,#0x1000 cmp ax,#0x9000 ! 已經把從0x8000 段開始的64k 程式碼移動完 jz end_move mov ds,ax ! source segment sub di,di ! di 減為0ds:si 源地址(初始為0x1000:0x0) sub si,si ! si 減為0 mov cx,#0x8000 ! 移動0x8000 字(64k 位元組)。 rep movsw jmp do_move ! 將作業系統移動到0x0000(es:di)處,地址0x0000-0x80000用於放作業系統 ! 之前的0x7c00要騰出空間給作業系統

記憶體地址:0x90000,長度:2,名稱:游標位置
記憶體地址:0x90002,長度:2,名稱:擴充套件記憶體數
記憶體地址:0x9000C,長度2,名稱:顯示卡引數
記憶體地址:0x901FC,長度:2,名稱:根裝置號

下邊要進入保護模式了

! then we load the segment descriptors
! 此後,我們載入段描述符。
! 從這裡開始會遇到32 位保護模式的操作,因此需要Intel 32 位保護模式程式設計方面的知識了,
! 有關這方面的資訊請查閱列表後的簡單介紹或附錄中的詳細說明。這裡僅作概要說明。
!
! lidt 指令用於載入中斷描述符表(idt)暫存器,它的運算元是6 個位元組,0-1 位元組是描述符表的
! 長度值(位元組);2-5 位元組是描述符表的32 位線性基地址(首地址),其形式參見下面
! 219-220 行和223-224 行的說明。中斷描述符表中的每一個表項(8 位元組)指出發生中斷時
! 需要呼叫的程式碼的資訊,與中斷向量有些相似,但要包含更多的資訊。
!
! lgdt 指令用於載入全域性描述符表(gdt)暫存器,其運算元格式與lidt 指令的相同。全域性描述符
! 表中的每個描述符項(8 位元組)描述了保護模式下資料和程式碼段(塊)的資訊。其中包括段的
! 最大長度限制(16 位)、段的線性基址(32 位)、段的特權級、段是否在記憶體、讀寫許可以及
! 其它一些保護模式執行的標誌。參見後面205-216 行。
!

end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax ! ds 指向本程式(setup)段。
lidt idt_48 ! load idt with 0,0IDT 是保護模式下的中斷函式表
! 載入中斷描述符表(idt)暫存器,idt_486 位元組運算元的位置
!2 位元組表示idt 表的限長,後4 位元組表示idt 表所處的基地址。

lgdt gdt_48 ! load gdt with whatever appropriateGDT是保護模式下的定址表
! 初始化表,載入全域性描述符表(gdt)暫存器,gdt_486 位元組運算元的位置

! that was painless, now we enable A20
! 以上的操作很簡單,現在我們開啟A20 地址線。參見程式列表後有關A20 訊號線的說明。


! 8042是鍵盤控制器,其輸出埠P2用來控制A20地址線
call empty_8042  ! 等待輸入緩衝器空。只有當輸入緩衝器為空時才可以對其進行寫命令。
mov al,#0xD1    ! command write ! 0xD1 命令碼-表示要寫資料到8042P2out #0x64,al    ! P2 埠的位1 用於A20 線的選通。


call empty_8042     ! 等待輸入緩衝器空,看命令是否被接受。
mov al,#0xDF    ! A20 on ! 選通A20 地址線的引數。
out #0x60,al    ! 資料要寫到0x60 口。
call empty_8042      ! 輸入緩衝器為空,則表示A20 線已經選通。

! well, that went ok, I hope. Now we have to reprogram the interrupts :-(
! we put them right after the intel-reserved hardware interrupts, at
! int 0x20-0x2F. There they won't mess up anything. Sadly IBM really
! messed this up with the original PC, and they haven't been able to
! rectify it afterwards. Thus the bios puts interrupts at 0x08-0x0f,
! which is used for the internal hardware interrupts as well. We just
! have to reprogram the 8259's, and it isn't fun.
!! 希望以上一切正常。現在我們必須重新對中斷進行程式設計??
!! 我們將它們放在正好處於intel 保留的硬體中斷後面,在int 0x20-0x2F!! 在那裡它們不會引起衝突。不幸的是IBM 在原PC 機中搞糟了,以後也沒有糾正過來。
!! PC 機的bios 將中斷放在了0x08-0x0f,這些中斷也被用於內部硬體中斷。
!! 所以我們就必須重新對8259 中斷控制器進行程式設計,這一點都沒勁。
……


! well, that certainly wasn't fun :-(. Hopefully it works, and we don't
! need no steenking BIOS anyway (except for the initial loading :-).
! The BIOS-routine wants lots of unnecessary data, and it's less
! "interesting" anyway. This is how REAL programmers do it.
!
! Well, now's the time to actually move into protected mode. To make
! things as simple as possible, we do no register set-up or anything,
! we let the gnu-compiled 32-bit programs do that. We just jump to
! absolute address 0x00000, in 32-bit protected mode.
!! 哼,上面這段當然沒勁,希望這樣能工作,而且我們也不再需要乏味的BIOS 了(除了
!! 初始的載入。BIOS 子程式要求很多不必要的資料,而且它一點都沒趣。那是“真正”的
!! 程式設計師所做的事。

! 這裡設定進入32 位保護模式執行。首先載入機器狀態字(lmsw - Load Machine Status Word),也稱
! 控制暫存器CR0,其位元位01 將導致CPU 工作在保護模式。
mov ax,#0x0001  ! protected mode (PE) bit ! 保護模式位元位(PE)。
lmsw ax     ! This is it! ! 就這樣載入機器狀態字!
jmpi 0,8    ! jmp offset 0 of segment 8 (cs) ! 跳轉至cs8,偏移0 處。
! 跳轉至0處,即跳轉到system
! jmpi 0,80 賦給IP,將8賦給CScs=8用來插gdt,按照之前的定址方式,段暫存器=CS<<4+IP=0x80,
! 這種定址方式,CSIP都是16位暫存器,CS<<4+IP,最多是20位地址,最大值的1M! 也就是說 計算機的定址空間是1M
! 現在的作業系統是4G的,16->32位,1M->4G,32位也叫保護模式
! 16位與32位的本質區別是CPU的解釋程式不一樣

! 我們已經將system 模組移動到0x00000 開始的地方,所以這裡的偏移地址是0。這裡的段
! 值的8 已經是保護模式下的段選擇符了,用於選擇描述符表和描述符表項以及所要求的特權級。
! 段選擇符長度為16 位(2 位元組);位0-1 表示請求的特權級0-3linux 作業系統只
! 用到兩級:0 級(系統級)和3 級(使用者級);位2 用於選擇全域性描述符表(0)還是區域性描
! 述符表(1);位3-15 是描述符表項的索引,指出選擇第幾項描述符。所以段選擇符
! 8(0b0000,0000,0000,1000)表示請求特權級0、使用全域性描述符表中的第1 項,該項指出
! 程式碼的基地址是0,因此這裡的跳轉指令就會去執行system 中的程式碼。

! This routine checks that the keyboard command queue is empty
! No timeout is used - if this hangs there is something wrong with
! the machine, and we probably couldn't proceed anyway.
! 下面這個子程式檢查鍵盤命令佇列是否為空。這裡不使用超時方法 - 如果這裡宕機,
! 則說明PC 機有問題,我們就沒有辦法再處理下去了。
! 只有當輸入緩衝器為空時(狀態暫存器位2 = 0)才可以對其進行寫命令。
empty_8042:
.word 0x00eb,0x00eb     ! 這是兩個跳轉指令的機器碼(跳轉到下一句),相當於延時空操作。
in al,#0x64     ! 8042 status port !AT 鍵盤控制器狀態暫存器。
test al,#2  ! is input buffer full? ! 測試位2,輸入緩衝器滿?
jnz empty_8042 ! yes - loop
ret


gdt: ! 全域性描述符表開始處。描述符表由多個8 位元組長的描述符項組成。
! 這裡給出了3 個描述符項。第1 項無用(206 行),但須存在。第2 項是系統程式碼段
! 描述符(208-211 行),第3 項是系統資料段描述符(213-216 行)。每個描述符的具體
! 含義參見列表後說明。
.word 0,0,0,0 ! dummy !0個表項,從0開始定址。第1 個描述符,不用。
! 這裡在gdt 表中的偏移量為0x08,當載入程式碼段暫存器(段選擇符)時,使用的是這個偏移值。以8個位元組為單位定址
! 第一個表項,一個word16位,共4個,表一個項是16*4位
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386

! 這裡在gdt 表中的偏移量是0x10,當載入資料段暫存器(如ds 等)時,使用的是這個偏移值。
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386

這裡寫圖片描述

cr0的最後一位:
置0是16位模式,即 真實模式
置1是32位模式,即保護模式

保護模式和真實模式下的地址翻譯與中斷處理

  • 真實模式下,地址是 CS<<4+IP
  • 保護模式下,地址是 根據CS查GDT,查到的值+IP,中斷處理的int n,中斷函式是查IDT表的n對應的資料

GDT:Global Descriptor Table 全域性描述符表,由硬體實現,快。GDT的內容存放,由硬體決定
CS 是selector 選擇子,存放的是索引,GDT的index(描述符索引)是 段的描述符 在描述符表的位置,該位置的值 與 IP一起,組成了32位的地址

這裡寫圖片描述

2.進入system

system模組的第一部分是 head.s

2.1 為什麼system模組的第一部分是head.s

這是由Makefile決定的,Makefile:

disk: Image # 表示disk 這個目標要由Image 產生。
dd bs=8192 if=Image of=/dev/PS0 # dd 為UNIX 標準命令:複製一個檔案,根據選項進行轉換和格式化
# bs=表示一次讀/寫的位元組數。
# if=表示輸入的檔案,of=表示輸出到的檔案。
# 這裡/dev/PS0 是指第一個軟盤驅動器(裝置檔案)。

Image: boot/bootsect boot/setup tools/system tools/build # 說明目標(Image 檔案)是由
# 冒號後面的4 個元素產生,分別是boot/bootsect 和setup 檔案、tools/system 和build 檔案。
# boot/bootsect依賴的是bootsect.s
# boot/setup依賴的是setup.s
……

# 表示tools/system 檔案要由分號右邊所列的元素生成。
# 其中 boot/head.o 依賴於head.s
# init/main.o依賴於 main.c
# DRIVERS置驅動
tools/system: boot/head.o init/main.o \
$(ARCHIVES) $(DRIVERS) $(MATH) $(LIBS) 
$(LD) $(LDFLAGS) boot/head.o init/main.o \
# $(LD) 是連結,將 boot/head.o init/main.o …… 連線到一起,就有了system  
……
-o tools/system > System.map # 生成system 的命令。
# 最後的 > System.map 表示gld 需要將連線映象重定向存放在System.map 檔案中。

2.2 head.s

setup是進入保護模式,head是進入之後的初始化

這裡寫圖片描述

movl $0x10,%eax # 將0x10的值賦給eax,這裡是32位彙編,與之前的16位彙編不同

/*
* linux/boot/head.s
*
* (C) 1991 Linus Torvalds
*/

/*
* head.s contains the 32-bit startup code.
*
* NOTE!!! Startup happens at absolute address 0x00000000, which is also where
* the page directory will exist. The startup code will be overwritten by
* the page directory.
*/
/*
* head.s 含有32 位啟動程式碼。
* 注意!!! 32 位啟動程式碼是從絕對地址0x00000000 開始的,這裡也同樣是頁目錄將存在的地方,
* 因此這裡的啟動程式碼將被頁目錄覆蓋掉。
*/
.text
.globl _idt,_gdt,_pg_dir,_tmp_floppy_area
_pg_dir: # 頁目錄將會存放在這裡。
startup_32: # 18-22 行設定各個資料段暫存器。
movl $0x10,%eax # 對於GNU 彙編來說,每個直接數要以'$'開始,否則是表示地址。
# 每個暫存器名都要以'%'開頭,eax 表示是32 位的ax 暫存器。
# 再次注意!!! 這裡已經處於32 位執行模式,因此這裡的$0x10 並不是把地址0x10 裝入各個
# 段暫存器,它現在其實是全域性段描述符表中的偏移值,或者更正確地說是一個描述符表項
# 的選擇符。這裡$0x10 的含義是請求特權級0(位0-1=0)、選擇全域性描述符表(位2=0)、
# 選擇表中第2 項(位3-15=2)。它正好指在當前的Linux 作業系統中,gas 和gld 已經分別更名為as 和ld。
# 向表中的資料段描述符項。
# 下面程式碼的含義是:置ds,es,fs,gs 中的選擇符為setup.s 中構造的資料段(全域性段描述符表
# 的第2 項)=0x10,並將堆疊放置在資料段中的_stack_start 陣列內,然後使用新的中斷描述
# 符表和全域性段描述表.新的全域性段描述表中初始內容與setup.s 中的完全一樣。
# 指向gdt的0x10項(資料段)
mov %ax,%ds
mov %ax,%es
mov %ax,%fs
mov %ax,%gs
lss _stack_start,%esp # 表示_stack_start??ss:esp,設定系統堆疊。
# stack_start 定義在kernel/sched.c,69 行。
call setup_idt # 呼叫設定中斷描述符表子程式。
call setup_gdt # 呼叫設定全域性描述符表子程式。


movl $0x10,%eax # reload all the segment registers
mov %ax,%ds # after changing gdt. CS was already
mov %ax,%es # reloaded in 'setup_gdt'
mov %ax,%fs # 因為修改了gdt,所以需要重新裝載所有的段暫存器。
mov %ax,%gs # CS 程式碼段暫存器已經在setup_gdt 中重新載入過了。

lss _stack_start,%esp


# 32-36 行用於測試A20 地址線是否已經開啟。採用的方法是向記憶體地址0x000000 處寫入任意
# 一個數值,然後看記憶體地址0x100000(1M)處是否也是這個數值。如果一直相同的話,就一直
# 比較下去,也即死迴圈、宕機。表示地址A20 線沒有選通,結果核心就不能使用1M 以上記憶體。
xorl %eax,%eax
1: incl %eax # check that A20 really IS enabled
movl %eax,0x000000 # loop forever if it isn't
cmpl %eax,0x100000
je 1b # '1b'表示向後(backward)跳轉到標號1 去(33 行)。若是'5f'則表示向前(forward)跳轉到標號5 去。
# 0地址處和1M地址處相同(A20沒開啟),就死迴圈
/*
* NOTE! 486 should set bit 16, to check for write-protect in supervisor
* mode. Then it would be unnecessary with the "verify_area()"-calls.
* 486 users probably want to set the NE (#5) bit also, so as to use
* int 16 for math errors.
*/
/*
* 注意!