Like Share Discussion Bookmark Smile

J.J. Huang   2019-07-01   x86組合語言   瀏覽次數:

x86組合語言 - 第一章 | 組合語言觀念

程式、技術越來越多,但是我在學習的過程中,發現會一直回去研究、探討、學習基礎的知識。因為前面的CE教學,發現組合語言在後面會需要一些基礎,於是乎上網找了些文章,發現很多啊⋯。

下面這些內容是2010年的資料了,我覺得內容不錯還是有參考價值;基本上是部分轉載,只是希望自己有概念,建議閱讀過稍微了解即可。

機器語言 與 80x86

大家家裡用的計算機器叫做個人電腦 (PC),可以拿來安裝 Windows、Linux,甚至 Mac OS X…等作業系統。

個人電腦的 CPU 演變歷史,可以說就是 Intel 的歷史。從最早的16位元CPU:「8088/8086 -> 80286」,再演化到32位元的 80386、80486…後來因為商標不能用數字註冊,Intel 不使用 80586 命名,從586開始,改名為歷史上的 Pentium CPU。

AMD 也差不多是在 Pentium 時代開始慢慢成為 Intel 在個人電腦處理器上的競爭者。

可以想見 Intel 就是個人電腦處理器的「唯一制定者」,Intel自己做新的 CPU也要向後相容以前的東西,就像 Windows 7 也得要能執行 Windows XP 的程式一般。你在 286 寫的程式,拿去給 486 的 CPU 也要能跑。

所以「個人電腦 CPU = x86 家族」…好啦,可能有人不認同這句話。

你跟美國人講話就要講英文、跟法國人講法文;跟 x86 家族的處理器講話,就要講「x86 機器語言」;跟 Intel 8051 單晶片處理器溝通,就講「8051 機器語言」;在算盤本裡面介紹的處理器是「MIPS」家族,就用「MIPS 機器語言」跟其溝通。

所有處理器裡面,x86家族功能當然是最強,每一代都有增加新功能,又要向後相容,所以其實該語言最複雜、不規則、不好學。但只用些基本的功能的話,還是過得去的。

組合語言—Intel Style 與 AT&T Style、MASM 與 NASM

機器語言因為電路關係,原始形式就是 010101 這種二進位形式,但你喜歡也可以轉成十六進位寫出來給別人看。

下面這是一個 x86 機器語言指令 (instruction):

1
05 0A 00 00 00 (十六進位表示)

用人類說法就是你告訴某顆 x86 家族的 CPU:「把你的 eax 暫存器內容取出,將其跟10相加,再把結果寫回 eax 暫存器」

用C語言表示法就是:

1
eax += 10;

機器語言形式顯然太麻煩了。

助憶符號

於是發明了「助憶符號」,比如用 add 代表「相加這個運算動作」;減法動作,用符號 sub 標記;將資料從A處複製過去B處的動作,就用 mov 助憶符號標記。

add, sub, mov…等是運算子,而 eax 暫存器跟 10 是運算參與單元 (運算元)。

如果綜合以上講的運算子跟運算元,想要寫出完整指令時,還會有一個問題!
若有 eax, ecx 兩個運算元,想要把 eax 的值取出,複製到 ecx 去

到底該寫 mov eax, ecx 還是 mov ecx, eax ?
哪邊來源?哪邊目的?

AT&T、Intel 各自有一套語法慣例

C語言 Intel AT&T
指派運算子的左邊是目的地
int eax = 4;
靠最左邊的運算元是目的地。
mov eax, 4
靠最右邊的運算元是運算結果放置處。
movl $4, %eax
(暫存器名稱前,需加 % 符號;而且4這個立即數值前,需加 $ 符號;且用 movl 表示 move long 這麼長)

西瓜靠大邊,跟大家一起用 Intel 慣例的寫法就好。

像上面 mov eax, 4 這樣子的指令形式,都叫「組合語言」,說穿了只是把當初的「x86 機器語言」寫成比較容易看懂的形式而已。

