1. 程式人生 > >3.4 復雜的x86指令舉例

3.4 復雜的x86指令舉例

完全 32位 進制 條件 依次 指定位置 內存 立即數 二次

計算機組成

3 指令系統體系結構

3.4 復雜的x86指令舉例

技術分享圖片

x86作為復雜指令系統的代表,自然會有不少相當復雜的指令。在這一節我們將會看到其中有代表性的一些例子。

技術分享圖片

關於復雜的x86指令,我們這裏舉四個例子。第一個是串操作指令。

技術分享圖片

串操作指令是將存儲器中的數據串進行每次一個元素的操作。所謂一個元素可以是字節或者是字。這個串可以很長,能夠達到64K個Byte。x86提供了5種不同的串操作指令,並且還有3種重復前綴,可以與串操作指令配合使用。

技術分享圖片

這張表就展示了這5種串操作指令和3種重復前綴。

技術分享圖片

我們來選擇其中一組進行介紹。這就是字節串傳送指令,這個指令的格式非常簡單,沒有任何的操作數。它的功能就是在存儲器中將指定位置的一個字節單元傳送到存儲器的另一個指定的位置。與它配合的經常是這個重復前綴REP,x86的體系結構中有很多種的前綴,這個前綴的涵義是,當CX寄存器的值不等於0時,就重復執行這個串操作指令。那麽很奇怪的是這個指令沒有任何操作數。其實大家要註意x86當中有很多這樣的沒有操作數的指令,但這並不意味著它們比那些有操作數的指令要簡單。因為它們不寫操作數,不是因為沒有操作數,很可能是因為操作數太多了,實在在指令中寫不下。因此它們實際上是有一些隱含的操作數。

技術分享圖片

對於這條串傳送指令,它要傳送的數據串稱為源串。源串的地址默認放在 DS:SI 這組寄存器指向的位置。而要傳送的目的,我們稱為目的串地址,默認放在 ES:DI 這組寄存器指向的位置。而要傳送的串的長度則放在 CX 寄存器當中。

我們可以看到,雖然沒有寫操作數,但是它實際有5個寄存器作為它的操作數。不僅它有隱含的操作數,還有一些隱含的操作。除了進行串的傳送之外,在完成這個操作之後,硬件上還會自動完成這些操作:

第一修改 SI 和 DI 寄存器,以指向下一個串元素。然後再判斷是否使用了重復前綴,如果是,則將 CX 寄存器的內容減 1。需要註意的是這些操作都是硬件自動完成的,不需要程序員在軟件中特別指定。

技術分享圖片

我們來看一個例子。假設我們在存儲器中要進行一次數據串的傳送。源串的位置在12040這個地址開始,一共三個字節,我們希望傳送到12060開始的地方。那我們編寫的程序是這樣的,假設事先已配置好了數據段寄存器DS為1000。

這個程序的前兩條指令實際是將數據段寄存器的內容傳送到附加段寄存器當中,只不過段寄存器之間不能直接傳送,所以借用了AX。

然後在 SI 寄存器當中保存源串的偏移地址, 在DI寄存器當中放入目的串的偏移地址,這樣DS和SI這組寄存器就指向了源串。而ES和DI這組寄存器就指向了目的串。

下一條指令CLD,這是確定傳送的方向,一會兒再進行解釋。

然後在CX寄存器當中存入3。

然後才是 REP MOVSB 這條串傳送指令。前面加上了重復前綴,這樣的配置就相當於連續執行了三次這條串傳送指令。

當執行第一次傳送之後,第一個字節被傳送到了目的串的位置,傳送完成後,SI和DI自動被增加,CX 自動被減1。這些操作都是由CPU完成的。

同時我還要說明,所謂的傳送這個字節實際上是被CPU發起的向12040地址的讀操作,讀入到CPU中,再發起一次向12060地址的存儲器寫操作,寫入到對應的字節單元。在第二次傳送後,SI和DI又被加1,CX又被減1。第三次傳送完之後,雖然SI和DI繼續加1,但CX已經減為0,所以不再繼續執行。

技術分享圖片

還需要說明一點的是串傳送的方向也是可以設置的。如果設置DF=0,則是從源串的低地址開始傳送,在傳送過程中,SI和DI是自動增量的修改。如果設置DF=1,則是從源串的高地址開始傳送,傳送過程中,SI和DI自動減量的修改。

