匯編語言如何發展到C語言的?
我給你說說二者的區別,再去看看演變歷史!
一、匯編語言是什么?
我們知道,CPU 只負責計算,本身不具備智能。你輸入一條指令(instruction),它就運行一次,然后停下來,等待下一條指令。這些指令都是二進制的,稱為操作碼(opcode),比如加法指令就是00000011。編譯器的作用,就是將高級語言寫好的程序,翻譯成一條條操作碼。對于人類來說,二進制程序是不可讀的,根本看不出來機器干了什么。為了解決可讀性的問題,以及偶爾的編輯需求,就誕生了匯編語言。匯編語言是二進制指令的文本形式,與指令是一一對應的關系。比如,加法指令00000011寫成匯編語言就是ADD。只要還原成二進制,匯編語言就可以被 CPU 直接執行,所以它是最底層的低級語言。二、來歷
最早的時候,編寫程序就是手寫二進制指令,然后通過各種開關輸入計算機,比如要做加法了,就按一下加法開關。后來,發明了紙帶打孔機,通過在紙帶上打孔,將二進制指令自動輸入計算機。為了解決二進制指令的可讀性問題,工程師將那些指令寫成了八進制。二進制轉八進制是輕而易舉的,但是八進制的可讀性也不行。很自然地,最后還是用文字表達,加法指令寫成ADD。內存地址也不再直接引用,而是用標簽表示。 這樣的話,就多出一個步驟,要把這些文字指令翻譯成二進制,這個步驟就稱為assembling,完成這個步驟的程序就叫做 assembler。它處理的文本,自然就叫做 aseembly code。標準化以后,稱為assembly language,縮寫為 asm,中文譯為匯編語言。 每一種 CPU的機器指令都是不一樣的,因此對應的匯編語言也不一樣。本文介紹的是目前最常見的 x86 匯編語言,即 Intel 公司的 CPU使用的那一種。三、寄存器
學習匯編語言,首先必須了解兩個知識點:寄存器和內存模型。
先來看寄存器。CPU 本身只負責運算,不負責儲存數據。數據一般都儲存在內存之中,CPU 要用的時候就去內存讀寫數據。但是,CPU的運算速度遠高于內存的讀寫速度,為了避免被拖慢,CPU 都自帶一級緩存和二級緩存。基本上,CPU 緩存可以看作是讀寫速度較快的內存。但是,CPU 緩存還是不夠快,另外數據在緩存里面的地址是不固定的,CPU 每次讀寫都要尋址也會拖慢速度。因此,除了緩存之外,CPU還自帶了寄存器(register),用來儲存最常用的數據。也就是說,那些最頻繁讀寫的數據(比如循環變量),都會放在寄存器里面,CPU優先讀寫寄存器,再由寄存器跟內存交換數據。 寄存器不依靠地址區分數據,而依靠名稱。每一個寄存器都有自己的名稱,我們告訴 CPU去具體的哪一個寄存器拿數據,這樣的速度是最快的。有人比喻寄存器是 CPU 的零級緩存。 四、寄存器的種類 早期的 x86 CPU只有8個寄存器,而且每個都有不同的用途。現在的寄存器已經有100多個了,都變成通用寄存器,不特別指定用途了,但是早期寄存器的名字都被保存了下來。
● EAX● EBX● ECX● EDX● EDI● ESI● EBP● ESP
上面這8個寄存器之中,前面七個都是通用的。ESP 寄存器有特定用途,保存當前 Stack 的地址(詳見下一節)。 我們常常看到 32位CPU、64位 CPU 這樣的名稱,其實指的就是寄存器的大小。32 位 CPU 的寄存器大小就是4個字節。
五、內存模型:Heap
寄存器只能存放很少量的數據,大多數時候,CPU 要指揮寄存器,直接跟內存交換數據。所以,除了寄存器,還必須了解內存怎么儲存數據。程序運行的時候,操作系統會給它分配一段內存,用來儲存程序和運行產生的數據。這段內存有起始地址和結束地址,比如從0x1000到0x8000,起始地址是較小的那個地址,結束地址是較大的那個地址。程序運行過程中,對于動態的內存占用請求(比如新建對象,或者使用malloc命令),系統就會從預先分配好的那段內存之中,劃出一部分給用戶,具體規則是從起始地址開始劃分(實際上,起始地址會有一段靜態數據,這里忽略)。舉例來說,用戶要求得到10個字節內存,那么從起始地址0x1000開始給他分配,一直分配到地址0x100A,如果再要求得到22個字節,那么就分配到0x1020。這種因為用戶主動請求而劃分出來的內存區域,叫做 Heap(堆)。它由起始地址開始,從低位(地址)向高位(地址)增長。Heap的一個重要特點就是不會自動消失,必須手動釋放,或者由垃圾回收機制來回收。
六、內存模型:Stack
除了 Heap 以外,其他的內存占用叫做 Stack(棧)。簡單說,Stack 是由于函數運行而臨時占用的內存區域。請看下面的例子。
1234上面代碼中,系統開始執行main函數時,會為它在內存里面建立一個幀(frame),所有main的內部變量(比如a和b)都保存在這個幀里面。main函數執行結束后,該幀就會被回收,釋放所有的內部變量,不再占用空間。如果函數內部調用了其他函數,會發生什么情況?
12345上面代碼中,main函數內部調用了add_a_and_b函數。執行到這一行的時候,系統也會為add_a_and_b新建一個幀,用來儲存它的內部變量。也就是說,此時同時存在兩個幀:main和add_a_and_b。一般來說,調用棧有多少層,就有多少幀。 等到add_a_and_b運行結束,它的幀就會被回收,系統會回到函數main剛才中斷執行的地方,繼續往下執行。通過這種機制,就實現了函數的層層調用,并且每一層都能使用自己的本地變量。所有的幀都存放在 Stack,由于幀是一層層疊加的,所以 Stack 叫做棧。生成新的幀,叫做”入棧”,英文是push;棧的回收叫做”出棧”,英文是 pop。Stack的特點就是,最晚入棧的幀最早出棧(因為最內層的函數調用,最先結束運行),這就叫做”后進先出”的數據結構。每一次函數執行結束,就自動釋放一個幀,所有函數執行結束,整個Stack 就都釋放了。
Stack是由內存區域的結束地址開始,從高位(地址)向低位(地址)分配。比如,內存區域的結束地址是0x8000,第一幀假定是16字節,那么下一次分配的地址就會從0x7FF0開始;第二幀假定需要64字節,那么地址就會移動到0x7FB0。
七、CPU 指令
7.1 一個實例
了解寄存器和內存模型以后,就可以來看匯編語言到底是什么了。下面是一個簡單的程序example.c。
123456gcc 將這個程序轉成匯編語言。
1上面的命令執行以后,會生成一個文本文件example.s,里面就是匯編語言,包含了幾十行指令。這么說吧,一個高級語言的簡單操作,底層可能由幾個,甚至幾十個 CPU 指令構成。CPU 依次執行這些指令,完成這一步操作。example.s經過簡化以后,大概是下面的樣子。
12345678910111213可以看到,原程序的兩個函數add_a_and_b和main,對應兩個標簽_add_a_and_b和_main。每個標簽里面是該函數所轉成的CPU 運行流程。 每一行就是 CPU 執行的一次操作。它又分成兩部分,就以其中一行為例。
1這一行里面,push是 CPU 指令,%ebx是該指令要用到的運算子。一個 CPU 指令可以有零個到多個運算子。下面我就一行一行講解這個匯編程序,建議讀者最好把這個程序,在另一個窗口拷貝一份,省得閱讀的時候再把頁面滾動上來。
7.2 push 指令
根據約定,程序從_main標簽開始執行,這時會在 Stack 上為main建立一個幀,并將 Stack 所指向的地址,寫入 ESP寄存器。后面如果有數據要寫入main這個幀,就會寫在 ESP 寄存器所保存的地址。 然后,開始執行第一行代碼。
1push指令用于將運算子放入 Stack,這里就是將3寫入main這個幀。 雖然看上去很簡單,push指令其實有一個前置操作。它會先取出ESP 寄存器里面的地址,將其減去4個字節,然后將新地址寫入 ESP 寄存器。使用減法是因為 Stack從高位向低位發展,4個字節則是因為3的類型是int,占用4個字節。得到新地址以后, 3 就會寫入這個地址開始的四個字節。
1第二行也是一樣,push指令將2寫入main這個幀,位置緊貼著前面寫入的3。這時,ESP 寄存器會再減去 4個字節(累計減去8)。
7.3 call 指令
第三行的call指令用來調用函數。
1上面的代碼表示調用add_a_and_b函數。這時,程序就會去找_add_a_and_b標簽,并為該函數建立一個新的幀。下面就開始執行_add_a_and_b的代碼。
1這一行表示將 EBX寄存器里面的值,寫入_add_a_and_b這個幀。這是因為后面要用到這個寄存器,就先把里面的值取出來,用完后再寫回去。這時,push指令會再將 ESP 寄存器里面的地址減去4個字節(累計減去12)。
7.4 mov 指令
mov指令用于將一個值寫入某個寄存器。
1這一行代碼表示,先將 ESP 寄存器里面的地址加上8個字節,得到一個新的地址,然后按照這個地址在 Stack 取出數據。根據前面的步驟,可以推算出這里取出的是2,再將2寫入 EAX 寄存器。下一行代碼也是干同樣的事情。
1上面的代碼將 ESP 寄存器的值加12個字節,再按照這個地址在 Stack 取出數據,這次取出的是3,將其寫入 EBX 寄存器。
7.5 add 指令
add指令用于將兩個運算子相加,并將結果寫入第一個運算子。
1上面的代碼將 EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到結果5,再將這個結果寫入第一個運算子 EAX 寄存器。
7.6 pop 指令
pop指令用于取出 Stack 最近一個寫入的值(即最低位地址的值),并將這個值寫入運算子指定的位置。
1上面的代碼表示,取出 Stack 最近寫入的值(即 EBX 寄存器的原始值),再將這個值寫回 EBX 寄存器(因為加法已經做完了,EBX 寄存器用不到了)。注意,pop指令還會將 ESP 寄存器里面的地址加4,即回收4個字節。