setup.s 分析—— Linux-0.11 学习笔记(二)
更新記錄
| 1.0 | 2018-4-14 | 增加了“獲取顯示模式”這一節,AL取值的表格 |
標題: setup.s 分析—— Linux-0.11 學習筆記(二)
老規矩,為了節省篇幅,完整的代碼就不貼了。
定義符號常量
INITSEG = 0x9000 ! bootsect.s 的段地址 SYSSEG = 0x1000 ! system loaded at 0x10000 SETUPSEG = 0x9020 ! 本程序的段地址注意:以上這些參數最好和 bootsect.s 中的相同。
獲取一些參數保存在 0x90000 處
保存光標的位置
mov ax,#INITSEG !INITSEG = 0x9000mov ds,ax ! ds = 0x9000mov ah,#0x03 ! 功能號=3,獲取光標的位置xor bh,bh ! bh = 頁號 = 0(輸入)int 0x10 ! 輸出: DH=行號,DL=列號mov [0],dx ! 保存光標的行號和列號到 0x90000,共占2字節.獲取從 1M 處開始的擴展內存大小
! 利用 BIOS 中斷 0x15 功能號 ah = 0x88 取系統所含擴展內存大小,并保存在內存 0x90002 處! 返回:ax=從0xl00000(lM)處開始的擴展內存大小(KB).若出錯則CF置位,ax=出錯碼mov ah,#0x88int 0x15mov [2],ax ! ax = 從1M處開始的擴展內存大小獲取顯示模式
! 獲取顯示卡當前的顯示模式! 調用 BIOS 中斷 0x10,功能號 ah = 0x0f! 返回: ah=字符列數; al=顯示模式;bh=當前顯示頁。! 0x90004(l個字)存放當前頁;0x90006(1字節)存放顯示模式;0x90007(1字節)存放字符列數。mov ah,#0x0fint 0x10mov [4],bx ! bh = 當前顯示頁mov [6],ax ! al = 顯示模式, ah = 字符列數(窗口寬度)AL 取值的含義如下表:
| 0 | text | 40x25 | 8x8* | 16/8 (shades) | CGA,EGA | b800 | Composite |
| 1 | text | 40x25 | 8x8* | 16/8 | CGA,EGA | b800 | Comp,RGB,Enh |
| 2 | text | 80x25 | 8x8* | 16/8 (shades) | CGA,EGA | b800 | Composite |
| 3 | text | 80x25 | 8x8* | 16/8 | CGA,EGA | b800 | Comp,RGB,Enh |
| 4 | graphic | 320x200 | 8x8 | 4 | CGA,EGA | b800 | Comp,RGB,Enh |
| 5 | graphic | 320x200 | 8x8 | 4 (shades) | CGA,EGA | b800 | Composite |
| 6 | graphic | 640x200 | 8x8 | 2 | CGA,EGA | b800 | Comp,RGB,Enh |
| 7 | text | 80x25 | 9x14* | 3 (b/w/bold) | MDA,EGA | b000 | TTL Mono |
| 8,9,0aH | PCjr modes | ||||||
| 0bH,0cH | (reserved; internal to EGA BIOS) | ||||||
| 0dH | graphic | 320x200 | 8x8 | 16 | EGA,VGA | a000 | Enh,Anlg |
| 0eH | graphic | 640x200 | 8x8 | 16 | EGA,VGA | a000 | Enh,Anlg |
| 0fH | graphic | 640x350 | 8x14 | 3 (b/w/bold) | EGA,VGA | a000 | Enh,Anlg,Mono |
| 10H | graphic | 640x350 | 8x14 | 4 or 16 | EGA,VGA | a000 | Enh,Anlg |
| 11H | graphic | 640x480 | 8x16 | 2 | VGA | a000 | Anlg |
| 12H | graphic | 640x480 | 8x16 | 16 | VGA | a000 | Anlg |
| 13H | graphic | 640x480 | 8x16 | 256 | VGA | a000 | Anlg |
Notes: With EGA, VGA, and PCjr you can add 80H to AL to initialize a video mode without clearing the screen.
*The character cell size for modes 0-3 and 7 varies, depending on the hardware. On modes 0-3: CGA=8x8, EGA=8x14, and VGA=9x16. For mode 7, MDPA and EGA=9x14, VGA=9x16, LCD=8x8.
檢查顯示方式(EGA/VGA)并獲取參數
! 檢查顯示方式(EGA/VGA)并獲取參數。! 調用 BIOS 中斷 0x10,功能號: ah = 0xl2,子功能號: bl = 0xl0! 返回:bh=顯示狀態。 0x00-彩色模式,I/O 端口=0x3dX! 0x01-單色模式,I/O 端口=0x3bX! bl = 安裝的顯示內存。0x00 - 64k! 0x01 - 128k! 0x02 - 192k! 0x03 - 256k! cx = 顯示卡特性參數。!mov ah,#0x12 ! 功能號mov bl,#0x10 ! 子功能號int 0x10mov [8],ax ! 我也不知道這個是什么(╯︵╰)mov [10],bx ! bh=顯示狀態(單色模式/彩色模式),bl=已安裝的顯存大小mov [12],cx ! ch=特性連接器比特位信息,cl=視頻開關設置信息關于返回參數的詳細解釋,還是看這張圖吧,圖片來自趙炯博士的《Linux內核完全剖析》(機械工業出版社,2006)。
BIOS 視頻中斷 0x10
復制硬盤參數表
復制 HD0 的硬盤參數表
! 復制 hd0 的硬盤參數表,參數表地址是中斷向量0x41的值,表長度16B ! 中斷向量在中斷向量表中的位置 = 中斷類型號N × 4 ! (N*4)的字單元存放偏移地址; ! (N*4+2)的字單元存放段基址。mov ax,#0x0000mov ds,ax ! ds=0 ! 將內存[4*0x41]處的低2字節(偏移地址)傳給si,高2字節(段地址)傳給dslds si,[4*0x41]mov ax,#INITSEGmov es,ax !es = 0x9000mov di,#0x0080mov cx,#0x10 !重復16次 ! ds:si --> es:di(0x9000:0x0080),共傳送16Brepmovsb復制 HD1 的硬盤參數表
! 復制 hd1 的硬盤參數表,參數表地址是中斷向量0x46的值,表長度16B ! 道理同上一小節,此處不贅述mov ax,#0x0000mov ds,axlds si,[4*0x46]mov ax,#INITSEG ! INITSEG = 0x9000mov es,axmov di,#0x0090mov cx,#0x10 ! ds:si --> es:di(0x9000:0x0090),共傳送16Brepmovsb檢查系統是否有第2個硬盤
! 檢查系統是否有第2個硬盤,如果沒有就把第2個參數表清零 ! 利用 BIOS 中斷調用 0x13 的取盤類型功能,功能號 ah = 0xl5; ! 輸入: dl=驅動器號(0x8X 是硬盤:0x80 指第 1 個硬盤,0x81 第 2 個硬盤) ! 輸出: ah=類型碼;00-沒有這個盤,CF 置位; ! 01-是軟驅,沒有 change-line 支持; ! 02 -是軟驅(或其他可移動設備),有 change-line 支持; ! 03 -是硬盤。 !mov ax,#0x01500 ! 功能號 ah=0x15,讀取盤類型mov dl,#0x81 ! dl=驅動器號,0x81代表第2個硬盤int 0x13 jc no_disk1 ! CF置位,表示沒有這個盤cmp ah,#3 je is_disk1 ! ah=3表示存在第2個硬盤,跳轉到is_disk1 no_disk1: ! 清空第2個表mov ax,#INITSEGmov es,axmov di,#0x0090 ! es:di = 0x9000:0x0090mov cx,#0x10mov ax,#0x00 ! AL=0repstosb ! Store AL at address es:di is_disk1:關中斷
! 為進入保護模式做準備cli ! no interrupts allowed !移動 system 模塊到 0x00000
bootsect.s 引導程序將 system 模塊讀入到 0xl0000 開始的位置。由于當時假設 system 模塊最大長度不會超過 0x80000 (512KB),即其末端不會超過內存地址 0x90000,所以 bootsect.s 會把自己移動到0x90000 開始的地方,并把 setup 加載到它的后面。下面這段程序的用途是再把整個 system 模塊移動到 0x00000 位置,即把從 0x10000 到 0x8ffff 的內存數據塊(共512KB)整塊地向內存低端移動了0x10000(64KB)。
! 從代碼實現來看,是一小塊(0x10000B=64KB)一小塊移動的,共移動8小塊。mov ax,#0x0000cld ! 'direction'=0, movs moves forward do_move:mov es,ax ! es是目的段地址add ax,#0x1000cmp ax,#0x9000 ! 當 ax==0x9000 時結束移動jz end_movemov ds,ax ! ds是源段地址,ds比es大0x1000sub di,di ! di = 0sub si,si ! si = 0mov cx,#0x8000 ! 重復 0x8000次rep ! ds:si --> es:dimovsw ! 每次移動2B.jmp do_move ! 本輪一共移動 0x8000*2B = 0x10000B=64KB. 準備下一輪移動end_move:上面的匯編代碼寫成偽C語言代碼如下:
ax = 0; cld;while(1){es = ax;ax += 0x1000;if(ax == 0x9000)break; //結束移動ds = ax;di = si = 0;for(int i=0; i<0x8000; ++i){memcpy(es:di, ds:si, 2);di += 2;si += 2;} }搬運示意圖如下:
加載IDT
end_move:mov ax,#SETUPSEG mov ds,ax !ds = 0x9020,指向本程序段,setup.s 被加載到 0x90200!idt_48 標號處的內容如下!idt_48:! .word 0 ! idt 界限值=0! .word 0,0 ! idt 基地址=0Llidt idt_48 ! load idt with 0,0加載GDT
!gdt_48 標號處的內容如下!gdt_48:!.word 0x800 ! 0x800 = 2048, 2048/8=256,可容納256個描述符, 其實0x7ff即可!.word 512+gdt,0x9 ! setup.s被加載到0x90200, gdt base = 0x90200+gdt = 0x90000+512+gdtlgdt gdt_48開啟A20
什么是A20?為什么要開啟?可以參考我的博文: 關于A20
PC機主板上的鍵盤接口是專用接口,它可以看作是常規串行端口的一個簡化版本。該接口被稱為鍵盤控制器,它使用串行通信協議接收鍵盤發來的掃描碼數據。主板上所采用的鍵盤控制器是 Intel 8042 芯片或其兼容芯片。現今的主板上已經不包括獨立的 8042 芯片了,但是主板上其他集成電路會為兼容目的而模擬 8042 芯片的功能。另外,該芯片輸出端口 P2 各位被分別用于其他目的。bit_0 (P20引腳)用于實現 CPU 的復位操作(低電平導致復位),bit_1(P21 引腳)用于控制 A20 信號線的開啟與否,為1時就開啟(選通)A20 信號線,為0則禁止 A20 信號線。
call empty_8042 ! 等待輸入緩沖器為空mov al,#0xD1 out #0x64,al call empty_8042 ! 等待輸入緩沖器為空,即命令被接受mov al,#0xDF ! A20 onout #0x60,alcall empty_8042 ! 等待輸入緩沖器為空,即參數被接受mov al,#0xD1
0xD1是命令碼,表示寫8042的輸出端口P2,原IBM PC使用P2的bit_1控制A20門。此命令后面帶一個字節的參數,這個參數由端口0x60寫入。要開啟A20,就要使參數的b1=1,另外還要使b0=1,否則系統會復位。
mov al,#0xDF
0xDF是參數,寫成2進制是1101_1111,可以看出,b0=1,b1=1。
至于其他bit的值是怎么得來的,我也不知道。(T▽T)
至于機器是否真正開啟了A20地址線,我們還需要在進入保護模式之后再測試一下。這個工作放在了head.s程序中。head.s的代碼咱們以后再分析。
empty_8042:.word 0x00eb,0x00eb !機器碼,跳轉到下一句,為了延時in al,#0x64 ! 8042 status porttest al,#2 ! is input buffer full?jnz empty_8042 ! yes - loopret解釋一下empty_8042這個過程。
in al,#0x64讀端口 0x64 到 AL.
讀端口0x64就是讀8042的狀態寄存器(一個8bit的只讀寄存器),bit_1為1時表示輸入緩沖器滿,為0時表示輸入緩沖器空。要向8042寫命令(通過0x64端口寫入),必須當輸入緩沖器為空時才可以。
test al,#2用于檢測bit_1,如果為1,則跳轉到empty_8042標號處繼續檢測,直到bit_1為0才返回。
所以empty_8042這個過程就是為了等待輸入緩沖器為空。
設置8259
; ICW1 mov al,#0x11 ! initialization sequenceout #0x20,al ! send ICW1 to Master.word 0x00eb,0x00eb ! jmp $+2, jmp $+2out #0xA0,al ! send ICW1 to Slave.word 0x00eb,0x00eb;------------------------------------------------------; ICW2mov al,#0x20 ! 送主芯片ICW2命令字,設置起始中斷號,要送奇端口 out #0x21,al.word 0x00eb,0x00ebmov al,#0x28 ! 送從芯片ICW2命令字,設置起始中斷號,要送奇端口out #0xA1,al.word 0x00eb,0x00eb;-------------------------------------------------------; ICW3mov al,#0x04 ! 8259-1 is masterout #0x21,al.word 0x00eb,0x00ebmov al,#0x02 ! 8259-2 is slaveout #0xA1,al.word 0x00eb,0x00eb;------------------------------------------------------; ICW4mov al,#0x01 out #0x21,al.word 0x00eb,0x00ebout #0xA1,al.word 0x00eb,0x00eb;------------------------------------------------------mov al,#0xFF ! mask off all interrupts for nowout #0x21,al.word 0x00eb,0x00ebout #0xA1,al字(0x00eb)是直接使用機器碼表示的一條相對跳轉指令,起延時作用。0xeb是直接近跳轉指令的操作碼,帶1個字節的相對位移值。因此跳轉范圍是 -128到 +127. CPU 通過把這個相對位移值加到 EIP 寄存器中就形成一個新的有效地址。注意:執行某條指令的時候,EIP會指向它的下一條指令。所以,CPU執行0x00eb的時候,會把EIP的值加上 0 ,其實就是下一條指令的地址,然后跳轉到那里去執行。
0x00eb,0x00eb這兩條指令共可提供 14~20 個 CPU 時鐘周期的延遲時間。在 as86 中沒有表示相應指令的助記符,因此 Linus 在 setup.s 等一些匯編程序中就直接使用機器碼來表示這種指令。另外,每個空操作指令 N0P 的時鐘周期數是 3 個,因此若要達到相同的延遲效果就需要 6 至 7 個 N0P 指令。
關于 8259A 的知識可以參考我的博文 : 詳解8259A
對于每個命令字的端口,我列了一張速查表。
| ICW1 | 0 | 0x20 | 0xA0 | D4 = 1 |
| ICW2 | 1 | 0x21 | 0xA1 | |
| ICW3 | 1 | 0x21 | 0xA1 | |
| ICW4 | 1 | 0x21 | 0xA1 | |
| OCW1 | 1 | 0x21 | 0xA1 | |
| OCW2 | 0 | 0x20 | 0xA0 | D4-D3 = 00 |
| OCW3 | 0 | 0x20 | 0xA0 | D4-D3 = 01 |
ICW1
mov al,#0x11 out #0x20,al向主片寫入0x11 = 0001_0001b, 表示初始化命令開始,它是 ICW1 命令字。 對照表格可以知道——邊沿觸發、 多片8259級聯、最后要發送 ICW4 命令字。
| D0 | 1:需要ICW4 0:不需要ICW4 |
| D1 | 1:單片 0:級聯 |
| D2 | =0; |
| D3 | 1:電平觸發 0:邊沿觸發 |
| D4 | =1 |
| D7-D5 | =000 |
ICW2
mov al,#0x20 ! start of hardware int's (0x20) out #0x21,al送主芯片 ICW2 命令字,設置起始中斷號為0x20,則主片 0~7 級對應的中斷號是 0x20~0x27;
mov al,#0x28 ! start of hardware int's 2 (0x28) out #0xA1,al送從芯片 ICW2 命令字,設置起始中斷號為0x28,則從片 8~15 級對應的中斷號是 0x28~0x2F;
ICW3
mov al,#0x04 ! 8259-1 is master out #0x21,al .word 0x00eb,0x00eb mov al,#0x02 ! 8259-2 is slave out #0xA1,al .word 0x00eb,0x00eb1~2行:送主芯片 ICW3 命令字,0x04 = 0000_0100b,表示主芯片的 IR2 連從芯片的 INT。
4~5行:送從芯片 ICW3 命令字,表示從芯片的 INT 連到主芯片的 IR2 引腳上。
ICW4
mov al,#0x01 out #0x21,al .word 0x00eb,0x00eb out #0xA1,al .word 0x00eb,0x00eb送 ICW4 命令字。普通 E0I(需發送指令來復位)、非緩沖方式、非特殊全嵌套。
| D7-D5 | =0 |
| D4 | 1:特殊全嵌套 0:非特殊全嵌套 |
| D3-D2 | 0X:非緩沖 10:緩沖-從片 11:緩沖-主片 |
| D1 | 1:自動 EOI 0:普通 EOI |
| D0 | =1 |
OCW1
mov al,#0xFF out #0x21,al ! 屏蔽主片所有中斷請求 .word 0x00eb,0x00eb out #0xA1,al ! 屏蔽從片所有中斷請求。OCW1 用于對8259的中斷屏蔽寄存器進行讀/寫操作,若Di=1,則屏蔽對應中斷請求級IRi.
進入保護模式
下面設置并進入32位保護模式運行。
首先加載機器狀態字(lmsw,Load Machine Status Word),也稱控制寄存器 CR0,其比特位 0 置 1 將使 CPU 切換到保護模式,并且運行在特權級0,即當前特權級 CPL = 0。此時各個段寄存器仍然指向與實地址模式中相同的線性地址處(在實地址模式下線性地址與物理地址相同)。在設置該比特位后,隨后一條指令必須是一條段間跳轉指令,用于刷新CPU當前指令隊列。因為 CPU 是在執行一條指令之前就已從內存讀取該指令并對其進行譯碼。然而在進入保護模式以后那些屬于實模式的預先取得的指令信息就變得不再有效。而一條段間跳轉指令就會刷新 CPU 的當前指令隊列,即丟棄這些無效信息。另外,Intel手冊上建議 80386 或以上 CPU 應該使用指令 mov cr0,ax 切換到保護模式。lmsw 指令僅用于兼容以前的 286 CPU。
mov ax,#0x0001 ! Protection Enable (bit 0 of CR0). lmsw ax ! 實際上lmsw指令僅僅加載CR0的低4位,由低到高分別是PE,MP,EM,TS jmpi 0,8 ! jmp offset 0 of segment 8 (cs)實際上lmsw指令僅僅加載CR0的低4位,由低到高分別是PE,MP,EM,TS. 這里我們僅關注 PE,其他的都設為0.
jmpi 0,8 段間跳轉指令。執行后,CS=8,IP=0.
關于這里的段間跳轉,要多說幾句。
即使是在實模式下,段寄存器的描述符高速緩存器也被用于訪問內存,僅低20位有效,高12位是全零。當處理器進入保護模式后,這些內容依然殘留著,但不影響使用,程序可以繼續執行。但是,這些殘留的內容在保護模式下是無效的,遲早會在執 行某些指令的時候出問題。因此,比較安全的做法是盡快刷新 CS、SS、DS 、ES 、FS 和 GS 的內容,包括它們的段選擇器和描述符高速緩存器。
在進入保護模式之前,有很多指令已經進入了流水線。因為處理器工作在實模式下,所以它們都是按16位操作數和16位地址長度進行譯碼的,即使是那些用 bits 32 編譯的指令。進入保護模式后,受CS 段描述符高速緩存器中實模式殘留內容的影響,處理器進入16位保護模式工作。如果保護模式下的代碼是16位的,影響可能不大,但如果是用 bits 32 編譯的,那么,由于對操作數和默認地址大小的解釋不同,指令的執行結果可能會不正確,所以必須清空流水線。同時,那些通過亂序執行得到的中間結果也是無效的,必須清理掉,讓處理器串行化執行,即重新按指令的自然順序執行。
怎么辦呢?這里有一個兩全其美的方案,那就是使用段間跳轉指jmpi。處理器最怕轉移指令,遇到這種指令,一般會淸空流水線,并串行化執行;另一方面,段間跳轉會重新加載段選擇器CS,并刷新描述符高速緩存器中的內容。
jmpi 0,8 中的 “8 ”是保護模式下的段選擇子,用于選擇描述符表(GDT或LDT)和描述符表項以及所要求的特權級。段選擇子長度為16位(2字節)。
| b1-b0 | 請求特權級(RPL) |
| b2 | 0:全局描述符表 1:局部描述符表 |
| b15-b3 | 描述符表項的索引, 指出選擇第幾項描述符(從0開始) |
位0-1表示請求特權級(RPL),Linux操作系統只用到兩級——0級(內核級)和3級(用戶級);位2 用于選擇全局描述符表還是局部描述符表;位3-15是描述符表項的索引,指出選擇第幾項描述符。所以段選擇子8(= 0000_0000_0000_1000b)表示請求特權級0、使用全局描述符表GDT中第1個段描述符項(GDT表在后文分析),該項是一個代碼段描述符,指出代碼段的基地址是0,又因為偏移值是0,所以這個跳轉指令會跳轉到0地址,即運行system模塊。
從邏輯地址到線性地址的轉換規則如下圖:
到這里,setup.s 文件就分析完了。不過還剩一個小尾巴,就是文件末尾定義的GDT表。
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有了這個小程序,分析段描述符再也不用發愁了,So easy !
80x86描述符總結及解析描述符的小程序
| 0 | 空描述符 | - | - | - | - | - | - | - |
| 1 | 代碼段 | 0 | 0X7FF | 4KB | 1 | 0 | 代碼段,非一致性,可讀 | 0x08 |
| 2 | 數據段 | 0 | 0X7FF | 4KB | 1 | 0 | 數據段,向上擴展,可寫 | 0x10 |
參考資料
1《Linux內核完全剖析》(趙炯,機械工業出版社,2006)
2《x86匯編語言:從實模式到保護模式》(李忠,2013)
3 http://webpages.charter.net/danrollins/techhelp/0114.HTM
總結
以上是生活随笔為你收集整理的setup.s 分析—— Linux-0.11 学习笔记(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 函数基础作业
- 下一篇: head.s 分析——Linux-0.1