早期計(jì)算機(jī)程序是通過「打孔卡」來實(shí)現(xiàn)的,人們在特定位置上打洞或者不打洞,來表示“0”或者“1”。

那為什么我們每天用高級語言的程序,最終是怎么變成一串串“0”和“1”的?這一串串“0”和“1”是怎么在CPU中處理的?今天來說說「機(jī)器碼」和「計(jì)算機(jī)指令」。
在軟硬件接口中,CPU幫我們做了什么事?
CPU,又名「中央處理器」,是計(jì)算機(jī)的電腦。
從硬件的角度,是一個(gè)超大規(guī)模集成電路,通過電路實(shí)現(xiàn)加法和乘法等各種處理邏輯。
從軟件的角度,CPU是一個(gè)執(zhí)行各種計(jì)算機(jī)指令的邏輯機(jī)器。這里的「計(jì)算機(jī)指令」好比一門CPU能聽得懂的語言,我們也稱之為“機(jī)器語言”。
不同的CPU能聽懂的語言也不同。比如,「個(gè)人電腦的Intel的CPU」和「蘋果手機(jī)ARM的CPU」就是不同的CPU,這兩者聽懂的語言也不同。類似這樣兩種CPU各自支持的語言,就是兩組不同的計(jì)算機(jī)指令集。
一個(gè)計(jì)算機(jī)程序,不可能只有一條指令,而是有成千上萬條指令。但是CPU不能一直放著這些指令,所以計(jì)算機(jī)程序一般是存儲(chǔ)在存儲(chǔ)器里的。這種程序指令存儲(chǔ)在存儲(chǔ)器里的計(jì)算機(jī),我們就叫做存儲(chǔ)程序計(jì)算機(jī)。
從編譯到匯編,代碼怎么變成機(jī)器碼?
// test.c
int main()
{
int a = 1;
int b = 2;
a = a + b;
}
這段代碼,要在Linux操作系統(tǒng)上跑起來,我們首先需要把整個(gè)程序翻譯成匯編語言的程序。這個(gè)過程,我們稱之為「編譯成匯編代碼」。
針對匯編代碼,我們可以再用「匯編器」翻譯成「機(jī)器碼」。這些機(jī)器碼是由“0”和“1”組成的機(jī)器語言表示。這一條條機(jī)器碼,也就是「計(jì)算機(jī)指令」。這一串串16進(jìn)制數(shù)字,就是CPU能夠識(shí)別的計(jì)算機(jī)指令。
在Linux操作系統(tǒng)上,運(yùn)行g(shù)cc和objdump這兩條指令。
$ gcc -g -c test.c
$ objdump -d -M intel -S test.o
test.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
int main()
{
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
int a = 1;
4: c7 45 fc 01 00 00 00 mov DWORD PTR [rbp-0x4],0x1
int b = 2;
b: c7 45 f8 02 00 00 00 mov DWORD PTR [rbp-0x8],0x2
a = a + b;
12: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8]
15: 01 45 fc add DWORD PTR [rbp-0x4],eax
}
18: 5d pop rbp
19: c3 ret
我們可以看到,左側(cè)一對數(shù)字,那就是「機(jī)器碼」。右側(cè)的push,mov,add這些是「匯編代碼」。你可能會(huì)問,我們實(shí)際在用GCC(GUC編譯器套餐)編譯器的時(shí)候,可以直接把「代碼」編譯成「機(jī)器碼」呀,為啥還要先編譯成「匯編代碼」?原因是你看這些機(jī)器碼看不懂,你看「匯編代碼」還能猜出一些含義。
因?yàn)椤竻R編代碼」就是「給程序員看的機(jī)器碼」。也正是這樣,「匯編代碼」和「機(jī)器碼」是一一對應(yīng)的。

從高級語言到匯編代碼,再到機(jī)器碼,就是日常開發(fā)程序,最終變成CPU可以執(zhí)行的計(jì)算機(jī)指令的過程。
解析指令和機(jī)器碼
接著我們看看一行行「匯編代碼」和「機(jī)器碼」都是啥意思。一般來說,常見的指令分為五大類。
第一類是算術(shù)類指令。加減乘除,在CPU層面,都會(huì)變成一個(gè)個(gè)算術(shù)類指令。
第二類是數(shù)據(jù)傳輸類指令。給「變量賦值」,在內(nèi)存里面「讀寫數(shù)據(jù)」,用的是數(shù)據(jù)傳輸類指令。
第三類是邏輯類指令。邏輯上的與或非。
第四類是條件分支類指令。日常寫的「if」「else」。
第五類是無條件跳轉(zhuǎn)類指令。寫一些大一點(diǎn)的程序,需要調(diào)用函數(shù),在「調(diào)用函數(shù)」的時(shí)候就是無條件跳轉(zhuǎn)類指令。

接著我們看看,「匯編代碼」如何變成「機(jī)器碼」的。
上述說過,不同的CPU對應(yīng)不同的「計(jì)算機(jī)指令集」,也對應(yīng)著不同的匯編語言和不同的機(jī)器碼。選用一個(gè)簡單的MIPS指令集,來看看「機(jī)器碼」是如何生成的。

MIPS是一個(gè)32位的整數(shù),高6位叫做「操作碼」(Opcode),也就是表示這條指令具體「是一條什么樣的指令」。剩下的26位有三種格式,分別是R,I和J。
R指令一般用作「算術(shù)」和「邏輯」類指令,里面讀取和寫入數(shù)據(jù)的寄存器地址。如果是邏輯位移操作,后面還有位移操作的位移量,而最后的「功能碼」,則是在前面的「操作碼」不夠的時(shí)候,擴(kuò)展「操作碼」表示對應(yīng)的具體指令的。
I指令則通常是用在數(shù)據(jù)傳輸、條件分支,以及在運(yùn)算的時(shí)候使用的并非變量還是常數(shù)的時(shí)候。這個(gè)時(shí)候,沒有了位移量和操作碼,也沒有了第三個(gè)寄存器,而是把這三部分直接合并成了一個(gè)地址值或者一個(gè)常數(shù)。
J指令就是一個(gè)跳轉(zhuǎn)指令,高 6 位之外的 26 位都是一個(gè)跳轉(zhuǎn)后的地址。
add $t0,$s2,$s1
我以一個(gè)簡單的加法算術(shù)指令 add t0,s1, $s2, 為例,給你解釋。為了方便,我們下面都用十進(jìn)制來表示對應(yīng)的代碼。
對應(yīng)的 MIPS 指令里 opcode 是 0,rs 代表第一個(gè)寄存器 s1 的地址是 17,rt 代表第二個(gè)寄存器 s2 的地址是 18,rd 代表目標(biāo)的臨時(shí)寄存器 t0 的地址,是 8。因?yàn)椴皇俏灰撇僮?,所以位移量?0。把這些數(shù)字拼在一起,就變成了一個(gè) MIPS 的加法指令。為了讀起來方便,我們一般把對應(yīng)的二進(jìn)制數(shù),用 16 進(jìn)制表示出來。在這里,也就是 0X02324020。這個(gè)數(shù)字也就是這條指令對應(yīng)的機(jī)器碼。

回到開頭我們說的打孔帶。如果我們用打孔代表 1,沒有打孔代表 0,用 4 行 8 列代表一條指令來打一個(gè)穿孔紙帶,那么這條命令大概就長這樣:

如果你想一起學(xué)習(xí)這門課,可以掃下面的二維碼購買:
