用戶按下開機鍵,幾秒的時間,都經歷了啥?
1、cpu各個寄存器賦初始值,cs.base=0xffff0000, eip=0xfff0,其他寄存器都是0,這時cs:ip得到的物理地址:0xfffffff0;
cpu上電后為啥會把cs:ip賦成這種初始值了? 可能是希望把BIOS-ROM放在可尋址4GB最高端,給操作系統和用戶程序大段完整的RAM空間,便於后者在運行時的內存管理
2、cpu跳轉到0xffff0執行。但由於該地址距離0xfffff(實模式下內存空間只有1M)僅16byte,空間十分有限,無法執行復雜邏輯,只能jmp到其0xf000:e05b繼續執行;
3、0xf000:e05b任然是BIOS的地址,繼續執行檢測代碼,看看內存(RAM)、顯示器、鍵盤、鼠標、硬盤等外設是否完好。如有問題,會發出長短不等的滴滴聲響,可憑此判斷故障類型
4、外設檢測完,如果一切正常,會查找用戶設置的啟動順序。普通用戶首次安裝OS時一般選擇從CD/DVD啟動,裝OS;裝好后取出光盤,BIOS會自動從磁盤加載MBR到0x7c00;
5、加載MBR到0x7c00后,jmp到這里繼續執行;由於只加載一個扇區,能執行的代碼不超過510字節(還有2字節是扇區結尾的標識:0xaa55),能干的事也有限,所以MBR一般會繼續從磁盤其他地方把os代碼都拷貝到內存,同時重定位代碼,完成os的加載;
6、繼續jmp到os代碼執行;
這6步中,1-4部不用我們操心,廠家的產品在出廠前已經做好;第5部,從磁盤的0柱面、0磁頭、1扇區加載MBR到內存0x7c00處也BIOS干的,不需要開發人員操心;真正需要開發人員編寫代碼的地方:
- MBR的代碼,這部分代碼被bios加載到內存后需要做什么?
- MBR代碼能運行的代碼不超過510字節,真正的os肯定不止這點代碼,剩余代碼怎么辦?
既然MBR能運行的代碼不超過510字節,能干的活有限,那么干脆簡單點,把os或用戶程序剩余代碼加載到內存,完成重定位,再跳到這些代碼執行,具體代碼如下:
1、MBR代碼
;MBR 主引導扇區 ;開機上電后,BIOS會自動從0x7c00處執行 lba_num equ 100;一共101個扇區,用戶程序在硬盤中的邏輯扇區號 SECTION mbr vstart=0x7c00 align=16;以16位對齊 ;cs和IP已經運行到這里,不用再設置了 mov ax,0 mov ss,ax;堆棧段從0開始 mov sp,ax; mov ax,[cs:phy_address];目前在cs段,如果不寫,默認讀ds段; mov dx,[cs:phy_address+0x2] mov bx,0x10 div bx;相當於右移4bit,得到0x1000,就是段地址,放在ax mov ds,ax; xor bx,bx; mov si,lba_num xor di,di call read_disk;先讀第一個扇區,把用戶程序的頭部加載到內存,才能得到重定位表 mov ax,[0];program_len分別放在ax和bx; mov dx,[2];從內存讀數據,不加段前綴的默認是ds; mov bx,512 div bx;ax = 用戶程序的扇區個數 dx=扇區余數,也就是最后不滿一個扇區內偏移 cmp dx,0; test dx,dx jnz cantDiv;不能被整除,說明有數據不滿一個扇區的數據,但也要占用一個扇區的空間 dec ax;扇區數減一:前面已經讀了一個扇區。 cantDiv: cmp ax,0;已經讀完了,可以直接重定位 jz realloc; mov cx,ax;剩余扇區數放入cx,方便后續loop push ds Continue_Read: inc si mov ax,ds add ax,0x20;基址增加0x20,相當於增加512byte,比如:ds:bx = 0000:0000 = 00000; ds:bx = 0020:0000 = 0200+0000=0x0200=512byte mov ds,ax;往高地址挪一個扇區512byte xor bx,bx;偏移清零,通過段基址挪動 call read_disk;相當於寄存器傳參 loop Continue_Read pop ds ;---------------上面都是把數據從磁盤讀到內存,下面開始重定位------------------------------------ ;先計算出用戶程序code_entry在內存的絕對地址 realloc: mov ax,[0x06];默認是ds段,此時已是0x1000;code_entry的section.code1.start低2字節 mov dx,[0x08];code_entry的section.code1.start高2字節 call reallocaddress mov [0x06],ax;把內存中的物理地址寫回去,這次得到絕對物理地址了; ;mov [0x06],ds mov cx,[0x0a];5個段需要重定位 mov bx,0x0c; ;用戶程序每個section都計算出內存的絕對地址,然后寫回去 reallocLoop: mov ax,[bx] mov dx,[bx+2] call reallocaddress mov [bx],ax; add bx,4 loop reallocLoop jmp far [0x04];內存操作默認以ds基址,這里是0x10000;跳轉到用戶程序start變量地址 ;mov ax, [0x04];得到offset,就是start的偏移地址 ;jmp 0x1000:ax ;dx:ax 32位偏移地址,寄存器傳參 ;輸出16位段基址,保存在ax reallocaddress: push dx add ax,[cs:phy_address];注意:目前在cs段,不加從內存讀數據默認用ds,此處為用戶程序;ax=0x0000+[0x10006]=0x0020; add dx,[cs:phy_address+0x2];dx=0x0001 shr ax,4;低16位地址的低4位去掉,高4位補零,得到段基址;ax=0x0002 ror dx,4;高16位地址循環右移;dx=0x1000 and dx,0xf000;取出最需要的4bit,其他清零;dx=0x1000 or ax,dx;ax=0x1002 pop dx ret ;ds:bx 從硬盤讀數據到該物理地址 ;di, si 是邏輯扇區號:邏輯扇區只用28位,所以di有4位是不用的;si是邏輯扇區低16位 ;可以通過int 0x13中斷讀取,也可以通過磁盤控制器讀取; read_disk: push ax push bx push cx push dx push si push di ;https://www.cnblogs.com/mlzrq/p/10223060.html 詳細說明 mov dx,0x1f2;磁盤端口,指定讀取或寫入的扇區數 mov al,1;每次讀一個扇區 out dx,al;往端口寫入數據 inc dx;0x1f3 lba地址的低8位,就是0-7位 mov ax,si; out dx,al;先把低8位寫入端口,因為用戶程序被寫入了磁盤100號扇區,所以調用函數傳參數di=100 inc dx;0x1f4 lba地址的中8位,就是8-15位 mov al,ah out dx,al; inc dx;0x1f5 lba地址的高8位,就是16-23位 mov ax,di; out dx,al; ;上面3個已經把前面24位填滿,這里填最高4位 inc dx;0x1f6 lba地址的前4位,就是24-27位 mov al,0xe0; 高4位是各種標志位: 0 CHS,1 LBA; 1; 0 從 1 主; 0; 這里是e; or al,ah out dx,al inc dx;0x1f7 mov al,0x20;發送讀扇區的請求:0x20 out dx,al ;------------------------------ and al,0x88 邏輯上出錯,先屏蔽試試 waits: in al,dx; 從0x1f7讀取磁盤狀態,一共有8位;第7位:1表示busy 第3位:1表示准備好讀寫操作,所以在0xxx1xxx的時候才能讀寫,其他狀態都不行; and al,0x88;第7位和第3位保持不變,其他清零 cmp al,0x08; jnz waits;狀態不等於0x08,說明沒准備好,繼續等待 mov dx,0x01f0;數據端口,16位,需要ax接數據;每個扇區512byte,每次讀2byte,要讀256次 mov cx,256; ;准備好了,開始讀磁盤 readw: in ax,dx; mov [bx],ax; add bx,2;每次讀2byte loop readw; pop di pop si pop dx pop cx pop bx pop ax ret phy_address dd 0x10000;用戶程序拷貝到內存地址 times 510 - ($-$$) db 0; dw 0xaa55
2、用戶程序
;用戶程序 ;段的數目並未限制,用戶可根據需求自行創建 ;------------------------------------------------------------------------------- SECTION header vstart=0;vstart=0連着寫,不能有空格 program_len dd program_end; code_entry dw start;變量偏移0x4; dd section.code1.start;code1段基址:變量偏移0x6 reallocate_item dw (header_end-code1Segment)/4 ;每個段偏移都是dd=4byte,變量偏移0xa ;重定位表,記錄重要段相對於程序起始位置的偏移 code1Segment dd section.code1.start;變量偏移0xc data1Segment dd section.data1.start;變量偏移0x10 本section在文件中的真實偏移量(真實地址),或則說相對開始的偏移地址 stack1Segment dd section.stack1.start;變量偏移0x14 use1Segment dd section.use1.start;變量偏移0x18 use1DataSegment dd section.use1Data.start;變量偏移0x1c header_end: ;有vstart = 0,header_end從vstart = 0開始算偏移 ;------------------------------------------------------------------------------- SECTION use1 align=16 vstart=0; ;------------------------------------------------------------------------------- SECTION use1Data align=16 vstart=0; use1Data_end: ;------------------------------------------------------------------------------- SECTION code1 align=16 vstart=0; ;vstart=0連這些,不能有空格 ;直接調用BIOS例程在顯示器打印 start: mov ax,[stack1Segment];初始化堆棧 mov ss,ax mov ax,stacker_pointer; mov sp,ax; xor ah,ah mov al,0x03 int 0x10;調用bios的0x10號中斷清屏 ;AL=寫模式,BH=頁碼,BL=顏色,CX=字符串長度,DH=行,DL=列,ES:BP=字符串偏移量 ;https://zh.wikipedia.org/wiki/INT_10H 有詳細說明 mov ah,0x13 mov al,1 xor bh,bh mov bl,0x04 mov cx, data1_end - msg;cx保存字符串長度 mov dh,12;顯示的行號 mov dl,25;顯示的列號 mov bp,msg; es:bp指向需要打印的字符串 push ax mov ax,[data1Segment] ;mov ax,cs; mov es,ax;es:bp 為串首地址 pop ax int 0x10 hlt;程序待機 ;------------------------------------------------------------------------------- SECTION data1 align=16 vstart=0 msg db 'are you ready?', 0 data1_end: ;------------------------------------------------------------------------------- SECTION stack1 align=16 vstart=0; resb 256; reserve byte,保留/分配256byte空間 stacker_pointer: ;棧底放在高地址 ;------------------------------------------------------------------------------- SECTION tail align=16; 這個段沒有vstart = 0,那就從開頭計算偏移,也就是SECTION header開始算; program_end:
說明: (1)SECTION用於定於段,沒有數量限制,開發人員可根據需求取舍
(2)vstart=0表示該段內的標識都從0開始計算偏移。如果沒有 vstart=0,那么段內標識比如msg、start等都從程序開始處計算偏移;
vstart=0千萬要緊挨着,不能有個空格,不能有個空格,不能有個空格,重要的事情說三遍。否則這種聲明無效,段內標識的偏移還是會從程序開頭處計算,導致后續邏輯出錯
(3)段的數量沒限制,但是建議把代碼段和數據段分開,各種變量盡量在數據段聲明;代碼段聲明的變量因未隔離開,容易被cpu當成代碼執行,導致異常或邏輯錯亂
(4)MBR為什么要用0xaa55了? 0xaa55=0b 1010 1010 0101 0101,看出來有啥特點了么? 0和1交叉呈現,就像梳子一樣;奇偶校驗總是為偶數,a和5與是0,或是f;