【計算機組成原理】05-代碼是如何變成機器碼的


  在 20 世紀 60-70 年代,寫程序還要用到打孔卡(Punched Card)。Coder 需要先把程序想好,在紙帶上打孔,然后把打孔卡交給計算機去處理。

FORTRAN 程序打孔卡

  不難看出,這張類似答題卡的紙帶上,通過打孔或不打孔來代表“0”和“1”。

  時至今日,CPU 本身也沒有理解高級編程語言的能力,仍然只能處理機器碼,也就是一連串的“0”和“1”。那么我們用高級編程語言編寫的代碼,是如何轉變成一連串的“0”和“1”的?這一串數字,又是如何在 CPU 中處理的?這篇博客就來看看,機器碼計算機指令(Instruction Code)究竟是怎么回事。

 

1. CPU 在軟硬件接口中做了什么

1.1 硬件工程師眼中的 CPU

  從硬件角度來看,CPU 就是一個超大規模集成電路,通過電路實現了加法、乘法等處理邏輯。

1.2 軟件工程師眼中的 CPU

  從軟件角度來講,CPU 就是一個執行各種計算機指令(Instruction Code)的邏輯機器。

1.3 計算機也有多語種——計算機指令與計算機指令集

  計算機指令,就好比一種 CPU 能聽懂的語言,我們也把它叫做機器語言(Machine Language)

  正如人類有多種語言,不同 CPU 能聽懂的語言也不大一樣。Intel 的 CPU 和 ARM 的 CPU,語言就不同。這是因為它們使用了不同的計算機指令集(Instruction Set)

1.4 存儲程序型計算機

  計算機程序是由成千上萬條指令組成的,不可能將這些指令一直存放在 CPU 中。計算機程序通常是存儲在存儲器中的,這種計算機就叫做存儲程序型計算機(Stored-program Computer)

  既然提到存儲程序型計算機,那么必然有不存儲程序的計算機咯。沒錯,在現代計算機出現之前,出現過插線板計算機(Plugboard Computer)。這種設備通過插拔電線來完成各種計算任務。

IBM 的 Plugboard Computer

 

2. 代碼到機器碼的華麗變身——編譯與匯編

  我們用一段 C 語言代碼來看看,我們平時編寫的代碼是如何變成計算機指令的。

1 // test.c
2 int main()
3 {
4   int a = 1; 
5   int b = 2;
6   a = a + b;
7 }

  想在 Linux 系統上運行這段代碼,要先把它翻譯成匯編語言(Assembly Language,ASM)的程序。從高級編程語言代碼到匯編語言的過程就是編譯(Compile)

  匯編代碼還不是機器碼,要用匯編器(Assembler)處理匯編代碼,才能生成機器碼(Machine Code)。這些機器碼由“0”和“1”組成的機器語言表示。這些機器碼就是一條條計算機指令,這些計算機指令才是 CPU 真正能讀懂的。

高級編程語言到機器語言的過程

  在 Linux 系統中,使用 gcc 和 objdump 這兩條指令,可以將對應的匯編代碼和機器碼都打印出來。

1 $ gcc -g -c test.c
2 $ objdump -d -M intel -S test.o

  左側的數字就是機器碼,右邊的 push、mov、add、pop 等就是匯編代碼。一行 C 語言代碼,有時只對應一條機器碼和匯編代碼,有時則對應兩條機器碼和匯編代碼。匯編代碼和機器碼則是一一對應的。

 1 test.o:     file format elf64-x86-64
 2 Disassembly of section .text:
 3 0000000000000000 <main>:
 4 int main()
 5 {
 6    0:   55                      push   rbp
 7    1:   48 89 e5                mov    rbp,rsp
 8   int a = 1; 
 9    4:   c7 45 fc 01 00 00 00    mov    DWORD PTR [rbp-0x4],0x1
10   int b = 2;
11    b:   c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2
12   a = a + b;
13   12:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
14   15:   01 45 fc                add    DWORD PTR [rbp-0x4],eax
15 }
16   18:   5d                      pop    rbp
17   19:   c3                      ret    

  我們在使用 GCC(GNU Compiler Collection,GNU 編譯器)時,可以直接將高級編程語言代碼編譯成機器碼。那為什么還要讓匯編代碼在中間轉一手呢?這是因為機器碼很難看懂,但匯編則容易理解得多。換句話說,匯編就是給程序員看的機器碼。也正是基於這一點,機器碼和匯編代碼是一一對應的。

 

3. 解析指令和機器碼

3.1 指令的分類

  上一節中反編譯出的匯編代碼和機器指令到底是什么意思呢?Intel CPU 有 2000 多條 CPU 指令,我們無法一一掌握。但通常來說,常見指令可以分成五類。

3.1.1 算數類指令

  加減乘除等運算,在 CPU 層面都會變成算術類指令。

3.1.2 數據傳輸類指令

  變量賦值、讀寫內存等操作,用的都是數據傳輸類指令。

3.1.3 邏輯類指令

  與或非都是此類指令。

3.1.4 條件分支類指令

  if-else 等都是此類指令。

3.1.5 無條件跳轉指令

  調用函數時,就是發起了一個無條件跳轉指令。

指令分類

3.2 匯編器如何將匯編代碼翻譯成機器碼

  前文說過,不同的 CPU 有不同的指令集,自然也就對應着不同的匯編語言和不同的機器碼。我們以最簡單的 MIPS 指令集為例,看看機器碼是如何生成的。

  MIPS 指令集是一組由 MIPS 公司在 80 年代中期設計出的 CPU 指令集,最近已經開源。如果想進一步研究 CPU 和指令集細節,請戳

  MIPS 的指令是一個 32 位整數,其中高 6 位是操作碼(Opcode),用於說明這是一條什么樣的指令。剩下的 26 位有三種格式,分別為 R、I、J。

MIPS 指令示意

3.2.1 R 指令

  用於算術和邏輯操作,其中有讀取、寫入數據用到的寄存器地址。如果是邏輯位移操作,后面還會加上位移操作的位移量。最后的功能碼,是在前面的操作碼不夠時,擴展操作碼以表示具體指令的。

  rs(register source):第一個源寄存器;

  rt 則有二義性。有些資料中說 t 是 target 之意,也有資料稱 t 是 s 的下一個字母,並非具體單詞的縮寫。此處應為第二個源寄存器,所以我偏向於第二種解釋。

  rd(register destination):目標寄存器。

3.2.2 I 指令

  用於數據傳輸、條件分支,以及運算時使用的並非常量而是常數的時候。

  此時沒有位移量和操作碼,也沒有第三個寄存器,於是把這三部分直接成一個地址值或常數。

3.2.3 J 指令

  跳轉指令,高 6 位之外的 26 位都是一個跳轉后的地址。

 

  接下來,我們用一條語句為例來解釋:

1 add $t0,$s2,$s1

  這條語句在 MIPS 指令中,opcode 為 0x0,rs 地址為 0x11,rt 地址為 0x12,rd 是目標的臨時寄存器地址,假設為 0x8。因為我們是加法而非位移操作,所以位移量為 0。將以上數字拼起來,就是下圖所示:

MIPS 指令示例(首行為十進制)

  接下來,我們將這條指令的機器碼打在打孔卡上(打孔表示 1,不打孔表示 0):

MIPS 指令的打孔卡

 

4. 拓展資料

  如果想了解 Intel CPU 的指令集,可以參考《計算機組成與設計:軟 / 硬件接口》第 5 版的 2.17 小節。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM