如上圖所示,Intel 32 位處理器有3種工做模式。編程
(1)實模式:工做方式至關於一個8086oop
(2)保護模式:提供支持多任務環境的工做方式,創建保護機制測試
(3)虛擬8086模式:這種方式可使用戶在保護模式下運行8086程序(好比cmd打開的console窗口,就是工做在虛擬8086模式)spa
有幾點須要特別說明:.net
(1)保護模式可分爲16位和32位的,由段描述符中的D標誌指明。對於32位代碼段和數據段,這個標誌老是設爲1;對於16位代碼和數據段,這個標誌被設置爲0.指針
D=1:默認使用32位地址和32位或8位的操做數。調試
D=0:默認使用16位地址和16位或8位的操做數。(主要是爲了可以在32位處理器上運行16位保護模式的程序)code
指令前綴0x66用來選擇非默認值得操做數大小,0x67用來選擇非默認值的地址大小。blog
(2)在實模式下,也可使用32位的寄存器,好比內存
mov eax,ecx mov ebx,0x12345678
(3)在書中,把實模式和16位的保護模式統稱爲「16位模式」;把32位保護模式稱爲「32位模式」。個人博文也沿用這種叫法。
(4)32位處理器能夠執行16位的程序,包括實模式和16位保護模式。
(5)當處理器在16位模式下運行時,可使用32位的寄存器,執行32位運算。
(6)在16位模式下,數據的大小是8位或者16位的;控制轉移和內存訪問時,偏移量也是16位的。
(7)32位保護模式兼容80286的16位保護模式。
(8)在16位模式下,處理器把全部指令都當作是16位的。
結合(5)和(8),咱們發現一個問題:當處理器運行16位模式下,既然把全部指令都當作16位的,那麼怎麼使用32位的寄存器,執行32位的運算呢?答案是利用指令前綴0x66和0x67.前面已經說過,指令前綴0x66用來選擇非默認值的操做數大小,0x67用來選擇非默認值的地址大小。
好比說,指令碼0x40在16位模式下對應的指令是
inc ax
若是加上前綴0x66,也就是指令碼66 40,當處理器在16位模式下運行,66 40對應的指令是
inc eax
同理,若是處理器運行在32位模式下,處理器認爲指令是32位的,若是加了0x66,那麼就表示指令的操做數是16位的。
在編寫程序的時候,咱們應該考慮指令的運行環境。爲了指令默認的運行環境,NASM提供了僞指令bits,用於指明其後的指令是被編譯成16位的仍是32位的。好比:
[bits 16] mov cx,dx ;89 D1 mov eax,ebx ;66 89 D8 [bits 32] mov cx,dx ;66 89 D1 mov eax,ebx ;89 D8
注意,[bits 16]和[bits 32]的方括號是能夠省略的。
因爲32位的處理器都擁有32位的寄存器和算術邏輯部件,並且同內存芯片之間的數據通路至少是32位的,所以,全部以寄存器或者內存單元爲操做數的指令都被擴充,以適應32位的算術邏輯操做。並且,這些擴展的操做即便是在16位模式下(實模式和16位保護模式)也是可用的。
我在博文 32位x86處理器編程導入——《x86彙編語言:從實模式到保護模式》讀書筆記08 中已經總結了通常指令的擴展,在這裏,我僅對PUSH指令進行實驗和總結。
實驗目的就是測試在3種模式下,PUSH指令的工做行爲(好比SP或ESP到底怎麼變化,壓入的數究竟是多少)。因此,我列了一個單子,把全部能想到的形式都列出來了,其中有的我也不肯定(或許這樣寫編譯都會報錯)。無論那麼多,先寫出來,而後讓編譯器篩選吧
1 ;測試各類push 2 3 ;操做數是當即數,分爲一字節、兩字節、四字節 4 push 0x80 5 push byte 0x80 6 7 push 0x8000 8 push word 0x8000 9 10 push 0x87654321 11 push dword 0x87654321 12 13 ;操做數是寄存器,分爲16位寄存器和32位寄存器 14 mov eax,0x86421357 15 push ax 16 push eax 17 18 ;操做數是內存單元,分爲一字節、兩字節、四字節 19 push [data] 20 push byte [data] 21 push word [data] 22 push dword [data]
是否是有的寫法明顯就不對呢?
首先,第20行,確定不對。由於若是是內存操做數的話,不能用byte修飾。剩下來的錯誤,我會在後文揭曉答案。
1 ;PUSH 指令實驗 2 3 jmp near start 4 5 data db 0x12,0x34,0x56,0x78 6 message db 'Hello,PUSH!' 7 8 start: 9 mov ax,0x7c0 ;設置數據段的段基地址 10 mov ds,ax 11 12 mov ax,0xb800 ;設置附加段基址到顯示緩衝區 13 mov es,ax 14 15 ;如下顯示字符串 16 mov si,message 17 mov di,0 18 mov cx,start-message 19 @g: 20 mov al,[si] 21 mov [es:di],al 22 inc di 23 mov byte [es:di],0x02 24 inc di 25 inc si 26 loop @g 27 28 ;測試各類push 29 push 0x80 30 push byte 0x80 31 32 push 0x8000 33 push word 0x8000 34 35 push 0x87654321 36 push dword 0x87654321 37 38 mov eax,0x86421357 39 push ax 40 push eax 41 42 ;push [data] 43 push word [data] 44 push dword [data] 45 46 push ds 47 push gs 48 49 jmp near $ 50 51 52 times 510-($-$$) db 0 53 db 0x55,0xaa
這段代碼不是用的配書代碼,是我本身寫的。
第5行,定義了4字節的數據,這是爲了後面驗證「push + 內存操做數」這一狀況。
第6行,定義了一個字符串,要把它顯示在屏幕上。這樣作是爲了調試方便,讓咱們知道咱們的程序已經RUN了。
第29行到47行,測試各類push,我會利用Bochs的調試功能,跟蹤每條Push的執行狀況,把結果總結出來。
好的,咱們開始編譯吧。
對於30行,有個警告:
push byte 0x80 ;warning: signed byte value exceeds bounds
對於35行,仍是一個警告:
push 0x87654321 ;warning: word data exceeds bounds
對於42行,呵呵,就是一個錯誤了。
push [data] ; error: operation size not specified
好吧,看來這樣不指定操做數的大小是不行的,因此咱們把42行註釋掉。
而後再編譯,好的,能夠了。
調試的過程就是不斷用n命令,反覆用print-stack命令,還有reg命令等,仔細觀察棧的變化和SP的變化。(此處省略2000字)
小二,上實驗報告!
經過上面的實驗,咱們能夠知道,若是CPU運行在實模式,若是用NASM編譯,push指令能夠這麼用:
請參考個人博文 關於80286——《x86彙編語言:從實模式到保護模式》讀書筆記15
實驗代碼由配書代碼(代碼清單11-1 (文件名:c11_mbr.asm))修改而成。目的就是咱們要從實模式進入16位的保護模式,而後測試16位保護模式下PUSH指令的行爲。
1 ;test push (16位保護模式下) 2 3 ;設置堆棧段和棧指針 4 mov ax,cs 5 mov ss,ax 6 mov sp,0x7c00 7 8 ;計算GDT所在的邏輯段地址 9 mov ax,[cs:gdt_base+0x7c00] ;低16位 10 mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位 11 mov bx,16 12 div bx 13 mov ds,ax ;令DS指向該段以進行操做 14 mov bx,dx ;段內起始偏移地址 15 16 ;建立0#描述符,它是空描述符,這是處理器的要求 17 mov dword [bx+0x00],0x00 18 mov dword [bx+0x04],0x00 19 20 ;建立#1描述符,保護模式下的代碼段描述符 21 mov dword [bx+0x08],0x7c0001ff 22 mov dword [bx+0x0c],0x00009800 23 24 ;建立#2描述符,保護模式下的數據段描述符(文本模式下的顯示緩衝區) 25 mov dword [bx+0x10],0x8000ffff 26 mov dword [bx+0x14],0x0000920b 27 28 ;建立#3描述符,保護模式下的堆棧段描述符 29 mov dword [bx+0x18],0x00007a00 30 mov dword [bx+0x1c],0x00009600 31 32 ;初始化描述符表寄存器GDTR 33 mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(總字節數減一) 34 35 lgdt [cs: gdt_size+0x7c00] 36 37 in al,0x92 ;南橋芯片內的端口 38 or al,0000_0010B 39 out 0x92,al ;打開A20 40 41 cli ;保護模式下中斷機制還沒有創建,應 42 ;禁止中斷 43 mov eax,cr0 44 or eax,1 45 mov cr0,eax ;設置PE位 46 47 ;如下進入保護模式... ... 48 jmp 0x0008:flush ;描述符選擇子:16位偏移 49 ;清流水線並串行化處理器 50 51 52 flush: 53 mov cx,00000000000_10_000B ;加載數據段選擇子(0x10) 54 mov ds,cx 55 56 ;如下在屏幕上顯示"ABCDEFGHIJK" 57 mov byte [0x00],'A' 58 mov byte [0x02],'B' 59 mov byte [0x04],'C' 60 mov byte [0x06],'D' 61 mov byte [0x08],'E' 62 mov byte [0x0a],'F' 63 mov byte [0x0c],'G' 64 mov byte [0x0e],'H' 65 mov byte [0x10],'I' 66 mov byte [0x12],'J' 67 mov byte [0x14],'K' 68 69 70 ;測試push 71 mov cx,00000000000_11_000B ;加載堆棧段選擇子 72 mov ss,cx 73 mov sp,0x7c00 74 75 76 push 0x80 77 push byte 0x80 ; warning: signed byte value exceeds bounds 78 79 push 0x8000 80 push word 0x8000 81 82 push 0x87654321 83 ;warning: word data exceeds bounds 84 push dword 0x87654321 85 86 87 mov eax,0x86421357 88 push ax 89 push eax 90 91 ;push [0x00]error: operation size not specified 92 push byte [0x00] 93 push word [0x00] 94 95 push dword [0x00] 96 97 push ds 98 push gs 99 push es 100 push cs 101 102 ghalt: 103 hlt ;已經禁止中斷,將不會被喚醒 104 105;------------------------------------------------------------------------------- 106 107 gdt_size dw 0 108 gdt_base dd 0x00007e00 ;GDT的物理地址 109 110 times 510-($-$$) db 0 111 db 0x55,0xaa
對比32位保護模式的代碼,就會發現16位保護模式的代碼略有不一樣。
首先,好比說22行,段描述符的定義是
22 mov dword [bx+0x0c],0x00009800
由於80286中,段描述符的格式是
因此,高4字節的16~32位所有爲0.
其次,
47 ;如下進入保護模式... ... 48 jmp 0x0008:flush ;描述符選擇子:16位偏移 49 ;清流水線並串行化處理器
這裏,沒有加僞指令[bits 32],並且,偏移flush沒有用dword修飾。由於操做數和偏移是16位的。
好了,代碼就說到這裏,咱們看實驗報告吧。
經過和實模式的對比,能夠發現,除了九、10兩行中的指令碼的偏移不同(這和數據存放的位置有關係,和PUSH沒有關係),PUSH指令的行爲是驚人的相同。因此咱們能夠得出結論,16位保護模式下,PUSH的用法和實模式是同樣的。我想,這也是在原書中,做者把實模式和16位的保護模式統稱爲「16位模式」,把32位保護模式稱爲「32位模式」的緣由吧。
實驗代碼由配書代碼(代碼清單11-1 (文件名:c11_mbr.asm))修改而成。
1 ;test push (32位保護模式) 2 3 ;設置堆棧段和棧指針 4 mov ax,cs 5 mov ss,ax 6 mov sp,0x7c00 7 8 ;計算GDT所在的邏輯段地址 9 mov ax,[cs:gdt_base+0x7c00] ;低16位 10 mov dx,[cs:gdt_base+0x7c00+0x02] ;高16位 11 mov bx,16 12 div bx 13 mov ds,ax ;令DS指向該段以進行操做 14 mov bx,dx ;段內起始偏移地址 15 16 ;建立0#描述符,它是空描述符,這是處理器的要求 17 mov dword [bx+0x00],0x00 18 mov dword [bx+0x04],0x00 19 20 ;建立#1描述符,保護模式下的代碼段描述符 21 mov dword [bx+0x08],0x7c0001ff 22 mov dword [bx+0x0c],0x00409800 23 24 ;建立#2描述符,保護模式下的數據段描述符(文本模式下的顯示緩衝區) 25 mov dword [bx+0x10],0x8000ffff 26 mov dword [bx+0x14],0x0040920b 27 28 ;建立#3描述符,保護模式下的堆棧段描述符 29 mov dword [bx+0x18],0x00007a00 30 mov dword [bx+0x1c],0x00409600 31 32 ;初始化描述符表寄存器GDTR 33 mov word [cs: gdt_size+0x7c00],31 ;描述符表的界限(總字節數減一) 34 35 lgdt [cs: gdt_size+0x7c00] 36 37 in al,0x92 ;南橋芯片內的端口 38 or al,0000_0010B 39 out 0x92,al ;打開A20 40 41 cli ;保護模式下中斷機制還沒有創建,應 42 ;禁止中斷 43 mov eax,cr0 44 or eax,1 45 mov cr0,eax ;設置PE位 46 47 ;如下進入保護模式... ... 48 jmp dword 0x0008:flush ;16位的描述符選擇子:32位偏移 49 ;清流水線並串行化處理器 50 [bits 32] 51 52 flush: 53 mov cx,00000000000_10_000B ;加載數據段選擇子(0x10) 54 mov ds,cx 55 56 ;如下在屏幕上顯示"ABCDEFGHIJK" 57 mov byte [0x00],'A' 58 mov byte [0x02],'B' 59 mov byte [0x04],'C' 60 mov byte [0x06],'D' 61 mov byte [0x08],'E' 62 mov byte [0x0a],'F' 63 mov byte [0x0c],'G' 64 mov byte [0x0e],'H' 65 mov byte [0x10],'I' 66 mov byte [0x12],'J' 67 mov byte [0x14],'K' 68 69 70 ;測試push 71 mov cx,00000000000_11_000B ;加載堆棧段選擇子 72 mov ss,cx 73 mov esp,0x7c00 74 75 76 push 0x80 77 push byte 0x80 ;warning: signed byte value exceeds bounds 78 79 push 0x8000 80 push word 0x8000 81 82 push 0x87654321 83 push dword 0x87654321 84 85 mov eax,0x86421357 86 push ax 87 push eax 88 89 90 push word [0x00] 91 push dword [0x00] 92 93 push ds 94 push gs 95 push es 96 push cs 97 98 ghalt: 99 hlt ;已經禁止中斷,將不會被喚醒 100 101;------------------------------------------------------------------------------- 102 103 gdt_size dw 0 104 gdt_base dd 0x00007e00 ;GDT的物理地址 105 106 times 510-($-$$) db 0 107 db 0x55,0xaa
若是對上面的代碼不熟悉的話,能夠參考個人博文 進入保護模式(一)——《x86彙編語言:從實模式到保護模式》讀書筆記12 等文章。
根據測試報告,咱們能夠概括出32位保護模式下,針對NASM編譯器的push指令用法:
(完)