記憶體模型

如果需要更詳細的了解可以參考x86

  • 16位元記憶體模型—Segment:Offset (分段記憶體模型)
  • 32位元記憶體模型—Flat Memory Model 加 Paging

個人電腦 x86 家族的 CPU,在 16 位元時代是 8088/8086/80286 這三位;而 x86 家族第一個 32 位元始祖是劃時代的 80386。

8088跟8086的位址匯流排都有20條線,每條線都是一端連接記憶體,一端連接處理器,在高低電位變化下 (0、1),總共可有 2^20 種控制變化。
換言之,依照每個記憶體位址對應一個位元組的慣例,可以定位 2^20 大小的記憶體位址;80286 則進化到 24 條位址線,定址能力達 2^24,即 16M 記憶體。省麻煩,把它當成跟 8088/8086 一樣,只能定址到 1M 記憶體就好。

在 x86 的術語中,記憶體位址可以分成三種:

  • 邏輯位址
  • 線性位址
  • 真實位址(物理位址)

必須先「邏輯位址→線性位址」,然後接著才是「線性位址→真實位址」。

自從32位元 CPU 出現 (自 80386 後),記憶體 Model 變成 Flat Memory,邏輯位址就已經等於線性位址了。

然後是因為有「分頁機制」武力介入,所以需要先透過分頁機制轉換,線性位址才會變成真正的物理位址。而分頁機制是從 80386 開始使用 (保護模式的完整版也是從 80386 開始)。

那為什麼 16 位元處理器,不使用 Flat Memory Model?為什麼當初的邏輯位址要先經過轉換才會變成線性位址?

因為16位元CPU內部,參與運算的暫存器當時都還停留在16位元(如:AX, BX, CX, DX),甚至最重要的指令暫存器 (IP) 也是 16 位元,故只有定位到 0~65535 也就是 64K 記憶體的能力。

Intel 用額外提供的四個「分段暫存器」(CS、DS、ES、SS),搭配其他暫存器後,使得每次記憶體定址方式其實是 Segment:Offset,此時這種位址表達法叫邏輯位址。

CS 是 Code Segment、DS 是 Data Segment、SS 是 Stack Segment …

邏輯位址→線性位址,公式是:「Segment Register * 0x10 + Offset」

假設我們有個程式,裡面的「全域變數」(不是放在堆疊的那種區域變數),有個很大的整數陣列,總共有 128K。當程式執行時,這些資料區段假設放在「線性位址=物理位址」的 0x0 ~ 0x1FFFF 這段連續的記憶體空間裡。

在邏輯位址(以寫程式的角度去觀看的位址),這 128K 資料會被分成兩段,第一段是 DS=0 且 offset = 0x0000~0xFFFF,第二段是 DS=1 且 offset = 0x0000~0xFFFF。

換言之,如果我要把某陣列元素移到 ax 暫存器,指令可能長這樣

1
mov ax, word ptr[0x1234]

如果執行這行時 DS=0,則會取到物理記憶體位址 0x01234 處 (第一段);
如果執行這行時 DS=1,則會取道物理記憶體位址 1*0x10 + 0x1234 = 0x11234。

若要讀取最後一個位元組到ax,只要執行以下指令即可:

1
2
mov ds, 1
mov al, byte ptr[0xFFFF]

因為不知道當時的 DS 值為多少,所以保險點,先設定 DS,然後因為 ax 是16 位元,不必用到這麼大。所以用 al 存放即可。 al 就是 ax 暫存器低 8 位元別名。
(ax 在 32 位元以上的 CPU 時,其實也是 eax 暫存器的低16位元處別名)

現今的執行檔,比如 PE 執行檔,往往內部都有分 .text (.code)、.data,可能就是承襲當初的記憶體分段機制?


註:以上參考了
[心得] 個人的 x86 組合語言觀念筆記