這個表格就說明了SI和DI的修改方法。那如何修改DF標誌位呢?其實x86提供了兩條控制指令,對標誌位進行操作。STD就是把DF標誌置1。CLD就是我們剛才的例子中的那條指令,是把DF清0。這就可以確定串傳送的方向。設置這樣的方向實際上是為了應對源串和目的串有可能重疊的問題。

技術分享圖片

我們簡單來看一個釋意。如果源串和目的串在內存中是互相不重疊的,那這時候設置DF為0,或者為1,都沒有關系。

但是如果你的源串和目的串有一個重疊,那必須設置DF為1,從高地址依次向低地址開始傳送,不然圖中綠色的重疊部分,就會在傳送的一開始被覆蓋,從而導致結果的錯誤。

那如果源串和目的串是靠右這張圖的重疊的形式,則必須設置DF為0。從低地址開始傳送,原因也是一樣的。

除了串傳送指令,還有其他類型的串操作。例如在一個數據串種,查找特定的數據,或者比較兩個數據串是否相同。這樣程序員有了很便利的手段,對一大塊數據進行操作。因此串操作指令是功能非常強大的指令,不過由於數據串當中的元素數量有可能很多,因此串操作指令的執行時間也可能很長,這是需要註意的。

技術分享圖片

第二個我們來看循環控制指令。

技術分享圖片

循環控制指令主要有這幾類。我們也選其中的一個來進行介紹。

技術分享圖片

這就是LOOPNE或者是LOOPNZ指令。這兩個指令的寫法不同,但其實它們的含義和指令的編碼其實是一樣的。它的操作就是每次將CX寄存器的內容減1,並且判斷CX是否為零,如果CX不等於0,而且標誌位ZF等於0,則轉移到指定的目標地址處繼續執行。否則,結束當前的循環,順序執行下一條指令。我們也來看一個例子。

技術分享圖片

如果我們要在一百個字符的字符串中,尋找第一個 $ 字符,我們可以這樣寫,先向CX寄存器中存入100。在這個循環體內部,最重要的是將SI所指向的內存字節與$ 字符進行比較,如果比較結果為相等,則標誌寄存器當中的Z位會被置為有效,然後 LOOPNZ 指令會進行判斷,如果Z位無效,則轉移到NEXT標號處繼續執行,也就是繼續進行循環,如果不是則退出循環。同時它還會檢查CX的內容,所以這個循環要麽執行完100次,要麽在循環的過程中就發現有比較相等的情況,從而退出循環。當然在循環的出口,還需要進行一些分析,以判斷是否找到以及在什麽位置找到的。其實我們也可以用更加簡單的條件轉移指令來完成循環語句的書寫,我們可以設想一下如何用條件轉移指令,例如 JNZ 來改寫這段程序,但是有了這樣的循環控制指令,就會給編程帶來很大的便利。這也是x86指令系統的一個很大的特點,就是雖然可以用其它指令的組合來進行替代,但是x86中寧願提供新的指令,從而更加方便的完成這個功能。

技術分享圖片

然後我們來看一個查表的指令。

技術分享圖片

查表指令 XLAT,它也是一個沒有操作數的指令,我們現在看到沒有操作數的指令,都應該保持警惕,這個指令其實相當的復雜。它首先需要在數據段中定義一個字節型的數據表,然後再執行這條指令時的操作是這樣的,它會從BX寄存器中取得數據表的起始地址的偏移量,然後從AL寄存器中取得數據表的索引值,然後根據這兩個值從數據表中查得表項的內容,並將查得的表項內容存入AL中。所以它的隱含操作數是BX、AL,而且還需要提前定義數據表,並且它還會修改AL的內容。我們也來看一個例子。

技術分享圖片

這是一段匯編語言程序。首先定義一個字節型的數據表,我們也可以簡單的把它理解為一個數組。這條指令是將這個數據表起始地址的偏移量放到BX寄存器中。然後在AL中存入4,再執行XLAT的指令,這時候會發生什麽呢?按照剛才的定義,XLAT指令會根據BX找到這個數據表,然後根據AL的內容找到這個數據表中對應的那個元素,並把這個元素的內容放到AL寄存器當中。因此,在執行完這條MOV指令之後,AL的內容應該是4,而執行完XLAT指令之後,它就成為了這個數據表中第4個元素,也就是66。

