https://www.cnblogs.com/Philip-Tell-Truth/p/5317983.html 這里有詳細的過程說明。文字很多,為了方便閱讀和理解,提煉了一些要點后歸納、整理了如下導圖:
這次主要介紹實模式下的中斷原理和demo示例;按照中斷來源,分外部硬件中斷、CPU內部中斷和軟中斷。
1、外部硬件中斷:可簡單理解為和CPU關聯的外部設備產生的中斷;
- 非可屏蔽中斷
(1)non maskable interupt,簡稱NMI。從字面上看,既然不可屏蔽,說明優先級最高,CPU必須立即處理,不能有絲毫怠慢,一般情況下遇到了嚴重錯誤,比如電源掉電、總線奇偶位錯誤、內存讀寫錯誤等;
(2)有專門的NMI引腳接受外部信號,一旦觸發(需要持續4個時鍾周期),產生2號中斷,隨后跳轉到IVT查找相應的處理例程;
(3)CMOS的0x70端口最高位置1時仍然能屏蔽NMI信號,所以嚴格講還是能被屏蔽的;
- 可屏蔽中斷
(1)通過INTR引腳觸發。那么問題來了:只有一個INTR引腳,但外設有很多(鍵盤、鼠標、顯示器、打印機、藍牙、wifi、串口、網卡等),cpu該怎么響應不同外設的中斷請求了?——通過8259A代理;8259A有8個輸入,可以接受8個外部中斷源的輸入。超過8個后還能互相級聯,將中斷源擴展至16個,如下:
(2)EFLAGS.IF=0時CPU不再響應通過8259輸入的中斷請求(EFLAGS在CPU內部,影響所有引腳);cli置IF=0,sti置IF=1;
(3)8259內置IMR寄存器,可選擇性屏蔽某個引腳的中斷信號,其他引腳不影響;
(4)從片的IR0連接系統定時器(8號口輸入的中斷,俗稱8號中斷),主要由os根據RTC維護,嚴格講僅僅是個計時器;
- 中斷向量表IVT
(1)整個地址的起始空間:0x00000~0x003ff;中斷號*4處,取高2byte作為cs,低2byte作為ip尋找具體的中斷處理例程
(2)發生中斷時,EGLAGS、cs、ip分別入棧,執行完后再調用iret依次彈出;入棧EFLAGS的同時IF和TF置0,但也可以人為設置sti打開中斷請求
(3)IVT可由os修改;中斷例程函數執行完后需要顯示執行end of interupt命令告訴主從芯片,以便重新打開中斷請求
2、內部中斷
CPU執行指令時產生的中斷,比如除0;不受EFLAGS.IF位和時鍾周期影響;
3、軟中斷
(1)BIOS自帶,從磁盤加載引導扇區之前就有了,所以加載過程中是可以直接調用的,比如 int 0x10再屏幕顯示字符、int 0x13從磁盤讀寫數據
(2)部分外設自帶ROM存儲了初始化代碼和中斷例程
4、時鍾中斷,如下:
(1)南橋內部有兩個核心芯片:RTC獨立於CPU,負責實時計時,為整個計算機提供最基礎的時間服務;CMOS負責存儲時間;
(2)8259主片: IR0連接系統定時器, 從片IR0連接RTC計時器
(3) 系統定時器每55ms向CPU發送中斷請求,然后進入8號中斷例程執行,這里無需用戶顯示執行int指令;
(4)PC定時器有個輸入頻率是1193180Hz,每個時鍾周期減1,減到0就輸出時鍾中斷信號;計數器是16位的,最大65536,那么時鍾中斷發生頻率=1193180Hz/65536=18Hz,也就是每秒輸出18次;每次輸出間隔=1000ms/18hz=55毫秒;
(5)8號中斷例程又會執行int 0x70,該例程默認只有iret,用戶可根據需求添加所需功能,比如線程切換、定時等
中斷號一覽表:
6、核心代碼解析
(1)用戶代碼加載到內核后重定位:實模式下把段基址(物理地址)寫回頭部事現預留的特定位置;保護模式把選擇子寫回;
(2)計算0x70號中斷在IVT的偏移地址,這里bx=0x1c0
mov al,0x70 mov bl,4 mul bl ;計算0x70號中斷在IVT中的偏移=0x70*0x4=0x1c0 mov bx,ax ;bx=0x1c0
(3)自定義的0x70例程的入口寫回IVT
cli ;防止改動期間發生新的0x70號中斷 push es mov ax,0x0000 ;IVT地址范圍:0x00000~0x003ff mov es,ax mov word [es:bx],new_int_0x70 ;自定義中斷處理例程的偏移地址 [0x1c0]=0x0000 mov word [es:bx+2],cs ;段地址[0x1c2]=0x1002 pop es
(4)RTC、CMOS RAM、8259各種參數設置
mov al,0x0b ;RTC寄存器B or al,0x80 ;阻斷NMI out 0x70,al mov al,0x12 ;設置寄存器B,禁止周期性中斷,開放更 out 0x71,al ;新結束后中斷,BCD碼,24小時制 mov al,0x0c out 0x70,al in al,0x71 ;讀RTC寄存器C,復位未決的中斷狀態 in al,0xa1 ;讀8259從片的IMR寄存器 and al,0xfe ;清除bit 0(此位連接RTC),讓該位的輸入有效 out 0xa1,al ;寫回此寄存器 sti ;重新開放中斷
(5)自定義0x70函數:主要是從RTC讀取當前時間,然后在顯示器打印出來
xor al,al or al,0x80 out 0x70,al in al,0x71 ;讀RTC當前時間(秒) push ax mov al,2 or al,0x80 out 0x70,al in al,0x71 ;讀RTC當前時間(分) push ax mov al,4 or al,0x80 out 0x70,al in al,0x71 ;讀RTC當前時間(時) push ax
(6)顯示效果如下:
(7)完整代碼:
- 加載扇區
app_lba_start equ 100 ;聲明常數(用戶程序起始邏輯扇區號) ;常數的聲明不會占用匯編地址 SECTION mbr align=16 vstart=0x7c00 ;設置堆棧段和棧指針 mov ax,0 mov ss,ax mov sp,ax mov ax,[cs:phy_base] ;計算用於加載用戶程序的邏輯段地址 mov dx,[cs:phy_base+0x02] mov bx,16 div bx mov ds,ax ;令DS和ES指向該段以進行操作 mov es,ax ;以下讀取程序的起始部分 xor di,di mov si,app_lba_start ;程序在硬盤上的起始邏輯扇區號 xor bx,bx ;加載到DS:0x0000處 call read_hard_disk_0 ;以下判斷整個程序有多大 mov dx,[2] ;曾經把dx寫成了ds,花了二十分鍾排錯 mov ax,[0] mov bx,512 ;512字節每扇區 div bx cmp dx,0 jnz @1 ;未除盡,因此結果比實際扇區數少1 dec ax ;已經讀了一個扇區,扇區總數減1 @1: cmp ax,0 ;考慮實際長度小於等於512個字節的情況 jz direct ;讀取剩余的扇區 push ds ;以下要用到並改變DS寄存器 mov cx,ax ;循環次數(剩余扇區數) @2: mov ax,ds add ax,0x20 ;得到下一個以512字節為邊界的段地址 mov ds,ax xor bx,bx ;每次讀時,偏移地址始終為0x0000 inc si ;下一個邏輯扇區 call read_hard_disk_0 loop @2 ;循環讀,直到讀完整個功能程序 pop ds ;恢復數據段基址到用戶程序頭部段 ;計算入口點代碼段基址 direct: mov dx,[0x08] mov ax,[0x06] call calc_segment_base mov [0x06],ax ;回填修正后的入口點代碼段基址 ;開始處理段重定位表 mov cx,[0x0a] ;需要重定位的項目數量 mov bx,0x0c ;重定位表首地址 realloc: mov dx,[bx+0x02] ;32位地址的高16位 mov ax,[bx] call calc_segment_base mov [bx],ax ;回填段的基址 add bx,4 ;下一個重定位項(每項占4個字節) loop realloc jmp far [0x04] ;轉移到用戶程序 ;------------------------------------------------------------------------------- read_hard_disk_0: ;從硬盤讀取一個邏輯扇區 ;輸入:DI:SI=起始邏輯扇區號 ; DS:BX=目標緩沖區地址 push ax push bx push cx push dx mov dx,0x1f2 mov al,1 out dx,al ;讀取的扇區數 inc dx ;0x1f3 mov ax,si out dx,al ;LBA地址7~0 inc dx ;0x1f4 mov al,ah out dx,al ;LBA地址15~8 inc dx ;0x1f5 mov ax,di out dx,al ;LBA地址23~16 inc dx ;0x1f6 mov al,0xe0 ;LBA28模式,主盤 or al,ah ;LBA地址27~24 out dx,al inc dx ;0x1f7 mov al,0x20 ;讀命令 out dx,al .waits: in al,dx and al,0x88 cmp al,0x08 jnz .waits ;不忙,且硬盤已准備好數據傳輸 mov cx,256 ;總共要讀取的字數 mov dx,0x1f0 .readw: in ax,dx mov [bx],ax add bx,2 loop .readw pop dx pop cx pop bx pop ax ret ;------------------------------------------------------------------------------- calc_segment_base: ;計算16位段地址 ;輸入:DX:AX=32位物理地址 ;返回:AX=16位段基地址 push dx add ax,[cs:phy_base] adc dx,[cs:phy_base+0x02] shr ax,4 ror dx,4 and dx,0xf000 or ax,dx pop dx ret ;------------------------------------------------------------------------------- phy_base dd 0x10000 ;用戶程序被加載的物理起始地址 times 510-($-$$) db 0 db 0x55,0xaa
- 中斷處理代碼
SECTION header vstart=0 ;定義用戶程序頭部段 program_length dd program_end ;程序總長度[0x00] ;用戶程序入口點 code_entry dw start ;偏移地址[0x04] dd section.code.start ;段地址[0x06] realloc_tbl_len dw (header_end-realloc_begin)/4 ;段重定位表項個數[0x0a] realloc_begin: ;段重定位表 code_segment dd section.code.start ;[0x0c] data_segment dd section.data.start ;[0x14] stack_segment dd section.stack.start ;[0x1c] header_end: ;=============================================================================== SECTION code align=16 vstart=0 ;定義代碼段(16字節對齊) new_int_0x70: push ax push bx push cx push dx push es .w0: mov al,0x0a ;阻斷NMI。當然,通常是不必要的 or al,0x80 out 0x70,al in al,0x71 ;讀寄存器A test al,0x80 ;測試第7位UIP jnz .w0 ;以上代碼對於更新周期結束中斷來說 ;是不必要的 xor al,al or al,0x80 out 0x70,al in al,0x71 ;讀RTC當前時間(秒) push ax mov al,2 or al,0x80 out 0x70,al in al,0x71 ;讀RTC當前時間(分) push ax mov al,4 or al,0x80 out 0x70,al in al,0x71 ;讀RTC當前時間(時) push ax mov al,0x0c ;寄存器C的索引。且開放NMI out 0x70,al in al,0x71 ;讀一下RTC的寄存器C,否則只發生一次中斷 ;此處不考慮鬧鍾和周期性中斷的情況 mov ax,0xb800 mov es,ax ;es指向顯存段,從這里開始的字符串會被打印 pop ax ;RTC當前時間(時) call bcd_to_ascii mov bx,12*160 + 36*2 ;從屏幕上的12行36列開始顯示 mov [es:bx],ah mov [es:bx+2],al ;顯示兩位小時數字 mov al,':' mov [es:bx+4],al ;顯示分隔符':' not byte [es:bx+5] ;反轉顯示屬性 pop ax call bcd_to_ascii mov [es:bx+6],ah mov [es:bx+8],al ;顯示兩位分鍾數字 mov al,':' mov [es:bx+10],al ;顯示分隔符':' not byte [es:bx+11] ;反轉顯示屬性 pop ax call bcd_to_ascii mov [es:bx+12],ah mov [es:bx+14],al ;顯示兩位秒數字 mov al,0x20 ;中斷結束命令EOI out 0xa0,al ;向從片發送 out 0x20,al ;向主片發送 pop es pop dx pop cx pop bx pop ax iret ;------------------------------------------------------------------------------- bcd_to_ascii: ;BCD碼轉ASCII ;輸入:AL=bcd碼 ;輸出:AX=ascii mov ah,al ;分拆成兩個數字 and al,0x0f ;僅保留低4位 add al,0x30 ;轉換成ASCII shr ah,4 ;邏輯右移4位 and ah,0x0f add ah,0x30 ret ;------------------------------------------------------------------------------- start: mov ax,[stack_segment] mov ss,ax mov sp,ss_pointer mov ax,[data_segment] mov ds,ax mov bx,init_msg ;顯示初始信息 call put_string mov bx,inst_msg ;顯示安裝信息 call put_string mov al,0x70 mov bl,4 mul bl ;計算0x70號中斷在IVT中的偏移=0x70*0x4=0x1c0 mov bx,ax ;bx=0x1c0 cli ;防止改動期間發生新的0x70號中斷 push es mov ax,0x0000 ;IVT地址范圍:0x00000~0x003ff mov es,ax mov word [es:bx],new_int_0x70 ;自定義中斷處理例程的偏移地址 [0x1c0]=0x0000 mov word [es:bx+2],cs ;段地址[0x1c2]=0x1002 pop es mov al,0x0b ;RTC寄存器B or al,0x80 ;阻斷NMI out 0x70,al mov al,0x12 ;設置寄存器B,禁止周期性中斷,開放更 out 0x71,al ;新結束后中斷,BCD碼,24小時制 mov al,0x0c out 0x70,al in al,0x71 ;讀RTC寄存器C,復位未決的中斷狀態 in al,0xa1 ;讀8259從片的IMR寄存器 and al,0xfe ;清除bit 0(此位連接RTC),讓該位的輸入有效 out 0xa1,al ;寫回此寄存器 sti ;重新開放中斷 mov bx,done_msg ;顯示安裝完成信息 call put_string mov bx,tips_msg ;顯示提示信息 call put_string mov cx,0xb800 mov ds,cx mov byte [12*160 + 33*2],'@' ;屏幕第12行,35列 .idle: hlt ;使CPU進入低功耗狀態,直到用中斷喚醒 not byte [12*160 + 33*2+1] ;反轉顯示屬性 jmp .idle ;------------------------------------------------------------------------------- put_string: ;顯示串(0結尾)。 ;輸入:DS:BX=串地址 mov cl,[bx] or cl,cl ;cl=0 ? jz .exit ;是的,返回主程序 call put_char inc bx ;下一個字符 jmp put_string .exit: ret ;------------------------------------------------------------------------------- put_char: ;顯示一個字符 ;輸入:cl=字符ascii push ax push bx push cx push dx push ds push es ;以下用於獲取光標的位置,光標的位置用一個16位的數表示,屏幕的0行0列開始從0開始編號,光標寄存器在顯卡寄存器中的索引為0x000e(光標位置的高8位)和0x000f(光標位置的低8位) mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 in al,dx ;讀取光標所在位置的高8位 mov ah,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 in al,dx ;低8位 mov bx,ax ;BX=代表光標位置的16位數 cmp cl,0x0d ;回車符? jnz .put_0a ;不是。看看是不是換行等字符 mov ax,bx ; mov bl,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;換行符? jnz .put_other ;不是,那就正常顯示字符 add bx,80 jmp .roll_screen .put_other: ;正常顯示字符 mov ax,0xb800 mov es,ax shl bx,1 mov [es:bx],cl ;以下將光標位置推進一個字符 shr bx,1 add bx,1 .roll_screen: cmp bx,2000 ;光標超出屏幕?滾屏 jl .set_cursor mov ax,0xb800 mov ds,ax mov es,ax cld mov si,0xa0 mov di,0x00 mov cx,1920 rep movsw mov bx,3840 ;清除屏幕最底一行 mov cx,80 .cls: mov word[es:bx],0x0720 add bx,2 loop .cls mov bx,1920 .set_cursor: mov dx,0x3d4 mov al,0x0e out dx,al mov dx,0x3d5 mov al,bh out dx,al mov dx,0x3d4 mov al,0x0f out dx,al mov dx,0x3d5 mov al,bl out dx,al pop es pop ds pop dx pop cx pop bx pop ax ret ;=============================================================================== SECTION data align=16 vstart=0 init_msg db 'Starting...',0x0d,0x0a,0 inst_msg db 'Installing a new interrupt 70H...',0 done_msg db 'Done.',0x0d,0x0a,0 tips_msg db 'Clock is now working.',0 ;=============================================================================== SECTION stack align=16 vstart=0 resb 256 ss_pointer: ;=============================================================================== SECTION program_trail program_end:
--------------------------------------------------------------------------------------分割線---------------------------------------------------------------------------------------------------------
以上是正常的demo,做實驗的過程中突然想到兩個問題:
(1)進入0x70例程中斷后,如果關閉中斷,同時死循環,在單核下是不是會卡死?多核下會不會一直占着這個核不放手了?如果是,那病毒、木馬豈不是都殺不死了?想想都有點小激動了,繼續查看中斷例程的物理地址,如下:
靜態分析也很容易找到中斷例程起始位置:header最后一個字段從0x1c開始,是dd類型,占用4字節,下一個就應該從0x20開始了,剛好也能16字節對齊;整個程序被加載到0x10000處,所以中斷例程從0x10020處開始:
0x10020確實是自定義的中斷例程開始地址:
隨即在末尾幾個pop前添加如下代碼:
cli ;這里關閉中斷 .DeadLoop: jmp .DeadLoop ;這里死循環,單核下是不是就卡死?多核下是不是該進程殺不死?
然后下斷點,運行后發現並未斷下來,而是和之前一樣打印時間,並一直刷新,效果沒區別...............
原因不詳,猜測可能和bochs斷點原理有關,也可能和IVT的原理有關,歡迎各位的大佬們留言指導~~~~
(2)非屏蔽中斷NMI,雖說叫非屏蔽,但仍可通過設置0X70端口的最高位來屏蔽。一旦NMI被屏蔽,是不是沒法通過硬件的關機鍵關機了?PC機只能拔電源關機了?筆記本只能等電池電量耗盡才能關機了?
(3)中斷提供了用戶態進入內核態(也就是3環進0環)的機制,和這個類似的另一個非常重要的機制就是系統調用,執行syscall/sysenter,也可以從3環進入0環;也會根據系統調用號跳轉到不同的函數處理這類請求;