那如果在AL中又存入了6,再一次執行XLAT指令,這時AL當中的數應該是多少呢?實際執行完以後,應該是7D,就是這個數據表的第6個元素。

技術分享圖片

最後我們來看一看十進制的調整指令。

技術分享圖片

十進制的調整指令主要有這幾個。我們也來看其中的一個例子。

技術分享圖片

DAA指令,被稱為加法十進制調整指令。它的格式也是沒有操作數。它的操作有這樣的要求,首先要跟在二進制加法指令之後,它是將AL中的“和”,也就是剛才這條加法指令運算的結果,調整為壓縮BCD數的格式,並將調整的結果再送回到AL當中。

技術分享圖片

BCD數,就是指用二進制編碼的十進制數。二進制編碼,可以用計算機進行保存,而十進制則便於人的識別。所以BCD數的設計目的,就是為人與計算機的聯系提供一個便利的中間表示。這個便利在哪裏呢?我們看一個例子,例如一個十進制數42。如果用二進制來表示,那就是0010 1010,即使寫成16進制的形式2A,看起來也和42差別很大。如果人想用計算機保存這樣的數,又想很直觀的能看到十進制這樣的顯示效果,那就可以采用BCD數的形式。在這個BCD數中,我們用四個比特來代表一個十進制數。對於42,高四個比特(bit)記錄了4,低四個比特記錄了2,這樣這個字節就可以看作記錄了一個十進制數42。那這個指令怎麽用呢?我們再看一個例子。

這段程序的第一條指令把27,註意是16進制的27H,放在AL寄存器當中,這個數相當於十進制的39,然後將AL的數與15H這個數相加。這條指令運算完後,AL中應該保存的是3CH,這個數實際上是十進制的60。然後我們再運行DAA這條指令,運行完之後,AL當中的數就變成了42H。那這個有什麽用呢?

十六進制 二進制 十進制
27 0010 0111 39
15 0001 0101 21
3C 0011 1100 60

實際上是這樣的,如果我們希望進行BCD數的運算,也就是我們想做27+15這個操作,如果按照正常的計算機編程的思路,我們應該將十進制的27和15都先轉換成二進制,然後再用這兩條指令進行運算,運算結束後,再將運算結果由二進制轉化為十進制,從而得到了結果42。但是如果想非常簡便的進行十進制的運算,而且這些數的範圍也不是很大,要麽直接就用這樣的表示形式,直接用27H代表27,15H代表15,然後我們期望運算的結果是27+15的結果,也就是42。但是這條加法指令並不會領會到這一點,它加完的結果仍然是3CH,所以我們額外增加一條DAA指令,這條指令就是按我們剛才說的十進制相加的思路,把這個結果進行轉換,所以它就會得到了42H。這樣的指令有什麽用呢?實際上在一些非常簡單的設備上,它需要進行很簡單的算術運算,又不想要有很多的轉換,就可以采用這樣的方式。當然現在真正這樣用的已經越來越少了。

技術分享圖片

最後我們從一個有趣的例子來看一看x86指令的復雜程度。這張圖是x86指令的通用格式,每一個小格都是指令格式中特定的位域。那我們可以人為的寫出一條指令來,這條指令是一個加法,而且有一個前綴LOCK,這和我們剛才學到的REP一樣,都是指令的前綴。這個加法其中一個源操作數是32位的立即數。另一個源操作數以及目的操作數是內存當中的一個32位的存儲單元,這個存儲單元本應默認在數據段,但這裏強制指定為在附加段,這個存儲單元的地址由EAX寄存器,ECX寄存器和一個立即數計算而得。要計算這個內存地址,我們看到需要一次乘法,兩次加法得到偏移地址,再和段基址進行移位並相加的操作,然後訪問這個存儲單元得到32位數,再與12345678這個立即數相加。然後再訪問這個存儲單元,將這個數存進去。這條指令的編碼一共有15個字節,可以認為是一條最長的x86指令。x86指令的復雜程度,由此可見一斑。

技術分享圖片

編程人員只用給出一條簡短的指令,計算機就可以完成非常復雜的工作,這自然是一件很好的事情,計算機似乎就應該這麽設計。可惜世界沒有這麽簡單, 有人提出了完全相反的做法,我們下一節再說。

3.4 復雜的x86指令舉例